--- ## Planner **Stability:** beta — the API shape is settled and documented; behaviour-level fixes may still land as breaking changes in a minor. Date-indexed planning board — a week, month or custom-range grid whose cells hold YOUR domain content (meals, shifts, bookings, content slots) via a generic `cell` snippet. Buckets `items` by calendar day, then handles navigation, ISO week numbers, keyboard a11y and a responsive column→stack layout. For timed appointments, multi-day spans or recurrence use `Calendar` instead. **Import:** `import { Planner } from '@urbicon-ui/blocks';` ### Examples ```svelte e.date} sort={(a, b) => MEAL_ORDER[a.mealType] - MEAL_ORDER[b.mealType]} bind:value={referenceDate} onNavigate={(_, range) => loadWeek(range.start)}> {#snippet cell({ items, isoDate })} {#each items as entry (entry.id)} {/each} {/snippet} ``` ```svelte e.date} isDateDisabled={(d) => closed.has(toIso(d))} /> ``` ### Variants - variant: bordered, default, ghost (default: default) - size: lg, md, sm (default: md) ### Api | Prop | Type | Required | Default | Description | | --- | --- | :---: | --- | --- | | getDate | `(item: T) => Date | string` | yes | | Map an item to its calendar day. Return a `Date`, or a local date string (`'2026-06-16'`) taken verbatim — never UTC-parsed, so a plain date never shifts across timezones. A date-*time* string is bucketed by its written date part too; if your value is a UTC instant whose local day matters (`'…T23:00:00Z'`), return `new Date(value)` so the local timezone applies. Required. | | ...HTMLAttributes | `HTMLAttributes` | no | | HTML attributes (excluding: 'children') | | ...PlannerVariants | `VariantProps` | no | | Styling variants from PlannerVariants | | animated | `boolean` | no | true | Slide-transition the grid on navigate (respects reduced-motion). | | cell | `Snippet<[PlannerCellContext]>` | no | | Render a day's content — the core of the API. Receives bucketed `items: T[]`. Called for **every** day, including empty ones (`items: []`) — unless an `empty` snippet is given, which then handles empty days instead. Put an "add" affordance here to keep it available on empty days. | | class | `string` | no | | Extra classes merged onto the root element. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. | | dayHeader | `Snippet<[PlannerDayContext]>` | no | | Customise each weekday/column header. | | disabled | `boolean` | no | false | Disable navigation and selection. | | empty | `Snippet<[PlannerCellContext]>` | no | | Placeholder rendered **instead of** `cell` for days with no items. Omit it to let `cell` render empty days too. | | fixedWeeks | `boolean` | no | false | Always render 6 week rows in `view="month"`, so the grid keeps its height across months of 4, 5 and 6 rows. Ignored in `week`/`range`. | | header | `Snippet<[PlannerHeaderContext]>` | no | | Replace the default toolbar (prev/next/today/title/week). | | highlightToday | `boolean` | no | true | Visually mark today's cell. | | highlightWeekend | `boolean` | no | false | Tint Saturday/Sunday cells. | | isDateDisabled | `(date: Date) => boolean` | no | | Disables every day it returns `true` for, on top of `minDate`/`maxDate` — the one way to disable single days, whether a rule (weekends) or a list (holidays). A disabled day is dimmed, reports `aria-disabled` and cannot be selected. It receives a `Date` of its own for each day — read only its calendar day. It runs again when `$state` it reads changes. Compare calendar days, not `Date` objects (`list.includes(d)` matches nothing): keep a list as a `Set` of `toIso` keys from `@urbicon-ui/blocks/date` and pass `(d) => blocked.has(toIso(d))`. It must not throw; the grid does not catch it. | | items | `T[]` | no | | The items to lay out. Each is bucketed onto a day via `getDate`. | | locale | `string` | no | 'auto' | BCP 47 locale tag for date formatting — month names, weekday names and the header title. Defaults to `'auto'`, which follows the active `` locale, so an app that already declares its language does not have to repeat it here. SSR-safe: the locale comes from context, so the server and the client resolve the same tag (`Intl` with `undefined` would follow the runtime and disagree across hydration). Falls back to the base locale (`en`) when no provider is mounted. Pass an explicit tag (e.g. `'de-DE'`, `'ja-JP'`) to override. | | maxDate | `Date` | no | | Latest navigable/selectable date. | | minDate | `Date` | no | | Earliest navigable/selectable date. | | onDateSelect | `(date: Date) => void` | no | | Fires when a day cell is activated (click / Enter / Space). | | onNavigate | `(date: Date, range: DateRange) => void` | no | | Fires after navigation. Receives the new reference date and visible range — load data here. | | preset | `string` | no | | Apply a named preset registered via ``. | | rangeEnd | `Date` | no | | End of the window for `view="range"`. | | rangeStart | `Date` | no | | Start of the window for `view="range"`. | | selectedDate | `Date` | no | | The active highlighted day. Supports `bind:selectedDate`. | | showWeekNumber | `boolean` | no | | Renamed to `showWeekNumbers` for parity with `Calendar`, which has carried the plural since long before Planner existed. Still honoured, and warns in DEV. | | showWeekNumbers | `boolean` | no | false | Show the ISO week-number column on the left. | | size | `'sm' | 'md' | 'lg'` | no | 'md' | Density of the grid and header. | | slotClasses | `Partial>` | no | | Per-slot class overrides merged with tv() styles. Slots: base | header | headerTitle | nav | navButton | grid | weekdayHeader | weekday | weekNumber | week | cell | cellHeader | cellWeekday | cellDate | cellItems | empty. In the same Tailwind bucket `class` wins over the `slotClasses` entry for the element it lands on. Presets and provider defaults sit below both. | | sort | `(a: T, b: T) => number` | no | | Comparator for items within a day cell (e.g. by meal type or start label). | | swipeable | `boolean` | no | true | Enable horizontal swipe-to-navigate on touch. | | unstyled | `boolean` | no | false | Remove all default tv() classes — only user-provided classes apply. Reaches the header's today-button tooltip. | | value | `Date` | no | today | Reference date the view is anchored on. Supports `bind:value`. | | variant | `'default' | 'bordered' | 'ghost'` | no | 'default' | Visual treatment. | | view | `PlannerView` | no | 'week' | Layout mode. | | weekStartsOn | `0 | 1 | 2 | 3 | 4 | 5 | 6` | no | 1 | First day of the week (0=Sun … 6=Sat). | Inherited from: - Omit (omit-pattern) - Omit, 'children'> (omit-pattern) ### Types ```ts type PlannerCellState = Pick< PlannerVariants, 'dayState' | 'selected' | 'weekend' | 'outside' > ``` ```ts interface DateRange { start: Date; end: Date; } ``` ```ts interface PlannerCellContext { /** The cell's date (local midnight). */ date: Date; /** `YYYY-MM-DD` local key for this date — the same key items were bucketed on. */ isoDate: string; /** Items falling on this day, bucketed and sorted (the consumer's `T`). */ items: T[]; /** Whether this date is today. */ isToday: boolean; /** Whether this date is the active selected day (`selectedDate`). */ isSelected: boolean; /** Whether this date is a Saturday or Sunday. */ isWeekend: boolean; /** Whether this date spills outside the focused month / requested range. */ isOutsideRange: boolean; /** * Whether this date cannot be selected — outside `[minDate, maxDate]`, rejected by * `isDateDisabled`, or the whole grid is `disabled`. The scaffold already sets * `aria-disabled` on the gridcell; read this to grey out your own content or hide an * "add" affordance. */ isDisabled: boolean; /** ISO 8601 week number of this date. */ weekNumber: number; /** Localized short weekday name (e.g. "Mo") — handy for stacked layouts. */ weekday: string; /** Make this day the active selection (fires `onDateSelect`). */ selectDate: () => void; } ``` ```ts interface PlannerDayContext { /** The concrete date in single-week (`week`) view; `undefined` in month/range * where a column spans many dates. */ date?: Date; /** `YYYY-MM-DD` local key, present only when `date` is. */ isoDate?: string; /** Localized short weekday name (e.g. "Mo"). */ weekday: string; /** Whether the column's concrete date is today (always false in month/range). */ isToday: boolean; /** Whether this is a weekend column. */ isWeekend: boolean; /** ISO week number of the column's date, when it has one. */ weekNumber?: number; } ``` ```ts interface PlannerHeaderContext { /** Localized title for the current view (e.g. "KW 25 · 15.–21. Juni"). */ title: string; /** First visible date of the view. */ rangeStart: Date; /** Last visible date of the view. */ rangeEnd: Date; /** ISO week number of the reference date. */ weekNumber: number; /** The active view. */ view: PlannerView; /** Step the view by `delta` periods (− back, + forward). */ navigate: (delta: number) => void; /** Jump to the period containing today. */ goToToday: () => void; /** Jump to the period containing `date`. */ goTo: (date: Date) => void; /** Whether back-navigation is within bounds. */ canGoBack: boolean; /** Whether forward-navigation is within bounds. */ canGoForward: boolean; /** Whether today is within `[minDate, maxDate]`; gate a custom Today control on it. */ canGoToToday: boolean; } ``` ```ts type PlannerView = 'week' | 'month' | 'range' ``` ### Slots (slotClasses keys) `base`, `grid`, `weekdayHeader`, `weekday`, `weekNumber`, `week`, `cell`, `cellHeader`, `cellWeekday`, `cellDate`, `cellItems`, `empty`