@eifi1/ui-kit 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -1 +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-parts\";\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-required={aria[\"aria-required\"]}\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":["_"]}
@@ -23,6 +23,9 @@ interface MultiEntityComboboxProps<V extends string | number> extends Omit<Compo
23
23
  /** Trigger summary when 1+ selected (default: the labels as a locale-formatted
24
24
  * list, or `combobox.selectedCount` when a label can't be resolved). */
25
25
  itemLabel?: (count: number) => string;
26
+ /** Offer a clear "×" while anything is chosen; it emits `[]`. A pointer shortcut out
27
+ * of the tab order — the keyboard's way is Delete or Backspace on the CLOSED trigger,
28
+ * or unticking rows in the open list. Not offered while `disabled` or locked. */
26
29
  clearable?: boolean;
27
30
  clearLabel?: string;
28
31
  /** The phone sheet's close button. Its own prop rather than a reuse of
@@ -31,15 +34,37 @@ interface MultiEntityComboboxProps<V extends string | number> extends Omit<Compo
31
34
  * so it is the one control here that MUST be in the reader's language. */
32
35
  closeLabel?: string;
33
36
  disabled?: boolean;
37
+ /** Why the selection cannot be changed — {@link EntityCombobox}'s `disabledReason`:
38
+ * focusable and `aria-disabled`, no list, no clear, no `onChange`, the reason in the
39
+ * kit {@link Tooltip}. Wins over `disabled`. */
40
+ disabledReason?: ReactNode;
41
+ /** This picker COMMITS — toggling saves. Locked the `disabledReason` way under a
42
+ * locked {@link WriteLockProvider}. See {@link EntityCombobox}'s `commit`. */
43
+ commit?: boolean;
34
44
  /** When set, a "create" row appears for a non-empty query with no exact match.
35
45
  * The panel stays open (adding the new entity to `value` is the caller's job). */
36
46
  onCreate?: (query: string) => void;
37
47
  createLabel?: (query: string) => string;
48
+ /** Offer the create row with nothing typed too, worded as this. See
49
+ * {@link EntityCombobox}'s `createEmptyLabel`. */
50
+ createEmptyLabel?: string;
51
+ /** The create row is a write while toggling is not: locked on its own under a locked
52
+ * {@link WriteLockProvider}. See {@link EntityCombobox}'s `createCommit`. */
53
+ createCommit?: boolean;
38
54
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
39
55
  invalid?: boolean;
40
56
  /** What is wrong with the value, as {@link Input}'s `error`: rendered under the
41
57
  * field, on the trigger's `aria-describedby`, and implies `invalid`. */
42
58
  error?: ReactNode;
59
+ /**
60
+ * Standing advice — the rest of the Combobox family's `hint` since 0.22, which this
61
+ * one was left without (keksdose K4): plain TEXT is a caption under the field on the
62
+ * trigger's `aria-describedby`, before any error; a {@link FieldHint} "?" rides the
63
+ * label line, or with no label sits at the trigger's end edge, outside the box.
64
+ * Passed at all — even `undefined` — the field keeps one box, so a hint coming and
65
+ * going never rebuilds the trigger under the cursor (the 0.22 rule).
66
+ */
67
+ hint?: ReactNode;
43
68
  /** Narrow `options` client-side by the query. Default `true`; `false` shows them
44
69
  * as given (a server-ranked list). */
45
70
  filter?: boolean;
@@ -56,6 +81,6 @@ interface MultiEntityComboboxProps<V extends string | number> extends Omit<Compo
56
81
  * with the same async `loadOptions`/`onCreate` support. Rows show a checkbox and
57
82
  * toggle without closing the panel; the trigger summarises the selection.
58
83
  */
