@eifi1/ui-kit 0.21.0 → 0.22.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 (278) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/iban-input.d.ts +66 -0
  72. package/dist/components/iban-input.js +125 -0
  73. package/dist/components/iban-input.js.map +1 -0
  74. package/dist/components/icon-picker.d.ts +12 -2
  75. package/dist/components/icon-picker.js +9 -2
  76. package/dist/components/icon-picker.js.map +1 -1
  77. package/dist/components/inline-edit-field.d.ts +167 -0
  78. package/dist/components/inline-edit-field.js +293 -0
  79. package/dist/components/inline-edit-field.js.map +1 -0
  80. package/dist/components/language-select.d.ts +127 -0
  81. package/dist/components/language-select.js +18 -0
  82. package/dist/components/language-select.js.map +1 -0
  83. package/dist/components/line-items.d.ts +25 -3
  84. package/dist/components/line-items.js +55 -46
  85. package/dist/components/line-items.js.map +1 -1
  86. package/dist/components/list.d.ts +25 -0
  87. package/dist/components/list.js +16 -5
  88. package/dist/components/list.js.map +1 -1
  89. package/dist/components/money-field.d.ts +20 -8
  90. package/dist/components/money-field.js.map +1 -1
  91. package/dist/components/month-picker.d.ts +52 -4
  92. package/dist/components/month-picker.js +290 -86
  93. package/dist/components/month-picker.js.map +1 -1
  94. package/dist/components/multi-select.d.ts +8 -1
  95. package/dist/components/multi-select.js +9 -4
  96. package/dist/components/multi-select.js.map +1 -1
  97. package/dist/components/number-field.d.ts +25 -13
  98. package/dist/components/number-field.js +6 -20
  99. package/dist/components/number-field.js.map +1 -1
  100. package/dist/components/number-input.d.ts +70 -13
  101. package/dist/components/number-input.js +90 -38
  102. package/dist/components/number-input.js.map +1 -1
  103. package/dist/components/numpad-sheet.d.ts +10 -8
  104. package/dist/components/one-time-code-input.d.ts +100 -0
  105. package/dist/components/one-time-code-input.js +137 -0
  106. package/dist/components/one-time-code-input.js.map +1 -0
  107. package/dist/components/phone-input.d.ts +66 -0
  108. package/dist/components/phone-input.js +171 -0
  109. package/dist/components/phone-input.js.map +1 -0
  110. package/dist/components/series-chart.d.ts +10 -8
  111. package/dist/components/settings-fields.d.ts +10 -8
  112. package/dist/components/share-card.d.ts +8 -6
  113. package/dist/components/sign-chip.d.ts +66 -0
  114. package/dist/components/sign-chip.js +39 -0
  115. package/dist/components/sign-chip.js.map +1 -0
  116. package/dist/components/swatch-picker.d.ts +27 -2
  117. package/dist/components/swatch-picker.js +73 -31
  118. package/dist/components/swatch-picker.js.map +1 -1
  119. package/dist/components/switch.d.ts +19 -0
  120. package/dist/components/switch.js +24 -6
  121. package/dist/components/switch.js.map +1 -1
  122. package/dist/components/text-link.d.ts +10 -8
  123. package/dist/components/tile-radio.d.ts +68 -13
  124. package/dist/components/tile-radio.js +3 -3
  125. package/dist/components/tile-radio.js.map +1 -1
  126. package/dist/components/time-input.d.ts +15 -9
  127. package/dist/components/time-input.js.map +1 -1
  128. package/dist/components/toggle-group.d.ts +47 -1
  129. package/dist/components/toggle-group.js +110 -69
  130. package/dist/components/toggle-group.js.map +1 -1
  131. package/dist/components/ui.d.ts +10 -8
  132. package/dist/components/ui.js +280 -78
  133. package/dist/components/ui.js.map +1 -1
  134. package/dist/components/use-table-state.d.ts +1 -2
  135. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  136. package/dist/data-table.d.ts +1 -2
  137. package/dist/data-table.js +2 -1
  138. package/dist/data-table.js.map +1 -1
  139. package/dist/feedback/feedback-attachment.d.ts +1 -1
  140. package/dist/feedback/feedback-attachment.js +210 -3
  141. package/dist/feedback/feedback-attachment.js.map +1 -1
  142. package/dist/feedback/feedback-dialog.d.ts +1 -1
  143. package/dist/feedback/feedback-dialog.js.map +1 -1
  144. package/dist/feedback/feedback-inbox.d.ts +1 -1
  145. package/dist/feedback/feedback-thread.d.ts +37 -3
  146. package/dist/feedback/feedback-thread.js +38 -9
  147. package/dist/feedback/feedback-thread.js.map +1 -1
  148. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  149. package/dist/feedback.d.ts +2 -2
  150. package/dist/hooks/use-file-drop.d.ts +10 -8
  151. package/dist/i18n/defaults.d.ts +10 -8
  152. package/dist/i18n/defaults.js +17 -2
  153. package/dist/i18n/defaults.js.map +1 -1
  154. package/dist/i18n/german.d.ts +10 -8
  155. package/dist/i18n/german.js +50 -5
  156. package/dist/i18n/german.js.map +1 -1
  157. package/dist/i18n/kit-labels.d.ts +10 -8
  158. package/dist/i18n/kit-labels.js +8 -2
  159. package/dist/i18n/kit-labels.js.map +1 -1
  160. package/dist/i18n/languages.d.ts +10 -8
  161. package/dist/i18n/locales/de-CH.d.ts +10 -8
  162. package/dist/i18n/locales/en.d.ts +10 -8
  163. package/dist/i18n/locales/en.js +9 -0
  164. package/dist/i18n/locales/en.js.map +1 -1
  165. package/dist/i18n/locales/es.d.ts +10 -8
  166. package/dist/i18n/locales/es.js +48 -5
  167. package/dist/i18n/locales/es.js.map +1 -1
  168. package/dist/i18n/locales/fr.d.ts +10 -8
  169. package/dist/i18n/locales/fr.js +48 -5
  170. package/dist/i18n/locales/fr.js.map +1 -1
  171. package/dist/i18n/locales/hu.d.ts +10 -8
  172. package/dist/i18n/locales/hu.js +49 -5
  173. package/dist/i18n/locales/hu.js.map +1 -1
  174. package/dist/i18n/locales/it.d.ts +10 -8
  175. package/dist/i18n/locales/it.js +49 -5
  176. package/dist/i18n/locales/it.js.map +1 -1
  177. package/dist/i18n/locales/zh.d.ts +10 -8
  178. package/dist/i18n/locales/zh.js +48 -5
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.d.ts +10 -8
  181. package/dist/i18n/review.js +8 -1
  182. package/dist/i18n/review.js.map +1 -1
  183. package/dist/index.d.ts +17 -10
  184. package/dist/index.js +57 -2
  185. package/dist/index.js.map +1 -1
  186. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  187. package/dist/lib/countries.d.ts +43 -0
  188. package/dist/lib/countries.js +39 -0
  189. package/dist/lib/countries.js.map +1 -0
  190. package/dist/lib/decimal-marks.d.ts +46 -0
  191. package/dist/lib/decimal-marks.js +40 -0
  192. package/dist/lib/decimal-marks.js.map +1 -0
  193. package/dist/lib/iban.d.ts +98 -0
  194. package/dist/lib/iban.js +154 -0
  195. package/dist/lib/iban.js.map +1 -0
  196. package/dist/lib/isin.d.ts +30 -0
  197. package/dist/lib/isin.js +36 -0
  198. package/dist/lib/isin.js.map +1 -0
  199. package/dist/lib/phone.d.ts +69 -0
  200. package/dist/lib/phone.js +121 -0
  201. package/dist/lib/phone.js.map +1 -0
  202. package/dist/rhf/fields.d.ts +148 -11
  203. package/dist/rhf/fields.js +332 -3
  204. package/dist/rhf/fields.js.map +1 -1
  205. package/dist/rhf/form.d.ts +10 -8
  206. package/dist/rhf.d.ts +13 -9
  207. package/dist/rhf.js.map +1 -1
  208. package/dist/shell/app-shell.d.ts +10 -8
  209. package/dist/shell/top-bar-brand.d.ts +10 -8
  210. package/dist/shell.d.ts +10 -8
  211. package/dist/tour/tour.js +20 -24
  212. package/dist/tour/tour.js.map +1 -1
  213. package/dist/wizard/stepper-nav.d.ts +10 -8
  214. package/dist/wizard.d.ts +10 -8
  215. package/package.json +1 -1
  216. package/src/components/account-chips.tsx +17 -3
  217. package/src/components/account-settings.tsx +92 -26
  218. package/src/components/amount-input.tsx +49 -56
  219. package/src/components/autocomplete.tsx +133 -110
  220. package/src/components/checkbox-group.tsx +229 -0
  221. package/src/components/checkbox.tsx +72 -8
  222. package/src/components/chip.tsx +80 -10
  223. package/src/components/combobox.tsx +292 -253
  224. package/src/components/country-select.tsx +443 -0
  225. package/src/components/currency-select.tsx +109 -33
  226. package/src/components/danger-confirm.tsx +345 -77
  227. package/src/components/data-table-labels.ts +16 -1
  228. package/src/components/data-table.tsx +175 -6
  229. package/src/components/date-picker.tsx +173 -62
  230. package/src/components/entity-combobox.tsx +107 -87
  231. package/src/components/field-anatomy.tsx +190 -0
  232. package/src/components/field-parts.tsx +184 -0
  233. package/src/components/file-button.tsx +36 -3
  234. package/src/components/file-dropzone.tsx +106 -38
  235. package/src/components/form-actions.tsx +115 -2
  236. package/src/components/iban-input.tsx +231 -0
  237. package/src/components/icon-picker.tsx +26 -4
  238. package/src/components/inline-edit-field.tsx +533 -0
  239. package/src/components/language-select.tsx +78 -0
  240. package/src/components/line-items.tsx +35 -2
  241. package/src/components/list.tsx +55 -6
  242. package/src/components/money-field.tsx +10 -0
  243. package/src/components/month-picker.tsx +399 -63
  244. package/src/components/multi-select.tsx +80 -58
  245. package/src/components/number-field.tsx +28 -46
  246. package/src/components/number-input.tsx +210 -43
  247. package/src/components/one-time-code-input.tsx +288 -0
  248. package/src/components/phone-input.tsx +299 -0
  249. package/src/components/sign-chip.tsx +103 -0
  250. package/src/components/swatch-picker.tsx +118 -30
  251. package/src/components/switch.tsx +58 -7
  252. package/src/components/tile-radio.tsx +71 -16
  253. package/src/components/time-input.tsx +8 -1
  254. package/src/components/toggle-group.tsx +206 -73
  255. package/src/components/ui.tsx +472 -105
  256. package/src/data-table.ts +2 -1
  257. package/src/feedback/feedback-attachment.tsx +313 -9
  258. package/src/feedback/feedback-dialog.tsx +2 -0
  259. package/src/feedback/feedback-thread.tsx +66 -5
  260. package/src/i18n/defaults.ts +16 -1
  261. package/src/i18n/german.ts +47 -0
  262. package/src/i18n/kit-labels.tsx +78 -2
  263. package/src/i18n/locales/en.ts +10 -0
  264. package/src/i18n/locales/es.ts +46 -0
  265. package/src/i18n/locales/fr.ts +45 -0
  266. package/src/i18n/locales/hu.ts +46 -0
  267. package/src/i18n/locales/it.ts +47 -0
  268. package/src/i18n/locales/zh.ts +44 -0
  269. package/src/i18n/review.ts +7 -0
  270. package/src/index.ts +53 -3
  271. package/src/lib/countries.ts +91 -0
  272. package/src/lib/decimal-marks.ts +99 -0
  273. package/src/lib/iban.ts +171 -0
  274. package/src/lib/isin.ts +62 -0
  275. package/src/lib/phone.ts +211 -0
  276. package/src/rhf/fields.tsx +606 -9
  277. package/src/rhf.ts +2 -1
  278. package/src/tour/tour.tsx +66 -28
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/month-picker.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { Calendar, ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep, type Direction } from \"../lib/direction\";\nimport { monthKey, pad } from \"../lib/dates\";\nimport { Button, FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from \"./ui\";\nimport { Popover } from \"./popover\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * Every string the month picker can speak. The month and year names are NOT here —\n * they come from `Intl` in the `locale` prop, so they are already in the user's\n * language. What is left is the two icon-only year arrows, whose `aria-label` is the\n * only name they have, the panel's name, and a month cell's name.\n */\nexport interface MonthPickerLabels {\n previousYear: string;\n nextYear: string;\n /** The popover's accessible name (`role=\"dialog\"`). */\n panel: string;\n /**\n * Accessible name of one month cell, given that month and its year formatted in\n * `locale` (\"August 2026\"). The cell's visible text is the short month alone, and\n * \"Aug\" read out on its own does not say which year the grid is on.\n */\n month: (monthYear: string) => string;\n /** `variant=\"stepper\"`: the icon-only arrows that step the value by one month. */\n previousMonth: string;\n nextMonth: string;\n /** `variant=\"stepper\"`: the button that jumps to the current month. Visible text. */\n today: string;\n}\n\n/** The English starting point every override is merged onto. Exported (unlike the\n * calendar's) because it is asked for by name: a host building its own translation\n * table can spread it rather than re-typing four keys to change one. */\nexport const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {\n previousYear: \"Previous year\",\n nextYear: \"Next year\",\n panel: \"Choose a month\",\n month: (monthYear) => monthYear,\n previousMonth: \"Previous month\",\n nextMonth: \"Next month\",\n today: \"Today\",\n};\n\n/** Merge caller overrides onto the English defaults. */\nfunction useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPickerLabels {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n return useKitLabels(\"monthPicker\", DEFAULT_MONTH_PICKER_LABELS, partial);\n}\n\n/**\n * `onChange` is omitted from the `<div>` attributes: the DOM event of that name carries\n * a `FormEvent`, and this one answers with a month key. Everything else reaches the\n * field's root, so a test id or a `data-tour` anchor can find it.\n */\nexport interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * The selected month as a `\"YYYY-MM\"` key, or `\"\"` for none.\n *\n * A string rather than `{ year, month }` or a `Date`, for the reason every date in\n * this kit is one: it is what `monthKey()` in `lib/dates` produces, it sorts\n * chronologically so the bounds are plain `<` / `>`, and it crosses a URL or an API\n * without a serialiser. A `{ year, month }` caller converts with\n * `` `${year}-${pad(month)}` `` (month 1–12) and back with `key.split(\"-\").map(Number)`.\n */\n value: string;\n /** Called with the picked month as `\"YYYY-MM\"`. */\n onChange: (month: string) => void;\n /** Drives the month and year names (`Intl.DateTimeFormat`). Defaults to the\n * `<UiKitProvider>` locale, then the runtime's. */\n locale?: string;\n /**\n * Inclusive bounds, as `\"YYYY-MM\"`. A full ISO date (`\"YYYY-MM-DD\"`) is accepted and\n * read as its month, so a host holding \"the first day we have data for\" can pass it\n * straight in. Months outside are shown but refuse selection; a year arrow is\n * disabled once the whole year beyond it is out of bounds.\n */\n min?: string;\n max?: string;\n /** Embedded top-boundary label, matching the other field-shaped controls. */\n label?: ReactNode;\n /** Trigger text when `value` is `\"\"`. */\n placeholder?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** Intl options for the trigger's text (default: long month + numeric year). */\n formatOptions?: Intl.DateTimeFormatOptions;\n /**\n * The month to ring as \"now\", as `\"YYYY-MM\"`. Defaults to the local calendar's\n * current month; pass one to pin it (a test, a host with its own notion of today).\n */\n currentMonth?: string;\n /** User-facing strings; see {@link MonthPickerLabels}. */\n labels?: Partial<MonthPickerLabels>;\n /** Extra classes for the field's root. */\n className?: string;\n /** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger\n * sitting between two icon buttons in a toolbar. */\n triggerClassName?: string;\n /**\n * `\"field\"` (default): the field-shaped trigger, for a form.\n *\n * `\"stepper\"`: the header of a month PAGE — the month as a heading that opens the\n * grid, then Today and ‹ › stepping the value by one month. What a calendar page\n * assembled by hand around the field (overriding its trigger to look like a\n * heading). `label`, `placeholder`, `invalid` and `triggerClassName` are the field's\n * and are not drawn here; with no `value` the arrows step from the current month.\n */\n variant?: \"field\" | \"stepper\";\n /** `variant=\"stepper\"`: the heading level the month is set in (`<h2>` by default), or\n * `false` for no heading element — the page already has one for the month. */\n headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | false;\n /** `variant=\"stepper\"`: draw the Today button. Default `true`. */\n showToday?: boolean;\n}\n\n// ── Month keys ────────────────────────────────────────────────────────────\n\ninterface YearMonth {\n year: number;\n /** 1–12, as in the key. */\n month: number;\n}\n\nconst KEY = /^(\\d{4})-(\\d{2})/;\n\nfunction parseMonth(key: string | undefined): YearMonth | null {\n const m = key ? KEY.exec(key) : null;\n if (!m) return null;\n const month = Number(m[2]);\n return month >= 1 && month <= 12 ? { year: Number(m[1]), month } : null;\n}\n\nfunction toKey({ year, month }: YearMonth): string {\n return `${year}-${pad(month)}`;\n}\n\n/** Move a month key by whole months, carrying into the year. */\nfunction shiftMonth(ym: YearMonth, delta: number): YearMonth {\n const index = ym.year * 12 + (ym.month - 1) + delta;\n return { year: Math.floor(index / 12), month: (index % 12) + 1 };\n}\n\n/** A bound, normalised to its month key — so `\"2024-03-15\"` compares as `\"2024-03\"`. */\nfunction boundKey(bound: string | undefined): string | null {\n const ym = parseMonth(bound);\n return ym ? toKey(ym) : null;\n}\n\n/** Local-calendar Date for the 1st of a month, for `Intl` to format. */\nfunction firstOf({ year, month }: YearMonth): Date {\n return new Date(year, month - 1, 1);\n}\n\nconst DEFAULT_TRIGGER_FORMAT: Intl.DateTimeFormatOptions = { month: \"long\", year: \"numeric\" };\nconst MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: \"long\", year: \"numeric\" };\n\n// ── Trigger ───────────────────────────────────────────────────────────────\n\n/** Hand focus back to the trigger when the panel closes with focus still inside it. */\nfunction useFocusBackOnClose(open: boolean, triggerRef: RefObject<HTMLButtonElement | null>) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when focus went down with the panel. An outside click has already put it\n // somewhere real, and dragging it back here would be worse than leaving it.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n }, [open, triggerRef]);\n}\n\n/**\n * The stepper's trigger: the month as a heading-sized button that opens the grid.\n *\n * Its own text is its name (\"September 2026\"), so it needs no label of its own, and\n * it is `aria-live` for the reason `MiniCalendar`'s caption is: pressing ‹ or › leaves\n * focus on the arrow, and the heading is the only thing that says where the arrow\n * went. A plain button with `aria-haspopup`, not the field's `combobox` — it does not\n * look or behave like a field, and a heading reading \"combobox\" is a surprise.\n */\nfunction MonthHeadingTrigger({\n open,\n toggle,\n triggerRef,\n text,\n panelId,\n disabled,\n aria,\n}: {\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n text: string;\n panelId: string;\n disabled?: boolean;\n aria: TriggerAria;\n}) {\n useFocusBackOnClose(open, triggerRef);\n return (\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n aria-haspopup=\"dialog\"\n aria-controls={panelId}\n aria-expanded={open}\n id={aria.id}\n aria-label={aria[\"aria-label\"]}\n aria-labelledby={aria[\"aria-labelledby\"]}\n aria-describedby={aria[\"aria-describedby\"]}\n className={cn(\n \"-ms-1.5 inline-flex h-9 min-w-0 items-center gap-1 rounded-md px-1.5 text-lg font-semibold text-[var(--text-primary)] transition-colors\",\n \"hover:bg-[var(--bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <span aria-live=\"polite\" className=\"truncate\">\n {text || \"\\u00a0\"}\n </span>\n <ChevronDown aria-hidden className=\"size-4 shrink-0 text-[var(--text-muted)]\" />\n </button>\n );\n}\n\n/**\n * The field-shaped button that opens the grid. The same shape as `DatePicker`'s\n * trigger, for the same reasons written down there: `role=\"combobox\"` so it may carry\n * `aria-expanded` / `aria-invalid`, a name made of the label AND the value, and focus\n * handed back here when the panel closes with focus still inside it.\n */\nfunction MonthFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n panelId,\n padded,\n disabled,\n invalid,\n className,\n aria,\n}: {\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n triggerText: string;\n hasValue: boolean;\n labelledBy: string;\n /** The caller's naming attributes — see `trigger-aria.ts`. */\n aria: TriggerAria;\n valueId: string;\n panelId: string;\n padded: boolean;\n disabled?: boolean;\n invalid?: boolean;\n className?: string;\n}) {\n useFocusBackOnClose(open, triggerRef);\n\n return (\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-controls={panelId}\n aria-expanded={open}\n id={aria.id}\n aria-invalid={invalid || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\" || undefined}\n aria-labelledby={aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy}\n aria-label={aria[\"aria-label\"]}\n aria-describedby={aria[\"aria-describedby\"]}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n invalid && FIELD_INVALID,\n className,\n )}\n >\n {/* The U+00A0 fallback keeps the line box when there is no text — see\n DatePicker. The value arrives capitalised (see `upperFirst`); this span does\n NOT use `capitalize`, which upper-cases every WORD — \"Choose A Month\",\n \"Septiembre De 2026\". */}\n <span\n id={valueId}\n className={cn(\"truncate\", !hasValue && \"text-[var(--text-placeholder)]\")}\n >\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n// ── Grid ──────────────────────────────────────────────────────────────────\n\nconst COLUMNS = 3;\n\n/**\n * The panel: a year caption between ‹ › arrows, over a 4×3 grid of months.\n *\n * Mounted only while the popover is open, so every opening starts from the current\n * value's year — a value moved from outside (a prev/next-month button beside the\n * field) is where the grid lands, with no effect needed to snap it back.\n *\n * A grid in the APG sense, like `MiniCalendar`: one tab stop (the roving tabindex),\n * the arrows walk the months — left/right by one, up/down by a row of three — and\n * walking off either end of the year MOVES the year rather than stopping. PageUp /\n * PageDown step a whole year. Out-of-bounds months are `aria-disabled`, not\n * `disabled`, so the roving tab stop never falls into a hole.\n */\nfunction MonthGrid({\n value,\n onPick,\n locale,\n minKey,\n maxKey,\n currentKey,\n labels,\n}: {\n value: YearMonth | null;\n onPick: (key: string) => void;\n locale: string | undefined;\n minKey: string | null;\n maxKey: string | null;\n currentKey: string;\n labels: MonthPickerLabels;\n}) {\n const outOfBounds = (key: string) =>\n Boolean((minKey && key < minKey) || (maxKey && key > maxKey));\n\n // The month holding the tab order, and with it the year on screen — one piece of\n // state, as in MiniCalendar, so the two cannot disagree. Opens on the value, or on\n // the current month, pulled inside the bounds.\n const [active, setActive] = useState<YearMonth>(() => {\n const start = value ? toKey(value) : currentKey;\n if (minKey && start < minKey) return parseMonth(minKey)!;\n if (maxKey && start > maxKey) return parseMonth(maxKey)!;\n return parseMonth(start)!;\n });\n const year = active.year;\n\n const gridRef = useRef<HTMLDivElement>(null);\n const captionId = `${useId()}-year`;\n\n // Short names for the cells, long \"Month YYYY\" for their accessible names.\n const shortNames = useMemo(() => {\n const fmt = new Intl.DateTimeFormat(locale, { month: \"short\" });\n return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2000, i, 1)));\n }, [locale]);\n const longName = useMemo(() => new Intl.DateTimeFormat(locale, MONTH_NAME_FORMAT), [locale]);\n const yearCaption = new Intl.DateTimeFormat(locale, { year: \"numeric\" }).format(\n firstOf({ year, month: 1 }),\n );\n\n // A month whose cell must take focus after the next commit. Arrowing across a year\n // boundary re-labels the same twelve buttons, so the one to focus is a DIFFERENT\n // button from the one the key landed on.\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const key = pendingFocus.current;\n if (!key) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-month=\"${key}\"]`)?.focus();\n });\n\n // Opened because the user asked to pick a month: land them on it. Runs before the\n // popover's focus trap (a child's effect fires first), which then keeps this focus.\n useEffect(() => {\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus();\n }, []);\n\n const moveTo = (next: YearMonth, focus: boolean) => {\n if (focus) pendingFocus.current = toKey(next);\n setActive(next);\n };\n\n const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n // Left and right are VISUAL directions: in a right-to-left page the grid runs the\n // other way, so \"right\" is the previous month there. The panel is portalled, but\n // it carries the field's `dir` (see MonthPicker), so the nearest `dir` is right.\n const step = horizontalStep(e.key, e.currentTarget);\n const col = (active.month - 1) % COLUMNS;\n let delta: number;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n delta = step;\n break;\n case \"ArrowUp\":\n delta = -COLUMNS;\n break;\n case \"ArrowDown\":\n delta = COLUMNS;\n break;\n case \"Home\":\n delta = -col;\n break;\n case \"End\":\n delta = COLUMNS - 1 - col;\n break;\n case \"PageUp\":\n delta = -12;\n break;\n case \"PageDown\":\n delta = 12;\n break;\n default:\n return;\n }\n // Before any no-op check: these keys also scroll the panel, and a month that\n // cannot move still must not move the page.\n e.preventDefault();\n if (delta !== 0) moveTo(shiftMonth(active, delta), true);\n };\n\n // An arrow is dead once EVERY month of the year it leads to is out of bounds.\n const prevBlocked = Boolean(minKey && toKey({ year: year - 1, month: 12 }) < minKey);\n const nextBlocked = Boolean(maxKey && toKey({ year: year + 1, month: 1 }) > maxKey);\n const selectedKey = value ? toKey(value) : null;\n\n const arrow =\n \"flex size-7 items-center justify-center rounded text-[var(--text-muted)] hover:bg-[var(--bg-hover)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent\";\n\n return (\n <div className=\"select-none\">\n <div className=\"mb-1 flex items-center justify-between\">\n <button\n type=\"button\"\n aria-label={labels.previousYear}\n disabled={prevBlocked}\n // Focus stays on the arrow being pressed — only the grid moves.\n onClick={() => moveTo(shiftMonth(active, -12), false)}\n className={arrow}\n >\n {/* Mirrored in RTL, where \"previous\" points the other way. */}\n <ChevronLeft className=\"size-4 rtl:-scale-x-100\" />\n </button>\n {/* Names the grid, and `aria-live` so stepping a year says which one you are\n on — the user pressing the arrow is not inside the grid to hear it change. */}\n <div\n id={captionId}\n aria-live=\"polite\"\n className=\"text-sm font-semibold tabular-nums text-[var(--text-primary)]\"\n >\n {yearCaption}\n </div>\n <button\n type=\"button\"\n aria-label={labels.nextYear}\n disabled={nextBlocked}\n onClick={() => moveTo(shiftMonth(active, 12), false)}\n className={arrow}\n >\n <ChevronRight className=\"size-4 rtl:-scale-x-100\" />\n </button>\n </div>\n <div ref={gridRef} role=\"grid\" aria-labelledby={captionId} className=\"grid gap-1\">\n {Array.from({ length: 12 / COLUMNS }, (_, row) => (\n <div key={row} role=\"row\" className=\"grid grid-cols-3 gap-1\">\n {Array.from({ length: COLUMNS }, (_, c) => {\n const ym = { year, month: row * COLUMNS + c + 1 };\n const key = toKey(ym);\n const disabled = outOfBounds(key);\n const selected = key === selectedKey;\n const current = key === currentKey;\n return (\n <button\n key={c}\n type=\"button\"\n role=\"gridcell\"\n data-month={key}\n tabIndex={ym.month === active.month ? 0 : -1}\n aria-label={labels.month(longName.format(firstOf(ym)))}\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n aria-current={current ? \"date\" : undefined}\n onClick={() => {\n // The guard `disabled` would have been; see the component note.\n if (disabled) return;\n setActive(ym);\n onPick(key);\n }}\n onKeyDown={onCellKeyDown}\n className={cn(\n \"rounded px-2 py-1.5 text-sm capitalize transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset\",\n // A --brand ring on a --brand fill exists only in the DOM.\n selected && !disabled\n ? \"focus-visible:ring-[var(--brand-contrast)]\"\n : \"focus-visible:ring-[var(--brand)]\",\n disabled\n ? \"cursor-not-allowed text-[var(--text-placeholder)]\"\n : selected\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\"\n : \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n current && !selected && !disabled && \"ring-1 ring-inset ring-[var(--border-strong)]\",\n )}\n >\n {shortNames[ym.month - 1]}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n// ── Field ─────────────────────────────────────────────────────────────────\n\n/**\n * Month picker: a field showing the selected month (\"August 2026\"), opening a 12-month\n * grid with year arrows. For a value that IS a month — a budget period, a calendar\n * view, a statement — where a day picker would ask for a day nobody means.\n *\n * One component for the two app-local pickers it replaces: keksdose's budget-header\n * popover (grid + year steppers + a floor) and kastlan's calendar header (a month list\n * and a year list in two popovers). The grid does both jobs — the year arrows and\n * PageUp/PageDown cover what the year list did, without a fixed ±5-year window.\n */\n/** Upper-case the first letter only, in `locale`. Several locales (fr, es, it, pt, …)\n * write month names in lower case, which at the start of a field reads as a typo —\n * but only the FIRST letter is the field's business: \"septiembre de 2026\" becomes\n * \"Septiembre de 2026\", not \"Septiembre De 2026\". */\nfunction upperFirst(text: string, locale: string | undefined): string {\n return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);\n}\n\nexport function MonthPicker({\n value,\n onChange,\n locale: localeProp,\n min,\n max,\n label,\n placeholder,\n disabled,\n invalid,\n formatOptions,\n currentMonth,\n labels: labelsProp,\n className,\n triggerClassName,\n variant = \"field\",\n headingLevel = 2,\n showToday = true,\n ...rest\n}: MonthPickerProps) {\n const labels = useMonthPickerLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const selected = parseMonth(value);\n const minKey = boundKey(min);\n const maxKey = boundKey(max);\n const currentKey = boundKey(currentMonth) ?? monthKey(new Date());\n\n const triggerText = selected\n ? upperFirst(\n new Intl.DateTimeFormat(locale, formatOptions ?? DEFAULT_TRIGGER_FORMAT).format(\n firstOf(selected),\n ),\n locale,\n )\n : (placeholder ?? \"\");\n\n const id = useId();\n const [aria, wrapperRest] = splitTriggerAria(rest);\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can double as a name; a ReactNode may hold its own controls.\n const named = typeof label === \"string\";\n // The panel is portalled to <body>, out of the subtree whose `dir` it inherited — an\n // RTL form got an LTR grid. Read when opening (`toggle` is the popover's only way\n // open) and put back on the panel. Also what the arrow keys above read.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n\n const grid = (close: () => void) => (\n <MonthGrid\n value={selected}\n locale={locale}\n minKey={minKey}\n maxKey={maxKey}\n currentKey={currentKey}\n labels={labels}\n onPick={(key) => {\n onChange(key);\n close();\n }}\n />\n );\n\n if (variant === \"stepper\") {\n // The arrows step from the value, or from \"now\" when there is none; each is dead\n // once the month it leads to is out of bounds, and Today once \"now\" is.\n const base = selected ?? parseMonth(currentKey)!;\n const prevKey = toKey(shiftMonth(base, -1));\n const nextKey = toKey(shiftMonth(base, 1));\n const outside = (key: string) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));\n const Heading = headingLevel ? (`h${headingLevel}` as const) : null;\n const trigger = (\n <Popover\n width={256}\n panelId={panelId}\n labels={{ panel: labels.panel }}\n dir={dir}\n trigger={({ open, toggle, ref }) => (\n <MonthHeadingTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n text={triggerText}\n panelId={panelId}\n disabled={disabled}\n aria={aria}\n />\n )}\n >\n {grid}\n </Popover>\n );\n return (\n <div {...wrapperRest} ref={rootRef} className={cn(\"flex min-w-0 items-center gap-2\", className)}>\n {Heading ? <Heading className=\"m-0 min-w-0 text-lg font-semibold\">{trigger}</Heading> : trigger}\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {showToday && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled || outside(currentKey)}\n onClick={() => {\n if (currentKey !== value) onChange(currentKey);\n }}\n >\n {labels.today}\n </Button>\n )}\n <IconButton\n aria-label={labels.previousMonth}\n disabled={disabled || outside(prevKey)}\n onClick={() => onChange(prevKey)}\n >\n {/* Mirrored in RTL, where \"previous\" points the other way. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n aria-label={labels.nextMonth}\n disabled={disabled || outside(nextKey)}\n onClick={() => onChange(nextKey)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n </div>\n );\n }\n\n return (\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,\n and inside this `relative` root either way. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n <Popover\n width={256}\n panelId={panelId}\n labels={{ panel: labels.panel }}\n dir={dir}\n trigger={({ open, toggle, ref }) => (\n <MonthFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={selected != null}\n // As DatePicker: a caller's reference first; an `id` without a label of our\n // own references the trigger itself, so an external <label htmlFor> names it.\n labelledBy={[\n aria[\"aria-labelledby\"],\n named ? labelId : !aria[\"aria-labelledby\"] && aria.id ? aria.id : undefined,\n valueId,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria={aria}\n valueId={valueId}\n panelId={panelId}\n padded={label !== undefined}\n disabled={disabled}\n invalid={invalid}\n className={triggerClassName}\n />\n )}\n >\n {grid}\n </Popover>\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\"\n />\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
1
+ {"version":3,"sources":["../../src/components/month-picker.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { Calendar, ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep, type Direction } from \"../lib/direction\";\nimport { monthKey, pad } from \"../lib/dates\";\nimport { Button, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from \"./ui\";\nimport { Popover } from \"./popover\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { useKitDateFormatter, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { FieldBox, FieldLabelLine, hasContent, useFieldMessages } from \"./field-anatomy\";\n\n/**\n * Every string the month picker can speak. The month and year names are NOT here —\n * they come from `Intl` in the `locale` prop, so they are already in the user's\n * language. What is left is the two icon-only year arrows, whose `aria-label` is the\n * only name they have, the panel's name, and a month cell's name.\n */\nexport interface MonthPickerLabels {\n previousYear: string;\n nextYear: string;\n /** The popover's accessible name (`role=\"dialog\"`). */\n panel: string;\n /**\n * Accessible name of one month cell, given that month and its year formatted in\n * `locale` (\"August 2026\"). The cell's visible text is the short month alone, and\n * \"Aug\" read out on its own does not say which year the grid is on.\n */\n month: (monthYear: string) => string;\n /** `variant=\"stepper\"`: the icon-only arrows that step the value by one month. */\n previousMonth: string;\n nextMonth: string;\n /** `variant=\"stepper\"`: the button that jumps to the current month. Visible text. */\n today: string;\n /** `mode=\"year\"`: the popover's accessible name. */\n yearPanel: string;\n /** `mode=\"year\"`: the icon-only arrows that page the year grid back and forward by a\n * page of twelve years. */\n earlierYears: string;\n laterYears: string;\n /** `mode=\"year\"`, `variant=\"stepper\"`: the button that jumps to the current year.\n * Visible text. */\n thisYear: string;\n}\n\n/** The English starting point every override is merged onto. Exported (unlike the\n * calendar's) because it is asked for by name: a host building its own translation\n * table can spread it rather than re-typing four keys to change one. */\nexport const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {\n previousYear: \"Previous year\",\n nextYear: \"Next year\",\n panel: \"Choose a month\",\n month: (monthYear) => monthYear,\n previousMonth: \"Previous month\",\n nextMonth: \"Next month\",\n today: \"Today\",\n yearPanel: \"Choose a year\",\n earlierYears: \"Earlier years\",\n laterYears: \"Later years\",\n thisYear: \"This year\",\n};\n\n/** Merge caller overrides onto the English defaults. */\nfunction useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPickerLabels {\n // Prop > <UiKitProvider> > English, like every labelled component in the kit.\n return useKitLabels(\"monthPicker\", DEFAULT_MONTH_PICKER_LABELS, partial);\n}\n\n/**\n * `onChange` is omitted from the `<div>` attributes: the DOM event of that name carries\n * a `FormEvent`, and this one answers with a month key. Everything else reaches the\n * field's root, so a test id or a `data-tour` anchor can find it.\n */\nexport interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * The selected month as a `\"YYYY-MM\"` key, or `\"\"` for none.\n *\n * A string rather than `{ year, month }` or a `Date`, for the reason every date in\n * this kit is one: it is what `monthKey()` in `lib/dates` produces, it sorts\n * chronologically so the bounds are plain `<` / `>`, and it crosses a URL or an API\n * without a serialiser. A `{ year, month }` caller converts with\n * `` `${year}-${pad(month)}` `` (month 1–12) and back with `key.split(\"-\").map(Number)`.\n */\n value: string;\n /** Called with the picked month as `\"YYYY-MM\"`. */\n onChange: (month: string) => void;\n /** Drives the month and year names (`Intl.DateTimeFormat`). Defaults to the\n * `<UiKitProvider>` locale, then the runtime's. */\n locale?: string;\n /**\n * Inclusive bounds, as `\"YYYY-MM\"`. A full ISO date (`\"YYYY-MM-DD\"`) is accepted and\n * read as its month, so a host holding \"the first day we have data for\" can pass it\n * straight in. Months outside are shown but refuse selection; a year arrow is\n * disabled once the whole year beyond it is out of bounds.\n */\n min?: string;\n max?: string;\n /** Embedded top-boundary label, matching the other field-shaped controls. */\n label?: ReactNode;\n /** Trigger text when `value` is `\"\"`. */\n placeholder?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /**\n * What is wrong with the month, in the caller's words — {@link Input}'s `error`\n * (keksdose K4): under the field, attached to the trigger with `aria-describedby`\n * (merged, after any caption), and implies `invalid`. Passing the key keeps the\n * field's box while there is no message, so the trigger is never remounted.\n */\n error?: ReactNode;\n /** Standing advice, as on a {@link Select}: text is a caption under the field; a\n * {@link FieldHint} rides the label line. */\n hint?: ReactNode;\n /**\n * `\"md\"` (default): the form field. `\"sm\"`: the toolbar trigger — 36px tall (an\n * `IconButton`'s height) and as wide as the month it shows, for a picker that sits\n * between two icon buttons in a page header (keksdose K13: the budget header wrote\n * `triggerClassName=\"h-9 w-auto … py-0\"` by hand, live #260 — 34px between two 36px\n * buttons, *\"the kind of 2px that reads as sloppy\"*). Unlabelled pickers only, as on\n * {@link Select}: a floating label needs the tall box to float in. The width follows\n * the month's name; add a `min-w-*` in `triggerClassName` if the buttons beside it\n * must not move as it changes.\n */\n size?: \"sm\" | \"md\";\n /**\n * `\"month\"` (default): the value is a month, `\"YYYY-MM\"`.\n *\n * `\"year\"`: the value is a YEAR, `\"YYYY\"`, and the panel is a grid of years — kastlan's\n * budget fiscal year (a `Select` of 2020 to three years ahead) and keksdose's tax tab\n * (\"this year and four before\", tax-tab.tsx:33). `min` / `max` are read as their year\n * (`\"2022\"`, or any longer key). A bounded span of twelve years or fewer is shown\n * whole, with no paging; otherwise the grid pages twelve years at a time (PageUp /\n * PageDown, or the arrows), starting from `min` when there is one. The stepper\n * variant steps a year, and its Today button reads \"This year\".\n *\n * A string rather than a number, for the reason the month key is one: it is the\n * same type across both modes, it crosses a URL as is, and it sorts. A numeric\n * caller converts with `String(year)` and `Number(key)`.\n */\n mode?: \"month\" | \"year\";\n /** Intl options for the trigger's text (default: long month + numeric year, or the\n * numeric year in `mode=\"year\"`). Win over the provider's `formatDate`. */\n formatOptions?: Intl.DateTimeFormatOptions;\n /**\n * The month to ring as \"now\", as `\"YYYY-MM\"`. Defaults to the local calendar's\n * current month; pass one to pin it (a test, a host with its own notion of today).\n */\n currentMonth?: string;\n /** User-facing strings; see {@link MonthPickerLabels}. */\n labels?: Partial<MonthPickerLabels>;\n /** Extra classes for the field's root. */\n className?: string;\n /** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger\n * sitting between two icon buttons in a toolbar. */\n triggerClassName?: string;\n /**\n * `\"field\"` (default): the field-shaped trigger, for a form.\n *\n * `\"stepper\"`: the header of a month PAGE — the month as a heading that opens the\n * grid, then Today and ‹ › stepping the value by one month. What a calendar page\n * assembled by hand around the field (overriding its trigger to look like a\n * heading). `label`, `placeholder`, `invalid`, `hint`, `error`, `size` and\n * `triggerClassName` are the field's and are not drawn here; with no `value` the\n * arrows step from the current month (or year).\n */\n variant?: \"field\" | \"stepper\";\n /** `variant=\"stepper\"`: the heading level the month is set in (`<h2>` by default), or\n * `false` for no heading element — the page already has one for the month. */\n headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | false;\n /** `variant=\"stepper\"`: draw the Today button. Default `true`. */\n showToday?: boolean;\n}\n\n// ── Month keys ────────────────────────────────────────────────────────────\n\ninterface YearMonth {\n year: number;\n /** 1–12, as in the key. */\n month: number;\n}\n\nconst KEY = /^(\\d{4})-(\\d{2})/;\n\nfunction parseMonth(key: string | undefined): YearMonth | null {\n const m = key ? KEY.exec(key) : null;\n if (!m) return null;\n const month = Number(m[2]);\n return month >= 1 && month <= 12 ? { year: Number(m[1]), month } : null;\n}\n\nfunction toKey({ year, month }: YearMonth): string {\n return `${year}-${pad(month)}`;\n}\n\n/** Move a month key by whole months, carrying into the year. */\nfunction shiftMonth(ym: YearMonth, delta: number): YearMonth {\n const index = ym.year * 12 + (ym.month - 1) + delta;\n return { year: Math.floor(index / 12), month: (index % 12) + 1 };\n}\n\n/** A bound, normalised to its month key — so `\"2024-03-15\"` compares as `\"2024-03\"`. */\nfunction boundKey(bound: string | undefined): string | null {\n const ym = parseMonth(bound);\n return ym ? toKey(ym) : null;\n}\n\n/** Local-calendar Date for the 1st of a month, for `Intl` to format. */\nfunction firstOf({ year, month }: YearMonth): Date {\n return new Date(year, month - 1, 1);\n}\n\nconst DEFAULT_TRIGGER_FORMAT: Intl.DateTimeFormatOptions = { month: \"long\", year: \"numeric\" };\nconst MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: \"long\", year: \"numeric\" };\nconst YEAR_FORMAT: Intl.DateTimeFormatOptions = { year: \"numeric\" };\n\n// ── Year keys ─────────────────────────────────────────────────────────────\n\nconst YEAR = /^(\\d{4})/;\n\n/** The year of `\"YYYY\"`, `\"YYYY-MM\"` or `\"YYYY-MM-DD\"`, or null. */\nfunction parseYear(key: string | undefined): number | null {\n const m = key ? YEAR.exec(key) : null;\n return m ? Number(m[1]) : null;\n}\n\n// ── Trigger ───────────────────────────────────────────────────────────────\n\n/** Hand focus back to the trigger when the panel closes with focus still inside it. */\nfunction useFocusBackOnClose(open: boolean, triggerRef: RefObject<HTMLButtonElement | null>) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when focus went down with the panel. An outside click has already put it\n // somewhere real, and dragging it back here would be worse than leaving it.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n }, [open, triggerRef]);\n}\n\n/**\n * The stepper's trigger: the month as a heading-sized button that opens the grid.\n *\n * Its own text is its name (\"September 2026\"), so it needs no label of its own, and\n * it is `aria-live` for the reason `MiniCalendar`'s caption is: pressing ‹ or › leaves\n * focus on the arrow, and the heading is the only thing that says where the arrow\n * went. A plain button with `aria-haspopup`, not the field's `combobox` — it does not\n * look or behave like a field, and a heading reading \"combobox\" is a surprise.\n */\nfunction MonthHeadingTrigger({\n open,\n toggle,\n triggerRef,\n text,\n panelId,\n disabled,\n aria,\n}: {\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n text: string;\n panelId: string;\n disabled?: boolean;\n aria: TriggerAria;\n}) {\n useFocusBackOnClose(open, triggerRef);\n return (\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n aria-haspopup=\"dialog\"\n aria-controls={panelId}\n aria-expanded={open}\n id={aria.id}\n aria-label={aria[\"aria-label\"]}\n aria-labelledby={aria[\"aria-labelledby\"]}\n aria-describedby={aria[\"aria-describedby\"]}\n className={cn(\n \"-ms-1.5 inline-flex h-9 min-w-0 items-center gap-1 rounded-md px-1.5 text-lg font-semibold text-[var(--text-primary)] transition-colors\",\n \"hover:bg-[var(--bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <span aria-live=\"polite\" className=\"truncate\">\n {text || \"\\u00a0\"}\n </span>\n <ChevronDown aria-hidden className=\"size-4 shrink-0 text-[var(--text-muted)]\" />\n </button>\n );\n}\n\n/**\n * The field-shaped button that opens the grid. The same shape as `DatePicker`'s\n * trigger, for the same reasons written down there: `role=\"combobox\"` so it may carry\n * `aria-expanded` / `aria-invalid`, a name made of the label AND the value, and focus\n * handed back here when the panel closes with focus still inside it.\n */\nfunction MonthFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n panelId,\n padded,\n small,\n disabled,\n invalid,\n className,\n aria,\n}: {\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n triggerText: string;\n hasValue: boolean;\n labelledBy: string;\n /** The caller's naming attributes — see `trigger-aria.ts`. */\n aria: TriggerAria;\n valueId: string;\n panelId: string;\n padded: boolean;\n /** `size=\"sm\"`: the 36px, content-wide toolbar trigger. */\n small: boolean;\n disabled?: boolean;\n invalid?: boolean;\n className?: string;\n}) {\n useFocusBackOnClose(open, triggerRef);\n\n return (\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-controls={panelId}\n aria-expanded={open}\n id={aria.id}\n aria-invalid={invalid || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\" || undefined}\n aria-labelledby={aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy}\n aria-label={aria[\"aria-label\"]}\n aria-describedby={aria[\"aria-describedby\"]}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n // keksdose K13 (live #260): an IconButton's 36px, and only as wide as the\n // month — `py-0` with a fixed height so the box is 36px whatever the type's\n // line height. `pe-9` stays: the calendar glyph still sits at the end.\n small && \"h-9 w-auto py-0\",\n disabled && \"cursor-not-allowed opacity-50\",\n invalid && FIELD_INVALID,\n className,\n )}\n >\n {/* The U+00A0 fallback keeps the line box when there is no text — see\n DatePicker. The value arrives capitalised (see `upperFirst`); this span does\n NOT use `capitalize`, which upper-cases every WORD — \"Choose A Month\",\n \"Septiembre De 2026\". */}\n <span\n id={valueId}\n className={cn(\"truncate\", !hasValue && \"text-[var(--text-placeholder)]\")}\n >\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n// ── Grid ──────────────────────────────────────────────────────────────────\n\nconst COLUMNS = 3;\n\n/**\n * The panel: a year caption between ‹ › arrows, over a 4×3 grid of months.\n *\n * Mounted only while the popover is open, so every opening starts from the current\n * value's year — a value moved from outside (a prev/next-month button beside the\n * field) is where the grid lands, with no effect needed to snap it back.\n *\n * A grid in the APG sense, like `MiniCalendar`: one tab stop (the roving tabindex),\n * the arrows walk the months — left/right by one, up/down by a row of three — and\n * walking off either end of the year MOVES the year rather than stopping. PageUp /\n * PageDown step a whole year. Out-of-bounds months are `aria-disabled`, not\n * `disabled`, so the roving tab stop never falls into a hole.\n */\nfunction MonthGrid({\n value,\n onPick,\n locale,\n minKey,\n maxKey,\n currentKey,\n labels,\n}: {\n value: YearMonth | null;\n onPick: (key: string) => void;\n locale: string | undefined;\n minKey: string | null;\n maxKey: string | null;\n currentKey: string;\n labels: MonthPickerLabels;\n}) {\n const outOfBounds = (key: string) =>\n Boolean((minKey && key < minKey) || (maxKey && key > maxKey));\n\n // The month holding the tab order, and with it the year on screen — one piece of\n // state, as in MiniCalendar, so the two cannot disagree. Opens on the value, or on\n // the current month, pulled inside the bounds.\n const [active, setActive] = useState<YearMonth>(() => {\n const start = value ? toKey(value) : currentKey;\n if (minKey && start < minKey) return parseMonth(minKey)!;\n if (maxKey && start > maxKey) return parseMonth(maxKey)!;\n return parseMonth(start)!;\n });\n const year = active.year;\n\n const gridRef = useRef<HTMLDivElement>(null);\n const captionId = `${useId()}-year`;\n\n // Short names for the cells, long \"Month YYYY\" for their accessible names.\n const shortNames = useMemo(() => {\n const fmt = new Intl.DateTimeFormat(locale, { month: \"short\" });\n return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2000, i, 1)));\n }, [locale]);\n const longName = useMemo(() => new Intl.DateTimeFormat(locale, MONTH_NAME_FORMAT), [locale]);\n const yearCaption = new Intl.DateTimeFormat(locale, { year: \"numeric\" }).format(\n firstOf({ year, month: 1 }),\n );\n\n // A month whose cell must take focus after the next commit. Arrowing across a year\n // boundary re-labels the same twelve buttons, so the one to focus is a DIFFERENT\n // button from the one the key landed on.\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const key = pendingFocus.current;\n if (!key) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-month=\"${key}\"]`)?.focus();\n });\n\n // Opened because the user asked to pick a month: land them on it. Runs before the\n // popover's focus trap (a child's effect fires first), which then keeps this focus.\n useEffect(() => {\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus();\n }, []);\n\n const moveTo = (next: YearMonth, focus: boolean) => {\n if (focus) pendingFocus.current = toKey(next);\n setActive(next);\n };\n\n const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n // Left and right are VISUAL directions: in a right-to-left page the grid runs the\n // other way, so \"right\" is the previous month there. The panel is portalled, but\n // it carries the field's `dir` (see MonthPicker), so the nearest `dir` is right.\n const step = horizontalStep(e.key, e.currentTarget);\n const col = (active.month - 1) % COLUMNS;\n let delta: number;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n delta = step;\n break;\n case \"ArrowUp\":\n delta = -COLUMNS;\n break;\n case \"ArrowDown\":\n delta = COLUMNS;\n break;\n case \"Home\":\n delta = -col;\n break;\n case \"End\":\n delta = COLUMNS - 1 - col;\n break;\n case \"PageUp\":\n delta = -12;\n break;\n case \"PageDown\":\n delta = 12;\n break;\n default:\n return;\n }\n // Before any no-op check: these keys also scroll the panel, and a month that\n // cannot move still must not move the page.\n e.preventDefault();\n if (delta !== 0) moveTo(shiftMonth(active, delta), true);\n };\n\n // An arrow is dead once EVERY month of the year it leads to is out of bounds.\n const prevBlocked = Boolean(minKey && toKey({ year: year - 1, month: 12 }) < minKey);\n const nextBlocked = Boolean(maxKey && toKey({ year: year + 1, month: 1 }) > maxKey);\n const selectedKey = value ? toKey(value) : null;\n\n const arrow =\n \"flex size-7 items-center justify-center rounded text-[var(--text-muted)] hover:bg-[var(--bg-hover)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent\";\n\n return (\n <div className=\"select-none\">\n <div className=\"mb-1 flex items-center justify-between\">\n <button\n type=\"button\"\n aria-label={labels.previousYear}\n disabled={prevBlocked}\n // Focus stays on the arrow being pressed — only the grid moves.\n onClick={() => moveTo(shiftMonth(active, -12), false)}\n className={arrow}\n >\n {/* Mirrored in RTL, where \"previous\" points the other way. */}\n <ChevronLeft className=\"size-4 rtl:-scale-x-100\" />\n </button>\n {/* Names the grid, and `aria-live` so stepping a year says which one you are\n on — the user pressing the arrow is not inside the grid to hear it change. */}\n <div\n id={captionId}\n aria-live=\"polite\"\n className=\"text-sm font-semibold tabular-nums text-[var(--text-primary)]\"\n >\n {yearCaption}\n </div>\n <button\n type=\"button\"\n aria-label={labels.nextYear}\n disabled={nextBlocked}\n onClick={() => moveTo(shiftMonth(active, 12), false)}\n className={arrow}\n >\n <ChevronRight className=\"size-4 rtl:-scale-x-100\" />\n </button>\n </div>\n <div ref={gridRef} role=\"grid\" aria-labelledby={captionId} className=\"grid gap-1\">\n {Array.from({ length: 12 / COLUMNS }, (_, row) => (\n <div key={row} role=\"row\" className=\"grid grid-cols-3 gap-1\">\n {Array.from({ length: COLUMNS }, (_, c) => {\n const ym = { year, month: row * COLUMNS + c + 1 };\n const key = toKey(ym);\n const disabled = outOfBounds(key);\n const selected = key === selectedKey;\n const current = key === currentKey;\n return (\n <button\n key={c}\n type=\"button\"\n role=\"gridcell\"\n data-month={key}\n tabIndex={ym.month === active.month ? 0 : -1}\n aria-label={labels.month(longName.format(firstOf(ym)))}\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n aria-current={current ? \"date\" : undefined}\n onClick={() => {\n // The guard `disabled` would have been; see the component note.\n if (disabled) return;\n setActive(ym);\n onPick(key);\n }}\n onKeyDown={onCellKeyDown}\n className={cn(\n \"rounded px-2 py-1.5 text-sm capitalize transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset\",\n // A --brand ring on a --brand fill exists only in the DOM.\n selected && !disabled\n ? \"focus-visible:ring-[var(--brand-contrast)]\"\n : \"focus-visible:ring-[var(--brand)]\",\n disabled\n ? \"cursor-not-allowed text-[var(--text-placeholder)]\"\n : selected\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\"\n : \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n current && !selected && !disabled && \"ring-1 ring-inset ring-[var(--border-strong)]\",\n )}\n >\n {shortNames[ym.month - 1]}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n/** Years on one page of the year grid: the month grid's 4×3. */\nconst YEAR_PAGE = 12;\n\n/**\n * `mode=\"year\"`'s panel: a page of years between ‹ › arrows — {@link MonthGrid}'s shape\n * and keyboard, one level up (kastlan 7, keksdose tax-tab.tsx:33).\n *\n * Which years a page holds:\n * - Bounded on both sides by twelve years or fewer: exactly those years, and no\n * arrows — keksdose's \"this year and four before\" is five cells, not a page of\n * twelve with seven greyed out.\n * - Otherwise twelve at a time, starting from `min` when there is one (so the first\n * page opens on it), else ending at `max`, else on multiples of twelve.\n *\n * The arrow keys walk the years (up/down by a row of three), PageUp / PageDown a page,\n * Home / End the row; walking off a page turns it. Focus stops at the bounds rather\n * than walking into years that refuse selection: a page has no \"next year\" past the\n * last selectable one worth landing on.\n */\nfunction YearGrid({\n value,\n onPick,\n locale,\n minYear,\n maxYear,\n currentYear,\n labels,\n}: {\n value: number | null;\n onPick: (key: string) => void;\n locale: string | undefined;\n minYear: number | null;\n maxYear: number | null;\n currentYear: number;\n labels: MonthPickerLabels;\n}) {\n const clamp = (y: number) => Math.min(maxYear ?? y, Math.max(minYear ?? y, y));\n // A bounded span that fits one page is shown whole.\n const whole =\n minYear !== null && maxYear !== null && maxYear >= minYear && maxYear - minYear < YEAR_PAGE\n ? { first: minYear, count: maxYear - minYear + 1 }\n : null;\n // The year holding the tab order, and with it the page on screen — one piece of\n // state, as in MonthGrid. Opens on the value, or on this year, inside the bounds.\n const [active, setActive] = useState(() => clamp(value ?? currentYear));\n const anchor = minYear ?? (maxYear !== null ? maxYear - (YEAR_PAGE - 1) : 0);\n const first = whole ? whole.first : anchor + Math.floor((active - anchor) / YEAR_PAGE) * YEAR_PAGE;\n const count = whole ? whole.count : YEAR_PAGE;\n const years = Array.from({ length: count }, (_, i) => first + i);\n const outOfBounds = (y: number) => (minYear !== null && y < minYear) || (maxYear !== null && y > maxYear);\n\n const gridRef = useRef<HTMLDivElement>(null);\n const captionId = `${useId()}-years`;\n const yearFormat = useMemo(() => new Intl.DateTimeFormat(locale, YEAR_FORMAT), [locale]);\n // \"2016–2027\" in the locale's own range punctuation and digits.\n const caption = yearFormat.formatRange(new Date(first, 0, 1), new Date(first + count - 1, 0, 1));\n\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const year = pendingFocus.current;\n if (year === null) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-year=\"${year}\"]`)?.focus();\n });\n // Opened to pick a year: land on it (before the popover's trap, which keeps it).\n useEffect(() => {\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus();\n }, []);\n\n const moveTo = (next: number, focus: boolean) => {\n const year = clamp(next);\n if (focus) pendingFocus.current = year;\n setActive(year);\n };\n\n const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {\n const step = horizontalStep(e.key, e.currentTarget);\n const col = (active - first) % COLUMNS;\n let delta: number;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n delta = step;\n break;\n case \"ArrowUp\":\n delta = -COLUMNS;\n break;\n case \"ArrowDown\":\n delta = COLUMNS;\n break;\n case \"Home\":\n delta = -col;\n break;\n case \"End\":\n delta = Math.min(COLUMNS - 1 - col, first + count - 1 - active);\n break;\n case \"PageUp\":\n delta = -YEAR_PAGE;\n break;\n case \"PageDown\":\n delta = YEAR_PAGE;\n break;\n default:\n return;\n }\n e.preventDefault();\n if (delta !== 0) moveTo(active + delta, true);\n };\n\n const prevBlocked = minYear !== null && first - 1 < minYear;\n const nextBlocked = maxYear !== null && first + count > maxYear;\n const arrow =\n \"flex size-7 items-center justify-center rounded text-[var(--text-muted)] hover:bg-[var(--bg-hover)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent\";\n\n return (\n <div className=\"select-none\">\n <div className={cn(\"mb-1 flex items-center\", whole ? \"h-7 justify-center\" : \"justify-between\")}>\n {!whole && (\n <button\n type=\"button\"\n aria-label={labels.earlierYears}\n disabled={prevBlocked}\n onClick={() => moveTo(active - YEAR_PAGE, false)}\n className={arrow}\n >\n <ChevronLeft className=\"size-4 rtl:-scale-x-100\" />\n </button>\n )}\n <div id={captionId} aria-live=\"polite\" className=\"text-sm font-semibold tabular-nums text-[var(--text-primary)]\">\n {caption}\n </div>\n {!whole && (\n <button\n type=\"button\"\n aria-label={labels.laterYears}\n disabled={nextBlocked}\n onClick={() => moveTo(active + YEAR_PAGE, false)}\n className={arrow}\n >\n <ChevronRight className=\"size-4 rtl:-scale-x-100\" />\n </button>\n )}\n </div>\n <div ref={gridRef} role=\"grid\" aria-labelledby={captionId} className=\"grid gap-1\">\n {Array.from({ length: Math.ceil(count / COLUMNS) }, (_, row) => (\n <div key={row} role=\"row\" className=\"grid grid-cols-3 gap-1\">\n {years.slice(row * COLUMNS, row * COLUMNS + COLUMNS).map((year) => {\n const disabled = outOfBounds(year);\n const selected = year === value;\n const current = year === currentYear;\n return (\n <button\n key={year}\n type=\"button\"\n role=\"gridcell\"\n data-year={year}\n tabIndex={year === active ? 0 : -1}\n aria-selected={selected}\n aria-disabled={disabled || undefined}\n aria-current={current ? \"date\" : undefined}\n onClick={() => {\n if (disabled) return;\n setActive(year);\n onPick(String(year));\n }}\n onKeyDown={onCellKeyDown}\n className={cn(\n \"rounded px-2 py-1.5 text-sm tabular-nums transition-colors\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset\",\n selected && !disabled\n ? \"focus-visible:ring-[var(--brand-contrast)]\"\n : \"focus-visible:ring-[var(--brand)]\",\n disabled\n ? \"cursor-not-allowed text-[var(--text-placeholder)]\"\n : selected\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]\"\n : \"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\",\n current && !selected && !disabled && \"ring-1 ring-inset ring-[var(--border-strong)]\",\n )}\n >\n {yearFormat.format(new Date(year, 0, 1))}\n </button>\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n}\n\n// ── Field ─────────────────────────────────────────────────────────────────\n\n/**\n * Month picker: a field showing the selected month (\"August 2026\"), opening a 12-month\n * grid with year arrows. For a value that IS a month — a budget period, a calendar\n * view, a statement — where a day picker would ask for a day nobody means.\n *\n * One component for the two app-local pickers it replaces: keksdose's budget-header\n * popover (grid + year steppers + a floor) and kastlan's calendar header (a month list\n * and a year list in two popovers). The grid does both jobs — the year arrows and\n * PageUp/PageDown cover what the year list did, without a fixed ±5-year window.\n */\n/** Upper-case the first letter only, in `locale`. Several locales (fr, es, it, pt, …)\n * write month names in lower case, which at the start of a field reads as a typo —\n * but only the FIRST letter is the field's business: \"septiembre de 2026\" becomes\n * \"Septiembre de 2026\", not \"Septiembre De 2026\". */\nfunction upperFirst(text: string, locale: string | undefined): string {\n return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);\n}\n\nexport function MonthPicker(props: MonthPickerProps) {\n const {\n value,\n onChange,\n locale: localeProp,\n min,\n max,\n label,\n placeholder,\n disabled,\n invalid,\n error,\n hint,\n size = \"md\",\n mode = \"month\",\n formatOptions,\n currentMonth,\n labels: labelsProp,\n className,\n triggerClassName,\n variant = \"field\",\n headingLevel = 2,\n showToday = true,\n ...rest\n } = props;\n const labels = useMonthPickerLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const fromProvider = useKitDateFormatter();\n const years = mode === \"year\";\n const selected = years ? null : parseMonth(value);\n const selectedYear = years ? parseYear(value) : null;\n const minKey = boundKey(min);\n const maxKey = boundKey(max);\n const minYear = parseYear(min);\n const maxYear = parseYear(max);\n const currentKey = boundKey(currentMonth) ?? monthKey(new Date());\n const currentYear = parseYear(currentKey)!;\n // The key the trigger shows, in the mode's own shape: \"YYYY-MM\" or \"YYYY\".\n const shownKey = years ? (selectedYear !== null ? String(selectedYear) : null) : selected ? toKey(selected) : null;\n\n // The kit's order: `formatOptions` (this field's own Intl look), then the provider's\n // `formatDate` (keksdose K12 — the app's one way of writing a month), then the default.\n const triggerText = shownKey\n ? (formatOptions === undefined &&\n fromProvider?.(shownKey, { unit: years ? \"year\" : \"month\", source: \"monthPicker\", locale, weekday: false })) ||\n upperFirst(\n new Intl.DateTimeFormat(locale, formatOptions ?? (years ? YEAR_FORMAT : DEFAULT_TRIGGER_FORMAT)).format(\n years ? new Date(selectedYear!, 0, 1) : firstOf(selected!),\n ),\n locale,\n )\n : (placeholder ?? \"\");\n\n const id = useId();\n // keksdose K4: the caption and the error, on the trigger after the caller's own\n // description. Painted for `invalid` and a message — not for a bare `aria-invalid`,\n // which this field has never painted for.\n const messages = useFieldMessages({ hint, error, describedBy: rest[\"aria-describedby\"] });\n const [ownAria, wrapperRest] = splitTriggerAria(rest);\n // The field's trigger is described by the caption and the error too; the stepper\n // draws neither, so its heading keeps the caller's own description.\n const aria: TriggerAria = { ...ownAria, \"aria-describedby\": messages.describedBy };\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can double as a name; a ReactNode may hold its own controls.\n const named = typeof label === \"string\";\n // `sm` is the toolbar trigger, for an unlabelled picker only (see `size`).\n const small = size === \"sm\" && label === undefined;\n // The panel is portalled to <body>, out of the subtree whose `dir` it inherited — an\n // RTL form got an LTR grid. Read when opening (`toggle` is the popover's only way\n // open) and put back on the panel. Also what the arrow keys above read.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const panelLabel = years ? labels.yearPanel : labels.panel;\n\n const grid = (close: () => void) =>\n years ? (\n <YearGrid\n value={selectedYear}\n locale={locale}\n minYear={minYear}\n maxYear={maxYear}\n currentYear={currentYear}\n labels={labels}\n onPick={(key) => {\n onChange(key);\n close();\n }}\n />\n ) : (\n <MonthGrid\n value={selected}\n locale={locale}\n minKey={minKey}\n maxKey={maxKey}\n currentKey={currentKey}\n labels={labels}\n onPick={(key) => {\n onChange(key);\n close();\n }}\n />\n );\n\n if (variant === \"stepper\") {\n // The arrows step from the value, or from \"now\" when there is none; each is dead\n // once the month (or year) it leads to is out of bounds, and Today once \"now\" is.\n let prevKey: string;\n let nextKey: string;\n let nowKey: string;\n let outside: (key: string) => boolean;\n if (years) {\n const base = selectedYear ?? currentYear;\n prevKey = String(base - 1);\n nextKey = String(base + 1);\n nowKey = String(currentYear);\n outside = (key) => {\n const y = Number(key);\n return (minYear !== null && y < minYear) || (maxYear !== null && y > maxYear);\n };\n } else {\n const base = selected ?? parseMonth(currentKey)!;\n prevKey = toKey(shiftMonth(base, -1));\n nextKey = toKey(shiftMonth(base, 1));\n nowKey = currentKey;\n outside = (key) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));\n }\n const Heading = headingLevel ? (`h${headingLevel}` as const) : null;\n const trigger = (\n <Popover\n width={256}\n panelId={panelId}\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={({ open, toggle, ref }) => (\n <MonthHeadingTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n text={triggerText}\n panelId={panelId}\n disabled={disabled}\n aria={ownAria}\n />\n )}\n >\n {grid}\n </Popover>\n );\n return (\n <div {...wrapperRest} ref={rootRef} className={cn(\"flex min-w-0 items-center gap-2\", className)}>\n {Heading ? <Heading className=\"m-0 min-w-0 text-lg font-semibold\">{trigger}</Heading> : trigger}\n <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n {showToday && (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n disabled={disabled || outside(nowKey)}\n onClick={() => {\n if (nowKey !== shownKey) onChange(nowKey);\n }}\n >\n {years ? labels.thisYear : labels.today}\n </Button>\n )}\n <IconButton\n aria-label={years ? labels.previousYear : labels.previousMonth}\n disabled={disabled || outside(prevKey)}\n onClick={() => onChange(prevKey)}\n >\n {/* Mirrored in RTL, where \"previous\" points the other way. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n aria-label={years ? labels.nextYear : labels.nextMonth}\n disabled={disabled || outside(nextKey)}\n onClick={() => onChange(nextKey)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n </div>\n );\n }\n\n const field = (\n // `w-fit` for the toolbar size: the calendar glyph is placed against this box, and a\n // full-width box would park it at the far end of the row, away from its trigger.\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", small && \"w-fit max-w-full\", className)}>\n {label !== undefined && <FieldLabelLine label={label} hint={messages.labelHint} />}\n {/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,\n and inside this `relative` root either way. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n <Popover\n width={256}\n panelId={panelId}\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={({ open, toggle, ref }) => (\n <MonthFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={shownKey !== null}\n // As DatePicker: a caller's reference first; an `id` without a label of our\n // own references the trigger itself, so an external <label htmlFor> names it.\n labelledBy={[\n aria[\"aria-labelledby\"],\n named ? labelId : !aria[\"aria-labelledby\"] && aria.id ? aria.id : undefined,\n valueId,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria={aria}\n valueId={valueId}\n panelId={panelId}\n padded={label !== undefined}\n small={small}\n disabled={disabled}\n invalid={invalid || hasContent(error)}\n className={triggerClassName}\n />\n )}\n >\n {grid}\n </Popover>\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\"\n />\n </div>\n );\n // An unlabelled picker has no label line: a FieldHint goes at its end, outside the\n // box, as on an unlabelled Select.\n const endHint = label === undefined ? messages.labelHint : undefined;\n return (\n <FieldBox below={messages.below} reserve={\"error\" in props || \"hint\" in props}>\n {endHint === undefined ? (\n field\n ) : (\n <div className=\"flex items-center gap-1.5\">\n <div className={cn(\"min-w-0\", !small && \"flex-1\")}>{field}</div>\n <span className=\"flex shrink-0 items-center\">{endHint}</span>\n </div>\n )}\n </FieldBox>\n );\n}\n"],"mappings":"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","names":["_"]}
@@ -40,10 +40,17 @@ interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChan
40
40
  * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same
