@eifi1/ui-kit 0.21.0 → 0.23.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 (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -1,22 +1,26 @@
1
1
  import { useEffect, useId, useMemo, useRef, useState } from "react";
2
- import type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from "react";
3
- import { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from "lucide-react";
2
+ import type { ComponentPropsWithoutRef, ComponentType, KeyboardEvent, ReactNode, RefObject } from "react";
3
+ import { Calendar, CalendarClock, ChevronDown, ChevronLeft, ChevronRight, X } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
- import { dirOf, type Direction } from "../lib/direction";
6
- import { addDaysIso, parseIsoDate } from "../lib/dates";
5
+ import { dirOf, horizontalStep, type Direction } from "../lib/direction";
6
+ import { addDaysIso, monthKey, pad, parseIsoDate } from "../lib/dates";
7
7
  import {
8
8
  DEFAULT_DATE_PICKER_LABELS,
9
9
  DEFAULT_PICKER_SHEET_LABELS,
10
+ useKitDateFormatter,
10
11
  useKitLabels,
11
12
  useKitLocale,
12
13
  type DatePickerLabels,
14
+ type KitDateFormatContext,
13
15
  } from "../i18n/kit-labels";
14
16
  import { splitTriggerAria } from "./trigger-aria";
15
17
  import type { TriggerAria } from "./trigger-aria";
16
- import { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
18
+ import { Button, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from "./ui";
19
+ import { FieldBox, FieldLabelLine, useFieldMessages } from "./field-parts";
17
20
  import { FullBleedDialog } from "./full-bleed-dialog";
18
21
  import { useMediaQuery } from "../hooks/use-media-query";
19
- import { MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
22
+ import { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
23
+ import { DEFAULT_MONTH_PICKER_LABELS } from "./month-picker";
20
24
  import { Popover } from "./popover";
21
25
  import { Tooltip } from "./tooltip";
22
26
 
@@ -47,19 +51,80 @@ interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onC
47
51
  disabled?: boolean;
48
52
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
49
53
  invalid?: boolean;
54
+ /**
55
+ * Standing advice for the field (keksdose K4, "uniform field anatomy"), as on a
56
+ * {@link Select}: plain TEXT is a caption under the field, attached with
57
+ * `aria-describedby`; a {@link FieldHint} rides the label line (or, unlabelled, sits
58
+ * at the field's end, outside the box).
59
+ */
60
+ hint?: ReactNode;
61
+ /**
62
+ * What is wrong with the date, in the caller's words ("The due date is before the
63
+ * invoice date") — {@link Input}'s `error` (keksdose K4: DateField errors had no slot).
64
+ * Rendered under the field (under the whole row, with `step` / `today`), attached to
65
+ * the trigger with `aria-describedby` — merged with any the caller passed, after the
66
+ * caption — and implies `invalid`. Passing the key at all keeps the field's box while
67
+ * there is no message, so the trigger is never remounted as one comes and goes.
68
+ */
69
+ error?: ReactNode;
50
70
  className?: string;
51
- /** Intl options for the trigger's formatted date (default: locale short date). */
71
+ /** Intl options for the trigger's formatted date (default: locale short date). Win
72
+ * over the provider's `formatDate`. */
52
73
  formatOptions?: Intl.DateTimeFormatOptions;
53
- /** Render the trigger's text for an ISO date yourself; wins over `formatOptions`.
74
+ /** Render the trigger's text for an ISO date yourself; wins over `formatOptions` and
75
+ * over the provider's `formatDate`.
54
76
  * For a host whose date rendering is more than one `Intl` call can say — Keksdose
55
77
  * puts the weekday's name in the UI language beside digits ordered by a separate
56
- * format preference, two locales in one string. `locale` still drives the calendar. */
78
+ * format preference, two locales in one string. `locale` still drives the calendar.
79
+ * Set that rendering once with `<UiKitProvider formatDate>` (keksdose K12) and this
80
+ * is for the one field that differs. */
57
81
  formatValue?: (iso: string) => string;
58
82
  /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are
59
83
  * icon-only, so their `aria-label` is the only name they have — and the package
60
84
  * ships English defaults, which a translating host has to be able to replace.
61
85
  * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */
62
86
  calendarLabels?: MiniCalendarProps["labels"];
87
+ /**
88
+ * Make the panel's caption ("July 2026") a button that opens the year's twelve
89
+ * months, with ‹ › stepping a year; picking a month returns to its days. Far dates
90
+ * stop being a march of month arrows.
91
+ *
92
+ * kastlan is why: its service-charge period runs July to June, and with one month on
93
+ * screen and ‹ › the only way across, the end of the period was eleven clicks past
94
+ * the start — so kastlan kept two date fields instead of `RhfDateRangePicker`. With
95
+ * the jump the whole period is open, caption, Jul, 1, caption, next year, Jun, 30.
96
+ *
97
+ * What it adds, for the keyboard and a screen reader:
98
+ * - the caption is a disclosure button (`aria-expanded`). Its name is the month it
99
+ * shows, so speech input can say what it sees; it is `aria-live`, as the
100
+ * calendar's caption was, so paging months still says where you landed; and it is
101
+ * described by `monthPicker.panel` ("Choose a month").
102
+ * - the month grid is `MonthPicker`'s grid: one tab stop, arrows walk the months
103
+ * (across the year's edge), Home/End the row, PageUp/PageDown a year, Enter or a
104
+ * click picks. Focus opens on the month on show and comes back on its days — the
105
+ * selected day if it is in that month, else the 1st. Months outside `min`/`max`
106
+ * are `aria-disabled`, and a year arrow is once the whole year beyond is.
107
+ * - Escape in the month grid goes back to the days without closing the panel (focus
108
+ * on the caption); in the days, Escape closes the panel as before.
109
+ *
110
+ * No new strings: the month arrows keep `miniCalendar.previousMonth` / `nextMonth`
111
+ * (`calendarLabels`), and the month grid speaks with `MonthPicker`'s words —
112
+ * `monthPicker.previousYear` / `nextYear` / `month` / `panel` — because it is that
113
+ * grid, and a host that translated it once should not have to again.
114
+ *
115
+ * Off by default: the caption becomes one more stop in the panel's tab order, which a
116
+ * 0.x minor does not spring on a form. Works in the popover and in
117
+ * `DateRangePicker`'s phone sheet alike, beside presets and in `commit="apply"`
118
+ * (a preset picked while the months are open goes back to the days, on its start).
119
+ *
120
+ * Why not a two-month panel (`months={2}`) instead, or beside it: for a twelve-month
121
+ * period it still pages six times where this takes one year step, and two
122
+ * `MiniCalendar`s side by side each follow the value on their own — picking the end
123
+ * in the right-hand month would pull the left one onto it — so the calendar would
124
+ * have to learn to share a month first. Not worth that for a saving of one arrow on a
125
+ * range across a single month boundary.
126
+ */
127
+ monthJump?: boolean;
63
128
  }
64
129
 
65
130
  /** Format an ISO date for the trigger. `lib/dates`' `formatIsoDate` insists on a
@@ -69,6 +134,30 @@ function formatDate(iso: string, locale: string | undefined, options?: Intl.Date
69
134
  return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? "";
70
135
  }
71
136
 
137
+ /**
138
+ * The trigger's date renderer, in the kit's order of precedence: the picker's own
139
+ * `formatValue`, then its `formatOptions` (an explicit `Intl` look for this field),
140
+ * then the provider's `formatDate` (keksdose K12), then the locale's short date. A
141
+ * provider formatter that answers `""` falls through to the default for that date.
142
+ */
143
+ function useTriggerFormat(
144
+ source: KitDateFormatContext["source"],
145
+ locale: string | undefined,
146
+ formatOptions: Intl.DateTimeFormatOptions | undefined,
147
+ formatValue: ((iso: string) => string) | undefined,
148
+ ): (iso: string) => string {
149
+ const fromProvider = useKitDateFormatter();
150
+ if (formatValue) return formatValue;
151
+ return (iso) => {
152
+ if (formatOptions === undefined && fromProvider) {
153
+ // A single date stands alone, where the weekday helps; a range's two ends do not.
154
+ const text = fromProvider(iso, { unit: "day", source, locale, weekday: source === "datePicker" });
155
+ if (text) return text;
156
+ }
157
+ return formatDate(iso, locale, formatOptions);
158
+ };
159
+ }
160
+
72
161
  /**
73
162
  * Everything the kit's own trigger button carries, handed to a custom one
74
163
  * ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread
@@ -285,6 +374,7 @@ function DateField({
285
374
  sheet = false,
286
375
  sheetBackCloses = true,
287
376
  renderTrigger,
377
+ labelHint,
288
378
  children,
289
379
  // The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
290
380
  // pair, and a required date read as optional while it sat on the role-less div.
@@ -295,6 +385,8 @@ function DateField({
295
385
  triggerText: string;
296
386
  hasValue: boolean;
297
387
  label?: ReactNode;
388
+ /** A `FieldHint` for the label line, beside the label. */
389
+ labelHint?: ReactNode;
298
390
  disabled?: boolean;
299
391
  clearable?: boolean;
300
392
  /** Already resolved by the picker (prop > provider > English). */
@@ -412,7 +504,7 @@ function DateField({
412
504
  // The caller's attributes land here, on the field's own box — the trigger inside is
413
505
  // named by `aria-labelledby` and must keep the id pair it is given.
414
506
  <div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
415
- {label !== undefined && <FieldLabel>{label}</FieldLabel>}
507
+ {label !== undefined && <FieldLabelLine label={label} hint={labelHint} />}
416
508
  {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names
417
509
  nothing on its own — this hidden twin is what the trigger is named by.
418
510
  `aria-hidden` because a name reference reads a hidden element deliberately
@@ -511,6 +603,374 @@ function StepButton({
511
603
  );
512
604
  }
513
605
 
606
+ // ── Month jump (`monthJump`, kastlan) ─────────────────────────────────────
607
+
608
+ /** A "YYYY-MM" key as a count of months, and back: month arithmetic with no `Date`
609
+ * rollover to step around. */
610
+ const monthIndex = (key: string) => Number(key.slice(0, 4)) * 12 + Number(key.slice(5, 7)) - 1;
611
+ const keyOfIndex = (index: number) =>
612
+ `${String(Math.floor(index / 12)).padStart(4, "0")}-${pad((index % 12) + 1)}`;
613
+ const shiftKey = (key: string, delta: number) => keyOfIndex(monthIndex(key) + delta);
614
+ /** The 1st of a "YYYY-MM" month, for `Intl` to name. */
615
+ const firstOfKey = (key: string) => new Date(Number(key.slice(0, 4)), Number(key.slice(5, 7)) - 1, 1);
616
+
617
+ /** Upper-case the first letter only, as `MonthPicker` does: "julio de 2026" heads a
618
+ * panel as "Julio de 2026" — not "Julio De 2026", which CSS `capitalize` would write. */
619
+ const upperFirst = (text: string, locale: string | undefined) =>
620
+ text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
621
+
622
+ /** Months per row of the month grid — `MonthPicker`'s 4×3. */
623
+ const MONTH_COLUMNS = 3;
624
+
625
+ /**
626
+ * The month `MiniCalendar` opens on — the selection, or today, pulled inside the
627
+ * bounds — worked out the way it does, so the caption drawn over a calendar that was
628
+ * given no `month` names the month that calendar is showing.
629
+ */
630
+ function openingMonth(from: string, to: string, lo: Date | null, hi: Date | null): string {
631
+ const start = parseIsoDate(from) ?? parseIsoDate(to) ?? new Date();
632
+ if (lo && start < lo) return monthKey(lo);
633
+ if (hi && start > hi) return monthKey(hi);
634
+ return monthKey(start);
635
+ }
636
+
637
+ type PanelCalendarProps = Pick<
638
+ MiniCalendarProps,
639
+ "mode" | "from" | "to" | "locale" | "min" | "max" | "labels" | "onSelect" | "focusOnOpen"
640
+ > & {
641
+ /** See {@link DatePickerBaseProps.monthJump}. */
642
+ monthJump?: boolean;
643
+ };
644
+
645
+ /** The panel's calendar: the plain {@link MiniCalendar}, exactly as before — or, with
646
+ * `monthJump`, the same calendar under a caption that opens a month grid. */
647
+ function PanelCalendar({ monthJump, ...calendar }: PanelCalendarProps) {
648
+ return monthJump ? <JumpCalendar {...calendar} /> : <MiniCalendar {...calendar} />;
649
+ }
650
+
651
+ /** How the day grid is mounted: `key` remounts it, `month` is the month it follows
652
+ * (left out until the panel first moves, so it opens exactly where it always has), and
653
+ * `focus` is its `focusOnOpen`. */
654
+ interface DayGridMount {
655
+ key: number;
656
+ month?: string;
657
+ focus: boolean;
658
+ }
659
+
660
+ /**
661
+ * {@link MiniCalendar} with its navigation drawn here — ‹ caption › — so the caption can
662
+ * be a button that swaps the days for a month grid ({@link DatePickerBaseProps.monthJump}).
663
+ *
664
+ * Built on the calendar's own `hideNavigation` / `month` / `onMonthChange`, not a fork
665
+ * of it: the days, their keys, the range band and every announcement stay the
666
+ * calendar's. What this keeps is `shown`, the month on screen, for the caption — fed by
667
+ * the arrows here, by the calendar's `onMonthChange` (PageUp, arrowing off the edge),
668
+ * and by the value, whose changes the calendar follows WITHOUT reporting them (a preset
669
+ * in `commit="apply"` moves the grid to its start). That last one is mirrored below with
670
+ * the calendar's own rule, and when it lands in another month the day grid is remounted
671
+ * there rather than steered by `month`: a `month` that changes in the same render as the
672
+ * value would win over the value's day, and the grid's tab stop would land on the
673
+ * month's same-numbered day instead of the preset's start.
674
+ *
675
+ * The month grid is `MonthPicker`'s, redrawn here because that one is internal to its
676
+ * file; same roles, keys and words (`monthPicker` labels).
677
+ */
678
+ function JumpCalendar({
679
+ from = "",
680
+ to = "",
681
+ mode = "range",
682
+ locale,
683
+ min,
684
+ max,
685
+ labels,
686
+ onSelect,
687
+ focusOnOpen,
688
+ }: Omit<PanelCalendarProps, "monthJump">) {
689
+ // The month arrows say what they said when the calendar drew them; the month grid
690
+ // says what `MonthPicker`'s says.
691
+ const calendarText = useKitLabels("miniCalendar", DEFAULT_MINI_CALENDAR_LABELS, labels);
692
+ const monthText = useKitLabels("monthPicker", DEFAULT_MONTH_PICKER_LABELS);
693
+ const id = useId();
694
+ const captionId = `${id}-caption`;
695
+ const hintId = `${id}-hint`;
696
+ const captionRef = useRef<HTMLButtonElement>(null);
697
+ const monthsRef = useRef<HTMLDivElement>(null);
698
+
699
+ const lo = min ? parseIsoDate(min) : null;
700
+ const hi = max ? parseIsoDate(max) : null;
701
+ const minKey = lo ? monthKey(lo) : null;
702
+ const maxKey = hi ? monthKey(hi) : null;
703
+ const outOfBounds = (key: string) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
704
+ const clampKey = (key: string) => (minKey && key < minKey ? minKey : maxKey && key > maxKey ? maxKey : key);
705
+
706
+ const [view, setView] = useState<"days" | "months">("days");
707
+ const [shown, setShown] = useState(() => openingMonth(from, to, lo, hi));
708
+ const [dayGrid, setDayGrid] = useState<DayGridMount>(() => ({ key: 0, focus: Boolean(focusOnOpen) }));
709
+ // The month holding the month grid's tab stop — and with it the year on show.
710
+ const [active, setActive] = useState(shown);
711
+
712
+ // The value moved from outside the day grid (a preset, a reset): follow the end that
713
+ // changed, by the calendar's own rule, and remount the days there — see above. Also
714
+ // what takes an open month grid back to the days when a preset is picked beside it.
715
+ const [lastValue, setLastValue] = useState({ from, to });
716
+ if (lastValue.from !== from || lastValue.to !== to) {
717
+ const changed = lastValue.from !== from ? from : to;
718
+ setLastValue({ from, to });
719
+ const next = parseIsoDate(changed) ?? parseIsoDate(from) ?? parseIsoDate(to);
720
+ const key = next ? monthKey(next) : null;
721
+ if (key && (key !== shown || view === "months")) {
722
+ setShown(key);
723
+ setView("days");
724
+ setDayGrid((d) => ({ key: d.key + 1, month: key, focus: false }));
725
+ }
726
+ }
727
+
728
+ // A month cell that has to take focus once it exists: opening the grid, and arrowing
729
+ // across a year's edge, which relabels the same twelve buttons.
730
+ const pendingFocus = useRef<string | null>(null);
731
+ useEffect(() => {
732
+ const key = pendingFocus.current;
733
+ if (!key) return;
734
+ pendingFocus.current = null;
735
+ monthsRef.current?.querySelector<HTMLElement>(`[data-month="${key}"]`)?.focus();
736
+ });
737
+
738
+ const showMonth = (key: string) => {
739
+ setShown(key);
740
+ // Follows by `month`: the calendar keeps the day of the month (clamped), and the
741
+ // focus stays on the arrow being pressed.
742
+ setDayGrid((d) => ({ ...d, month: key }));
743
+ };
744
+ const openMonths = () => {
745
+ const start = clampKey(shown);
746
+ pendingFocus.current = start;
747
+ setActive(start);
748
+ setView("months");
749
+ };
750
+ const closeMonths = (focusCaption: boolean) => {
751
+ setView("days");
752
+ setDayGrid((d) => ({ ...d, month: shown, focus: false }));
753
+ if (focusCaption) captionRef.current?.focus();
754
+ };
755
+ const pick = (key: string) => {
756
+ setShown(key);
757
+ setView("days");
758
+ // Picked to go there: the focus goes with it, onto the days.
759
+ setDayGrid((d) => ({ key: d.key + 1, month: key, focus: true }));
760
+ };
761
+ const moveActive = (key: string, focus: boolean) => {
762
+ if (focus) pendingFocus.current = key;
763
+ setActive(key);
764
+ };
765
+
766
+ const inDays = view === "days";
767
+ // Escape in the month grid is "back", not "close": the month grid is a step INSIDE the
768
+ // panel, and losing the half-made range to a key meant to undo that step would be the
769
+ // worse surprise. Stopped here, so neither the popover nor the phone sheet sees it.
770
+ const escapeToDays = (e: KeyboardEvent<HTMLElement>) => {
771
+ if (e.key !== "Escape" || inDays) return;
772
+ e.preventDefault();
773
+ e.stopPropagation();
774
+ closeMonths(true);
775
+ };
776
+
777
+ const onMonthKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
778
+ if (e.key === "Escape") return escapeToDays(e);
779
+ // Left and right are visual: the grid runs the other way in a right-to-left panel.
780
+ const step = horizontalStep(e.key, e.currentTarget);
781
+ const col = (monthIndex(active) % 12) % MONTH_COLUMNS;
782
+ let delta: number;
783
+ switch (step ? "horizontal" : e.key) {
784
+ case "horizontal":
785
+ delta = step;
786
+ break;
787
+ case "ArrowUp":
788
+ delta = -MONTH_COLUMNS;
789
+ break;
790
+ case "ArrowDown":
791
+ delta = MONTH_COLUMNS;
792
+ break;
793
+ case "Home":
794
+ delta = -col;
795
+ break;
796
+ case "End":
797
+ delta = MONTH_COLUMNS - 1 - col;
798
+ break;
799
+ case "PageUp":
800
+ delta = -12;
801
+ break;
802
+ case "PageDown":
803
+ delta = 12;
804
+ break;
805
+ default:
806
+ return;
807
+ }
808
+ // Before the no-op check: these keys also scroll the panel.
809
+ e.preventDefault();
810
+ if (delta !== 0) moveActive(shiftKey(active, delta), true);
811
+ };
812
+
813
+ const year = Number(active.slice(0, 4));
814
+ // A year arrow is dead once every month of the year it leads to is out of bounds.
815
+ const prevYearBlocked = Boolean(minKey && year - 1 < Number(minKey.slice(0, 4)));
816
+ const nextYearBlocked = Boolean(maxKey && year + 1 > Number(maxKey.slice(0, 4)));
817
+ const prevBlocked = !inDays && prevYearBlocked;
818
+ const nextBlocked = !inDays && nextYearBlocked;
819
+
820
+ const longName = useMemo(() => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }), [locale]);
821
+ const shortNames = useMemo(() => {
822
+ const fmt = new Intl.DateTimeFormat(locale, { month: "short" });
823
+ return Array.from({ length: 12 }, (_, i) => upperFirst(fmt.format(new Date(2000, i, 1)), locale));
824
+ }, [locale]);
825
+ const caption = inDays
826
+ ? upperFirst(longName.format(firstOfKey(shown)), locale)
827
+ : new Intl.DateTimeFormat(locale, { year: "numeric" }).format(new Date(year, 0, 1));
828
+
829
+ // The selection, as months: both ends, and the months between (a range's band).
830
+ const fromDate = parseIsoDate(from);
831
+ const toDate = parseIsoDate(to);
832
+ const fromKey = fromDate ? monthKey(fromDate) : null;
833
+ const toKey = toDate ? monthKey(toDate) : null;
834
+ const low = fromKey ?? toKey;
835
+ const high = toKey ?? fromKey;
836
+ const currentKey = monthKey(new Date());
837
+
838
+ // Aria-disabled rather than disabled, like the day cells: a focused button that
839
+ // turns `disabled` drops the focus to <body>, mid-panel.
840
+ const arrow =
841
+ "rounded p-1 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] aria-disabled:cursor-not-allowed aria-disabled:opacity-30 aria-disabled:hover:bg-transparent";
842
+
843
+ return (
844
+ <div className="select-none">
845
+ {/* The calendar's own row, redrawn: the same arrows in the same places. Each
846
+ button stays mounted across the two views and changes what it steps, so a
847
+ press never pulls the element out from under the pointer or the focus. */}
848
+ <div className="flex items-center justify-between gap-1 pb-1">
849
+ <button
850
+ type="button"
851
+ aria-label={inDays ? calendarText.previousMonth : monthText.previousYear}
852
+ aria-disabled={prevBlocked || undefined}
853
+ onClick={() => {
854
+ if (inDays) showMonth(shiftKey(shown, -1));
855
+ else if (!prevBlocked) moveActive(shiftKey(active, -12), false);
856
+ }}
857
+ onKeyDown={escapeToDays}
858
+ className={arrow}
859
+ >
860
+ {/* Mirrored in RTL: "previous" points to the reading START, which is right. */}
861
+ <ChevronLeft className="size-4 rtl:-scale-x-100" aria-hidden />
862
+ </button>
863
+ <button
864
+ ref={captionRef}
865
+ type="button"
866
+ aria-expanded={!inDays}
867
+ aria-describedby={inDays ? hintId : undefined}
868
+ onClick={inDays ? openMonths : () => closeMonths(false)}
869
+ onKeyDown={escapeToDays}
870
+ className="inline-flex min-w-0 items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
871
+ >
872
+ {/* The name is the text, so it says what is on screen; `aria-live` because
873
+ pressing an arrow leaves focus ON the arrow, and this is the only thing
874
+ that says where it went. It also names the month grid. */}
875
+ <span id={captionId} aria-live="polite" className="truncate">
876
+ {caption}
877
+ </span>
878
+ <ChevronDown
879
+ aria-hidden
880
+ className={cn("size-3.5 shrink-0 text-[var(--text-muted)]", !inDays && "rotate-180")}
881
+ />
882
+ </button>
883
+ <button
884
+ type="button"
885
+ aria-label={inDays ? calendarText.nextMonth : monthText.nextYear}
886
+ aria-disabled={nextBlocked || undefined}
887
+ onClick={() => {
888
+ if (inDays) showMonth(shiftKey(shown, 1));
889
+ else if (!nextBlocked) moveActive(shiftKey(active, 12), false);
890
+ }}
891
+ onKeyDown={escapeToDays}
892
+ className={arrow}
893
+ >
894
+ <ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
895
+ </button>
896
+ </div>
897
+ {/* The caption's description — read through the reference, never on its own. */}
898
+ <span id={hintId} hidden>
899
+ {monthText.panel}
900
+ </span>
901
+ {inDays ? (
902
+ <MiniCalendar
903
+ key={dayGrid.key}
904
+ mode={mode}
905
+ from={from}
906
+ to={to}
907
+ locale={locale}
908
+ min={min}
909
+ max={max}
910
+ labels={labels}
911
+ onSelect={onSelect}
912
+ focusOnOpen={dayGrid.focus}
913
+ hideNavigation
914
+ month={dayGrid.month}
915
+ onMonthChange={showMonth}
916
+ />
917
+ ) : (
918
+ <div ref={monthsRef} role="grid" aria-labelledby={captionId} className="grid gap-1">
919
+ {Array.from({ length: 12 / MONTH_COLUMNS }, (_, row) => (
920
+ <div key={row} role="row" className="grid grid-cols-3 gap-1">
921
+ {Array.from({ length: MONTH_COLUMNS }, (_, c) => {
922
+ const index = row * MONTH_COLUMNS + c;
923
+ const key = keyOfIndex(year * 12 + index);
924
+ const disabled = outOfBounds(key);
925
+ const selected = low !== null && high !== null && key >= low && key <= high;
926
+ const end = key === fromKey || key === toKey;
927
+ const current = key === currentKey;
928
+ return (
929
+ <button
930
+ key={c}
931
+ type="button"
932
+ role="gridcell"
933
+ data-month={key}
934
+ tabIndex={key === active ? 0 : -1}
935
+ aria-label={monthText.month(longName.format(firstOfKey(key)))}
936
+ aria-selected={selected}
937
+ aria-disabled={disabled || undefined}
938
+ aria-current={current ? "date" : undefined}
939
+ onClick={() => {
940
+ // The guard `disabled` would have been: out-of-bounds months stay
941
+ // focusable, so the roving tab stop never falls into a hole.
942
+ if (!disabled) pick(key);
943
+ }}
944
+ onKeyDown={onMonthKeyDown}
945
+ className={cn(
946
+ // 44px rows: the touch-target size, and four of them stand as tall
947
+ // as a month of days, so the panel does not jump as the views swap.
948
+ "h-11 rounded px-1 text-sm tabular-nums transition-colors",
949
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset",
950
+ // A --brand ring on a --brand fill exists only in the DOM.
951
+ end && !disabled ? "focus-visible:ring-[var(--brand-contrast)]" : "focus-visible:ring-[var(--brand)]",
952
+ disabled
953
+ ? "cursor-not-allowed text-[var(--text-placeholder)]"
954
+ : end
955
+ ? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
956
+ : selected
957
+ ? "bg-[var(--brand-bg)] font-medium text-[var(--brand-muted)] hover:bg-[var(--brand-bg-hover)]"
958
+ : "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
959
+ current && !selected && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
960
+ )}
961
+ >
962
+ {shortNames[index]}
963
+ </button>
964
+ );
965
+ })}
966
+ </div>
967
+ ))}
968
+ </div>
969
+ )}
970
+ </div>
971
+ );
972
+ }
973
+
514
974
  export interface DatePickerProps extends DatePickerBaseProps {
515
975
  /** ISO "YYYY-MM-DD", or "" for empty. */
516
976
  value: string;
@@ -568,35 +1028,62 @@ function useDatePickerLabels(
568
1028
  return useKitLabels("datePicker", DEFAULT_DATE_PICKER_LABELS, fromProps);
569
1029
  }
570
1030
 
1031
+ /**
1032
+ * The caller's `hint` when it has no label line to ride: at the end of the field,
1033
+ * outside the box, as an unlabelled {@link Select} places its FieldHint (0.15.5 P8).
1034
+ */
1035
+ function withEndHint(field: ReactNode, hint: ReactNode): ReactNode {
1036
+ if (hint === undefined) return field;
1037
+ return (
1038
+ <div className="flex items-center gap-1.5">
1039
+ <div className="min-w-0 flex-1">{field}</div>
1040
+ <span className="flex shrink-0 items-center">{hint}</span>
1041
+ </div>
1042
+ );
1043
+ }
1044
+
571
1045
  /** Single-date picker: a field showing the formatted date, opening a calendar. */
572
- export function DatePicker({
573
- value,
574
- onChange,
575
- locale: localeProp,
576
- placeholder,
577
- min,
578
- max,
579
- formatOptions,
580
- formatValue,
581
- step,
582
- stepLabels,
583
- today,
584
- todayLabel,
585
- className,
586
- calendarLabels,
587
- label,
588
- clearable,
589
- clearLabel,
590
- disabled,
591
- invalid,
592
- // Named so it reaches the field (and so its trigger) when a step/today row is the
593
- // root and takes the rest of the caller's props.
594
- "aria-required": ariaRequired,
595
- ...rest
596
- }: DatePickerProps) {
1046
+ export function DatePicker(props: DatePickerProps) {
1047
+ const {
1048
+ value,
1049
+ onChange,
1050
+ locale: localeProp,
1051
+ placeholder,
1052
+ min,
1053
+ max,
1054
+ formatOptions,
1055
+ formatValue,
1056
+ step,
1057
+ stepLabels,
1058
+ today,
1059
+ todayLabel,
1060
+ className,
1061
+ calendarLabels,
1062
+ monthJump,
1063
+ label,
1064
+ clearable,
1065
+ clearLabel,
1066
+ disabled,
1067
+ invalid,
1068
+ hint,
1069
+ error,
1070
+ // Named so it reaches the field (and so its trigger) when a step/today row is the
1071
+ // root and takes the rest of the caller's props.
1072
+ "aria-required": ariaRequired,
1073
+ ...rest
1074
+ } = props;
597
1075
  const locale = useKitLocale(localeProp);
598
1076
  const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);
599
- const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));
1077
+ const render = useTriggerFormat("datePicker", locale, formatOptions, formatValue);
1078
+ // keksdose K4: the caption and the error under the field, attached to the TRIGGER
1079
+ // (the element a screen reader is on) after any description of the caller's own.
1080
+ const messages = useFieldMessages({
1081
+ hint,
1082
+ error,
1083
+ invalid,
1084
+ describedBy: rest["aria-describedby"],
1085
+ ariaInvalid: rest["aria-invalid"],
1086
+ });
600
1087
  // With a step or a today button beside it the field is no longer the outermost
601
1088
  // element — the flex row at the bottom is, and a caller's attributes belong on
602
1089
  // whichever of the two is actually the root. Naming the shared props rather than
@@ -608,20 +1095,23 @@ export function DatePicker({
608
1095
  // With step/today buttons the flex row is the root and takes the caller's props —
609
1096
  // except the ones that name the field, which still belong on its trigger.
610
1097
  {...(wrapped ? triggerAria : rest)}
1098
+ aria-describedby={messages.describedBy}
611
1099
  aria-required={ariaRequired}
612
1100
  label={label}
1101
+ labelHint={messages.labelHint}
613
1102
  clearable={clearable}
614
1103
  clearLabel={text.clear}
615
1104
  panelLabel={text.panel}
616
1105
  disabled={disabled}
617
- invalid={invalid}
1106
+ invalid={messages.isInvalid}
618
1107
  className={step ? "min-w-0 flex-1" : className}
619
1108
  hasValue={Boolean(value)}
620
1109
  triggerText={value ? render(value) : (placeholder ?? "")}
621
1110
  onClear={() => onChange("")}
622
1111
  >
623
1112
  {(close) => (
624
- <MiniCalendar
1113
+ <PanelCalendar
1114
+ monthJump={monthJump}
625
1115
  mode="single"
626
1116
  focusOnOpen
627
1117
  from={value}
@@ -638,7 +1128,16 @@ export function DatePicker({
638
1128
  )}
639
1129
  </DateField>
640
1130
  );
641
- if (!wrapped) return field;
1131
+ // Passed at all — even as `undefined` — the field keeps its box; see FieldBox.
1132
+ const reserve = "error" in props || "hint" in props;
1133
+ const endHint = label === undefined ? messages.labelHint : undefined;
1134
+ if (!wrapped) {
1135
+ return (
1136
+ <FieldBox below={messages.below} reserve={reserve}>
1137
+ {withEndHint(field, endHint)}
1138
+ </FieldBox>
1139
+ );
1140
+ }
642
1141
 
643
1142
  // An empty field has nothing to step from, so both buttons are dead until a date
644
1143
  // is picked. Bounds are compared as strings: "YYYY-MM-DD" sorts chronologically.
@@ -649,7 +1148,7 @@ export function DatePicker({
649
1148
  if (!next || disabled) return true;
650
1149
  return outOfBounds(next);
651
1150
  };
652
- return (
1151
+ const row = (
653
1152
  // ONE joined control, not three boxes in a row: the buttons and the field share
654
1153
  // their borders (each piece overlaps the previous by a pixel) and only the outer
655
1154
  // ends are rounded — the button-group shape, so ‹ date › reads as one field with
@@ -701,6 +1200,13 @@ export function DatePicker({
701
1200
  )}
702
1201
  </div>
703
1202
  );
1203
+ // Under the whole row, not under the field between the buttons: the message is about
1204
+ // the date, and the steppers are part of how it is entered.
1205
+ return (
1206
+ <FieldBox below={messages.below} reserve={reserve}>
1207
+ {withEndHint(row, endHint)}
1208
+ </FieldBox>
1209
+ );
704
1210
  }
705
1211
 
706
1212
  export interface DateRangePickerPreset {
@@ -939,7 +1445,7 @@ function RangePanel({
939
1445
  frame: PanelFrame;
940
1446
  sheet: boolean;
941
1447
  commitRange: (from: string, to: string, presetId: string | undefined) => void;
942
- calendarProps: Pick<MiniCalendarProps, "locale" | "min" | "max" | "labels">;
1448
+ calendarProps: Pick<PanelCalendarProps, "locale" | "min" | "max" | "labels" | "monthJump">;
943
1449
  labels: DatePickerLabels;
944
1450
  renderDraftSummary?: DateRangePickerProps["renderDraftSummary"];
945
1451
  }) {
@@ -962,7 +1468,7 @@ function RangePanel({
962
1468
  : null;
963
1469
 
964
1470
  const picker = (
965
- <MiniCalendar
1471
+ <PanelCalendar
966
1472
  {...calendarProps}
967
1473
  focusOnOpen
968
1474
  from={shown.from}
@@ -1052,34 +1558,45 @@ function RangePanel({
1052
1558
  /** Two-date range picker: click a start then an end; closes once both are set.
1053
1559
  * With `presets`, a column of named ranges is shown beside the calendar; with
1054
1560
  * `commit="apply"` the panel drafts and commits on Apply (see {@link DateRangeCommit}). */
1055
- export function DateRangePicker({
1056
- from,
1057
- to,
1058
- onChange,
1059
- locale: localeProp,
1060
- placeholder,
1061
- min,
1062
- max,
1063
- formatOptions,
1064
- formatValue,
1065
- separator = " – ",
1066
- presets,
1067
- preset,
1068
- commit = "immediate",
1069
- renderTrigger,
1070
- sheetBackCloses,
1071
- renderDraftSummary,
1072
- calendarLabels,
1073
- label,
1074
- clearable,
1075
- clearLabel,
1076
- disabled,
1077
- invalid,
1078
- ...rest
1079
- }: DateRangePickerProps) {
1561
+ export function DateRangePicker(props: DateRangePickerProps) {
1562
+ const {
1563
+ from,
1564
+ to,
1565
+ onChange,
1566
+ locale: localeProp,
1567
+ placeholder,
1568
+ min,
1569
+ max,
1570
+ formatOptions,
1571
+ formatValue,
1572
+ separator = " – ",
1573
+ presets,
1574
+ preset,
1575
+ commit = "immediate",
1576
+ renderTrigger,
1577
+ sheetBackCloses,
1578
+ renderDraftSummary,
1579
+ calendarLabels,
1580
+ monthJump,
1581
+ label,
1582
+ clearable,
1583
+ clearLabel,
1584
+ disabled,
1585
+ invalid,
1586
+ hint,
1587
+ error,
1588
+ ...rest
1589
+ } = props;
1080
1590
  const locale = useKitLocale(localeProp);
1081
1591
  const text = useDatePickerLabels(clearLabel, undefined, undefined);
1082
- const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));
1592
+ const render = useTriggerFormat("dateRangePicker", locale, formatOptions, formatValue);
1593
+ const messages = useFieldMessages({
1594
+ hint,
1595
+ error,
1596
+ invalid,
1597
+ describedBy: rest["aria-describedby"],
1598
+ ariaInvalid: rest["aria-invalid"],
1599
+ });
1083
1600
  // The preset this picker committed itself, for a caller that does not control
1084
1601
  // `preset`. Cleared by a hand-picked range or a clear.
1085
1602
  const [ownPreset, setOwnPreset] = useState<string | undefined>(undefined);
@@ -1101,15 +1618,17 @@ export function DateRangePicker({
1101
1618
  if (presetId === undefined) onChange(f, t);
1102
1619
  else onChange(f, t, presetId);
1103
1620
  };
1104
- return (
1621
+ const field = (
1105
1622
  <DateField
1106
1623
  {...rest}
1624
+ aria-describedby={messages.describedBy}
1107
1625
  label={label}
1626
+ labelHint={messages.labelHint}
1108
1627
  clearable={clearable}
1109
1628
  clearLabel={text.clear}
1110
1629
  panelLabel={text.rangePanel}
1111
1630
  disabled={disabled}
1112
- invalid={invalid}
1631
+ invalid={messages.isInvalid}
1113
1632
  // Widen so the preset column sits beside the calendar (default otherwise).
1114
1633
  width={hasPresets ? 440 : undefined}
1115
1634
  hasValue={Boolean(from || to)}
@@ -1143,11 +1662,16 @@ export function DateRangePicker({
1143
1662
  frame={frame}
1144
1663
  sheet={sheet}
1145
1664
  commitRange={commitRange}
1146
- calendarProps={{ locale, min, max, labels: calendarLabels }}
1665
+ calendarProps={{ locale, min, max, labels: calendarLabels, monthJump }}
1147
1666
  labels={text}
1148
1667
  renderDraftSummary={renderDraftSummary}
1149
1668
  />
1150
1669
  )}
1151
1670
  </DateField>
1152
1671
  );
1672
+ return (
1673
+ <FieldBox below={messages.below} reserve={"error" in props || "hint" in props}>
1674
+ {withEndHint(field, label === undefined ? messages.labelHint : undefined)}
1675
+ </FieldBox>
1676
+ );
1153
1677
  }