Date & time
The STD datetime kit (src/components/ds/datetime/) — the workflow-primitive family for picking instants, ranges, and day schedules. Each is a thin, presentational composition over the Surface base primitive and existing DS parts (the @phauna/ds DatePicker, the ds/events TimeInput, and the ds/calendar time-grid engine). Everything takes plain props + callbacks — no server actions, no Prisma rows.
- value shapes
- DateTimePicker carries a single string whose shape follows granularity: "YYYY-MM-DD" (date), "HH:MM" (time), or "YYYY-MM-DDTHH:MM" (datetime). splitDateTime / joinDateTime are exported for callers that need the halves.
- composition
- DateTimePicker = DatePicker + TimeInput grouped in a Surface. DateTimeRangePicker = two DateTimePickers. SchedulerStrip = ds/calendar dayHours + splitDayEvents + placeTimed laid onto a Surface. No date math is re-implemented here.
- presentational
- No fetch, no server actions, no router. The parent owns every value and reacts to onChange / onSelectSlot / onSelectEvent / onValidityChange.
- a11y
- Native date/time inputs keep their accessible pickers; SchedulerStrip's slot lattice is a roving-tabindex grid (ArrowUp/Down + Enter), event blocks are focusable buttons/links, and Today / now-line read via text + aria (not color alone). Hover transitions respect reduced-motion.
DateTimePicker
One controlled control that yields a date, a time, or a datetime depending on granularity. The DatePicker and TimeInput halves sit side-by-side inside a Surface group, each keeping its native accessible picker. stepMinutes drives the time increment, timezoneLabel adds a decorative tz suffix, framed draws a Surface inset well, and invalid flips both halves to the error ring.
granularity="datetime" (step 15m): 2026-07-04T18:30
granularity="date": 2026-07-04
granularity="time" (step 30m): 09:00
framed (Surface inset well): 2026-07-04T18:30
invalid state: 2026-07-04
- value / onChange
- Controlled string in the granularity's shape (see above). onChange echoes the same shape; partial values (date with no time, etc.) are preserved verbatim.
- granularity
- 'date' | 'datetime' (default) | 'time' — picks which halves render and the value shape.
- stepMinutes / timezoneLabel
- stepMinutes sets the native time step (minutes → seconds). timezoneLabel renders a small, purely-decorative suffix (e.g. 'ET').
- min / max / invalid / disabled / required / framed
- min/max bound the active half (date string for date/datetime, 'HH:MM' for time). invalid flips both halves to the error ring; framed wraps the group in a bordered Surface inset well.
DateTimeRangePicker
A start/end pair of DateTimePickers under one labeled, Surface-grouped control, with built-in start≤end validation. The end half's lower bound is pinned to the chosen start so the native picker discourages an inverted range, and an inline alert surfaces if one slips through. Validity is reported via onValidityChange.
valid: true · 2026-07-04T18:00 → 2026-07-04T20:00
Set the end before the start to trigger the inline validation.
2026-07-04 → 2026-07-06
- startValue / endValue + onStartChange / onEndChange
- Controlled; the parent owns both values. Each half is a DateTimePicker, so they share granularity / stepMinutes / timezoneLabel / min / max.
- validation
- Inverted ranges (end before start) flip the end half to its error ring and show an inline role=alert. error overrides the default message; onValidityChange(valid) fires when the computed validity flips. isInvertedRange is exported.
- startLabel / endLabel
- Field labels (default 'Start' / 'End'), each wired to its half via htmlFor.
SchedulerStrip
A single-day time grid (6am→11pm, from the ds/calendar engine) that lays events out as positioned blocks over a clickable slot lattice, with a now-line on today and an all-day strip for untimed events. Overlapping events lane-pack side-by-side (shared placement engine). On narrow screens it collapses to a plain agenda list. Click an empty slot to pick a time, or a block to select the event.
Tuesday, July 28Today
6 eventslast slot picked: — · last event: —
Click an empty slot to pick a time, or an event block to select it. Narrow the viewport (below the sm breakpoint) to see it collapse to the agenda list.
- events / date
- events: CalendarEvent[] (the ds/calendar shared shape — id, title, startsAt/endsAt as ISO string|Date, optional tone). date: the day to render. Multi-day spans show their slice on this day; untimed events go to the all-day strip.
- onSelectSlot / onSelectEvent
- onSelectSlot(slotStart: Date) fires when an empty slot in the lattice is chosen (granularity = slotMinutes, default 30). onSelectEvent(event) fires when a block/strip/agenda row is activated.
- today / slotMinutes
- today (default new Date()) drives the now-line + Today chip — injectable for deterministic rendering. slotMinutes sets the selectable lattice + agenda granularity.
- responsive
- The time grid renders from the sm breakpoint up; below it the same events collapse to a sorted agenda list so the strip stays usable on phones.