41
41
  * rules. */
42
42
  error?: ReactNode;
43
+ /**
44
+ * Standing advice for the field — {@link Combobox}'s `hint` (keksdose K4): plain TEXT
45
+ * is a caption under the field on the trigger's `aria-describedby`, before any error;
46
+ * a {@link FieldHint} "?" rides the label line, or with no label sits at the trigger's
47
+ * end edge, outside the box. Not an option's `hint`, which stays on its row.
48
+ */
49
+ hint?: ReactNode;
43
50
  /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a
44
51
  * disabled {@link Select} drops its own), and a dimmed label. */
45
52
  disabled?: boolean;
46
53
  }
47
- declare function MultiSelect({ options, values, onChange, allLabel, itemLabel, placeholder, label, searchLabel, selectAllLabel, clearLabel, className, panelClassName, invalid, error, disabled, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...rest }: MultiSelectProps): react.JSX.Element;
54
+ declare function MultiSelect({ options, values, onChange, allLabel, itemLabel, placeholder, label, searchLabel, selectAllLabel, clearLabel, className, panelClassName, invalid, error, hint, disabled, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, ...rest }: MultiSelectProps): react.JSX.Element;
48
55
 
49
56
  export { MultiSelect, type MultiSelectOption, type MultiSelectProps };
