@krizaka/ui 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/achievement.js +1 -1
- package/dist/action-bar.js +1 -1
- package/dist/app-shell.js +1 -1
- package/dist/argument-card.js +2 -2
- package/dist/card-grid.js +1 -1
- package/dist/card.js +1 -1
- package/dist/choice-grid.js +1 -1
- package/dist/{chunk-OGJI5X4W.js → chunk-2PUDI7JS.js} +2 -2
- package/dist/chunk-4LOMIQVX.js +21 -0
- package/dist/chunk-4LOMIQVX.js.map +1 -0
- package/dist/{chunk-K3SWZXYT.js → chunk-DGQMDJXK.js} +2 -2
- package/dist/chunk-G3ETON2A.js +22 -0
- package/dist/chunk-G3ETON2A.js.map +1 -0
- package/dist/chunk-I5RVM5CW.js +27 -0
- package/dist/chunk-I5RVM5CW.js.map +1 -0
- package/dist/chunk-IZ7MV6HM.js +26 -0
- package/dist/chunk-IZ7MV6HM.js.map +1 -0
- package/dist/chunk-ZNMFG62E.js +20 -0
- package/dist/chunk-ZNMFG62E.js.map +1 -0
- package/dist/cn.d.ts +9 -1
- package/dist/cn.js +5 -3
- package/dist/code-block.js +1 -1
- package/dist/command.js +1 -1
- package/dist/confirm-button.js +1 -1
- package/dist/continue-prompt.js +1 -1
- package/dist/cta-panel.js +1 -1
- package/dist/data-table.js +2 -2
- package/dist/date-picker.d.ts +650 -61
- package/dist/date-picker.js +918 -122
- package/dist/date-picker.js.map +1 -1
- package/dist/decision-list.js +1 -1
- package/dist/dropdown-menu.js +1 -1
- package/dist/error-page.js +1 -1
- package/dist/field.js +1 -1
- package/dist/filter-bar.js +2 -2
- package/dist/form/server.d.ts +96 -0
- package/dist/form/server.js +82 -0
- package/dist/form/server.js.map +1 -0
- package/dist/form.d.ts +503 -0
- package/dist/form.js +760 -0
- package/dist/form.js.map +1 -0
- package/dist/gallery-page.js +1 -1
- package/dist/glow-card.js +1 -1
- package/dist/goal-gauge.js +1 -1
- package/dist/how-it-works-page.js +1 -1
- package/dist/landscape.js +1 -1
- package/dist/link-card.js +1 -1
- package/dist/longform-page.js +1 -1
- package/dist/marquee.js +1 -1
- package/dist/mascot-quote.js +1 -1
- package/dist/motion.css +18 -1
- package/dist/native/form.d.ts +376 -0
- package/dist/native/form.js +650 -0
- package/dist/native/form.js.map +1 -0
- package/dist/native.d.ts +351 -3
- package/dist/native.js +1205 -399
- package/dist/native.js.map +1 -1
- package/dist/nav-bar.js +1 -1
- package/dist/page-hero.js +2 -2
- package/dist/page.js +2 -2
- package/dist/popover.js +1 -1
- package/dist/pricing-page.js +1 -1
- package/dist/product-page.js +1 -1
- package/dist/profile-card.js +1 -1
- package/dist/promise-card.js +1 -1
- package/dist/scroll-stage.js +1 -1
- package/dist/stage-banner.js +1 -1
- package/dist/terminal-replay.js +1 -1
- package/dist/toast.js +1 -1
- package/dist/toc.js +1 -1
- package/dist/tooltip.js +1 -1
- package/package.json +50 -7
- package/registry/achievement.json +1 -1
- package/registry/action-bar.json +1 -1
- package/registry/alert.json +1 -1
- package/registry/app-shell.json +1 -1
- package/registry/argument-card.json +1 -1
- package/registry/audio-player.json +1 -1
- package/registry/auth-layout.json +1 -1
- package/registry/avatar.json +1 -1
- package/registry/badge.json +1 -1
- package/registry/button.json +1 -1
- package/registry/card-grid.json +1 -1
- package/registry/card.json +1 -1
- package/registry/catalogue-page.json +1 -1
- package/registry/changelog-page.json +1 -1
- package/registry/checkbox.json +89 -4
- package/registry/chip-link.json +1 -1
- package/registry/chip.json +1 -1
- package/registry/choice-grid.json +1 -1
- package/registry/cn.json +1 -1
- package/registry/code-block.json +1 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +1 -1
- package/registry/component-page.json +1 -1
- package/registry/confirm-button.json +1 -1
- package/registry/contact-page.json +1 -1
- package/registry/continue-prompt.json +1 -1
- package/registry/countdown.json +1 -1
- package/registry/cta-panel.json +1 -1
- package/registry/dashboard-layout.json +1 -1
- package/registry/data-table.json +1 -1
- package/registry/date-picker.json +1811 -236
- package/registry/decision-list.json +1 -1
- package/registry/detail-layout.json +1 -1
- package/registry/dialog.json +1 -1
- package/registry/docs-article-page.json +1 -1
- package/registry/docs-hub-page.json +1 -1
- package/registry/docs-layout.json +1 -1
- package/registry/dropdown-menu.json +1 -1
- package/registry/empty-state.json +1 -1
- package/registry/error-page.json +1 -1
- package/registry/examples/checkbox/native/terms.d.ts +4 -0
- package/registry/examples/checkbox/native/terms.dark.png +0 -0
- package/registry/examples/checkbox/native/terms.js +26 -0
- package/registry/examples/checkbox/native/terms.light.png +0 -0
- package/registry/examples/checkbox/native/terms.tsx +28 -0
- package/registry/examples/date-picker/birth-date-wheel.d.ts +4 -0
- package/registry/examples/date-picker/birth-date-wheel.js +13 -0
- package/registry/examples/date-picker/birth-date-wheel.tsx +11 -0
- package/registry/examples/date-picker/birth-date.d.ts +4 -0
- package/registry/examples/date-picker/birth-date.js +25 -0
- package/registry/examples/date-picker/birth-date.tsx +21 -0
- package/registry/examples/date-picker/calendar.js +1 -1
- package/registry/examples/date-picker/calendar.tsx +2 -6
- package/registry/examples/date-picker/day-strip.d.ts +4 -0
- package/registry/examples/date-picker/day-strip.js +28 -0
- package/registry/examples/date-picker/day-strip.tsx +27 -0
- package/registry/examples/date-picker/month-year.d.ts +4 -0
- package/registry/examples/date-picker/month-year.js +13 -0
- package/registry/examples/date-picker/month-year.tsx +10 -0
- package/registry/examples/date-picker/native/birth-date-wheel.d.ts +4 -0
- package/registry/examples/date-picker/native/birth-date-wheel.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date-wheel.js +13 -0
- package/registry/examples/date-picker/native/birth-date-wheel.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date-wheel.tsx +9 -0
- package/registry/examples/date-picker/native/birth-date.d.ts +4 -0
- package/registry/examples/date-picker/native/birth-date.dark.png +0 -0
- package/registry/examples/date-picker/native/birth-date.js +30 -0
- package/registry/examples/date-picker/native/birth-date.light.png +0 -0
- package/registry/examples/date-picker/native/birth-date.tsx +27 -0
- package/registry/examples/date-picker/native/calendar.d.ts +4 -0
- package/registry/examples/date-picker/native/calendar.dark.png +0 -0
- package/registry/examples/date-picker/native/calendar.js +13 -0
- package/registry/examples/date-picker/native/calendar.light.png +0 -0
- package/registry/examples/date-picker/native/calendar.tsx +8 -0
- package/registry/examples/date-picker/native/day-strip.d.ts +4 -0
- package/registry/examples/date-picker/native/day-strip.dark.png +0 -0
- package/registry/examples/date-picker/native/day-strip.js +29 -0
- package/registry/examples/date-picker/native/day-strip.light.png +0 -0
- package/registry/examples/date-picker/native/day-strip.tsx +26 -0
- package/registry/examples/date-picker/native/month-year.d.ts +4 -0
- package/registry/examples/date-picker/native/month-year.dark.png +0 -0
- package/registry/examples/date-picker/native/month-year.js +13 -0
- package/registry/examples/date-picker/native/month-year.light.png +0 -0
- package/registry/examples/date-picker/native/month-year.tsx +8 -0
- package/registry/examples/date-picker/native/time-slots.d.ts +4 -0
- package/registry/examples/date-picker/native/time-slots.dark.png +0 -0
- package/registry/examples/date-picker/native/time-slots.js +15 -0
- package/registry/examples/date-picker/native/time-slots.light.png +0 -0
- package/registry/examples/date-picker/native/time-slots.tsx +15 -0
- package/registry/examples/date-picker/native/time-wheel.d.ts +4 -0
- package/registry/examples/date-picker/native/time-wheel.dark.png +0 -0
- package/registry/examples/date-picker/native/time-wheel.js +13 -0
- package/registry/examples/date-picker/native/time-wheel.light.png +0 -0
- package/registry/examples/date-picker/native/time-wheel.tsx +8 -0
- package/registry/examples/date-picker/time-slots.d.ts +4 -0
- package/registry/examples/date-picker/time-slots.js +26 -0
- package/registry/examples/date-picker/time-slots.tsx +25 -0
- package/registry/examples/date-picker/time-wheel.d.ts +4 -0
- package/registry/examples/date-picker/time-wheel.js +13 -0
- package/registry/examples/date-picker/time-wheel.tsx +10 -0
- package/registry/examples/date-picker/wheel.d.ts +4 -0
- package/registry/examples/date-picker/wheel.js +14 -0
- package/registry/examples/date-picker/wheel.tsx +16 -0
- package/registry/examples/field/native/default.d.ts +4 -0
- package/registry/examples/field/native/default.dark.png +0 -0
- package/registry/examples/field/native/default.js +29 -0
- package/registry/examples/field/native/default.light.png +0 -0
- package/registry/examples/field/native/default.tsx +23 -0
- package/registry/examples/form/amount.d.ts +4 -0
- package/registry/examples/form/amount.js +30 -0
- package/registry/examples/form/amount.tsx +36 -0
- package/registry/examples/form/audience.d.ts +4 -0
- package/registry/examples/form/audience.js +29 -0
- package/registry/examples/form/audience.tsx +31 -0
- package/registry/examples/form/native/amount.d.ts +4 -0
- package/registry/examples/form/native/amount.dark.png +0 -0
- package/registry/examples/form/native/amount.js +26 -0
- package/registry/examples/form/native/amount.light.png +0 -0
- package/registry/examples/form/native/amount.tsx +30 -0
- package/registry/examples/form/native/audience.d.ts +4 -0
- package/registry/examples/form/native/audience.dark.png +0 -0
- package/registry/examples/form/native/audience.js +24 -0
- package/registry/examples/form/native/audience.light.png +0 -0
- package/registry/examples/form/native/audience.tsx +24 -0
- package/registry/examples/form/native/profile.d.ts +4 -0
- package/registry/examples/form/native/profile.dark.png +0 -0
- package/registry/examples/form/native/profile.js +33 -0
- package/registry/examples/form/native/profile.light.png +0 -0
- package/registry/examples/form/native/profile.tsx +36 -0
- package/registry/examples/form/native/settings.d.ts +4 -0
- package/registry/examples/form/native/settings.dark.png +0 -0
- package/registry/examples/form/native/settings.js +25 -0
- package/registry/examples/form/native/settings.light.png +0 -0
- package/registry/examples/form/native/settings.tsx +23 -0
- package/registry/examples/form/native/sign-up.d.ts +4 -0
- package/registry/examples/form/native/sign-up.dark.png +0 -0
- package/registry/examples/form/native/sign-up.js +38 -0
- package/registry/examples/form/native/sign-up.light.png +0 -0
- package/registry/examples/form/native/sign-up.tsx +34 -0
- package/registry/examples/form/native/upload.d.ts +4 -0
- package/registry/examples/form/native/upload.dark.png +0 -0
- package/registry/examples/form/native/upload.js +23 -0
- package/registry/examples/form/native/upload.light.png +0 -0
- package/registry/examples/form/native/upload.tsx +22 -0
- package/registry/examples/form/profile.d.ts +4 -0
- package/registry/examples/form/profile.js +35 -0
- package/registry/examples/form/profile.tsx +40 -0
- package/registry/examples/form/report.d.ts +4 -0
- package/registry/examples/form/report.js +30 -0
- package/registry/examples/form/report.tsx +30 -0
- package/registry/examples/form/settings.d.ts +4 -0
- package/registry/examples/form/settings.js +30 -0
- package/registry/examples/form/settings.tsx +31 -0
- package/registry/examples/form/sign-up.d.ts +4 -0
- package/registry/examples/form/sign-up.js +38 -0
- package/registry/examples/form/sign-up.tsx +37 -0
- package/registry/examples/form/upload.d.ts +4 -0
- package/registry/examples/form/upload.js +32 -0
- package/registry/examples/form/upload.tsx +32 -0
- package/registry/field.json +104 -4
- package/registry/file-upload.json +1 -1
- package/registry/filter-bar.json +1 -1
- package/registry/form.json +2940 -0
- package/registry/gallery-page.json +1 -1
- package/registry/glow-card.json +1 -1
- package/registry/goal-gauge.json +1 -1
- package/registry/how-it-works-page.json +1 -1
- package/registry/image-cropper.json +1 -1
- package/registry/index.json +43 -10
- package/registry/iso.json +1 -1
- package/registry/kbd.json +1 -1
- package/registry/landing-page.json +1 -1
- package/registry/landscape.json +1 -1
- package/registry/legal-page.json +1 -1
- package/registry/link-card.json +1 -1
- package/registry/loader.json +1 -1
- package/registry/longform-page.json +1 -1
- package/registry/marketing-layout.json +1 -1
- package/registry/marquee.json +1 -1
- package/registry/mascot-quote.json +1 -1
- package/registry/mascot-state.json +2 -2
- package/registry/mascots.json +2 -2
- package/registry/media-capture.json +1 -1
- package/registry/meter.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/number-ticker.json +1 -1
- package/registry/page-header.json +1 -1
- package/registry/page-hero.json +1 -1
- package/registry/page.json +1 -1
- package/registry/pagination.json +1 -1
- package/registry/pointer-light.json +1 -1
- package/registry/popover.json +1 -1
- package/registry/pricing-page.json +1 -1
- package/registry/product-page.json +1 -1
- package/registry/profile-card.json +1 -1
- package/registry/progress.json +1 -1
- package/registry/promise-card.json +1 -1
- package/registry/radio-group.json +1 -1
- package/registry/scroll-stage.json +1 -1
- package/registry/search-field.json +1 -1
- package/registry/section-backdrop.json +1 -1
- package/registry/separator.json +1 -1
- package/registry/settings-layout.json +1 -1
- package/registry/skeleton.json +1 -1
- package/registry/slider.json +1 -1
- package/registry/slot.json +1 -1
- package/registry/spinner.json +1 -1
- package/registry/split-view.json +1 -1
- package/registry/stage-banner.json +1 -1
- package/registry/stat.json +1 -1
- package/registry/status-page.json +1 -1
- package/registry/stepper.json +1 -1
- package/registry/switch.json +1 -1
- package/registry/tabs.json +1 -1
- package/registry/tag-input.json +1 -1
- package/registry/terminal-replay.json +1 -1
- package/registry/theme.json +8 -1
- package/registry/toast.json +1 -1
- package/registry/toc.json +1 -1
- package/registry/tooltip.json +1 -1
- package/registry/video-player.json +1 -1
- package/registry/video-trimmer.json +1 -1
- package/registry/virtual.json +1 -1
- package/skills/date-picker/SKILL.md +43 -0
- package/skills/form/SKILL.md +44 -0
- package/dist/chunk-2USC2YT6.js +0 -17
- package/dist/chunk-2USC2YT6.js.map +0 -1
- package/dist/chunk-5AXLA4LK.js +0 -13
- package/dist/chunk-5AXLA4LK.js.map +0 -1
- package/dist/chunk-FJDSLNXK.js +0 -11
- package/dist/chunk-FJDSLNXK.js.map +0 -1
- package/dist/chunk-KNBYZGSQ.js +0 -11
- package/dist/chunk-KNBYZGSQ.js.map +0 -1
- package/dist/chunk-VCAAHCHL.js +0 -17
- package/dist/chunk-VCAAHCHL.js.map +0 -1
- /package/dist/{chunk-OGJI5X4W.js.map → chunk-2PUDI7JS.js.map} +0 -0
- /package/dist/{chunk-K3SWZXYT.js.map → chunk-DGQMDJXK.js.map} +0 -0
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
"name": "date-picker",
|
|
3
3
|
"type": "primitive",
|
|
4
4
|
"title": "Date picker",
|
|
5
|
-
"summary": "
|
|
6
|
-
"description": "
|
|
5
|
+
"summary": "Dates and times chosen the way the task asks — a calendar, a period with presets, a strip of days, wheels, time slots, a month, a date of birth — as ISO values (`2026-10-11`, `2026-10`, `18:30`) the form, the address and the API share, in the locale's own words.",
|
|
6
|
+
"description": "Dates and times chosen the way the task asks — a calendar, a period with presets, a strip of days, wheels, time slots, a month, a date of birth — as ISO values (`2026-10-11`, `2026-10`, `18:30`) the form, the address and the API share, in the locale's own words.",
|
|
7
7
|
"status": "beta",
|
|
8
8
|
"category": "forms",
|
|
9
|
-
"platforms": "
|
|
9
|
+
"platforms": "both",
|
|
10
10
|
"whenToUse": [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
11
|
+
"`DatePicker`: a day near today in a form — a publication, an auction's end, a reminder.",
|
|
12
|
+
"`DateRangePicker`: a period — an earnings report, an audit log — with presets and two months side by side.",
|
|
13
|
+
"`Calendar` inline (`variant=\"card\"`): when the month itself is the interface (availability, a booking).",
|
|
14
|
+
"`DayStrip`: the next days in a row (a delivery day, a session, an agenda) — one thumb away on a phone.",
|
|
15
|
+
"`TimePicker`: wheels to set a time (a reminder); `variant=\"slots\"` to book one among the free times.",
|
|
16
|
+
"`MonthPicker`: a month of a year (a billing month, a card's expiry, a report).",
|
|
17
|
+
"`DateField` (typed) or `DateWheel` (wheels): a memorable date far away — a date of birth with an age gate."
|
|
14
18
|
],
|
|
15
19
|
"whenNotToUse": [
|
|
16
20
|
{
|
|
17
|
-
"when": "For a date of birth or
|
|
21
|
+
"when": "For a date of birth or any date years away: a calendar makes it slow — use `DateField` or `DateWheel`.",
|
|
18
22
|
"use": "field"
|
|
19
23
|
},
|
|
20
24
|
{
|
|
@@ -23,28 +27,33 @@
|
|
|
23
27
|
}
|
|
24
28
|
],
|
|
25
29
|
"bestPractices": [
|
|
26
|
-
"Always pass the person's `locale`:
|
|
27
|
-
"Keep
|
|
28
|
-
"Bound the choice with `min` / `max`
|
|
29
|
-
"Offer presets on a period picker: most people want “Last 30 days”, not two clicks."
|
|
30
|
+
"Always pass the person's `locale`: names, the first day of the week, the order of day · month · year and 12/24 hours follow it.",
|
|
31
|
+
"Keep values as ISO strings in state and in the address (`YYYY-MM-DD`, `YYYY-MM`, `HH:mm`); format them only for display.",
|
|
32
|
+
"Bound the choice with `min` / `max`, `isDateDisabled` or `isTimeDisabled` rather than refusing after the fact — an age gate is `max={latestBirthDate(18)}`.",
|
|
33
|
+
"Offer presets on a period picker: most people want “Last 30 days”, not two clicks.",
|
|
34
|
+
"Mark the days that have something (`isDateMarked`) on a strip: the dot is read in the day's name too."
|
|
30
35
|
],
|
|
31
36
|
"accessibility": {
|
|
32
37
|
"keyboard": [
|
|
33
38
|
{
|
|
34
39
|
"keys": "Arrow keys",
|
|
35
|
-
"action": "
|
|
40
|
+
"action": "Calendar: move by a day or a week. Month picker: by a month or a row. Day strip and slots: move and pick."
|
|
36
41
|
},
|
|
37
42
|
{
|
|
38
43
|
"keys": "Home / End",
|
|
39
|
-
"action": "First or last day of the week."
|
|
44
|
+
"action": "First or last day of the week (calendar), first or last item (strip, slots, wheel)."
|
|
40
45
|
},
|
|
41
46
|
{
|
|
42
47
|
"keys": "Page Up / Page Down",
|
|
43
|
-
"action": "
|
|
48
|
+
"action": "Calendar: previous or next month (with Shift: year). Month picker: year. Wheel: by five values."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"keys": "Up / Down",
|
|
52
|
+
"action": "Wheel: the previous or next value."
|
|
44
53
|
},
|
|
45
54
|
{
|
|
46
55
|
"keys": "Enter / Space",
|
|
47
|
-
"action": "Pick the focused day."
|
|
56
|
+
"action": "Pick the focused day or month."
|
|
48
57
|
},
|
|
49
58
|
{
|
|
50
59
|
"keys": "Esc",
|
|
@@ -52,20 +61,23 @@
|
|
|
52
61
|
}
|
|
53
62
|
],
|
|
54
63
|
"notes": [
|
|
55
|
-
"The APG date picker: the month is a `grid` labelled by its
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"The
|
|
64
|
+
"The APG date picker: the month is a `grid` labelled by its month, one day in the tab order, each day named by its full date; today carries `aria-current=\"date\"`, chosen days `aria-selected`, unavailable days are disabled.",
|
|
65
|
+
"The month's title is a button that opens the months of the year (`aria-expanded`); a change of month is announced by a polite live region.",
|
|
66
|
+
"A wheel is a `spinbutton` (`aria-valuetext` reads the value as words); on React Native an adjustable control — swipe up or down with VoiceOver or TalkBack.",
|
|
67
|
+
"The day strip and the time slots are radio groups: one tab stop, the arrows move and pick, taken or closed items are disabled and skipped.",
|
|
68
|
+
"`DateField` is a fieldset named by its legend, each box named by its part; the error is read with the field (`aria-describedby`).",
|
|
69
|
+
"Nothing moves under reduced motion: the month's slide stops; the wheel's curve is tied to the scroll, not to time."
|
|
59
70
|
]
|
|
60
71
|
},
|
|
61
72
|
"related": [
|
|
62
73
|
"field",
|
|
63
74
|
"popover",
|
|
64
|
-
"filter-bar"
|
|
75
|
+
"filter-bar",
|
|
76
|
+
"form"
|
|
65
77
|
],
|
|
66
78
|
"web": {
|
|
67
79
|
"entry": "@krizaka/ui/date-picker",
|
|
68
|
-
"import": "import { DatePicker, DateRangePicker, Calendar } from \"@krizaka/ui/date-picker\";",
|
|
80
|
+
"import": "import { DatePicker, DateRangePicker, Calendar, MonthPicker, DayStrip, WheelPicker, Wheel, TimePicker, DateField, DateWheel, latestBirthDate, timeSlots } from \"@krizaka/ui/date-picker\";",
|
|
69
81
|
"props": [
|
|
70
82
|
{
|
|
71
83
|
"component": "Calendar",
|
|
@@ -131,7 +143,14 @@
|
|
|
131
143
|
"name": "month",
|
|
132
144
|
"type": "string",
|
|
133
145
|
"default": null,
|
|
134
|
-
"description": "The month shown, any day of it (controlled).",
|
|
146
|
+
"description": "The month shown (the first one, with `numberOfMonths={2}`), any day of it (controlled).",
|
|
147
|
+
"required": false
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "numberOfMonths",
|
|
151
|
+
"type": "1 | 2",
|
|
152
|
+
"default": "1",
|
|
153
|
+
"description": "One month, or two side by side (a period: from a phone's width up, they stack on a phone).",
|
|
135
154
|
"required": false
|
|
136
155
|
},
|
|
137
156
|
{
|
|
@@ -152,7 +171,7 @@
|
|
|
152
171
|
"name": "range",
|
|
153
172
|
"type": "DateRange | null",
|
|
154
173
|
"default": null,
|
|
155
|
-
"description": "The chosen range (range mode): its ends are
|
|
174
|
+
"description": "The chosen range (range mode): its ends are lit, the days between are one band.",
|
|
156
175
|
"required": false
|
|
157
176
|
},
|
|
158
177
|
{
|
|
@@ -169,6 +188,13 @@
|
|
|
169
188
|
"description": "The chosen date (one-date mode).",
|
|
170
189
|
"required": false
|
|
171
190
|
},
|
|
191
|
+
{
|
|
192
|
+
"name": "variant",
|
|
193
|
+
"type": "\"plain\" | \"card\"",
|
|
194
|
+
"default": null,
|
|
195
|
+
"description": "`plain` (inside a popover, a card of the app) or `card` (the calendar is the interface: its own raised surface).",
|
|
196
|
+
"required": false
|
|
197
|
+
},
|
|
172
198
|
{
|
|
173
199
|
"name": "weekStartsOn",
|
|
174
200
|
"type": "number",
|
|
@@ -178,6 +204,207 @@
|
|
|
178
204
|
}
|
|
179
205
|
]
|
|
180
206
|
},
|
|
207
|
+
{
|
|
208
|
+
"component": "DateField",
|
|
209
|
+
"description": "Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full.",
|
|
210
|
+
"props": [
|
|
211
|
+
{
|
|
212
|
+
"name": "label",
|
|
213
|
+
"type": "string",
|
|
214
|
+
"default": null,
|
|
215
|
+
"description": "The visible label, passed translated (the group's name).",
|
|
216
|
+
"required": true
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "locale",
|
|
220
|
+
"type": "string",
|
|
221
|
+
"default": null,
|
|
222
|
+
"description": "The BCP 47 locale: the order of the parts, the names of the months.",
|
|
223
|
+
"required": true
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "autoComplete",
|
|
227
|
+
"type": "\"off\" | \"bday\"",
|
|
228
|
+
"default": null,
|
|
229
|
+
"description": "`bday` for a date of birth: the browser offers to fill it.",
|
|
230
|
+
"required": false
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "className",
|
|
234
|
+
"type": "string",
|
|
235
|
+
"default": null,
|
|
236
|
+
"description": "Classes merged on the root.",
|
|
237
|
+
"required": false
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"name": "defaultValue",
|
|
241
|
+
"type": "string",
|
|
242
|
+
"default": "",
|
|
243
|
+
"description": "The date at first, uncontrolled.",
|
|
244
|
+
"required": false
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "disabled",
|
|
248
|
+
"type": "boolean",
|
|
249
|
+
"default": null,
|
|
250
|
+
"description": "Not available.",
|
|
251
|
+
"required": false
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"name": "error",
|
|
255
|
+
"type": "string",
|
|
256
|
+
"default": null,
|
|
257
|
+
"description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
|
|
258
|
+
"required": false
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "hint",
|
|
262
|
+
"type": "string",
|
|
263
|
+
"default": null,
|
|
264
|
+
"description": "A help text under the field (\"For example, 27 3 1990\").",
|
|
265
|
+
"required": false
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "labels",
|
|
269
|
+
"type": "Partial<DateFieldLabels>",
|
|
270
|
+
"default": null,
|
|
271
|
+
"description": "The names of the parts, passed translated; English by default.",
|
|
272
|
+
"required": false
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "name",
|
|
276
|
+
"type": "string",
|
|
277
|
+
"default": null,
|
|
278
|
+
"description": "The name of a hidden input holding the value, for a plain `<form>` post.",
|
|
279
|
+
"required": false
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"name": "onBlur",
|
|
283
|
+
"type": "(() => void)",
|
|
284
|
+
"default": null,
|
|
285
|
+
"description": "Called when the focus leaves the field.",
|
|
286
|
+
"required": false
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "onValueChange",
|
|
290
|
+
"type": "((value: string) => void)",
|
|
291
|
+
"default": null,
|
|
292
|
+
"description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
|
|
293
|
+
"required": false
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "value",
|
|
297
|
+
"type": "string",
|
|
298
|
+
"default": null,
|
|
299
|
+
"description": "The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides.",
|
|
300
|
+
"required": false
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"component": "DateWheel",
|
|
306
|
+
"description": "Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date.",
|
|
307
|
+
"props": [
|
|
308
|
+
{
|
|
309
|
+
"name": "label",
|
|
310
|
+
"type": "string",
|
|
311
|
+
"default": null,
|
|
312
|
+
"description": "The visible label, passed translated (the group's name).",
|
|
313
|
+
"required": true
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "locale",
|
|
317
|
+
"type": "string",
|
|
318
|
+
"default": null,
|
|
319
|
+
"description": "The BCP 47 locale: the order of the parts, the names of the months.",
|
|
320
|
+
"required": true
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "className",
|
|
324
|
+
"type": "string",
|
|
325
|
+
"default": null,
|
|
326
|
+
"description": "Classes merged on the root.",
|
|
327
|
+
"required": false
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"name": "defaultValue",
|
|
331
|
+
"type": "string | null",
|
|
332
|
+
"default": "null",
|
|
333
|
+
"description": "The date at first, uncontrolled.",
|
|
334
|
+
"required": false
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"name": "defaultView",
|
|
338
|
+
"type": "string",
|
|
339
|
+
"default": null,
|
|
340
|
+
"description": "Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed).",
|
|
341
|
+
"required": false
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "disabled",
|
|
345
|
+
"type": "boolean",
|
|
346
|
+
"default": null,
|
|
347
|
+
"description": "Not available.",
|
|
348
|
+
"required": false
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "error",
|
|
352
|
+
"type": "string",
|
|
353
|
+
"default": null,
|
|
354
|
+
"description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
|
|
355
|
+
"required": false
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"name": "hint",
|
|
359
|
+
"type": "string",
|
|
360
|
+
"default": null,
|
|
361
|
+
"description": "A help text under the field (\"For example, 27 3 1990\").",
|
|
362
|
+
"required": false
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"name": "labels",
|
|
366
|
+
"type": "Partial<DateFieldLabels>",
|
|
367
|
+
"default": null,
|
|
368
|
+
"description": "The names of the parts, passed translated; English by default.",
|
|
369
|
+
"required": false
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"name": "max",
|
|
373
|
+
"type": "string",
|
|
374
|
+
"default": null,
|
|
375
|
+
"description": "The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today.",
|
|
376
|
+
"required": false
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "min",
|
|
380
|
+
"type": "string",
|
|
381
|
+
"default": null,
|
|
382
|
+
"description": "The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago.",
|
|
383
|
+
"required": false
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "name",
|
|
387
|
+
"type": "string",
|
|
388
|
+
"default": null,
|
|
389
|
+
"description": "The name of a hidden input holding the value, for a plain `<form>` post.",
|
|
390
|
+
"required": false
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "onValueChange",
|
|
394
|
+
"type": "((value: string) => void)",
|
|
395
|
+
"default": null,
|
|
396
|
+
"description": "Called with the date under the band, always a real date within `min` / `max`.",
|
|
397
|
+
"required": false
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"name": "value",
|
|
401
|
+
"type": "string | null",
|
|
402
|
+
"default": null,
|
|
403
|
+
"description": "The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`).",
|
|
404
|
+
"required": false
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
},
|
|
181
408
|
{
|
|
182
409
|
"component": "DatePicker",
|
|
183
410
|
"description": "A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value.",
|
|
@@ -433,6 +660,13 @@
|
|
|
433
660
|
"description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
|
|
434
661
|
"required": false
|
|
435
662
|
},
|
|
663
|
+
{
|
|
664
|
+
"name": "numberOfMonths",
|
|
665
|
+
"type": "1 | 2",
|
|
666
|
+
"default": "2",
|
|
667
|
+
"description": "Two months side by side (default, from a phone's width up) or one.",
|
|
668
|
+
"required": false
|
|
669
|
+
},
|
|
436
670
|
{
|
|
437
671
|
"name": "onValueChange",
|
|
438
672
|
"type": "((value: { start: string; end: string; } | null) => void)",
|
|
@@ -476,99 +710,1138 @@
|
|
|
476
710
|
"required": false
|
|
477
711
|
}
|
|
478
712
|
]
|
|
479
|
-
}
|
|
480
|
-
],
|
|
481
|
-
"examples": [
|
|
482
|
-
{
|
|
483
|
-
"name": "schedule",
|
|
484
|
-
"title": "Schedule",
|
|
485
|
-
"description": "A publication date, from today on, written the way the locale writes it.",
|
|
486
|
-
"path": "examples/date-picker/schedule.tsx",
|
|
487
|
-
"module": "@krizaka/ui/registry/examples/date-picker/schedule",
|
|
488
|
-
"code": "\"use client\";\n\nimport { DatePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// `today` is fixed here for the documentation; leave it out in an app.\nexport default function DatePickerSchedule() {\n const [date, setDate] = React.useState<string | null>(\"2026-10-14\");\n return (\n <DatePicker\n label=\"Publish on\"\n locale=\"en-US\"\n value={date}\n onValueChange={setDate}\n min=\"2026-10-11\"\n today=\"2026-10-11\"\n clearable\n placeholder=\"Publish now\"\n hint=\"Your followers are told when it goes live.\"\n />\n );\n}\n"
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
"name": "open",
|
|
492
|
-
"title": "Calendar open",
|
|
493
|
-
"description": "The popover open on the month: today marked, the chosen day filled.",
|
|
494
|
-
"path": "examples/date-picker/open.tsx",
|
|
495
|
-
"module": "@krizaka/ui/registry/examples/date-picker/open",
|
|
496
|
-
"code": "\"use client\";\n\nimport { DatePicker } from \"@krizaka/ui/date-picker\";\n\n// The calendar open: today marked, the chosen day filled, the past unavailable.\nexport default function DatePickerOpen({ defaultOpen }: { defaultOpen?: boolean }) {\n return <DatePicker label=\"Auction ends\" locale=\"en-US\" defaultValue=\"2026-10-18\" min=\"2026-10-11\" today=\"2026-10-11\" defaultOpen={defaultOpen} />;\n}\n"
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
"name": "range",
|
|
500
|
-
"title": "Period with presets",
|
|
501
|
-
"description": "`DateRangePicker` for an earnings report: presets beside the calendar.",
|
|
502
|
-
"path": "examples/date-picker/range.tsx",
|
|
503
|
-
"module": "@krizaka/ui/registry/examples/date-picker/range",
|
|
504
|
-
"code": "\"use client\";\n\nimport { DateRangePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// An earnings report: presets for the usual periods, two clicks for any other.\nexport default function DatePickerRange({ defaultOpen }: { defaultOpen?: boolean }) {\n const [period, setPeriod] = React.useState<{ start: string; end: string } | null>({ start: \"2026-10-01\", end: \"2026-10-11\" });\n return (\n <DateRangePicker\n label=\"Period\"\n locale=\"en-US\"\n value={period}\n onValueChange={setPeriod}\n max=\"2026-10-11\"\n today=\"2026-10-11\"\n defaultOpen={defaultOpen}\n presets={[\n { label: \"Last 7 days\", start: \"2026-10-05\", end: \"2026-10-11\" },\n { label: \"This month\", start: \"2026-10-01\", end: \"2026-10-11\" },\n { label: \"Last month\", start: \"2026-09-01\", end: \"2026-09-30\" },\n ]}\n />\n );\n}\n"
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
"name": "french",
|
|
508
|
-
"title": "In French",
|
|
509
|
-
"description": "`locale=\"fr-CA\"`: the names, the order and the first day of the week follow.",
|
|
510
|
-
"path": "examples/date-picker/french.tsx",
|
|
511
|
-
"module": "@krizaka/ui/registry/examples/date-picker/french",
|
|
512
|
-
"code": "import { DatePicker } from \"@krizaka/ui/date-picker\";\n\n// fr-CA: “11 oct. 2026”, the months and days in French, the week as Canada counts it.\nexport default function DatePickerFrench() {\n return (\n <DatePicker\n label=\"Date du rendez-vous\"\n locale=\"fr-CA\"\n defaultValue=\"2026-10-11\"\n today=\"2026-10-11\"\n labels={{ previousMonth: \"Mois précédent\", nextMonth: \"Mois suivant\", clear: \"Effacer la date\" }}\n />\n );\n}\n"
|
|
513
713
|
},
|
|
514
714
|
{
|
|
515
|
-
"
|
|
516
|
-
"
|
|
517
|
-
"
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
"content": "// Client. A month as an ARIA grid (the APG date picker): one day in the tab order, the arrows walk the days, Home /\n// End the week, Page Up / Page Down the months (Shift: the years), Enter or Space picks. Dates are ISO calendar dates\n// (`\"2026-10-11\"`): no time zone, nothing to convert, safe in an address. Month and day names come from\n// `@krizaka/intl` for the `locale` given — never the runtime's default.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { ChevronGlyph } from \"../glyphs\";\n\n/** A calendar date, `YYYY-MM-DD`. */\nexport type IsoDate = string;\n/** A range of calendar dates; `end` is `null` while the second date is not chosen. */\nexport type DateRange = { start: IsoDate; end: IsoDate | null };\n\nexport const calendar = tv({\n slots: {\n root: \"w-[17.5rem] select-none text-sm text-fg\",\n header: \"mb-2 flex items-center justify-between gap-2\",\n title: \"text-sm font-semibold capitalize\",\n nav:\n \"inline-flex h-8 w-8 items-center justify-center rounded-md text-fg-secondary hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\",\n grid: \"w-full border-collapse\",\n weekday: \"h-8 w-10 text-center text-[11px] font-semibold uppercase text-fg-secondary\",\n cell: \"p-0 text-center\",\n day:\n \"relative h-10 w-10 rounded-lg text-sm tabular-nums text-fg transition-colors hover:bg-surface-3 \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"data-[outside]:text-fg-secondary data-[today]:font-bold data-[today]:not-data-[selected]:text-fg-accent \" +\n \"data-[in-range]:rounded-none data-[in-range]:bg-accent-soft \" +\n \"data-[selected]:bg-accent data-[selected]:text-on-accent data-[selected]:hover:bg-accent \" +\n \"disabled:cursor-not-allowed disabled:text-fg-muted disabled:line-through disabled:opacity-50 disabled:hover:bg-transparent\",\n },\n});\n\nexport const pad = (n: number) => String(n).padStart(2, \"0\");\nconst parts = (iso: IsoDate) => iso.split(\"-\").map(Number) as [number, number, number];\n/** A local `Date` at midnight for an ISO date (for formatting). */\nexport const toLocalDate = (iso: IsoDate) => {\n const [y, m, d] = parts(iso);\n return new Date(y, m - 1, d);\n};\n/** The ISO date of a local `Date`. */\nexport const toIsoDate = (date: Date): IsoDate => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\nexport const addDays = (iso: IsoDate, days: number) => {\n const date = toLocalDate(iso);\n date.setDate(date.getDate() + days);\n return toIsoDate(date);\n};\nexport const addMonths = (iso: IsoDate, months: number) => {\n const [y, m, d] = parts(iso);\n const first = new Date(y, m - 1 + months, 1);\n const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();\n return toIsoDate(new Date(first.getFullYear(), first.getMonth(), Math.min(d, last)));\n};\nconst monthOf = (iso: IsoDate) => `${iso.slice(0, 7)}-01`;\nconst clampIso = (iso: IsoDate, min?: IsoDate, max?: IsoDate) => (min && iso < min ? min : max && iso > max ? max : iso);\n\n/** The first day of the week for a locale (0 = Sunday … 6 = Saturday), from `Intl.Locale` week info; Monday otherwise. */\nexport function weekStartOf(locale: string): number {\n try {\n const info = new Intl.Locale(locale) as Intl.Locale & { getWeekInfo?: () => { firstDay: number }; weekInfo?: { firstDay: number } };\n const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;\n return firstDay ? firstDay % 7 : 1;\n } catch {\n return 1;\n }\n}\n\nexport type CalendarLabels = {\n /** The previous-month button (\"Previous month\"). */\n previousMonth: string;\n /** The next-month button (\"Next month\"). */\n nextMonth: string;\n};\n\nexport const calendarLabels: CalendarLabels = { previousMonth: \"Previous month\", nextMonth: \"Next month\" };\n\nexport type CalendarProps = {\n /** The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit. */\n locale: string;\n /** The chosen date (one-date mode). */\n value?: IsoDate | null;\n /** The chosen range (range mode): its ends are marked, the days between tinted. */\n range?: DateRange | null;\n /** Called with the day picked. */\n onSelect?: (date: IsoDate) => void;\n /** The month shown, any day of it (controlled). */\n month?: IsoDate;\n /** The month shown at first. Default: the value's, or today's. */\n defaultMonth?: IsoDate;\n /** Called when the month shown changes. */\n onMonthChange?: (month: IsoDate) => void;\n /** The earliest date that can be picked. */\n min?: IsoDate;\n /** The latest date that can be picked. */\n max?: IsoDate;\n /** Refuses some dates (`(d) => isWeekend(d)`). */\n isDateDisabled?: (date: IsoDate) => boolean;\n /** The first day of the week, 0 = Sunday. Default: the locale's. */\n weekStartsOn?: number;\n /** Today (marked): inject it for a stable server render or a test. Default: the device's date. */\n today?: IsoDate;\n /** Moves the focus to the active day when it mounts (inside a popover). */\n autoFocus?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<CalendarLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** A month grid to pick a date or the ends of a range, with the full keyboard of the APG date picker. */\nexport function Calendar({\n locale,\n value,\n range,\n onSelect,\n month: monthProp,\n defaultMonth,\n onMonthChange,\n min,\n max,\n isDateDisabled,\n weekStartsOn,\n today: todayProp,\n autoFocus,\n labels: given,\n className,\n}: CalendarProps) {\n const labels = { ...calendarLabels, ...given };\n const s = calendar();\n const today = todayProp ?? toIsoDate(new Date());\n const anchor = value ?? range?.start ?? clampIso(today, min, max);\n const [innerMonth, setInnerMonth] = React.useState(monthOf(defaultMonth ?? anchor));\n const month = monthProp ? monthOf(monthProp) : innerMonth;\n const [focused, setFocused] = React.useState<IsoDate>(anchor.slice(0, 7) === month.slice(0, 7) ? anchor : month);\n const [hover, setHover] = React.useState<IsoDate | null>(null);\n const grid = React.useRef<HTMLTableElement>(null);\n const moved = React.useRef(Boolean(autoFocus));\n const titleId = React.useId();\n\n const setMonth = (next: IsoDate) => {\n const m = monthOf(next);\n if (!monthProp) setInnerMonth(m);\n onMonthChange?.(m);\n };\n // The focused day follows the month shown when it is changed from outside (the buttons, a controlled `month`).\n const active = focused.slice(0, 7) === month.slice(0, 7) ? focused : month;\n\n React.useEffect(() => {\n if (!moved.current) return;\n moved.current = false;\n grid.current?.querySelector<HTMLButtonElement>(`[data-date=\"${active}\"]`)?.focus();\n }, [active]);\n\n const disabled = (iso: IsoDate) => Boolean((min && iso < min) || (max && iso > max) || isDateDisabled?.(iso));\n const start = weekStartsOn ?? weekStartOf(locale);\n const first = toLocalDate(month);\n const lead = (first.getDay() - start + 7) % 7;\n const gridStart = addDays(month, -lead);\n const days = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));\n const weeks = Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7)).filter((week, w) => w < 5 || week[0]!.slice(0, 7) === month.slice(0, 7));\n const weekdayShort = getDateTimeFormat(locale, { weekday: \"short\" });\n const weekdayLong = getDateTimeFormat(locale, { weekday: \"long\" });\n const title = getDateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(first);\n\n const rangeEnd = range ? (range.end ?? hover) : null;\n const [lo, hi] = range && rangeEnd ? (range.start <= rangeEnd ? [range.start, rangeEnd] : [rangeEnd, range.start]) : [null, null];\n\n const focusDay = (iso: IsoDate) => {\n moved.current = true;\n setFocused(iso);\n if (iso.slice(0, 7) !== month.slice(0, 7)) setMonth(iso);\n };\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const weekday = (toLocalDate(active).getDay() - start + 7) % 7;\n const moves: Record<string, () => IsoDate> = {\n ArrowLeft: () => addDays(active, -1),\n ArrowRight: () => addDays(active, 1),\n ArrowUp: () => addDays(active, -7),\n ArrowDown: () => addDays(active, 7),\n Home: () => addDays(active, -weekday),\n End: () => addDays(active, 6 - weekday),\n PageUp: () => addMonths(active, event.shiftKey ? -12 : -1),\n PageDown: () => addMonths(active, event.shiftKey ? 12 : 1),\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n focusDay(move());\n };\n\n const prevDisabled = Boolean(min && addDays(month, -1) < min);\n const nextDisabled = Boolean(max && addMonths(month, 1) > max);\n\n return (\n <div className={s.root({ className })}>\n <div className={s.header()}>\n <button type=\"button\" className={s.nav()} aria-label={labels.previousMonth} disabled={prevDisabled} onClick={() => setMonth(addMonths(month, -1))}>\n <ChevronGlyph direction=\"left\" />\n </button>\n <h2 id={titleId} className={s.title()} aria-live=\"polite\">\n {title}\n </h2>\n <button type=\"button\" className={s.nav()} aria-label={labels.nextMonth} disabled={nextDisabled} onClick={() => setMonth(addMonths(month, 1))}>\n <ChevronGlyph direction=\"right\" />\n </button>\n </div>\n <table ref={grid} role=\"grid\" aria-labelledby={titleId} className={s.grid()} onKeyDown={onKeyDown}>\n <thead>\n <tr>\n {days.slice(0, 7).map((iso) => (\n <th key={iso} scope=\"col\" abbr={weekdayLong.format(toLocalDate(iso))} className={s.weekday()}>\n {weekdayShort.format(toLocalDate(iso)).replace(/\\.$/, \"\")}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {weeks.map((week) => (\n <tr key={week[0]}>\n {week.map((iso) => {\n const selected = iso === value || (range && (iso === range.start || iso === range.end));\n const inRange = Boolean(lo && hi && iso > lo && iso < hi);\n return (\n <td key={iso} role=\"gridcell\" aria-selected={selected || inRange || undefined} className={s.cell()}>\n <button\n type=\"button\"\n data-date={iso}\n tabIndex={iso === active ? 0 : -1}\n aria-label={formatDate(toLocalDate(iso), { locale, style: \"long\" })}\n aria-current={iso === today ? \"date\" : undefined}\n data-today={iso === today ? \"\" : undefined}\n data-outside={iso.slice(0, 7) !== month.slice(0, 7) ? \"\" : undefined}\n data-selected={selected ? \"\" : undefined}\n data-in-range={inRange ? \"\" : undefined}\n disabled={disabled(iso)}\n className={s.day()}\n onClick={() => {\n setFocused(iso);\n onSelect?.(iso);\n }}\n onMouseEnter={range && !range.end ? () => setHover(iso) : undefined}\n onFocus={range && !range.end ? () => setHover(iso) : undefined}\n >\n {Number(iso.slice(8))}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n"
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
"content": "// Client (a portal). A field-like button that opens the `Calendar` in a Popover (Radix: Escape closes, the focus\n// returns to the button). The value is an ISO date (`\"2026-10-11\"`) — the same string in a form, an address and an\n// API; the button reads it in the `locale`'s own format through `@krizaka/intl`.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { CalendarGlyph, CloseGlyph } from \"../glyphs\";\nimport { useControllable } from \"../media\";\nimport { Popover } from \"../popover\";\nimport { Calendar, type CalendarLabels, type CalendarProps, type DateRange, type IsoDate, toLocalDate } from \"./calendar\";\n\nexport const datePicker = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n label: \"text-xs font-semibold text-fg-secondary\",\n row: \"relative flex w-full\",\n trigger:\n \"flex h-11 w-full items-center gap-2.5 rounded-lg border border-border-default bg-surface-2 px-3 text-left text-sm text-fg \" +\n \"transition-[border-color,box-shadow] focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring \" +\n \"data-[placeholder]:text-fg-muted aria-invalid:border-danger disabled:cursor-not-allowed disabled:opacity-50 data-[clearable]:pr-10\",\n icon: \"h-4 w-4 shrink-0 text-fg-secondary\",\n clear:\n \"absolute right-1.5 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-fg-secondary hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n content: \"w-auto p-3\",\n layout: \"flex flex-col gap-3 sm:flex-row\",\n presets: \"flex flex-wrap gap-1.5 sm:w-36 sm:flex-col sm:flex-nowrap sm:border-r sm:border-border-default sm:pr-3\",\n preset:\n \"rounded-md px-2.5 py-1.5 text-left text-xs font-semibold text-fg-secondary hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-accent-soft aria-pressed:text-fg\",\n hint: \"text-xs text-fg-secondary\",\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n },\n});\n\nexport type DatePickerLabels = CalendarLabels & {\n /** The clear button (\"Clear date\"). */\n clear: string;\n};\n\nexport const datePickerLabels: DatePickerLabels = { previousMonth: \"Previous month\", nextMonth: \"Next month\", clear: \"Clear date\" };\n\ntype Shared = Pick<CalendarProps, \"locale\" | \"min\" | \"max\" | \"isDateDisabled\" | \"weekStartsOn\" | \"today\"> & {\n /** The visible label, passed translated (also the button's accessible name, with the value). */\n label: string;\n /** Shown while nothing is chosen (\"Pick a date\"), passed translated. */\n placeholder?: string;\n /** `short` · `medium` (default) · `long`: how the button writes the date. */\n format?: \"short\" | \"medium\" | \"long\";\n /** Adds a button that empties the value. */\n clearable?: boolean;\n /** A help text under the field. */\n hint?: string;\n /** An error under the field: marks it invalid and is read with it. */\n error?: string;\n /** Not available. */\n disabled?: boolean;\n /** The name of a hidden input holding the ISO value, for a plain `<form>` post. */\n name?: string;\n /** Opens the calendar at first (a story, a test). */\n defaultOpen?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<DatePickerLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type DatePickerProps = Shared & {\n /** The chosen date, ISO (controlled); `null` for none. */\n value?: IsoDate | null;\n /** The chosen date at first, uncontrolled. */\n defaultValue?: IsoDate | null;\n /** Called with the date picked, or `null` when cleared. */\n onValueChange?: (value: IsoDate | null) => void;\n};\n\nfunction Field({\n shared,\n text,\n hasValue,\n onClear,\n hidden,\n open,\n setOpen,\n children,\n}: {\n shared: Shared;\n text: string | null;\n hasValue: boolean;\n onClear: () => void;\n hidden: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n children: React.ReactNode;\n}) {\n const { label, placeholder, clearable, hint, error, disabled, name, className } = shared;\n const labels = { ...datePickerLabels, ...shared.labels };\n const s = datePicker();\n const id = React.useId();\n const describedBy = [hint ? `${id}-hint` : null, error ? `${id}-error` : null].filter(Boolean).join(\" \") || undefined;\n return (\n <div className={s.root({ className })}>\n <label htmlFor={`${id}-trigger`} className={s.label()}>\n {label}\n </label>\n <Popover.Root open={open} onOpenChange={setOpen}>\n <div className={s.row()}>\n <Popover.Trigger asChild>\n <button\n id={`${id}-trigger`}\n type=\"button\"\n aria-haspopup=\"dialog\"\n aria-describedby={describedBy}\n aria-invalid={error ? true : undefined}\n disabled={disabled}\n data-placeholder={hasValue ? undefined : \"\"}\n data-clearable={clearable && hasValue ? \"\" : undefined}\n className={s.trigger()}\n >\n <CalendarGlyph className={s.icon()} />\n <span className=\"truncate\">{text ?? placeholder ?? \"\"}</span>\n </button>\n </Popover.Trigger>\n {clearable && hasValue && !disabled ? (\n <button type=\"button\" aria-label={labels.clear} className={s.clear()} onClick={onClear}>\n <CloseGlyph className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n <Popover.Content align=\"start\" aria-label={label} className={s.content()}>\n {children}\n </Popover.Content>\n </Popover.Root>\n {name ? <input type=\"hidden\" name={name} value={hidden} /> : null}\n {hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null}\n {error ? (\n <p id={`${id}-error`} className={s.error()}>\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value. */\nexport function DatePicker({ value: valueProp, defaultValue = null, onValueChange, defaultOpen = false, ...shared }: DatePickerProps) {\n const [value, setValue] = useControllable<IsoDate | null>(valueProp, defaultValue, onValueChange);\n const [open, setOpen] = React.useState(defaultOpen);\n const { locale, format = \"medium\" } = shared;\n return (\n <Field\n shared={shared}\n text={value ? formatDate(toLocalDate(value), { locale, style: format }) : null}\n hasValue={Boolean(value)}\n onClear={() => setValue(null)}\n hidden={value ?? \"\"}\n open={open}\n setOpen={setOpen}\n >\n <Calendar\n {...shared}\n autoFocus\n value={value}\n onSelect={(date) => {\n setValue(date);\n setOpen(false);\n }}\n />\n </Field>\n );\n}\n\nexport type DateRangePreset = {\n /** Its name, passed translated (\"Last 7 days\"). */\n label: string;\n /** The first day, ISO. */\n start: IsoDate;\n /** The last day, ISO. */\n end: IsoDate;\n};\n\nexport type DateRangePickerProps = Shared & {\n /** The chosen range (controlled); `null` for none. `end` is never `null` in a committed value. */\n value?: { start: IsoDate; end: IsoDate } | null;\n /** The range at first, uncontrolled. */\n defaultValue?: { start: IsoDate; end: IsoDate } | null;\n /** Called once both ends are chosen (or with `null` when cleared). */\n onValueChange?: (value: { start: IsoDate; end: IsoDate } | null) => void;\n /** Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar. */\n presets?: readonly DateRangePreset[];\n};\n\n/** Formats a range as the locale writes one (\"Oct 1 – 11, 2026\"), through Intl's `formatRange` where available. */\nfunction formatRange(locale: string, start: IsoDate, end: IsoDate, style: \"short\" | \"medium\" | \"long\") {\n const formatter = getDateTimeFormat(locale, { dateStyle: style }) as Intl.DateTimeFormat & { formatRange?: (a: Date, b: Date) => string };\n return formatter.formatRange\n ? formatter.formatRange(toLocalDate(start), toLocalDate(end))\n : `${formatter.format(toLocalDate(start))} – ${formatter.format(toLocalDate(end))}`;\n}\n\n/** A period field: two clicks on one calendar (the days between tinted as you hover), optional presets. */\nexport function DateRangePicker({ value: valueProp, defaultValue = null, onValueChange, presets, defaultOpen = false, ...shared }: DateRangePickerProps) {\n const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange);\n const [draft, setDraft] = React.useState<DateRange | null>(null);\n const [open, setOpenState] = React.useState(defaultOpen);\n const { locale, format = \"medium\" } = shared;\n const s = datePicker();\n const setOpen = (next: boolean) => {\n setOpenState(next);\n setDraft(null);\n };\n const shown: DateRange | null = draft ?? value;\n const pick = (date: IsoDate) => {\n if (!draft) setDraft({ start: date, end: null });\n else {\n const [start, end] = date < draft.start ? [date, draft.start] : [draft.start, date];\n setValue({ start, end });\n setOpen(false);\n }\n };\n return (\n <Field\n shared={shared}\n text={value ? formatRange(locale, value.start, value.end, format) : null}\n hasValue={Boolean(value)}\n onClear={() => setValue(null)}\n hidden={value ? `${value.start}/${value.end}` : \"\"}\n open={open}\n setOpen={setOpen}\n >\n <div className={s.layout()}>\n {presets?.length ? (\n <div className={s.presets()}>\n {presets.map((preset) => (\n <button\n key={preset.label}\n type=\"button\"\n aria-pressed={value?.start === preset.start && value?.end === preset.end}\n className={s.preset()}\n onClick={() => {\n setValue({ start: preset.start, end: preset.end });\n setOpen(false);\n }}\n >\n {preset.label}\n </button>\n ))}\n </div>\n ) : null}\n <Calendar {...shared} autoFocus range={shown} onSelect={pick} />\n </div>\n </Field>\n );\n}\n"
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
715
|
+
"component": "DayStrip",
|
|
716
|
+
"description": "The days ahead in a row that scrolls sideways: one picked, today and the marked days shown.",
|
|
717
|
+
"props": [
|
|
718
|
+
{
|
|
719
|
+
"name": "label",
|
|
720
|
+
"type": "string",
|
|
721
|
+
"default": null,
|
|
722
|
+
"description": "The group's accessible name (\"Delivery day\"), passed translated.",
|
|
723
|
+
"required": true
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"name": "locale",
|
|
727
|
+
"type": "string",
|
|
728
|
+
"default": null,
|
|
729
|
+
"description": "The BCP 47 locale of the names and the dates: always explicit.",
|
|
730
|
+
"required": true
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
"name": "className",
|
|
734
|
+
"type": "string",
|
|
735
|
+
"default": null,
|
|
736
|
+
"description": "Classes merged on the root.",
|
|
737
|
+
"required": false
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
"name": "days",
|
|
741
|
+
"type": "number",
|
|
742
|
+
"default": "14",
|
|
743
|
+
"description": "How many days. Default 14.",
|
|
744
|
+
"required": false
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
"name": "defaultValue",
|
|
748
|
+
"type": "string | null",
|
|
749
|
+
"default": "null",
|
|
750
|
+
"description": "The chosen day at first, uncontrolled.",
|
|
751
|
+
"required": false
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"name": "isDateDisabled",
|
|
755
|
+
"type": "((date: string) => boolean)",
|
|
756
|
+
"default": null,
|
|
757
|
+
"description": "Refuses some days (closed, full): drawn dashed, skipped by the arrows.",
|
|
758
|
+
"required": false
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
"name": "isDateMarked",
|
|
762
|
+
"type": "((date: string) => boolean)",
|
|
763
|
+
"default": null,
|
|
764
|
+
"description": "Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name.",
|
|
765
|
+
"required": false
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"name": "labels",
|
|
769
|
+
"type": "Partial<DayStripLabels>",
|
|
770
|
+
"default": null,
|
|
771
|
+
"description": "The words, passed translated; English by default.",
|
|
772
|
+
"required": false
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
"name": "onValueChange",
|
|
776
|
+
"type": "((value: string) => void)",
|
|
777
|
+
"default": null,
|
|
778
|
+
"description": "Called with the day picked.",
|
|
779
|
+
"required": false
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"name": "showHeader",
|
|
783
|
+
"type": "boolean",
|
|
784
|
+
"default": "true",
|
|
785
|
+
"description": "Shows the month of the chosen day (or of the first) and the page buttons above the strip.",
|
|
786
|
+
"required": false
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"name": "start",
|
|
790
|
+
"type": "string",
|
|
791
|
+
"default": null,
|
|
792
|
+
"description": "The first day of the strip. Default: today.",
|
|
793
|
+
"required": false
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
"name": "today",
|
|
797
|
+
"type": "string",
|
|
798
|
+
"default": null,
|
|
799
|
+
"description": "Today (marked by the node): inject it for a stable render or a test. Default: the device's date.",
|
|
800
|
+
"required": false
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
"name": "value",
|
|
804
|
+
"type": "string | null",
|
|
805
|
+
"default": null,
|
|
806
|
+
"description": "The chosen day, ISO (controlled); `null` for none.",
|
|
807
|
+
"required": false
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
"component": "MonthPicker",
|
|
813
|
+
"description": "A month of a year: twelve months in a grid, the year above them.",
|
|
814
|
+
"props": [
|
|
815
|
+
{
|
|
816
|
+
"name": "locale",
|
|
817
|
+
"type": "string",
|
|
818
|
+
"default": null,
|
|
819
|
+
"description": "The BCP 47 locale of the month names: always explicit.",
|
|
820
|
+
"required": true
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
"name": "autoFocus",
|
|
824
|
+
"type": "boolean",
|
|
825
|
+
"default": null,
|
|
826
|
+
"description": "Moves the focus to the active month when it mounts.",
|
|
827
|
+
"required": false
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
"name": "bare",
|
|
831
|
+
"type": "boolean",
|
|
832
|
+
"default": null,
|
|
833
|
+
"description": "Without its own year header (inside the calendar, which has one).",
|
|
834
|
+
"required": false
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
"name": "className",
|
|
838
|
+
"type": "string",
|
|
839
|
+
"default": null,
|
|
840
|
+
"description": "Classes merged on the root.",
|
|
841
|
+
"required": false
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
"name": "current",
|
|
845
|
+
"type": "string",
|
|
846
|
+
"default": null,
|
|
847
|
+
"description": "This month (marked): inject it for a stable render or a test. Default: the device's.",
|
|
848
|
+
"required": false
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"name": "defaultValue",
|
|
852
|
+
"type": "string | null",
|
|
853
|
+
"default": "null",
|
|
854
|
+
"description": "The chosen month at first, uncontrolled.",
|
|
855
|
+
"required": false
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
"name": "labels",
|
|
859
|
+
"type": "Partial<MonthPickerLabels>",
|
|
860
|
+
"default": null,
|
|
861
|
+
"description": "The words, passed translated; English by default.",
|
|
862
|
+
"required": false
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
"name": "max",
|
|
866
|
+
"type": "string",
|
|
867
|
+
"default": null,
|
|
868
|
+
"description": "The latest month that can be picked, `YYYY-MM`.",
|
|
869
|
+
"required": false
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
"name": "min",
|
|
873
|
+
"type": "string",
|
|
874
|
+
"default": null,
|
|
875
|
+
"description": "The earliest month that can be picked, `YYYY-MM`.",
|
|
876
|
+
"required": false
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
"name": "onValueChange",
|
|
880
|
+
"type": "((value: string | null) => void)",
|
|
881
|
+
"default": null,
|
|
882
|
+
"description": "Called with the month picked.",
|
|
883
|
+
"required": false
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
"name": "onYearChange",
|
|
887
|
+
"type": "((year: number) => void)",
|
|
888
|
+
"default": null,
|
|
889
|
+
"description": "Called when the year shown changes.",
|
|
890
|
+
"required": false
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"name": "value",
|
|
894
|
+
"type": "string | null",
|
|
895
|
+
"default": null,
|
|
896
|
+
"description": "The chosen month, `YYYY-MM` (controlled); `null` for none.",
|
|
897
|
+
"required": false
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
"name": "variant",
|
|
901
|
+
"type": "\"plain\" | \"card\"",
|
|
902
|
+
"default": null,
|
|
903
|
+
"description": "`plain` or `card` (its own raised surface).",
|
|
904
|
+
"required": false
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"name": "year",
|
|
908
|
+
"type": "number",
|
|
909
|
+
"default": null,
|
|
910
|
+
"description": "The year shown (controlled).",
|
|
911
|
+
"required": false
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"component": "TimePicker",
|
|
917
|
+
"description": "A time of day: wheels to set one, or slots to book one.",
|
|
918
|
+
"props": [
|
|
919
|
+
{
|
|
920
|
+
"name": "label",
|
|
921
|
+
"type": "string",
|
|
922
|
+
"default": null,
|
|
923
|
+
"description": "The accessible name of the picker (\"Pick-up time\"), passed translated.",
|
|
924
|
+
"required": true
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"name": "locale",
|
|
928
|
+
"type": "string",
|
|
929
|
+
"default": null,
|
|
930
|
+
"description": "The BCP 47 locale: how times are written, and whether the day has a period (AM/PM).",
|
|
931
|
+
"required": true
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"name": "className",
|
|
935
|
+
"type": "string",
|
|
936
|
+
"default": null,
|
|
937
|
+
"description": "Classes merged on the root.",
|
|
938
|
+
"required": false
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
"name": "defaultValue",
|
|
942
|
+
"type": "string | null",
|
|
943
|
+
"default": null,
|
|
944
|
+
"description": "The time at first, uncontrolled.",
|
|
945
|
+
"required": false
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
"name": "disabled",
|
|
949
|
+
"type": "boolean",
|
|
950
|
+
"default": null,
|
|
951
|
+
"description": "Not available.",
|
|
952
|
+
"required": false
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
"name": "groupLabels",
|
|
956
|
+
"type": "{ morning: string; afternoon: string; evening: string; }",
|
|
957
|
+
"default": null,
|
|
958
|
+
"description": "Splits the slots under headings by part of the day: `{ morning: \"Morning\", afternoon: \"Afternoon\", evening: \"Evening\" }`.",
|
|
959
|
+
"required": false
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"name": "hour12",
|
|
963
|
+
"type": "boolean",
|
|
964
|
+
"default": null,
|
|
965
|
+
"description": "Forces 12 or 24 hours. Default: the locale's.",
|
|
966
|
+
"required": false
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
"name": "isTimeDisabled",
|
|
970
|
+
"type": "((time: string) => boolean)",
|
|
971
|
+
"default": null,
|
|
972
|
+
"description": "Refuses some slots (taken, too soon): drawn struck through, skipped by the arrows.",
|
|
973
|
+
"required": false
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
"name": "labels",
|
|
977
|
+
"type": "Partial<TimePickerLabels>",
|
|
978
|
+
"default": null,
|
|
979
|
+
"description": "The words of the wheels, passed translated; English by default.",
|
|
980
|
+
"required": false
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
"name": "minuteStep",
|
|
984
|
+
"type": "number",
|
|
985
|
+
"default": null,
|
|
986
|
+
"description": "The minutes' step: 1, 5 (default), 10, 15, 30.",
|
|
987
|
+
"required": false
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
"name": "onValueChange",
|
|
991
|
+
"type": "((value: string) => void)",
|
|
992
|
+
"default": null,
|
|
993
|
+
"description": "Called with the time chosen.",
|
|
994
|
+
"required": false
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
"name": "slots",
|
|
998
|
+
"type": "readonly string[]",
|
|
999
|
+
"default": null,
|
|
1000
|
+
"description": "The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`).",
|
|
1001
|
+
"required": false
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
"name": "value",
|
|
1005
|
+
"type": "string | null",
|
|
1006
|
+
"default": null,
|
|
1007
|
+
"description": "The time, `HH:mm` on 24 hours (controlled); `null` for none.",
|
|
1008
|
+
"required": false
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
"name": "variant",
|
|
1012
|
+
"type": "\"slots\" | \"wheel\"",
|
|
1013
|
+
"default": null,
|
|
1014
|
+
"description": "`wheel`: hours and minutes (and the period) as wheels — a time you set (a reminder, an alarm).\n`slots`: the free times as buttons — a booking, a delivery window, an appointment.",
|
|
1015
|
+
"required": false
|
|
1016
|
+
}
|
|
1017
|
+
]
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
"component": "Wheel",
|
|
1021
|
+
"description": "One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values.",
|
|
1022
|
+
"props": [
|
|
1023
|
+
{
|
|
1024
|
+
"name": "label",
|
|
1025
|
+
"type": "string",
|
|
1026
|
+
"default": null,
|
|
1027
|
+
"description": "The column's accessible name (\"Hour\", \"Month\"), passed translated.",
|
|
1028
|
+
"required": true
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"name": "options",
|
|
1032
|
+
"type": "readonly WheelOption[]",
|
|
1033
|
+
"default": null,
|
|
1034
|
+
"description": "The values, in order.",
|
|
1035
|
+
"required": true
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
"name": "className",
|
|
1039
|
+
"type": "string",
|
|
1040
|
+
"default": null,
|
|
1041
|
+
"description": "Classes merged on the column (a width: `w-20`).",
|
|
1042
|
+
"required": false
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
"name": "defaultValue",
|
|
1046
|
+
"type": "string",
|
|
1047
|
+
"default": null,
|
|
1048
|
+
"description": "The chosen value at first, uncontrolled. Default: the first one.",
|
|
1049
|
+
"required": false
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
"name": "disabled",
|
|
1053
|
+
"type": "boolean",
|
|
1054
|
+
"default": null,
|
|
1055
|
+
"description": "Not available.",
|
|
1056
|
+
"required": false
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
"name": "onValueChange",
|
|
1060
|
+
"type": "((value: string) => void)",
|
|
1061
|
+
"default": null,
|
|
1062
|
+
"description": "Called with the value that comes to rest under the band.",
|
|
1063
|
+
"required": false
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
"name": "value",
|
|
1067
|
+
"type": "string | null",
|
|
1068
|
+
"default": null,
|
|
1069
|
+
"description": "The chosen value (controlled).",
|
|
1070
|
+
"required": false
|
|
1071
|
+
}
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
"component": "WheelDivider",
|
|
1076
|
+
"description": "A divider between two wheels (`:` between the hours and the minutes).",
|
|
1077
|
+
"props": []
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
"component": "WheelPicker",
|
|
1081
|
+
"description": "The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`.",
|
|
1082
|
+
"props": [
|
|
1083
|
+
{
|
|
1084
|
+
"name": "children",
|
|
1085
|
+
"type": "ReactNode",
|
|
1086
|
+
"default": null,
|
|
1087
|
+
"description": "The wheels (`Wheel`), and dividers (`WheelDivider`) between them.",
|
|
1088
|
+
"required": true
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
"name": "label",
|
|
1092
|
+
"type": "string",
|
|
1093
|
+
"default": null,
|
|
1094
|
+
"description": "The group's accessible name (\"Departure time\"), passed translated.",
|
|
1095
|
+
"required": true
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"name": "aria-describedby",
|
|
1099
|
+
"type": "string",
|
|
1100
|
+
"default": null,
|
|
1101
|
+
"description": "The help text or the error that describes the group (their ids).",
|
|
1102
|
+
"required": false
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
"name": "className",
|
|
1106
|
+
"type": "string",
|
|
1107
|
+
"default": null,
|
|
1108
|
+
"description": "Classes merged on the root.",
|
|
1109
|
+
"required": false
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
"name": "invalid",
|
|
1113
|
+
"type": "boolean",
|
|
1114
|
+
"default": null,
|
|
1115
|
+
"description": "Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`.",
|
|
1116
|
+
"required": false
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
"name": "visible",
|
|
1120
|
+
"type": "3 | 5 | 7",
|
|
1121
|
+
"default": "5",
|
|
1122
|
+
"description": "How many values are visible: 3, 5 (default) or 7.",
|
|
1123
|
+
"required": false
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
}
|
|
1127
|
+
],
|
|
1128
|
+
"examples": [
|
|
1129
|
+
{
|
|
1130
|
+
"name": "schedule",
|
|
1131
|
+
"title": "Schedule",
|
|
1132
|
+
"description": "A publication date, from today on, written the way the locale writes it.",
|
|
1133
|
+
"path": "examples/date-picker/schedule.tsx",
|
|
1134
|
+
"module": "@krizaka/ui/registry/examples/date-picker/schedule",
|
|
1135
|
+
"code": "\"use client\";\n\nimport { DatePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// `today` is fixed here for the documentation; leave it out in an app.\nexport default function DatePickerSchedule() {\n const [date, setDate] = React.useState<string | null>(\"2026-10-14\");\n return (\n <DatePicker\n label=\"Publish on\"\n locale=\"en-US\"\n value={date}\n onValueChange={setDate}\n min=\"2026-10-11\"\n today=\"2026-10-11\"\n clearable\n placeholder=\"Publish now\"\n hint=\"Your followers are told when it goes live.\"\n />\n );\n}\n"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"name": "open",
|
|
1139
|
+
"title": "Calendar open",
|
|
1140
|
+
"description": "The popover open on the month: today's node, the chosen day lit.",
|
|
1141
|
+
"path": "examples/date-picker/open.tsx",
|
|
1142
|
+
"module": "@krizaka/ui/registry/examples/date-picker/open",
|
|
1143
|
+
"code": "\"use client\";\n\nimport { DatePicker } from \"@krizaka/ui/date-picker\";\n\n// The calendar open: today marked, the chosen day filled, the past unavailable.\nexport default function DatePickerOpen({ defaultOpen }: { defaultOpen?: boolean }) {\n return <DatePicker label=\"Auction ends\" locale=\"en-US\" defaultValue=\"2026-10-18\" min=\"2026-10-11\" today=\"2026-10-11\" defaultOpen={defaultOpen} />;\n}\n"
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
"name": "range",
|
|
1147
|
+
"title": "Period with presets",
|
|
1148
|
+
"description": "`DateRangePicker` for an earnings report: presets beside two months, the period as one band.",
|
|
1149
|
+
"path": "examples/date-picker/range.tsx",
|
|
1150
|
+
"module": "@krizaka/ui/registry/examples/date-picker/range",
|
|
1151
|
+
"code": "\"use client\";\n\nimport { DateRangePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// An earnings report: presets for the usual periods, two clicks for any other.\nexport default function DatePickerRange({ defaultOpen }: { defaultOpen?: boolean }) {\n const [period, setPeriod] = React.useState<{ start: string; end: string } | null>({ start: \"2026-10-01\", end: \"2026-10-11\" });\n return (\n <DateRangePicker\n label=\"Period\"\n locale=\"en-US\"\n value={period}\n onValueChange={setPeriod}\n max=\"2026-10-11\"\n today=\"2026-10-11\"\n defaultOpen={defaultOpen}\n presets={[\n { label: \"Last 7 days\", start: \"2026-10-05\", end: \"2026-10-11\" },\n { label: \"This month\", start: \"2026-10-01\", end: \"2026-10-11\" },\n { label: \"Last month\", start: \"2026-09-01\", end: \"2026-09-30\" },\n ]}\n />\n );\n}\n"
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
"name": "french",
|
|
1155
|
+
"title": "In French",
|
|
1156
|
+
"description": "`locale=\"fr-CA\"`: the names, the order and the first day of the week follow.",
|
|
1157
|
+
"path": "examples/date-picker/french.tsx",
|
|
1158
|
+
"module": "@krizaka/ui/registry/examples/date-picker/french",
|
|
1159
|
+
"code": "import { DatePicker } from \"@krizaka/ui/date-picker\";\n\n// fr-CA: “11 oct. 2026”, the months and days in French, the week as Canada counts it.\nexport default function DatePickerFrench() {\n return (\n <DatePicker\n label=\"Date du rendez-vous\"\n locale=\"fr-CA\"\n defaultValue=\"2026-10-11\"\n today=\"2026-10-11\"\n labels={{ previousMonth: \"Mois précédent\", nextMonth: \"Mois suivant\", clear: \"Effacer la date\" }}\n />\n );\n}\n"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"name": "calendar",
|
|
1163
|
+
"title": "Inline calendar",
|
|
1164
|
+
"description": "`Calendar variant=\"card\"`: the month is the interface, weekends unavailable; its title opens the months.",
|
|
1165
|
+
"path": "examples/date-picker/calendar.tsx",
|
|
1166
|
+
"module": "@krizaka/ui/registry/examples/date-picker/calendar",
|
|
1167
|
+
"code": "\"use client\";\n\nimport { Calendar } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\nconst weekend = (iso: string) => [0, 6].includes(new Date(`${iso}T12:00:00`).getDay());\n\n// The month is the interface: a card of its own, weekends unavailable, today marked by the node.\nexport default function DatePickerCalendar() {\n const [day, setDay] = React.useState(\"2026-10-15\");\n return <Calendar variant=\"card\" locale=\"en-GB\" value={day} onSelect={setDay} isDateDisabled={weekend} min=\"2026-10-12\" today=\"2026-10-11\" />;\n}\n"
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
"name": "month-year",
|
|
1171
|
+
"title": "Month and year",
|
|
1172
|
+
"description": "`MonthPicker`: a billing month, nothing later than this one.",
|
|
1173
|
+
"path": "examples/date-picker/month-year.tsx",
|
|
1174
|
+
"module": "@krizaka/ui/registry/examples/date-picker/month-year",
|
|
1175
|
+
"code": "\"use client\";\n\nimport { MonthPicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// A billing month: twelve months at a glance, the year above them, nothing later than this month.\nexport default function DatePickerMonthYear() {\n const [month, setMonth] = React.useState<string | null>(\"2026-09\");\n return <MonthPicker variant=\"card\" locale=\"en-US\" value={month} onValueChange={setMonth} max=\"2026-10\" current=\"2026-10\" />;\n}\n"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"name": "day-strip",
|
|
1179
|
+
"title": "Day strip",
|
|
1180
|
+
"description": "`DayStrip`: the next two weeks in a row, closed days dashed, the days with a session marked.",
|
|
1181
|
+
"path": "examples/date-picker/day-strip.tsx",
|
|
1182
|
+
"module": "@krizaka/ui/registry/examples/date-picker/day-strip",
|
|
1183
|
+
"code": "\"use client\";\n\nimport { DayStrip } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\nconst closed = (iso: string) => new Date(`${iso}T12:00:00`).getDay() === 0;\nconst sessions = new Set([\"2026-10-14\", \"2026-10-17\", \"2026-10-21\"]);\n\n// The next two weeks in a row: Sundays closed, the days with a session marked.\nexport default function DatePickerDayStrip() {\n const [day, setDay] = React.useState<string | null>(\"2026-10-14\");\n return (\n <div className=\"w-full max-w-md\">\n <DayStrip\n label=\"Session day\"\n locale=\"en-US\"\n value={day}\n onValueChange={setDay}\n start=\"2026-10-11\"\n today=\"2026-10-11\"\n isDateDisabled={closed}\n isDateMarked={(d) => sessions.has(d)}\n labels={{ marked: \"has a session\" }}\n />\n </div>\n );\n}\n"
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
"name": "wheel",
|
|
1187
|
+
"title": "Wheel",
|
|
1188
|
+
"description": "`WheelPicker` + `Wheel`: any short list turned by the thumb or the arrows — a party size.",
|
|
1189
|
+
"path": "examples/date-picker/wheel.tsx",
|
|
1190
|
+
"module": "@krizaka/ui/registry/examples/date-picker/wheel",
|
|
1191
|
+
"code": "\"use client\";\n\nimport { Wheel, WheelPicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\nconst sizes = Array.from({ length: 12 }, (_, i) => ({ value: String(i + 1), label: i === 0 ? \"1 guest\" : `${i + 1} guests` }));\n\n// Any short list turned by the thumb: a party size. The keyboard moves it too (↑ ↓, Page Up / Down, Home / End).\nexport default function DatePickerWheel() {\n const [size, setSize] = React.useState(\"4\");\n return (\n <WheelPicker label=\"Party size\">\n <Wheel label=\"Guests\" options={sizes} value={size} onValueChange={setSize} className=\"w-36\" />\n </WheelPicker>\n );\n}\n"
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"name": "time-wheel",
|
|
1195
|
+
"title": "Time, wheels",
|
|
1196
|
+
"description": "`TimePicker`: hours, minutes by 5 and the period of a 12-hour locale; the value is `HH:mm`.",
|
|
1197
|
+
"path": "examples/date-picker/time-wheel.tsx",
|
|
1198
|
+
"module": "@krizaka/ui/registry/examples/date-picker/time-wheel",
|
|
1199
|
+
"code": "\"use client\";\n\nimport { TimePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// A reminder set by the thumb: hours, minutes by 5 and the period, as the locale writes them. The value is \"HH:mm\".\nexport default function DatePickerTimeWheel() {\n const [time, setTime] = React.useState<string | null>(\"19:30\");\n return <TimePicker label=\"Remind me at\" locale=\"en-US\" value={time} onValueChange={setTime} minuteStep={5} />;\n}\n"
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
"name": "time-slots",
|
|
1203
|
+
"title": "Time, slots",
|
|
1204
|
+
"description": "`TimePicker variant=\"slots\"`: the free times of an agenda by part of the day, taken ones struck through.",
|
|
1205
|
+
"path": "examples/date-picker/time-slots.tsx",
|
|
1206
|
+
"module": "@krizaka/ui/registry/examples/date-picker/time-slots",
|
|
1207
|
+
"code": "\"use client\";\n\nimport { TimePicker, timeSlots } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\nconst taken = new Set([\"10:00\", \"13:30\", \"14:00\"]);\n\n// The free times of an agenda, by part of the day; taken slots are struck through and skipped by the arrows.\nexport default function DatePickerTimeSlots() {\n const [time, setTime] = React.useState<string | null>(\"11:00\");\n return (\n <div className=\"w-full max-w-md\">\n <TimePicker\n variant=\"slots\"\n label=\"Appointment time\"\n locale=\"en-US\"\n slots={timeSlots(\"09:00\", \"17:00\", 30)}\n isTimeDisabled={(t) => taken.has(t)}\n groupLabels={{ morning: \"Morning\", afternoon: \"Afternoon\", evening: \"Evening\" }}\n value={time}\n onValueChange={setTime}\n />\n </div>\n );\n}\n"
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
"name": "birth-date",
|
|
1211
|
+
"title": "Date of birth, typed",
|
|
1212
|
+
"description": "`DateField`: day · month · year in the locale's order, autofilled, an under-18 date refused.",
|
|
1213
|
+
"path": "examples/date-picker/birth-date.tsx",
|
|
1214
|
+
"module": "@krizaka/ui/registry/examples/date-picker/birth-date",
|
|
1215
|
+
"code": "\"use client\";\n\nimport { DateField, isRealDate, latestBirthDate } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// A date of birth typed as day, month and year (the browser can fill it); an under-18 date is refused at once.\nexport default function DatePickerBirthDate() {\n const [birth, setBirth] = React.useState(\"2015-03-27\");\n const minor = isRealDate(birth) && birth > latestBirthDate(18);\n return (\n <DateField\n label=\"Date of birth\"\n locale=\"en-GB\"\n autoComplete=\"bday\"\n value={birth}\n onValueChange={setBirth}\n hint=\"Private, never shown. For example, 27 3 1990.\"\n error={minor ? \"You must be 18 or older to create an account.\" : undefined}\n />\n );\n}\n"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"name": "birth-date-wheel",
|
|
1219
|
+
"title": "Date of birth, wheels",
|
|
1220
|
+
"description": "`DateWheel` with `max` at the 18th birthday: an under-18 date cannot be chosen.",
|
|
1221
|
+
"path": "examples/date-picker/birth-date-wheel.tsx",
|
|
1222
|
+
"module": "@krizaka/ui/registry/examples/date-picker/birth-date-wheel",
|
|
1223
|
+
"code": "\"use client\";\n\nimport { DateWheel } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// The wheels of a date of birth, in the locale's order, ending at the 18th birthday: an under-18 date cannot be chosen.\n// `max` is fixed here for the documentation; in an app, `max={latestBirthDate(18)}`.\nexport default function DatePickerBirthDateWheel() {\n const [birth, setBirth] = React.useState<string | null>(\"1994-06-18\");\n return <DateWheel label=\"Date of birth\" locale=\"en-US\" value={birth} onValueChange={setBirth} max=\"2008-10-11\" hint=\"You must be 18 or older.\" />;\n}\n"
|
|
1224
|
+
}
|
|
1225
|
+
]
|
|
1226
|
+
},
|
|
1227
|
+
"native": {
|
|
1228
|
+
"entry": "@krizaka/ui/native",
|
|
1229
|
+
"import": "import { Calendar, MonthPicker, DayStrip, TimePicker, DateWheel, WheelPicker, Wheel, DateField, latestBirthDate, timeSlots } from \"@krizaka/ui/native\";",
|
|
1230
|
+
"differences": [
|
|
1231
|
+
"No popover field (`DatePicker`, `DateRangePicker`): on a phone, the `Calendar`, the `DayStrip` or the wheels sit in the screen or in a `Sheet`.",
|
|
1232
|
+
"`DateField` is typed (day · month · year, numeric keypads); `DateWheel` is the same date as three wheels — the native form's `DateField` uses the wheels.",
|
|
1233
|
+
"Each value a wheel passes, each day or slot pressed plays a `selection` haptic — when the app passes `haptics` to the `ThemeProvider` (expo-haptics): the platform has no native module.",
|
|
1234
|
+
"A wheel is an adjustable control for VoiceOver and TalkBack (swipe up or down), named by its `label`, its value read as words."
|
|
1235
|
+
],
|
|
1236
|
+
"props": [
|
|
1237
|
+
{
|
|
1238
|
+
"component": "DateField",
|
|
1239
|
+
"description": "A date typed as day, month and year — a memorable date (a birth date, an expiry) that a calendar would make slow to\nreach: numeric keypads, the boxes in the locale's order, the next box focused once one is full. No native module.",
|
|
1240
|
+
"props": [
|
|
1241
|
+
{
|
|
1242
|
+
"name": "label",
|
|
1243
|
+
"type": "string",
|
|
1244
|
+
"default": null,
|
|
1245
|
+
"description": "The visible label of the date, passed translated (the group's accessible name).",
|
|
1246
|
+
"required": true
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
"name": "autoComplete",
|
|
1250
|
+
"type": "\"off\" | \"bday\"",
|
|
1251
|
+
"default": null,
|
|
1252
|
+
"description": "`bday` for a date of birth: the system offers to fill it.",
|
|
1253
|
+
"required": false
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
"name": "defaultValue",
|
|
1257
|
+
"type": "string",
|
|
1258
|
+
"default": null,
|
|
1259
|
+
"description": "The date at first, uncontrolled.",
|
|
1260
|
+
"required": false
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
"name": "disabled",
|
|
1264
|
+
"type": "boolean",
|
|
1265
|
+
"default": null,
|
|
1266
|
+
"description": "Not available.",
|
|
1267
|
+
"required": false
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
"name": "error",
|
|
1271
|
+
"type": "string",
|
|
1272
|
+
"default": null,
|
|
1273
|
+
"description": "An error under the boxes: a danger border and caption, read with them and announced. Replaces the hint.",
|
|
1274
|
+
"required": false
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
"name": "hint",
|
|
1278
|
+
"type": "string",
|
|
1279
|
+
"default": null,
|
|
1280
|
+
"description": "A help text under the boxes (\"For example, 27 3 1990\").",
|
|
1281
|
+
"required": false
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
"name": "inputRef",
|
|
1285
|
+
"type": "Ref<TextInput>",
|
|
1286
|
+
"default": null,
|
|
1287
|
+
"description": "The first box (in the locale's order), to focus the field from the one before it.",
|
|
1288
|
+
"required": false
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
"name": "labels",
|
|
1292
|
+
"type": "Partial<DateFieldLabels>",
|
|
1293
|
+
"default": null,
|
|
1294
|
+
"description": "The names of the boxes, passed translated; English by default.",
|
|
1295
|
+
"required": false
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
"name": "locale",
|
|
1299
|
+
"type": "string",
|
|
1300
|
+
"default": "en-US",
|
|
1301
|
+
"description": "The locale that orders the boxes (day · month · year, or month · day · year). Default `en-US`.",
|
|
1302
|
+
"required": false
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"name": "onBlur",
|
|
1306
|
+
"type": "(() => void)",
|
|
1307
|
+
"default": null,
|
|
1308
|
+
"description": "Called when focus leaves the last box.",
|
|
1309
|
+
"required": false
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
"name": "onSubmitEditing",
|
|
1313
|
+
"type": "(() => void)",
|
|
1314
|
+
"default": null,
|
|
1315
|
+
"description": "Called by the last box's return key (`returnKeyType`): move to the next field, or send the form.",
|
|
1316
|
+
"required": false
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
"name": "onValueChange",
|
|
1320
|
+
"type": "((value: string) => void)",
|
|
1321
|
+
"default": null,
|
|
1322
|
+
"description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
|
|
1323
|
+
"required": false
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"name": "returnKeyType",
|
|
1327
|
+
"type": "\"next\" | \"done\" | \"go\" | \"send\"",
|
|
1328
|
+
"default": "done",
|
|
1329
|
+
"description": "The last box's return key. Default `done`.",
|
|
1330
|
+
"required": false
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
"name": "style",
|
|
1334
|
+
"type": "StyleProp<ViewStyle>",
|
|
1335
|
+
"default": null,
|
|
1336
|
+
"description": "Styles merged last, on the field.",
|
|
1337
|
+
"required": false
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
"name": "value",
|
|
1341
|
+
"type": "string",
|
|
1342
|
+
"default": null,
|
|
1343
|
+
"description": "The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back in the same order, real date or not: the schema decides.",
|
|
1344
|
+
"required": false
|
|
1345
|
+
}
|
|
1346
|
+
]
|
|
1347
|
+
}
|
|
1348
|
+
],
|
|
1349
|
+
"examples": [
|
|
1350
|
+
{
|
|
1351
|
+
"name": "birth-date",
|
|
1352
|
+
"title": "Date of birth, typed",
|
|
1353
|
+
"description": "Day, month and year with `autoComplete=\"bday\"`, and the error of an under-18 date.",
|
|
1354
|
+
"path": "examples/date-picker/native/birth-date.tsx",
|
|
1355
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/birth-date",
|
|
1356
|
+
"code": "import { DateField } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst latestAdult = () => {\n const today = new Date();\n return new Date(Date.UTC(today.getFullYear() - 18, today.getMonth(), today.getDate())).toISOString().slice(0, 10);\n};\n\n// A date of birth typed as day, month and year, in the locale's order; an under-18 date is refused at once.\nexport default function NativeDateFieldBirthDate() {\n const [birth, setBirth] = React.useState(\"2015-03-27\");\n const minor = /^\\d{4}-\\d{2}-\\d{2}$/.test(birth) && birth > latestAdult();\n return (\n <View style={{ width: 296 }}>\n <DateField\n label=\"Date of birth\"\n locale=\"en-GB\"\n autoComplete=\"bday\"\n value={birth}\n onValueChange={setBirth}\n hint=\"Private, never shown. For example, 27 3 1990.\"\n error={minor ? \"You must be 18 or older to create an account.\" : undefined}\n />\n </View>\n );\n}\n",
|
|
1357
|
+
"screenshots": {
|
|
1358
|
+
"dark": "examples/date-picker/native/birth-date.dark.png",
|
|
1359
|
+
"light": "examples/date-picker/native/birth-date.light.png"
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
"name": "birth-date-wheel",
|
|
1364
|
+
"title": "Date of birth, wheels",
|
|
1365
|
+
"description": "`DateWheel` ending at the 18th birthday: an under-18 date cannot be chosen.",
|
|
1366
|
+
"path": "examples/date-picker/native/birth-date-wheel.tsx",
|
|
1367
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/birth-date-wheel",
|
|
1368
|
+
"code": "import { DateWheel } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\n\n// The wheels of a date of birth in the locale's order, ending at the 18th birthday; each value passed ticks under the\n// thumb (the app's `haptics`). `max` is fixed here for the documentation; in an app, `max={latestBirthDate(18)}`.\nexport default function NativeDatePickerBirthDateWheel() {\n const [birth, setBirth] = React.useState<string | null>(\"1994-06-18\");\n return <DateWheel label=\"Date of birth\" locale=\"en-US\" value={birth} onValueChange={setBirth} max=\"2008-10-11\" hint=\"You must be 18 or older.\" />;\n}\n",
|
|
1369
|
+
"screenshots": {
|
|
1370
|
+
"dark": "examples/date-picker/native/birth-date-wheel.dark.png",
|
|
1371
|
+
"light": "examples/date-picker/native/birth-date-wheel.light.png"
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
"name": "calendar",
|
|
1376
|
+
"title": "Calendar",
|
|
1377
|
+
"description": "A month to book a day: 44-pt days, today's node, the past unavailable.",
|
|
1378
|
+
"path": "examples/date-picker/native/calendar.tsx",
|
|
1379
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/calendar",
|
|
1380
|
+
"code": "import { Calendar } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\n\n// A month to book a day: 44-pt days, today's node, the past unavailable.\nexport default function NativeDatePickerCalendar() {\n const [day, setDay] = React.useState(\"2026-10-15\");\n return <Calendar locale=\"en-US\" value={day} onSelect={setDay} min=\"2026-10-11\" today=\"2026-10-11\" />;\n}\n",
|
|
1381
|
+
"screenshots": {
|
|
1382
|
+
"dark": "examples/date-picker/native/calendar.dark.png",
|
|
1383
|
+
"light": "examples/date-picker/native/calendar.light.png"
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
"name": "day-strip",
|
|
1388
|
+
"title": "Day strip",
|
|
1389
|
+
"description": "The days ahead swiped sideways, closed days dashed, marked days with a dot.",
|
|
1390
|
+
"path": "examples/date-picker/native/day-strip.tsx",
|
|
1391
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/day-strip",
|
|
1392
|
+
"code": "import { DayStrip } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst sessions = new Set([\"2026-10-14\", \"2026-10-17\"]);\n\n// The days ahead, swiped sideways: Sundays closed, the days with a session marked.\nexport default function NativeDatePickerDayStrip() {\n const [day, setDay] = React.useState<string | null>(\"2026-10-14\");\n return (\n <View style={{ width: 320 }}>\n <DayStrip\n label=\"Session day\"\n locale=\"en-US\"\n value={day}\n onValueChange={setDay}\n start=\"2026-10-11\"\n today=\"2026-10-11\"\n days={10}\n isDateDisabled={(d) => new Date(`${d}T12:00:00`).getDay() === 0}\n isDateMarked={(d) => sessions.has(d)}\n markedLabel=\"has a session\"\n />\n </View>\n );\n}\n",
|
|
1393
|
+
"screenshots": {
|
|
1394
|
+
"dark": "examples/date-picker/native/day-strip.dark.png",
|
|
1395
|
+
"light": "examples/date-picker/native/day-strip.light.png"
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
"name": "time-wheel",
|
|
1400
|
+
"title": "Time, wheels",
|
|
1401
|
+
"description": "Hours, minutes by 5 and the period; the value is `HH:mm`.",
|
|
1402
|
+
"path": "examples/date-picker/native/time-wheel.tsx",
|
|
1403
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/time-wheel",
|
|
1404
|
+
"code": "import { TimePicker } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\n\n// A reminder set by the thumb: hours, minutes by 5, the period of a 12-hour locale. The value is \"HH:mm\".\nexport default function NativeDatePickerTimeWheel() {\n const [time, setTime] = React.useState<string | null>(\"19:30\");\n return <TimePicker label=\"Remind me at\" locale=\"en-US\" value={time} onValueChange={setTime} />;\n}\n",
|
|
1405
|
+
"screenshots": {
|
|
1406
|
+
"dark": "examples/date-picker/native/time-wheel.dark.png",
|
|
1407
|
+
"light": "examples/date-picker/native/time-wheel.light.png"
|
|
1408
|
+
}
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
"name": "time-slots",
|
|
1412
|
+
"title": "Time, slots",
|
|
1413
|
+
"description": "The free times of an agenda as buttons, taken ones dashed.",
|
|
1414
|
+
"path": "examples/date-picker/native/time-slots.tsx",
|
|
1415
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/time-slots",
|
|
1416
|
+
"code": "import { TimePicker, timeSlots } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\nimport { View } from \"react-native\";\n\nconst taken = new Set([\"10:00\", \"11:30\"]);\n\n// The free times of an agenda as buttons; taken ones dashed and struck through.\nexport default function NativeDatePickerTimeSlots() {\n const [time, setTime] = React.useState<string | null>(\"10:30\");\n return (\n <View style={{ width: 320 }}>\n <TimePicker variant=\"slots\" label=\"Appointment time\" locale=\"en-US\" slots={timeSlots(\"09:00\", \"12:00\", 30)} isTimeDisabled={(t) => taken.has(t)} value={time} onValueChange={setTime} />\n </View>\n );\n}\n",
|
|
1417
|
+
"screenshots": {
|
|
1418
|
+
"dark": "examples/date-picker/native/time-slots.dark.png",
|
|
1419
|
+
"light": "examples/date-picker/native/time-slots.light.png"
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
"name": "month-year",
|
|
1424
|
+
"title": "Month and year",
|
|
1425
|
+
"description": "`MonthPicker`: the twelve months, nothing later than this one.",
|
|
1426
|
+
"path": "examples/date-picker/native/month-year.tsx",
|
|
1427
|
+
"module": "@krizaka/ui/registry/examples/date-picker/native/month-year",
|
|
1428
|
+
"code": "import { MonthPicker } from \"@krizaka/ui/native\";\nimport * as React from \"react\";\n\n// A billing month: the twelve months of the year, nothing later than this month.\nexport default function NativeDatePickerMonthYear() {\n const [month, setMonth] = React.useState<string | null>(\"2026-09\");\n return <MonthPicker locale=\"en-US\" value={month} onValueChange={setMonth} max=\"2026-10\" />;\n}\n",
|
|
1429
|
+
"screenshots": {
|
|
1430
|
+
"dark": "examples/date-picker/native/month-year.dark.png",
|
|
1431
|
+
"light": "examples/date-picker/native/month-year.light.png"
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
]
|
|
1435
|
+
},
|
|
1436
|
+
"files": [
|
|
1437
|
+
{
|
|
1438
|
+
"path": "date-picker/calendar.tsx",
|
|
1439
|
+
"content": "// Client. A month as an ARIA grid (the APG date picker): one day in the tab order, the arrows walk the days, Home /\n// End the week, Page Up / Page Down the months (Shift: the years), Enter or Space picks. Dates are ISO calendar dates\n// (`\"2026-10-11\"`): no time zone, nothing to convert, safe in an address. Month and day names come from\n// `@krizaka/intl` for the `locale` given — never the runtime's default.\n//\n// The material (BRAND.md §6): the chosen day is a lit fill (the accent, its shade, its glow), today carries the node —\n// the diamond of the marks — and a range is one continuous band. The month's title opens the months of the year\n// (`MonthPicker`): years are reached in a click, not by paging.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\n\nimport { ChevronGlyph } from \"../glyphs\";\nimport { tv } from \"../tv\";\nimport { MonthPicker } from \"./month-picker\";\nimport { addDays, addMonths, toIsoDate, toLocalDate, weekStartOf } from \"./parts\";\n\nexport { addDays, addMonths, toIsoDate, toLocalDate, weekStartOf };\n\n/** A calendar date, `YYYY-MM-DD`. */\nexport type IsoDate = string;\n/** A range of calendar dates; `end` is `null` while the second date is not chosen. */\nexport type DateRange = { start: IsoDate; end: IsoDate | null };\n\nexport const calendar = tv({\n slots: {\n root: \"inline-flex select-none flex-col text-sm text-fg\",\n header: \"mb-3 flex items-center justify-between gap-2\",\n caption:\n \"inline-flex h-9 items-center gap-1.5 rounded-lg px-2.5 text-sm font-semibold capitalize text-fg transition-colors hover:bg-surface-3 \" +\n \"focus-visible:outline-hidden focus-visible:focus-ring aria-expanded:bg-surface-3\",\n nav:\n \"inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 bg-sheen text-fg-secondary inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"disabled:pointer-events-none disabled:opacity-35\",\n months: \"flex flex-col gap-6 sm:flex-row\",\n month: \"kz-month flex flex-col gap-2\",\n monthTitle: \"px-1 text-xs font-semibold capitalize text-fg-secondary\",\n grid: \"border-separate border-spacing-y-1\",\n weekday: \"h-7 w-10 pb-1 text-center text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n cell:\n \"p-0 text-center data-[band]:bg-accent-soft data-[band=start]:rounded-l-xl data-[band=end]:rounded-r-xl data-[band=solo]:rounded-xl \" +\n \"data-[band=preview]:bg-surface-3\",\n day:\n \"relative h-10 w-10 rounded-xl text-sm tabular-nums text-fg transition-[background-color,box-shadow,transform] duration-150 \" +\n \"hover:bg-surface-3 active:scale-95 motion-reduce:transition-none motion-reduce:active:scale-100 \" +\n \"focus-visible:z-10 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"data-[outside]:text-fg-secondary data-[outside]:font-normal data-[weekend]:not-data-[selected]:text-fg-secondary \" +\n \"data-[today]:font-bold data-[today]:not-data-[selected]:text-fg-accent \" +\n \"data-[in-range]:hover:bg-accent/15 \" +\n \"data-[selected]:bg-accent data-[selected]:bg-fill-shade data-[selected]:font-semibold data-[selected]:text-on-accent \" +\n \"data-[selected]:shadow-glow data-[selected]:inset-shadow-edge-fill data-[selected]:hover:bg-accent \" +\n \"disabled:cursor-not-allowed disabled:text-fg-muted disabled:line-through disabled:opacity-50 disabled:hover:bg-transparent\",\n // Today: the node under the number (the marks' diamond), on-accent when today is the chosen day.\n node: \"pointer-events-none absolute bottom-1 left-1/2 h-1 w-1 -translate-x-1/2 rotate-45 rounded-[1px] bg-accent group-data-[selected]:bg-on-accent\",\n live: \"sr-only\",\n },\n variants: {\n variant: {\n plain: {},\n card: { root: \"rounded-2xl border border-border-default bg-surface-1 bg-sheen p-4 shadow-raised inset-shadow-edge\" },\n },\n },\n defaultVariants: { variant: \"plain\" },\n});\n\nconst monthOf = (iso: IsoDate) => `${iso.slice(0, 7)}-01`;\nconst clampIso = (iso: IsoDate, min?: IsoDate, max?: IsoDate) => (min && iso < min ? min : max && iso > max ? max : iso);\nconst monthsBetween = (a: IsoDate, b: IsoDate) => (Number(b.slice(0, 4)) - Number(a.slice(0, 4))) * 12 + Number(b.slice(5, 7)) - Number(a.slice(5, 7));\n\nexport type CalendarLabels = {\n /** The previous-month button (\"Previous month\"). */\n previousMonth: string;\n /** The next-month button (\"Next month\"). */\n nextMonth: string;\n /** The month's title as a button: opens the months of the year (\"Choose month and year\"). */\n chooseMonth: string;\n /** The previous-year button of the months view (\"Previous year\"). */\n previousYear: string;\n /** The next-year button of the months view (\"Next year\"). */\n nextYear: string;\n};\n\nexport const calendarLabels: CalendarLabels = {\n previousMonth: \"Previous month\",\n nextMonth: \"Next month\",\n chooseMonth: \"Choose month and year\",\n previousYear: \"Previous year\",\n nextYear: \"Next year\",\n};\n\nexport type CalendarProps = {\n /** The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit. */\n locale: string;\n /** The chosen date (one-date mode). */\n value?: IsoDate | null;\n /** The chosen range (range mode): its ends are lit, the days between are one band. */\n range?: DateRange | null;\n /** Called with the day picked. */\n onSelect?: (date: IsoDate) => void;\n /** The month shown (the first one, with `numberOfMonths={2}`), any day of it (controlled). */\n month?: IsoDate;\n /** The month shown at first. Default: the value's, or today's. */\n defaultMonth?: IsoDate;\n /** Called when the month shown changes. */\n onMonthChange?: (month: IsoDate) => void;\n /** One month, or two side by side (a period: from a phone's width up, they stack on a phone). */\n numberOfMonths?: 1 | 2;\n /** The earliest date that can be picked. */\n min?: IsoDate;\n /** The latest date that can be picked. */\n max?: IsoDate;\n /** Refuses some dates (`(d) => isWeekend(d)`). */\n isDateDisabled?: (date: IsoDate) => boolean;\n /** The first day of the week, 0 = Sunday. Default: the locale's. */\n weekStartsOn?: number;\n /** Today (marked): inject it for a stable server render or a test. Default: the device's date. */\n today?: IsoDate;\n /** Moves the focus to the active day when it mounts (inside a popover). */\n autoFocus?: boolean;\n /** `plain` (inside a popover, a card of the app) or `card` (the calendar is the interface: its own raised surface). */\n variant?: \"plain\" | \"card\";\n /** The words, passed translated; English by default. */\n labels?: Partial<CalendarLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** A month grid to pick a date or the ends of a range, with the full keyboard of the APG date picker. */\nexport function Calendar({\n locale,\n value,\n range,\n onSelect,\n month: monthProp,\n defaultMonth,\n onMonthChange,\n numberOfMonths = 1,\n min,\n max,\n isDateDisabled,\n weekStartsOn,\n today: todayProp,\n autoFocus,\n variant,\n labels: given,\n className,\n}: CalendarProps) {\n const labels = { ...calendarLabels, ...given };\n const s = calendar({ variant });\n const today = todayProp ?? toIsoDate(new Date());\n const anchor = value ?? range?.start ?? clampIso(today, min, max);\n const [innerMonth, setInnerMonth] = React.useState(monthOf(defaultMonth ?? anchor));\n const month = monthProp ? monthOf(monthProp) : innerMonth;\n const shown = Array.from({ length: numberOfMonths }, (_, i) => addMonths(month, i));\n const lastShown = shown[shown.length - 1]!;\n const isShown = (iso: IsoDate) => iso.slice(0, 7) >= month.slice(0, 7) && iso.slice(0, 7) <= lastShown.slice(0, 7);\n const [focused, setFocused] = React.useState<IsoDate>(isShown(anchor) ? anchor : month);\n const [hover, setHover] = React.useState<IsoDate | null>(null);\n const [view, setView] = React.useState<\"days\" | \"months\">(\"days\");\n const [direction, setDirection] = React.useState<\"next\" | \"previous\" | null>(null);\n const root = React.useRef<HTMLDivElement>(null);\n const moved = React.useRef(Boolean(autoFocus));\n const titleId = React.useId();\n\n const setMonth = (next: IsoDate) => {\n const m = monthOf(next);\n setDirection(m > month ? \"next\" : m < month ? \"previous\" : null);\n if (!monthProp) setInnerMonth(m);\n onMonthChange?.(m);\n };\n // The focused day follows the months shown when they are changed from outside (the buttons, a controlled `month`).\n const active = isShown(focused) ? focused : month;\n\n React.useEffect(() => {\n if (!moved.current || view !== \"days\") return;\n moved.current = false;\n root.current?.querySelector<HTMLButtonElement>(`[data-date=\"${active}\"]`)?.focus();\n }, [active, view]);\n\n const disabled = (iso: IsoDate) => Boolean((min && iso < min) || (max && iso > max) || isDateDisabled?.(iso));\n const start = weekStartsOn ?? weekStartOf(locale);\n const weekdayShort = getDateTimeFormat(locale, { weekday: \"short\" });\n const weekdayLong = getDateTimeFormat(locale, { weekday: \"long\" });\n const titleFormat = getDateTimeFormat(locale, { month: \"long\", year: \"numeric\" });\n const title = titleFormat.format(toLocalDate(month));\n const fullTitle = numberOfMonths === 2 ? `${title} – ${titleFormat.format(toLocalDate(lastShown))}` : title;\n\n const rangeEnd = range ? (range.end ?? hover) : null;\n const [lo, hi] = range && rangeEnd ? (range.start <= rangeEnd ? [range.start, rangeEnd] : [rangeEnd, range.start]) : [null, null];\n const previewing = Boolean(range && !range.end);\n\n const focusDay = (iso: IsoDate) => {\n moved.current = true;\n setFocused(iso);\n if (!isShown(iso)) setMonth(iso < month ? iso : addMonths(iso, -(numberOfMonths - 1)));\n };\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const weekday = (toLocalDate(active).getDay() - start + 7) % 7;\n const moves: Record<string, () => IsoDate> = {\n ArrowLeft: () => addDays(active, -1),\n ArrowRight: () => addDays(active, 1),\n ArrowUp: () => addDays(active, -7),\n ArrowDown: () => addDays(active, 7),\n Home: () => addDays(active, -weekday),\n End: () => addDays(active, 6 - weekday),\n PageUp: () => addMonths(active, event.shiftKey ? -12 : -1),\n PageDown: () => addMonths(active, event.shiftKey ? 12 : 1),\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n focusDay(move());\n };\n\n const prevDisabled = Boolean(min && addDays(month, -1) < min);\n const nextDisabled = Boolean(max && addMonths(lastShown, 1) > max);\n\n const band = (iso: IsoDate): string | undefined => {\n if (!lo || !hi) return undefined;\n if (iso < lo || iso > hi) return undefined;\n if (lo === hi) return undefined;\n if (previewing && iso !== range?.start) return iso === lo ? \"start\" : iso === hi ? \"end\" : \"preview\";\n return iso === lo ? \"start\" : iso === hi ? \"end\" : \"middle\";\n };\n\n const renderMonth = (first: IsoDate, index: number) => {\n const firstDate = toLocalDate(first);\n const lead = (firstDate.getDay() - start + 7) % 7;\n const gridStart = addDays(first, -lead);\n const days = Array.from({ length: 42 }, (_, i) => addDays(gridStart, i));\n const weeks = Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7)).filter((week, w) => w < 5 || week[0]!.slice(0, 7) === first.slice(0, 7));\n const monthTitle = titleFormat.format(firstDate);\n const gridLabelId = `${titleId}-m${index}`;\n return (\n <div key={first} className={s.month()} data-dir={direction ?? undefined}>\n {numberOfMonths === 2 ? (\n <div id={gridLabelId} className={s.monthTitle()}>\n {monthTitle}\n </div>\n ) : null}\n <table role=\"grid\" aria-label={monthTitle} className={s.grid()} onKeyDown={onKeyDown}>\n <thead>\n <tr>\n {days.slice(0, 7).map((iso) => (\n <th key={iso} scope=\"col\" abbr={weekdayLong.format(toLocalDate(iso))} className={s.weekday()}>\n {weekdayShort.format(toLocalDate(iso)).replace(/\\.$/, \"\")}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {weeks.map((week) => (\n <tr key={week[0]}>\n {week.map((iso) => {\n const outside = iso.slice(0, 7) !== first.slice(0, 7);\n // With two months, a day outside its month is the other month's: drawn once, where it belongs.\n if (outside && numberOfMonths === 2) return <td key={iso} role=\"gridcell\" className={s.cell()} />;\n const selected = iso === value || Boolean(range && (iso === range.start || iso === range.end));\n const inRange = Boolean(lo && hi && iso > lo && iso < hi);\n const dow = toLocalDate(iso).getDay();\n return (\n <td key={iso} role=\"gridcell\" aria-selected={selected || inRange || undefined} data-band={band(iso)} className={s.cell()}>\n <button\n type=\"button\"\n data-date={iso}\n tabIndex={iso === active ? 0 : -1}\n aria-label={formatDate(toLocalDate(iso), { locale, style: \"long\" })}\n aria-current={iso === today ? \"date\" : undefined}\n data-today={iso === today ? \"\" : undefined}\n data-outside={outside ? \"\" : undefined}\n data-weekend={dow === 0 || dow === 6 ? \"\" : undefined}\n data-selected={selected ? \"\" : undefined}\n data-in-range={inRange ? \"\" : undefined}\n disabled={disabled(iso)}\n className={s.day({ className: \"group\" })}\n onClick={() => {\n setFocused(iso);\n onSelect?.(iso);\n }}\n onMouseEnter={previewing ? () => setHover(iso) : undefined}\n onFocus={previewing ? () => setHover(iso) : undefined}\n >\n {Number(iso.slice(8))}\n {iso === today ? <span aria-hidden className={s.node()} /> : null}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n };\n\n return (\n <div ref={root} className={s.root({ className })}>\n <div className={s.header()}>\n <button\n type=\"button\"\n className={s.nav()}\n aria-label={view === \"days\" ? labels.previousMonth : labels.previousYear}\n disabled={view === \"days\" ? prevDisabled : Boolean(min && Number(month.slice(0, 4)) - 1 < Number(min.slice(0, 4)))}\n onClick={() => setMonth(addMonths(month, view === \"days\" ? -1 : -12))}\n >\n <ChevronGlyph direction=\"left\" className=\"h-4 w-4\" />\n </button>\n <button type=\"button\" className={s.caption()} aria-expanded={view === \"months\"} aria-label={`${labels.chooseMonth}, ${fullTitle}`} onClick={() => setView(view === \"days\" ? \"months\" : \"days\")}>\n <span aria-hidden>{view === \"days\" ? fullTitle : month.slice(0, 4)}</span>\n <ChevronGlyph direction={view === \"months\" ? \"up\" : \"down\"} className=\"h-3.5 w-3.5 text-fg-secondary\" />\n </button>\n <button\n type=\"button\"\n className={s.nav()}\n aria-label={view === \"days\" ? labels.nextMonth : labels.nextYear}\n disabled={view === \"days\" ? nextDisabled : Boolean(max && Number(month.slice(0, 4)) + 1 > Number(max.slice(0, 4)))}\n onClick={() => setMonth(addMonths(month, view === \"days\" ? 1 : 12))}\n >\n <ChevronGlyph direction=\"right\" className=\"h-4 w-4\" />\n </button>\n </div>\n <span id={titleId} className={s.live()} aria-live=\"polite\">\n {fullTitle}\n </span>\n {view === \"days\" ? (\n <div className={s.months()}>{shown.map(renderMonth)}</div>\n ) : (\n <MonthPicker\n locale={locale}\n bare\n year={Number(month.slice(0, 4))}\n onYearChange={(y) => setMonth(`${y}-${month.slice(5, 7)}-01`)}\n value={month.slice(0, 7)}\n min={min?.slice(0, 7)}\n max={max?.slice(0, 7)}\n autoFocus\n onValueChange={(m) => {\n if (!m) return;\n const target = `${m}-01`;\n setMonth(target);\n setView(\"days\");\n moved.current = true;\n setFocused(clampIso(addMonths(active, monthsBetween(monthOf(active), target)), min, max));\n }}\n />\n )}\n </div>\n );\n}\n"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"path": "date-picker/date-field.tsx",
|
|
1443
|
+
"content": "// Client. Two ways to give a memorable date — a birth date, an expiry — that a calendar would make slow to reach:\n// `DateField` (day · month · year typed in the locale's order, the next box focused once one is full; the web form's\n// `entry=\"parts\"`) and `DateWheel` (three wheels: the touch idiom, bounded by `min` / `max` — an age gate is\n// `max={latestBirthDate(18)}`). The value is `YYYY-MM-DD` either way, like the native `DateField` and `DateWheel`.\nimport * as React from \"react\";\n\nimport { useControllable } from \"../media\";\nimport { tv } from \"../tv\";\nimport { composeDate, DATE_PART_LENGTH, type DatePart, datePartsOrder, daysInMonth, isoOf, isoParts, monthNames, splitDate, todayIso, yearRange } from \"./parts\";\nimport { Wheel, WheelPicker } from \"./wheel\";\n\nexport const dateField = tv({\n slots: {\n root: \"flex flex-col gap-1.5 border-0 p-0\",\n legend: \"mb-1.5 p-0 text-xs font-semibold text-fg-secondary\",\n row: \"flex gap-2\",\n part: \"flex flex-col gap-1 text-xs text-fg-secondary\",\n input:\n \"h-11 rounded-lg border border-border-default bg-surface-2 px-2 text-center text-base tabular-nums text-fg inset-shadow-well transition-[border-color,box-shadow] \" +\n \"placeholder:text-fg-muted focus-visible:border-accent focus-visible:outline-hidden focus-visible:focus-ring-inset aria-invalid:border-danger disabled:cursor-not-allowed disabled:opacity-50\",\n hint: \"text-xs text-fg-secondary\",\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n },\n});\n\nexport type DateFieldLabels = {\n /** The name of the day (\"Day\"). */\n day: string;\n /** The name of the month (\"Month\"). */\n month: string;\n /** The name of the year (\"Year\"). */\n year: string;\n};\n\nexport const dateFieldLabels: DateFieldLabels = { day: \"Day\", month: \"Month\", year: \"Year\" };\n\ntype Common = {\n /** The visible label, passed translated (the group's name). */\n label: string;\n /** The BCP 47 locale: the order of the parts, the names of the months. */\n locale: string;\n /** A help text under the field (\"For example, 27 3 1990\"). */\n hint?: string;\n /** An error under the field: marks it invalid and is read with it. Replaces the hint. */\n error?: string;\n /** Not available. */\n disabled?: boolean;\n /** The name of a hidden input holding the value, for a plain `<form>` post. */\n name?: string;\n /** The names of the parts, passed translated; English by default. */\n labels?: Partial<DateFieldLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type DateFieldProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides. */\n value?: string;\n /** The date at first, uncontrolled. */\n defaultValue?: string;\n /** Called on every key with the date typed so far (`\"\"` when the boxes are empty). */\n onValueChange?: (value: string) => void;\n /** Called when the focus leaves the field. */\n onBlur?: () => void;\n /** `bday` for a date of birth: the browser offers to fill it. */\n autoComplete?: \"bday\" | \"off\";\n};\n\nfunction Caption({ id, hint, error }: { id: string; hint?: string; error?: string }) {\n const s = dateField();\n if (error)\n return (\n <p id={`${id}-error`} className={s.error()}>\n {error}\n </p>\n );\n return hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null;\n}\n\n/** Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full. */\nexport function DateField({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = \"\", onValueChange, onBlur, autoComplete }: DateFieldProps) {\n const labels = { ...dateFieldLabels, ...given };\n const s = dateField();\n const id = React.useId();\n const [value, setValue] = useControllable<string>(valueProp, defaultValue, onValueChange);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const [parts, setParts] = React.useState(() => splitDate(value));\n // A value set from outside (a reset, a suggestion) replaces what is in the boxes.\n if ((value ?? \"\") !== composeDate(parts) && composeDate(splitDate(value)) === (value ?? \"\")) setParts(splitDate(value));\n const refs = { day: React.useRef<HTMLInputElement>(null), month: React.useRef<HTMLInputElement>(null), year: React.useRef<HTMLInputElement>(null) };\n const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined;\n return (\n <fieldset className={s.root({ className })} aria-describedby={describedBy} onBlur={(e) => !e.currentTarget.contains(e.relatedTarget as Node | null) && onBlur?.()}>\n <legend className={s.legend()}>{label}</legend>\n <div className={s.row()}>\n {order.map((part: DatePart) => (\n <label key={part} className={s.part()}>\n {labels[part]}\n <input\n ref={refs[part]}\n inputMode=\"numeric\"\n autoComplete={autoComplete === \"bday\" ? `bday-${part}` : \"off\"}\n maxLength={DATE_PART_LENGTH[part]}\n value={parts[part]}\n disabled={disabled}\n aria-invalid={error ? true : undefined}\n className={s.input({ className: part === \"year\" ? \"w-20\" : \"w-14\" })}\n onChange={(e) => {\n const digits = e.target.value.replace(/\\D/g, \"\").slice(0, DATE_PART_LENGTH[part]);\n const next = { ...parts, [part]: digits };\n setParts(next);\n setValue(composeDate(next));\n const following = order[order.indexOf(part) + 1];\n if (digits.length === DATE_PART_LENGTH[part] && following) refs[following].current?.focus();\n }}\n />\n </label>\n ))}\n </div>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </fieldset>\n );\n}\n\nexport type DateWheelProps = Common & {\n /** The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`). */\n value?: string | null;\n /** The date at first, uncontrolled. */\n defaultValue?: string | null;\n /** Called with the date under the band, always a real date within `min` / `max`. */\n onValueChange?: (value: string) => void;\n /** The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago. */\n min?: string;\n /** The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today. */\n max?: string;\n /** Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed). */\n defaultView?: string;\n};\n\n/** Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date. */\nexport function DateWheel({ label, locale, hint, error, disabled, name, labels: given, className, value: valueProp, defaultValue = null, onValueChange, min, max, defaultView }: DateWheelProps) {\n const labels = { ...dateFieldLabels, ...given };\n const id = React.useId();\n const [value, setValue] = useControllable<string | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const today = todayIso();\n const hi = max ?? today;\n const lo = min ?? isoOf(Number(today.slice(0, 4)) - 120, 1, 1);\n const shown = value || defaultView || hi;\n const { year, month, day } = isoParts(shown);\n const order = React.useMemo(() => datePartsOrder(locale), [locale]);\n const months = React.useMemo(() => monthNames(locale, \"long\"), [locale]);\n const number = React.useMemo(() => new Intl.NumberFormat(locale, { useGrouping: false }), [locale]);\n const inBounds = (iso: string) => iso >= lo && iso <= hi;\n const set = (y: number, m: number, d: number) => {\n let next = isoOf(y, m, d);\n if (next > hi) next = hi;\n if (next < lo) next = lo;\n setValue(next);\n };\n const lastDay = daysInMonth(year, month);\n const columns: Record<DatePart, React.ReactNode> = {\n day: (\n <Wheel\n key=\"day\"\n label={labels.day}\n disabled={disabled}\n options={Array.from({ length: lastDay }, (_, i) => ({ value: String(i + 1), label: number.format(i + 1), disabled: !inBounds(isoOf(year, month, i + 1)) }))}\n value={String(Math.min(day, lastDay))}\n onValueChange={(v) => set(year, month, Number(v))}\n className=\"min-w-14\"\n />\n ),\n month: (\n <Wheel\n key=\"month\"\n label={labels.month}\n disabled={disabled}\n options={months.map((n, i) => ({ value: String(i + 1), label: n, disabled: isoOf(year, i + 1, daysInMonth(year, i + 1)) < lo || isoOf(year, i + 1, 1) > hi }))}\n value={String(month)}\n onValueChange={(v) => set(year, Number(v), day)}\n className=\"min-w-32 capitalize\"\n />\n ),\n year: (\n <Wheel\n key=\"year\"\n label={labels.year}\n disabled={disabled}\n options={yearRange(Number(hi.slice(0, 4)), Number(lo.slice(0, 4))).map((y) => ({ value: String(y), label: number.format(y) }))}\n value={String(year)}\n onValueChange={(v) => set(Number(v), month, day)}\n className=\"min-w-20\"\n />\n ),\n };\n return (\n <div className={dateField().root({ className })}>\n <span aria-hidden className={dateField().legend()}>\n {label}\n </span>\n <WheelPicker label={label} aria-describedby={error ? `${id}-error` : hint ? `${id}-hint` : undefined} invalid={Boolean(error)}>{order.map((part) => columns[part])}</WheelPicker>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n <Caption id={id} hint={hint} error={error} />\n </div>\n );\n}\n"
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
"path": "date-picker/date-picker.tsx",
|
|
1447
|
+
"content": "// Client (a portal). A field-like button that opens the `Calendar` in a Popover (Radix: Escape closes, the focus\n// returns to the button). The value is an ISO date (`\"2026-10-11\"`) — the same string in a form, an address and an\n// API; the button reads it in the `locale`'s own format through `@krizaka/intl`.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\n\nimport { CalendarGlyph, CloseGlyph } from \"../glyphs\";\nimport { useControllable } from \"../media\";\nimport { Popover } from \"../popover\";\nimport { tv } from \"../tv\";\nimport { Calendar, type CalendarLabels, calendarLabels, type CalendarProps, type DateRange, type IsoDate, toLocalDate } from \"./calendar\";\n\nexport const datePicker = tv({\n slots: {\n root: \"flex w-full flex-col gap-1.5\",\n label: \"text-xs font-semibold text-fg-secondary\",\n row: \"relative flex w-full\",\n // The field is a well (inset), the calendar opens on a raised, lit surface.\n trigger:\n \"group flex h-11 w-full items-center gap-2.5 rounded-lg border border-border-default bg-surface-2 px-3 text-left text-sm text-fg inset-shadow-well \" +\n \"transition-[border-color,box-shadow] hover:border-border-strong focus-visible:border-accent focus-visible:outline-hidden focus-visible:focus-ring-inset \" +\n \"data-[state=open]:border-accent data-[placeholder]:text-fg-muted aria-invalid:border-danger disabled:cursor-not-allowed disabled:opacity-50 data-[clearable]:pr-10\",\n icon: \"h-4 w-4 shrink-0 text-fg-secondary transition-colors group-data-[state=open]:text-fg-accent\",\n clear:\n \"absolute right-1.5 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-fg-secondary hover:bg-surface-3 hover:text-fg \" +\n \"focus-visible:outline-hidden focus-visible:focus-ring\",\n content: \"w-auto rounded-2xl bg-sheen p-4 shadow-floating inset-shadow-edge\",\n layout: \"flex flex-col gap-4 sm:flex-row\",\n presets: \"flex gap-1.5 overflow-x-auto pb-1 sm:w-40 sm:flex-col sm:overflow-visible sm:border-r sm:border-border-subtle sm:pb-0 sm:pr-4\",\n preset:\n \"inline-flex shrink-0 items-center gap-2.5 whitespace-nowrap rounded-lg px-2.5 py-2 text-left text-xs font-semibold text-fg-secondary transition-colors \" +\n \"hover:bg-surface-3 hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring aria-pressed:bg-accent-soft aria-pressed:text-fg\",\n presetNode: \"node shrink-0 group-aria-pressed:node-lit\",\n hint: \"text-xs text-fg-secondary\",\n error: \"flex items-center gap-1.5 text-xs font-medium text-fg-danger before:h-1.5 before:w-1.5 before:shrink-0 before:rounded-full before:bg-danger\",\n },\n});\n\nexport type DatePickerLabels = CalendarLabels & {\n /** The clear button (\"Clear date\"). */\n clear: string;\n};\n\nexport const datePickerLabels: DatePickerLabels = { ...calendarLabels, clear: \"Clear date\" };\n\ntype Shared = Pick<CalendarProps, \"locale\" | \"min\" | \"max\" | \"isDateDisabled\" | \"weekStartsOn\" | \"today\"> & {\n /** The visible label, passed translated (also the button's accessible name, with the value). */\n label: string;\n /** Shown while nothing is chosen (\"Pick a date\"), passed translated. */\n placeholder?: string;\n /** `short` · `medium` (default) · `long`: how the button writes the date. */\n format?: \"short\" | \"medium\" | \"long\";\n /** Adds a button that empties the value. */\n clearable?: boolean;\n /** A help text under the field. */\n hint?: string;\n /** An error under the field: marks it invalid and is read with it. */\n error?: string;\n /** Not available. */\n disabled?: boolean;\n /** The name of a hidden input holding the ISO value, for a plain `<form>` post. */\n name?: string;\n /** Opens the calendar at first (a story, a test). */\n defaultOpen?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<DatePickerLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type DatePickerProps = Shared & {\n /** The chosen date, ISO (controlled); `null` for none. */\n value?: IsoDate | null;\n /** The chosen date at first, uncontrolled. */\n defaultValue?: IsoDate | null;\n /** Called with the date picked, or `null` when cleared. */\n onValueChange?: (value: IsoDate | null) => void;\n};\n\nfunction Field({\n shared,\n text,\n hasValue,\n onClear,\n hidden,\n open,\n setOpen,\n children,\n}: {\n shared: Shared;\n text: string | null;\n hasValue: boolean;\n onClear: () => void;\n hidden: string;\n open: boolean;\n setOpen: (open: boolean) => void;\n children: React.ReactNode;\n}) {\n const { label, placeholder, clearable, hint, error, disabled, name, className } = shared;\n const labels = { ...datePickerLabels, ...shared.labels };\n const s = datePicker();\n const id = React.useId();\n const describedBy = [hint ? `${id}-hint` : null, error ? `${id}-error` : null].filter(Boolean).join(\" \") || undefined;\n return (\n <div className={s.root({ className })}>\n <label htmlFor={`${id}-trigger`} className={s.label()}>\n {label}\n </label>\n <Popover.Root open={open} onOpenChange={setOpen}>\n <div className={s.row()}>\n <Popover.Trigger asChild>\n <button\n id={`${id}-trigger`}\n type=\"button\"\n aria-haspopup=\"dialog\"\n aria-describedby={describedBy}\n aria-invalid={error ? true : undefined}\n disabled={disabled}\n data-placeholder={hasValue ? undefined : \"\"}\n data-clearable={clearable && hasValue ? \"\" : undefined}\n className={s.trigger()}\n >\n <CalendarGlyph className={s.icon()} />\n <span className=\"truncate\">{text ?? placeholder ?? \"\"}</span>\n </button>\n </Popover.Trigger>\n {clearable && hasValue && !disabled ? (\n <button type=\"button\" aria-label={labels.clear} className={s.clear()} onClick={onClear}>\n <CloseGlyph className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n <Popover.Content align=\"start\" aria-label={label} className={s.content()}>\n {children}\n </Popover.Content>\n </Popover.Root>\n {name ? <input type=\"hidden\" name={name} value={hidden} /> : null}\n {hint ? (\n <p id={`${id}-hint`} className={s.hint()}>\n {hint}\n </p>\n ) : null}\n {error ? (\n <p id={`${id}-error`} className={s.error()}>\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\n/** A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value. */\nexport function DatePicker({ value: valueProp, defaultValue = null, onValueChange, defaultOpen = false, ...shared }: DatePickerProps) {\n const [value, setValue] = useControllable<IsoDate | null>(valueProp, defaultValue, onValueChange);\n const [open, setOpen] = React.useState(defaultOpen);\n const { locale, format = \"medium\" } = shared;\n return (\n <Field\n shared={shared}\n text={value ? formatDate(toLocalDate(value), { locale, style: format }) : null}\n hasValue={Boolean(value)}\n onClear={() => setValue(null)}\n hidden={value ?? \"\"}\n open={open}\n setOpen={setOpen}\n >\n <Calendar\n {...shared}\n autoFocus\n value={value}\n onSelect={(date) => {\n setValue(date);\n setOpen(false);\n }}\n />\n </Field>\n );\n}\n\nexport type DateRangePreset = {\n /** Its name, passed translated (\"Last 7 days\"). */\n label: string;\n /** The first day, ISO. */\n start: IsoDate;\n /** The last day, ISO. */\n end: IsoDate;\n};\n\nexport type DateRangePickerProps = Shared & {\n /** The chosen range (controlled); `null` for none. `end` is never `null` in a committed value. */\n value?: { start: IsoDate; end: IsoDate } | null;\n /** The range at first, uncontrolled. */\n defaultValue?: { start: IsoDate; end: IsoDate } | null;\n /** Called once both ends are chosen (or with `null` when cleared). */\n onValueChange?: (value: { start: IsoDate; end: IsoDate } | null) => void;\n /** Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar. */\n presets?: readonly DateRangePreset[];\n /** Two months side by side (default, from a phone's width up) or one. */\n numberOfMonths?: 1 | 2;\n};\n\n/** Formats a range as the locale writes one (\"Oct 1 – 11, 2026\"), through Intl's `formatRange` where available. */\nfunction formatRange(locale: string, start: IsoDate, end: IsoDate, style: \"short\" | \"medium\" | \"long\") {\n const formatter = getDateTimeFormat(locale, { dateStyle: style }) as Intl.DateTimeFormat & { formatRange?: (a: Date, b: Date) => string };\n return formatter.formatRange\n ? formatter.formatRange(toLocalDate(start), toLocalDate(end))\n : `${formatter.format(toLocalDate(start))} – ${formatter.format(toLocalDate(end))}`;\n}\n\n/** A period field: two clicks on one calendar (the days between tinted as you hover), optional presets. */\nexport function DateRangePicker({ value: valueProp, defaultValue = null, onValueChange, presets, numberOfMonths = 2, defaultOpen = false, ...shared }: DateRangePickerProps) {\n const [value, setValue] = useControllable(valueProp, defaultValue, onValueChange);\n const [draft, setDraft] = React.useState<DateRange | null>(null);\n const [open, setOpenState] = React.useState(defaultOpen);\n const { locale, format = \"medium\" } = shared;\n const s = datePicker();\n const setOpen = (next: boolean) => {\n setOpenState(next);\n setDraft(null);\n };\n const shown: DateRange | null = draft ?? value;\n const pick = (date: IsoDate) => {\n if (!draft) setDraft({ start: date, end: null });\n else {\n const [start, end] = date < draft.start ? [date, draft.start] : [draft.start, date];\n setValue({ start, end });\n setOpen(false);\n }\n };\n return (\n <Field\n shared={shared}\n text={value ? formatRange(locale, value.start, value.end, format) : null}\n hasValue={Boolean(value)}\n onClear={() => setValue(null)}\n hidden={value ? `${value.start}/${value.end}` : \"\"}\n open={open}\n setOpen={setOpen}\n >\n <div className={s.layout()}>\n {presets?.length ? (\n <div className={s.presets()}>\n {presets.map((preset) => (\n <button\n key={preset.label}\n type=\"button\"\n aria-pressed={value?.start === preset.start && value?.end === preset.end}\n className={s.preset({ className: \"group\" })}\n onClick={() => {\n setValue({ start: preset.start, end: preset.end });\n setOpen(false);\n }}\n >\n <span aria-hidden className={s.presetNode()} />\n {preset.label}\n </button>\n ))}\n </div>\n ) : null}\n <Calendar {...shared} autoFocus range={shown} onSelect={pick} numberOfMonths={numberOfMonths} />\n </div>\n </Field>\n );\n}\n"
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
"path": "date-picker/day-strip.tsx",
|
|
1451
|
+
"content": "// Client. The days ahead as a strip that scrolls sideways — the agenda of the next two weeks, a delivery day, a\n// session. A radio group: one tab stop, the arrows move and pick (Home / End: the first and last day), each day named by\n// its full date; a marked day (it has something) says so in its name. The buttons at the ends page by a screen.\nimport { formatDate, getDateTimeFormat } from \"@krizaka/intl\";\nimport * as React from \"react\";\n\nimport { ChevronGlyph } from \"../glyphs\";\nimport { useControllable, usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\nimport { addDays, type IsoDate, toIsoDate, toLocalDate } from \"./calendar\";\n\nexport const dayStrip = tv({\n slots: {\n root: \"flex w-full min-w-0 flex-col gap-2\",\n header: \"flex items-center justify-between gap-2\",\n title: \"text-sm font-semibold capitalize text-fg\",\n navs: \"flex gap-1.5\",\n nav:\n \"inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 bg-sheen text-fg-secondary inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring disabled:pointer-events-none disabled:opacity-35\",\n track:\n \"-mx-1 flex snap-x snap-mandatory gap-2 overflow-x-auto scroll-smooth px-1 py-1.5 [scrollbar-width:none] motion-reduce:scroll-auto [&::-webkit-scrollbar]:hidden\",\n day:\n \"relative flex h-[4.5rem] w-14 shrink-0 snap-start flex-col items-center justify-center gap-0.5 rounded-2xl border border-border-default bg-surface-2 bg-sheen text-fg inset-shadow-edge \" +\n \"transition-[background-color,border-color,box-shadow,transform] duration-150 hover:border-border-strong active:scale-95 \" +\n \"motion-reduce:transition-none motion-reduce:active:scale-100 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"aria-checked:border-transparent aria-checked:bg-accent aria-checked:bg-fill-shade aria-checked:text-on-accent aria-checked:shadow-glow aria-checked:inset-shadow-edge-fill \" +\n \"disabled:cursor-not-allowed disabled:border-dashed disabled:bg-transparent disabled:bg-none disabled:text-fg-muted disabled:shadow-none\",\n weekday: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary group-aria-checked:text-on-accent group-disabled:text-fg-muted\",\n number: \"text-lg font-semibold tabular-nums leading-none\",\n // Today: the node of the marks above the number; a marked day: a dot under it.\n node: \"absolute top-1.5 h-1 w-1 rotate-45 rounded-[1px] bg-accent group-aria-checked:bg-on-accent\",\n mark: \"h-1.5 w-1.5 rounded-full bg-fg-secondary group-aria-checked:bg-on-accent\",\n noMark: \"h-1.5 w-1.5\",\n },\n});\n\nexport type DayStripLabels = {\n /** The button that shows the earlier days (\"Earlier days\"). */\n previous: string;\n /** The button that shows the later days (\"Later days\"). */\n next: string;\n /** Added to the name of a marked day (\"has events\"). */\n marked: string;\n};\n\nexport const dayStripLabels: DayStripLabels = { previous: \"Earlier days\", next: \"Later days\", marked: \"has events\" };\n\nexport type DayStripProps = {\n /** The group's accessible name (\"Delivery day\"), passed translated. */\n label: string;\n /** The BCP 47 locale of the names and the dates: always explicit. */\n locale: string;\n /** The chosen day, ISO (controlled); `null` for none. */\n value?: IsoDate | null;\n /** The chosen day at first, uncontrolled. */\n defaultValue?: IsoDate | null;\n /** Called with the day picked. */\n onValueChange?: (value: IsoDate) => void;\n /** The first day of the strip. Default: today. */\n start?: IsoDate;\n /** How many days. Default 14. */\n days?: number;\n /** Refuses some days (closed, full): drawn dashed, skipped by the arrows. */\n isDateDisabled?: (date: IsoDate) => boolean;\n /** Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name. */\n isDateMarked?: (date: IsoDate) => boolean;\n /** Today (marked by the node): inject it for a stable render or a test. Default: the device's date. */\n today?: IsoDate;\n /** Shows the month of the chosen day (or of the first) and the page buttons above the strip. */\n showHeader?: boolean;\n /** The words, passed translated; English by default. */\n labels?: Partial<DayStripLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** The days ahead in a row that scrolls sideways: one picked, today and the marked days shown. */\nexport function DayStrip({ label, locale, value: valueProp, defaultValue = null, onValueChange, start, days = 14, isDateDisabled, isDateMarked, today: todayProp, showHeader = true, labels: given, className }: DayStripProps) {\n const labels = { ...dayStripLabels, ...given };\n const s = dayStrip();\n const today = todayProp ?? toIsoDate(new Date());\n const first = start ?? today;\n const list = React.useMemo(() => Array.from({ length: days }, (_, i) => addDays(first, i)), [first, days]);\n const [value, setValue] = useControllable<IsoDate | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const enabled = list.filter((d) => !isDateDisabled?.(d));\n const tabStop = value && enabled.includes(value) ? value : enabled[0];\n const track = React.useRef<HTMLDivElement>(null);\n const reduce = usePrefersReducedMotion();\n const weekday = getDateTimeFormat(locale, { weekday: \"short\" });\n const monthTitle = getDateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(toLocalDate(value ?? first));\n\n const reveal = (iso: IsoDate, focus: boolean) => {\n const el = track.current?.querySelector<HTMLButtonElement>(`[data-date=\"${iso}\"]`);\n if (!el) return;\n if (focus) el.focus({ preventScroll: true });\n el.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\", behavior: reduce ? \"auto\" : \"smooth\" });\n };\n // The chosen day is in view when the strip appears.\n React.useEffect(() => {\n if (value) track.current?.querySelector<HTMLElement>(`[data-date=\"${value}\"]`)?.scrollIntoView?.({ block: \"nearest\", inline: \"center\" });\n // Only once: afterwards the person scrolls.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const i = enabled.indexOf(tabStop ?? \"\");\n const moves: Record<string, number> = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: enabled.length - 1 };\n const to = moves[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = enabled[Math.min(enabled.length - 1, Math.max(0, to))];\n if (!next) return;\n setValue(next);\n reveal(next, true);\n };\n const page = (by: number) => {\n const el = track.current;\n if (!el) return;\n el.scrollBy?.({ left: by * el.clientWidth * 0.8, behavior: reduce ? \"auto\" : \"smooth\" });\n };\n\n return (\n <div className={s.root({ className })}>\n {showHeader ? (\n <div className={s.header()}>\n <span className={s.title()} aria-hidden>\n {monthTitle}\n </span>\n <span className={s.navs()}>\n <button type=\"button\" className={s.nav()} aria-label={labels.previous} onClick={() => page(-1)}>\n <ChevronGlyph direction=\"left\" className=\"h-4 w-4\" />\n </button>\n <button type=\"button\" className={s.nav()} aria-label={labels.next} onClick={() => page(1)}>\n <ChevronGlyph direction=\"right\" className=\"h-4 w-4\" />\n </button>\n </span>\n </div>\n ) : null}\n <div ref={track} role=\"radiogroup\" aria-label={label} className={s.track()} onKeyDown={onKeyDown}>\n {list.map((iso) => {\n const marked = Boolean(isDateMarked?.(iso));\n const name = formatDate(toLocalDate(iso), { locale, style: \"long\" });\n return (\n <button\n key={iso}\n type=\"button\"\n role=\"radio\"\n data-date={iso}\n aria-checked={iso === value}\n aria-current={iso === today ? \"date\" : undefined}\n aria-label={marked ? `${name}, ${labels.marked}` : name}\n tabIndex={iso === tabStop ? 0 : -1}\n disabled={Boolean(isDateDisabled?.(iso))}\n className={s.day({ className: \"group\" })}\n onClick={() => setValue(iso)}\n >\n {iso === today ? <span aria-hidden className={s.node()} /> : null}\n <span aria-hidden className={s.weekday()}>\n {weekday.format(toLocalDate(iso)).replace(/\\.$/, \"\")}\n </span>\n <span aria-hidden className={s.number()}>\n {Number(iso.slice(8))}\n </span>\n <span aria-hidden className={marked ? s.mark() : s.noMark()} />\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n"
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
"path": "date-picker/index.ts",
|
|
1455
|
+
"content": "/**\n * @krizaka/ui/date-picker — dates and times, ISO values the form, the address and the API share, the locale's words\n * through @krizaka/intl: `DatePicker`, `DateRangePicker` (presets, two months), `Calendar` (inline), `MonthPicker`,\n * `DayStrip`, `WheelPicker` + `Wheel`, `TimePicker` (wheels or slots), `DateField` (typed), `DateWheel`, and the date\n * and time helpers. Client (\"use client\").\n */\nexport {\n addDays,\n addMonths,\n Calendar,\n calendar,\n type CalendarLabels,\n calendarLabels,\n type CalendarProps,\n type DateRange,\n type IsoDate,\n toIsoDate,\n toLocalDate,\n weekStartOf,\n} from \"./calendar\";\nexport { DateField, dateField, type DateFieldLabels, dateFieldLabels, type DateFieldProps, DateWheel, type DateWheelProps } from \"./date-field\";\nexport {\n DatePicker,\n datePicker,\n type DatePickerLabels,\n datePickerLabels,\n type DatePickerProps,\n DateRangePicker,\n type DateRangePickerProps,\n type DateRangePreset,\n} from \"./date-picker\";\nexport { DayStrip, dayStrip, type DayStripLabels, dayStripLabels, type DayStripProps } from \"./day-strip\";\nexport { type IsoMonth, MonthPicker, monthPicker, type MonthPickerLabels, monthPickerLabels, type MonthPickerProps } from \"./month-picker\";\nexport {\n ageOn,\n composeDate,\n type DatePart,\n datePartsOrder,\n dayPeriods,\n daysInMonth,\n formatTime,\n isoOf,\n isRealDate,\n latestBirthDate,\n minutesToTime,\n monthNames,\n timeSlots,\n timeToMinutes,\n type TimeValue,\n todayIso,\n uses12Hours,\n yearRange,\n} from \"./parts\";\nexport { TimePicker, timePicker, type TimePickerLabels, timePickerLabels, type TimePickerProps } from \"./time-picker\";\nexport { Wheel, wheel, WheelDivider, type WheelOption, WheelPicker, type WheelPickerProps, type WheelProps } from \"./wheel\";\n"
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
"path": "date-picker/meta.ts",
|
|
1459
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Date picker\",\n summary:\n \"Dates and times chosen the way the task asks — a calendar, a period with presets, a strip of days, wheels, time slots, a month, a date of birth — as ISO values (`2026-10-11`, `2026-10`, `18:30`) the form, the address and the API share, in the locale's own words.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"both\",\n whenToUse: [\n \"`DatePicker`: a day near today in a form — a publication, an auction's end, a reminder.\",\n \"`DateRangePicker`: a period — an earnings report, an audit log — with presets and two months side by side.\",\n \"`Calendar` inline (`variant=\\\"card\\\"`): when the month itself is the interface (availability, a booking).\",\n \"`DayStrip`: the next days in a row (a delivery day, a session, an agenda) — one thumb away on a phone.\",\n \"`TimePicker`: wheels to set a time (a reminder); `variant=\\\"slots\\\"` to book one among the free times.\",\n \"`MonthPicker`: a month of a year (a billing month, a card's expiry, a report).\",\n \"`DateField` (typed) or `DateWheel` (wheels): a memorable date far away — a date of birth with an age gate.\",\n ],\n whenNotToUse: [\n { when: \"For a date of birth or any date years away: a calendar makes it slow — use `DateField` or `DateWheel`.\", use: \"field\" },\n { when: \"For a relative choice (“last 7 days”, “this month”) without a calendar: chips.\", use: \"chip\" },\n ],\n bestPractices: [\n \"Always pass the person's `locale`: names, the first day of the week, the order of day · month · year and 12/24 hours follow it.\",\n \"Keep values as ISO strings in state and in the address (`YYYY-MM-DD`, `YYYY-MM`, `HH:mm`); format them only for display.\",\n \"Bound the choice with `min` / `max`, `isDateDisabled` or `isTimeDisabled` rather than refusing after the fact — an age gate is `max={latestBirthDate(18)}`.\",\n \"Offer presets on a period picker: most people want “Last 30 days”, not two clicks.\",\n \"Mark the days that have something (`isDateMarked`) on a strip: the dot is read in the day's name too.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow keys\", action: \"Calendar: move by a day or a week. Month picker: by a month or a row. Day strip and slots: move and pick.\" },\n { keys: \"Home / End\", action: \"First or last day of the week (calendar), first or last item (strip, slots, wheel).\" },\n { keys: \"Page Up / Page Down\", action: \"Calendar: previous or next month (with Shift: year). Month picker: year. Wheel: by five values.\" },\n { keys: \"Up / Down\", action: \"Wheel: the previous or next value.\" },\n { keys: \"Enter / Space\", action: \"Pick the focused day or month.\" },\n { keys: \"Esc\", action: \"Close the calendar and return to the field.\" },\n ],\n notes: [\n \"The APG date picker: the month is a `grid` labelled by its month, one day in the tab order, each day named by its full date; today carries `aria-current=\\\"date\\\"`, chosen days `aria-selected`, unavailable days are disabled.\",\n \"The month's title is a button that opens the months of the year (`aria-expanded`); a change of month is announced by a polite live region.\",\n \"A wheel is a `spinbutton` (`aria-valuetext` reads the value as words); on React Native an adjustable control — swipe up or down with VoiceOver or TalkBack.\",\n \"The day strip and the time slots are radio groups: one tab stop, the arrows move and pick, taken or closed items are disabled and skipped.\",\n \"`DateField` is a fieldset named by its legend, each box named by its part; the error is read with the field (`aria-describedby`).\",\n \"Nothing moves under reduced motion: the month's slide stops; the wheel's curve is tied to the scroll, not to time.\",\n ],\n },\n related: [\"field\", \"popover\", \"filter-bar\", \"form\"],\n web: {\n imports: [\"DatePicker\", \"DateRangePicker\", \"Calendar\", \"MonthPicker\", \"DayStrip\", \"WheelPicker\", \"Wheel\", \"TimePicker\", \"DateField\", \"DateWheel\", \"latestBirthDate\", \"timeSlots\"],\n examples: [\n { name: \"schedule\", title: \"Schedule\", description: \"A publication date, from today on, written the way the locale writes it.\" },\n { name: \"open\", title: \"Calendar open\", description: \"The popover open on the month: today's node, the chosen day lit.\" },\n { name: \"range\", title: \"Period with presets\", description: \"`DateRangePicker` for an earnings report: presets beside two months, the period as one band.\" },\n { name: \"french\", title: \"In French\", description: \"`locale=\\\"fr-CA\\\"`: the names, the order and the first day of the week follow.\" },\n { name: \"calendar\", title: \"Inline calendar\", description: \"`Calendar variant=\\\"card\\\"`: the month is the interface, weekends unavailable; its title opens the months.\" },\n { name: \"month-year\", title: \"Month and year\", description: \"`MonthPicker`: a billing month, nothing later than this one.\" },\n { name: \"day-strip\", title: \"Day strip\", description: \"`DayStrip`: the next two weeks in a row, closed days dashed, the days with a session marked.\" },\n { name: \"wheel\", title: \"Wheel\", description: \"`WheelPicker` + `Wheel`: any short list turned by the thumb or the arrows — a party size.\" },\n { name: \"time-wheel\", title: \"Time, wheels\", description: \"`TimePicker`: hours, minutes by 5 and the period of a 12-hour locale; the value is `HH:mm`.\" },\n { name: \"time-slots\", title: \"Time, slots\", description: \"`TimePicker variant=\\\"slots\\\"`: the free times of an agenda by part of the day, taken ones struck through.\" },\n { name: \"birth-date\", title: \"Date of birth, typed\", description: \"`DateField`: day · month · year in the locale's order, autofilled, an under-18 date refused.\" },\n { name: \"birth-date-wheel\", title: \"Date of birth, wheels\", description: \"`DateWheel` with `max` at the 18th birthday: an under-18 date cannot be chosen.\" },\n ],\n },\n native: {\n imports: [\"Calendar\", \"MonthPicker\", \"DayStrip\", \"TimePicker\", \"DateWheel\", \"WheelPicker\", \"Wheel\", \"DateField\", \"latestBirthDate\", \"timeSlots\"],\n differences: [\n \"No popover field (`DatePicker`, `DateRangePicker`): on a phone, the `Calendar`, the `DayStrip` or the wheels sit in the screen or in a `Sheet`.\",\n \"`DateField` is typed (day · month · year, numeric keypads); `DateWheel` is the same date as three wheels — the native form's `DateField` uses the wheels.\",\n \"Each value a wheel passes, each day or slot pressed plays a `selection` haptic — when the app passes `haptics` to the `ThemeProvider` (expo-haptics): the platform has no native module.\",\n \"A wheel is an adjustable control for VoiceOver and TalkBack (swipe up or down), named by its `label`, its value read as words.\",\n ],\n examples: [\n { name: \"birth-date\", title: \"Date of birth, typed\", description: \"Day, month and year with `autoComplete=\\\"bday\\\"`, and the error of an under-18 date.\" },\n { name: \"birth-date-wheel\", title: \"Date of birth, wheels\", description: \"`DateWheel` ending at the 18th birthday: an under-18 date cannot be chosen.\" },\n { name: \"calendar\", title: \"Calendar\", description: \"A month to book a day: 44-pt days, today's node, the past unavailable.\" },\n { name: \"day-strip\", title: \"Day strip\", description: \"The days ahead swiped sideways, closed days dashed, marked days with a dot.\" },\n { name: \"time-wheel\", title: \"Time, wheels\", description: \"Hours, minutes by 5 and the period; the value is `HH:mm`.\" },\n { name: \"time-slots\", title: \"Time, slots\", description: \"The free times of an agenda as buttons, taken ones dashed.\" },\n { name: \"month-year\", title: \"Month and year\", description: \"`MonthPicker`: the twelve months, nothing later than this one.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
"path": "date-picker/month-picker.tsx",
|
|
1463
|
+
"content": "// Client. The twelve months of a year as a grid (three columns): the arrows walk them (Up / Down by a row), Page Up /\n// Page Down change the year, Enter or Space picks. The value is `YYYY-MM` — a billing month, a card's expiry, a report.\n// The calendar's title opens it, so a year away is two clicks, not twelve pages.\nimport * as React from \"react\";\n\nimport { ChevronGlyph } from \"../glyphs\";\nimport { tv } from \"../tv\";\nimport { monthNames } from \"./parts\";\n\n/** A calendar month, `YYYY-MM`. */\nexport type IsoMonth = string;\n\nexport const monthPicker = tv({\n slots: {\n root: \"inline-flex select-none flex-col gap-3 text-sm text-fg\",\n header: \"flex items-center justify-between gap-2\",\n year: \"text-sm font-semibold tabular-nums\",\n nav:\n \"inline-flex h-9 w-9 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 bg-sheen text-fg-secondary inset-shadow-edge \" +\n \"transition-colors hover:border-border-strong hover:text-fg focus-visible:outline-hidden focus-visible:focus-ring disabled:pointer-events-none disabled:opacity-35\",\n grid: \"kz-fade grid grid-cols-3 gap-1.5\",\n month:\n \"relative h-11 min-w-[5.5rem] rounded-xl px-2 text-sm font-medium capitalize text-fg transition-[background-color,box-shadow,transform] duration-150 \" +\n \"hover:bg-surface-3 active:scale-95 motion-reduce:transition-none motion-reduce:active:scale-100 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"data-[current]:font-bold data-[current]:not-data-[selected]:text-fg-accent \" +\n \"data-[selected]:bg-accent data-[selected]:bg-fill-shade data-[selected]:font-semibold data-[selected]:text-on-accent data-[selected]:shadow-glow data-[selected]:inset-shadow-edge-fill \" +\n \"disabled:cursor-not-allowed disabled:text-fg-muted disabled:opacity-50 disabled:hover:bg-transparent\",\n },\n variants: {\n variant: {\n plain: {},\n card: { root: \"rounded-2xl border border-border-default bg-surface-1 bg-sheen p-4 shadow-raised inset-shadow-edge\" },\n },\n },\n defaultVariants: { variant: \"plain\" },\n});\n\nexport type MonthPickerLabels = {\n /** The previous-year button (\"Previous year\"). */\n previousYear: string;\n /** The next-year button (\"Next year\"). */\n nextYear: string;\n};\n\nexport const monthPickerLabels: MonthPickerLabels = { previousYear: \"Previous year\", nextYear: \"Next year\" };\n\nexport type MonthPickerProps = {\n /** The BCP 47 locale of the month names: always explicit. */\n locale: string;\n /** The chosen month, `YYYY-MM` (controlled); `null` for none. */\n value?: IsoMonth | null;\n /** The chosen month at first, uncontrolled. */\n defaultValue?: IsoMonth | null;\n /** Called with the month picked. */\n onValueChange?: (value: IsoMonth | null) => void;\n /** The year shown (controlled). */\n year?: number;\n /** Called when the year shown changes. */\n onYearChange?: (year: number) => void;\n /** The earliest month that can be picked, `YYYY-MM`. */\n min?: IsoMonth;\n /** The latest month that can be picked, `YYYY-MM`. */\n max?: IsoMonth;\n /** This month (marked): inject it for a stable render or a test. Default: the device's. */\n current?: IsoMonth;\n /** Moves the focus to the active month when it mounts. */\n autoFocus?: boolean;\n /** Without its own year header (inside the calendar, which has one). */\n bare?: boolean;\n /** `plain` or `card` (its own raised surface). */\n variant?: \"plain\" | \"card\";\n /** The words, passed translated; English by default. */\n labels?: Partial<MonthPickerLabels>;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** A month of a year: twelve months in a grid, the year above them. */\nexport function MonthPicker({ locale, value: valueProp, defaultValue = null, onValueChange, year: yearProp, onYearChange, min, max, current, autoFocus, bare, variant, labels: given, className }: MonthPickerProps) {\n const labels = { ...monthPickerLabels, ...given };\n const s = monthPicker({ variant });\n const [own, setOwn] = React.useState<IsoMonth | null>(defaultValue);\n const value = valueProp !== undefined ? valueProp : own;\n const now = new Date();\n const thisMonth = current ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n const [ownYear, setOwnYear] = React.useState(() => Number((value ?? thisMonth).slice(0, 4)));\n const year = yearProp ?? ownYear;\n const setYear = (y: number) => {\n if (yearProp === undefined) setOwnYear(y);\n onYearChange?.(y);\n };\n const names = React.useMemo(() => monthNames(locale, \"short\").map((n) => n.replace(/\\.$/, \"\")), [locale]);\n const longNames = React.useMemo(() => monthNames(locale, \"long\"), [locale]);\n const iso = (y: number, m: number) => `${String(y).padStart(4, \"0\")}-${String(m).padStart(2, \"0\")}`;\n const disabled = (m: IsoMonth) => Boolean((min && m < min) || (max && m > max));\n const [focused, setFocused] = React.useState(() => (value && Number(value.slice(0, 4)) === year ? Number(value.slice(5, 7)) : Number(thisMonth.slice(5, 7))));\n const grid = React.useRef<HTMLDivElement>(null);\n const moved = React.useRef(Boolean(autoFocus));\n React.useEffect(() => {\n if (!moved.current) return;\n moved.current = false;\n grid.current?.querySelector<HTMLButtonElement>(`[data-month=\"${iso(year, focused)}\"]`)?.focus();\n }, [year, focused]);\n\n const pick = (m: IsoMonth) => {\n if (valueProp === undefined) setOwn(m);\n onValueChange?.(m);\n };\n const move = (to: number) => {\n moved.current = true;\n const y = year + Math.floor((to - 1) / 12);\n const m = ((((to - 1) % 12) + 12) % 12) + 1;\n if (y !== year) setYear(y);\n setFocused(m);\n };\n const onKeyDown = (event: React.KeyboardEvent) => {\n const moves: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -3, ArrowDown: 3, PageUp: -12, PageDown: 12 };\n if (event.key === \"Home\" || event.key === \"End\") {\n event.preventDefault();\n moved.current = true;\n setFocused(event.key === \"Home\" ? 1 : 12);\n return;\n }\n const by = moves[event.key];\n if (by === undefined) return;\n event.preventDefault();\n move(focused + by);\n };\n const rows = [0, 1, 2, 3].map((r) => [1, 2, 3].map((c) => r * 3 + c));\n return (\n <div className={s.root({ className })}>\n {bare ? null : (\n <div className={s.header()}>\n <button type=\"button\" className={s.nav()} aria-label={labels.previousYear} disabled={Boolean(min && year - 1 < Number(min.slice(0, 4)))} onClick={() => setYear(year - 1)}>\n <ChevronGlyph direction=\"left\" className=\"h-4 w-4\" />\n </button>\n <span className={s.year()} aria-live=\"polite\">\n {year}\n </span>\n <button type=\"button\" className={s.nav()} aria-label={labels.nextYear} disabled={Boolean(max && year + 1 > Number(max.slice(0, 4)))} onClick={() => setYear(year + 1)}>\n <ChevronGlyph direction=\"right\" className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n <div ref={grid} role=\"grid\" aria-label={String(year)} className={s.grid()} key={year} onKeyDown={onKeyDown}>\n {rows.map((row) => (\n <div role=\"row\" key={row[0]} className=\"contents\">\n {row.map((m) => {\n const id = iso(year, m);\n return (\n <div role=\"gridcell\" key={m} aria-selected={id === value || undefined} className=\"flex\">\n <button\n type=\"button\"\n data-month={id}\n tabIndex={m === focused ? 0 : -1}\n aria-current={id === thisMonth ? \"date\" : undefined}\n data-current={id === thisMonth ? \"\" : undefined}\n data-selected={id === value ? \"\" : undefined}\n disabled={disabled(id)}\n className={s.month({ className: \"w-full\" })}\n onClick={() => {\n setFocused(m);\n pick(id);\n }}\n >\n <span aria-hidden>{names[m - 1]}</span>\n <span className=\"sr-only\">{`${longNames[m - 1]} ${year}`}</span>\n </button>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
"path": "date-picker/parts.ts",
|
|
1467
|
+
"content": "// Platform-neutral: a date typed as day, month and year (the native `DateField`, the form's `DateField entry=\"parts\"`).\n// Values are ISO calendar dates, `YYYY-MM-DD`, compared as strings.\n\n/** A part of a typed date. */\nexport type DatePart = \"day\" | \"month\" | \"year\";\n\n/** The digits of each part. */\nexport const DATE_PART_LENGTH: Readonly<Record<DatePart, number>> = { day: 2, month: 2, year: 4 };\n\n/** The order of day, month and year in a locale (`en-US` → month, day, year; `fr` → day, month, year). */\nexport function datePartsOrder(locale: string): DatePart[] {\n try {\n const parts = new Intl.DateTimeFormat(locale, { day: \"2-digit\", month: \"2-digit\", year: \"numeric\" }).formatToParts(new Date(2001, 10, 22));\n const order = parts.map((p) => p.type).filter((t): t is DatePart => t === \"day\" || t === \"month\" || t === \"year\");\n if (order.length === 3) return order;\n } catch {\n // An engine without Intl's parts: day, month, year.\n }\n return [\"day\", \"month\", \"year\"];\n}\n\n/** The ISO-ordered value of what was typed: `\"\"` when nothing is, `YYYY-MM-DD` otherwise (not necessarily a real date). */\nexport function composeDate(parts: Readonly<Record<DatePart, string>>): string {\n if (!parts.day && !parts.month && !parts.year) return \"\";\n const pad = (v: string) => (v.length === 1 ? `0${v}` : v);\n return `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`;\n}\n\n/** The parts of a value (`\"\"` → three empty parts). */\nexport function splitDate(value: string | null | undefined): Record<DatePart, string> {\n const m = /^(\\d*)-(\\d*)-(\\d*)$/.exec(value ?? \"\");\n return m ? { year: m[1]!, month: m[2]!, day: m[3]! } : { day: \"\", month: \"\", year: \"\" };\n}\n\n/** True for a real calendar date written `YYYY-MM-DD` (not `2001-02-30`). */\nexport function isRealDate(value: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false;\n const date = new Date(`${value}T00:00:00Z`);\n return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;\n}\n\n/** The latest date of birth of someone `age` years old today (`YYYY-MM-DD`): an age gate's `max`, `value <= latestBirthDate(18)`. */\nexport function latestBirthDate(age: number, today = new Date()): string {\n return new Date(Date.UTC(today.getFullYear() - age, today.getMonth(), today.getDate())).toISOString().slice(0, 10);\n}\n\n// ─── Shared by the pickers, web and native (no DOM, no React) ───────────────────────────────────────────────────────\n\n/** The days of a month (`month` 1–12). */\nexport function daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\n/** `2026-10-11` → `{ year: 2026, month: 10, day: 11 }` (no check: pair it with `isRealDate`). */\nexport function isoParts(iso: string): { year: number; month: number; day: number } {\n const [year = 0, month = 0, day = 0] = iso.split(\"-\").map(Number);\n return { year, month, day };\n}\n\n/** `{ 2026, 10, 11 }` → `2026-10-11`, the day kept inside the month (`2026-02-31` → `2026-02-28`). */\nexport function isoOf(year: number, month: number, day: number): string {\n const d = Math.min(Math.max(day, 1), daysInMonth(year, month));\n return `${String(year).padStart(4, \"0\")}-${String(month).padStart(2, \"0\")}-${String(d).padStart(2, \"0\")}`;\n}\n\n/** The ISO date of today on this device (or of the `Date` given). */\nexport function todayIso(now = new Date()): string {\n return isoOf(now.getFullYear(), now.getMonth() + 1, now.getDate());\n}\n\n/** The age in whole years on `today` of someone born on `birth` (both ISO). */\nexport function ageOn(birth: string, today: string = todayIso()): number {\n const b = isoParts(birth);\n const t = isoParts(today);\n return t.year - b.year - (t.month < b.month || (t.month === b.month && t.day < b.day) ? 1 : 0);\n}\n\n/** The month names of a locale, January first (`long` · `short` · `narrow`). */\nexport function monthNames(locale: string, style: \"long\" | \"short\" | \"narrow\" = \"long\"): string[] {\n const format = new Intl.DateTimeFormat(locale, { month: style, timeZone: \"UTC\" });\n return Array.from({ length: 12 }, (_, i) => format.format(new Date(Date.UTC(2001, i, 1))));\n}\n\n/** The years from `from` to `to`, both included, in that order (descending when `from > to`). */\nexport function yearRange(from: number, to: number): number[] {\n const step = from <= to ? 1 : -1;\n return Array.from({ length: Math.abs(to - from) + 1 }, (_, i) => from + i * step);\n}\n\n/** A time of day, `HH:mm` on 24 hours (`\"09:30\"`, `\"18:00\"`): the value of every time picker. */\nexport type TimeValue = string;\n\n/** `\"9:5\"` / `\"09:05\"` → minutes since midnight; `null` when it is not a time. */\nexport function timeToMinutes(value: string | null | undefined): number | null {\n const m = /^(\\d{1,2}):(\\d{1,2})$/.exec(value ?? \"\");\n if (!m) return null;\n const h = Number(m[1]);\n const min = Number(m[2]);\n return h < 24 && min < 60 ? h * 60 + min : null;\n}\n\n/** Minutes since midnight → `HH:mm`. */\nexport function minutesToTime(minutes: number): TimeValue {\n const m = ((Math.round(minutes) % 1440) + 1440) % 1440;\n return `${String(Math.floor(m / 60)).padStart(2, \"0\")}:${String(m % 60).padStart(2, \"0\")}`;\n}\n\n/** The slots of a day from `start` to `end` (excluded) every `step` minutes: `timeSlots(\"09:00\", \"12:00\", 30)`. */\nexport function timeSlots(start: TimeValue, end: TimeValue, step = 30): TimeValue[] {\n const a = timeToMinutes(start);\n const b = timeToMinutes(end);\n if (a === null || b === null || step <= 0) return [];\n const slots: TimeValue[] = [];\n for (let m = a; m < b; m += step) slots.push(minutesToTime(m));\n return slots;\n}\n\n/** True when the locale writes the time on 12 hours with a day period (`en-US` → true, `fr-CA` → false). */\nexport function uses12Hours(locale: string): boolean {\n try {\n const options = new Intl.DateTimeFormat(locale, { hour: \"numeric\" }).resolvedOptions() as Intl.ResolvedDateTimeFormatOptions & { hourCycle?: string };\n return options.hourCycle ? options.hourCycle === \"h12\" || options.hourCycle === \"h11\" : Boolean(options.hour12);\n } catch {\n return false;\n }\n}\n\n/** The day periods of a locale, morning first (`[\"AM\", \"PM\"]`, `[\"a.m.\", \"p.m.\"]`). */\nexport function dayPeriods(locale: string): [string, string] {\n const format = new Intl.DateTimeFormat(locale, { hour: \"numeric\", hour12: true, timeZone: \"UTC\" });\n const period = (h: number) => format.formatToParts(new Date(Date.UTC(2001, 0, 1, h))).find((p) => p.type === \"dayPeriod\")?.value ?? (h < 12 ? \"AM\" : \"PM\");\n return [period(9), period(15)];\n}\n\n/** A time written the locale's way (`\"18:30\"` → `\"6:30 PM\"` in en-US, `\"18 h 30\"` in fr-CA). */\nexport function formatTime(locale: string, value: TimeValue): string {\n const minutes = timeToMinutes(value);\n if (minutes === null) return \"\";\n return new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", timeZone: \"UTC\" }).format(new Date(Date.UTC(2001, 0, 1, 0, minutes)));\n}\n\n/** A local `Date` at midnight for an ISO date (for formatting). */\nexport const toLocalDate = (iso: string) => {\n const { year, month, day } = isoParts(iso);\n return new Date(year, month - 1, day);\n};\n/** The ISO date of a local `Date`. */\nexport const toIsoDate = (date: Date): string => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n/** The ISO date `days` later (or earlier). */\nexport const addDays = (iso: string, days: number) => {\n const date = toLocalDate(iso);\n date.setDate(date.getDate() + days);\n return toIsoDate(date);\n};\n/** The ISO date `months` later (or earlier), the day kept inside the shorter months (`01-31` + 1 → `02-28`). */\nexport const addMonths = (iso: string, months: number) => {\n const { year, month, day } = isoParts(iso);\n const first = new Date(year, month - 1 + months, 1);\n return isoOf(first.getFullYear(), first.getMonth() + 1, day);\n};\n\n/** The first day of the week for a locale (0 = Sunday … 6 = Saturday), from `Intl.Locale` week info; Monday otherwise. */\nexport function weekStartOf(locale: string): number {\n try {\n const info = new Intl.Locale(locale) as Intl.Locale & { getWeekInfo?: () => { firstDay: number }; weekInfo?: { firstDay: number } };\n const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;\n return firstDay ? firstDay % 7 : 1;\n } catch {\n return 1;\n }\n}\n\n/** The weeks of a month's grid (six at most), each seven ISO dates from the locale's first day of the week. */\nexport function monthGrid(month: string, weekStartsOn: number): string[][] {\n const first = `${month.slice(0, 7)}-01`;\n const lead = (toLocalDate(first).getDay() - weekStartsOn + 7) % 7;\n const start = addDays(first, -lead);\n const days = Array.from({ length: 42 }, (_, i) => addDays(start, i));\n return Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7)).filter((week, w) => w < 5 || week[0]!.slice(0, 7) === first.slice(0, 7));\n}\n"
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
"path": "date-picker/time-picker.tsx",
|
|
1471
|
+
"content": "// Client. A time of day, `HH:mm` on 24 hours whatever the locale shows: as wheels (hours, minutes by a step, the day\n// period where the locale uses one) or as slots (the free times of an agenda, a radio group: one tab stop, the arrows\n// move and pick). The words come from Intl for the `locale` given.\nimport * as React from \"react\";\n\nimport { useControllable } from \"../media\";\nimport { tv } from \"../tv\";\nimport { dayPeriods, formatTime, timeToMinutes, type TimeValue, uses12Hours } from \"./parts\";\nimport { Wheel, WheelDivider, type WheelOption, WheelPicker } from \"./wheel\";\n\nexport const timePicker = tv({\n slots: {\n slots: \"grid grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] gap-2\",\n slot:\n \"h-11 rounded-xl border border-border-default bg-surface-2 bg-sheen px-3 text-sm font-semibold tabular-nums text-fg inset-shadow-edge \" +\n \"transition-[background-color,border-color,box-shadow,transform] duration-150 hover:border-border-strong active:scale-[0.97] \" +\n \"motion-reduce:transition-none motion-reduce:active:scale-100 focus-visible:outline-hidden focus-visible:focus-ring \" +\n \"aria-checked:border-transparent aria-checked:bg-accent aria-checked:bg-fill-shade aria-checked:text-on-accent aria-checked:shadow-glow aria-checked:inset-shadow-edge-fill \" +\n \"disabled:cursor-not-allowed disabled:border-dashed disabled:bg-transparent disabled:bg-none disabled:font-normal disabled:text-fg-muted disabled:line-through disabled:shadow-none\",\n group: \"flex flex-col gap-2\",\n heading: \"text-[11px] font-semibold uppercase tracking-wider text-fg-secondary\",\n },\n});\n\nexport type TimePickerLabels = {\n /** The hours' wheel (\"Hours\"). */\n hours: string;\n /** The minutes' wheel (\"Minutes\"). */\n minutes: string;\n /** The day period's wheel (\"AM/PM\"). */\n period: string;\n};\n\nexport const timePickerLabels: TimePickerLabels = { hours: \"Hours\", minutes: \"Minutes\", period: \"AM/PM\" };\n\ntype Shared = {\n /** The accessible name of the picker (\"Pick-up time\"), passed translated. */\n label: string;\n /** The BCP 47 locale: how times are written, and whether the day has a period (AM/PM). */\n locale: string;\n /** The time, `HH:mm` on 24 hours (controlled); `null` for none. */\n value?: TimeValue | null;\n /** The time at first, uncontrolled. */\n defaultValue?: TimeValue | null;\n /** Called with the time chosen. */\n onValueChange?: (value: TimeValue) => void;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\nexport type TimePickerProps = Shared &\n (\n | {\n /** `wheel`: hours and minutes (and the period) as wheels — a time you set (a reminder, an alarm). */\n variant?: \"wheel\";\n /** The minutes' step: 1, 5 (default), 10, 15, 30. */\n minuteStep?: number;\n /** Forces 12 or 24 hours. Default: the locale's. */\n hour12?: boolean;\n /** The words of the wheels, passed translated; English by default. */\n labels?: Partial<TimePickerLabels>;\n slots?: never;\n isTimeDisabled?: never;\n groupLabels?: never;\n }\n | {\n /** `slots`: the free times as buttons — a booking, a delivery window, an appointment. */\n variant: \"slots\";\n /** The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`). */\n slots: readonly TimeValue[];\n /** Refuses some slots (taken, too soon): drawn struck through, skipped by the arrows. */\n isTimeDisabled?: (time: TimeValue) => boolean;\n /** Splits the slots under headings by part of the day: `{ morning: \"Morning\", afternoon: \"Afternoon\", evening: \"Evening\" }`. */\n groupLabels?: { morning: string; afternoon: string; evening: string };\n minuteStep?: never;\n hour12?: never;\n labels?: never;\n }\n );\n\n/** A time of day: wheels to set one, or slots to book one. */\nexport function TimePicker(props: TimePickerProps) {\n return props.variant === \"slots\" ? <TimeSlots {...props} /> : <TimeWheels {...props} />;\n}\n\nfunction TimeWheels({ label, locale, value: valueProp, defaultValue = null, onValueChange, disabled, className, minuteStep = 5, hour12, labels: given }: Extract<TimePickerProps, { variant?: \"wheel\" }>) {\n const labels = { ...timePickerLabels, ...given };\n const [value, setValue] = useControllable<TimeValue | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const twelve = hour12 ?? uses12Hours(locale);\n const total = timeToMinutes(value) ?? 9 * 60;\n const h24 = Math.floor(total / 60);\n const minute = total % 60;\n const pm = h24 >= 12;\n const two = (n: number) => String(n).padStart(2, \"0\");\n const number = React.useMemo(() => new Intl.NumberFormat(locale, { minimumIntegerDigits: 2, useGrouping: false }), [locale]);\n const hours: WheelOption[] = twelve\n ? Array.from({ length: 12 }, (_, i) => ({ value: String(i === 0 ? 12 : i), label: String(i === 0 ? 12 : i) })).sort((a, b) => (Number(a.value) % 12) - (Number(b.value) % 12))\n : Array.from({ length: 24 }, (_, i) => ({ value: String(i), label: number.format(i) }));\n const minutes: WheelOption[] = Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => ({ value: String(i * minuteStep), label: number.format(i * minuteStep) }));\n // A minute that is not on the step (an existing value) is rounded to the nearest one shown.\n const shownMinute = String(Math.min(60 - minuteStep, Math.round(minute / minuteStep) * minuteStep));\n const [am, pmLabel] = dayPeriods(locale);\n const set = (h: number, m: number) => setValue(`${two(h)}:${two(m)}`);\n return (\n <WheelPicker label={`${label}, ${value ? formatTime(locale, value) : \"\"}`.replace(/, $/, \"\")} className={className}>\n <Wheel\n label={labels.hours}\n options={hours}\n disabled={disabled}\n value={twelve ? String(h24 % 12 === 0 ? 12 : h24 % 12) : String(h24)}\n onValueChange={(v) => set(twelve ? (Number(v) % 12) + (pm ? 12 : 0) : Number(v), minute)}\n />\n <WheelDivider>:</WheelDivider>\n <Wheel label={labels.minutes} options={minutes} disabled={disabled} value={shownMinute} onValueChange={(v) => set(h24, Number(v))} />\n {twelve ? (\n <Wheel\n label={labels.period}\n options={[\n { value: \"am\", label: am },\n { value: \"pm\", label: pmLabel },\n ]}\n disabled={disabled}\n value={pm ? \"pm\" : \"am\"}\n onValueChange={(v) => set((h24 % 12) + (v === \"pm\" ? 12 : 0), minute)}\n />\n ) : null}\n </WheelPicker>\n );\n}\n\nfunction TimeSlots({ label, locale, value: valueProp, defaultValue = null, onValueChange, disabled, className, slots, isTimeDisabled, groupLabels }: Extract<TimePickerProps, { variant: \"slots\" }>) {\n const s = timePicker();\n const [value, setValue] = useControllable<TimeValue | null>(valueProp, defaultValue, (v) => v && onValueChange?.(v));\n const enabled = slots.filter((t) => !disabled && !isTimeDisabled?.(t));\n const tabStop = value && enabled.includes(value) ? value : enabled[0];\n const root = React.useRef<HTMLDivElement>(null);\n const onKeyDown = (event: React.KeyboardEvent) => {\n const i = enabled.indexOf(tabStop ?? \"\");\n const moves: Record<string, number> = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: enabled.length - 1 };\n const to = moves[event.key];\n if (to === undefined) return;\n event.preventDefault();\n const next = enabled[(to + enabled.length) % enabled.length];\n if (!next) return;\n setValue(next);\n root.current?.querySelector<HTMLButtonElement>(`[data-time=\"${next}\"]`)?.focus();\n };\n const button = (t: TimeValue) => {\n const off = disabled || Boolean(isTimeDisabled?.(t));\n return (\n <button\n key={t}\n type=\"button\"\n role=\"radio\"\n data-time={t}\n aria-checked={t === value}\n tabIndex={t === tabStop ? 0 : -1}\n disabled={off}\n className={s.slot()}\n onClick={() => setValue(t)}\n >\n {formatTime(locale, t)}\n </button>\n );\n };\n const part = (t: TimeValue) => {\n const m = timeToMinutes(t) ?? 0;\n return m < 12 * 60 ? \"morning\" : m < 18 * 60 ? \"afternoon\" : \"evening\";\n };\n return (\n <div ref={root} role=\"radiogroup\" aria-label={label} aria-disabled={disabled || undefined} className={className} onKeyDown={onKeyDown}>\n {groupLabels ? (\n <div className=\"flex flex-col gap-4\">\n {([\"morning\", \"afternoon\", \"evening\"] as const).map((p) => {\n const inPart = slots.filter((t) => part(t) === p);\n if (!inPart.length) return null;\n return (\n <div key={p} role=\"group\" aria-label={groupLabels[p]} className={s.group()}>\n <span aria-hidden className={s.heading()}>\n {groupLabels[p]}\n </span>\n <div className={s.slots()}>{inPart.map(button)}</div>\n </div>\n );\n })}\n </div>\n ) : (\n <div className={s.slots()}>{slots.map(button)}</div>\n )}\n </div>\n );\n}\n\n\n"
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
"path": "date-picker/wheel.tsx",
|
|
1475
|
+
"content": "// Client. A wheel: a column of values that turns under a lit band — the touch idiom of a time, a date of birth, a\n// quantity. Each column is a `spinbutton` for assistive technologies and the keyboard (Up / Down, Page Up / Page Down by\n// five, Home / End), its value read as words (`aria-valuetext`); the hand scrolls it (scroll snap) or clicks a value.\n// The curve is a scroll-driven animation (CSS `animation-timeline: view()`): no script per frame, flat where unsupported.\nimport * as React from \"react\";\n\nimport { usePrefersReducedMotion } from \"../media\";\nimport { tv } from \"../tv\";\n\n/** A value of a wheel: what is stored, and its words (passed translated or formatted for the locale). */\nexport type WheelOption = { value: string; label: string; disabled?: boolean };\n\nexport const wheel = tv({\n slots: {\n root: \"relative inline-flex select-none items-stretch gap-1 rounded-2xl border border-border-default data-[invalid]:border-danger bg-surface-1 bg-sheen px-2 shadow-raised inset-shadow-edge\",\n band: \"pointer-events-none absolute inset-x-1.5 top-1/2 -translate-y-1/2 rounded-xl border border-border-subtle bg-surface-3 inset-shadow-well\",\n column:\n \"relative z-[1] min-w-14 flex-1 rounded-xl outline-hidden focus-visible:focus-ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-40\",\n scroller:\n \"kz-wheel h-full snap-y snap-mandatory overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden \" +\n \"[mask-image:linear-gradient(to_bottom,transparent,#000_30%,#000_70%,transparent)]\",\n item:\n \"kz-wheel-item flex snap-center items-center justify-center px-2 text-base tabular-nums text-fg-secondary transition-colors \" +\n \"data-[selected]:font-semibold data-[selected]:text-fg data-[disabled]:text-fg-muted data-[disabled]:line-through\",\n divider: \"z-[1] self-center text-base font-semibold text-fg-secondary\",\n },\n});\n\nconst ITEM = 40;\n\nexport type WheelProps = {\n /** The column's accessible name (\"Hour\", \"Month\"), passed translated. */\n label: string;\n /** The values, in order. */\n options: readonly WheelOption[];\n /** The chosen value (controlled). */\n value?: string | null;\n /** The chosen value at first, uncontrolled. Default: the first one. */\n defaultValue?: string;\n /** Called with the value that comes to rest under the band. */\n onValueChange?: (value: string) => void;\n /** Not available. */\n disabled?: boolean;\n /** Classes merged on the column (a width: `w-20`). */\n className?: string;\n};\n\nexport type WheelPickerProps = {\n /** The group's accessible name (\"Departure time\"), passed translated. */\n label: string;\n /** The wheels (`Wheel`), and dividers (`WheelDivider`) between them. */\n children: React.ReactNode;\n /** How many values are visible: 3, 5 (default) or 7. */\n visible?: 3 | 5 | 7;\n /** The help text or the error that describes the group (their ids). */\n \"aria-describedby\"?: string;\n /** Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`. */\n invalid?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\nconst VisibleContext = React.createContext(5);\n\n/** The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`. */\nexport function WheelPicker({ label, children, visible = 5, \"aria-describedby\": describedBy, invalid, className }: WheelPickerProps) {\n const s = wheel();\n return (\n <VisibleContext.Provider value={visible}>\n <div role=\"group\" aria-label={label} aria-describedby={describedBy} data-invalid={invalid ? \"\" : undefined} className={s.root({ className })} style={{ height: ITEM * visible }}>\n <span aria-hidden className={s.band()} style={{ height: ITEM }} />\n {children}\n </div>\n </VisibleContext.Provider>\n );\n}\n\n/** A divider between two wheels (`:` between the hours and the minutes). */\nexport function WheelDivider({ children }: { children: React.ReactNode }) {\n return (\n <span aria-hidden className={wheel().divider()}>\n {children}\n </span>\n );\n}\n\nconst nearestEnabled = (options: readonly WheelOption[], index: number) => {\n for (let d = 0; d < options.length; d++) {\n if (options[index + d] && !options[index + d]!.disabled) return index + d;\n if (options[index - d] && !options[index - d]!.disabled) return index - d;\n }\n return index;\n};\n\n/** One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values. */\nexport function Wheel({ label, options, value: valueProp, defaultValue, onValueChange, disabled, className }: WheelProps) {\n const s = wheel();\n const visible = React.useContext(VisibleContext);\n const [own, setOwn] = React.useState(defaultValue ?? options[0]?.value ?? \"\");\n const value = valueProp ?? own;\n const index = Math.max(0, options.findIndex((o) => o.value === value));\n const scroller = React.useRef<HTMLDivElement>(null);\n const settle = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const programmatic = React.useRef(false);\n const reduce = usePrefersReducedMotion();\n const mounted = React.useRef(false);\n\n const commit = React.useCallback(\n (i: number) => {\n const target = nearestEnabled(options, Math.min(options.length - 1, Math.max(0, i)));\n const option = options[target];\n if (!option) return;\n if (valueProp === undefined) setOwn(option.value);\n if (option.value !== value) onValueChange?.(option.value);\n },\n [options, valueProp, value, onValueChange],\n );\n\n // The list follows the value (a key, a click, a change from outside): at once the first time, smoothly after.\n React.useEffect(() => {\n const el = scroller.current;\n if (!el) return;\n const top = index * ITEM;\n if (Math.abs(el.scrollTop - top) < 1) return;\n programmatic.current = true;\n if (typeof el.scrollTo === \"function\") el.scrollTo({ top, behavior: mounted.current && !reduce ? \"smooth\" : \"auto\" });\n else el.scrollTop = top;\n mounted.current = true;\n const t = setTimeout(() => (programmatic.current = false), 400);\n return () => clearTimeout(t);\n }, [index, reduce]);\n\n const onScroll = () => {\n if (programmatic.current) return;\n if (settle.current) clearTimeout(settle.current);\n settle.current = setTimeout(() => {\n const el = scroller.current;\n if (el) commit(Math.round(el.scrollTop / ITEM));\n }, 90);\n };\n React.useEffect(() => () => void (settle.current && clearTimeout(settle.current)), []);\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n const step = (by: number) => {\n let i = index + by;\n while (options[i]?.disabled) i += Math.sign(by);\n return options[i] ? i : index;\n };\n const moves: Record<string, () => number> = {\n ArrowUp: () => step(-1),\n ArrowDown: () => step(1),\n PageUp: () => Math.max(0, nearestEnabled(options, index - 5)),\n PageDown: () => Math.min(options.length - 1, nearestEnabled(options, index + 5)),\n Home: () => nearestEnabled(options, 0),\n End: () => nearestEnabled(options, options.length - 1),\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n commit(move());\n };\n\n const pad = ((visible - 1) / 2) * ITEM;\n const current = options[index];\n return (\n <div\n role=\"spinbutton\"\n tabIndex={disabled ? -1 : 0}\n aria-label={label}\n aria-valuenow={index}\n aria-valuemin={0}\n aria-valuemax={Math.max(0, options.length - 1)}\n aria-valuetext={current?.label}\n aria-disabled={disabled || undefined}\n data-disabled={disabled ? \"\" : undefined}\n className={s.column({ className })}\n onKeyDown={onKeyDown}\n >\n <div ref={scroller} aria-hidden className={s.scroller()} style={{ paddingBlock: pad, scrollPaddingBlock: pad }} onScroll={onScroll}>\n {options.map((o, i) => (\n <div\n key={o.value}\n data-value={o.value}\n data-selected={i === index ? \"\" : undefined}\n data-disabled={o.disabled ? \"\" : undefined}\n className={s.item()}\n style={{ height: ITEM }}\n onClick={() => !o.disabled && commit(i)}\n >\n {o.label}\n </div>\n ))}\n </div>\n </div>\n );\n}\n"
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
"path": "glyphs.tsx",
|
|
1479
|
+
"content": "// The small line glyphs the data and media primitives draw (play, volume, search, chevrons…): decorative, 24-unit\n// box, `currentColor`, sized by the caller's class. Shared by the entries that need them; never exported on its own —\n// a product draws its own icons with @krizaka/icons.\nimport type * as React from \"react\";\n\ntype GlyphProps = { className?: string };\n\nfunction Glyph({ className = \"h-4 w-4\", fill, children }: GlyphProps & { fill?: boolean; children: React.ReactNode }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n className={className}\n fill={fill ? \"currentColor\" : \"none\"}\n stroke={fill ? \"none\" : \"currentColor\"}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n focusable=\"false\"\n >\n {children}\n </svg>\n );\n}\n\nexport const PlayGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <path d=\"M8 5.5v13a1 1 0 0 0 1.5.86l10.4-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z\" />\n </Glyph>\n);\nexport const PauseGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </Glyph>\n);\nexport const VolumeGlyph = ({ muted, ...p }: GlyphProps & { muted?: boolean }) => (\n <Glyph {...p}>\n <path d=\"M11 5 6 9H3v6h3l5 4V5Z\" />\n {muted ? <path d=\"m22 9-6 6m0-6 6 6\" /> : <path d=\"M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13\" />}\n </Glyph>\n);\nexport const FullscreenGlyph = ({ exit, ...p }: GlyphProps & { exit?: boolean }) => (\n <Glyph {...p}>\n {exit ? <path d=\"M9 4v5H4m11-5v5h5M9 20v-5H4m11 5v-5h5\" /> : <path d=\"M4 9V4h5m6 0h5v5M4 15v5h5m11-5v5h-5\" />}\n </Glyph>\n);\nexport const CaptionsGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10.5a2 2 0 1 0 0 3M17 10.5a2 2 0 1 0 0 3\" />\n </Glyph>\n);\nexport const PipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <rect x=\"12\" y=\"12\" width=\"6\" height=\"4\" rx=\"1\" />\n </Glyph>\n);\nexport const SearchGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </Glyph>\n);\nexport const CloseGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </Glyph>\n);\nexport const ChevronGlyph = ({ direction = \"down\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" | \"up\" | \"down\" }) => (\n <Glyph {...p}>\n <path d={{ left: \"m15 6-6 6 6 6\", right: \"m9 6 6 6-6 6\", up: \"m6 15 6-6 6 6\", down: \"m6 9 6 6 6-6\" }[direction]} />\n </Glyph>\n);\nexport const CheckGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Glyph>\n);\nexport const PlusGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\n);\nexport const CalendarGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2\" />\n <path d=\"M3 10h18M8 3v4M16 3v4\" />\n </Glyph>\n);\nexport const UploadGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 16V4m0 0L7 9m5-5 5 5\" />\n <path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\" />\n </Glyph>\n);\nexport const FileGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z\" />\n <path d=\"M14 3v5h5\" />\n </Glyph>\n);\nexport const CameraGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z\" />\n <circle cx=\"12\" cy=\"13.5\" r=\"3.5\" />\n </Glyph>\n);\nexport const FlipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3\" />\n <path d=\"M18 3v4h-4M6 21v-4h4\" />\n </Glyph>\n);\nexport const RetryGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 1 0 2.3-5.7L4 8.5\" />\n <path d=\"M4 4v4.5h4.5\" />\n </Glyph>\n);\nexport const SortGlyph = ({ direction, ...p }: GlyphProps & { direction?: \"ascending\" | \"descending\" }) => (\n <Glyph {...p}>\n {direction === \"ascending\" ? <path d=\"m7 14 5-5 5 5\" /> : direction === \"descending\" ? <path d=\"m7 10 5 5 5-5\" /> : <path d=\"m8 9 4-4 4 4M8 15l4 4 4-4\" />}\n </Glyph>\n);\nexport const ArrowGlyph = ({ direction = \"right\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" }) => (\n <Glyph {...p}>\n <path d={direction === \"left\" ? \"M19 12H5m0 0 6-6m-6 6 6 6\" : \"M5 12h14m0 0-6-6m6 6-6 6\"} />\n </Glyph>\n);\nexport const ExternalGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 4h6v6M20 4l-9 9\" />\n <path d=\"M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\" />\n </Glyph>\n);\nexport const WarningGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 4 2.5 20h19L12 4Z\" />\n <path d=\"M12 10v4M12 17.2v.1\" />\n </Glyph>\n);\nexport const CopyGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3\" />\n </Glyph>\n);\n"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"path": "media.ts",
|
|
1483
|
+
"content": "// Shared by the data and media primitives (client): the clock format, the reduced-motion preference, the\n// in-view observer, a debounced callback. Never an entry of its own.\nimport * as React from \"react\";\n\n/** Seconds as a clock: `75` → `1:15`, `3725` → `1:02:05`. Negative or unknown values read `0:00`. */\nexport function formatClock(seconds: number): string {\n const s = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const rest = String(s % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${rest}` : `${m}:${rest}`;\n}\n\n/** Clamps `value` between `min` and `max`. */\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst REDUCE = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduce = (notify: () => void) => {\n if (typeof window === \"undefined\" || !window.matchMedia) return () => undefined;\n const query = window.matchMedia(REDUCE);\n query.addEventListener?.(\"change\", notify);\n return () => query.removeEventListener?.(\"change\", notify);\n};\n\n/** `true` when the person asked for less motion (`prefers-reduced-motion: reduce`); follows a change of the setting. */\nexport function usePrefersReducedMotion(): boolean {\n return React.useSyncExternalStore(\n subscribeReduce,\n () => (typeof window !== \"undefined\" && window.matchMedia ? window.matchMedia(REDUCE).matches : false),\n () => false,\n );\n}\n\n/** The latest value of a callback, for an effect or an event that must not re-subscribe when it changes. */\nexport function useLatest<T>(value: T) {\n const ref = React.useRef(value);\n React.useLayoutEffect(() => {\n ref.current = value;\n });\n return ref;\n}\n\n/** Calls `onChange(visible)` when at least `threshold` of the element enters or leaves the viewport. */\nexport function useInView(ref: React.RefObject<Element | null>, onChange: (visible: boolean) => void, enabled = true, threshold = 0.25) {\n const latest = useLatest(onChange);\n React.useEffect(() => {\n const element = ref.current;\n if (!enabled || !element || typeof IntersectionObserver === \"undefined\") return;\n const observer = new IntersectionObserver(([entry]) => entry && latest.current(entry.isIntersecting), { threshold });\n observer.observe(element);\n return () => observer.disconnect();\n }, [ref, enabled, threshold, latest]);\n}\n\n/** A stable function that runs `callback` once `delay` ms have passed without a new call; `cancel()` drops the pending one. */\nexport function useDebouncedCallback<A extends unknown[]>(callback: (...args: A) => void, delay: number) {\n const latest = useLatest(callback);\n const [debounced] = React.useState(() => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let wait = delay;\n const run = (...args: A) => {\n clearTimeout(timer);\n if (wait <= 0) latest.current(...args);\n else timer = setTimeout(() => latest.current(...args), wait);\n };\n return Object.assign(run, {\n cancel: () => clearTimeout(timer),\n setDelay: (next: number) => {\n wait = next;\n },\n });\n });\n React.useEffect(() => debounced.setDelay(delay), [debounced, delay]);\n React.useEffect(() => () => debounced.cancel(), [debounced]);\n return debounced;\n}\n\n/** A value controlled by a prop when it is given, held inside otherwise; `set` calls `onChange` either way. */\nexport function useControllable<T>(value: T | undefined, defaultValue: T, onChange?: (next: T) => void) {\n const [inner, setInner] = React.useState(defaultValue);\n const controlled = value !== undefined;\n const current = controlled ? value : inner;\n const set = React.useCallback(\n (next: T) => {\n if (!controlled) setInner(next);\n onChange?.(next);\n },\n [controlled, onChange],\n );\n return [current, set] as const;\n}\n\n/** Starts a media element without an unhandled rejection (autoplay refused, the source replaced meanwhile). */\nexport function playSafely(media: HTMLMediaElement | null | undefined) {\n try {\n const started = media?.play() as Promise<void> | undefined;\n started?.catch?.(() => undefined);\n } catch {\n // Not playable here (no media support): the controls stay as they are.\n }\n}\n"
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
"path": "meta.ts",
|
|
1487
|
+
"content": "// The documentation of a component, carried by its code: each primitive has a `meta.ts` beside its source\n// (`src/<name>/meta.ts`; a component that exists only in React Native: `src/native/meta/<name>.ts`). Never built into\n// the package: `scripts/build-registry.mjs` reads them into `registry/<name>.json`, which krizaka.com renders as\n// /docs/ui/<name>, and `scripts/registry.test.mjs` fails when one is missing or incomplete.\n//\n// Why a typed module and not JSDoc tags: the fields are lists and links (`related`, `whenNotToUse[].use`, examples),\n// which a type checks and a comment does not; the file is plain data (erasable TypeScript), so Node reads it without a\n// compiler, and an editor completes it.\n\n/** Every documented component: the web primitives (one entry of the package each) and the native-only ones. */\nexport type ComponentName =\n | \"achievement\"\n | \"action-bar\"\n | \"alert\"\n | \"app-shell\"\n | \"argument-card\"\n | \"audio-player\"\n | \"auth-layout\"\n | \"avatar\"\n | \"badge\"\n | \"button\"\n | \"card\"\n | \"card-grid\"\n | \"catalogue-page\"\n | \"changelog-page\"\n | \"checkbox\"\n | \"chip\"\n | \"chip-link\"\n | \"choice-grid\"\n | \"cn\"\n | \"code-block\"\n | \"combobox\"\n | \"command\"\n | \"component-page\"\n | \"confirm-button\"\n | \"contact-page\"\n | \"continue-prompt\"\n | \"countdown\"\n | \"cta-panel\"\n | \"dashboard-layout\"\n | \"data-table\"\n | \"date-picker\"\n | \"decision-list\"\n | \"detail-layout\"\n | \"dialog\"\n | \"docs-article-page\"\n | \"docs-hub-page\"\n | \"docs-layout\"\n | \"dropdown-menu\"\n | \"empty-state\"\n | \"error-page\"\n | \"field\"\n | \"file-upload\"\n | \"filter-bar\"\n | \"form\"\n | \"gallery-page\"\n | \"glow-card\"\n | \"goal-gauge\"\n | \"how-it-works-page\"\n | \"image-cropper\"\n | \"iso\"\n | \"kbd\"\n | \"landing-page\"\n | \"landscape\"\n | \"legal-page\"\n | \"link-card\"\n | \"list\"\n | \"loader\"\n | \"longform-page\"\n | \"marketing-layout\"\n | \"marquee\"\n | \"mascot-quote\"\n | \"mascot-state\"\n | \"mascots\"\n | \"media-capture\"\n | \"meter\"\n | \"nav-bar\"\n | \"number-ticker\"\n | \"page\"\n | \"page-header\"\n | \"page-hero\"\n | \"pagination\"\n | \"pointer-light\"\n | \"popover\"\n | \"pricing-page\"\n | \"product-page\"\n | \"profile-card\"\n | \"progress\"\n | \"promise-card\"\n | \"radio-group\"\n | \"scroll-stage\"\n | \"search-field\"\n | \"section-backdrop\"\n | \"segmented\"\n | \"separator\"\n | \"settings-layout\"\n | \"skeleton\"\n | \"slider\"\n | \"slot\"\n | \"spinner\"\n | \"split-view\"\n | \"stage-banner\"\n | \"stat\"\n | \"status-page\"\n | \"stepper\"\n | \"switch\"\n | \"tabs\"\n | \"tag-input\"\n | \"terminal-replay\"\n | \"theme\"\n | \"toast\"\n | \"toc\"\n | \"tooltip\"\n | \"txt\"\n | \"video-player\"\n | \"video-trimmer\"\n | \"virtual\";\n\n/** Where a component exists: `@krizaka/ui/<name>` (web), `@krizaka/ui/native` (React Native), or both. */\nexport type Platform = \"web\" | \"native\" | \"both\";\n\n/** `stable`: its API only changes in a major. `beta`: it may still change in a minor. */\nexport type Status = \"stable\" | \"beta\";\n\n/**\n * The group of the catalogue. `media`: video, sound, camera, files, cropping. `blocks`: the page blocks — the frame,\n * the hero, sections, cards that lead or argue, decisions, code, calls to act — that every page of a site is made of.\n * `layouts`: the shells of whole screens — an application, a dashboard, settings, sign-in, an object, a list and its\n * item, a site's page, a documentation page — each with its screens. `templates`: the page templates — whole pages of a\n * site (a landing, a product, a docs hub, a long read, a changelog, pricing, status, an error…), each a distinct\n * composition of the blocks, words and content as props.\n */\nexport type Category = \"actions\" | \"forms\" | \"navigation\" | \"overlays\" | \"feedback\" | \"data-display\" | \"media\" | \"blocks\" | \"layouts\" | \"templates\" | \"layout\" | \"foundations\";\n\n/**\n * A named example: the file `registry/examples/<component>/<name>.tsx` (web) or\n * `registry/examples/<component>/native/<name>.tsx` (React Native) — at most 40 lines, imported by the stories and\n * shown (live, with its code) by the documentation.\n */\nexport interface Example {\n /** The file name, kebab-case (`primary`, `icon-button`). */\n readonly name: string;\n /** A short title (`Primary`, `Icon button`). */\n readonly title: string;\n /** One or two sentences: when this variant is the right one. */\n readonly description: string;\n /**\n * A template: a whole screen of a product composed from the platform (a layout and its blocks, real words, its phone\n * version thought out), copied as a starting point — at most 160 lines, listed on krizaka.com/docs/ui/templates.\n */\n readonly template?: boolean;\n}\n\n/** One platform's side of a component. */\nexport interface PlatformDocs {\n /** The names to import (`[\"Button\", \"IconButton\"]`): from `@krizaka/ui/<name>` on the web, `@krizaka/ui/native` in React Native. */\n readonly imports: readonly string[];\n /** The examples, in reading order; the first one is the component's preview. */\n readonly examples: readonly Example[];\n}\n\n/** The React Native side: the same concept, and what differs from the web API. */\nexport interface NativeDocs extends PlatformDocs {\n /**\n * The entry to import from when it is not `@krizaka/ui/native`: a component that needs an Expo module (an optional\n * peer) has its own, `@krizaka/ui/native/<name>`, so an app that does not use it never installs the module.\n */\n readonly entry?: string;\n /** What differs from the web API (`label` instead of children…); empty for a native-only component. */\n readonly differences: readonly string[];\n}\n\nexport interface KeyboardInteraction {\n /** The keys, as written on a keyboard: `Space`, `Enter`, `Arrow keys`, `Esc`, `Tab`. */\n readonly keys: string;\n /** What they do. */\n readonly action: string;\n}\n\nexport interface ComponentMeta {\n /** The display name: `Button`, `Dropdown menu`. */\n readonly title: string;\n /** One sentence: what it is. */\n readonly summary: string;\n readonly status: Status;\n readonly category: Category;\n /** Must match the sides below: `both` ⇔ `web` and `native`. */\n readonly platforms: Platform;\n /** When to reach for it: concrete situations, one per item. */\n readonly whenToUse: readonly string[];\n /** When not to — and what to use instead (`use`), when another component is the answer. */\n readonly whenNotToUse: readonly { readonly when: string; readonly use?: ComponentName }[];\n /** How to use it well: wording, placement, composition. */\n readonly bestPractices: readonly string[];\n readonly accessibility: {\n /** The keyboard, when it does more than Tab and Enter. */\n readonly keyboard: readonly KeyboardInteraction[];\n /** Roles, names and announcements: what the component does, what the app must provide. */\n readonly notes: readonly string[];\n };\n /** The components a reader of this page should know about. */\n readonly related: readonly ComponentName[];\n readonly web?: PlatformDocs;\n readonly native?: NativeDocs;\n}\n"
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
"path": "tv.ts",
|
|
1491
|
+
"content": "import { createTV } from \"tailwind-variants\";\n\nimport { twMergeConfig } from \"./cn\";\n\n/** `tv` with the preset's utilities known to the merge engine (`twMergeConfig`): what a primitive wearing the material is styled with. */\nexport const tv = createTV({ twMergeConfig });\n"
|
|
1492
|
+
}
|
|
1493
|
+
],
|
|
1494
|
+
"dependencies": [
|
|
1495
|
+
"@krizaka/intl@^0.1.0",
|
|
1496
|
+
"tailwind-variants@^3.3.1"
|
|
1497
|
+
],
|
|
1498
|
+
"registryDependencies": [
|
|
1499
|
+
"cn",
|
|
1500
|
+
"popover"
|
|
1501
|
+
],
|
|
1502
|
+
"demo": {
|
|
1503
|
+
"path": "examples/date-picker/schedule.tsx",
|
|
1504
|
+
"content": "\"use client\";\n\nimport { DatePicker } from \"@krizaka/ui/date-picker\";\nimport * as React from \"react\";\n\n// `today` is fixed here for the documentation; leave it out in an app.\nexport default function DatePickerSchedule() {\n const [date, setDate] = React.useState<string | null>(\"2026-10-14\");\n return (\n <DatePicker\n label=\"Publish on\"\n locale=\"en-US\"\n value={date}\n onValueChange={setDate}\n min=\"2026-10-11\"\n today=\"2026-10-11\"\n clearable\n placeholder=\"Publish now\"\n hint=\"Your followers are told when it goes live.\"\n />\n );\n}\n"
|
|
1505
|
+
},
|
|
1506
|
+
"props": [
|
|
1507
|
+
{
|
|
1508
|
+
"component": "Calendar",
|
|
1509
|
+
"description": "A month grid to pick a date or the ends of a range, with the full keyboard of the APG date picker.",
|
|
1510
|
+
"props": [
|
|
1511
|
+
{
|
|
1512
|
+
"name": "locale",
|
|
1513
|
+
"type": "string",
|
|
1514
|
+
"default": null,
|
|
1515
|
+
"description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
|
|
1516
|
+
"required": true
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
"name": "autoFocus",
|
|
1520
|
+
"type": "boolean",
|
|
1521
|
+
"default": null,
|
|
1522
|
+
"description": "Moves the focus to the active day when it mounts (inside a popover).",
|
|
1523
|
+
"required": false
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
"name": "className",
|
|
1527
|
+
"type": "string",
|
|
1528
|
+
"default": null,
|
|
1529
|
+
"description": "Classes merged on the root.",
|
|
1530
|
+
"required": false
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
"name": "defaultMonth",
|
|
1534
|
+
"type": "string",
|
|
1535
|
+
"default": null,
|
|
1536
|
+
"description": "The month shown at first. Default: the value's, or today's.",
|
|
1537
|
+
"required": false
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
"name": "isDateDisabled",
|
|
1541
|
+
"type": "((date: string) => boolean)",
|
|
1542
|
+
"default": null,
|
|
1543
|
+
"description": "Refuses some dates (`(d) => isWeekend(d)`).",
|
|
1544
|
+
"required": false
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
"name": "labels",
|
|
1548
|
+
"type": "Partial<CalendarLabels>",
|
|
1549
|
+
"default": null,
|
|
1550
|
+
"description": "The words, passed translated; English by default.",
|
|
1551
|
+
"required": false
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
"name": "max",
|
|
1555
|
+
"type": "string",
|
|
1556
|
+
"default": null,
|
|
1557
|
+
"description": "The latest date that can be picked.",
|
|
1558
|
+
"required": false
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
"name": "min",
|
|
1562
|
+
"type": "string",
|
|
1563
|
+
"default": null,
|
|
1564
|
+
"description": "The earliest date that can be picked.",
|
|
1565
|
+
"required": false
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
"name": "month",
|
|
1569
|
+
"type": "string",
|
|
1570
|
+
"default": null,
|
|
1571
|
+
"description": "The month shown (the first one, with `numberOfMonths={2}`), any day of it (controlled).",
|
|
1572
|
+
"required": false
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
"name": "numberOfMonths",
|
|
1576
|
+
"type": "1 | 2",
|
|
1577
|
+
"default": "1",
|
|
1578
|
+
"description": "One month, or two side by side (a period: from a phone's width up, they stack on a phone).",
|
|
1579
|
+
"required": false
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
"name": "onMonthChange",
|
|
1583
|
+
"type": "((month: string) => void)",
|
|
1584
|
+
"default": null,
|
|
1585
|
+
"description": "Called when the month shown changes.",
|
|
1586
|
+
"required": false
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
"name": "onSelect",
|
|
1590
|
+
"type": "((date: string) => void)",
|
|
1591
|
+
"default": null,
|
|
1592
|
+
"description": "Called with the day picked.",
|
|
1593
|
+
"required": false
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
"name": "range",
|
|
1597
|
+
"type": "DateRange | null",
|
|
1598
|
+
"default": null,
|
|
1599
|
+
"description": "The chosen range (range mode): its ends are lit, the days between are one band.",
|
|
1600
|
+
"required": false
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
"name": "today",
|
|
1604
|
+
"type": "string",
|
|
1605
|
+
"default": null,
|
|
1606
|
+
"description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
|
|
1607
|
+
"required": false
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
"name": "value",
|
|
1611
|
+
"type": "string | null",
|
|
1612
|
+
"default": null,
|
|
1613
|
+
"description": "The chosen date (one-date mode).",
|
|
1614
|
+
"required": false
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"name": "variant",
|
|
1618
|
+
"type": "\"plain\" | \"card\"",
|
|
1619
|
+
"default": null,
|
|
1620
|
+
"description": "`plain` (inside a popover, a card of the app) or `card` (the calendar is the interface: its own raised surface).",
|
|
1621
|
+
"required": false
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
"name": "weekStartsOn",
|
|
1625
|
+
"type": "number",
|
|
1626
|
+
"default": null,
|
|
1627
|
+
"description": "The first day of the week, 0 = Sunday. Default: the locale's.",
|
|
1628
|
+
"required": false
|
|
1629
|
+
}
|
|
1630
|
+
]
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
"component": "DateField",
|
|
1634
|
+
"description": "Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full.",
|
|
1635
|
+
"props": [
|
|
1636
|
+
{
|
|
1637
|
+
"name": "label",
|
|
1638
|
+
"type": "string",
|
|
1639
|
+
"default": null,
|
|
1640
|
+
"description": "The visible label, passed translated (the group's name).",
|
|
1641
|
+
"required": true
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
"name": "locale",
|
|
1645
|
+
"type": "string",
|
|
1646
|
+
"default": null,
|
|
1647
|
+
"description": "The BCP 47 locale: the order of the parts, the names of the months.",
|
|
1648
|
+
"required": true
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
"name": "autoComplete",
|
|
1652
|
+
"type": "\"off\" | \"bday\"",
|
|
1653
|
+
"default": null,
|
|
1654
|
+
"description": "`bday` for a date of birth: the browser offers to fill it.",
|
|
1655
|
+
"required": false
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
"name": "className",
|
|
1659
|
+
"type": "string",
|
|
1660
|
+
"default": null,
|
|
1661
|
+
"description": "Classes merged on the root.",
|
|
1662
|
+
"required": false
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
"name": "defaultValue",
|
|
1666
|
+
"type": "string",
|
|
1667
|
+
"default": "",
|
|
1668
|
+
"description": "The date at first, uncontrolled.",
|
|
1669
|
+
"required": false
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"name": "disabled",
|
|
1673
|
+
"type": "boolean",
|
|
1674
|
+
"default": null,
|
|
1675
|
+
"description": "Not available.",
|
|
1676
|
+
"required": false
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
"name": "error",
|
|
1680
|
+
"type": "string",
|
|
1681
|
+
"default": null,
|
|
1682
|
+
"description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
|
|
1683
|
+
"required": false
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
"name": "hint",
|
|
1687
|
+
"type": "string",
|
|
1688
|
+
"default": null,
|
|
1689
|
+
"description": "A help text under the field (\"For example, 27 3 1990\").",
|
|
1690
|
+
"required": false
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
"name": "labels",
|
|
1694
|
+
"type": "Partial<DateFieldLabels>",
|
|
1695
|
+
"default": null,
|
|
1696
|
+
"description": "The names of the parts, passed translated; English by default.",
|
|
1697
|
+
"required": false
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
"name": "name",
|
|
1701
|
+
"type": "string",
|
|
1702
|
+
"default": null,
|
|
1703
|
+
"description": "The name of a hidden input holding the value, for a plain `<form>` post.",
|
|
1704
|
+
"required": false
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
"name": "onBlur",
|
|
1708
|
+
"type": "(() => void)",
|
|
1709
|
+
"default": null,
|
|
1710
|
+
"description": "Called when the focus leaves the field.",
|
|
1711
|
+
"required": false
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"name": "onValueChange",
|
|
1715
|
+
"type": "((value: string) => void)",
|
|
1716
|
+
"default": null,
|
|
1717
|
+
"description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
|
|
1718
|
+
"required": false
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
"name": "value",
|
|
1722
|
+
"type": "string",
|
|
1723
|
+
"default": null,
|
|
1724
|
+
"description": "The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides.",
|
|
1725
|
+
"required": false
|
|
1726
|
+
}
|
|
1727
|
+
]
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
"component": "DateWheel",
|
|
1731
|
+
"description": "Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date.",
|
|
1732
|
+
"props": [
|
|
1733
|
+
{
|
|
1734
|
+
"name": "label",
|
|
1735
|
+
"type": "string",
|
|
1736
|
+
"default": null,
|
|
1737
|
+
"description": "The visible label, passed translated (the group's name).",
|
|
1738
|
+
"required": true
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
"name": "locale",
|
|
1742
|
+
"type": "string",
|
|
1743
|
+
"default": null,
|
|
1744
|
+
"description": "The BCP 47 locale: the order of the parts, the names of the months.",
|
|
1745
|
+
"required": true
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
"name": "className",
|
|
1749
|
+
"type": "string",
|
|
1750
|
+
"default": null,
|
|
1751
|
+
"description": "Classes merged on the root.",
|
|
1752
|
+
"required": false
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
"name": "defaultValue",
|
|
1756
|
+
"type": "string | null",
|
|
1757
|
+
"default": "null",
|
|
1758
|
+
"description": "The date at first, uncontrolled.",
|
|
1759
|
+
"required": false
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
"name": "defaultView",
|
|
1763
|
+
"type": "string",
|
|
1764
|
+
"default": null,
|
|
1765
|
+
"description": "Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed).",
|
|
1766
|
+
"required": false
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
"name": "disabled",
|
|
1770
|
+
"type": "boolean",
|
|
1771
|
+
"default": null,
|
|
1772
|
+
"description": "Not available.",
|
|
1773
|
+
"required": false
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
"name": "error",
|
|
1777
|
+
"type": "string",
|
|
1778
|
+
"default": null,
|
|
1779
|
+
"description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
|
|
1780
|
+
"required": false
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
"name": "hint",
|
|
1784
|
+
"type": "string",
|
|
1785
|
+
"default": null,
|
|
1786
|
+
"description": "A help text under the field (\"For example, 27 3 1990\").",
|
|
1787
|
+
"required": false
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
"name": "labels",
|
|
1791
|
+
"type": "Partial<DateFieldLabels>",
|
|
1792
|
+
"default": null,
|
|
1793
|
+
"description": "The names of the parts, passed translated; English by default.",
|
|
1794
|
+
"required": false
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
"name": "max",
|
|
1798
|
+
"type": "string",
|
|
1799
|
+
"default": null,
|
|
1800
|
+
"description": "The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today.",
|
|
1801
|
+
"required": false
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
"name": "min",
|
|
1805
|
+
"type": "string",
|
|
1806
|
+
"default": null,
|
|
1807
|
+
"description": "The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago.",
|
|
1808
|
+
"required": false
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
"name": "name",
|
|
1812
|
+
"type": "string",
|
|
1813
|
+
"default": null,
|
|
1814
|
+
"description": "The name of a hidden input holding the value, for a plain `<form>` post.",
|
|
1815
|
+
"required": false
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
"name": "onValueChange",
|
|
1819
|
+
"type": "((value: string) => void)",
|
|
1820
|
+
"default": null,
|
|
1821
|
+
"description": "Called with the date under the band, always a real date within `min` / `max`.",
|
|
1822
|
+
"required": false
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
"name": "value",
|
|
1826
|
+
"type": "string | null",
|
|
1827
|
+
"default": null,
|
|
1828
|
+
"description": "The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`).",
|
|
1829
|
+
"required": false
|
|
1830
|
+
}
|
|
1831
|
+
]
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
"component": "DatePicker",
|
|
1835
|
+
"description": "A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value.",
|
|
1836
|
+
"props": [
|
|
1837
|
+
{
|
|
1838
|
+
"name": "label",
|
|
1839
|
+
"type": "string",
|
|
1840
|
+
"default": null,
|
|
1841
|
+
"description": "The visible label, passed translated (also the button's accessible name, with the value).",
|
|
1842
|
+
"required": true
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
572
1845
|
"name": "locale",
|
|
573
1846
|
"type": "string",
|
|
574
1847
|
"default": null,
|
|
@@ -576,12 +1849,151 @@
|
|
|
576
1849
|
"required": true
|
|
577
1850
|
},
|
|
578
1851
|
{
|
|
579
|
-
"name": "
|
|
1852
|
+
"name": "className",
|
|
1853
|
+
"type": "string",
|
|
1854
|
+
"default": null,
|
|
1855
|
+
"description": "Classes merged on the root.",
|
|
1856
|
+
"required": false
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
"name": "clearable",
|
|
580
1860
|
"type": "boolean",
|
|
581
1861
|
"default": null,
|
|
582
|
-
"description": "
|
|
1862
|
+
"description": "Adds a button that empties the value.",
|
|
1863
|
+
"required": false
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
"name": "defaultOpen",
|
|
1867
|
+
"type": "boolean",
|
|
1868
|
+
"default": "false",
|
|
1869
|
+
"description": "Opens the calendar at first (a story, a test).",
|
|
1870
|
+
"required": false
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
"name": "defaultValue",
|
|
1874
|
+
"type": "string | null",
|
|
1875
|
+
"default": "null",
|
|
1876
|
+
"description": "The chosen date at first, uncontrolled.",
|
|
1877
|
+
"required": false
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
"name": "disabled",
|
|
1881
|
+
"type": "boolean",
|
|
1882
|
+
"default": null,
|
|
1883
|
+
"description": "Not available.",
|
|
1884
|
+
"required": false
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
"name": "error",
|
|
1888
|
+
"type": "string",
|
|
1889
|
+
"default": null,
|
|
1890
|
+
"description": "An error under the field: marks it invalid and is read with it.",
|
|
1891
|
+
"required": false
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
"name": "format",
|
|
1895
|
+
"type": "\"short\" | \"medium\" | \"long\"",
|
|
1896
|
+
"default": null,
|
|
1897
|
+
"description": "`short` · `medium` (default) · `long`: how the button writes the date.",
|
|
1898
|
+
"required": false
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
"name": "hint",
|
|
1902
|
+
"type": "string",
|
|
1903
|
+
"default": null,
|
|
1904
|
+
"description": "A help text under the field.",
|
|
1905
|
+
"required": false
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
"name": "isDateDisabled",
|
|
1909
|
+
"type": "((date: string) => boolean)",
|
|
1910
|
+
"default": null,
|
|
1911
|
+
"description": "Refuses some dates (`(d) => isWeekend(d)`).",
|
|
583
1912
|
"required": false
|
|
584
1913
|
},
|
|
1914
|
+
{
|
|
1915
|
+
"name": "labels",
|
|
1916
|
+
"type": "Partial<DatePickerLabels>",
|
|
1917
|
+
"default": null,
|
|
1918
|
+
"description": "The words, passed translated; English by default.",
|
|
1919
|
+
"required": false
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
"name": "max",
|
|
1923
|
+
"type": "string",
|
|
1924
|
+
"default": null,
|
|
1925
|
+
"description": "The latest date that can be picked.",
|
|
1926
|
+
"required": false
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
"name": "min",
|
|
1930
|
+
"type": "string",
|
|
1931
|
+
"default": null,
|
|
1932
|
+
"description": "The earliest date that can be picked.",
|
|
1933
|
+
"required": false
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
"name": "name",
|
|
1937
|
+
"type": "string",
|
|
1938
|
+
"default": null,
|
|
1939
|
+
"description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
|
|
1940
|
+
"required": false
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
"name": "onValueChange",
|
|
1944
|
+
"type": "((value: string | null) => void)",
|
|
1945
|
+
"default": null,
|
|
1946
|
+
"description": "Called with the date picked, or `null` when cleared.",
|
|
1947
|
+
"required": false
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
"name": "placeholder",
|
|
1951
|
+
"type": "string",
|
|
1952
|
+
"default": null,
|
|
1953
|
+
"description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
|
|
1954
|
+
"required": false
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"name": "today",
|
|
1958
|
+
"type": "string",
|
|
1959
|
+
"default": null,
|
|
1960
|
+
"description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
|
|
1961
|
+
"required": false
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
"name": "value",
|
|
1965
|
+
"type": "string | null",
|
|
1966
|
+
"default": null,
|
|
1967
|
+
"description": "The chosen date, ISO (controlled); `null` for none.",
|
|
1968
|
+
"required": false
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
"name": "weekStartsOn",
|
|
1972
|
+
"type": "number",
|
|
1973
|
+
"default": null,
|
|
1974
|
+
"description": "The first day of the week, 0 = Sunday. Default: the locale's.",
|
|
1975
|
+
"required": false
|
|
1976
|
+
}
|
|
1977
|
+
]
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
"component": "DateRangePicker",
|
|
1981
|
+
"description": "A period field: two clicks on one calendar (the days between tinted as you hover), optional presets.",
|
|
1982
|
+
"props": [
|
|
1983
|
+
{
|
|
1984
|
+
"name": "label",
|
|
1985
|
+
"type": "string",
|
|
1986
|
+
"default": null,
|
|
1987
|
+
"description": "The visible label, passed translated (also the button's accessible name, with the value).",
|
|
1988
|
+
"required": true
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
"name": "locale",
|
|
1992
|
+
"type": "string",
|
|
1993
|
+
"default": null,
|
|
1994
|
+
"description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
|
|
1995
|
+
"required": true
|
|
1996
|
+
},
|
|
585
1997
|
{
|
|
586
1998
|
"name": "className",
|
|
587
1999
|
"type": "string",
|
|
@@ -590,10 +2002,52 @@
|
|
|
590
2002
|
"required": false
|
|
591
2003
|
},
|
|
592
2004
|
{
|
|
593
|
-
"name": "
|
|
2005
|
+
"name": "clearable",
|
|
2006
|
+
"type": "boolean",
|
|
2007
|
+
"default": null,
|
|
2008
|
+
"description": "Adds a button that empties the value.",
|
|
2009
|
+
"required": false
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
"name": "defaultOpen",
|
|
2013
|
+
"type": "boolean",
|
|
2014
|
+
"default": "false",
|
|
2015
|
+
"description": "Opens the calendar at first (a story, a test).",
|
|
2016
|
+
"required": false
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
"name": "defaultValue",
|
|
2020
|
+
"type": "{ start: string; end: string; } | null",
|
|
2021
|
+
"default": "null",
|
|
2022
|
+
"description": "The range at first, uncontrolled.",
|
|
2023
|
+
"required": false
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
"name": "disabled",
|
|
2027
|
+
"type": "boolean",
|
|
2028
|
+
"default": null,
|
|
2029
|
+
"description": "Not available.",
|
|
2030
|
+
"required": false
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
"name": "error",
|
|
594
2034
|
"type": "string",
|
|
595
2035
|
"default": null,
|
|
596
|
-
"description": "
|
|
2036
|
+
"description": "An error under the field: marks it invalid and is read with it.",
|
|
2037
|
+
"required": false
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
"name": "format",
|
|
2041
|
+
"type": "\"short\" | \"medium\" | \"long\"",
|
|
2042
|
+
"default": null,
|
|
2043
|
+
"description": "`short` · `medium` (default) · `long`: how the button writes the date.",
|
|
2044
|
+
"required": false
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
"name": "hint",
|
|
2048
|
+
"type": "string",
|
|
2049
|
+
"default": null,
|
|
2050
|
+
"description": "A help text under the field.",
|
|
597
2051
|
"required": false
|
|
598
2052
|
},
|
|
599
2053
|
{
|
|
@@ -605,7 +2059,7 @@
|
|
|
605
2059
|
},
|
|
606
2060
|
{
|
|
607
2061
|
"name": "labels",
|
|
608
|
-
"type": "Partial<
|
|
2062
|
+
"type": "Partial<DatePickerLabels>",
|
|
609
2063
|
"default": null,
|
|
610
2064
|
"description": "The words, passed translated; English by default.",
|
|
611
2065
|
"required": false
|
|
@@ -625,31 +2079,38 @@
|
|
|
625
2079
|
"required": false
|
|
626
2080
|
},
|
|
627
2081
|
{
|
|
628
|
-
"name": "
|
|
2082
|
+
"name": "name",
|
|
629
2083
|
"type": "string",
|
|
630
2084
|
"default": null,
|
|
631
|
-
"description": "The
|
|
2085
|
+
"description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
|
|
632
2086
|
"required": false
|
|
633
2087
|
},
|
|
634
2088
|
{
|
|
635
|
-
"name": "
|
|
636
|
-
"type": "
|
|
2089
|
+
"name": "numberOfMonths",
|
|
2090
|
+
"type": "1 | 2",
|
|
2091
|
+
"default": "2",
|
|
2092
|
+
"description": "Two months side by side (default, from a phone's width up) or one.",
|
|
2093
|
+
"required": false
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
"name": "onValueChange",
|
|
2097
|
+
"type": "((value: { start: string; end: string; } | null) => void)",
|
|
637
2098
|
"default": null,
|
|
638
|
-
"description": "Called
|
|
2099
|
+
"description": "Called once both ends are chosen (or with `null` when cleared).",
|
|
639
2100
|
"required": false
|
|
640
2101
|
},
|
|
641
2102
|
{
|
|
642
|
-
"name": "
|
|
643
|
-
"type": "
|
|
2103
|
+
"name": "placeholder",
|
|
2104
|
+
"type": "string",
|
|
644
2105
|
"default": null,
|
|
645
|
-
"description": "
|
|
2106
|
+
"description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
|
|
646
2107
|
"required": false
|
|
647
2108
|
},
|
|
648
2109
|
{
|
|
649
|
-
"name": "
|
|
650
|
-
"type": "
|
|
2110
|
+
"name": "presets",
|
|
2111
|
+
"type": "readonly DateRangePreset[]",
|
|
651
2112
|
"default": null,
|
|
652
|
-
"description": "
|
|
2113
|
+
"description": "Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar.",
|
|
653
2114
|
"required": false
|
|
654
2115
|
},
|
|
655
2116
|
{
|
|
@@ -661,9 +2122,9 @@
|
|
|
661
2122
|
},
|
|
662
2123
|
{
|
|
663
2124
|
"name": "value",
|
|
664
|
-
"type": "string | null",
|
|
2125
|
+
"type": "{ start: string; end: string; } | null",
|
|
665
2126
|
"default": null,
|
|
666
|
-
"description": "The chosen
|
|
2127
|
+
"description": "The chosen range (controlled); `null` for none. `end` is never `null` in a committed value.",
|
|
667
2128
|
"required": false
|
|
668
2129
|
},
|
|
669
2130
|
{
|
|
@@ -676,21 +2137,21 @@
|
|
|
676
2137
|
]
|
|
677
2138
|
},
|
|
678
2139
|
{
|
|
679
|
-
"component": "
|
|
680
|
-
"description": "
|
|
2140
|
+
"component": "DayStrip",
|
|
2141
|
+
"description": "The days ahead in a row that scrolls sideways: one picked, today and the marked days shown.",
|
|
681
2142
|
"props": [
|
|
682
2143
|
{
|
|
683
2144
|
"name": "label",
|
|
684
2145
|
"type": "string",
|
|
685
2146
|
"default": null,
|
|
686
|
-
"description": "The
|
|
2147
|
+
"description": "The group's accessible name (\"Delivery day\"), passed translated.",
|
|
687
2148
|
"required": true
|
|
688
2149
|
},
|
|
689
2150
|
{
|
|
690
2151
|
"name": "locale",
|
|
691
2152
|
"type": "string",
|
|
692
2153
|
"default": null,
|
|
693
|
-
"description": "The BCP 47 locale of the
|
|
2154
|
+
"description": "The BCP 47 locale of the names and the dates: always explicit.",
|
|
694
2155
|
"required": true
|
|
695
2156
|
},
|
|
696
2157
|
{
|
|
@@ -701,142 +2162,197 @@
|
|
|
701
2162
|
"required": false
|
|
702
2163
|
},
|
|
703
2164
|
{
|
|
704
|
-
"name": "
|
|
705
|
-
"type": "
|
|
2165
|
+
"name": "days",
|
|
2166
|
+
"type": "number",
|
|
2167
|
+
"default": "14",
|
|
2168
|
+
"description": "How many days. Default 14.",
|
|
2169
|
+
"required": false
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
"name": "defaultValue",
|
|
2173
|
+
"type": "string | null",
|
|
2174
|
+
"default": "null",
|
|
2175
|
+
"description": "The chosen day at first, uncontrolled.",
|
|
2176
|
+
"required": false
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
"name": "isDateDisabled",
|
|
2180
|
+
"type": "((date: string) => boolean)",
|
|
706
2181
|
"default": null,
|
|
707
|
-
"description": "
|
|
2182
|
+
"description": "Refuses some days (closed, full): drawn dashed, skipped by the arrows.",
|
|
708
2183
|
"required": false
|
|
709
2184
|
},
|
|
710
2185
|
{
|
|
711
|
-
"name": "
|
|
712
|
-
"type": "boolean",
|
|
713
|
-
"default":
|
|
714
|
-
"description": "
|
|
2186
|
+
"name": "isDateMarked",
|
|
2187
|
+
"type": "((date: string) => boolean)",
|
|
2188
|
+
"default": null,
|
|
2189
|
+
"description": "Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name.",
|
|
715
2190
|
"required": false
|
|
716
2191
|
},
|
|
717
2192
|
{
|
|
718
|
-
"name": "
|
|
719
|
-
"type": "
|
|
720
|
-
"default":
|
|
721
|
-
"description": "The
|
|
2193
|
+
"name": "labels",
|
|
2194
|
+
"type": "Partial<DayStripLabels>",
|
|
2195
|
+
"default": null,
|
|
2196
|
+
"description": "The words, passed translated; English by default.",
|
|
722
2197
|
"required": false
|
|
723
2198
|
},
|
|
724
2199
|
{
|
|
725
|
-
"name": "
|
|
726
|
-
"type": "
|
|
2200
|
+
"name": "onValueChange",
|
|
2201
|
+
"type": "((value: string) => void)",
|
|
727
2202
|
"default": null,
|
|
728
|
-
"description": "
|
|
2203
|
+
"description": "Called with the day picked.",
|
|
729
2204
|
"required": false
|
|
730
2205
|
},
|
|
731
2206
|
{
|
|
732
|
-
"name": "
|
|
2207
|
+
"name": "showHeader",
|
|
2208
|
+
"type": "boolean",
|
|
2209
|
+
"default": "true",
|
|
2210
|
+
"description": "Shows the month of the chosen day (or of the first) and the page buttons above the strip.",
|
|
2211
|
+
"required": false
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
"name": "start",
|
|
733
2215
|
"type": "string",
|
|
734
2216
|
"default": null,
|
|
735
|
-
"description": "
|
|
2217
|
+
"description": "The first day of the strip. Default: today.",
|
|
736
2218
|
"required": false
|
|
737
2219
|
},
|
|
738
2220
|
{
|
|
739
|
-
"name": "
|
|
740
|
-
"type": "
|
|
2221
|
+
"name": "today",
|
|
2222
|
+
"type": "string",
|
|
741
2223
|
"default": null,
|
|
742
|
-
"description": "
|
|
2224
|
+
"description": "Today (marked by the node): inject it for a stable render or a test. Default: the device's date.",
|
|
743
2225
|
"required": false
|
|
744
2226
|
},
|
|
745
2227
|
{
|
|
746
|
-
"name": "
|
|
2228
|
+
"name": "value",
|
|
2229
|
+
"type": "string | null",
|
|
2230
|
+
"default": null,
|
|
2231
|
+
"description": "The chosen day, ISO (controlled); `null` for none.",
|
|
2232
|
+
"required": false
|
|
2233
|
+
}
|
|
2234
|
+
]
|
|
2235
|
+
},
|
|
2236
|
+
{
|
|
2237
|
+
"component": "MonthPicker",
|
|
2238
|
+
"description": "A month of a year: twelve months in a grid, the year above them.",
|
|
2239
|
+
"props": [
|
|
2240
|
+
{
|
|
2241
|
+
"name": "locale",
|
|
747
2242
|
"type": "string",
|
|
748
2243
|
"default": null,
|
|
749
|
-
"description": "
|
|
2244
|
+
"description": "The BCP 47 locale of the month names: always explicit.",
|
|
2245
|
+
"required": true
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
"name": "autoFocus",
|
|
2249
|
+
"type": "boolean",
|
|
2250
|
+
"default": null,
|
|
2251
|
+
"description": "Moves the focus to the active month when it mounts.",
|
|
750
2252
|
"required": false
|
|
751
2253
|
},
|
|
752
2254
|
{
|
|
753
|
-
"name": "
|
|
754
|
-
"type": "
|
|
2255
|
+
"name": "bare",
|
|
2256
|
+
"type": "boolean",
|
|
755
2257
|
"default": null,
|
|
756
|
-
"description": "
|
|
2258
|
+
"description": "Without its own year header (inside the calendar, which has one).",
|
|
757
2259
|
"required": false
|
|
758
2260
|
},
|
|
759
2261
|
{
|
|
760
|
-
"name": "
|
|
761
|
-
"type": "
|
|
2262
|
+
"name": "className",
|
|
2263
|
+
"type": "string",
|
|
762
2264
|
"default": null,
|
|
763
|
-
"description": "
|
|
2265
|
+
"description": "Classes merged on the root.",
|
|
764
2266
|
"required": false
|
|
765
2267
|
},
|
|
766
2268
|
{
|
|
767
|
-
"name": "
|
|
2269
|
+
"name": "current",
|
|
768
2270
|
"type": "string",
|
|
769
2271
|
"default": null,
|
|
770
|
-
"description": "
|
|
2272
|
+
"description": "This month (marked): inject it for a stable render or a test. Default: the device's.",
|
|
771
2273
|
"required": false
|
|
772
2274
|
},
|
|
773
2275
|
{
|
|
774
|
-
"name": "
|
|
2276
|
+
"name": "defaultValue",
|
|
2277
|
+
"type": "string | null",
|
|
2278
|
+
"default": "null",
|
|
2279
|
+
"description": "The chosen month at first, uncontrolled.",
|
|
2280
|
+
"required": false
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
"name": "labels",
|
|
2284
|
+
"type": "Partial<MonthPickerLabels>",
|
|
2285
|
+
"default": null,
|
|
2286
|
+
"description": "The words, passed translated; English by default.",
|
|
2287
|
+
"required": false
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
"name": "max",
|
|
775
2291
|
"type": "string",
|
|
776
2292
|
"default": null,
|
|
777
|
-
"description": "The
|
|
2293
|
+
"description": "The latest month that can be picked, `YYYY-MM`.",
|
|
778
2294
|
"required": false
|
|
779
2295
|
},
|
|
780
2296
|
{
|
|
781
|
-
"name": "
|
|
2297
|
+
"name": "min",
|
|
782
2298
|
"type": "string",
|
|
783
2299
|
"default": null,
|
|
784
|
-
"description": "The
|
|
2300
|
+
"description": "The earliest month that can be picked, `YYYY-MM`.",
|
|
785
2301
|
"required": false
|
|
786
2302
|
},
|
|
787
2303
|
{
|
|
788
2304
|
"name": "onValueChange",
|
|
789
2305
|
"type": "((value: string | null) => void)",
|
|
790
2306
|
"default": null,
|
|
791
|
-
"description": "Called with the
|
|
2307
|
+
"description": "Called with the month picked.",
|
|
792
2308
|
"required": false
|
|
793
2309
|
},
|
|
794
2310
|
{
|
|
795
|
-
"name": "
|
|
796
|
-
"type": "
|
|
2311
|
+
"name": "onYearChange",
|
|
2312
|
+
"type": "((year: number) => void)",
|
|
797
2313
|
"default": null,
|
|
798
|
-
"description": "
|
|
2314
|
+
"description": "Called when the year shown changes.",
|
|
799
2315
|
"required": false
|
|
800
2316
|
},
|
|
801
2317
|
{
|
|
802
|
-
"name": "
|
|
803
|
-
"type": "string",
|
|
2318
|
+
"name": "value",
|
|
2319
|
+
"type": "string | null",
|
|
804
2320
|
"default": null,
|
|
805
|
-
"description": "
|
|
2321
|
+
"description": "The chosen month, `YYYY-MM` (controlled); `null` for none.",
|
|
806
2322
|
"required": false
|
|
807
2323
|
},
|
|
808
2324
|
{
|
|
809
|
-
"name": "
|
|
810
|
-
"type": "
|
|
2325
|
+
"name": "variant",
|
|
2326
|
+
"type": "\"plain\" | \"card\"",
|
|
811
2327
|
"default": null,
|
|
812
|
-
"description": "
|
|
2328
|
+
"description": "`plain` or `card` (its own raised surface).",
|
|
813
2329
|
"required": false
|
|
814
2330
|
},
|
|
815
2331
|
{
|
|
816
|
-
"name": "
|
|
2332
|
+
"name": "year",
|
|
817
2333
|
"type": "number",
|
|
818
2334
|
"default": null,
|
|
819
|
-
"description": "The
|
|
2335
|
+
"description": "The year shown (controlled).",
|
|
820
2336
|
"required": false
|
|
821
2337
|
}
|
|
822
2338
|
]
|
|
823
2339
|
},
|
|
824
2340
|
{
|
|
825
|
-
"component": "
|
|
826
|
-
"description": "A
|
|
2341
|
+
"component": "TimePicker",
|
|
2342
|
+
"description": "A time of day: wheels to set one, or slots to book one.",
|
|
827
2343
|
"props": [
|
|
828
2344
|
{
|
|
829
2345
|
"name": "label",
|
|
830
2346
|
"type": "string",
|
|
831
2347
|
"default": null,
|
|
832
|
-
"description": "The
|
|
2348
|
+
"description": "The accessible name of the picker (\"Pick-up time\"), passed translated.",
|
|
833
2349
|
"required": true
|
|
834
2350
|
},
|
|
835
2351
|
{
|
|
836
2352
|
"name": "locale",
|
|
837
2353
|
"type": "string",
|
|
838
2354
|
"default": null,
|
|
839
|
-
"description": "The BCP 47 locale
|
|
2355
|
+
"description": "The BCP 47 locale: how times are written, and whether the day has a period (AM/PM).",
|
|
840
2356
|
"required": true
|
|
841
2357
|
},
|
|
842
2358
|
{
|
|
@@ -847,129 +2363,188 @@
|
|
|
847
2363
|
"required": false
|
|
848
2364
|
},
|
|
849
2365
|
{
|
|
850
|
-
"name": "
|
|
851
|
-
"type": "
|
|
2366
|
+
"name": "defaultValue",
|
|
2367
|
+
"type": "string | null",
|
|
852
2368
|
"default": null,
|
|
853
|
-
"description": "
|
|
2369
|
+
"description": "The time at first, uncontrolled.",
|
|
854
2370
|
"required": false
|
|
855
2371
|
},
|
|
856
2372
|
{
|
|
857
|
-
"name": "
|
|
2373
|
+
"name": "disabled",
|
|
858
2374
|
"type": "boolean",
|
|
859
|
-
"default":
|
|
860
|
-
"description": "
|
|
2375
|
+
"default": null,
|
|
2376
|
+
"description": "Not available.",
|
|
861
2377
|
"required": false
|
|
862
2378
|
},
|
|
863
2379
|
{
|
|
864
|
-
"name": "
|
|
865
|
-
"type": "{
|
|
866
|
-
"default":
|
|
867
|
-
"description": "
|
|
2380
|
+
"name": "groupLabels",
|
|
2381
|
+
"type": "{ morning: string; afternoon: string; evening: string; }",
|
|
2382
|
+
"default": null,
|
|
2383
|
+
"description": "Splits the slots under headings by part of the day: `{ morning: \"Morning\", afternoon: \"Afternoon\", evening: \"Evening\" }`.",
|
|
868
2384
|
"required": false
|
|
869
2385
|
},
|
|
870
2386
|
{
|
|
871
|
-
"name": "
|
|
2387
|
+
"name": "hour12",
|
|
872
2388
|
"type": "boolean",
|
|
873
2389
|
"default": null,
|
|
874
|
-
"description": "
|
|
2390
|
+
"description": "Forces 12 or 24 hours. Default: the locale's.",
|
|
875
2391
|
"required": false
|
|
876
2392
|
},
|
|
877
2393
|
{
|
|
878
|
-
"name": "
|
|
879
|
-
"type": "string",
|
|
2394
|
+
"name": "isTimeDisabled",
|
|
2395
|
+
"type": "((time: string) => boolean)",
|
|
880
2396
|
"default": null,
|
|
881
|
-
"description": "
|
|
2397
|
+
"description": "Refuses some slots (taken, too soon): drawn struck through, skipped by the arrows.",
|
|
882
2398
|
"required": false
|
|
883
2399
|
},
|
|
884
2400
|
{
|
|
885
|
-
"name": "
|
|
886
|
-
"type": "
|
|
2401
|
+
"name": "labels",
|
|
2402
|
+
"type": "Partial<TimePickerLabels>",
|
|
887
2403
|
"default": null,
|
|
888
|
-
"description": "
|
|
2404
|
+
"description": "The words of the wheels, passed translated; English by default.",
|
|
889
2405
|
"required": false
|
|
890
2406
|
},
|
|
891
2407
|
{
|
|
892
|
-
"name": "
|
|
893
|
-
"type": "
|
|
2408
|
+
"name": "minuteStep",
|
|
2409
|
+
"type": "number",
|
|
894
2410
|
"default": null,
|
|
895
|
-
"description": "
|
|
2411
|
+
"description": "The minutes' step: 1, 5 (default), 10, 15, 30.",
|
|
896
2412
|
"required": false
|
|
897
2413
|
},
|
|
898
2414
|
{
|
|
899
|
-
"name": "
|
|
900
|
-
"type": "((
|
|
2415
|
+
"name": "onValueChange",
|
|
2416
|
+
"type": "((value: string) => void)",
|
|
901
2417
|
"default": null,
|
|
902
|
-
"description": "
|
|
2418
|
+
"description": "Called with the time chosen.",
|
|
903
2419
|
"required": false
|
|
904
2420
|
},
|
|
905
2421
|
{
|
|
906
|
-
"name": "
|
|
907
|
-
"type": "
|
|
2422
|
+
"name": "slots",
|
|
2423
|
+
"type": "readonly string[]",
|
|
908
2424
|
"default": null,
|
|
909
|
-
"description": "The
|
|
2425
|
+
"description": "The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`).",
|
|
910
2426
|
"required": false
|
|
911
2427
|
},
|
|
912
2428
|
{
|
|
913
|
-
"name": "
|
|
914
|
-
"type": "string",
|
|
2429
|
+
"name": "value",
|
|
2430
|
+
"type": "string | null",
|
|
915
2431
|
"default": null,
|
|
916
|
-
"description": "The
|
|
2432
|
+
"description": "The time, `HH:mm` on 24 hours (controlled); `null` for none.",
|
|
917
2433
|
"required": false
|
|
918
2434
|
},
|
|
919
2435
|
{
|
|
920
|
-
"name": "
|
|
2436
|
+
"name": "variant",
|
|
2437
|
+
"type": "\"slots\" | \"wheel\"",
|
|
2438
|
+
"default": null,
|
|
2439
|
+
"description": "`wheel`: hours and minutes (and the period) as wheels — a time you set (a reminder, an alarm).\n`slots`: the free times as buttons — a booking, a delivery window, an appointment.",
|
|
2440
|
+
"required": false
|
|
2441
|
+
}
|
|
2442
|
+
]
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
"component": "Wheel",
|
|
2446
|
+
"description": "One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values.",
|
|
2447
|
+
"props": [
|
|
2448
|
+
{
|
|
2449
|
+
"name": "label",
|
|
921
2450
|
"type": "string",
|
|
922
2451
|
"default": null,
|
|
923
|
-
"description": "The
|
|
2452
|
+
"description": "The column's accessible name (\"Hour\", \"Month\"), passed translated.",
|
|
2453
|
+
"required": true
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
"name": "options",
|
|
2457
|
+
"type": "readonly WheelOption[]",
|
|
2458
|
+
"default": null,
|
|
2459
|
+
"description": "The values, in order.",
|
|
2460
|
+
"required": true
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
"name": "className",
|
|
2464
|
+
"type": "string",
|
|
2465
|
+
"default": null,
|
|
2466
|
+
"description": "Classes merged on the column (a width: `w-20`).",
|
|
924
2467
|
"required": false
|
|
925
2468
|
},
|
|
926
2469
|
{
|
|
927
|
-
"name": "
|
|
2470
|
+
"name": "defaultValue",
|
|
928
2471
|
"type": "string",
|
|
929
2472
|
"default": null,
|
|
930
|
-
"description": "The
|
|
2473
|
+
"description": "The chosen value at first, uncontrolled. Default: the first one.",
|
|
931
2474
|
"required": false
|
|
932
2475
|
},
|
|
933
2476
|
{
|
|
934
|
-
"name": "
|
|
935
|
-
"type": "
|
|
2477
|
+
"name": "disabled",
|
|
2478
|
+
"type": "boolean",
|
|
936
2479
|
"default": null,
|
|
937
|
-
"description": "
|
|
2480
|
+
"description": "Not available.",
|
|
938
2481
|
"required": false
|
|
939
2482
|
},
|
|
940
2483
|
{
|
|
941
|
-
"name": "
|
|
942
|
-
"type": "string",
|
|
2484
|
+
"name": "onValueChange",
|
|
2485
|
+
"type": "((value: string) => void)",
|
|
943
2486
|
"default": null,
|
|
944
|
-
"description": "
|
|
2487
|
+
"description": "Called with the value that comes to rest under the band.",
|
|
945
2488
|
"required": false
|
|
946
2489
|
},
|
|
947
2490
|
{
|
|
948
|
-
"name": "
|
|
949
|
-
"type": "
|
|
2491
|
+
"name": "value",
|
|
2492
|
+
"type": "string | null",
|
|
950
2493
|
"default": null,
|
|
951
|
-
"description": "
|
|
2494
|
+
"description": "The chosen value (controlled).",
|
|
952
2495
|
"required": false
|
|
2496
|
+
}
|
|
2497
|
+
]
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
"component": "WheelDivider",
|
|
2501
|
+
"description": "A divider between two wheels (`:` between the hours and the minutes).",
|
|
2502
|
+
"props": []
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
"component": "WheelPicker",
|
|
2506
|
+
"description": "The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`.",
|
|
2507
|
+
"props": [
|
|
2508
|
+
{
|
|
2509
|
+
"name": "children",
|
|
2510
|
+
"type": "ReactNode",
|
|
2511
|
+
"default": null,
|
|
2512
|
+
"description": "The wheels (`Wheel`), and dividers (`WheelDivider`) between them.",
|
|
2513
|
+
"required": true
|
|
953
2514
|
},
|
|
954
2515
|
{
|
|
955
|
-
"name": "
|
|
2516
|
+
"name": "label",
|
|
956
2517
|
"type": "string",
|
|
957
2518
|
"default": null,
|
|
958
|
-
"description": "
|
|
2519
|
+
"description": "The group's accessible name (\"Departure time\"), passed translated.",
|
|
2520
|
+
"required": true
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
"name": "aria-describedby",
|
|
2524
|
+
"type": "string",
|
|
2525
|
+
"default": null,
|
|
2526
|
+
"description": "The help text or the error that describes the group (their ids).",
|
|
959
2527
|
"required": false
|
|
960
2528
|
},
|
|
961
2529
|
{
|
|
962
|
-
"name": "
|
|
963
|
-
"type": "
|
|
2530
|
+
"name": "className",
|
|
2531
|
+
"type": "string",
|
|
964
2532
|
"default": null,
|
|
965
|
-
"description": "
|
|
2533
|
+
"description": "Classes merged on the root.",
|
|
966
2534
|
"required": false
|
|
967
2535
|
},
|
|
968
2536
|
{
|
|
969
|
-
"name": "
|
|
970
|
-
"type": "
|
|
2537
|
+
"name": "invalid",
|
|
2538
|
+
"type": "boolean",
|
|
971
2539
|
"default": null,
|
|
972
|
-
"description": "
|
|
2540
|
+
"description": "Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`.",
|
|
2541
|
+
"required": false
|
|
2542
|
+
},
|
|
2543
|
+
{
|
|
2544
|
+
"name": "visible",
|
|
2545
|
+
"type": "3 | 5 | 7",
|
|
2546
|
+
"default": "5",
|
|
2547
|
+
"description": "How many values are visible: 3, 5 (default) or 7.",
|
|
973
2548
|
"required": false
|
|
974
2549
|
}
|
|
975
2550
|
]
|