Appearance
Resources
Resources combines Calendar with a finite resource inventory. It is available in the Web implementation (Vanilla, React, Vue) and native SwiftUI implementation. Resources do not add resource lanes to Calendar or change the resource Timeline.
Web
ts
import { createResources } from '@xsigns/chronosketch/resources'
import '@xsigns/chronosketch/resources/styles.css'
const resources = createResources({
date: '2026-09-14',
locale: 'de-DE',
title: 'Verfügbare Ressourcen',
caption: 'Zum Einplanen in den Kalender ziehen.',
defaultDurationMinutes: 60,
showRemainingCount: true,
data: {
items: [
{ id: 'client', title: 'Client demo', quantity: 1 },
{ id: 'team', title: 'Team meeting', quantity: 2, description: 'Project team' },
],
placements: [],
},
onChangeRequested: ({ plan }) => resources.setData(plan),
onEventClick: event => { /* open your event editor */ },
})
resources.mount(document.querySelector('#resources')!)
// resources.destroy() when unmountingReact exports Resources from @xsigns/chronosketch/react with ResourcesOptions as individual props. Vue exports Resources from @xsigns/chronosketch/vue with an options prop. Both require the Resources stylesheet. Supply confirmed updates by replacing data; the wrappers dispose their owned component on unmount.
The Web Month calendar keeps week rows at the same height after multiple placements. It displays one event bar and a localized N weitere / N more button for additional placements on a date. The button opens their event actions in a dialog.
ResourcePlan.items contains unique, nonempty IDs, titles, nonnegative integer quantity (the total allowance) and optional descriptions. placements contains { resourceId, event }, where event is a standard CalendarEvent. Supply the whole plan, including placements outside the visible range. The library validates unique event IDs, source links, intervals and quantity before accepting a snapshot.
Remaining quantity is total minus placements. One item shows one card, multiple items show a small symbolic stack, zero hides the card. Layers do not encode an exact count. Accessible labels include the remaining count.
Web showRemainingCount (default false) adds the confirmed remaining count in an inset circular/pill badge at the lower right. Long titles keep the full card width; the badge stays inside the card. setShowRemainingCount(boolean) toggles it without resetting Calendar navigation. React/Vue synchronize this option. The Web demo enables it for review; native count badges are not implemented yet.
Month date cells and Week/Day all-day areas create a one-day all-day interval. Week/Day time grids use the dropped time snapped to 15 minutes, with configurable defaultDurationMinutes (1–1440, default 60). Ends remain exclusive and timed values remain floating local times. Select a resource card to choose a date and, optionally, a time without dragging; this also works in Agenda and compact views.
Web title configures the Resources sidebar heading; omitted or empty uses the localized default. caption configures its hint; omitted uses the localized default and caption: '' hides it. Use setTitle(value) and setCaption(value) for runtime updates (undefined restores defaults). React/Vue synchronize both options without resetting Calendar navigation. Text is rendered as plain text. The caption retains its existing compact-layout hiding behavior.
Confirmation, editing and deletion
onChangeRequested receives { previous, plan }. This is a proposal, not a save notification: confirm with setData(plan) or by replacing adapter data. Rejection leaves the confirmed inventory/events unchanged. For asynchronous persistence, compare the previous snapshot/revision against the authoritative plan and reject stale proposals. The component does not serialize remote saves or reserve stock.
Exports editResourcePlacement(plan, event) and removeResourcePlacement(plan, eventId) create validated immutable replacements. Editing preserves the source link and usage; deletion returns one unit. The demo shows an event editor that can convert all-day events to timed events. Forms and persistence are consumer-owned; validate edited events against any application availability policy before confirming. There is no resource quantity editor in V1.
The instance exposes getData, setData, setChangeHandler, mount, destroy and the composed calendar instance for Calendar configuration/navigation. Use setData to update events and inventory together; do not independently replace Calendar events. Calendar presentation options are accepted at construction. Web adapters synchronize data/callbacks, constraints, overlap policy, validator, constraint display, scroll policies, interaction flags, event actions, sizing and appearance. Locale/default-duration changes recreate the component. Other Calendar options remain construction-time inputs.
Native SwiftUI
swift
let item = try ResourceItem(id: "client", title: "Client demo", quantity: 1)
let model = try ResourcesModel(
plan: ResourcePlan(items: [item]), date: CivilDate("2026-09-14"),
language: .german, defaultDurationMinutes: 60
)
// Retain model in consumer state and place inside a ScrollView as needed:
ResourcesView(model: model, today: today, showRemainingCount: true, onChangeRequested: { previous, next in
if model.plan == previous { model.setPlan(next) }
}, onEventSelected: { event in /* consumer editor */ })Import ChronoSketchCore and ChronoSketchUI; iOS 26+. Swift uses ResourceItem.detail for the optional description. ResourcePlan.placing, .editing, .removing return validated snapshots; ResourcesModel.setPlan synchronizes Calendar without losing navigation. model.calendar owns Calendar presentation/availability settings.
Native drag transfers are scoped to one Resources instance. Month, Week/Day all-day areas and time-grid columns accept them. Native Month retains its date grid with selected-day agenda. Tapping a card opens native date/time pickers. Wide layouts show the sidebar to the left. Compact layouts place three square, widget-like resource cards per row above Calendar (80-point baseline, scaling with Dynamic Type). Compact cards show titles; descriptions remain available in the accessibility hint. Additional resources continue in the same horizontally scrollable row. Wide sidebars keep the heading visible and scroll their inventory vertically within the height of Calendar. Immediate swipes scroll; the native hold-to-drag gesture lifts a card for placement. Web and native cards use opaque, softly rounded paper surfaces with fanned backing cards. Accent-tinted fills and soft shadows separate the layers without outlines or a leading stripe. Light mode keeps a near-white front; dark mode uses distinct Accent shades for the front and rear cards. All layers remain opaque. Cards support Dynamic Type and accessible availability labels. Device VoiceOver and touch acceptance are separate from package tests and simulator inspection.
Native count and stack feedback
ResourcesView(showRemainingCount: true, ...) displays the remaining quantity inside each card at the lower right. It defaults to false; the native demo enables it. Titles retain the full card width, with a separate row for the count. The existing accessibility value announces availability without repeating the badge.
Each actual pickup and confirmed return exchanges the rear cards' tilt. The arrangement remains across drops, cancellations and inventory updates. Reduce Motion disables the short transition. Repeated placement uses native drag-session callbacks, so keeping a transfer provider alive cannot block the next pickup.
Native drag previews preserve transparent card corners and the card stack's own shadows during pickup and cancellation, without a rectangular system backdrop.
iPhone haptics
ResourcesView(model: model, today: today, hapticsEnabled: false, ...) disables Resources and composed Calendar interaction feedback; the default is true. A soft pulse marks native drag activation, light selection ticks mark a new date or snapped time target, and a success pulse follows acceptance through model.setPlan. Holding the same target does not repeat feedback. Ticks use the shared 80 ms limit without queued pulses. Cancelling or leaving the Calendar produces no success feedback.
The native source/target share an instance-scoped drag session and an in-process text provider tagged with the Resources instance identifier. Drop updates track positions within connected all-day bars as well as individual day/time columns. Unsupported hardware stays silent; simulator checks cannot verify the physical feel of the pulses.
Styling
Web hooks: .xc-resources, .xc-resources-sidebar, .xc-resources-list, .xc-resource-stack[data-multiple], .xc-resource-card, .xc-resource-name, .xc-resource-description, .xc-resources-dialog. Decorative backing layers are pseudo-elements and never independent drag targets. The stock theme follows system light/dark appearance; existing Calendar styles remain overridable.
Inventory scrolling
On wide Web layouts the sidebar is bounded by Calendar height and only .xc-resources-list scrolls vertically. At component widths up to 800px it becomes one horizontally scrollable row with three visible cards. The heading stays outside the scroll area. Touch swipes scroll inventory; holding still for 350 ms prepares a Web card drag. Mouse dragging and keyboard placement remain available. Card surfaces derive from --xc-accent when set on Resources, with opaque light and dark fallback colors. Forced-colors mode restores card borders.
Returning scheduled resources
Drag a scheduled placement onto its Resources sidebar to propose removing the placement and returning one unit. Web supports visible Month bars and Week/Day event cards; native iOS exposes this on Week/Day cards only, excluding Month and Agenda lists. The whole sidebar accepts returns, including an exhausted inventory. The normal onChangeRequested / setData or setPlan confirmation contract applies; a cancelled gesture or rejected proposal does not replenish inventory. Deletion remains available as a non-drag alternative.
Time-precise planning in a dialog (beta.18)
A spatial unit is consumer context, not automatically an inventory item. Supply its existing busy intervals through Calendar constraints; these consume no stock and need no artificial placements. Use Calendar alone if the application has no genuinely finite inventory. Resources counts placements across the entire plan, not simultaneous capacity or duration.
The consumer chooses the initial view and date, resolves its timezone, and supplies dayScrollPosition/weekScrollPosition: { time: '10:30' }. These are an entry point, never an allowed planning boundary. Account for exclusive midnight ends when deciding whether an interval occupies one or several local dates. Load every relevant busy interval when users navigate or extend outside the initial range; client validation cannot discover unavailable data.
ts
const resources = createResources({
data: completePlan,
date: localStartDate,
view: initialView, // consumer decision
dayScrollPosition: { time: localStartTime },
weekScrollPosition: { time: localStartTime },
constraints: busyIntervals,
constraintDisplay: 'details', // or 'hatched', 'label', or a resolver
preventEventOverlap: true,
width: '100%', height: 600,
onChangeRequested: change => proposeToApplication(change),
})
resources.calendar.setConstraints(nextBusyIntervals)
resources.calendar.scrollToTime('10:30')Use eventId only for a constraint owned by that exact editable event. Pass previous when validating form edits so the event does not collide with itself. Foreign busy intervals must not receive the draft's ID. setData imports an authoritative plan; it is not a booking validation command. Asynchronous backend saves must reject stale proposals and keep the consumer's draft/error state.
Resources dimensions now bound the entire composition, including inventory. Use resources.setSize(size) for replacements. The inventory moves above Calendar based on the component's width, even in a narrow dialog on a wide desktop. Week retains readable columns with horizontal scrolling; Day uses the available width. Very short heights may require outer scrolling to reach all controls. Existing .xc-resources remains the root; .xc-resources-layout owns its grid.
Dragging a timed card moves both endpoints. Dragging its bottom end grip changes only its end. Week also provides a right-edge grip to extend the end across days and periods; there is still one placement and one consumed unit. The complete interval is checked, including intermediate busy times. Constraint updates preserve the active timed draft, navigation and manual scroll while updating rejection feedback. Event/plan replacements cancel stale edits. The alternative placement dialog preserves invalid input. Touch and keyboard event editing use the consumer's start/end form via onEventClick; see the Calendar contract.
Native time planning (beta.18)
SwiftUI now supplies a right-edge end-date grip in Week, multi-day previews and red/valid feedback that can recover during the same drag. Availability changes retain timed drafts; event replacement cancels them. Resources drop highlighting checks current stock and Calendar availability before release; placement forms retain invalid input and reflect live policy changes. Confirmation remains owned by the consumer. The native demo edit form also checks current availability.
ResourcesView forwards .constraintDisplay, .weekScrollPosition and .dayScrollPosition; explicit jumps use model.calendar.scrollToTime. See the native Calendar contract. Native automatic edge scrolling/week paging during resizing is still open; use the start/end form beyond the visible period. No release is implied.
Custom Web inventory (beta.19)
renderInventory(host, { available, select }) optionally renders the contents of the inventory area. It returns a cleanup function, called before replacement and on destruction. available contains only items with remaining quantity. Keep the full plan in data, even when filtering displayed cards.
Use buttons with data-resource-id equal to the item ID, the xc-resource-card class (including touch-action rules), and a click handler calling select(item). The existing pointer placement, availability validation and keyboard placement dialog remain authoritative. A nested horizontal scroll track uses xc-resources-list so the existing touch swipe/hold arbitration can find it. The consumer owns accessible labels, tab/filter state, images and their disposal. Do not create interactive children inside each card button.
setInventoryRenderer(renderer) replaces the renderer without navigating the Calendar; pass undefined to restore the default inventory. Vue and React watch this option. This DOM rendering hook is Web-only; native custom content remains an explicit parity follow-up. No domain or quantity semantics change.
Timeline layout (beta.19)
layout: 'timeline' selects a horizontal planning surface in the same Resources component. Omitted layout (or 'calendar') preserves the existing Calendar API. Calendar-only options such as view, events, dayScrollPosition and Calendar validators are excluded from TimelineResourcesOptions. Timeline instances expose timeline; calendar instances retain their non-optional calendar. Code holding both variants must narrow the instance ('calendar' in instance).
ts
const resources = createResources({
layout: 'timeline',
range: { start: '2026-10-01T00:00', days: 7 },
date: '2026-10-01T08:00', scale: 'hour', zoom: 1,
groupBy: [{ key: 'site', label: 'Site' }, { key: 'area', label: 'Area' }],
data: {
items: [{ id: 'blue', title: 'Team Blue', usage: 'reusable' }],
rows: [{ id: 'apartment-1', label: 'Apartment 1',
groups: { site: 'Casa Luna', area: 'Ground floor' } }],
placements: [],
},
constraints: [{ id: 'reservation', rowId: 'apartment-1',
start: '2026-10-01T12:00', end: '2026-10-01T15:00', label: 'Reserved' }],
onChangeRequested: change => resources.confirm(change),
})Rows reuse TimelineResource (id, label, groups, optional avatarUrl). Every timeline placement is { resourceId, rowId, event }, with a timed CalendarEvent. Its event.id remains its stable identity. Groups cannot receive placements. All row, item, placement and constraint references are validated. Data is the complete plan; backend paging is consumer-owned. Separate placements never move together.
usage: 'reusable' replaces finite quantity with concurrent capacity (positive integer, default 1). Such items remain selectable, displaying ∞ instead of a total remaining count. Capacity counts simultaneous placements of that item across all rows; adjacent exclusive ends are allowed. Finite items still use total quantity minus placements across the complete plan. Usage is independent of layout; reusable items also work in Calendar. Snapshots exceeding reusable capacity are rejected.
Timeline constraints target exactly one rowId or resourceId. Row blocks render as background bands, consume no inventory and cannot be selected as placements. Item blocks apply regardless of row. preventRowOverlap defaults to true. validateInteraction receives { action, item, row, candidate, previous? } and returns { allowed, reason? }; it adds to built-in rules. Removal also consults that callback. Existing placements exclude themselves during edits. Inputs and ends retain floating minute and half-open interval semantics, with no timezone conversion. Unknown person/team memberships and remote conflicts remain external.
Select an item for a keyboard/touch form; select a bar to change its row, start, end or remove it. The same validation applies to external drops, moves, row changes, start/end grips and returns to inventory. snapMinutes defaults to 15 (integer 1–1440) at every scale and zoom. defaultDurationMinutes defaults to 60 (1–1440). Forms accept exact minutes without snapping. Escape, pointer cancellation, data replacement, grouping/zoom changes and teardown discard Web pointer drafts. Touch swipes scroll; holding still for 350 ms prepares Web dragging. For changes beyond the visible canvas, use scrolling before dragging or the complete form; automatic edge scrolling is not implemented.
onChangeRequested supplies { previous, plan, revision, action, placement, previousPlacement? }. Actions distinguish create, move, reassign, resize and remove. No optimistic inventory or occupancy changes occur while confirmation is pending. Several proposals may be outstanding for the same revision; accepting one makes its peers stale. confirm(change) returns false for stale requests or newly invalid candidates. setData(plan) imports authoritative data and invalidates older requests. React/Vue consumers confirm by replacing data and must compare the original snapshot against their current authoritative revision before applying async saves. Rejected proposals need no rollback. Never persist before backend validation.
Runtime: setData, setPolicy, inventory title/caption/renderer/count setters and setSize preserve navigation and groups. setPolicy replaces all constraints and validation settings. The composed timeline provides date, scale, zoom, groupBy, groupIcons, showZoom, showPeriodHeader and appearance controls. Changing groupBy resets collapse state; equal definitions do not. Layout, range, locale, iconSet, controlsStyle, snap and default duration are construction settings (React/Vue recreate on changes). Replace Timeline adapter options/data immutably. Data updates cancel open editors. Do not independently mutate the composed timeline's rows/data.
The demo uses ?component=resources&layout=timeline with Vanilla, React or Vue. The existing Resources stylesheet includes the shared Timeline styles.
Density and item images
Timeline density: 'comfortable' | 'compact' defaults to comfortable. Compact also reduces time-column widths to 80% (for example, 100px to 80px per hour column). Zoom still selects the same time interval; the grid, entries, constraints and pointer snapping share the narrower geometry. Switching density preserves the visible time center, subject to the horizon boundaries. Comfortable rows are 96px high with cards up to 80px high; compact rows are 44px with 28px bars. Group headers retain their existing size. Item imageUrl is optional decorative media, shown in inventory and placements. Comfortable cards show image, placement title and item description; compact bars show a thumbnail and title. Card width always reflects the actual interval and zoom, so cards are approximately square for suitable durations rather than distorting the time axis.
setDensity(value) and React/Vue option updates change density without remounting or resetting navigation/group state. Active pointer drafts are cancelled. Resize grips disappear on very narrow bars to leave the body draggable; the form remains available for resizing. Drag the card body to move earlier or later and use its edges to resize. Browser image/text dragging is suppressed inside this surface.
Native timeline layout
Swift selects the mode through ResourcesView(model: ResourcesTimelineModel, ...); the existing ResourcesModel initializer continues to select Calendar. Native ResourceTimelinePlan contains items, TimelineResource rows and ResourceTimelinePlacement(resourceID:rowID:event:); its event is a TimelineEntry with typed LocalDateTime endpoints. ResourceItem(id:title:capacity:) creates a reusable item; the quantity initializer remains finite.
ResourcesTimelineModel accepts start/days, grouping, scale/zoom, constraints, defaultDurationMinutes and snapMinutes. It exposes the composed timeline, setPlan, setConstraints, check, propose and revision-guarded confirm. validateInteraction adds consumer rules. The view supplies inventory selection, row drops, bar moves/returns, endpoint grips and a start/end/row form. The native demo's Timeline toggle selects the new surface. The form currently uses explicit floating date-time text fields; validation retains invalid input. Device VoiceOver, physical touch and automatic edge scrolling are not claimed by simulator builds.
Native presentation stays separate from the plan: the timeline ResourcesView initializer accepts showZoom, rowTitle, groupIcon and rowMedia; the model's showPeriodHeader constructor option and composed TimelineModel control the axis. The existing SwiftUI appearance modifiers remain applicable.
The native timeline initializer also accepts density: .comfortable (default) or .compact, with the same 96/44 point resource rows and 80% compact time-column width. Set ResourceItem.imageURL through either initializer for decorative media; detail supplies the secondary card text. Density updates keep the model and confirmed placements intact.
During Web Timeline movement and resizing, the entry itself is previewed at the pointer while moving, or at the snapped endpoint while resizing with its image, text and duration width. The confirmed source is hidden during the gesture and restored on cancellation; no floating text tooltip replaces the entry. Invalid targets keep the entry preview with rejection styling.
Dragging from Timeline inventory preserves the original card's appearance and dimensions, including its image, description and remaining count, just as in the Calendar layout. Density affects the target grid, not the card being dragged. The confirmed placement is then rendered as a timeline entry.
Timeline drop targets follow the cursor, independently of the floating card’s visible size. The full planned time interval must still pass overlap checks. During dragging, the cursor remains a closed hand across the page; resizing keeps the horizontal resize cursor. Normal cursors return when the gesture ends.
When dragging an existing Timeline entry, its preview stays at the grab point. Only releasing over a blocked interval snaps the entry around it: the cursor before the block's midpoint selects the left side; at or after the midpoint selects the right side. This uses the unrounded cursor position, independently of where the entry was grabbed. Adjacent blocks count as one interval. Duration and all availability checks remain intact; no valid position on the chosen side means rejection. This release rule applies to movement/reassignment on Web and iOS, not new placements, resizing or form input.
On Web, compact density also fits the sticky left column to the expanded row and group labels, including indentation, icons and padding. Its width stays stable while scrolling and is capped to leave room for the time grid. Comfortable density restores the standard column width.
On iOS, moving an existing Timeline placement uses the native drag preview of the entry itself, retaining its visible image/text. Tapping continues to open the placement form; edge grips retain their separate resize interaction.
Content and header density
density and headerDensity independently accept comfortable (default) or compact. Configure options/props or call setDensity / setHeaderDensity. See the shared density contract for geometry, live updates and demo controls. Available since beta.20.
Lazy Timeline plans and consumer editing (unreleased)
Web TimelineResourcesOptions.loadData(query, signal) optionally returns a Promise<ResourceTimelinePlan>. query contains the buffered local start, exclusive end, and visible row IDs in resourceIds. The consumer owns time-zone conversion, server filtering, authorization and persistence. Return the complete configured rows and inventory plus placements overlapping the requested window; do not return Timeline entries/assignments yourself. Rows must match the configured rows; replace them explicitly through data/setData.
ts
const planner = createResources({
layout: 'timeline', range: { start: '2026-11-01T00:00', days: 90 },
data: initialPlan,
loadData: async (query, signal) => readPlanWindow(query, signal),
onPlacementClick: placement => openEditor(placement),
onChangeRequested: change => saveProposal(change),
})Scrolling, scale and zoom changes reuse Timeline's coalesced loading, abort, retry and stale-response protection. Failed/invalid responses preserve confirmed placements. Inventory counts and capacity checks cover retained known placements only; server validation must remain authoritative for unloaded windows. Policy updates redraw constraints without completing or cancelling an outstanding load.
onPlacementClick replaces the built-in edit form, including keyboard activation. Omitting it retains the built-in form. setPlacementClickHandler changes/removes the callback. React/Vue synchronize callbacks and replacement providers without remounting; switching between static and lazy modes recreates the surface. Vanilla retains its construction mode and supports setLoadData to replace a provider in lazy mode. timeline.reload() explicitly retries the current window.
Drag previews show the target row and exact candidate interval in addition to the floating source card. Invalid targets use the rejection color and a dashed border; leaving or ending the gesture clears the marker. See the CSS contract for overrides.
Web (Vanilla, React, Vue), Swift and Kotlin now provide lazy Timeline plan loading in the current source checkout. Swift uses ResourcesTimelineModel.setDataProvider, Kotlin uses the same method with a ResourceTimelineDataProvider. Native consumers retain native selection/forms; the Web onPlacementClick option remains a Web-specific API. These source changes do not update existing published packages or installed demos.
Timeline layout accepts the same loading policy as Gantt. Cached placements keep their item references and click behavior. Unchanged available inventory preserves the sidebar DOM and custom inventory renderer, including its local tab/search state. Exhausted finite items do not cause inventory remounts. Subscription data updates belong to the most recently accepted provider window; new request results must resolve through loadData first.
Web Timeline dragging also highlights the candidate row and time interval, while the original card preview stays at the pointer. Rejected candidates have a dashed outline. Leaving the grid or cancelling clears the target highlight. Theme hooks are documented under Resources Timeline drop feedback in the styling guide.
Current demo behavior
The regular Timeline examples on Web, iOS and Android use delayed plan providers and authoritative in-memory confirmation stores. See demo walkthrough. Native providers must retain configured items and rows and return placements only for the requested resource/time rectangle. Cached placements keep their item references; unknown intervals are not empty availability. Confirmed plan updates invalidate reads, and future provider responses must include those changes.