@@ -6,6 +6,7 @@ import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_
6
6
  import { cn } from "../lib/cn.js";
7
7
  import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown.js";
8
8
  import { useActiveOptionScroll, useComboboxFieldError } from "./combobox-core.js";
9
+ import { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from "./field-parts.js";
9
10
  import {
10
11
  DEFAULT_COMMON_LABELS,
11
12
  DEFAULT_MULTI_SELECT_LABELS,
@@ -26,6 +27,7 @@ function MultiSelect({
26
27
  panelClassName,
27
28
  invalid,
28
29
  error,
30
+ hint,
29
31
  disabled,
30
32
  "aria-label": ariaLabel,
31
33
  "aria-describedby": ariaDescribedBy,
@@ -39,7 +41,8 @@ function MultiSelect({
39
41
  selectedCount: itemLabel
40
42
  });
41
43
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
42
- const field = useComboboxFieldError(error, invalid, ariaDescribedBy);
44
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
45
+ const field = useComboboxFieldError(error, invalid, hintParts.describedBy);
43
46
  const uid = useId();
44
47
  const listboxId = `${uid}-listbox`;
45
48
  const optionId = (index) => `${uid}-option-${index}`;
@@ -92,8 +95,8 @@ function MultiSelect({
92
95
  // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
93
96
  // handlers that open the list, cannot be replaced by accident from outside.
94
97
  /* @__PURE__ */ jsxs("div", { ...rest, ref: wrapperRef, className: cn("relative", className), children: [
95
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { className: disabled ? "opacity-50" : void 0, children: label }),
96
- /* @__PURE__ */ jsxs(
98
+ label !== void 0 && hintParts.labelHint === void 0 && /* @__PURE__ */ jsx(FieldLabel, { className: disabled ? "opacity-50" : void 0, children: label }),
99
+ /* @__PURE__ */ jsx(EndHintRow, { hint: label === void 0 ? hintParts.labelHint : void 0, children: /* @__PURE__ */ jsxs(
97
100
  "button",
98
101
  {
99
102
  ref: triggerRef,
@@ -133,7 +136,8 @@ function MultiSelect({
133
136
  !disabled && /* @__PURE__ */ jsx(FieldChevron, {})
134
137
  ]
135
138
  }
136
- ),
139
+ ) }),
140
+ label !== void 0 && hintParts.labelHint !== void 0 && /* @__PURE__ */ jsx(StaticLabelRow, { hint: hintParts.labelHint, children: /* @__PURE__ */ jsx(FieldLabel, { className: cn(LABEL_IN_ROW, disabled && "opacity-50"), children: label }) }),
137
141
  open && !disabled && /* @__PURE__ */ jsx(
138
142
  DropdownPanel,
139
143
  {
@@ -216,6 +220,7 @@ function MultiSelect({
216
220
  })
217
221
  }
218
222
  ),
223
+ /* @__PURE__ */ jsx(FieldCaption, { parts: hintParts }),
219
224
  field.errorEl
220
225
  ] })
221
226
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/multi-select.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport { useActiveOptionScroll, useComboboxFieldError } from \"./combobox-core\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_MULTI_SELECT_LABELS,\n useKitLabels,\n} from \"../i18n/kit-labels\";\n\nexport interface MultiSelectOption {\n value: string | number;\n label: string;\n hint?: string;\n}\n\n/** `onChange` is the kit's — \"the selection changed\", carrying the new values —\n * rather than the div's form event. */\nexport interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n options: MultiSelectOption[];\n values: (string | number)[];\n onChange: (next: (string | number)[]) => void;\n /** Displayed when no value is selected (i.e. \"all\"). Defaults to `placeholder`,\n * then to `multiSelect.all` from the {@link UiKitProvider}, then \"All\". */\n allLabel?: string;\n /** Singular/plural countable label, used when 1+ items are selected. Default:\n * `multiSelect.selectedCount` from the provider — the bare count in English. */\n itemLabel?: (count: number) => string;\n placeholder?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n /** Search-box placeholder. Default: `multiSelect.search` from the provider, else \"Search\". */\n searchLabel?: string;\n /** \"Select all\" action label. Default: `multiSelect.selectAll`, else \"Select all\". */\n selectAllLabel?: string;\n /** \"Clear\" action label. Default: `multiSelect.clear`, else \"Clear\". */\n clearLabel?: string;\n /** Extra classes for the open dropdown PANEL — the way to widen it past its\n * 16rem default when the rows carry more than a label (Keksdose feedback #147). */\n panelClassName?: string;\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** What is wrong with the selection, in the caller's words. Rendered under the\n * field, pointed at by the trigger's `aria-describedby` (merged with any the\n * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same\n * rules. */\n error?: ReactNode;\n /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a\n * disabled {@link Select} drops its own), and a dimmed label. */\n disabled?: boolean;\n}\n\nexport function MultiSelect({\n options,\n values,\n onChange,\n allLabel,\n itemLabel,\n placeholder,\n label,\n searchLabel,\n selectAllLabel,\n clearLabel,\n className,\n panelClassName,\n invalid,\n error,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: MultiSelectProps) {\n const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =\n useDropdownSearch();\n // `allLabel` is deliberately NOT folded in here: between it and the provider's\n // `all` sits `placeholder`, which has always doubled as the \"nothing picked\" text,\n // and a caller who set it must keep seeing it.\n const labels = useKitLabels(\"multiSelect\", DEFAULT_MULTI_SELECT_LABELS, {\n search: searchLabel,\n selectAll: selectAllLabel,\n clear: clearLabel,\n selectedCount: itemLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const field = useComboboxFieldError(error, invalid, ariaDescribedBy);\n // One id per instance: `aria-controls` on the trigger names the list while the\n // list is still closed.\n const uid = useId();\n const listboxId = `${uid}-listbox`;\n const optionId = (index: number) => `${uid}-option-${index}`;\n // Which row the arrow keys are on. Reset from the handlers that change what the\n // list contains rather than from an effect — a render pass is not where this\n // decision belongs, and the two callers that move the list are right here.\n const [active, setActive] = useState(0);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return options;\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(q) ||\n (o.hint ?? \"\").toLowerCase().includes(q),\n );\n }, [options, query]);\n\n const valueSet = useMemo(() => new Set(values), [values]);\n const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));\n const anyChecked = values.length > 0;\n const selectedSummary = (() => {\n if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;\n return labels.selectedCount(values.length);\n })();\n\n const toggle = (val: string | number) => {\n if (valueSet.has(val)) onChange(values.filter((v) => v !== val));\n else onChange([...values, val]);\n };\n\n const selectAll = () => onChange(options.map((o) => o.value));\n const clearAll = () => onChange([]);\n\n const activeId = active >= 0 && active < filtered.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // The WAI-ARIA APG's listbox keyboard, handled on the search box because that is\n // where focus is while the panel is up. Escape is `useDropdown`'s, on the document.\n const onListKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, filtered.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActive(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActive(filtered.length - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n // Toggling keeps the panel open, exactly as clicking a row does: this control\n // exists to answer \"which of these\", which is rarely one of them.\n if (filtered[active]) toggle(filtered[active].value);\n } else if (e.key === \"Tab\") {\n e.preventDefault();\n closeToTrigger();\n }\n };\n\n return (\n // `rest` dresses the wrapper; the name belongs to the trigger below it, which is\n // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the\n // handlers that open the list, cannot be replaced by accident from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel className={disabled ? \"opacity-50\" : undefined}>{label}</FieldLabel>}\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n // A combobox, not a bare button: this trigger carried `aria-invalid`, which\n // `button` does not support, so a required-and-empty filter painted its rose\n // border and announced nothing (ESLint's `role-supports-aria-props`, the\n // audit's §a11y). The role that describes it is the one that also supports\n // the attribute.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger announces only its summary — \"3 selected\", with nothing saying\n // three of WHAT. The label AND the summary, because `aria-label` replaces\n // the content rather than adding to it; the same composition its two\n // siblings use, so a form row of pickers reads the same way throughout.\n // A caller's own name wins: two filters labelled \"Accounts\" on one screen have\n // to be told apart, and only the caller knows by what.\n aria-label={\n ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, selectedSummary) : undefined)\n }\n onClick={() => {\n setOpen((v) => !v);\n setActive(0);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and\n // Space already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n setActive(0);\n }\n }}\n aria-invalid={field.isInvalid || undefined}\n // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on\n // the component is taken off the wrapper for the same reason.\n aria-describedby={field.describedBy}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_TRIGGER's hover would still light a trigger nothing can open.\n \"disabled:hover:bg-[var(--bg-surface-2)]\",\n // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;\n // only the label was, so a disabled MultiSelect read as live (showcase audit).\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather\n than restating a lighter one (Keksdose dev#477). */}\n <span className=\"truncate\">{selectedSummary}</span>\n {!disabled && <FieldChevron />}\n </button>\n {open && !disabled && (\n <DropdownPanel\n // 16rem by default; `panelClassName` is how a caller widens it (Keksdose\n // feedback #147: the market picker's rows carry a postcode, a town, a\n // distance and a receipt count, all of which were being truncated). The\n // override used to be applied from the OUTSIDE with a\n // `[&>div]:w-full` descendant selector, which worked and was a hack —\n // \"change the package\" was the right call.\n className={cn(\"w-64\", panelClassName)}\n empty={filtered.length === 0}\n // The rows below are options, so the list around them has to be a listbox\n // — and one that says several answers are allowed, which is the only thing\n // that made the trigger's \"3 selected\" make sense to a reader.\n listProps={{ id: listboxId, role: \"listbox\", \"aria-multiselectable\": true }}\n header={\n <>\n <DropdownSearchHeader\n query={query}\n onQueryChange={(v) => {\n setQuery(v);\n // The list under the highlight just changed; keeping the old index\n // would point `aria-activedescendant` at a different option than\n // the one the user was on.\n setActive(0);\n }}\n inputRef={inputRef}\n placeholder={labels.search}\n listboxId={listboxId}\n activeId={activeId}\n onKeyDown={onListKeyDown}\n />\n <div className=\"flex items-center justify-between gap-2 px-2 py-1.5 text-xs\">\n <button\n type=\"button\"\n onClick={selectAll}\n disabled={allChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.selectAll}\n </button>\n <button\n type=\"button\"\n onClick={clearAll}\n disabled={!anyChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.clear}\n </button>\n </div>\n </>\n }\n >\n {filtered.map((o, i) => {\n const checked = valueSet.has(o.value);\n return (\n <li key={o.value} role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // The checked state used to exist ONLY as the decorative span\n // below (the audit's §a11y): the row said its label and nothing\n // else, so \"selected\" was imperceptible without sight. This is\n // the same state, in the one place a reader looks for it.\n role=\"option\"\n aria-selected={checked}\n // Focus stays in the search box — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onClick={() => toggle(o.value)}\n onMouseEnter={() => setActive(i)}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-start hover:bg-[var(--bg-hover)]\",\n checked && \"bg-[var(--bg-active)]\",\n i === active && \"bg-[var(--bg-hover)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <span\n // A picture of `aria-selected` above; a reader that met both\n // would be told twice.\n aria-hidden\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n checked\n ? \"border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"border-[var(--border-strong)] bg-[var(--bg-surface)]\",\n )}\n >\n {checked && <Check className=\"size-3\" />}\n </span>\n <span className=\"truncate text-[var(--text-primary)]\">{o.label}</span>\n {o.hint && (\n <span className=\"truncate text-xs text-[var(--text-placeholder)]\">\n {o.hint}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n {/* After the panel, not before it: the panel is `absolute` with no `top`, so it\n opens at its place in the flow — under the message, had the message come\n first. */}\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/multi-select.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type {\n ComponentPropsWithoutRef,\n KeyboardEvent as ReactKeyboardEvent,\n ReactNode,\n} from \"react\";\nimport { Check } from \"lucide-react\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from \"./dropdown\";\nimport { useActiveOptionScroll, useComboboxFieldError } from \"./combobox-core\";\nimport { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from \"./field-parts\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_MULTI_SELECT_LABELS,\n useKitLabels,\n} from \"../i18n/kit-labels\";\n\nexport interface MultiSelectOption {\n value: string | number;\n label: string;\n hint?: string;\n}\n\n/** `onChange` is the kit's — \"the selection changed\", carrying the new values —\n * rather than the div's form event. */\nexport interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n options: MultiSelectOption[];\n values: (string | number)[];\n onChange: (next: (string | number)[]) => void;\n /** Displayed when no value is selected (i.e. \"all\"). Defaults to `placeholder`,\n * then to `multiSelect.all` from the {@link UiKitProvider}, then \"All\". */\n allLabel?: string;\n /** Singular/plural countable label, used when 1+ items are selected. Default:\n * `multiSelect.selectedCount` from the provider — the bare count in English. */\n itemLabel?: (count: number) => string;\n placeholder?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n /** Search-box placeholder. Default: `multiSelect.search` from the provider, else \"Search\". */\n searchLabel?: string;\n /** \"Select all\" action label. Default: `multiSelect.selectAll`, else \"Select all\". */\n selectAllLabel?: string;\n /** \"Clear\" action label. Default: `multiSelect.clear`, else \"Clear\". */\n clearLabel?: string;\n /** Extra classes for the open dropdown PANEL — the way to widen it past its\n * 16rem default when the rows carry more than a label (Keksdose feedback #147). */\n panelClassName?: string;\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /** What is wrong with the selection, in the caller's words. Rendered under the\n * field, pointed at by the trigger's `aria-describedby` (merged with any the\n * caller passed) and implies `invalid` — {@link Input}'s `error`, on the same\n * rules. */\n error?: ReactNode;\n /**\n * Standing advice for the field — {@link Combobox}'s `hint` (keksdose K4): plain TEXT\n * is a caption under the field on the trigger's `aria-describedby`, before any error;\n * a {@link FieldHint} \"?\" rides the label line, or with no label sits at the trigger's\n * end edge, outside the box. Not an option's `hint`, which stays on its row.\n */\n hint?: ReactNode;\n /** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a\n * disabled {@link Select} drops its own), and a dimmed label. */\n disabled?: boolean;\n}\n\nexport function MultiSelect({\n options,\n values,\n onChange,\n allLabel,\n itemLabel,\n placeholder,\n label,\n searchLabel,\n selectAllLabel,\n clearLabel,\n className,\n panelClassName,\n invalid,\n error,\n hint,\n disabled,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: MultiSelectProps) {\n const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =\n useDropdownSearch();\n // `allLabel` is deliberately NOT folded in here: between it and the provider's\n // `all` sits `placeholder`, which has always doubled as the \"nothing picked\" text,\n // and a caller who set it must keep seeing it.\n const labels = useKitLabels(\"multiSelect\", DEFAULT_MULTI_SELECT_LABELS, {\n search: searchLabel,\n selectAll: selectAllLabel,\n clear: clearLabel,\n selectedCount: itemLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(error, invalid, hintParts.describedBy);\n // One id per instance: `aria-controls` on the trigger names the list while the\n // list is still closed.\n const uid = useId();\n const listboxId = `${uid}-listbox`;\n const optionId = (index: number) => `${uid}-option-${index}`;\n // Which row the arrow keys are on. Reset from the handlers that change what the\n // list contains rather than from an effect — a render pass is not where this\n // decision belongs, and the two callers that move the list are right here.\n const [active, setActive] = useState(0);\n\n const filtered = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return options;\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(q) ||\n (o.hint ?? \"\").toLowerCase().includes(q),\n );\n }, [options, query]);\n\n const valueSet = useMemo(() => new Set(values), [values]);\n const allChecked = options.length > 0 && options.every((o) => valueSet.has(o.value));\n const anyChecked = values.length > 0;\n const selectedSummary = (() => {\n if (!anyChecked || allChecked) return allLabel ?? placeholder ?? labels.all;\n return labels.selectedCount(values.length);\n })();\n\n const toggle = (val: string | number) => {\n if (valueSet.has(val)) onChange(values.filter((v) => v !== val));\n else onChange([...values, val]);\n };\n\n const selectAll = () => onChange(options.map((o) => o.value));\n const clearAll = () => onChange([]);\n\n const activeId = active >= 0 && active < filtered.length ? optionId(active) : undefined;\n useActiveOptionScroll(activeId);\n\n // The WAI-ARIA APG's listbox keyboard, handled on the search box because that is\n // where focus is while the panel is up. Escape is `useDropdown`'s, on the document.\n const onListKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, filtered.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Home\") {\n e.preventDefault();\n setActive(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n setActive(filtered.length - 1);\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n // Toggling keeps the panel open, exactly as clicking a row does: this control\n // exists to answer \"which of these\", which is rarely one of them.\n if (filtered[active]) toggle(filtered[active].value);\n } else if (e.key === \"Tab\") {\n e.preventDefault();\n closeToTrigger();\n }\n };\n\n return (\n // `rest` dresses the wrapper; the name belongs to the trigger below it, which is\n // the thing with a role. Spread FIRST so the ARIA the trigger composes, and the\n // handlers that open the list, cannot be replaced by accident from outside.\n <div {...rest} ref={wrapperRef} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && (\n <FieldLabel className={disabled ? \"opacity-50\" : undefined}>{label}</FieldLabel>\n )}\n <EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n // A combobox, not a bare button: this trigger carried `aria-invalid`, which\n // `button` does not support, so a required-and-empty filter painted its rose\n // border and announced nothing (ESLint's `role-supports-aria-props`, the\n // audit's §a11y). The role that describes it is the one that also supports\n // the attribute.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger announces only its summary — \"3 selected\", with nothing saying\n // three of WHAT. The label AND the summary, because `aria-label` replaces\n // the content rather than adding to it; the same composition its two\n // siblings use, so a form row of pickers reads the same way throughout.\n // A caller's own name wins: two filters labelled \"Accounts\" on one screen have\n // to be told apart, and only the caller knows by what.\n aria-label={\n ariaLabel ?? (typeof label === \"string\" ? common.fieldValue(label, selectedSummary) : undefined)\n }\n onClick={() => {\n setOpen((v) => !v);\n setActive(0);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and\n // Space already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n setActive(0);\n }\n }}\n aria-invalid={field.isInvalid || undefined}\n // On the TRIGGER, where focus lands — the `aria-describedby` a caller put on\n // the component is taken off the wrapper for the same reason.\n aria-describedby={field.describedBy}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_TRIGGER's hover would still light a trigger nothing can open.\n \"disabled:hover:bg-[var(--bg-surface-2)]\",\n // Dimmed like EntityCombobox's and MultiEntityCombobox's disabled triggers;\n // only the label was, so a disabled MultiSelect read as live (showcase audit).\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n {/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather\n than restating a lighter one (Keksdose dev#477). */}\n <span className=\"truncate\">{selectedSummary}</span>\n {!disabled && <FieldChevron />}\n </button>\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={cn(LABEL_IN_ROW, disabled && \"opacity-50\")}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n {open && !disabled && (\n <DropdownPanel\n // 16rem by default; `panelClassName` is how a caller widens it (Keksdose\n // feedback #147: the market picker's rows carry a postcode, a town, a\n // distance and a receipt count, all of which were being truncated). The\n // override used to be applied from the OUTSIDE with a\n // `[&>div]:w-full` descendant selector, which worked and was a hack —\n // \"change the package\" was the right call.\n className={cn(\"w-64\", panelClassName)}\n empty={filtered.length === 0}\n // The rows below are options, so the list around them has to be a listbox\n // — and one that says several answers are allowed, which is the only thing\n // that made the trigger's \"3 selected\" make sense to a reader.\n listProps={{ id: listboxId, role: \"listbox\", \"aria-multiselectable\": true }}\n header={\n <>\n <DropdownSearchHeader\n query={query}\n onQueryChange={(v) => {\n setQuery(v);\n // The list under the highlight just changed; keeping the old index\n // would point `aria-activedescendant` at a different option than\n // the one the user was on.\n setActive(0);\n }}\n inputRef={inputRef}\n placeholder={labels.search}\n listboxId={listboxId}\n activeId={activeId}\n onKeyDown={onListKeyDown}\n />\n <div className=\"flex items-center justify-between gap-2 px-2 py-1.5 text-xs\">\n <button\n type=\"button\"\n onClick={selectAll}\n disabled={allChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.selectAll}\n </button>\n <button\n type=\"button\"\n onClick={clearAll}\n disabled={!anyChecked}\n className=\"rounded px-2 py-0.5 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] disabled:opacity-40\"\n >\n {labels.clear}\n </button>\n </div>\n </>\n }\n >\n {filtered.map((o, i) => {\n const checked = valueSet.has(o.value);\n return (\n <li key={o.value} role=\"presentation\">\n <button\n type=\"button\"\n id={optionId(i)}\n // The checked state used to exist ONLY as the decorative span\n // below (the audit's §a11y): the row said its label and nothing\n // else, so \"selected\" was imperceptible without sight. This is\n // the same state, in the one place a reader looks for it.\n role=\"option\"\n aria-selected={checked}\n // Focus stays in the search box — that is what\n // `aria-activedescendant` is for — so the rows are not tab stops.\n tabIndex={-1}\n onClick={() => toggle(o.value)}\n onMouseEnter={() => setActive(i)}\n className={cn(\n \"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-start hover:bg-[var(--bg-hover)]\",\n checked && \"bg-[var(--bg-active)]\",\n i === active && \"bg-[var(--bg-hover)]\",\n )}\n >\n <span className=\"flex items-center gap-2 min-w-0\">\n <span\n // A picture of `aria-selected` above; a reader that met both\n // would be told twice.\n aria-hidden\n className={cn(\n \"flex size-4 shrink-0 items-center justify-center rounded border\",\n checked\n ? \"border-[var(--bg-inverse)] bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"border-[var(--border-strong)] bg-[var(--bg-surface)]\",\n )}\n >\n {checked && <Check className=\"size-3\" />}\n </span>\n <span className=\"truncate text-[var(--text-primary)]\">{o.label}</span>\n {o.hint && (\n <span className=\"truncate text-xs text-[var(--text-placeholder)]\">\n {o.hint}\n </span>\n )}\n </span>\n </button>\n </li>\n );\n })}\n </DropdownPanel>\n )}\n {/* After the panel, not before it: the panel is `absolute` with no `top`, so it\n opens at its place in the flow — under the message, had the message come\n first. */}\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,8 +2,10 @@ import * as react from 'react';
2
2
  import { ComponentProps, ReactNode } from 'react';
3
3
  import { NumberInput } from './number-input.js';
4
4
  import './calculator.js';
5
- import '../kit-labels-D1J2_jyD.js';
6
- import './data-table-labels.js';
5
+ import '../kit-labels-R8mIAc9N.js';
6
+ import '../data-table-labels-B7OdnM0S.js';
7
+ import './data-table-sort.js';
8
+ import './swipeable-row.js';
7
9
  import './mini-calendar.js';
8
10
  import './calendar-heatmap.js';
9
11
  import './popover.js';
@@ -26,7 +28,6 @@ import './sparkline.js';
26
28
  import './stat-tile.js';
27
29
  import './signature-pad.js';
28
30
  import './password-strength.js';
29
- import './danger-confirm.js';
30
31
  import './swatch-picker.js';
31
32
  import './tile-radio.js';
32
33
  import './icon-picker.js';
@@ -34,11 +35,10 @@ import './dialog-frame.js';
34
35
  import './modal.js';
35
36
  import '../hooks/use-search-param-state.js';
36
37
  import './measured-grid.js';
37
- import '../feedback-attachment-WFiXJ8We.js';
38
+ import '../feedback-attachment-NhUm7Zga.js';
38
39
  import '../feedback/feedback-thread.js';
39
40
  import '../feedback/feedback-inbox.js';
40
41
  import './account-settings-labels.js';
41
- import './confirm-dialog.js';
42
42
  import './floating-panel.js';
43
43
  import './bulk-action-bar.js';
44
44
  import './list.js';
@@ -57,18 +57,20 @@ import './write-lock.js';
57
57
  import './legal.js';
58
58
  import './account-chips.js';
59
59
  import '../lib/format.js';
60
- import '../data-table-3mi2R3so.js';
61
- import './data-table-sort.js';
62
- import './swipeable-row.js';
63
60
  import './reauth-dialog.js';
64
61
  import './server-wake.js';
65
62
  import '../lib/server-wake.js';
66
63
  import './translation-review-labels.js';
67
64
  import '../lib/translation-review.js';
65
+ import './country-select.js';
66
+ import './inline-edit-field.js';
67
+ import '../lib/iban.js';
68
+ import '../lib/phone.js';
69
+ import './sign-chip.js';
68
70
  import './numpad-sheet.js';
69
71
 
70
72
  type NumberInputProps = ComponentProps<typeof NumberInput>;
71
- interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid"> {
73
+ interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid" | "error" | "locale"> {
72
74
  /** The committed number. `null` is "no value" — shown as an empty field. */
73
75
  value: number | null;
74
76
  /**
@@ -168,10 +170,20 @@ interface NumberFieldProps extends Omit<NumberInputProps, "value" | "onChange" |
168
170
  *
169
171
  * LOCALE. The decimal mark is the locale's: a German user sees and types "1,5".
170
172
  * NumberInput normalises every keystroke to a dot (so its evaluator reads one
171
- * alphabet); this field maps the dot back before the draft is shown, which also
172
- * means the numpad's "." key types the mark the field is displaying. There is no
173
- * digit grouping — "1.000" would read as a thousand in German and as one in English,
174
- * and a field that parses its own display back must not have that ambiguity.
173
+ * alphabet); this field maps the dot back before the draft is shown. The field never
174
+ * WRITES digit grouping — "1.000" would read as a thousand in German and as one in
175
+ * English, and a field that parses its own display back must not have that ambiguity —
176
+ * but since 0.22 it READS it, through NumberInput (keksdose K5): "1.234,56" typed by a
177
+ * German user commits 1234.56 rather than 1.23456, and "1,234.56" by an English one
178
+ * likewise. While a figure is typed the typist's own text stays on screen ("1.234,5",
179
+ * or a dot typed as the decimal in a comma locale); it is re-spelt in the locale's mark
180
+ * when it commits.
181
+ *
182
+ * NOT FOR MONEY. `digits={2}` rounds, but a currency is more than two decimals: JPY has
183
+ * none, a unit price has four, and the chip, the minor-unit default and `min`/`max` on
184
+ * the SIGNED figure are {@link MoneyField}'s (number value) and {@link AmountInput}'s
185
+ * (string value). A NumberField whose `unit` is a currency says so on the console in
186
+ * development — see NumberInput's money guard.
175
187
  */
176
188
  declare function NumberField(props: NumberFieldProps): react.JSX.Element;
177
189
  /**
@@ -3,15 +3,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId, useState } from "react";
4
4
  import { NumberInput } from "./number-input.js";
5
5
  import { evaluateExpression, formatResult } from "../lib/calc.js";
6
+ import { decimalMark } from "../lib/decimal-marks.js";
6
7
  import { useKitLocale } from "../i18n/kit-labels.js";
7
- function decimalMark(locale) {
8
- try {
9
- const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
10
- return part?.value === "," ? "," : ".";
11
- } catch {
12
- return ".";
13
- }
14
- }
15
8
  function parseLive(text) {
16
9
  const t = text.trim().replace(/,/g, ".");
17
10
  if (t === "") return null;
@@ -28,7 +21,6 @@ function roundTo(n, digits) {
28
21
  const rounded = Number(n.toFixed(d));
29
22
  return Number.isFinite(rounded) ? rounded : n;
30
23
  }
31
- const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
32
24
  function NumberField(props) {
33
25
  const {
34
26
  value,
@@ -103,9 +95,6 @@ function NumberField(props) {
103
95
  };
104
96
  const generatedId = useId();
105
97
  const fieldId = id ?? generatedId;
106
- const errorId = useId();
107
- const hasError = error !== void 0 && error !== null && error !== false && error !== "";
108
- const describedBy = [ariaDescribedBy, hasError ? errorId : void 0].filter(Boolean).join(" ");
109
98
  const required = ariaRequired === true || ariaRequired === "true";
110
99
  const labelWithUnit = unit !== void 0 && unitPlacement === "label" ? label === void 0 || label === "" ? unit : typeof label === "string" && typeof unit === "string" ? `${label} (${unit})` : /* @__PURE__ */ jsxs(Fragment, { children: [
111
100
  label,
@@ -113,12 +102,12 @@ function NumberField(props) {
113
102
  unit,
114
103
  ")"
115
104
  ] }) : label;
116
- const field = /* @__PURE__ */ jsx(
105
+ return /* @__PURE__ */ jsx(
117
106
  NumberInput,
118
107
  {
119
108
  ...rest,
120
109
  id: fieldId,
121
- "aria-describedby": describedBy || void 0,
110
+ "aria-describedby": ariaDescribedBy,
122
111
  "aria-required": required || void 0,
123
112
  label: labelWithUnit,
124
113
  value: draft,
@@ -126,15 +115,12 @@ function NumberField(props) {
126
115
  onCommit: commit,
127
116
  min,
128
117
  max,
118
+ locale,
129
119
  suffix: unitPlacement === "suffix" ? unit : void 0,
130
- invalid: invalid || hasError || ariaInvalid === true || ariaInvalid === "true"
120
+ invalid: invalid || ariaInvalid === true || ariaInvalid === "true",
121
+ ..."error" in props ? { error } : {}
131
122
  }
132
123
  );
133
- if (!hasError && !("error" in props)) return field;
134
- return /* @__PURE__ */ jsxs("div", { children: [
135
- field,
136
- hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
137
- ] });
138
124
  }
139
125
  function IntegerField(props) {
140
126
  return /* @__PURE__ */ jsx(NumberField, { ...props, digits: props.digits ?? 0, calculator: props.calculator ?? false });