@krizaka/ui 3.1.0 → 3.3.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 (269) 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-2FRWNSRO.js +23 -0
  9. package/dist/chunk-2FRWNSRO.js.map +1 -0
  10. package/dist/chunk-4LOMIQVX.js +21 -0
  11. package/dist/chunk-4LOMIQVX.js.map +1 -0
  12. package/dist/chunk-BTMFZ66D.js +135 -0
  13. package/dist/chunk-BTMFZ66D.js.map +1 -0
  14. package/dist/chunk-C2BRIFJG.js +13 -0
  15. package/dist/chunk-C2BRIFJG.js.map +1 -0
  16. package/dist/{chunk-K3SWZXYT.js → chunk-DGQMDJXK.js} +2 -2
  17. package/dist/chunk-G3ETON2A.js +22 -0
  18. package/dist/chunk-G3ETON2A.js.map +1 -0
  19. package/dist/chunk-IZ7MV6HM.js +26 -0
  20. package/dist/chunk-IZ7MV6HM.js.map +1 -0
  21. package/dist/chunk-KDXRYUD3.js +39 -0
  22. package/dist/chunk-KDXRYUD3.js.map +1 -0
  23. package/dist/{chunk-OGJI5X4W.js → chunk-ZKVQGXLD.js} +5 -3
  24. package/dist/{chunk-OGJI5X4W.js.map → chunk-ZKVQGXLD.js.map} +1 -1
  25. package/dist/chunk-ZNMFG62E.js +20 -0
  26. package/dist/chunk-ZNMFG62E.js.map +1 -0
  27. package/dist/cn.d.ts +9 -1
  28. package/dist/cn.js +5 -3
  29. package/dist/code-block.js +1 -1
  30. package/dist/command.js +1 -1
  31. package/dist/confirm-button.js +1 -1
  32. package/dist/continue-prompt.js +1 -1
  33. package/dist/cta-panel.js +1 -1
  34. package/dist/data-table.js +3 -2
  35. package/dist/data-table.js.map +1 -1
  36. package/dist/date-picker.d.ts +652 -61
  37. package/dist/date-picker.js +918 -122
  38. package/dist/date-picker.js.map +1 -1
  39. package/dist/decision-list.js +1 -1
  40. package/dist/dropdown-menu.js +1 -1
  41. package/dist/error-page.js +1 -1
  42. package/dist/field.js +1 -1
  43. package/dist/filter-bar.js +3 -2
  44. package/dist/form.d.ts +23 -12
  45. package/dist/form.js +11 -47
  46. package/dist/form.js.map +1 -1
  47. package/dist/gallery-page.js +1 -1
  48. package/dist/glow-card.js +1 -1
  49. package/dist/goal-gauge.js +1 -1
  50. package/dist/how-it-works-page.js +1 -1
  51. package/dist/image-cropper.js +3 -1
  52. package/dist/image-cropper.js.map +1 -1
  53. package/dist/landscape.js +1 -1
  54. package/dist/link-card.js +1 -1
  55. package/dist/longform-page.js +1 -1
  56. package/dist/marquee.js +1 -1
  57. package/dist/mascot-quote.js +1 -1
  58. package/dist/motion.css +18 -1
  59. package/dist/native/form.d.ts +60 -15
  60. package/dist/native/form.js +103 -42
  61. package/dist/native/form.js.map +1 -1
  62. package/dist/native.d.ts +443 -2
  63. package/dist/native.js +1655 -448
  64. package/dist/native.js.map +1 -1
  65. package/dist/nav-bar.js +1 -1
  66. package/dist/page-hero.js +2 -2
  67. package/dist/page.js +2 -2
  68. package/dist/popover.js +1 -1
  69. package/dist/pricing-page.js +1 -1
  70. package/dist/product-page.js +1 -1
  71. package/dist/profile-card.js +1 -1
  72. package/dist/promise-card.js +1 -1
  73. package/dist/radio-group.d.ts +66 -32
  74. package/dist/radio-group.js +54 -10
  75. package/dist/radio-group.js.map +1 -1
  76. package/dist/scroll-stage.js +1 -1
  77. package/dist/slider.d.ts +96 -35
  78. package/dist/slider.js +67 -13
  79. package/dist/slider.js.map +1 -1
  80. package/dist/stage-banner.js +1 -1
  81. package/dist/terminal-replay.js +34 -60
  82. package/dist/terminal-replay.js.map +1 -1
  83. package/dist/toast.js +1 -1
  84. package/dist/toc.js +1 -1
  85. package/dist/tooltip.js +1 -1
  86. package/dist/video-player.js +3 -1
  87. package/dist/video-player.js.map +1 -1
  88. package/package.json +8 -8
  89. package/registry/cn.json +1 -1
  90. package/registry/combobox.json +225 -3
  91. package/registry/date-picker.json +1739 -288
  92. package/registry/dialog.json +74 -3
  93. package/registry/examples/combobox/native/multiple.d.ts +4 -0
  94. package/registry/examples/combobox/native/multiple.dark.png +0 -0
  95. package/registry/examples/combobox/native/multiple.js +15 -0
  96. package/registry/examples/combobox/native/multiple.light.png +0 -0
  97. package/registry/examples/combobox/native/multiple.tsx +15 -0
  98. package/registry/examples/combobox/native/search.d.ts +4 -0
  99. package/registry/examples/combobox/native/search.dark.png +0 -0
  100. package/registry/examples/combobox/native/search.js +15 -0
  101. package/registry/examples/combobox/native/search.light.png +0 -0
  102. package/registry/examples/combobox/native/search.tsx +15 -0
  103. package/registry/examples/date-picker/birth-date-wheel.d.ts +4 -0
  104. package/registry/examples/date-picker/birth-date-wheel.js +13 -0
  105. package/registry/examples/date-picker/birth-date-wheel.tsx +11 -0
  106. package/registry/examples/date-picker/birth-date.d.ts +4 -0
  107. package/registry/examples/date-picker/birth-date.js +25 -0
  108. package/registry/examples/date-picker/birth-date.tsx +21 -0
  109. package/registry/examples/date-picker/calendar.js +1 -1
  110. package/registry/examples/date-picker/calendar.tsx +2 -6
  111. package/registry/examples/date-picker/day-strip.d.ts +4 -0
  112. package/registry/examples/date-picker/day-strip.js +28 -0
  113. package/registry/examples/date-picker/day-strip.tsx +27 -0
  114. package/registry/examples/date-picker/month-year.d.ts +4 -0
  115. package/registry/examples/date-picker/month-year.js +13 -0
  116. package/registry/examples/date-picker/month-year.tsx +10 -0
  117. package/registry/examples/date-picker/native/birth-date-wheel.d.ts +4 -0
  118. package/registry/examples/date-picker/native/birth-date-wheel.dark.png +0 -0
  119. package/registry/examples/date-picker/native/birth-date-wheel.js +13 -0
  120. package/registry/examples/date-picker/native/birth-date-wheel.light.png +0 -0
  121. package/registry/examples/date-picker/native/birth-date-wheel.tsx +9 -0
  122. package/registry/examples/date-picker/native/calendar.d.ts +4 -0
  123. package/registry/examples/date-picker/native/calendar.dark.png +0 -0
  124. package/registry/examples/date-picker/native/calendar.js +13 -0
  125. package/registry/examples/date-picker/native/calendar.light.png +0 -0
  126. package/registry/examples/date-picker/native/calendar.tsx +8 -0
  127. package/registry/examples/date-picker/native/day-strip.d.ts +4 -0
  128. package/registry/examples/date-picker/native/day-strip.dark.png +0 -0
  129. package/registry/examples/date-picker/native/day-strip.js +29 -0
  130. package/registry/examples/date-picker/native/day-strip.light.png +0 -0
  131. package/registry/examples/date-picker/native/day-strip.tsx +26 -0
  132. package/registry/examples/date-picker/native/month-year.d.ts +4 -0
  133. package/registry/examples/date-picker/native/month-year.dark.png +0 -0
  134. package/registry/examples/date-picker/native/month-year.js +13 -0
  135. package/registry/examples/date-picker/native/month-year.light.png +0 -0
  136. package/registry/examples/date-picker/native/month-year.tsx +8 -0
  137. package/registry/examples/date-picker/native/time-slots.d.ts +4 -0
  138. package/registry/examples/date-picker/native/time-slots.dark.png +0 -0
  139. package/registry/examples/date-picker/native/time-slots.js +15 -0
  140. package/registry/examples/date-picker/native/time-slots.light.png +0 -0
  141. package/registry/examples/date-picker/native/time-slots.tsx +15 -0
  142. package/registry/examples/date-picker/native/time-wheel.d.ts +4 -0
  143. package/registry/examples/date-picker/native/time-wheel.dark.png +0 -0
  144. package/registry/examples/date-picker/native/time-wheel.js +13 -0
  145. package/registry/examples/date-picker/native/time-wheel.light.png +0 -0
  146. package/registry/examples/date-picker/native/time-wheel.tsx +8 -0
  147. package/registry/examples/date-picker/time-slots.d.ts +4 -0
  148. package/registry/examples/date-picker/time-slots.js +26 -0
  149. package/registry/examples/date-picker/time-slots.tsx +25 -0
  150. package/registry/examples/date-picker/time-wheel.d.ts +4 -0
  151. package/registry/examples/date-picker/time-wheel.js +13 -0
  152. package/registry/examples/date-picker/time-wheel.tsx +10 -0
  153. package/registry/examples/date-picker/wheel.d.ts +4 -0
  154. package/registry/examples/date-picker/wheel.js +14 -0
  155. package/registry/examples/date-picker/wheel.tsx +16 -0
  156. package/registry/examples/dialog/native/sheet.d.ts +4 -0
  157. package/registry/examples/dialog/native/sheet.dark.png +0 -0
  158. package/registry/examples/dialog/native/sheet.js +21 -0
  159. package/registry/examples/dialog/native/sheet.light.png +0 -0
  160. package/registry/examples/dialog/native/sheet.tsx +18 -0
  161. package/registry/examples/field/native/select.d.ts +4 -0
  162. package/registry/examples/field/native/select.dark.png +0 -0
  163. package/registry/examples/field/native/select.js +29 -0
  164. package/registry/examples/field/native/select.light.png +0 -0
  165. package/registry/examples/field/native/select.tsx +25 -0
  166. package/registry/examples/form/native/audience.dark.png +0 -0
  167. package/registry/examples/form/native/audience.light.png +0 -0
  168. package/registry/examples/form/native/profile.dark.png +0 -0
  169. package/registry/examples/form/native/profile.light.png +0 -0
  170. package/registry/examples/form/native/settings.dark.png +0 -0
  171. package/registry/examples/form/native/settings.light.png +0 -0
  172. package/registry/examples/form/native/sign-up.dark.png +0 -0
  173. package/registry/examples/form/native/sign-up.light.png +0 -0
  174. package/registry/examples/form/native/upload.dark.png +0 -0
  175. package/registry/examples/form/native/upload.light.png +0 -0
  176. package/registry/examples/radio-group/buttons.d.ts +4 -0
  177. package/registry/examples/radio-group/buttons.js +17 -0
  178. package/registry/examples/radio-group/buttons.tsx +21 -0
  179. package/registry/examples/radio-group/cards.js +1 -1
  180. package/registry/examples/radio-group/cards.tsx +2 -2
  181. package/registry/examples/radio-group/native/buttons.d.ts +4 -0
  182. package/registry/examples/radio-group/native/buttons.dark.png +0 -0
  183. package/registry/examples/radio-group/native/buttons.js +25 -0
  184. package/registry/examples/radio-group/native/buttons.light.png +0 -0
  185. package/registry/examples/radio-group/native/buttons.tsx +21 -0
  186. package/registry/examples/radio-group/native/cards.d.ts +4 -0
  187. package/registry/examples/radio-group/native/cards.dark.png +0 -0
  188. package/registry/examples/radio-group/native/cards.js +23 -0
  189. package/registry/examples/radio-group/native/cards.light.png +0 -0
  190. package/registry/examples/radio-group/native/cards.tsx +19 -0
  191. package/registry/examples/radio-group/native/list.d.ts +4 -0
  192. package/registry/examples/radio-group/native/list.dark.png +0 -0
  193. package/registry/examples/radio-group/native/list.js +24 -0
  194. package/registry/examples/radio-group/native/list.light.png +0 -0
  195. package/registry/examples/radio-group/native/list.tsx +20 -0
  196. package/registry/examples/radio-group/native/segmented.d.ts +4 -0
  197. package/registry/examples/radio-group/native/segmented.dark.png +0 -0
  198. package/registry/examples/radio-group/native/segmented.js +24 -0
  199. package/registry/examples/radio-group/native/segmented.light.png +0 -0
  200. package/registry/examples/radio-group/native/segmented.tsx +20 -0
  201. package/registry/examples/radio-group/segmented.d.ts +4 -0
  202. package/registry/examples/radio-group/segmented.js +15 -0
  203. package/registry/examples/radio-group/segmented.tsx +12 -0
  204. package/registry/examples/slider/floating.d.ts +4 -0
  205. package/registry/examples/slider/floating.js +11 -0
  206. package/registry/examples/slider/floating.tsx +12 -0
  207. package/registry/examples/slider/native/default.d.ts +4 -0
  208. package/registry/examples/slider/native/default.dark.png +0 -0
  209. package/registry/examples/slider/native/default.js +14 -0
  210. package/registry/examples/slider/native/default.light.png +0 -0
  211. package/registry/examples/slider/native/default.tsx +13 -0
  212. package/registry/examples/slider/native/range.d.ts +4 -0
  213. package/registry/examples/slider/native/range.dark.png +0 -0
  214. package/registry/examples/slider/native/range.js +12 -0
  215. package/registry/examples/slider/native/range.light.png +0 -0
  216. package/registry/examples/slider/native/range.tsx +11 -0
  217. package/registry/examples/slider/native/stepped.d.ts +4 -0
  218. package/registry/examples/slider/native/stepped.dark.png +0 -0
  219. package/registry/examples/slider/native/stepped.js +28 -0
  220. package/registry/examples/slider/native/stepped.light.png +0 -0
  221. package/registry/examples/slider/native/stepped.tsx +24 -0
  222. package/registry/examples/slider/native/vertical.d.ts +4 -0
  223. package/registry/examples/slider/native/vertical.dark.png +0 -0
  224. package/registry/examples/slider/native/vertical.js +16 -0
  225. package/registry/examples/slider/native/vertical.light.png +0 -0
  226. package/registry/examples/slider/native/vertical.tsx +13 -0
  227. package/registry/examples/slider/price-range.d.ts +4 -0
  228. package/registry/examples/slider/price-range.js +29 -0
  229. package/registry/examples/slider/price-range.tsx +27 -0
  230. package/registry/examples/slider/stepped.d.ts +4 -0
  231. package/registry/examples/slider/stepped.js +28 -0
  232. package/registry/examples/slider/stepped.tsx +24 -0
  233. package/registry/examples/slider/vertical.d.ts +4 -0
  234. package/registry/examples/slider/vertical.js +15 -0
  235. package/registry/examples/slider/vertical.tsx +14 -0
  236. package/registry/examples/switch/native/default.d.ts +4 -0
  237. package/registry/examples/switch/native/default.dark.png +0 -0
  238. package/registry/examples/switch/native/default.js +16 -0
  239. package/registry/examples/switch/native/default.light.png +0 -0
  240. package/registry/examples/switch/native/default.tsx +13 -0
  241. package/registry/examples/switch/native/settings.d.ts +4 -0
  242. package/registry/examples/switch/native/settings.dark.png +0 -0
  243. package/registry/examples/switch/native/settings.js +20 -0
  244. package/registry/examples/switch/native/settings.light.png +0 -0
  245. package/registry/examples/switch/native/settings.tsx +17 -0
  246. package/registry/field.json +15 -3
  247. package/registry/form.json +250 -42
  248. package/registry/index.json +29 -24
  249. package/registry/radio-group.json +227 -15
  250. package/registry/slider.json +314 -13
  251. package/registry/switch.json +115 -3
  252. package/registry/tabs.json +2 -2
  253. package/registry/theme.json +7 -0
  254. package/skills/choice-controls/SKILL.md +39 -0
  255. package/skills/date-picker/SKILL.md +43 -0
  256. package/skills/form/SKILL.md +3 -0
  257. package/dist/chunk-2USC2YT6.js +0 -17
  258. package/dist/chunk-2USC2YT6.js.map +0 -1
  259. package/dist/chunk-5AXLA4LK.js +0 -13
  260. package/dist/chunk-5AXLA4LK.js.map +0 -1
  261. package/dist/chunk-FJDSLNXK.js +0 -11
  262. package/dist/chunk-FJDSLNXK.js.map +0 -1
  263. package/dist/chunk-KNBYZGSQ.js +0 -11
  264. package/dist/chunk-KNBYZGSQ.js.map +0 -1
  265. package/dist/chunk-RUPRYJ45.js +0 -82
  266. package/dist/chunk-RUPRYJ45.js.map +0 -1
  267. package/dist/chunk-VCAAHCHL.js +0 -17
  268. package/dist/chunk-VCAAHCHL.js.map +0 -1
  269. /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,236 +710,1144 @@
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": "unset",
1120
+ "type": "boolean",
1121
+ "default": null,
1122
+ "description": "Nothing is chosen yet: the values under the band stay quiet until a wheel is turned.",
1123
+ "required": false
1124
+ },
1125
+ {
1126
+ "name": "visible",
1127
+ "type": "3 | 5 | 7",
1128
+ "default": "5",
1129
+ "description": "How many values are visible: 3, 5 (default) or 7.",
1130
+ "required": false
1131
+ }
1132
+ ]
1133
+ }
1134
+ ],
1135
+ "examples": [
1136
+ {
1137
+ "name": "schedule",
1138
+ "title": "Schedule",
1139
+ "description": "A publication date, from today on, written the way the locale writes it.",
1140
+ "path": "examples/date-picker/schedule.tsx",
1141
+ "module": "@krizaka/ui/registry/examples/date-picker/schedule",
1142
+ "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"
1143
+ },
1144
+ {
1145
+ "name": "open",
1146
+ "title": "Calendar open",
1147
+ "description": "The popover open on the month: today's node, the chosen day lit.",
1148
+ "path": "examples/date-picker/open.tsx",
1149
+ "module": "@krizaka/ui/registry/examples/date-picker/open",
1150
+ "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"
1151
+ },
1152
+ {
1153
+ "name": "range",
1154
+ "title": "Period with presets",
1155
+ "description": "`DateRangePicker` for an earnings report: presets beside two months, the period as one band.",
1156
+ "path": "examples/date-picker/range.tsx",
1157
+ "module": "@krizaka/ui/registry/examples/date-picker/range",
1158
+ "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"
1159
+ },
1160
+ {
1161
+ "name": "french",
1162
+ "title": "In French",
1163
+ "description": "`locale=\"fr-CA\"`: the names, the order and the first day of the week follow.",
1164
+ "path": "examples/date-picker/french.tsx",
1165
+ "module": "@krizaka/ui/registry/examples/date-picker/french",
1166
+ "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"
1167
+ },
1168
+ {
1169
+ "name": "calendar",
1170
+ "title": "Inline calendar",
1171
+ "description": "`Calendar variant=\"card\"`: the month is the interface, weekends unavailable; its title opens the months.",
1172
+ "path": "examples/date-picker/calendar.tsx",
1173
+ "module": "@krizaka/ui/registry/examples/date-picker/calendar",
1174
+ "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"
1175
+ },
1176
+ {
1177
+ "name": "month-year",
1178
+ "title": "Month and year",
1179
+ "description": "`MonthPicker`: a billing month, nothing later than this one.",
1180
+ "path": "examples/date-picker/month-year.tsx",
1181
+ "module": "@krizaka/ui/registry/examples/date-picker/month-year",
1182
+ "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"
1183
+ },
1184
+ {
1185
+ "name": "day-strip",
1186
+ "title": "Day strip",
1187
+ "description": "`DayStrip`: the next two weeks in a row, closed days dashed, the days with a session marked.",
1188
+ "path": "examples/date-picker/day-strip.tsx",
1189
+ "module": "@krizaka/ui/registry/examples/date-picker/day-strip",
1190
+ "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"
1191
+ },
1192
+ {
1193
+ "name": "wheel",
1194
+ "title": "Wheel",
1195
+ "description": "`WheelPicker` + `Wheel`: any short list turned by the thumb or the arrows — a party size.",
1196
+ "path": "examples/date-picker/wheel.tsx",
1197
+ "module": "@krizaka/ui/registry/examples/date-picker/wheel",
1198
+ "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"
1199
+ },
1200
+ {
1201
+ "name": "time-wheel",
1202
+ "title": "Time, wheels",
1203
+ "description": "`TimePicker`: hours, minutes by 5 and the period of a 12-hour locale; the value is `HH:mm`.",
1204
+ "path": "examples/date-picker/time-wheel.tsx",
1205
+ "module": "@krizaka/ui/registry/examples/date-picker/time-wheel",
1206
+ "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"
1207
+ },
1208
+ {
1209
+ "name": "time-slots",
1210
+ "title": "Time, slots",
1211
+ "description": "`TimePicker variant=\"slots\"`: the free times of an agenda by part of the day, taken ones struck through.",
1212
+ "path": "examples/date-picker/time-slots.tsx",
1213
+ "module": "@krizaka/ui/registry/examples/date-picker/time-slots",
1214
+ "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"
1215
+ },
1216
+ {
1217
+ "name": "birth-date",
1218
+ "title": "Date of birth, typed",
1219
+ "description": "`DateField`: day · month · year in the locale's order, autofilled, an under-18 date refused.",
1220
+ "path": "examples/date-picker/birth-date.tsx",
1221
+ "module": "@krizaka/ui/registry/examples/date-picker/birth-date",
1222
+ "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"
1223
+ },
1224
+ {
1225
+ "name": "birth-date-wheel",
1226
+ "title": "Date of birth, wheels",
1227
+ "description": "`DateWheel` with `max` at the 18th birthday: an under-18 date cannot be chosen.",
1228
+ "path": "examples/date-picker/birth-date-wheel.tsx",
1229
+ "module": "@krizaka/ui/registry/examples/date-picker/birth-date-wheel",
1230
+ "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"
1231
+ }
1232
+ ]
1233
+ },
1234
+ "native": {
1235
+ "entry": "@krizaka/ui/native",
1236
+ "import": "import { Calendar, MonthPicker, DayStrip, TimePicker, DateWheel, WheelPicker, Wheel, DateField, latestBirthDate, timeSlots } from \"@krizaka/ui/native\";",
1237
+ "differences": [
1238
+ "No popover field (`DatePicker`, `DateRangePicker`): on a phone, the `Calendar`, the `DayStrip` or the wheels sit in the screen or in a `Sheet`.",
1239
+ "`DateField` is typed (day · month · year, numeric keypads); `DateWheel` is the same date as three wheels — the native form's `DateField` uses the wheels.",
1240
+ "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.",
1241
+ "A wheel is an adjustable control for VoiceOver and TalkBack (swipe up or down), named by its `label`, its value read as words."
1242
+ ],
1243
+ "props": [
1244
+ {
1245
+ "component": "DateField",
1246
+ "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.",
1247
+ "props": [
1248
+ {
1249
+ "name": "label",
1250
+ "type": "string",
1251
+ "default": null,
1252
+ "description": "The visible label of the date, passed translated (the group's accessible name).",
1253
+ "required": true
1254
+ },
1255
+ {
1256
+ "name": "autoComplete",
1257
+ "type": "\"off\" | \"bday\"",
1258
+ "default": null,
1259
+ "description": "`bday` for a date of birth: the system offers to fill it.",
1260
+ "required": false
1261
+ },
1262
+ {
1263
+ "name": "defaultValue",
1264
+ "type": "string",
1265
+ "default": null,
1266
+ "description": "The date at first, uncontrolled.",
1267
+ "required": false
1268
+ },
1269
+ {
1270
+ "name": "disabled",
1271
+ "type": "boolean",
1272
+ "default": null,
1273
+ "description": "Not available.",
1274
+ "required": false
1275
+ },
1276
+ {
1277
+ "name": "error",
1278
+ "type": "string",
1279
+ "default": null,
1280
+ "description": "An error under the boxes: a danger border and caption, read with them and announced. Replaces the hint.",
1281
+ "required": false
1282
+ },
1283
+ {
1284
+ "name": "hint",
1285
+ "type": "string",
1286
+ "default": null,
1287
+ "description": "A help text under the boxes (\"For example, 27 3 1990\").",
1288
+ "required": false
1289
+ },
1290
+ {
1291
+ "name": "inputRef",
1292
+ "type": "Ref<TextInput>",
1293
+ "default": null,
1294
+ "description": "The first box (in the locale's order), to focus the field from the one before it.",
1295
+ "required": false
1296
+ },
1297
+ {
1298
+ "name": "labels",
1299
+ "type": "Partial<DateFieldLabels>",
1300
+ "default": null,
1301
+ "description": "The names of the boxes, passed translated; English by default.",
1302
+ "required": false
1303
+ },
1304
+ {
1305
+ "name": "locale",
1306
+ "type": "string",
1307
+ "default": "en-US",
1308
+ "description": "The locale that orders the boxes (day · month · year, or month · day · year). Default `en-US`.",
1309
+ "required": false
1310
+ },
1311
+ {
1312
+ "name": "onBlur",
1313
+ "type": "(() => void)",
1314
+ "default": null,
1315
+ "description": "Called when focus leaves the last box.",
1316
+ "required": false
1317
+ },
1318
+ {
1319
+ "name": "onSubmitEditing",
1320
+ "type": "(() => void)",
1321
+ "default": null,
1322
+ "description": "Called by the last box's return key (`returnKeyType`): move to the next field, or send the form.",
1323
+ "required": false
1324
+ },
1325
+ {
1326
+ "name": "onValueChange",
1327
+ "type": "((value: string) => void)",
1328
+ "default": null,
1329
+ "description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
1330
+ "required": false
1331
+ },
1332
+ {
1333
+ "name": "returnKeyType",
1334
+ "type": "\"next\" | \"done\" | \"go\" | \"send\"",
1335
+ "default": "done",
1336
+ "description": "The last box's return key. Default `done`.",
1337
+ "required": false
1338
+ },
1339
+ {
1340
+ "name": "style",
1341
+ "type": "StyleProp<ViewStyle>",
1342
+ "default": null,
1343
+ "description": "Styles merged last, on the field.",
1344
+ "required": false
1345
+ },
1346
+ {
1347
+ "name": "value",
1348
+ "type": "string",
1349
+ "default": null,
1350
+ "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.",
1351
+ "required": false
1352
+ }
1353
+ ]
1354
+ }
1355
+ ],
1356
+ "examples": [
1357
+ {
1358
+ "name": "birth-date",
1359
+ "title": "Date of birth, typed",
1360
+ "description": "Day, month and year with `autoComplete=\"bday\"`, and the error of an under-18 date.",
1361
+ "path": "examples/date-picker/native/birth-date.tsx",
1362
+ "module": "@krizaka/ui/registry/examples/date-picker/native/birth-date",
1363
+ "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",
1364
+ "screenshots": {
1365
+ "dark": "examples/date-picker/native/birth-date.dark.png",
1366
+ "light": "examples/date-picker/native/birth-date.light.png"
1367
+ }
1368
+ },
1369
+ {
1370
+ "name": "birth-date-wheel",
1371
+ "title": "Date of birth, wheels",
1372
+ "description": "`DateWheel` ending at the 18th birthday: an under-18 date cannot be chosen.",
1373
+ "path": "examples/date-picker/native/birth-date-wheel.tsx",
1374
+ "module": "@krizaka/ui/registry/examples/date-picker/native/birth-date-wheel",
1375
+ "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",
1376
+ "screenshots": {
1377
+ "dark": "examples/date-picker/native/birth-date-wheel.dark.png",
1378
+ "light": "examples/date-picker/native/birth-date-wheel.light.png"
1379
+ }
1380
+ },
1381
+ {
1382
+ "name": "calendar",
1383
+ "title": "Calendar",
1384
+ "description": "A month to book a day: 44-pt days, today's node, the past unavailable.",
1385
+ "path": "examples/date-picker/native/calendar.tsx",
1386
+ "module": "@krizaka/ui/registry/examples/date-picker/native/calendar",
1387
+ "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",
1388
+ "screenshots": {
1389
+ "dark": "examples/date-picker/native/calendar.dark.png",
1390
+ "light": "examples/date-picker/native/calendar.light.png"
1391
+ }
1392
+ },
1393
+ {
1394
+ "name": "day-strip",
1395
+ "title": "Day strip",
1396
+ "description": "The days ahead swiped sideways, closed days dashed, marked days with a dot.",
1397
+ "path": "examples/date-picker/native/day-strip.tsx",
1398
+ "module": "@krizaka/ui/registry/examples/date-picker/native/day-strip",
1399
+ "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",
1400
+ "screenshots": {
1401
+ "dark": "examples/date-picker/native/day-strip.dark.png",
1402
+ "light": "examples/date-picker/native/day-strip.light.png"
1403
+ }
1404
+ },
1405
+ {
1406
+ "name": "time-wheel",
1407
+ "title": "Time, wheels",
1408
+ "description": "Hours, minutes by 5 and the period; the value is `HH:mm`.",
1409
+ "path": "examples/date-picker/native/time-wheel.tsx",
1410
+ "module": "@krizaka/ui/registry/examples/date-picker/native/time-wheel",
1411
+ "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",
1412
+ "screenshots": {
1413
+ "dark": "examples/date-picker/native/time-wheel.dark.png",
1414
+ "light": "examples/date-picker/native/time-wheel.light.png"
1415
+ }
1416
+ },
1417
+ {
1418
+ "name": "time-slots",
1419
+ "title": "Time, slots",
1420
+ "description": "The free times of an agenda as buttons, taken ones dashed.",
1421
+ "path": "examples/date-picker/native/time-slots.tsx",
1422
+ "module": "@krizaka/ui/registry/examples/date-picker/native/time-slots",
1423
+ "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",
1424
+ "screenshots": {
1425
+ "dark": "examples/date-picker/native/time-slots.dark.png",
1426
+ "light": "examples/date-picker/native/time-slots.light.png"
1427
+ }
1428
+ },
1429
+ {
1430
+ "name": "month-year",
1431
+ "title": "Month and year",
1432
+ "description": "`MonthPicker`: the twelve months, nothing later than this one.",
1433
+ "path": "examples/date-picker/native/month-year.tsx",
1434
+ "module": "@krizaka/ui/registry/examples/date-picker/native/month-year",
1435
+ "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",
1436
+ "screenshots": {
1437
+ "dark": "examples/date-picker/native/month-year.dark.png",
1438
+ "light": "examples/date-picker/native/month-year.light.png"
1439
+ }
1440
+ }
1441
+ ]
1442
+ },
1443
+ "files": [
1444
+ {
1445
+ "path": "date-picker/calendar.tsx",
1446
+ "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"
1447
+ },
1448
+ {
1449
+ "path": "date-picker/date-field.tsx",
1450
+ "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)} unset={!value}>{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"
1451
+ },
1452
+ {
665
1453
  "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"
1454
+ "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"
1455
+ },
1456
+ {
1457
+ "path": "date-picker/day-strip.tsx",
1458
+ "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
1459
  },
668
1460
  {
669
1461
  "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"
1462
+ "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
1463
  },
672
1464
  {
673
1465
  "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"
1466
+ "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"
1467
+ },
1468
+ {
1469
+ "path": "date-picker/month-picker.tsx",
1470
+ "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"
1471
+ },
1472
+ {
1473
+ "path": "date-picker/parts.ts",
1474
+ "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"
675
1475
  },
676
1476
  {
677
- "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"
1477
+ "path": "date-picker/time-picker.tsx",
1478
+ "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"
1479
+ },
1480
+ {
1481
+ "path": "date-picker/wheel.tsx",
1482
+ "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: \"group/wheel 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 \"group-data-[unset]/wheel:data-[selected]:font-normal group-data-[unset]/wheel:data-[selected]:text-fg-secondary\",\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 /** Nothing is chosen yet: the values under the band stay quiet until a wheel is turned. */\n unset?: 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, unset, 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} data-unset={unset ? \"\" : 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"
1483
+ },
1484
+ {
1485
+ "path": "glyphs.tsx",
1486
+ "content": "// The small line glyphs the data and media primitives draw (play, volume, search, chevrons…): decorative, 24-unit\n// box, `currentColor`, sized by the caller's class. Shared by the entries that need them; never exported on its own —\n// a product draws its own icons with @krizaka/icons.\nimport type * as React from \"react\";\n\ntype GlyphProps = { className?: string };\n\nfunction Glyph({ className = \"h-4 w-4\", fill, children }: GlyphProps & { fill?: boolean; children: React.ReactNode }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n className={className}\n fill={fill ? \"currentColor\" : \"none\"}\n stroke={fill ? \"none\" : \"currentColor\"}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n focusable=\"false\"\n >\n {children}\n </svg>\n );\n}\n\nexport const PlayGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <path d=\"M8 5.5v13a1 1 0 0 0 1.5.86l10.4-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z\" />\n </Glyph>\n);\nexport const PauseGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </Glyph>\n);\nexport const VolumeGlyph = ({ muted, ...p }: GlyphProps & { muted?: boolean }) => (\n <Glyph {...p}>\n <path d=\"M11 5 6 9H3v6h3l5 4V5Z\" />\n {muted ? <path d=\"m22 9-6 6m0-6 6 6\" /> : <path d=\"M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13\" />}\n </Glyph>\n);\nexport const FullscreenGlyph = ({ exit, ...p }: GlyphProps & { exit?: boolean }) => (\n <Glyph {...p}>\n {exit ? <path d=\"M9 4v5H4m11-5v5h5M9 20v-5H4m11 5v-5h5\" /> : <path d=\"M4 9V4h5m6 0h5v5M4 15v5h5m11-5v5h-5\" />}\n </Glyph>\n);\nexport const CaptionsGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10.5a2 2 0 1 0 0 3M17 10.5a2 2 0 1 0 0 3\" />\n </Glyph>\n);\nexport const PipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <rect x=\"12\" y=\"12\" width=\"6\" height=\"4\" rx=\"1\" />\n </Glyph>\n);\nexport const SearchGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </Glyph>\n);\nexport const CloseGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </Glyph>\n);\nexport const ChevronGlyph = ({ direction = \"down\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" | \"up\" | \"down\" }) => (\n <Glyph {...p}>\n <path d={{ left: \"m15 6-6 6 6 6\", right: \"m9 6 6 6-6 6\", up: \"m6 15 6-6 6 6\", down: \"m6 9 6 6 6-6\" }[direction]} />\n </Glyph>\n);\nexport const CheckGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Glyph>\n);\nexport const PlusGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\n);\nexport const CalendarGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2\" />\n <path d=\"M3 10h18M8 3v4M16 3v4\" />\n </Glyph>\n);\nexport const UploadGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 16V4m0 0L7 9m5-5 5 5\" />\n <path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\" />\n </Glyph>\n);\nexport const FileGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z\" />\n <path d=\"M14 3v5h5\" />\n </Glyph>\n);\nexport const CameraGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z\" />\n <circle cx=\"12\" cy=\"13.5\" r=\"3.5\" />\n </Glyph>\n);\nexport const FlipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3\" />\n <path d=\"M18 3v4h-4M6 21v-4h4\" />\n </Glyph>\n);\nexport const RetryGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 1 0 2.3-5.7L4 8.5\" />\n <path d=\"M4 4v4.5h4.5\" />\n </Glyph>\n);\nexport const SortGlyph = ({ direction, ...p }: GlyphProps & { direction?: \"ascending\" | \"descending\" }) => (\n <Glyph {...p}>\n {direction === \"ascending\" ? <path d=\"m7 14 5-5 5 5\" /> : direction === \"descending\" ? <path d=\"m7 10 5 5 5-5\" /> : <path d=\"m8 9 4-4 4 4M8 15l4 4 4-4\" />}\n </Glyph>\n);\nexport const ArrowGlyph = ({ direction = \"right\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" }) => (\n <Glyph {...p}>\n <path d={direction === \"left\" ? \"M19 12H5m0 0 6-6m-6 6 6 6\" : \"M5 12h14m0 0-6-6m6 6-6 6\"} />\n </Glyph>\n);\nexport const ExternalGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 4h6v6M20 4l-9 9\" />\n <path d=\"M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\" />\n </Glyph>\n);\nexport const WarningGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 4 2.5 20h19L12 4Z\" />\n <path d=\"M12 10v4M12 17.2v.1\" />\n </Glyph>\n);\nexport const CopyGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3\" />\n </Glyph>\n);\n"
1487
+ },
1488
+ {
1489
+ "path": "media.ts",
1490
+ "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"
1491
+ },
1492
+ {
1493
+ "path": "meta.ts",
1494
+ "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"
1495
+ },
1496
+ {
1497
+ "path": "tv.ts",
1498
+ "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"
1499
+ }
1500
+ ],
1501
+ "dependencies": [
1502
+ "@krizaka/intl@^0.1.0",
1503
+ "tailwind-variants@^3.3.1"
1504
+ ],
1505
+ "registryDependencies": [
1506
+ "cn",
1507
+ "popover"
1508
+ ],
1509
+ "demo": {
1510
+ "path": "examples/date-picker/schedule.tsx",
1511
+ "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"
1512
+ },
1513
+ "props": [
1514
+ {
1515
+ "component": "Calendar",
1516
+ "description": "A month grid to pick a date or the ends of a range, with the full keyboard of the APG date picker.",
1517
+ "props": [
1518
+ {
1519
+ "name": "locale",
1520
+ "type": "string",
1521
+ "default": null,
1522
+ "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
1523
+ "required": true
1524
+ },
1525
+ {
1526
+ "name": "autoFocus",
1527
+ "type": "boolean",
1528
+ "default": null,
1529
+ "description": "Moves the focus to the active day when it mounts (inside a popover).",
1530
+ "required": false
1531
+ },
1532
+ {
1533
+ "name": "className",
1534
+ "type": "string",
1535
+ "default": null,
1536
+ "description": "Classes merged on the root.",
1537
+ "required": false
1538
+ },
1539
+ {
1540
+ "name": "defaultMonth",
1541
+ "type": "string",
1542
+ "default": null,
1543
+ "description": "The month shown at first. Default: the value's, or today's.",
1544
+ "required": false
1545
+ },
1546
+ {
1547
+ "name": "isDateDisabled",
1548
+ "type": "((date: string) => boolean)",
1549
+ "default": null,
1550
+ "description": "Refuses some dates (`(d) => isWeekend(d)`).",
1551
+ "required": false
1552
+ },
1553
+ {
1554
+ "name": "labels",
1555
+ "type": "Partial<CalendarLabels>",
1556
+ "default": null,
1557
+ "description": "The words, passed translated; English by default.",
1558
+ "required": false
1559
+ },
1560
+ {
1561
+ "name": "max",
1562
+ "type": "string",
1563
+ "default": null,
1564
+ "description": "The latest date that can be picked.",
1565
+ "required": false
1566
+ },
1567
+ {
1568
+ "name": "min",
1569
+ "type": "string",
1570
+ "default": null,
1571
+ "description": "The earliest date that can be picked.",
1572
+ "required": false
1573
+ },
1574
+ {
1575
+ "name": "month",
1576
+ "type": "string",
1577
+ "default": null,
1578
+ "description": "The month shown (the first one, with `numberOfMonths={2}`), any day of it (controlled).",
1579
+ "required": false
1580
+ },
1581
+ {
1582
+ "name": "numberOfMonths",
1583
+ "type": "1 | 2",
1584
+ "default": "1",
1585
+ "description": "One month, or two side by side (a period: from a phone's width up, they stack on a phone).",
1586
+ "required": false
1587
+ },
1588
+ {
1589
+ "name": "onMonthChange",
1590
+ "type": "((month: string) => void)",
1591
+ "default": null,
1592
+ "description": "Called when the month shown changes.",
1593
+ "required": false
1594
+ },
1595
+ {
1596
+ "name": "onSelect",
1597
+ "type": "((date: string) => void)",
1598
+ "default": null,
1599
+ "description": "Called with the day picked.",
1600
+ "required": false
1601
+ },
1602
+ {
1603
+ "name": "range",
1604
+ "type": "DateRange | null",
1605
+ "default": null,
1606
+ "description": "The chosen range (range mode): its ends are lit, the days between are one band.",
1607
+ "required": false
1608
+ },
1609
+ {
1610
+ "name": "today",
1611
+ "type": "string",
1612
+ "default": null,
1613
+ "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
1614
+ "required": false
1615
+ },
1616
+ {
1617
+ "name": "value",
1618
+ "type": "string | null",
1619
+ "default": null,
1620
+ "description": "The chosen date (one-date mode).",
1621
+ "required": false
1622
+ },
1623
+ {
1624
+ "name": "variant",
1625
+ "type": "\"plain\" | \"card\"",
1626
+ "default": null,
1627
+ "description": "`plain` (inside a popover, a card of the app) or `card` (the calendar is the interface: its own raised surface).",
1628
+ "required": false
1629
+ },
1630
+ {
1631
+ "name": "weekStartsOn",
1632
+ "type": "number",
1633
+ "default": null,
1634
+ "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
1635
+ "required": false
1636
+ }
1637
+ ]
679
1638
  },
680
1639
  {
681
- "path": "glyphs.tsx",
682
- "content": "// The small line glyphs the data and media primitives draw (play, volume, search, chevrons…): decorative, 24-unit\n// box, `currentColor`, sized by the caller's class. Shared by the entries that need them; never exported on its own —\n// a product draws its own icons with @krizaka/icons.\nimport type * as React from \"react\";\n\ntype GlyphProps = { className?: string };\n\nfunction Glyph({ className = \"h-4 w-4\", fill, children }: GlyphProps & { fill?: boolean; children: React.ReactNode }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n className={className}\n fill={fill ? \"currentColor\" : \"none\"}\n stroke={fill ? \"none\" : \"currentColor\"}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n focusable=\"false\"\n >\n {children}\n </svg>\n );\n}\n\nexport const PlayGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <path d=\"M8 5.5v13a1 1 0 0 0 1.5.86l10.4-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z\" />\n </Glyph>\n);\nexport const PauseGlyph = (p: GlyphProps) => (\n <Glyph {...p} fill>\n <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n </Glyph>\n);\nexport const VolumeGlyph = ({ muted, ...p }: GlyphProps & { muted?: boolean }) => (\n <Glyph {...p}>\n <path d=\"M11 5 6 9H3v6h3l5 4V5Z\" />\n {muted ? <path d=\"m22 9-6 6m0-6 6 6\" /> : <path d=\"M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13\" />}\n </Glyph>\n);\nexport const FullscreenGlyph = ({ exit, ...p }: GlyphProps & { exit?: boolean }) => (\n <Glyph {...p}>\n {exit ? <path d=\"M9 4v5H4m11-5v5h5M9 20v-5H4m11 5v-5h5\" /> : <path d=\"M4 9V4h5m6 0h5v5M4 15v5h5m11-5v5h-5\" />}\n </Glyph>\n);\nexport const CaptionsGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <path d=\"M10 10.5a2 2 0 1 0 0 3M17 10.5a2 2 0 1 0 0 3\" />\n </Glyph>\n);\nexport const PipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\" />\n <rect x=\"12\" y=\"12\" width=\"6\" height=\"4\" rx=\"1\" />\n </Glyph>\n);\nexport const SearchGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </Glyph>\n);\nexport const CloseGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </Glyph>\n);\nexport const ChevronGlyph = ({ direction = \"down\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" | \"up\" | \"down\" }) => (\n <Glyph {...p}>\n <path d={{ left: \"m15 6-6 6 6 6\", right: \"m9 6 6 6-6 6\", up: \"m6 15 6-6 6 6\", down: \"m6 9 6 6 6-6\" }[direction]} />\n </Glyph>\n);\nexport const CheckGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Glyph>\n);\nexport const PlusGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 5v14M5 12h14\" />\n </Glyph>\n);\nexport const CalendarGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2\" />\n <path d=\"M3 10h18M8 3v4M16 3v4\" />\n </Glyph>\n);\nexport const UploadGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 16V4m0 0L7 9m5-5 5 5\" />\n <path d=\"M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2\" />\n </Glyph>\n);\nexport const FileGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z\" />\n <path d=\"M14 3v5h5\" />\n </Glyph>\n);\nexport const CameraGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z\" />\n <circle cx=\"12\" cy=\"13.5\" r=\"3.5\" />\n </Glyph>\n);\nexport const FlipGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3\" />\n <path d=\"M18 3v4h-4M6 21v-4h4\" />\n </Glyph>\n);\nexport const RetryGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M4 12a8 8 0 1 0 2.3-5.7L4 8.5\" />\n <path d=\"M4 4v4.5h4.5\" />\n </Glyph>\n);\nexport const SortGlyph = ({ direction, ...p }: GlyphProps & { direction?: \"ascending\" | \"descending\" }) => (\n <Glyph {...p}>\n {direction === \"ascending\" ? <path d=\"m7 14 5-5 5 5\" /> : direction === \"descending\" ? <path d=\"m7 10 5 5 5-5\" /> : <path d=\"m8 9 4-4 4 4M8 15l4 4 4-4\" />}\n </Glyph>\n);\nexport const ArrowGlyph = ({ direction = \"right\", ...p }: GlyphProps & { direction?: \"left\" | \"right\" }) => (\n <Glyph {...p}>\n <path d={direction === \"left\" ? \"M19 12H5m0 0 6-6m-6 6 6 6\" : \"M5 12h14m0 0-6-6m6 6-6 6\"} />\n </Glyph>\n);\nexport const ExternalGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M14 4h6v6M20 4l-9 9\" />\n <path d=\"M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4\" />\n </Glyph>\n);\nexport const WarningGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <path d=\"M12 4 2.5 20h19L12 4Z\" />\n <path d=\"M12 10v4M12 17.2v.1\" />\n </Glyph>\n);\nexport const CopyGlyph = (p: GlyphProps) => (\n <Glyph {...p}>\n <rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" />\n <path d=\"M15 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h3\" />\n </Glyph>\n);\n"
1640
+ "component": "DateField",
1641
+ "description": "Day · month · year typed in the locale's order: numeric keyboards, the next box focused when one is full.",
1642
+ "props": [
1643
+ {
1644
+ "name": "label",
1645
+ "type": "string",
1646
+ "default": null,
1647
+ "description": "The visible label, passed translated (the group's name).",
1648
+ "required": true
1649
+ },
1650
+ {
1651
+ "name": "locale",
1652
+ "type": "string",
1653
+ "default": null,
1654
+ "description": "The BCP 47 locale: the order of the parts, the names of the months.",
1655
+ "required": true
1656
+ },
1657
+ {
1658
+ "name": "autoComplete",
1659
+ "type": "\"off\" | \"bday\"",
1660
+ "default": null,
1661
+ "description": "`bday` for a date of birth: the browser offers to fill it.",
1662
+ "required": false
1663
+ },
1664
+ {
1665
+ "name": "className",
1666
+ "type": "string",
1667
+ "default": null,
1668
+ "description": "Classes merged on the root.",
1669
+ "required": false
1670
+ },
1671
+ {
1672
+ "name": "defaultValue",
1673
+ "type": "string",
1674
+ "default": "",
1675
+ "description": "The date at first, uncontrolled.",
1676
+ "required": false
1677
+ },
1678
+ {
1679
+ "name": "disabled",
1680
+ "type": "boolean",
1681
+ "default": null,
1682
+ "description": "Not available.",
1683
+ "required": false
1684
+ },
1685
+ {
1686
+ "name": "error",
1687
+ "type": "string",
1688
+ "default": null,
1689
+ "description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
1690
+ "required": false
1691
+ },
1692
+ {
1693
+ "name": "hint",
1694
+ "type": "string",
1695
+ "default": null,
1696
+ "description": "A help text under the field (\"For example, 27 3 1990\").",
1697
+ "required": false
1698
+ },
1699
+ {
1700
+ "name": "labels",
1701
+ "type": "Partial<DateFieldLabels>",
1702
+ "default": null,
1703
+ "description": "The names of the parts, passed translated; English by default.",
1704
+ "required": false
1705
+ },
1706
+ {
1707
+ "name": "name",
1708
+ "type": "string",
1709
+ "default": null,
1710
+ "description": "The name of a hidden input holding the value, for a plain `<form>` post.",
1711
+ "required": false
1712
+ },
1713
+ {
1714
+ "name": "onBlur",
1715
+ "type": "(() => void)",
1716
+ "default": null,
1717
+ "description": "Called when the focus leaves the field.",
1718
+ "required": false
1719
+ },
1720
+ {
1721
+ "name": "onValueChange",
1722
+ "type": "((value: string) => void)",
1723
+ "default": null,
1724
+ "description": "Called on every key with the date typed so far (`\"\"` when the boxes are empty).",
1725
+ "required": false
1726
+ },
1727
+ {
1728
+ "name": "value",
1729
+ "type": "string",
1730
+ "default": null,
1731
+ "description": "The date, `YYYY-MM-DD` (controlled); `\"\"` for none. What is typed comes back as it is, real date or not: the schema decides.",
1732
+ "required": false
1733
+ }
1734
+ ]
683
1735
  },
684
1736
  {
685
- "path": "media.ts",
686
- "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"
1737
+ "component": "DateWheel",
1738
+ "description": "Day, month and year as three wheels in the locale's order, bounded by `min` and `max`: always a real date.",
1739
+ "props": [
1740
+ {
1741
+ "name": "label",
1742
+ "type": "string",
1743
+ "default": null,
1744
+ "description": "The visible label, passed translated (the group's name).",
1745
+ "required": true
1746
+ },
1747
+ {
1748
+ "name": "locale",
1749
+ "type": "string",
1750
+ "default": null,
1751
+ "description": "The BCP 47 locale: the order of the parts, the names of the months.",
1752
+ "required": true
1753
+ },
1754
+ {
1755
+ "name": "className",
1756
+ "type": "string",
1757
+ "default": null,
1758
+ "description": "Classes merged on the root.",
1759
+ "required": false
1760
+ },
1761
+ {
1762
+ "name": "defaultValue",
1763
+ "type": "string | null",
1764
+ "default": "null",
1765
+ "description": "The date at first, uncontrolled.",
1766
+ "required": false
1767
+ },
1768
+ {
1769
+ "name": "defaultView",
1770
+ "type": "string",
1771
+ "default": null,
1772
+ "description": "Where the wheels rest while there is no value. Default: `max` (a birth date: the youngest allowed).",
1773
+ "required": false
1774
+ },
1775
+ {
1776
+ "name": "disabled",
1777
+ "type": "boolean",
1778
+ "default": null,
1779
+ "description": "Not available.",
1780
+ "required": false
1781
+ },
1782
+ {
1783
+ "name": "error",
1784
+ "type": "string",
1785
+ "default": null,
1786
+ "description": "An error under the field: marks it invalid and is read with it. Replaces the hint.",
1787
+ "required": false
1788
+ },
1789
+ {
1790
+ "name": "hint",
1791
+ "type": "string",
1792
+ "default": null,
1793
+ "description": "A help text under the field (\"For example, 27 3 1990\").",
1794
+ "required": false
1795
+ },
1796
+ {
1797
+ "name": "labels",
1798
+ "type": "Partial<DateFieldLabels>",
1799
+ "default": null,
1800
+ "description": "The names of the parts, passed translated; English by default.",
1801
+ "required": false
1802
+ },
1803
+ {
1804
+ "name": "max",
1805
+ "type": "string",
1806
+ "default": null,
1807
+ "description": "The latest date (ISO): `latestBirthDate(18)` for an age gate. Default: today.",
1808
+ "required": false
1809
+ },
1810
+ {
1811
+ "name": "min",
1812
+ "type": "string",
1813
+ "default": null,
1814
+ "description": "The earliest date (ISO): the years start there, the days before it are refused. Default: 120 years ago.",
1815
+ "required": false
1816
+ },
1817
+ {
1818
+ "name": "name",
1819
+ "type": "string",
1820
+ "default": null,
1821
+ "description": "The name of a hidden input holding the value, for a plain `<form>` post.",
1822
+ "required": false
1823
+ },
1824
+ {
1825
+ "name": "onValueChange",
1826
+ "type": "((value: string) => void)",
1827
+ "default": null,
1828
+ "description": "Called with the date under the band, always a real date within `min` / `max`.",
1829
+ "required": false
1830
+ },
1831
+ {
1832
+ "name": "value",
1833
+ "type": "string | null",
1834
+ "default": null,
1835
+ "description": "The date, `YYYY-MM-DD` (controlled); `\"\"` or `null` for none (the wheels then rest on `defaultView`).",
1836
+ "required": false
1837
+ }
1838
+ ]
687
1839
  },
688
1840
  {
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.",
1841
+ "component": "DatePicker",
1842
+ "description": "A date field: a button showing the date in the locale's format, a calendar in a popover, an ISO value.",
708
1843
  "props": [
1844
+ {
1845
+ "name": "label",
1846
+ "type": "string",
1847
+ "default": null,
1848
+ "description": "The visible label, passed translated (also the button's accessible name, with the value).",
1849
+ "required": true
1850
+ },
709
1851
  {
710
1852
  "name": "locale",
711
1853
  "type": "string",
@@ -714,12 +1856,151 @@
714
1856
  "required": true
715
1857
  },
716
1858
  {
717
- "name": "autoFocus",
1859
+ "name": "className",
1860
+ "type": "string",
1861
+ "default": null,
1862
+ "description": "Classes merged on the root.",
1863
+ "required": false
1864
+ },
1865
+ {
1866
+ "name": "clearable",
718
1867
  "type": "boolean",
719
1868
  "default": null,
720
- "description": "Moves the focus to the active day when it mounts (inside a popover).",
1869
+ "description": "Adds a button that empties the value.",
1870
+ "required": false
1871
+ },
1872
+ {
1873
+ "name": "defaultOpen",
1874
+ "type": "boolean",
1875
+ "default": "false",
1876
+ "description": "Opens the calendar at first (a story, a test).",
1877
+ "required": false
1878
+ },
1879
+ {
1880
+ "name": "defaultValue",
1881
+ "type": "string | null",
1882
+ "default": "null",
1883
+ "description": "The chosen date at first, uncontrolled.",
1884
+ "required": false
1885
+ },
1886
+ {
1887
+ "name": "disabled",
1888
+ "type": "boolean",
1889
+ "default": null,
1890
+ "description": "Not available.",
1891
+ "required": false
1892
+ },
1893
+ {
1894
+ "name": "error",
1895
+ "type": "string",
1896
+ "default": null,
1897
+ "description": "An error under the field: marks it invalid and is read with it.",
1898
+ "required": false
1899
+ },
1900
+ {
1901
+ "name": "format",
1902
+ "type": "\"short\" | \"medium\" | \"long\"",
1903
+ "default": null,
1904
+ "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
1905
+ "required": false
1906
+ },
1907
+ {
1908
+ "name": "hint",
1909
+ "type": "string",
1910
+ "default": null,
1911
+ "description": "A help text under the field.",
1912
+ "required": false
1913
+ },
1914
+ {
1915
+ "name": "isDateDisabled",
1916
+ "type": "((date: string) => boolean)",
1917
+ "default": null,
1918
+ "description": "Refuses some dates (`(d) => isWeekend(d)`).",
1919
+ "required": false
1920
+ },
1921
+ {
1922
+ "name": "labels",
1923
+ "type": "Partial<DatePickerLabels>",
1924
+ "default": null,
1925
+ "description": "The words, passed translated; English by default.",
1926
+ "required": false
1927
+ },
1928
+ {
1929
+ "name": "max",
1930
+ "type": "string",
1931
+ "default": null,
1932
+ "description": "The latest date that can be picked.",
1933
+ "required": false
1934
+ },
1935
+ {
1936
+ "name": "min",
1937
+ "type": "string",
1938
+ "default": null,
1939
+ "description": "The earliest date that can be picked.",
1940
+ "required": false
1941
+ },
1942
+ {
1943
+ "name": "name",
1944
+ "type": "string",
1945
+ "default": null,
1946
+ "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
1947
+ "required": false
1948
+ },
1949
+ {
1950
+ "name": "onValueChange",
1951
+ "type": "((value: string | null) => void)",
1952
+ "default": null,
1953
+ "description": "Called with the date picked, or `null` when cleared.",
721
1954
  "required": false
722
1955
  },
1956
+ {
1957
+ "name": "placeholder",
1958
+ "type": "string",
1959
+ "default": null,
1960
+ "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
1961
+ "required": false
1962
+ },
1963
+ {
1964
+ "name": "today",
1965
+ "type": "string",
1966
+ "default": null,
1967
+ "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
1968
+ "required": false
1969
+ },
1970
+ {
1971
+ "name": "value",
1972
+ "type": "string | null",
1973
+ "default": null,
1974
+ "description": "The chosen date, ISO (controlled); `null` for none.",
1975
+ "required": false
1976
+ },
1977
+ {
1978
+ "name": "weekStartsOn",
1979
+ "type": "number",
1980
+ "default": null,
1981
+ "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
1982
+ "required": false
1983
+ }
1984
+ ]
1985
+ },
1986
+ {
1987
+ "component": "DateRangePicker",
1988
+ "description": "A period field: two clicks on one calendar (the days between tinted as you hover), optional presets.",
1989
+ "props": [
1990
+ {
1991
+ "name": "label",
1992
+ "type": "string",
1993
+ "default": null,
1994
+ "description": "The visible label, passed translated (also the button's accessible name, with the value).",
1995
+ "required": true
1996
+ },
1997
+ {
1998
+ "name": "locale",
1999
+ "type": "string",
2000
+ "default": null,
2001
+ "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
2002
+ "required": true
2003
+ },
723
2004
  {
724
2005
  "name": "className",
725
2006
  "type": "string",
@@ -728,10 +2009,52 @@
728
2009
  "required": false
729
2010
  },
730
2011
  {
731
- "name": "defaultMonth",
2012
+ "name": "clearable",
2013
+ "type": "boolean",
2014
+ "default": null,
2015
+ "description": "Adds a button that empties the value.",
2016
+ "required": false
2017
+ },
2018
+ {
2019
+ "name": "defaultOpen",
2020
+ "type": "boolean",
2021
+ "default": "false",
2022
+ "description": "Opens the calendar at first (a story, a test).",
2023
+ "required": false
2024
+ },
2025
+ {
2026
+ "name": "defaultValue",
2027
+ "type": "{ start: string; end: string; } | null",
2028
+ "default": "null",
2029
+ "description": "The range at first, uncontrolled.",
2030
+ "required": false
2031
+ },
2032
+ {
2033
+ "name": "disabled",
2034
+ "type": "boolean",
2035
+ "default": null,
2036
+ "description": "Not available.",
2037
+ "required": false
2038
+ },
2039
+ {
2040
+ "name": "error",
732
2041
  "type": "string",
733
2042
  "default": null,
734
- "description": "The month shown at first. Default: the value's, or today's.",
2043
+ "description": "An error under the field: marks it invalid and is read with it.",
2044
+ "required": false
2045
+ },
2046
+ {
2047
+ "name": "format",
2048
+ "type": "\"short\" | \"medium\" | \"long\"",
2049
+ "default": null,
2050
+ "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
2051
+ "required": false
2052
+ },
2053
+ {
2054
+ "name": "hint",
2055
+ "type": "string",
2056
+ "default": null,
2057
+ "description": "A help text under the field.",
735
2058
  "required": false
736
2059
  },
737
2060
  {
@@ -743,7 +2066,7 @@
743
2066
  },
744
2067
  {
745
2068
  "name": "labels",
746
- "type": "Partial<CalendarLabels>",
2069
+ "type": "Partial<DatePickerLabels>",
747
2070
  "default": null,
748
2071
  "description": "The words, passed translated; English by default.",
749
2072
  "required": false
@@ -763,31 +2086,38 @@
763
2086
  "required": false
764
2087
  },
765
2088
  {
766
- "name": "month",
2089
+ "name": "name",
767
2090
  "type": "string",
768
2091
  "default": null,
769
- "description": "The month shown, any day of it (controlled).",
2092
+ "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
770
2093
  "required": false
771
2094
  },
772
2095
  {
773
- "name": "onMonthChange",
774
- "type": "((month: string) => void)",
2096
+ "name": "numberOfMonths",
2097
+ "type": "1 | 2",
2098
+ "default": "2",
2099
+ "description": "Two months side by side (default, from a phone's width up) or one.",
2100
+ "required": false
2101
+ },
2102
+ {
2103
+ "name": "onValueChange",
2104
+ "type": "((value: { start: string; end: string; } | null) => void)",
775
2105
  "default": null,
776
- "description": "Called when the month shown changes.",
2106
+ "description": "Called once both ends are chosen (or with `null` when cleared).",
777
2107
  "required": false
778
2108
  },
779
2109
  {
780
- "name": "onSelect",
781
- "type": "((date: string) => void)",
2110
+ "name": "placeholder",
2111
+ "type": "string",
782
2112
  "default": null,
783
- "description": "Called with the day picked.",
2113
+ "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
784
2114
  "required": false
785
2115
  },
786
2116
  {
787
- "name": "range",
788
- "type": "DateRange | null",
2117
+ "name": "presets",
2118
+ "type": "readonly DateRangePreset[]",
789
2119
  "default": null,
790
- "description": "The chosen range (range mode): its ends are marked, the days between tinted.",
2120
+ "description": "Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar.",
791
2121
  "required": false
792
2122
  },
793
2123
  {
@@ -799,9 +2129,9 @@
799
2129
  },
800
2130
  {
801
2131
  "name": "value",
802
- "type": "string | null",
2132
+ "type": "{ start: string; end: string; } | null",
803
2133
  "default": null,
804
- "description": "The chosen date (one-date mode).",
2134
+ "description": "The chosen range (controlled); `null` for none. `end` is never `null` in a committed value.",
805
2135
  "required": false
806
2136
  },
807
2137
  {
@@ -814,21 +2144,21 @@
814
2144
  ]
815
2145
  },
816
2146
  {
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.",
2147
+ "component": "DayStrip",
2148
+ "description": "The days ahead in a row that scrolls sideways: one picked, today and the marked days shown.",
819
2149
  "props": [
820
2150
  {
821
2151
  "name": "label",
822
2152
  "type": "string",
823
2153
  "default": null,
824
- "description": "The visible label, passed translated (also the button's accessible name, with the value).",
2154
+ "description": "The group's accessible name (\"Delivery day\"), passed translated.",
825
2155
  "required": true
826
2156
  },
827
2157
  {
828
2158
  "name": "locale",
829
2159
  "type": "string",
830
2160
  "default": null,
831
- "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
2161
+ "description": "The BCP 47 locale of the names and the dates: always explicit.",
832
2162
  "required": true
833
2163
  },
834
2164
  {
@@ -839,142 +2169,197 @@
839
2169
  "required": false
840
2170
  },
841
2171
  {
842
- "name": "clearable",
843
- "type": "boolean",
2172
+ "name": "days",
2173
+ "type": "number",
2174
+ "default": "14",
2175
+ "description": "How many days. Default 14.",
2176
+ "required": false
2177
+ },
2178
+ {
2179
+ "name": "defaultValue",
2180
+ "type": "string | null",
2181
+ "default": "null",
2182
+ "description": "The chosen day at first, uncontrolled.",
2183
+ "required": false
2184
+ },
2185
+ {
2186
+ "name": "isDateDisabled",
2187
+ "type": "((date: string) => boolean)",
844
2188
  "default": null,
845
- "description": "Adds a button that empties the value.",
2189
+ "description": "Refuses some days (closed, full): drawn dashed, skipped by the arrows.",
846
2190
  "required": false
847
2191
  },
848
2192
  {
849
- "name": "defaultOpen",
850
- "type": "boolean",
851
- "default": "false",
852
- "description": "Opens the calendar at first (a story, a test).",
2193
+ "name": "isDateMarked",
2194
+ "type": "((date: string) => boolean)",
2195
+ "default": null,
2196
+ "description": "Marks the days that have something (a session, an event): a dot, and `labels.marked` in their name.",
853
2197
  "required": false
854
2198
  },
855
2199
  {
856
- "name": "defaultValue",
857
- "type": "string | null",
858
- "default": "null",
859
- "description": "The chosen date at first, uncontrolled.",
2200
+ "name": "labels",
2201
+ "type": "Partial<DayStripLabels>",
2202
+ "default": null,
2203
+ "description": "The words, passed translated; English by default.",
860
2204
  "required": false
861
2205
  },
862
2206
  {
863
- "name": "disabled",
864
- "type": "boolean",
2207
+ "name": "onValueChange",
2208
+ "type": "((value: string) => void)",
865
2209
  "default": null,
866
- "description": "Not available.",
2210
+ "description": "Called with the day picked.",
867
2211
  "required": false
868
2212
  },
869
2213
  {
870
- "name": "error",
2214
+ "name": "showHeader",
2215
+ "type": "boolean",
2216
+ "default": "true",
2217
+ "description": "Shows the month of the chosen day (or of the first) and the page buttons above the strip.",
2218
+ "required": false
2219
+ },
2220
+ {
2221
+ "name": "start",
871
2222
  "type": "string",
872
2223
  "default": null,
873
- "description": "An error under the field: marks it invalid and is read with it.",
2224
+ "description": "The first day of the strip. Default: today.",
874
2225
  "required": false
875
2226
  },
876
2227
  {
877
- "name": "format",
878
- "type": "\"short\" | \"medium\" | \"long\"",
2228
+ "name": "today",
2229
+ "type": "string",
879
2230
  "default": null,
880
- "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
2231
+ "description": "Today (marked by the node): inject it for a stable render or a test. Default: the device's date.",
881
2232
  "required": false
882
2233
  },
883
2234
  {
884
- "name": "hint",
2235
+ "name": "value",
2236
+ "type": "string | null",
2237
+ "default": null,
2238
+ "description": "The chosen day, ISO (controlled); `null` for none.",
2239
+ "required": false
2240
+ }
2241
+ ]
2242
+ },
2243
+ {
2244
+ "component": "MonthPicker",
2245
+ "description": "A month of a year: twelve months in a grid, the year above them.",
2246
+ "props": [
2247
+ {
2248
+ "name": "locale",
885
2249
  "type": "string",
886
2250
  "default": null,
887
- "description": "A help text under the field.",
2251
+ "description": "The BCP 47 locale of the month names: always explicit.",
2252
+ "required": true
2253
+ },
2254
+ {
2255
+ "name": "autoFocus",
2256
+ "type": "boolean",
2257
+ "default": null,
2258
+ "description": "Moves the focus to the active month when it mounts.",
888
2259
  "required": false
889
2260
  },
890
2261
  {
891
- "name": "isDateDisabled",
892
- "type": "((date: string) => boolean)",
2262
+ "name": "bare",
2263
+ "type": "boolean",
893
2264
  "default": null,
894
- "description": "Refuses some dates (`(d) => isWeekend(d)`).",
2265
+ "description": "Without its own year header (inside the calendar, which has one).",
895
2266
  "required": false
896
2267
  },
897
2268
  {
898
- "name": "labels",
899
- "type": "Partial<DatePickerLabels>",
2269
+ "name": "className",
2270
+ "type": "string",
900
2271
  "default": null,
901
- "description": "The words, passed translated; English by default.",
2272
+ "description": "Classes merged on the root.",
902
2273
  "required": false
903
2274
  },
904
2275
  {
905
- "name": "max",
2276
+ "name": "current",
906
2277
  "type": "string",
907
2278
  "default": null,
908
- "description": "The latest date that can be picked.",
2279
+ "description": "This month (marked): inject it for a stable render or a test. Default: the device's.",
909
2280
  "required": false
910
2281
  },
911
2282
  {
912
- "name": "min",
2283
+ "name": "defaultValue",
2284
+ "type": "string | null",
2285
+ "default": "null",
2286
+ "description": "The chosen month at first, uncontrolled.",
2287
+ "required": false
2288
+ },
2289
+ {
2290
+ "name": "labels",
2291
+ "type": "Partial<MonthPickerLabels>",
2292
+ "default": null,
2293
+ "description": "The words, passed translated; English by default.",
2294
+ "required": false
2295
+ },
2296
+ {
2297
+ "name": "max",
913
2298
  "type": "string",
914
2299
  "default": null,
915
- "description": "The earliest date that can be picked.",
2300
+ "description": "The latest month that can be picked, `YYYY-MM`.",
916
2301
  "required": false
917
2302
  },
918
2303
  {
919
- "name": "name",
2304
+ "name": "min",
920
2305
  "type": "string",
921
2306
  "default": null,
922
- "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
2307
+ "description": "The earliest month that can be picked, `YYYY-MM`.",
923
2308
  "required": false
924
2309
  },
925
2310
  {
926
2311
  "name": "onValueChange",
927
2312
  "type": "((value: string | null) => void)",
928
2313
  "default": null,
929
- "description": "Called with the date picked, or `null` when cleared.",
2314
+ "description": "Called with the month picked.",
930
2315
  "required": false
931
2316
  },
932
2317
  {
933
- "name": "placeholder",
934
- "type": "string",
2318
+ "name": "onYearChange",
2319
+ "type": "((year: number) => void)",
935
2320
  "default": null,
936
- "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
2321
+ "description": "Called when the year shown changes.",
937
2322
  "required": false
938
2323
  },
939
2324
  {
940
- "name": "today",
941
- "type": "string",
2325
+ "name": "value",
2326
+ "type": "string | null",
942
2327
  "default": null,
943
- "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
2328
+ "description": "The chosen month, `YYYY-MM` (controlled); `null` for none.",
944
2329
  "required": false
945
2330
  },
946
2331
  {
947
- "name": "value",
948
- "type": "string | null",
2332
+ "name": "variant",
2333
+ "type": "\"plain\" | \"card\"",
949
2334
  "default": null,
950
- "description": "The chosen date, ISO (controlled); `null` for none.",
2335
+ "description": "`plain` or `card` (its own raised surface).",
951
2336
  "required": false
952
2337
  },
953
2338
  {
954
- "name": "weekStartsOn",
2339
+ "name": "year",
955
2340
  "type": "number",
956
2341
  "default": null,
957
- "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
2342
+ "description": "The year shown (controlled).",
958
2343
  "required": false
959
2344
  }
960
2345
  ]
961
2346
  },
962
2347
  {
963
- "component": "DateRangePicker",
964
- "description": "A period field: two clicks on one calendar (the days between tinted as you hover), optional presets.",
2348
+ "component": "TimePicker",
2349
+ "description": "A time of day: wheels to set one, or slots to book one.",
965
2350
  "props": [
966
2351
  {
967
2352
  "name": "label",
968
2353
  "type": "string",
969
2354
  "default": null,
970
- "description": "The visible label, passed translated (also the button's accessible name, with the value).",
2355
+ "description": "The accessible name of the picker (\"Pick-up time\"), passed translated.",
971
2356
  "required": true
972
2357
  },
973
2358
  {
974
2359
  "name": "locale",
975
2360
  "type": "string",
976
2361
  "default": null,
977
- "description": "The BCP 47 locale of the month and day names (`\"en-US\"`, `\"fr-CA\"`): always explicit.",
2362
+ "description": "The BCP 47 locale: how times are written, and whether the day has a period (AM/PM).",
978
2363
  "required": true
979
2364
  },
980
2365
  {
@@ -985,129 +2370,195 @@
985
2370
  "required": false
986
2371
  },
987
2372
  {
988
- "name": "clearable",
989
- "type": "boolean",
2373
+ "name": "defaultValue",
2374
+ "type": "string | null",
990
2375
  "default": null,
991
- "description": "Adds a button that empties the value.",
2376
+ "description": "The time at first, uncontrolled.",
992
2377
  "required": false
993
2378
  },
994
2379
  {
995
- "name": "defaultOpen",
2380
+ "name": "disabled",
996
2381
  "type": "boolean",
997
- "default": "false",
998
- "description": "Opens the calendar at first (a story, a test).",
2382
+ "default": null,
2383
+ "description": "Not available.",
999
2384
  "required": false
1000
2385
  },
1001
2386
  {
1002
- "name": "defaultValue",
1003
- "type": "{ start: string; end: string; } | null",
1004
- "default": "null",
1005
- "description": "The range at first, uncontrolled.",
2387
+ "name": "groupLabels",
2388
+ "type": "{ morning: string; afternoon: string; evening: string; }",
2389
+ "default": null,
2390
+ "description": "Splits the slots under headings by part of the day: `{ morning: \"Morning\", afternoon: \"Afternoon\", evening: \"Evening\" }`.",
1006
2391
  "required": false
1007
2392
  },
1008
2393
  {
1009
- "name": "disabled",
2394
+ "name": "hour12",
1010
2395
  "type": "boolean",
1011
2396
  "default": null,
1012
- "description": "Not available.",
2397
+ "description": "Forces 12 or 24 hours. Default: the locale's.",
1013
2398
  "required": false
1014
2399
  },
1015
2400
  {
1016
- "name": "error",
1017
- "type": "string",
2401
+ "name": "isTimeDisabled",
2402
+ "type": "((time: string) => boolean)",
1018
2403
  "default": null,
1019
- "description": "An error under the field: marks it invalid and is read with it.",
2404
+ "description": "Refuses some slots (taken, too soon): drawn struck through, skipped by the arrows.",
1020
2405
  "required": false
1021
2406
  },
1022
2407
  {
1023
- "name": "format",
1024
- "type": "\"short\" | \"medium\" | \"long\"",
2408
+ "name": "labels",
2409
+ "type": "Partial<TimePickerLabels>",
1025
2410
  "default": null,
1026
- "description": "`short` · `medium` (default) · `long`: how the button writes the date.",
2411
+ "description": "The words of the wheels, passed translated; English by default.",
1027
2412
  "required": false
1028
2413
  },
1029
2414
  {
1030
- "name": "hint",
1031
- "type": "string",
2415
+ "name": "minuteStep",
2416
+ "type": "number",
1032
2417
  "default": null,
1033
- "description": "A help text under the field.",
2418
+ "description": "The minutes' step: 1, 5 (default), 10, 15, 30.",
1034
2419
  "required": false
1035
2420
  },
1036
2421
  {
1037
- "name": "isDateDisabled",
1038
- "type": "((date: string) => boolean)",
2422
+ "name": "onValueChange",
2423
+ "type": "((value: string) => void)",
1039
2424
  "default": null,
1040
- "description": "Refuses some dates (`(d) => isWeekend(d)`).",
2425
+ "description": "Called with the time chosen.",
1041
2426
  "required": false
1042
2427
  },
1043
2428
  {
1044
- "name": "labels",
1045
- "type": "Partial<DatePickerLabels>",
2429
+ "name": "slots",
2430
+ "type": "readonly string[]",
1046
2431
  "default": null,
1047
- "description": "The words, passed translated; English by default.",
2432
+ "description": "The times offered, `HH:mm` (`timeSlots(\"09:00\", \"17:00\", 30)`).",
1048
2433
  "required": false
1049
2434
  },
1050
2435
  {
1051
- "name": "max",
1052
- "type": "string",
2436
+ "name": "value",
2437
+ "type": "string | null",
1053
2438
  "default": null,
1054
- "description": "The latest date that can be picked.",
2439
+ "description": "The time, `HH:mm` on 24 hours (controlled); `null` for none.",
1055
2440
  "required": false
1056
2441
  },
1057
2442
  {
1058
- "name": "min",
2443
+ "name": "variant",
2444
+ "type": "\"slots\" | \"wheel\"",
2445
+ "default": null,
2446
+ "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.",
2447
+ "required": false
2448
+ }
2449
+ ]
2450
+ },
2451
+ {
2452
+ "component": "Wheel",
2453
+ "description": "One column of a `WheelPicker`: a spinbutton over a scroll-snapped list of values.",
2454
+ "props": [
2455
+ {
2456
+ "name": "label",
1059
2457
  "type": "string",
1060
2458
  "default": null,
1061
- "description": "The earliest date that can be picked.",
2459
+ "description": "The column's accessible name (\"Hour\", \"Month\"), passed translated.",
2460
+ "required": true
2461
+ },
2462
+ {
2463
+ "name": "options",
2464
+ "type": "readonly WheelOption[]",
2465
+ "default": null,
2466
+ "description": "The values, in order.",
2467
+ "required": true
2468
+ },
2469
+ {
2470
+ "name": "className",
2471
+ "type": "string",
2472
+ "default": null,
2473
+ "description": "Classes merged on the column (a width: `w-20`).",
1062
2474
  "required": false
1063
2475
  },
1064
2476
  {
1065
- "name": "name",
2477
+ "name": "defaultValue",
1066
2478
  "type": "string",
1067
2479
  "default": null,
1068
- "description": "The name of a hidden input holding the ISO value, for a plain `<form>` post.",
2480
+ "description": "The chosen value at first, uncontrolled. Default: the first one.",
2481
+ "required": false
2482
+ },
2483
+ {
2484
+ "name": "disabled",
2485
+ "type": "boolean",
2486
+ "default": null,
2487
+ "description": "Not available.",
1069
2488
  "required": false
1070
2489
  },
1071
2490
  {
1072
2491
  "name": "onValueChange",
1073
- "type": "((value: { start: string; end: string; } | null) => void)",
2492
+ "type": "((value: string) => void)",
1074
2493
  "default": null,
1075
- "description": "Called once both ends are chosen (or with `null` when cleared).",
2494
+ "description": "Called with the value that comes to rest under the band.",
1076
2495
  "required": false
1077
2496
  },
1078
2497
  {
1079
- "name": "placeholder",
1080
- "type": "string",
2498
+ "name": "value",
2499
+ "type": "string | null",
1081
2500
  "default": null,
1082
- "description": "Shown while nothing is chosen (\"Pick a date\"), passed translated.",
2501
+ "description": "The chosen value (controlled).",
1083
2502
  "required": false
2503
+ }
2504
+ ]
2505
+ },
2506
+ {
2507
+ "component": "WheelDivider",
2508
+ "description": "A divider between two wheels (`:` between the hours and the minutes).",
2509
+ "props": []
2510
+ },
2511
+ {
2512
+ "component": "WheelPicker",
2513
+ "description": "The frame of one or several wheels: a raised surface, the lit band across them, a group named by `label`.",
2514
+ "props": [
2515
+ {
2516
+ "name": "children",
2517
+ "type": "ReactNode",
2518
+ "default": null,
2519
+ "description": "The wheels (`Wheel`), and dividers (`WheelDivider`) between them.",
2520
+ "required": true
1084
2521
  },
1085
2522
  {
1086
- "name": "presets",
1087
- "type": "readonly DateRangePreset[]",
2523
+ "name": "label",
2524
+ "type": "string",
1088
2525
  "default": null,
1089
- "description": "Ranges one click away (\"Last 7 days\", \"This month\"), beside the calendar.",
2526
+ "description": "The group's accessible name (\"Departure time\"), passed translated.",
2527
+ "required": true
2528
+ },
2529
+ {
2530
+ "name": "aria-describedby",
2531
+ "type": "string",
2532
+ "default": null,
2533
+ "description": "The help text or the error that describes the group (their ids).",
1090
2534
  "required": false
1091
2535
  },
1092
2536
  {
1093
- "name": "today",
2537
+ "name": "className",
1094
2538
  "type": "string",
1095
2539
  "default": null,
1096
- "description": "Today (marked): inject it for a stable server render or a test. Default: the device's date.",
2540
+ "description": "Classes merged on the root.",
1097
2541
  "required": false
1098
2542
  },
1099
2543
  {
1100
- "name": "value",
1101
- "type": "{ start: string; end: string; } | null",
2544
+ "name": "invalid",
2545
+ "type": "boolean",
1102
2546
  "default": null,
1103
- "description": "The chosen range (controlled); `null` for none. `end` is never `null` in a committed value.",
2547
+ "description": "Marks the group invalid (a danger edge): pair it with an error read through `aria-describedby`.",
1104
2548
  "required": false
1105
2549
  },
1106
2550
  {
1107
- "name": "weekStartsOn",
1108
- "type": "number",
2551
+ "name": "unset",
2552
+ "type": "boolean",
1109
2553
  "default": null,
1110
- "description": "The first day of the week, 0 = Sunday. Default: the locale's.",
2554
+ "description": "Nothing is chosen yet: the values under the band stay quiet until a wheel is turned.",
2555
+ "required": false
2556
+ },
2557
+ {
2558
+ "name": "visible",
2559
+ "type": "3 | 5 | 7",
2560
+ "default": "5",
2561
+ "description": "How many values are visible: 3, 5 (default) or 7.",
1111
2562
  "required": false
1112
2563
  }
1113
2564
  ]