59
- declare function MultiEntityCombobox<V extends string | number>({ value, onChange, options, loadOptions, loading, label, placeholder, searchPlaceholder, emptyLabel, itemLabel, clearable, clearLabel, closeLabel, disabled, onCreate, createLabel, className, invalid, error, filter, minChars, debounceMs, loadErrorLabel, "aria-label": ariaLabel, id, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: MultiEntityComboboxProps<V>): react.JSX.Element;
84
+ declare function MultiEntityCombobox<V extends string | number>(props: MultiEntityComboboxProps<V>): react.JSX.Element;
60
85
 
61
86
  export { MultiEntityCombobox, type MultiEntityComboboxProps };
@@ -6,6 +6,8 @@ import { cn } from "../lib/cn.js";
6
6
  import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui.js";
7
7
  import {
8
8
  ComboboxPanel,
9
+ endHintRowProps,
10
+ offersCreate,
9
11
  useComboboxCore,
10
12
  useComboboxFieldError
11
13
  } from "./combobox-core.js";
@@ -15,42 +17,59 @@ import {
15
17
  useKitLabels,
16
18
  useKitLocale
17
19
  } from "../i18n/kit-labels.js";
18
- function MultiEntityCombobox({
19
- value,
20
- onChange,
21
- options,
22
- loadOptions,
23
- loading,
24
- label,
25
- placeholder,
26
- searchPlaceholder,
27
- emptyLabel,
28
- itemLabel,
29
- clearable,
30
- clearLabel,
31
- closeLabel,
32
- disabled,
33
- onCreate,
34
- createLabel,
35
- className,
36
- invalid,
37
- error,
38
- filter,
39
- minChars,
40
- debounceMs,
41
- loadErrorLabel,
42
- "aria-label": ariaLabel,
43
- // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
44
- // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
45
- // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
46
- // nothing and the hint and required state described nothing — kastlan's
47
- // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
48
- id,
49
- "aria-describedby": ariaDescribedBy,
50
- "aria-invalid": ariaInvalid,
51
- "aria-required": ariaRequired,
52
- ...rest
53
- }) {
20
+ import {
21
+ EndHintRow,
22
+ FieldCaption,
23
+ LABEL_IN_ROW,
24
+ LockedReason,
25
+ StaticLabelRow,
26
+ useFieldHint,
27
+ useLockReason
28
+ } from "./field-parts.js";
29
+ import { mergeDescribedBy } from "./choice-parts.js";
30
+ import { useCommitReason } from "./write-lock.js";
31
+ function MultiEntityCombobox(props) {
32
+ const {
33
+ value,
34
+ onChange,
35
+ options,
36
+ loadOptions,
37
+ loading,
38
+ label,
39
+ placeholder,
40
+ searchPlaceholder,
41
+ emptyLabel,
42
+ itemLabel,
43
+ clearable,
44
+ clearLabel,
45
+ closeLabel,
46
+ disabled,
47
+ disabledReason,
48
+ commit,
49
+ onCreate,
50
+ createLabel,
51
+ createEmptyLabel,
52
+ createCommit,
53
+ className,
54
+ invalid,
55
+ error,
56
+ hint,
57
+ filter,
58
+ minChars,
59
+ debounceMs,
60
+ loadErrorLabel,
61
+ "aria-label": ariaLabel,
62
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
63
+ // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
64
+ // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
65
+ // nothing and the hint and required state described nothing — kastlan's
66
+ // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
67
+ id,
68
+ "aria-describedby": ariaDescribedBy,
69
+ "aria-invalid": ariaInvalid,
70
+ "aria-required": ariaRequired,
71
+ ...rest
72
+ } = props;
54
73
  const core = useComboboxCore({
55
74
  options,
56
75
  loadOptions,
@@ -59,10 +78,11 @@ function MultiEntityCombobox({
59
78
  minChars,
60
79
  debounceMs
61
80
  });
81
+ const hintParts = useFieldHint(hint, ariaDescribedBy);
62
82
  const field = useComboboxFieldError(
63
83
  error,
64
84
  invalid || ariaInvalid === true || ariaInvalid === "true",
65
- ariaDescribedBy
85
+ hintParts.describedBy
66
86
  );
67
87
  const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
68
88
  search: searchPlaceholder,
@@ -72,6 +92,10 @@ function MultiEntityCombobox({
72
92
  });
73
93
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
74
94
  const { open, results, resolve, setOpen, query, triggerRef } = core;
95
+ const lock = useLockReason(commit, disabledReason);
96
+ const inert = lock.locked || Boolean(disabled);
97
+ if (lock.locked && open) setOpen(false);
98
+ const createReason = useCommitReason(createCommit, void 0);
75
99
  const listboxId = useId();
76
100
  const valueSet = useMemo(() => new Set(value), [value]);
77
101
  const locale = useKitLocale();
@@ -85,10 +109,17 @@ function MultiEntityCombobox({
85
109
  const chosen = value.map(resolve);
86
110
  return chosen.every(Boolean) ? listFormat.format(chosen.map((o) => o.label)) : labels.selectedCount(value.length);
87
111
  })();
112
+ const withLock = (trigger) => lock.locked ? /* @__PURE__ */ jsx(LockedReason, { lock, className: "block", children: trigger }) : trigger;
88
113
  const q = query.trim();
89
- const showCreate = Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
90
- const showClear = Boolean(clearable && value.length > 0 && !disabled);
114
+ const showCreate = offersCreate(
115
+ onCreate,
116
+ q,
117
+ results.map((o) => o.label),
118
+ createEmptyLabel
119
+ );
120
+ const showClear = Boolean(clearable && value.length > 0 && !inert);
91
121
  const toggle = (o) => {
122
+ if (lock.locked) return;
92
123
  core.rememberOption(o);
93
124
  if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));
94
125
  else onChange([...value, o.value]);
@@ -99,74 +130,81 @@ function MultiEntityCombobox({
99
130
  // has one. Spread FIRST so the trigger's ARIA and the
100
131
  // handlers that open the panel cannot be clobbered from outside.
101
132
  /* @__PURE__ */ jsxs("div", { ...rest, className: cn("relative", className), children: [
102
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
103
- /* @__PURE__ */ jsxs(
104
- "button",
105
- {
106
- ref: triggerRef,
107
- id,
108
- type: "button",
109
- role: "combobox",
110
- "aria-expanded": open,
111
- "aria-controls": listboxId,
112
- "aria-haspopup": "listbox",
113
- "aria-label": ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, summary) : void 0),
114
- disabled,
115
- "aria-invalid": field.isInvalid || void 0,
116
- "aria-describedby": field.describedBy,
117
- "aria-required": ariaRequired,
118
- onClick: () => !disabled && setOpen((o) => !o),
119
- onKeyDown: (e) => {
120
- if (disabled) return;
121
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
122
- e.preventDefault();
123
- setOpen(true);
124
- }
125
- },
126
- className: cn(
127
- FIELD_TRIGGER,
128
- "pe-9",
129
- label !== void 0 && FIELD_FLOATING_PAD,
130
- disabled && "cursor-not-allowed opacity-50",
131
- field.isInvalid && FIELD_INVALID
132
- ),
133
- children: [
134
- /* @__PURE__ */ jsx(
135
- "span",
136
- {
137
- className: cn(
138
- "min-w-0 truncate",
139
- // Inherit FIELD_BASE's ink for a value, keep the placeholder tone
140
- // (`--text-placeholder`) — the same rule every field on a form row
141
- // follows (Keksdose dev#477).
142
- !value.length && "text-[var(--text-placeholder)]"
143
- ),
144
- children: summary
133
+ label !== void 0 && hintParts.labelHint === void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
134
+ /* @__PURE__ */ jsx(EndHintRow, { ...endHintRowProps("hint" in props, label !== void 0, hintParts.labelHint), children: withLock(
135
+ /* @__PURE__ */ jsxs(
136
+ "button",
137
+ {
138
+ ref: triggerRef,
139
+ id,
140
+ type: "button",
141
+ role: "combobox",
142
+ "aria-expanded": open,
143
+ "aria-controls": listboxId,
144
+ "aria-haspopup": "listbox",
145
+ "aria-label": ariaLabel ?? (typeof label === "string" ? common.fieldValue(label, summary) : void 0),
146
+ disabled: lock.locked ? void 0 : disabled,
147
+ "aria-disabled": lock.locked || void 0,
148
+ "aria-invalid": field.isInvalid || void 0,
149
+ "aria-describedby": mergeDescribedBy(field.describedBy, lock.locked && lock.reasonId),
150
+ "aria-required": ariaRequired,
151
+ onClick: () => !inert && setOpen((o) => !o),
152
+ onKeyDown: (e) => {
153
+ if (inert) return;
154
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
155
+ e.preventDefault();
156
+ setOpen(true);
157
+ } else if (showClear && !open && (e.key === "Delete" || e.key === "Backspace")) {
158
+ e.preventDefault();
159
+ onChange([]);
145
160
  }
161
+ },
162
+ className: cn(
163
+ FIELD_TRIGGER,
164
+ "pe-9",
165
+ label !== void 0 && FIELD_FLOATING_PAD,
166
+ inert && "cursor-not-allowed opacity-50",
167
+ field.isInvalid && FIELD_INVALID
146
168
  ),
147
- showClear ? (
148
- /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
149
- the trigger <button>, so it never holds focus and a key handler here could
150
- never fire. A pointer shortcut; the keyboard path is unticking the options
151
- in the open list. */
169
+ children: [
152
170
  /* @__PURE__ */ jsx(
153
171
  "span",
154
172
  {
155
- role: "button",
156
- tabIndex: -1,
157
- "aria-label": labels.clear,
158
- onClick: (e) => {
159
- e.stopPropagation();
160
- onChange([]);
161
- },
162
- className: "absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
163
- children: /* @__PURE__ */ jsx(X, { className: "size-4" })
173
+ className: cn(
174
+ "min-w-0 truncate",
175
+ // Inherit FIELD_BASE's ink for a value, keep the placeholder tone
176
+ // (`--text-placeholder`) — the same rule every field on a form row
177
+ // follows (Keksdose dev#477).
178
+ !value.length && "text-[var(--text-placeholder)]"
179
+ ),
180
+ children: summary
164
181
  }
165
- )
166
- ) : /* @__PURE__ */ jsx(FieldChevron, {})
167
- ]
168
- }
169
- ),
182
+ ),
183
+ showClear ? (
184
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
185
+ the trigger <button>, so it never holds focus and a key handler here could
186
+ never fire. A pointer shortcut; the keyboard paths are Delete on the closed
187
+ trigger and unticking the options in the open list. */
188
+ /* @__PURE__ */ jsx(
189
+ "span",
190
+ {
191
+ role: "button",
192
+ tabIndex: -1,
193
+ "aria-label": labels.clear,
194
+ onClick: (e) => {
195
+ e.stopPropagation();
196
+ onChange([]);
197
+ },
198
+ className: "absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
199
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
200
+ }
201
+ )
202
+ ) : /* @__PURE__ */ jsx(FieldChevron, {})
203
+ ]
204
+ }
205
+ )
206
+ ) }),
207
+ label !== void 0 && hintParts.labelHint !== void 0 && /* @__PURE__ */ jsx(StaticLabelRow, { hint: hintParts.labelHint, children: /* @__PURE__ */ jsx(FieldLabel, { className: LABEL_IN_ROW, children: label }) }),
170
208
  /* @__PURE__ */ jsx(
171
209
  ComboboxPanel,
172
210
  {
@@ -182,12 +220,15 @@ function MultiEntityCombobox({
182
220
  onChoose: toggle,
183
221
  showCreate,
184
222
  onCreate: () => {
223
+ if (lock.locked) return;
185
224
  onCreate?.(q);
186
225
  core.setQuery("");
187
226
  },
188
- createContent: labels.create(q)
227
+ createContent: q ? labels.create(q) : createEmptyLabel,
228
+ createDisabledReason: createReason
189
229
  }
190
230
  ),
231
+ /* @__PURE__ */ jsx(FieldCaption, { parts: hintParts }),
191
232
  field.errorEl
192
233
  ] })
193
234
  );