Date Picker
A button that opens a Calendar in a popup.
src/components/rahti_ui/date_picker.rs
Basic
shadcn's recipe is a Popover wrapping a Button and a Calendar, with the chosen date held as page state. That last part does not port — the Calendar is a PulsePoint scope of its own — so this is a component, and the date reaches the label as a DOM event rather than as a prop.
Click the button to open, pick a day and it closes itself. Escape closes it too, and so does a click anywhere outside — a document listener registered only while the popup is open, and taken away again by the effect's cleanup.
Range
shadcn's range recipe is the same three parts with `mode="range"` on the Calendar and a two-ended label on the button. `number_of_months` is what a range picker usually wants, and it is handed straight through.
A range closes on its second date rather than its first, which is the only thing the popup needs to know about the mode. The label spells both ends and joins them with an en dash; `medium` is the shorter of the two above.
Date of birth
shadcn's second recipe: `captionLayout="dropdown"` so a year eighty back is two clicks rather than nine hundred, and a `max` so tomorrow is not a birthday. `month` opens it somewhere useful instead of on this month.
This is the example that needs the guard. The caption's month and year are native `<select>`s, and a `<select>` fires its own `change` — which bubbles through the picker's root exactly like the Calendar's does. A native change carries `detail === 0`, so the handler reads the detail's shape and ignores anything without a `mode` on it. Without that, changing the year would be read as choosing a date and would close the popup.
Date style
shadcn writes `format(date, "PPP")` from `date-fns`. There is no `date-fns` here, so the four `Intl.DateTimeFormat` date styles are what this port offers instead — `long` is the nearest thing to `PPP`, and is the default.
The label is built in the browser and not on the server, because the locale that decides it is the reader's. That is also why a seeded selection still shows as a formatted date on the first paint — the value crosses as `yyyy-mm-dd` and is spelled out at mount.
Constraints
`min`, `max`, `disabled_dates` and `disabled_weekdays` are shadcn's `disabled` matcher in the two shapes this library ports. `min` and `max` also stop the nav, so there is no month to reach where nothing is selectable.
Every one of these is the Calendar's prop, handed on unchanged. The picker decides the button, the box and when to close; the Calendar decides which days exist and which can be had.
Alignment
Open the right-hand one: hung from the trigger's left edge it would run off a narrow page, and `align="end"` is what a Popover's `align` would have decided.
Not a positioner's `align`. There is no floating engine here and no collision detection, so this is a choice between the root's two edges — `left-0` or `right-0` — and the popup always opens below. The Combobox makes the same trade and the module docs say so in the same words.
States
An empty picker says what it wants and a filled one says what it has. `disabled` closes both halves, and `invalid` reddens the trigger's border through the Button's own `aria-invalid:` rules.
The placeholder is greyed by `data-[empty=true]:text-muted-foreground` — shadcn's own utility, and one of the few state names a binding can safely carry here. `empty` is not an HTML boolean attribute, so the runtime leaves the attribute's name alone where it would have mangled `data-selected`.
As a form posts it
`name` is handed to the Calendar and the Calendar is what posts it — a run of real `<input type="hidden">` fields, one per date. Nothing is added here, which is the point: composing two components did not cost a third mechanism. Submit and read the query string.
The seam
A binding does not cross a `#[component]` and an event does — that one asymmetry is what makes this component possible at all. The Calendar announces what it chose, the picker listens on its own root, and neither knows anything else about the other. It is also why the Calendar has an `announce` at all: nothing inside it needs one.
// the Calendar, on every commit
new CustomEvent("change", { bubbles: true, detail: { value, mode } })
// the Date Picker, on its own root
const caught = (e) => {
const d = e.detail;
if (!d || !d.mode) { return; } // a caption select, not a day
setLabel(spell(values));
if (d.mode === "single" && values.length) { setOpen(false); }
};The port
shadcn's `PopoverContent` class list is kept whole in one constant rather than mixed into this component's own, so there is exactly one thing to delete when a real Popover lands — the arrangement the Field already uses for the Separator it is waiting on. The popup still carries `data-slot="popover-content"`, and that is load-bearing: the Calendar's root reads it and drops its own background so the popover's shows through. shadcn's rule survives because the attribute is the contract, not the component. The time-picker and natural-language recipes are a second control beside this one rather than anything it decides, so both stay a call site's to assemble.
// shadcn — a recipe, in the page
const [date, setDate] = React.useState()
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
<CalendarIcon />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={date} onSelect={setDate} />
</PopoverContent>
</Popover>
// rahti-ui — a component
<DatePicker name="due" class="w-[280px]" />
date_picker().range().number_of_months(2).name("stay").render()