@krizaka/ui 3.1.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.
Files changed (135) hide show
  1. package/dist/achievement.js +1 -1
  2. package/dist/action-bar.js +1 -1
  3. package/dist/app-shell.js +1 -1
  4. package/dist/argument-card.js +2 -2
  5. package/dist/card-grid.js +1 -1
  6. package/dist/card.js +1 -1
  7. package/dist/choice-grid.js +1 -1
  8. package/dist/{chunk-OGJI5X4W.js → chunk-2PUDI7JS.js} +2 -2
  9. package/dist/chunk-4LOMIQVX.js +21 -0
  10. package/dist/chunk-4LOMIQVX.js.map +1 -0
  11. package/dist/{chunk-K3SWZXYT.js → chunk-DGQMDJXK.js} +2 -2
  12. package/dist/chunk-G3ETON2A.js +22 -0
  13. package/dist/chunk-G3ETON2A.js.map +1 -0
  14. package/dist/chunk-I5RVM5CW.js +27 -0
  15. package/dist/chunk-I5RVM5CW.js.map +1 -0
  16. package/dist/chunk-IZ7MV6HM.js +26 -0
  17. package/dist/chunk-IZ7MV6HM.js.map +1 -0
  18. package/dist/chunk-ZNMFG62E.js +20 -0
  19. package/dist/chunk-ZNMFG62E.js.map +1 -0
  20. package/dist/cn.d.ts +9 -1
  21. package/dist/cn.js +5 -3
  22. package/dist/code-block.js +1 -1
  23. package/dist/command.js +1 -1
  24. package/dist/confirm-button.js +1 -1
  25. package/dist/continue-prompt.js +1 -1
  26. package/dist/cta-panel.js +1 -1
  27. package/dist/data-table.js +2 -2
  28. package/dist/date-picker.d.ts +650 -61
  29. package/dist/date-picker.js +918 -122
  30. package/dist/date-picker.js.map +1 -1
  31. package/dist/decision-list.js +1 -1
  32. package/dist/dropdown-menu.js +1 -1
  33. package/dist/error-page.js +1 -1
  34. package/dist/field.js +1 -1
  35. package/dist/filter-bar.js +2 -2
  36. package/dist/form.js.map +1 -1
  37. package/dist/gallery-page.js +1 -1
  38. package/dist/glow-card.js +1 -1
  39. package/dist/goal-gauge.js +1 -1
  40. package/dist/how-it-works-page.js +1 -1
  41. package/dist/landscape.js +1 -1
  42. package/dist/link-card.js +1 -1
  43. package/dist/longform-page.js +1 -1
  44. package/dist/marquee.js +1 -1
  45. package/dist/mascot-quote.js +1 -1
  46. package/dist/motion.css +18 -1
  47. package/dist/native/form.js.map +1 -1
  48. package/dist/native.d.ts +239 -2
  49. package/dist/native.js +955 -379
  50. package/dist/native.js.map +1 -1
  51. package/dist/nav-bar.js +1 -1
  52. package/dist/page-hero.js +2 -2
  53. package/dist/page.js +2 -2
  54. package/dist/popover.js +1 -1
  55. package/dist/pricing-page.js +1 -1
  56. package/dist/product-page.js +1 -1
  57. package/dist/profile-card.js +1 -1
  58. package/dist/promise-card.js +1 -1
  59. package/dist/scroll-stage.js +1 -1
  60. package/dist/stage-banner.js +1 -1
  61. package/dist/terminal-replay.js +1 -1
  62. package/dist/toast.js +1 -1
  63. package/dist/toc.js +1 -1
  64. package/dist/tooltip.js +1 -1
  65. package/package.json +7 -7
  66. package/registry/cn.json +1 -1
  67. package/registry/date-picker.json +1721 -284
  68. package/registry/examples/date-picker/birth-date-wheel.d.ts +4 -0
  69. package/registry/examples/date-picker/birth-date-wheel.js +13 -0
  70. package/registry/examples/date-picker/birth-date-wheel.tsx +11 -0
  71. package/registry/examples/date-picker/birth-date.d.ts +4 -0
  72. package/registry/examples/date-picker/birth-date.js +25 -0
  73. package/registry/examples/date-picker/birth-date.tsx +21 -0
  74. package/registry/examples/date-picker/calendar.js +1 -1
  75. package/registry/examples/date-picker/calendar.tsx +2 -6
  76. package/registry/examples/date-picker/day-strip.d.ts +4 -0
  77. package/registry/examples/date-picker/day-strip.js +28 -0
  78. package/registry/examples/date-picker/day-strip.tsx +27 -0
  79. package/registry/examples/date-picker/month-year.d.ts +4 -0
  80. package/registry/examples/date-picker/month-year.js +13 -0
  81. package/registry/examples/date-picker/month-year.tsx +10 -0
  82. package/registry/examples/date-picker/native/birth-date-wheel.d.ts +4 -0
  83. package/registry/examples/date-picker/native/birth-date-wheel.dark.png +0 -0
  84. package/registry/examples/date-picker/native/birth-date-wheel.js +13 -0
  85. package/registry/examples/date-picker/native/birth-date-wheel.light.png +0 -0
  86. package/registry/examples/date-picker/native/birth-date-wheel.tsx +9 -0
  87. package/registry/examples/date-picker/native/calendar.d.ts +4 -0
  88. package/registry/examples/date-picker/native/calendar.dark.png +0 -0
  89. package/registry/examples/date-picker/native/calendar.js +13 -0
  90. package/registry/examples/date-picker/native/calendar.light.png +0 -0
  91. package/registry/examples/date-picker/native/calendar.tsx +8 -0
  92. package/registry/examples/date-picker/native/day-strip.d.ts +4 -0
  93. package/registry/examples/date-picker/native/day-strip.dark.png +0 -0
  94. package/registry/examples/date-picker/native/day-strip.js +29 -0
  95. package/registry/examples/date-picker/native/day-strip.light.png +0 -0
  96. package/registry/examples/date-picker/native/day-strip.tsx +26 -0
  97. package/registry/examples/date-picker/native/month-year.d.ts +4 -0
  98. package/registry/examples/date-picker/native/month-year.dark.png +0 -0
  99. package/registry/examples/date-picker/native/month-year.js +13 -0
  100. package/registry/examples/date-picker/native/month-year.light.png +0 -0
  101. package/registry/examples/date-picker/native/month-year.tsx +8 -0
  102. package/registry/examples/date-picker/native/time-slots.d.ts +4 -0
  103. package/registry/examples/date-picker/native/time-slots.dark.png +0 -0
  104. package/registry/examples/date-picker/native/time-slots.js +15 -0
  105. package/registry/examples/date-picker/native/time-slots.light.png +0 -0
  106. package/registry/examples/date-picker/native/time-slots.tsx +15 -0
  107. package/registry/examples/date-picker/native/time-wheel.d.ts +4 -0
  108. package/registry/examples/date-picker/native/time-wheel.dark.png +0 -0
  109. package/registry/examples/date-picker/native/time-wheel.js +13 -0
  110. package/registry/examples/date-picker/native/time-wheel.light.png +0 -0
  111. package/registry/examples/date-picker/native/time-wheel.tsx +8 -0
  112. package/registry/examples/date-picker/time-slots.d.ts +4 -0
  113. package/registry/examples/date-picker/time-slots.js +26 -0
  114. package/registry/examples/date-picker/time-slots.tsx +25 -0
  115. package/registry/examples/date-picker/time-wheel.d.ts +4 -0
  116. package/registry/examples/date-picker/time-wheel.js +13 -0
  117. package/registry/examples/date-picker/time-wheel.tsx +10 -0
  118. package/registry/examples/date-picker/wheel.d.ts +4 -0
  119. package/registry/examples/date-picker/wheel.js +14 -0
  120. package/registry/examples/date-picker/wheel.tsx +16 -0
  121. package/registry/index.json +6 -5
  122. package/registry/theme.json +7 -0
  123. package/skills/date-picker/SKILL.md +43 -0
  124. package/dist/chunk-2USC2YT6.js +0 -17
  125. package/dist/chunk-2USC2YT6.js.map +0 -1
  126. package/dist/chunk-5AXLA4LK.js +0 -13
  127. package/dist/chunk-5AXLA4LK.js.map +0 -1
  128. package/dist/chunk-FJDSLNXK.js +0 -11
  129. package/dist/chunk-FJDSLNXK.js.map +0 -1
  130. package/dist/chunk-KNBYZGSQ.js +0 -11
  131. package/dist/chunk-KNBYZGSQ.js.map +0 -1
  132. package/dist/chunk-VCAAHCHL.js +0 -17
  133. package/dist/chunk-VCAAHCHL.js.map +0 -1
  134. /package/dist/{chunk-OGJI5X4W.js.map → chunk-2PUDI7JS.js.map} +0 -0
  135. /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": "A date or a period chosen on a calendar — ISO values (`2026-10-11`) the form, the address and the API share, read in the locale's own words through @krizaka/intl.",
