ResourceTimelinebeta
One lane per room, chair or vehicle against a day axis: each item is a bar over the days it occupies, stacked where two overlap. Calendar and Planner map dates onto a grid; this one adds a lane per resource.
Playground
<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
import { addDays, isoToDate } from '@urbicon-ui/blocks/date';
const value = new Date(2026, 5, 15);
const resources = [
{ id: 'cala-01', label: 'Cala 01', description: 'Garden Room', groupId: 'cala' },
{ id: 'cala-04', label: 'Cala 04', description: 'Room', groupId: 'cala' },
{ id: 'cala-11', label: 'Cala 11', description: 'Suite', groupId: 'cala' },
{ id: 'firn-02', label: 'Firn 02', description: 'Room', groupId: 'firn' },
{ id: 'firn-08', label: 'Firn 08', description: 'Suite', groupId: 'firn' }
];
const groups = [{ id: 'cala', label: 'Cala · Menorca' }, { id: 'firn', label: 'Firn · Engadin' }];
const categories = [
{ id: 'confirmed', label: 'Confirmed', color: 'oklch(0.62 0.13 250)' },
{ id: 'option', label: 'Option', color: 'oklch(0.83 0.13 90)' }
];
const items = [
{ id: 'b1', roomId: 'cala-01', guest: 'Lindqvist', checkIn: '2026-06-12', checkOut: '2026-06-18', state: 'confirmed' },
{ id: 'b2', roomId: 'cala-01', guest: 'Okafor', checkIn: '2026-06-20', checkOut: '2026-06-26', state: 'confirmed' },
{ id: 'b3', roomId: 'cala-04', guest: 'Bianchi', checkIn: '2026-06-16', checkOut: '2026-06-19', state: 'option' },
{ id: 'b4', roomId: 'cala-11', guest: 'Sørensen', checkIn: '2026-06-18', checkOut: '2026-06-23', state: 'confirmed' },
{ id: 'b5', roomId: 'firn-02', guest: 'Weber', checkIn: '2026-06-15', checkOut: '2026-06-18', state: 'confirmed' },
{ id: 'b6', roomId: 'firn-08', guest: 'Ferreira', checkIn: '2026-06-21', checkOut: '2026-06-28', state: 'option' }
];
const getResourceId = (booking) => booking.roomId;
const getCategoryId = (booking) => booking.state;
const getLabel = (booking) => booking.guest;
const getRange = (booking) => ({ start: booking.checkIn, end: addDays(isoToDate(booking.checkOut), -1) });
</script>
<ResourceTimeline
{value}
{resources}
{groups}
{items}
{categories}
{getResourceId}
{getCategoryId}
{getLabel}
{getRange}
/>01 Examples
Hotel occupancy
getRange is inclusive, so a booking stored as check-in and check-out becomes a range by subtracting one day from check-out. Two stays that meet on the same morning (one leaving as the next arrives) then sit side by side instead of stacking into two rows. groups adds the house headings, categories colours the bars and their legend, and onItemClick fires for a bar from a click or the keyboard.Pick a bar to select a stay.
<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
import { addDays, isoToDate } from '@urbicon-ui/blocks/date';
interface Booking {
roomId: string;
guest: string;
/** The first night of the stay. */
checkIn: string;
/** The morning the guest leaves — not a night. */
checkOut: string;
state: 'confirmed' | 'option' | 'blocked';
}
const houses = [
{ id: 'cala', label: 'Cala · Menorca' },
{ id: 'firn', label: 'Firn · Engadin' }
];
const rooms = [
{ id: 'cala-01', label: 'Cala 01', description: 'Garden Room', groupId: 'cala' },
{ id: 'cala-04', label: 'Cala 04', description: 'Room', groupId: 'cala' },
{ id: 'cala-07', label: 'Cala 07', description: 'Corner Room', groupId: 'cala' },
{ id: 'firn-02', label: 'Firn 02', description: 'Room', groupId: 'firn' },
{ id: 'firn-05', label: 'Firn 05', description: 'Suite', groupId: 'firn' }
];
const states = [
{ id: 'confirmed', label: 'Confirmed', color: 'oklch(0.62 0.13 250)' },
{ id: 'option', label: 'Option', color: 'oklch(0.83 0.13 90)' },
{ id: 'blocked', label: 'Blocked', color: 'oklch(0.62 0.02 260)' }
];
const bookings: Booking[] = [
// Weber leaves on the 18th and Haldar arrives the same morning: the two bars
// meet without touching. Drop the −1 below and they overlap into two rows.
{
roomId: 'firn-02',
guest: 'Weber',
checkIn: '2026-06-15',
checkOut: '2026-06-18',
state: 'confirmed'
},
{
roomId: 'firn-02',
guest: 'Haldar',
checkIn: '2026-06-18',
checkOut: '2026-06-24',
state: 'confirmed'
},
{
roomId: 'cala-01',
guest: 'Lindqvist',
checkIn: '2026-06-12',
checkOut: '2026-06-18',
state: 'confirmed'
},
{
roomId: 'cala-04',
guest: 'Bianchi',
checkIn: '2026-06-15',
checkOut: '2026-06-19',
state: 'option'
},
{
roomId: 'cala-07',
guest: 'Amaral',
checkIn: '2026-06-16',
checkOut: '2026-07-02',
state: 'confirmed'
},
{
roomId: 'firn-05',
guest: 'Repainting',
checkIn: '2026-06-17',
checkOut: '2026-06-21',
state: 'blocked'
}
];
let picked = $state('');
</script>
<div class="w-full">
<ResourceTimeline
view="days"
days={14}
value={new Date(2026, 5, 15)}
locale="en-US"
resources={rooms}
groups={houses}
items={bookings}
categories={states}
getResourceId={(booking) => booking.roomId}
getCategoryId={(booking) => booking.state}
getLabel={(booking) => booking.guest}
onItemClick={(booking, room) => (picked = `${booking.guest} · ${room.label}`)}
getRange={(booking) => ({
// Both ends are nights: the stay's last night is check-out minus one day.
start: booking.checkIn,
end: addDays(isoToDate(booking.checkOut), -1)
})}
/>
<p class="text-text-secondary mt-3 text-sm">
{picked ? `Selected: ${picked}` : 'Pick a bar to select a stay.'}
</p>
</div>
A free night is where you add one
onCellClick fires only for a cell no bar covers, the hook for an “add booking” affordance. The cell snippet fills those free cells; activating a day inside an existing stay fires onItemClick instead, from click or keyboard.Click a free night, or press Enter on one.
<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
import { toIso } from '@urbicon-ui/blocks/date';
interface Stay {
id: string;
roomId: string;
guest: string;
from: string;
/** Inclusive: the last night, not the check-out morning. */
to: string;
}
const rooms = [
{ id: 'firn-02', label: 'Firn 02', description: 'Room' },
{ id: 'firn-05', label: 'Firn 05', description: 'Corner Room' },
{ id: 'firn-08', label: 'Firn 08', description: 'Suite' }
];
const stays: Stay[] = [
{ id: 's1', roomId: 'firn-02', guest: 'Weber', from: '2026-06-15', to: '2026-06-17' },
{ id: 's2', roomId: 'firn-05', guest: 'Haldar', from: '2026-06-18', to: '2026-06-21' },
{ id: 's3', roomId: 'firn-08', guest: 'Ferreira', from: '2026-06-16', to: '2026-06-18' }
];
let status = $state('');
</script>
<div class="w-full">
<ResourceTimeline
value={new Date(2026, 5, 15)}
locale="en-US"
resources={rooms}
items={stays}
getResourceId={(stay) => stay.roomId}
getRange={(stay) => ({ start: stay.from, end: stay.to })}
getLabel={(stay) => stay.guest}
onCellClick={(room, date) => (status = `New booking: ${room.label} · ${toIso(date)}`)}
onItemClick={(stay) => (status = `${stay.guest} is already booked here`)}
>
{#snippet cell({ isOccupied, isDisabled })}
<!-- Painted on free nights only; the click itself is onCellClick's job, so
the cell keeps its single tab stop and works from the keyboard too. -->
{#if !isOccupied && !isDisabled}
<span class="text-text-tertiary grid h-full place-items-center text-xs opacity-50">+</span>
{/if}
{/snippet}
</ResourceTimeline>
<p class="text-text-secondary mt-3 text-sm">
{status || 'Click a free night, or press Enter on one.'}
</p>
</div>
The same rows in a calendar and a timeline
CalendarEvent[]. You don't convert the array; you point the timeline at the fields it needs: getResourceId picks each lane, getRange the nights. Both legends read the same categories, because DateCategory is one type across Calendar, Planner and ResourceTimeline. Keep a converted copy only where the two views genuinely need different data.<script lang="ts">
import { Calendar, ResourceTimeline } from '@urbicon-ui/blocks';
import type { CalendarEvent, DateCategory, TimelineResource } from '@urbicon-ui/blocks';
// One array, two views. These are `CalendarEvent`s (the shape a Calendar
// already holds), and the timeline reads them through its accessors instead of
// a converted copy: `getResourceId` finds the lane, `getRange` the nights.
const events: CalendarEvent[] = [
{
id: 'b1',
title: 'Lindqvist',
start: new Date(2026, 5, 15),
end: new Date(2026, 5, 18),
categoryId: 'garden',
meta: { roomId: 'r-01' }
},
{
id: 'b2',
title: 'Amaral',
start: new Date(2026, 5, 16),
end: new Date(2026, 5, 21),
categoryId: 'suite',
meta: { roomId: 'r-02' }
},
{
id: 'b3',
title: 'Weber',
start: new Date(2026, 5, 19),
end: new Date(2026, 5, 20),
categoryId: 'garden',
meta: { roomId: 'r-01' }
}
];
// The same categories drive both legends: `DateCategory` is one type across
// Calendar, Planner and ResourceTimeline, so nothing is mapped here either.
const categories: DateCategory[] = [
{ id: 'garden', label: 'Garden Room', color: 'oklch(0.62 0.13 250)' },
{ id: 'suite', label: 'Suite', color: 'oklch(0.72 0.15 60)' }
];
const rooms: TimelineResource[] = [
{ id: 'r-01', label: 'Room 01', description: 'Garden Room' },
{ id: 'r-02', label: 'Room 02', description: 'Suite' }
];
</script>
<div class="space-y-6">
<ResourceTimeline
view="days"
days={10}
size="sm"
value={new Date(2026, 5, 14)}
locale="en-GB"
resources={rooms}
items={events}
{categories}
getResourceId={(event) => String(event.meta?.roomId ?? '')}
getRange={(event) => ({ start: event.start, end: event.end ?? event.start })}
getCategoryId={(event) => event.categoryId}
getLabel={(event) => event.title}
/>
<!-- The same three stays as month bars: one axis instead of two, which is the
whole difference between the two views. -->
<Calendar
view="month"
views={['month']}
defaultDate={new Date(2026, 5, 14)}
showViewSwitcher={false}
showEventList={false}
size="sm"
locale="en-GB"
{events}
{categories}
/>
</div>
Bars you render yourself
span snippet hands you your item with its own type, plus the layout geometry for the bar. isStart and isEnd are false where a stay runs past the window; the leading and trailing ellipses key off them. getLabel still supplies the bar's accessible name.<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
interface Stay {
id: string;
roomId: string;
guest: string;
firstNight: string;
lastNight: string;
}
const rooms = [
{ id: 'cala-01', label: 'Cala 01', description: 'Garden Room' },
{ id: 'cala-07', label: 'Cala 07', description: 'Corner Room' },
{ id: 'cala-11', label: 'Cala 11', description: 'Suite' }
];
const stays: Stay[] = [
{
id: 's1',
roomId: 'cala-01',
guest: 'Amaral',
firstNight: '2026-06-10',
lastNight: '2026-06-17'
},
{
id: 's2',
roomId: 'cala-07',
guest: 'Bianchi',
firstNight: '2026-06-13',
lastNight: '2026-06-25'
},
{
id: 's3',
roomId: 'cala-11',
guest: 'Marek',
firstNight: '2026-06-17',
lastNight: '2026-06-19'
}
];
</script>
<ResourceTimeline
size="lg"
value={new Date(2026, 5, 15)}
locale="en-US"
resources={rooms}
items={stays}
getResourceId={(stay) => stay.roomId}
getRange={(stay) => ({ start: stay.firstNight, end: stay.lastNight })}
getLabel={(stay) => `${stay.guest}, ${stay.firstNight} to ${stay.lastNight}`}
>
{#snippet span({ item, totalDays, isStart, isEnd })}
<span class="truncate">
{isStart ? '' : '… '}{item.guest} · {totalDays} nights{isEnd ? '' : ' …'}
</span>
{/snippet}
</ResourceTimeline>
02 Customization
slotClasses
ghost variant (no grid lines, pill-shaped bars). Density is a separate axis: size sets --rt-lane-w, --rt-day-w and --rt-bar-h on the track slot, so a slotClasses.track of [--rt-day-w:4rem] re-tunes the geometry without a new template.<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
interface Stay {
id: string;
roomId: string;
guest: string;
firstNight: string;
lastNight: string;
state: 'confirmed' | 'option';
}
const rooms = [
{ id: 'duna-03', label: 'Duna 03', description: 'Room' },
{ id: 'duna-06', label: 'Duna 06', description: 'Garden Room' },
{ id: 'duna-09', label: 'Duna 09', description: 'Suite' }
];
const states = [
{ id: 'confirmed', label: 'Confirmed', color: 'oklch(0.62 0.13 250)' },
{ id: 'option', label: 'Option', color: 'oklch(0.83 0.13 90)' }
];
const stays: Stay[] = [
{
id: 's1',
roomId: 'duna-03',
guest: 'Varela',
firstNight: '2026-06-15',
lastNight: '2026-06-17',
state: 'confirmed'
},
{
id: 's2',
roomId: 'duna-06',
guest: 'Camacho',
firstNight: '2026-06-17',
lastNight: '2026-06-20',
state: 'option'
},
{
id: 's3',
roomId: 'duna-09',
guest: 'Pons',
firstNight: '2026-06-16',
lastNight: '2026-06-21',
state: 'confirmed'
}
];
</script>
<ResourceTimeline
variant="ghost"
value={new Date(2026, 5, 15)}
locale="en-US"
resources={rooms}
items={stays}
categories={states}
getResourceId={(stay) => stay.roomId}
getCategoryId={(stay) => stay.state}
getRange={(stay) => ({ start: stay.firstNight, end: stay.lastNight })}
getLabel={(stay) => stay.guest}
slotClasses={{
base: 'bg-surface-elevated border-border-subtle rounded-2xl border px-4 pb-3',
header: 'border-b-0',
headerTitle: 'text-text-tertiary text-xs tracking-[0.18em] uppercase',
corner: 'bg-transparent',
dayHeaderRow: 'border-b-0',
dayHeaderWeekday: 'text-text-tertiary',
dayHeaderDate: 'text-text-secondary font-normal',
laneHeader: 'bg-transparent',
laneLabel: 'text-2xs text-text-secondary tracking-[0.14em] uppercase',
laneDescription: 'hidden',
span: 'rounded-full shadow-[var(--blocks-shadow-sm)]',
legend: 'justify-end'
}}
/>
Load only the visible window
onNavigate fires after every step with the reference date and the window that is now on screen. Fetch there and swap items; resources stay put.<script lang="ts">
import { ResourceTimeline } from '@urbicon-ui/blocks';
import { toIso } from '@urbicon-ui/blocks/date';
let { data } = $props();
let bookings = $state(data.bookings);
async function load(range: { start: Date; end: Date }) {
const query = new URLSearchParams({ from: toIso(range.start), to: toIso(range.end) });
bookings = await fetch(`/api/bookings?${query}`).then((r) => r.json());
}
</script>
<ResourceTimeline
view="days"
days={14}
resources={data.rooms}
items={bookings}
getResourceId={(b) => b.roomId}
getRange={(b) => ({ start: b.firstNight, end: b.lastNight })}
onNavigate={(_date, range) => load(range)}
/>03 Accessibility
The ARIA grid pattern
The day track is a role="grid": one row per resource, the lane
label as its rowheader, one gridcell per day carrying aria-colindex, and a columnheader per column in the header row. A
roving tabindex keeps the whole grid to a single tab stop.
Keyboard
ArrowLeft/ArrowRight move one day inside the lane, ArrowUp/ArrowDown move one lane at the same day. An arrow at the window edge stays put rather than paging. Home/End jump to the first and last column of the lane, with Ctrl to the first and last cell of the grid, and PageUp/PageDown step the window. Enter/Space activate.
Bars are reachable from the cells they cover
A bar is a button anchored in its first visible day and overhangs the rest, so activating
any cell it covers triggers that item's onItemClick; the free-cell hook fires
only on cells no bar covers. Where two bars stack on one day, activating again steps to the
next, top row first, then wraps back to the top. getLabel is the bar's accessible name; without it a bar reads as “Occupied”.
Today stays announced
Today's column header and cells carry aria-current="date" whether or not highlightToday tints them, so turning the tint off keeps the semantic marker.
The localized window title is mirrored into an aria-live="polite" region, so navigating is announced.
Motion and scrolling
Navigation replaces the window instantly, with no transition. overflow-x sits on the day track, never on the root, so a window wider than the viewport
scrolls the grid instead of the page.
04 API Reference
42 propsProp | Type | Default | Description | |
|---|---|---|---|---|
getRange required | (item: T) => TimelineRange | — | The item's **inclusive** day range: both start and end are days the
bar covers. A stay ending at check-out passes checkOut − 1. Return
Dates, or local date strings ('2026-06-16') which are read verbatim and
never UTC-parsed. A range whose end precedes its start is rendered with the
two swapped and warns in DEV. Required. | |
getResourceId required | (item: T) => string | — | Which lane an item belongs to. An id that is in no resources entry drops the item (DEV warns). Required. | |
categories | DateCategory[] | — | Colour buckets for the bars, and the legend below the grid. | |
cell | Snippet<[TimelineCellContext]> | — | Render extra content inside every (resource, day) cell, e.g. an "add" affordance on free days. | |
class | string | — | Extra classes merged onto the root element. | |
dayHeader | Snippet<[TimelineDayContext]> | — | Customise a day column's header. | |
days | number | 14 | Column count for view="days". Ignored in week. | |
disabled | boolean | false | Disable navigation and cell activation. | |
empty | Snippet | — | Replace the "no resources" message shown when resources is empty. | |
getCategoryId | (item: T) => string | undefined | — | The item's category id, looked up in categories. Falls back to resource.categoryId. | |
getId | (item: T) => string | — | Stable key for an item, used as the {#each} key. Defaults to resource id + start day + index. | |
getLabel | (item: T) => string | — | The bar's text and accessible name. Without it a bar renders as a plain occupancy block. | |
groupLabel | Snippet<[TimelineGroupContext]> | — | Customise a group heading row. | |
groups | TimelineGroup[] | — | Heading rows above the lanes carrying the matching groupId. Supplying
them re-orders the lanes to follow this list; a lane whose groupId names
no group is appended without a heading rather than dropped. | |
header | Snippet<[TimelineHeaderContext]> | — | Replace the default toolbar (prev/title/today/next). | |
highlightToday | boolean | true | Tint today's column. Visual only — aria-current="date" is set on today's
header and cells either way, so switching the highlight off never costs the
semantic pointer. | |
highlightWeekend | boolean | false | Tint Saturday/Sunday columns. | |
isDateDisabled | (date: Date) => boolean | — | Predicate for days that cannot be activated, on top of minDate/maxDate. | |
items | T[] | — | The items to lay out as bars. | |
legend | Snippet<[TimelineLegendContext]> | — | Replace the default category legend. | |
locale | string | 'auto' | BCP 47 locale tag for the weekday names and the header title. Defaults to
'auto', which follows the active <I18nProvider> locale, so an app that
already declares its language does not repeat it here. SSR-safe: the locale
comes from context, so server and client resolve the same tag. Pass an
explicit tag (e.g. 'de-DE') to override. | |
maxDate | Date | — | Latest navigable date. The window clamps span-preserving, so it never collapses at the bound. | |
maxRowsPerLane | number | — | Bar rows to render per lane; anything past it becomes a +n chip at the lane's right edge. Unset renders every row. | |
minDate | Date | — | Earliest navigable date. The window clamps span-preserving, so it never collapses at the bound. | |
onCellClick | (resource: TimelineResource, date: Date) => void | — | Fires when a cell **no bar covers** is activated: the hook for an "add
booking" affordance. Activating a day inside an existing stay fires
onItemClick instead, from click or keyboard. | |
onItemClick | (item: T, resource: TimelineResource) => void | — | Fires when a bar is activated: a click on it, or Enter/Space on **any** cell it covers (the bar overhangs those cells, so the keyboard reaches what the pointer hits). Where several bars stack on one day, activating again steps through them, top row first, then wraps. | |
onNavigate | (date: Date, range: DateRange) => void | — | Fires after navigation with the new reference date and the visible window; load data here. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ ResourceTimeline: {...} }}>. | |
resourceLabel | Snippet<[TimelineResourceContext]> | — | Customise a lane's label in the resource column. | |
resources | TimelineResource[] | — | The lanes, top to bottom. An empty list renders the empty state. | |
showLegend | boolean | true | Render the category legend below the grid. Ignored without categories. | |
size | smmdlg | 'md' | Density: lane width, day-column width and bar height. | |
slotClasses | Partial<Record<ResourceTimelineSlots, string>> | — | Per-slot class overrides merged with tv() styles. Slots: base | header | headerTitle | nav | navButton | track | dayHeaderRow | corner | dayHeader | dayHeaderWeekday | dayHeaderDate | body | groupRow | groupLabel | lane | laneHeader | laneLabel | laneDescription | dayCell | span | spanLabel | overflow | legend | legendItem | legendDot | legendLabel | empty | |
span | Snippet<[TimelineSpanContext<T>]> | — | Render a bar's content: receives the clipped geometry and the typed item. | |
stickyResourceColumn | boolean | true | Keep the resource column pinned while the day track scrolls sideways. Turn it off inside a shell that already provides its own horizontal scrolling. | |
unstyled | boolean | false | Remove all default tv() classes; only user-provided classes apply, and the
header's today-button tooltip goes bare with them. Note
that this also strips the layout's custom properties (--rt-lane-w,
--rt-day-w, --rt-bar-h …), so an unstyled timeline has to re-declare
them along with the look. | |
value | Date | today | Reference date the window is anchored on. Supports bind:value. | |
variant | defaultborderedghost | 'default' | Visual style: default, bordered (a framed grid) or ghost (no grid lines, pill bars). | |
view | ResourceTimelineView | 'week' | week snaps to the week containing value; days starts at value. | |
weekStartsOn | 0123 +3 more | 1 | First day of the week for view="week" (0=Sun … 6=Sat). | |
...HTMLAttributes<HTMLDivElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') | |
...ResourceTimelineVariants variant | VariantProps | — | Styling variants from ResourceTimelineVariants |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ResourceTimelineProps | interface | props | 0 | — | |
DateCategory | interface | helper | 1 | A colour bucket for a calendar event, a timeline span, and the legend row
below the grid. One type across Calendar, Planner and ResourceTimeline, so
moving categories between them needs no mapping.
color takes any CSS colour value: hex, rgb(), oklch() or a
var(--token) reference. Where a bar or event chip carries a LABEL on the
colour, the component picks black or white by measuring that value, which
only works for a value it can read. A var() or a color-mix() is resolved
by the browser, not by the component, so those fall back to white: pass a
colour dark enough to carry it, or set the label colour yourself through that
component's slotClasses. | |
DateRange | interface | helper | 0 | An inclusive start/end date pair: a selected range, a visible window, the
range onNavigate reports. One type for Calendar, Planner and
ResourceTimeline alike.
ResourceTimeline.getRange is not this type: it also accepts local date
strings ('2026-06-16'), which a selection value must not. | |
ResourceTimelineView | type | helper | 1 | Window mode. week is the ISO week containing the reference date; days is
a fixed N-day window **starting at** the reference date. | |
TimelineCellContext | interface | helper | 0 | Per-cell context for the cell snippet — one (resource, day) intersection. | |
TimelineDayContext | interface | helper | 0 | Per-column context for the dayHeader snippet. | |
TimelineGroup | interface | helper | 1 | A heading row above the lanes that carry its id in groupId. | |
TimelineGroupContext | interface | helper | 0 | Context for the groupLabel snippet. | |
TimelineHeaderContext | interface | helper | 0 | Context for the header snippet — everything to build a custom toolbar. Same shape as PlannerHeaderContext, deliberately. | |
TimelineLegendContext | interface | helper | 0 | Context for the legend snippet. | |
TimelineRange | interface | helper | 0 | The **inclusive** day range a span occupies: both start and end are days
the bar covers. A hotel stay converts by subtracting one day from check-out
(the last *night* is checkOut − 1); the same convention CalendarEvent.end
and getEventDayInfo use.
Strings are read as local calendar days, verbatim, through the same parser
Planner.getDate uses; '2026-06-16' is never UTC-parsed and so never
shifts a day west of Greenwich. | |
TimelineResource | interface | helper | 1 | One lane of the timeline: a room, a chair, a vehicle, a person. | |
TimelineResourceContext | interface | helper | 0 | Context for the resourceLabel snippet. | |
TimelineSpanContext | interface | helper | 0 | The value the span snippet receives per bar. | |
ResourceTimelineSlots | type | variant | 0 | Slot names derived from the tv() config — single source of truth for slotClasses. | |
ResourceTimelineVariants | type | variant | 1 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: | |
VariantProps | type | helper | 1 | — |
06 Installation
Import
import { ResourceTimeline } from '@urbicon-ui/blocks';