Výběr termínu
Nabídnuté termíny seskupené po dnech, jedna volba, ovladatelné jen klávesnicí — sémantika radiogroup.
schedulinginput
Install
pnpm dlx shadcn@4.23.0 add @ansora/slot-pickerExamples
Usage
SlotPicker renders offerable appointment slots grouped by day, single selection, radiogroup semantics. It is keyboard-first: the arrows walk every slot in document order across day boundaries, wrap at both ends, and — per the APG radio-group pattern — the selection follows the focus. Home and End jump to the first and last slot. Exactly ONE slot is in the tab order (the selected one; the first selectable one when nothing is selected; the first slot when every slot is blocked), and it is derived from value on every render rather than stored, so it can never disagree with the selection your screen owns.
Required: groups, value, onChange, label (the invisible name of the radiogroup) and empty — one object with a title AND a description, so a half-supplied empty state is a compile error. "Žádné volné termíny" is a frequent state here, not an edge case, and only you know whether the emptiness is bad news. Optional: footer and className. There is no defaultValue and there will not be one: this component owns no view state, and the selection belongs in your screen's URL.
A slot's start is an ISO 8601 instant and MUST carry a zone designator — 2026-08-12T09:15:00+02:00 or the same instant spelled with a trailing Z. A zone-less local-time string is re-interpreted by whatever zone the host is set to, and around a DST change that is an hour which either happens twice or never happens at all: a missed appointment, not a formatting wobble. The component throws rather than rendering a plausible wrong time. Everything visible is formatted inside, through @ansora/core (formatTime, formatDayMonthLong, formatUnit) with Europe/Prague already pinned there.
A disabled slot is a compile-time pair. disabled: true does not compile without disabledReason, and tone does not compile without note — a tone with no note paints a pill that is never rendered. A disabled slot stays FOCUSABLE (aria-disabled, never the disabled attribute) and keeps its place in the arrow walk, so "why can't I book at eleven" is answerable without a pointer; landing on one moves the focus and not the selection. Its reason is part of the radio's accessible NAME rather than an aria-describedby, because a description is the first thing a verbosity setting drops.
A group's label is optional and falls back to formatDayMonthLong of its first slot. Pass it when the day has a relative name ("Dnes", "Zítra") — this component has no clock and must not grow one, because reading one during render is a hydration mismatch.
It stays dumb about capacity and conflicts, on purpose. It renders what it is handed, in the order it is handed. Whether a slot collides with something else is your screen's question; the moment it moves in here the component stops fitting inside a call panel.
The honest ceiling: no virtualization. Two hundred slots in one picker is the case it was designed against and is comfortable; 200–600 is degraded (every slot is a <button> with two to three text nodes); above ~600, page the day range on the screen rather than rendering more here.
footer renders OUTSIDE the radiogroup and renders even when there is nothing to pick — the moment a user needs "none of these work" most is the moment the list is empty.
import { SlotPicker } from "@/components/ansora/slot-picker"
const [slot, setSlot] = React.useState<string | null>(null)
<SlotPicker
label="Nabídnuté termíny"
value={slot}
onChange={setSlot}
empty={{
title: "Žádné volné termíny",
description: "V zadaném rozsahu není nic volného. Zkuste jiný týden.",
}}
groups={[
{
id: "dnes",
label: "Dnes",
slots: [
{ id: "a", start: "2026-08-12T09:15:00+02:00", durationMin: 30, provider: "MUDr. Doležal" },
{ id: "b", start: "2026-08-12T11:00:00+02:00", durationMin: 30, note: "Přednostní", tone: "warn" },
{ id: "c", start: "2026-08-12T11:30:00+02:00", durationMin: 30, disabled: true, disabledReason: "Obsazeno" },
],
},
]}
footer={<Button variant="outline">Žádný termín nevyhovuje</Button>}
/>
Files
registry/ansora/components/slot-picker.tsx→@components/ansora/slot-picker.tsx
Dependencies
@ansora/base@ansora/status-pill@ansora/empty-state
Sign-off card
Who committed to this record, in which role, at what moment and in which version — the artifact an audit asks for, stated rather than enforced.
Transcript
An ordered list of what was said, with speaker, offset and an optional confidence band — auto-following the tail by position rather than by guessing who scrolled.