6
- "description": "A date or a period chosen on a calendar — ISO values (`2026-10-11`) the form, the address and the API share, read in the locale's own words through @krizaka/intl.",
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
9
  "platforms": "both",
10
10
  "whenToUse": [
11
- "To schedule something on a day: a publication, an auction's end, a reminder.",
12
- "With `DateRangePicker` for a period: an earnings report, an audit log, a campaign — with presets.",
13
- "With `Calendar` alone when the month itself is the interface (availability, a booking)."
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 a date far away: three fields (day, month, year) are faster than paging through years.",
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`: month and day names, the first day of the week and the date's format follow it.",
27
- "Keep the value as an ISO date in state and in the address; format it only for display.",
28
- "Bound the choice with `min` / `max` and `isDateDisabled` rather than refusing after the fact.",
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": "Move by a day or a week."
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": "Previous or next month (with Shift: year)."
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 title, one day in the tab order, each day named by its full date.",
56
- "Today carries `aria-current=\"date\"`; chosen days `aria-selected`; unavailable days are disabled buttons.",
57
- "The month's title is a polite live region: a change of month is heard.",
58
- "The field is a button named by its `<label>`, with `aria-haspopup=\"dialog\"`; Radix returns the focus to it on close."
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 marked, the days between tinted.",
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,206 +710,769 @@
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
- "name": "calendar",
516
- "title": "Calendar alone",
517
- "description": "`Calendar` inline: the month is the interface, weekends unavailable.",
518
- "path": "examples/date-picker/calendar.tsx",
519
- "module": "@krizaka/ui/registry/examples/date-picker/calendar",
520
- "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: pick a day for a call, weekends unavailable.\nexport default function DatePickerCalendar() {\n const [day, setDay] = React.useState(\"2026-10-15\");\n return (\n <div className=\"inline-block rounded-xl border border-border-default bg-surface-1 p-3\">\n <Calendar locale=\"en-GB\" value={day} onSelect={setDay} isDateDisabled={weekend} min=\"2026-10-12\" today=\"2026-10-11\" />\n </div>\n );\n}\n"
521
- }
522
- ]
523
- },
524
- "native": {
525
- "entry": "@krizaka/ui/native",
526
- "import": "import { DateField, datePartsOrder, composeDate } from \"@krizaka/ui/native\";",
527
- "differences": [
528
- "`DateField` is a date typed as day, month and year (a memorable date: a birth date, an expiry), not a calendar: numeric keypads, the boxes in the locale's order, the next one focused when a box is full.",
529
- "The value is a `YYYY-MM-DD` string of what was typed (`\"\"` when empty), real date or not: the form's schema decides (and the server again). No native module."
530
- ],
531
- "props": [
532
- {
533
- "component": "DateField",
534
- "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.",
715
+ "component": "DayStrip",
716
+ "description": "The days ahead in a row that scrolls sideways: one picked, today and the marked days shown.",
535
717
  "props": [
536
718
  {
537
719
  "name": "label",
538
720
  "type": "string",
539
721
  "default": null,
540
- "description": "The visible label of the date, passed translated (the group's accessible name).",
722
+ "description": "The group's accessible name (\"Delivery day\"), passed translated.",
541
723
  "required": true
542
724
  },
543
725
  {
544
- "name": "autoComplete",
545
- "type": "\"off\" | \"bday\"",
726
+ "name": "locale",
727
+ "type": "string",
546
728
  "default": null,
547
- "description": "`bday` for a date of birth: the system offers to fill it.",
548
- "required": false
729
+ "description": "The BCP 47 locale of the names and the dates: always explicit.",
730
+ "required": true
549
731
  },
550
732
  {
551
- "name": "defaultValue",
733
+ "name": "className",
552
734
  "type": "string",
553
735
  "default": null,
554
- "description": "The date at first, uncontrolled.",
736
+ "description": "Classes merged on the root.",
555
737
  "required": false
556
738
  },
557
739
  {
558
- "name": "disabled",
559
- "type": "boolean",
560
- "default": null,
561
- "description": "Not available.",
740
+ "name": "days",
741
+ "type": "number",
742
+ "default": "14",
743
+ "description": "How many days. Default 14.",
562
744
  "required": false
563
745
  },
564
746
  {
565
- "name": "error",
566
- "type": "string",
567
- "default": null,
568
- "description": "An error under the boxes: a danger border and caption, read with them and announced. Replaces the hint.",
569
- "required": false
747
+ "name": "defaultValue",
748
+ "type": "string | null",
749
+ "default": "null",
750
+ "description": "The chosen day at first, uncontrolled.",
751
+ "required": false
570
752
  },
571
753
  {
572
- "name": "hint",
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",
573
790
  "type": "string",
574
791
  "default": null,
575
- "description": "A help text under the boxes (\"For example, 27 3 1990\").",
792
+ "description": "The first day of the strip. Default: today.",
576
793
  "required": false
577
794
  },
578
795
  {
579
- "name": "inputRef",
580
- "type": "Ref<TextInput>",
796
+ "name": "today",
797
+ "type": "string",
581
798
  "default": null,
582
- "description": "The first box (in the locale's order), to focus the field from the one before it.",
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.",
583
855
  "required": false
584
856
  },
585
857
  {
586
858
  "name": "labels",
587
- "type": "Partial<DateFieldLabels>",
859
+ "type": "Partial<MonthPickerLabels>",
588
860
  "default": null,
589
- "description": "The names of the boxes, passed translated; English by default.",
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).",
590
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
591
925
  },
592
926
  {
593
927
  "name": "locale",
594
928
  "type": "string",
595
- "default": "en-US",
596
- "description": "The locale that orders the boxes (day · month · year, or month · day · year). Default `en-US`.",
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.",
597
938
  "required": false
598
939
  },
599
940
  {
600
- "name": "onBlur",
601
- "type": "(() => void)",
941
+ "name": "defaultValue",
942
+ "type": "string | null",
602
943
  "default": null,
603
- "description": "Called when focus leaves the last box.",
944
+ "description": "The time at first, uncontrolled.",
604
945
  "required": false
605
946
  },
606
947
  {
607
- "name": "onSubmitEditing",
608
- "type": "(() => void)",
948
+ "name": "disabled",
949
+ "type": "boolean",
609
950
  "default": null,
610
- "description": "Called by the last box's return key (`returnKeyType`): move to the next field, or send the form.",
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.",
611
987
  "required": false
612
988
  },
613
989
  {
614
990
  "name": "onValueChange",
615
991
  "type": "((value: string) => void)",
616
992
  "default": null,
617
- "description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
993
+ "description": "Called with the time chosen.",
618
994
  "required": false
619
995
  },
620
996
  {
621
- "name": "returnKeyType",
622
- "type": "\"next\" | \"done\" | \"go\" | \"send\"",
623
- "default": "done",
624
- "description": "The last box's return key. Default `done`.",
997
+ "name": "slots",
998
+ "type": "readonly string[]",
999
+ "default": null,
1000
+ "description": "The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`).",
625
1001
  "required": false
626
1002
  },
627
1003
  {
628
- "name": "style",
629
- "type": "StyleProp<ViewStyle>",
1004
+ "name": "value",
1005
+ "type": "string | null",
630
1006
  "default": null,
631
- "description": "Styles merged last, on the field.",
1007
+ "description": "The time, `HH:mm` on 24 hours (controlled); `null` for none.",
632
1008
  "required": false
633
1009
  },
634
1010
  {
635
- "name": "value",
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",
636
1025
  "type": "string",
637
1026
  "default": null,
638
- "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.",
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).",
639
1070
  "required": false
640
1071
  }
641
1072
  ]
642
- }
643
- ],
644
- "examples": [
1073
+ },
645
1074
  {
646
- "name": "birth-date",
647
- "title": "Date of birth",
648
- "description": "Day, month and year with `autoComplete=\"bday\"`, and the error of an under-18 date.",
649
- "path": "examples/date-picker/native/birth-date.tsx",
650
- "module": "@krizaka/ui/registry/examples/date-picker/native/birth-date",
651
- "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",
652
- "screenshots": {
653
- "dark": "examples/date-picker/native/birth-date.dark.png",
654
- "light": "examples/date-picker/native/birth-date.light.png"
655
- }
656
- }
657
- ]
658
- },
659
- "files": [
660
- {
661
- "path": "date-picker/calendar.tsx",
662
- "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"
663
- },
664
- {
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
+ {
665
1446
  "path": "date-picker/date-picker.tsx",
666
- "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"
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"
667
1452
  },
668
1453
  {
669
1454
  "path": "date-picker/index.ts",
670
- "content": "/** @krizaka/ui/date-picker — `DatePicker`, `DateRangePicker`, `Calendar` (ISO dates, the locale's names through @krizaka/intl), the date helpers and slots. Client (\"use client\"). */\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 {\n DatePicker,\n datePicker,\n type DatePickerLabels,\n datePickerLabels,\n type DatePickerProps,\n DateRangePicker,\n type DateRangePickerProps,\n type DateRangePreset,\n} from \"./date-picker\";\n"
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"
671
1456
  },
672
1457
  {
673
1458
  "path": "date-picker/meta.ts",
674
- "content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Date picker\",\n summary: \"A date or a period chosen on a calendar — ISO values (`2026-10-11`) the form, the address and the API share, read in the locale's own words through @krizaka/intl.\",\n status: \"beta\",\n category: \"forms\",\n platforms: \"both\",\n whenToUse: [\n \"To schedule something on a day: a publication, an auction's end, a reminder.\",\n \"With `DateRangePicker` for a period: an earnings report, an audit log, a campaign — with presets.\",\n \"With `Calendar` alone when the month itself is the interface (availability, a booking).\",\n ],\n whenNotToUse: [\n { when: \"For a date of birth or a date far away: three fields (day, month, year) are faster than paging through years.\", 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`: month and day names, the first day of the week and the date's format follow it.\",\n \"Keep the value as an ISO date in state and in the address; format it only for display.\",\n \"Bound the choice with `min` / `max` and `isDateDisabled` rather than refusing after the fact.\",\n \"Offer presets on a period picker: most people want “Last 30 days”, not two clicks.\",\n ],\n accessibility: {\n keyboard: [\n { keys: \"Arrow keys\", action: \"Move by a day or a week.\" },\n { keys: \"Home / End\", action: \"First or last day of the week.\" },\n { keys: \"Page Up / Page Down\", action: \"Previous or next month (with Shift: year).\" },\n { keys: \"Enter / Space\", action: \"Pick the focused day.\" },\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 title, one day in the tab order, each day named by its full date.\",\n \"Today carries `aria-current=\\\"date\\\"`; chosen days `aria-selected`; unavailable days are disabled buttons.\",\n \"The month's title is a polite live region: a change of month is heard.\",\n \"The field is a button named by its `<label>`, with `aria-haspopup=\\\"dialog\\\"`; Radix returns the focus to it on close.\",\n ],\n },\n related: [\"field\", \"popover\", \"filter-bar\"],\n web: {\n imports: [\"DatePicker\", \"DateRangePicker\", \"Calendar\"],\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 marked, the chosen day filled.\" },\n { name: \"range\", title: \"Period with presets\", description: \"`DateRangePicker` for an earnings report: presets beside the calendar.\" },\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: \"Calendar alone\", description: \"`Calendar` inline: the month is the interface, weekends unavailable.\" },\n ],\n },\n native: {\n imports: [\"DateField\", \"datePartsOrder\", \"composeDate\"],\n differences: [\n \"`DateField` is a date typed as day, month and year (a memorable date: a birth date, an expiry), not a calendar: numeric keypads, the boxes in the locale's order, the next one focused when a box is full.\",\n \"The value is a `YYYY-MM-DD` string of what was typed (`\\\"\\\"` when empty), real date or not: the form's schema decides (and the server again). No native module.\",\n ],\n examples: [\n { name: \"birth-date\", title: \"Date of birth\", description: \"Day, month and year with `autoComplete=\\\"bday\\\"`, and the error of an under-18 date.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
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"
675
1464
  },
676
1465
  {
677
1466
  "path": "date-picker/parts.ts",
678
- "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"
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"
679
1476
  },
680
1477
  {
681
1478
  "path": "glyphs.tsx",
@@ -686,26 +1483,364 @@
686
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"
687
1484
  },
688
1485
  {
689
- "path": "meta.ts",
690
- "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"
691
- }
692
- ],
693
- "dependencies": [
694
- "@krizaka/intl@^0.1.0",
695
- "tailwind-variants@^3.3.1"
696
- ],
697
- "registryDependencies": [
698
- "popover"
699
- ],
700
- "demo": {
701
- "path": "examples/date-picker/schedule.tsx",
702
- "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"
703
- },
704
- "props": [
705
- {
706
- "component": "Calendar",
707
- "description": "A month grid to pick a date or the ends of a range, with the full keyboard of the APG date picker.",
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.",
708
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
+ },
709
1844
  {
710
1845
  "name": "locale",
711
1846
  "type": "string",
@@ -714,12 +1849,151 @@
714
1849
  "required": true
715
1850
  },
716
1851
  {
717
- "name": "autoFocus",
1852
+ "name": "className",
1853
+ "type": "string",
1854
+ "default": null,
1855
+ "description": "Classes merged on the root.",
1856
+ "required": false
1857
+ },
1858
+ {
1859
+ "name": "clearable",
718
1860
  "type": "boolean",
719
1861
  "default": null,
720
- "description": "Moves the focus to the active day when it mounts (inside a popover).",
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)`).",
721
1912
  "required": false
722
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
+ },
723
1997
  {
724
1998
  "name": "className",
725
1999
  "type": "string",
@@ -728,10 +2002,52 @@
728
2002
  "required": false
729
2003
  },
730
2004
  {
731
- "name": "defaultMonth",
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",
732
2034
  "type": "string",
733
2035
  "default": null,
734
- "description": "The month shown at first. Default: the value's, or today's.",
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.",
735
2051
  "required": false
736
2052
  },
737
2053
  {
@@ -743,7 +2059,7 @@
743
2059
  },
744
2060
  {
745
2061
  "name": "labels",
746
- "type": "Partial<CalendarLabels>",
2062
+ "type": "Partial<DatePickerLabels>",
747
2063
  "default": null,
748
2064
  "description": "The words, passed translated; English by default.",
749
2065
  "required": false
@@ -763,31 +2079,38 @@
763
2079
  "required": false
764
2080
  },
765
2081
  {
766
- "name": "month",
2082
+ "name": "name",
767
2083
  "type": "string",
768
2084
  "default": null,
769
- "description": "The month shown, any day of it (controlled).",
2085
+ "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
770
2086
  "required": false
771
2087
  },
772
2088
  {
773
- "name": "onMonthChange",
774
- "type": "((month: string) => void)",
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)",
775
2098
  "default": null,
776
- "description": "Called when the month shown changes.",
2099
+ "description": "Called once both ends are chosen (or with `null` when cleared).",
777
2100
  "required": false
778
2101
  },
779
2102
  {
780
- "name": "onSelect",
781
- "type": "((date: string) => void)",
2103
+ "name": "placeholder",
2104
+ "type": "string",
782
2105
  "default": null,
783
- "description": "Called with the day picked.",
2106
+ "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
784
2107
  "required": false
785
2108
  },
786
2109
  {
787
- "name": "range",
788
- "type": "DateRange | null",
2110
+ "name": "presets",
2111
+ "type": "readonly DateRangePreset[]",
789
2112
  "default": null,
790
- "description": "The chosen range (range mode): its ends are marked, the days between tinted.",
2113
+ "description": "Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar.",
791
2114
  "required": false
792
2115
  },
793
2116
  {
@@ -799,9 +2122,9 @@
799
2122
  },
800
2123
  {
801
2124
  "name": "value",
802
- "type": "string | null",
2125
+ "type": "{ start: string; end: string; } | null",
803
2126
  "default": null,
804
- "description": "The chosen date (one-date mode).",
2127
+ "description": "The chosen range (controlled); `null` for none. `end` is never `null` in a committed value.",
805
2128
  "required": false
806
2129
  },
807
2130
  {
@@ -814,21 +2137,21 @@
814
2137
  ]
815
2138
  },
816
2139
  {
817
- "component": "DatePicker",
818
- "description": "A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value.",
2140
+ "component": "DayStrip",
2141
+ "description": "The days ahead in a row that scrolls sideways: one picked, today and the marked days shown.",
819
2142
  "props": [
820
2143
  {
821
2144
  "name": "label",
822
2145
  "type": "string",
823
2146
  "default": null,
824
- "description": "The visible label, passed translated (also the button's accessible name, with the value).",
2147
+ "description": "The group's accessible name (\"Delivery day\"), passed translated.",
825
2148
  "required": true
826
2149
  },
827
2150
  {
828
2151
  "name": "locale",
829
2152
  "type": "string",
830
2153
  "default": null,
831
- "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
2154
+ "description": "The BCP 47 locale of the names and the dates: always explicit.",
832
2155
  "required": true
833
2156
  },
834
2157
  {
@@ -839,142 +2162,197 @@
839
2162
  "required": false
840
2163
  },
841
2164
  {
842
- "name": "clearable",
843
- "type": "boolean",
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)",
844
2181
  "default": null,
845
- "description": "Adds a button that empties the value.",
2182
+ "description": "Refuses some days (closed, full): drawn dashed, skipped by the arrows.",
846
2183
  "required": false
847
2184
  },
848
2185
  {
849
- "name": "defaultOpen",
850
- "type": "boolean",
851
- "default": "false",
852
- "description": "Opens the calendar at first (a story, a test).",
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.",
853
2190
  "required": false
854
2191
  },
855
2192
  {
856
- "name": "defaultValue",
857
- "type": "string | null",
858
- "default": "null",
859
- "description": "The chosen date at first, uncontrolled.",
2193
+ "name": "labels",
2194
+ "type": "Partial<DayStripLabels>",
2195
+ "default": null,
2196
+ "description": "The words, passed translated; English by default.",
860
2197
  "required": false
861
2198
  },
862
2199
  {
863
- "name": "disabled",
864
- "type": "boolean",
2200
+ "name": "onValueChange",
2201
+ "type": "((value: string) => void)",
865
2202
  "default": null,
866
- "description": "Not available.",
2203
+ "description": "Called with the day picked.",
867
2204
  "required": false
868
2205
  },
869
2206
  {
870
- "name": "error",
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",
871
2215
  "type": "string",
872
2216
  "default": null,
873
- "description": "An error under the field: marks it invalid and is read with it.",
2217
+ "description": "The first day of the strip. Default: today.",
874
2218
  "required": false
875
2219
  },
876
2220
  {
877
- "name": "format",
878
- "type": "\"short\" | \"medium\" | \"long\"",
2221
+ "name": "today",
2222
+ "type": "string",
879
2223
  "default": null,
880
- "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
2224
+ "description": "Today (marked by the node): inject it for a stable render or a test. Default: the device's date.",
881
2225
  "required": false
882
2226
  },
883
2227
  {
884
- "name": "hint",
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",
885
2242
  "type": "string",
886
2243
  "default": null,
887
- "description": "A help text under the field.",
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.",
888
2252
  "required": false
889
2253
  },
890
2254
  {
891
- "name": "isDateDisabled",
892
- "type": "((date: string) => boolean)",
2255
+ "name": "bare",
2256
+ "type": "boolean",
893
2257
  "default": null,
894
- "description": "Refuses some dates (`(d) => isWeekend(d)`).",
2258
+ "description": "Without its own year header (inside the calendar, which has one).",
895
2259
  "required": false
896
2260
  },
897
2261
  {
898
- "name": "labels",
899
- "type": "Partial<DatePickerLabels>",
2262
+ "name": "className",
2263
+ "type": "string",
900
2264
  "default": null,
901
- "description": "The words, passed translated; English by default.",
2265
+ "description": "Classes merged on the root.",
902
2266
  "required": false
903
2267
  },
904
2268
  {
905
- "name": "max",
2269
+ "name": "current",
906
2270
  "type": "string",
907
2271
  "default": null,
908
- "description": "The latest date that can be picked.",
2272
+ "description": "This month (marked): inject it for a stable render or a test. Default: the device's.",
909
2273
  "required": false
910
2274
  },
911
2275
  {
912
- "name": "min",
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",
913
2291
  "type": "string",
914
2292
  "default": null,
915
- "description": "The earliest date that can be picked.",
2293
+ "description": "The latest month that can be picked, `YYYY-MM`.",
916
2294
  "required": false
917
2295
  },
918
2296
  {
919
- "name": "name",
2297
+ "name": "min",
920
2298
  "type": "string",
921
2299
  "default": null,
922
- "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
2300
+ "description": "The earliest month that can be picked, `YYYY-MM`.",
923
2301
  "required": false
924
2302
  },
925
2303
  {
926
2304
  "name": "onValueChange",
927
2305
  "type": "((value: string | null) => void)",
928
2306
  "default": null,
929
- "description": "Called with the date picked, or `null` when cleared.",
2307
+ "description": "Called with the month picked.",
930
2308
  "required": false
931
2309
  },
932
2310
  {
933
- "name": "placeholder",
934
- "type": "string",
2311
+ "name": "onYearChange",
2312
+ "type": "((year: number) => void)",
935
2313
  "default": null,
936
- "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
2314
+ "description": "Called when the year shown changes.",
937
2315
  "required": false
938
2316
  },
939
2317
  {
940
- "name": "today",
941
- "type": "string",
2318
+ "name": "value",
2319
+ "type": "string | null",
942
2320
  "default": null,
943
- "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
2321
+ "description": "The chosen month, `YYYY-MM` (controlled); `null` for none.",
944
2322
  "required": false
945
2323
  },
946
2324
  {
947
- "name": "value",
948
- "type": "string | null",
2325
+ "name": "variant",
2326
+ "type": "\"plain\" | \"card\"",
949
2327
  "default": null,
950
- "description": "The chosen date, ISO (controlled); `null` for none.",
2328
+ "description": "`plain` or `card` (its own raised surface).",
951
2329
  "required": false
952
2330
  },
953
2331
  {
954
- "name": "weekStartsOn",
2332
+ "name": "year",
955
2333
  "type": "number",
956
2334
  "default": null,
957
- "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
2335
+ "description": "The year shown (controlled).",
958
2336
  "required": false
959
2337
  }
960
2338
  ]
961
2339
  },
962
2340
  {
963
- "component": "DateRangePicker",
964
- "description": "A period field: two clicks on one calendar (the days between tinted as you hover), optional presets.",
2341
+ "component": "TimePicker",
2342
+ "description": "A time of day: wheels to set one, or slots to book one.",
965
2343
  "props": [
966
2344
  {
967
2345
  "name": "label",
968
2346
  "type": "string",
969
2347
  "default": null,
970
- "description": "The visible label, passed translated (also the button's accessible name, with the value).",
2348
+ "description": "The accessible name of the picker (\"Pick-up time\"), passed translated.",
971
2349
  "required": true
972
2350
  },
973
2351
  {
974
2352
  "name": "locale",
975
2353
  "type": "string",
976
2354
  "default": null,
977
- "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
2355
+ "description": "The BCP 47 locale: how times are written, and whether the day has a period (AM/PM).",
978
2356
  "required": true
979
2357
  },
980
2358
  {
@@ -985,129 +2363,188 @@
985
2363
  "required": false
986
2364
  },
987
2365
  {
988
- "name": "clearable",
989
- "type": "boolean",
2366
+ "name": "defaultValue",
2367
+ "type": "string | null",
990
2368
  "default": null,
991
- "description": "Adds a button that empties the value.",
2369
+ "description": "The time at first, uncontrolled.",
992
2370
  "required": false
993
2371
  },
994
2372
  {
995
- "name": "defaultOpen",
2373
+ "name": "disabled",
996
2374
  "type": "boolean",
997
- "default": "false",
998
- "description": "Opens the calendar at first (a story, a test).",
2375
+ "default": null,
2376
+ "description": "Not available.",
999
2377
  "required": false
1000
2378
  },
1001
2379
  {
1002
- "name": "defaultValue",
1003
- "type": "{ start: string; end: string; } | null",
1004
- "default": "null",
1005
- "description": "The range at first, uncontrolled.",
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\" }`.",
1006
2384
  "required": false
1007
2385
  },
1008
2386
  {
1009
- "name": "disabled",
2387
+ "name": "hour12",
1010
2388
  "type": "boolean",
1011
2389
  "default": null,
1012
- "description": "Not available.",
2390
+ "description": "Forces 12 or 24 hours. Default: the locale's.",
1013
2391
  "required": false
1014
2392
  },
1015
2393
  {
1016
- "name": "error",
1017
- "type": "string",
2394
+ "name": "isTimeDisabled",
2395
+ "type": "((time: string) => boolean)",
1018
2396
  "default": null,
1019
- "description": "An error under the field: marks it invalid and is read with it.",
2397
+ "description": "Refuses some slots (taken, too soon): drawn struck through, skipped by the arrows.",
1020
2398
  "required": false
1021
2399
  },
1022
2400
  {
1023
- "name": "format",
1024
- "type": "\"short\" | \"medium\" | \"long\"",
2401
+ "name": "labels",
2402
+ "type": "Partial<TimePickerLabels>",
1025
2403
  "default": null,
1026
- "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
2404
+ "description": "The words of the wheels, passed translated; English by default.",
1027
2405
  "required": false
1028
2406
  },
1029
2407
  {
1030
- "name": "hint",
1031
- "type": "string",
2408
+ "name": "minuteStep",
2409
+ "type": "number",
1032
2410
  "default": null,
1033
- "description": "A help text under the field.",
2411
+ "description": "The minutes' step: 1, 5 (default), 10, 15, 30.",
1034
2412
  "required": false
1035
2413
  },
1036
2414
  {
1037
- "name": "isDateDisabled",
1038
- "type": "((date: string) => boolean)",
2415
+ "name": "onValueChange",
2416
+ "type": "((value: string) => void)",
1039
2417
  "default": null,
1040
- "description": "Refuses some dates (`(d) => isWeekend(d)`).",
2418
+ "description": "Called with the time chosen.",
1041
2419
  "required": false
1042
2420
  },
1043
2421
  {
1044
- "name": "labels",
1045
- "type": "Partial<DatePickerLabels>",
2422
+ "name": "slots",
2423
+ "type": "readonly string[]",
1046
2424
  "default": null,
1047
- "description": "The words, passed translated; English by default.",
2425
+ "description": "The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`).",
1048
2426
  "required": false
1049
2427
  },
1050
2428
  {
1051
- "name": "max",
1052
- "type": "string",
2429
+ "name": "value",
2430
+ "type": "string | null",
1053
2431
  "default": null,
1054
- "description": "The latest date that can be picked.",
2432
+ "description": "The time, `HH:mm` on 24 hours (controlled); `null` for none.",
1055
2433
  "required": false
1056
2434
  },
1057
2435
  {
1058
- "name": "min",
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",
1059
2450
  "type": "string",
1060
2451
  "default": null,
1061
- "description": "The earliest date that can be picked.",
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`).",
1062
2467
  "required": false
1063
2468
  },
1064
2469
  {
1065
- "name": "name",
2470
+ "name": "defaultValue",
1066
2471
  "type": "string",
1067
2472
  "default": null,
1068
- "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
2473
+ "description": "The chosen value at first, uncontrolled. Default: the first one.",
1069
2474
  "required": false
1070
2475
  },
1071
2476
  {
1072
- "name": "onValueChange",
1073
- "type": "((value: { start: string; end: string; } | null) => void)",
2477
+ "name": "disabled",
2478
+ "type": "boolean",
1074
2479
  "default": null,
1075
- "description": "Called once both ends are chosen (or with `null` when cleared).",
2480
+ "description": "Not available.",
1076
2481
  "required": false
1077
2482
  },
1078
2483
  {
1079
- "name": "placeholder",
1080
- "type": "string",
2484
+ "name": "onValueChange",
2485
+ "type": "((value: string) => void)",
1081
2486
  "default": null,
1082
- "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
2487
+ "description": "Called with the value that comes to rest under the band.",
1083
2488
  "required": false
1084
2489
  },
1085
2490
  {
1086
- "name": "presets",
1087
- "type": "readonly DateRangePreset[]",
2491
+ "name": "value",
2492
+ "type": "string | null",
1088
2493
  "default": null,
1089
- "description": "Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar.",
2494
+ "description": "The chosen value (controlled).",
1090
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
1091
2514
  },
1092
2515
  {
1093
- "name": "today",
2516
+ "name": "label",
1094
2517
  "type": "string",
1095
2518
  "default": null,
1096
- "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
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).",
1097
2527
  "required": false
1098
2528
  },
1099
2529
  {
1100
- "name": "value",
1101
- "type": "{ start: string; end: string; } | null",
2530
+ "name": "className",
2531
+ "type": "string",
1102
2532
  "default": null,
1103
- "description": "The chosen range (controlled); `null` for none. `end` is never `null` in a committed value.",
2533
+ "description": "Classes merged on the root.",
1104
2534
  "required": false
1105
2535
  },
1106
2536
  {
1107
- "name": "weekStartsOn",
1108
- "type": "number",
2537
+ "name": "invalid",
2538
+ "type": "boolean",
1109
2539
  "default": null,
1110
- "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
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.",
1111
2548
  "required": false
1112
2549
  }
1113
2550
  ]