@eifi1/ui-kit 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +35 -5
  2. package/dist/admin/role-select.d.ts +3 -2
  3. package/dist/admin/role-select.js +2 -0
  4. package/dist/admin/role-select.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +2 -2
  6. package/dist/components/amount-input.js.map +1 -1
  7. package/dist/components/autocomplete.d.ts +2 -2
  8. package/dist/components/autocomplete.js +2 -1
  9. package/dist/components/autocomplete.js.map +1 -1
  10. package/dist/components/button-look.d.ts +1 -1
  11. package/dist/components/button-look.js +9 -4
  12. package/dist/components/button-look.js.map +1 -1
  13. package/dist/components/checkbox-group.d.ts +5 -2
  14. package/dist/components/checkbox-group.js +15 -4
  15. package/dist/components/checkbox-group.js.map +1 -1
  16. package/dist/components/column-mapper.d.ts +5 -3
  17. package/dist/components/column-mapper.js +2 -0
  18. package/dist/components/column-mapper.js.map +1 -1
  19. package/dist/components/combobox.d.ts +2 -2
  20. package/dist/components/combobox.js +2 -1
  21. package/dist/components/combobox.js.map +1 -1
  22. package/dist/components/country-select.d.ts +3 -3
  23. package/dist/components/country-select.js +3 -0
  24. package/dist/components/country-select.js.map +1 -1
  25. package/dist/components/currency-select.d.ts +3 -2
  26. package/dist/components/currency-select.js +10 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/date-field.d.ts +2 -1
  29. package/dist/components/date-field.js.map +1 -1
  30. package/dist/components/date-picker.js +3 -0
  31. package/dist/components/date-picker.js.map +1 -1
  32. package/dist/components/date-range-picker.js +3 -0
  33. package/dist/components/date-range-picker.js.map +1 -1
  34. package/dist/components/entity-combobox.d.ts +2 -2
  35. package/dist/components/entity-combobox.js +2 -1
  36. package/dist/components/entity-combobox.js.map +1 -1
  37. package/dist/components/field-parts.d.ts +66 -11
  38. package/dist/components/field-parts.js +32 -10
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +3 -2
  41. package/dist/components/field-strip.js +2 -1
  42. package/dist/components/field-strip.js.map +1 -1
  43. package/dist/components/field.d.ts +3 -2
  44. package/dist/components/field.js +24 -15
  45. package/dist/components/field.js.map +1 -1
  46. package/dist/components/help-popover.d.ts +9 -7
  47. package/dist/components/help-popover.js.map +1 -1
  48. package/dist/components/icon-picker.d.ts +3 -2
  49. package/dist/components/icon-picker.js +3 -0
  50. package/dist/components/icon-picker.js.map +1 -1
  51. package/dist/components/inline-entity-combobox.d.ts +2 -2
  52. package/dist/components/inline-entity-combobox.js +2 -1
  53. package/dist/components/inline-entity-combobox.js.map +1 -1
  54. package/dist/components/input.d.ts +2 -2
  55. package/dist/components/input.js +2 -1
  56. package/dist/components/input.js.map +1 -1
  57. package/dist/components/month-picker.d.ts +2 -1
  58. package/dist/components/month-picker.js +8 -1
  59. package/dist/components/month-picker.js.map +1 -1
  60. package/dist/components/multi-entity-combobox.d.ts +2 -2
  61. package/dist/components/multi-entity-combobox.js +2 -1
  62. package/dist/components/multi-entity-combobox.js.map +1 -1
  63. package/dist/components/multi-select.d.ts +2 -2
  64. package/dist/components/multi-select.js +3 -0
  65. package/dist/components/multi-select.js.map +1 -1
  66. package/dist/components/number-input.d.ts +2 -2
  67. package/dist/components/number-input.js.map +1 -1
  68. package/dist/components/numeric-field.d.ts +5 -2
  69. package/dist/components/numeric-field.js +4 -1
  70. package/dist/components/numeric-field.js.map +1 -1
  71. package/dist/components/one-time-code-input.d.ts +2 -1
  72. package/dist/components/one-time-code-input.js +2 -1
  73. package/dist/components/one-time-code-input.js.map +1 -1
  74. package/dist/components/phone-input.d.ts +1 -1
  75. package/dist/components/phone-input.js +4 -0
  76. package/dist/components/phone-input.js.map +1 -1
  77. package/dist/components/popover.js +1 -1
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/select.d.ts +4 -3
  80. package/dist/components/select.js +2 -1
  81. package/dist/components/select.js.map +1 -1
  82. package/dist/components/slider.d.ts +3 -2
  83. package/dist/components/slider.js +7 -1
  84. package/dist/components/slider.js.map +1 -1
  85. package/dist/components/stat-tile.js +2 -11
  86. package/dist/components/stat-tile.js.map +1 -1
  87. package/dist/components/swatch-picker.d.ts +3 -2
  88. package/dist/components/swatch-picker.js +3 -0
  89. package/dist/components/swatch-picker.js.map +1 -1
  90. package/dist/components/text-link.js +3 -1
  91. package/dist/components/text-link.js.map +1 -1
  92. package/dist/components/textarea.d.ts +2 -1
  93. package/dist/components/textarea.js +9 -3
  94. package/dist/components/textarea.js.map +1 -1
  95. package/dist/components/toggle-group.d.ts +2 -2
  96. package/dist/components/toggle-group.js +7 -3
  97. package/dist/components/toggle-group.js.map +1 -1
  98. package/dist/eslint/index.js +1 -1
  99. package/dist/eslint/role-utilities.d.ts +34 -2
  100. package/dist/eslint/role-utilities.js +30 -9
  101. package/dist/eslint/role-utilities.js.map +1 -1
  102. package/dist/i18n/defaults.js +5 -1
  103. package/dist/i18n/defaults.js.map +1 -1
  104. package/dist/i18n/german.js +97 -1
  105. package/dist/i18n/german.js.map +1 -1
  106. package/dist/i18n/kit-labels.d.ts +16 -0
  107. package/dist/i18n/kit-labels.js +21 -1
  108. package/dist/i18n/kit-labels.js.map +1 -1
  109. package/dist/i18n/locales/en.js +103 -2
  110. package/dist/i18n/locales/en.js.map +3 -3
  111. package/dist/i18n/locales/es.js +95 -1
  112. package/dist/i18n/locales/es.js.map +1 -1
  113. package/dist/i18n/locales/fr.js +96 -1
  114. package/dist/i18n/locales/fr.js.map +1 -1
  115. package/dist/i18n/locales/hu.js +95 -1
  116. package/dist/i18n/locales/hu.js.map +1 -1
  117. package/dist/i18n/locales/it.js +96 -1
  118. package/dist/i18n/locales/it.js.map +1 -1
  119. package/dist/i18n/locales/zh.js +95 -1
  120. package/dist/i18n/locales/zh.js.map +1 -1
  121. package/dist/i18n/review.js +13 -0
  122. package/dist/i18n/review.js.map +1 -1
  123. package/dist/i18n/shared-labels.js +3 -1
  124. package/dist/i18n/shared-labels.js.map +1 -1
  125. package/dist/index.d.ts +21 -0
  126. package/dist/index.js +38 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/lib/device-settings.d.ts +72 -0
  129. package/dist/lib/device-settings.js +64 -0
  130. package/dist/lib/device-settings.js.map +1 -0
  131. package/dist/lib/plain-text.d.ts +11 -0
  132. package/dist/lib/plain-text.js +16 -0
  133. package/dist/lib/plain-text.js.map +1 -0
  134. package/dist/lib/safe-storage.d.ts +6 -0
  135. package/dist/lib/safe-storage.js +11 -0
  136. package/dist/lib/safe-storage.js.map +1 -1
  137. package/dist/notifications/notification-parts.d.ts +39 -0
  138. package/dist/notifications/notification-parts.js +47 -0
  139. package/dist/notifications/notification-parts.js.map +1 -0
  140. package/dist/notifications/notification-types-setting.d.ts +62 -0
  141. package/dist/notifications/notification-types-setting.js +192 -0
  142. package/dist/notifications/notification-types-setting.js.map +1 -0
  143. package/dist/notifications/notification-types.d.ts +75 -0
  144. package/dist/notifications/notification-types.js +2 -0
  145. package/dist/notifications/notification-types.js.map +1 -0
  146. package/dist/notifications/notifications-labels.d.ts +131 -0
  147. package/dist/notifications/notifications-labels.js +83 -0
  148. package/dist/notifications/notifications-labels.js.map +1 -0
  149. package/dist/notifications/push-device-setting.d.ts +37 -0
  150. package/dist/notifications/push-device-setting.js +164 -0
  151. package/dist/notifications/push-device-setting.js.map +1 -0
  152. package/dist/notifications/push-devices-setting.d.ts +44 -0
  153. package/dist/notifications/push-devices-setting.js +203 -0
  154. package/dist/notifications/push-devices-setting.js.map +1 -0
  155. package/dist/notifications/quiet-hours-setting.d.ts +47 -0
  156. package/dist/notifications/quiet-hours-setting.js +131 -0
  157. package/dist/notifications/quiet-hours-setting.js.map +1 -0
  158. package/dist/notifications/time-zone-select.d.ts +50 -0
  159. package/dist/notifications/time-zone-select.js +76 -0
  160. package/dist/notifications/time-zone-select.js.map +1 -0
  161. package/dist/notifications/unsubscribe-panel.d.ts +53 -0
  162. package/dist/notifications/unsubscribe-panel.js +139 -0
  163. package/dist/notifications/unsubscribe-panel.js.map +1 -0
  164. package/dist/notifications/use-push-device.d.ts +122 -0
  165. package/dist/notifications/use-push-device.js +230 -0
  166. package/dist/notifications/use-push-device.js.map +1 -0
  167. package/dist/rhf/fields.d.ts +26 -19
  168. package/dist/rhf/fields.js +51 -22
  169. package/dist/rhf/fields.js.map +1 -1
  170. package/dist/settings/device-reset-setting-labels.d.ts +3 -0
  171. package/dist/settings/device-reset-setting-labels.js +15 -0
  172. package/dist/settings/device-reset-setting-labels.js.map +1 -0
  173. package/dist/settings/device-reset-setting.d.ts +58 -0
  174. package/dist/settings/device-reset-setting.js +51 -0
  175. package/dist/settings/device-reset-setting.js.map +1 -0
  176. package/dist/shell/app-shell.js +5 -0
  177. package/dist/shell/app-shell.js.map +1 -1
  178. package/dist/theme/contrast-tokens.d.ts +42 -4
  179. package/dist/theme/contrast-tokens.js +56 -9
  180. package/dist/theme/contrast-tokens.js.map +1 -1
  181. package/dist/theme/contrast.d.ts +4 -2
  182. package/dist/theme/contrast.js +13 -0
  183. package/dist/theme/contrast.js.map +1 -1
  184. package/dist/theme/palette-derive.d.ts +15 -1
  185. package/dist/theme/palette-derive.js +37 -8
  186. package/dist/theme/palette-derive.js.map +1 -1
  187. package/dist/theme/palette-presets.d.ts +2 -1
  188. package/dist/theme/palette-presets.js +5 -5
  189. package/dist/theme/palette-presets.js.map +1 -1
  190. package/dist/theme/palette-store.js +6 -0
  191. package/dist/theme/palette-store.js.map +1 -1
  192. package/dist/theme/text-size.js +6 -0
  193. package/dist/theme/text-size.js.map +1 -1
  194. package/dist/theme/theme-store.js +6 -0
  195. package/dist/theme/theme-store.js.map +1 -1
  196. package/package.json +1 -1
  197. package/src/admin/role-select.tsx +4 -1
  198. package/src/components/amount-input.tsx +2 -1
  199. package/src/components/autocomplete.tsx +5 -2
  200. package/src/components/button-look.ts +15 -5
  201. package/src/components/checkbox-group.tsx +22 -4
  202. package/src/components/column-mapper.tsx +6 -2
  203. package/src/components/combobox.tsx +5 -2
  204. package/src/components/country-select.tsx +6 -1
  205. package/src/components/currency-select.tsx +12 -3
  206. package/src/components/date-field.tsx +2 -2
  207. package/src/components/date-picker.tsx +3 -0
  208. package/src/components/date-range-picker.tsx +3 -0
  209. package/src/components/entity-combobox.tsx +5 -2
  210. package/src/components/field-parts.tsx +107 -19
  211. package/src/components/field-strip.tsx +4 -3
  212. package/src/components/field.tsx +28 -15
  213. package/src/components/help-popover.tsx +9 -7
  214. package/src/components/icon-picker.tsx +8 -3
  215. package/src/components/inline-entity-combobox.tsx +5 -2
  216. package/src/components/input.tsx +4 -2
  217. package/src/components/month-picker.tsx +10 -3
  218. package/src/components/multi-entity-combobox.tsx +5 -2
  219. package/src/components/multi-select.tsx +6 -1
  220. package/src/components/number-input.tsx +2 -2
  221. package/src/components/numeric-field.ts +10 -2
  222. package/src/components/one-time-code-input.tsx +5 -3
  223. package/src/components/phone-input.tsx +4 -0
  224. package/src/components/popover.tsx +3 -1
  225. package/src/components/select.tsx +6 -3
  226. package/src/components/slider.tsx +10 -3
  227. package/src/components/stat-tile.tsx +3 -18
  228. package/src/components/swatch-picker.tsx +9 -4
  229. package/src/components/text-link.tsx +3 -1
  230. package/src/components/textarea.tsx +11 -4
  231. package/src/components/toggle-group.tsx +21 -5
  232. package/src/eslint/role-utilities.ts +67 -11
  233. package/src/i18n/defaults.ts +4 -0
  234. package/src/i18n/german.ts +113 -0
  235. package/src/i18n/kit-labels.tsx +52 -1
  236. package/src/i18n/locales/en.ts +6 -0
  237. package/src/i18n/locales/es.ts +106 -0
  238. package/src/i18n/locales/fr.ts +106 -0
  239. package/src/i18n/locales/hu.ts +104 -0
  240. package/src/i18n/locales/it.ts +105 -0
  241. package/src/i18n/locales/zh.ts +96 -0
  242. package/src/i18n/review.ts +13 -0
  243. package/src/i18n/shared-labels.ts +2 -0
  244. package/src/index.ts +50 -0
  245. package/src/lib/device-settings.ts +146 -0
  246. package/src/lib/plain-text.ts +22 -0
  247. package/src/lib/safe-storage.ts +16 -0
  248. package/src/notifications/notification-parts.tsx +83 -0
  249. package/src/notifications/notification-types-setting.tsx +309 -0
  250. package/src/notifications/notification-types.ts +80 -0
  251. package/src/notifications/notifications-labels.ts +238 -0
  252. package/src/notifications/push-device-setting.tsx +224 -0
  253. package/src/notifications/push-devices-setting.tsx +306 -0
  254. package/src/notifications/quiet-hours-setting.tsx +185 -0
  255. package/src/notifications/time-zone-select.tsx +126 -0
  256. package/src/notifications/unsubscribe-panel.tsx +203 -0
  257. package/src/notifications/use-push-device.ts +392 -0
  258. package/src/rhf/fields.tsx +77 -29
  259. package/src/settings/device-reset-setting-labels.ts +20 -0
  260. package/src/settings/device-reset-setting.tsx +119 -0
  261. package/src/shell/app-shell.tsx +8 -0
  262. package/src/theme/contrast-tokens.ts +106 -12
  263. package/src/theme/contrast.ts +30 -4
  264. package/src/theme/palette-derive.ts +68 -7
  265. package/src/theme/palette-presets.ts +9 -6
  266. package/src/theme/palette-store.ts +12 -0
  267. package/src/theme/text-size.ts +8 -0
  268. package/src/theme/theme-store.ts +10 -0
  269. package/tokens.css +38 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/country-select.tsx"],"sourcesContent":["import { useCallback, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { COUNTRY_CODES, countryName, normalizeCountryCode } from \"../lib/countries\";\nimport { rankMatch } from \"../lib/text-match\";\nimport {\n FieldChevron,\n FIELD_FLOATING_PAD,\n FIELD_INVALID,\n FIELD_TRIGGER,\n FloatingField,\n} from \"./field-base\";\nimport {\n assignRef,\n EndHintRow,\n LockedReason,\n mergeDescribedBy,\n useFieldMessages,\n useLockReason,\n type DisabledReasonDisplay,\n} from \"./field-parts\";\nimport { CurrencyFlag } from \"./currency-select\";\nimport { ComboboxPanel, useComboboxCore, type ComboOption } from \"./combobox-core\";\nimport type { CommitScope } from \"./write-lock\";\nimport {\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n useKitLabels,\n useKitLocale,\n type LabelOverride,\n} from \"../i18n/kit-labels\";\nimport { DEFAULT_COUNTRY_SELECT_LABELS } from \"./country-select-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link CountrySelect} renders on its own behalf. The list's \"no results\",\n * \"loading\" and result-count announcements are the shared `combobox` namespace's,\n * as on every other searchable picker. */\nexport interface CountrySelectLabels {\n /** What the field is, when the caller gives no `label`: the first half of the\n * trigger's accessible name, the phone sheet's title, and the empty trigger's\n * text. */\n country: string;\n /** The search box in the open list. */\n search: string;\n /** The heading between the `preferred` countries and all the others. Shown only\n * when `preferred` puts something above it. */\n others: string;\n}\n\n// Defined in ./country-select-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_COUNTRY_SELECT_LABELS };\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\n/**\n * `onChange` is the kit's — \"a country was picked\", carrying its code — rather than\n * the div's form event. `id` and the `aria-*` wiring below go to the TRIGGER, which is\n * the control a reader meets; everything else in `rest` dresses the wrapper.\n *\n * `Clearable` follows `clearable`, the way {@link EntityCombobox}'s `C` follows its\n * `clearValue`: it is inferred from the prop, so `onChange` is `(code: string) => void`\n * for the field as it always was (`onChange={setCountry}` over a `string` state still\n * type-checks), and `(code: string | null) => void` once `clearable` is set — the\n * `null` of a clear cannot reach a handler that was never told about it.\n */\nexport interface CountrySelectProps<Clearable extends boolean = false>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\"> {\n /** The chosen country as an ISO 3166-1 alpha-2 code (\"CH\"), in either case;\n * `null`, `undefined` or `\"\"` when none is. A code outside the list still shows\n * its name — the list restricts what can be PICKED, not what can be shown. */\n value: string | null | undefined;\n /** A country was picked: its alpha-2 code, always upper-case. With `clearable`,\n * also `null`: the \"×\" (or Delete on the trigger) emptied the field. */\n onChange: (code: Clearable extends true ? string | null : string) => void;\n /**\n * Offer a clear \"×\" in place of the chevron while a country is chosen; it hands\n * `onChange` `null` (the kit's later list: an optional country — a contact's\n * nationality, a bank filter's \"any country\" — could be set but never unset again).\n *\n * The combobox family's clear, as {@link EntityCombobox} draws it: a pointer\n * shortcut inside the trigger, out of the tab order so it is not a second stop\n * between this field and the next, named by `clearLabel` (default: the\n * `combobox.clear` label every picker in the family shares). The keyboard way is\n * Delete or Backspace on the closed trigger. Not offered while `disabled` or locked\n * (`commit` / `disabledReason`): a clear is a change like any other.\n */\n clearable?: Clearable;\n /** The clear \"×\"'s accessible name. Wins over `combobox.clear` from the\n * {@link UiKitProvider}, as {@link EntityCombobox}'s `clearLabel` does. */\n clearLabel?: string;\n /**\n * The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop,\n * the way {@link MoneyField} takes its input's (kastlan: react-hook-form's\n * focus-on-error calls `focus()` on whatever `field.ref` was handed, and without a\n * ref the first field in error on an address form was never focused). See\n * `RhfCountrySelect`, which passes it.\n */\n ref?: Ref<HTMLButtonElement>;\n /**\n * Offer only these codes (any case; duplicates and malformed entries dropped).\n * Default: every ISO 3166-1 country, {@link COUNTRY_CODES}. Sorted by name either\n * way — the order given is not kept, because the reader looks a country up by its\n * name in their own language, and any other order is one they cannot predict.\n *\n * keksdose's bank picker offers the fifteen countries its banks are in\n * (DE AT CH FR IT ES NL BE PT PL SE DK FI GB IE). A code that is not in ISO's list\n * is allowed here on purpose: `XK` (Kosovo) is the app's decision, not the kit's.\n */\n countries?: readonly string[];\n /**\n * A short group shown FIRST, in the order given, above the alphabetical rest —\n * kastlan's CH, LI, DE, AT, FR, IT, the six countries most of its customers live\n * in, out of a list of 249. Each code appears once: a preferred country is not\n * repeated in the rest. The rest gets a heading (`labels.others`) so the boundary\n * is visible. Search keeps the split: matching preferred countries stay on top.\n * Codes not in `countries` are ignored — `countries` decides what can be picked.\n */\n preferred?: readonly string[];\n /**\n * Draw each country's flag as a flag-icons span (`fi fi-ch`), in the rows and on\n * the trigger. Default `false`, because the kit ships no flag stylesheet — the same\n * convention as `LanguageMenu`'s `country` and `CurrencySelect`: the host imports\n * `flag-icons/css/flag-icons.min.css`, and without it a flag is an empty box.\n */\n flags?: boolean;\n /** The floating label, as on every field. A string also names the trigger\n * (\"Country: Switzerland\"). */\n label?: ReactNode;\n /** As {@link Select}'s `hint`: plain TEXT is a caption under the field, attached\n * through `aria-describedby`; a {@link FieldHint} rides the label line (or the\n * field's end, when there is no label). */\n hint?: ReactNode;\n /** As {@link Input}'s `error`: rendered under the field, on the trigger's\n * `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}, as on every field. */\n invalid?: boolean;\n /** The empty trigger's text. Default: nothing when the field has a `label` or an\n * `aria-labelledby` (the label already says what goes here), else `labels.country`. */\n placeholder?: string;\n /** As on the other pickers: no focus, no list, the settled look. */\n disabled?: boolean;\n /**\n * Why the country cannot be changed — {@link Button}'s `disabledReason`, for a field\n * that saves itself. The trigger stays focusable and `aria-disabled`, the list does\n * not open and `onChange` never fires; the reason is a line under the field at Large\n * and on a touch screen, else in the kit {@link Tooltip} (`disabledReasonDisplay`), and\n * on the trigger's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This select SAVES on change (a settings row with no Save button). Under a locked\n * {@link WriteLockProvider} it is locked the `disabledReason` way, with the lock's\n * reason. A country field inside a form that has its own Save stays editable under\n * the lock — leave this off there, and put `commit` on the Save.\n */\n commit?: CommitScope;\n /** The language the names are in. Default: the `<UiKitProvider locale>`, else\n * English (see {@link countryName}). */\n locale?: string;\n /** Per-instance overrides of {@link CountrySelectLabels}, over the provider's\n * `countrySelect` namespace, over English. */\n labels?: LabelOverride<CountrySelectLabels>;\n /**\n * Name the trigger by an element of the caller's — a form's own label above the\n * field, which is how `RhfCountrySelect` names it. The trigger is then named by\n * that element AND the chosen country (two references, as {@link DatePicker}'s\n * trigger is named, so \"Country Switzerland\" needs no punctuation to translate),\n * instead of the composed \"label: country\". An `aria-label` still wins. Before\n * 0.23 this landed on the wrapper `<div>`, which has no role to be named.\n */\n \"aria-labelledby\"?: string;\n}\n\ninterface Entry {\n code: string;\n name: string;\n}\n\n/** Best rank first; a stable sort, so equal ranks keep the collated (or preferred)\n * order they came in. The rank is the kit's one (`rankMatch`, DUP-5 — written here\n * first): the code itself (a two-letter query \"ch\" means Switzerland before it means\n * Chad), then the start of the name, then anywhere in it, with accents and case folded\n * — \"osterreich\" finds Österreich, \"cote\" Côte d'Ivoire. */\nfunction search(entries: Entry[], q: string, locale: string): Entry[] {\n return entries\n .map((e) => ({ e, r: rankMatch(e.name, q, { code: e.code, locale }) }))\n .filter((x) => x.r >= 0)\n .sort((a, b) => a.r - b.r)\n .map((x) => x.e);\n}\n\nfunction toOption(e: Entry, flags: boolean, group?: string): ComboOption<string> {\n return {\n value: e.code,\n label: e.name,\n icon: flags ? <CurrencyFlag country={e.code.toLowerCase()} /> : undefined,\n group,\n };\n}\n\nfunction collator(locale: string): Intl.Collator {\n try {\n return new Intl.Collator(locale);\n } catch {\n return new Intl.Collator();\n }\n}\n\n/**\n * A country picker: the value is the ISO 3166-1 alpha-2 code (\"CH\"), the names are the\n * runtime's own (`Intl.DisplayNames`) in the provider's locale, sorted by that\n * locale's collation. Requested by kastlan (customer and handover addresses, with its\n * six neighbours on top) and keksdose (the bank picker's fifteen countries) — both\n * apps carried a hand-written list of names in two languages.\n *\n * ## Built on the kit's searchable combobox\n *\n * 249 countries is not a menu, it is a lookup, and the list has to work on a phone. So\n * this is the entity pickers' machinery — `useComboboxCore` and `ComboboxPanel`, which\n * {@link EntityCombobox} is also made of — not a native `<select>`: the anchored panel\n * with a search box on a pointer device, the full-screen sheet with its own search box\n * on a phone (live #200), the same keyboard (APG listbox), the same announcements, the\n * same group headings. Only the trigger is this component's own, because the trigger\n * is where the write lock lives: `EntityCombobox`'s is a native `disabled` button,\n * and a locked field has to stay focusable to say why.\n *\n * The ranking is this component's own too: it folds accents and case (\"osterreich\"\n * finds Österreich), matches the code (\"ch\" ranks Switzerland first), and ranks\n * name-starts above name-contains — the kit's `rankMatch`, which began here. The panel\n * is handed those rows in place of the core's filter, which does not rank.\n *\n * Field anatomy as on every field: floating `label`, `hint`, `error`, `invalid`; the\n * trigger is named \"label: value\" as the other trigger pickers are.\n *\n * Since 0.23 it takes a `ref` to the trigger (react-hook-form's focus-on-error, kastlan)\n * and `clearable`, which hands `onChange` `null`.\n */\nexport function CountrySelect<Clearable extends boolean = false>({\n value,\n onChange,\n countries,\n preferred,\n flags = false,\n label,\n hint,\n error,\n invalid,\n placeholder,\n disabled,\n disabledReason: ownReason,\n disabledReasonDisplay,\n commit,\n clearable,\n clearLabel,\n locale: localeProp,\n labels: labelsProp,\n className,\n id,\n ref,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: CountrySelectProps<Clearable>) {\n const text = useKitLabels(\"countrySelect\", DEFAULT_COUNTRY_SELECT_LABELS, labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The clear's name is the family's (`combobox.clear`), as on EntityCombobox; a prop\n // left `undefined` falls through to the provider rather than masking it.\n const comboText = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, { clear: clearLabel });\n // English without a locale — see `countryName`.\n const locale = useKitLocale(localeProp) ?? \"en\";\n\n // ── The list ──────────────────────────────────────────────────────────────\n // Keyed on the arrays' CONTENTS: `countries={[\"DE\", \"AT\"]}` written inline is a new\n // array every render, and resorting 249 names on every keystroke elsewhere in the\n // form is work for nothing.\n const countriesKey = countries?.join(\",\");\n const preferredKey = preferred?.join(\",\");\n const { top, rest: others } = useMemo(() => {\n const seen = new Set<string>();\n const pool: string[] = [];\n for (const raw of countriesKey !== undefined ? countriesKey.split(\",\") : COUNTRY_CODES) {\n const code = normalizeCountryCode(raw);\n if (code && !seen.has(code)) {\n seen.add(code);\n pool.push(code);\n }\n }\n const entry = (code: string): Entry => ({ code, name: countryName(code, locale) });\n const firsts: Entry[] = [];\n const picked = new Set<string>();\n for (const raw of preferredKey ? preferredKey.split(\",\") : []) {\n const code = normalizeCountryCode(raw);\n if (code && seen.has(code) && !picked.has(code)) {\n picked.add(code);\n firsts.push(entry(code));\n }\n }\n const compare = collator(locale).compare;\n const sorted = pool\n .filter((c) => !picked.has(c))\n .map(entry)\n .sort((a, b) => compare(a.name, b.name));\n return { top: firsts, rest: sorted };\n }, [countriesKey, preferredKey, locale]);\n\n // `filter: false` — the panel is handed the rows `search` ranks below instead of\n // the core's substring filter; the core still owns open, query, the highlighted row,\n // anchoring and dismissal.\n const allOptions = useMemo(\n () => [...top, ...others].map((e) => toOption(e, flags)),\n [top, others, flags],\n );\n const core = useComboboxCore<string>({ options: allOptions, filter: false });\n const { open, setOpen, query, triggerRef, closeToTrigger } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n\n const q = query.trim();\n const results = useMemo(() => {\n const firsts = q ? search(top, q, locale) : top;\n const below = q ? search(others, q, locale) : others;\n // The heading only where it separates something: with no preferred country in\n // view it would head the whole list and divide nothing.\n const heading = firsts.length > 0 ? text.others : undefined;\n return [...firsts.map((e) => toOption(e, flags)), ...below.map((e) => toOption(e, flags, heading))];\n }, [top, others, q, text.others, flags, locale]);\n\n // ── The lock ──────────────────────────────────────────────────────────────\n const lock = useLockReason(commit, ownReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n // A lock (or `disabled`) arriving while the list is up closes it — adjusted while\n // rendering, as the core adjusts its own query, so no frame shows a list that can\n // no longer be used.\n if (inert && open) setOpen(false);\n\n // ── The field ─────────────────────────────────────────────────────────────\n const generated = useId();\n const triggerId = id ?? generated;\n const listboxId = `${generated}-listbox`;\n // Text is a caption under the field; a FieldHint rides the label line, and is that\n // caption at Large and on touch (§4). The caller's ids, the caption, a lock's reason,\n // the error — Select's order. Through useFieldMessages since 0.34 (DUP-2, DUP-8).\n const field = useFieldMessages({\n hint,\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid,\n reasonId: locked && lock.reasonId,\n });\n\n const code = normalizeCountryCode(value);\n const selectedName = code ? countryName(code, locale) : undefined;\n // A field named by a label of the caller's (`aria-labelledby`) is a labelled field too:\n // the label already says what goes here.\n const emptyText = placeholder ?? (label === undefined && !ariaLabelledBy ? text.country : \"\");\n const fieldName = typeof label === \"string\" ? label : text.country;\n const valueId = `${generated}-value`;\n // Named by the caller's element (a form's label) — then the shown text is the second\n // reference, as the composed name below has the value second; but only when it IS a\n // value or the caller's placeholder, never the `country` fallback word.\n const labelledBy =\n ariaLabel === undefined && ariaLabelledBy\n ? mergeDescribedBy(ariaLabelledBy, (selectedName !== undefined || Boolean(placeholder)) && valueId)\n : undefined;\n const name =\n labelledBy !== undefined\n ? undefined\n : (ariaLabel ??\n (selectedName !== undefined\n ? common.fieldValue(fieldName, selectedName)\n : placeholder\n ? common.fieldValue(fieldName, placeholder)\n : fieldName));\n\n const showClear = Boolean(clearable && code && !inert);\n // `onChange` widened to take the clear's `null` — its type says so once `clearable`\n // is set, and the clear is only ever offered then.\n const emit = onChange as (code: string | null) => void;\n const clear = () => {\n if (!showClear) return;\n emit(null);\n };\n\n const choose = (o: ComboOption<string>) => {\n if (inert) return;\n emit(o.value);\n // Back to the trigger: the panel holding focus is about to unmount, and a keyboard\n // user who has answered this field should stand on it, ready to Tab on.\n closeToTrigger();\n };\n\n const trigger = (\n <button\n ref={setTrigger}\n id={triggerId}\n type=\"button\"\n // A closed choice that expands into the list below — the role every trigger\n // picker in the kit wears (see EntityCombobox), and the one that supports\n // `aria-invalid`.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || undefined}\n aria-required={ariaRequired}\n aria-describedby={field.describedBy}\n // The focusable kind of disabled while locked (Button's rule): native `disabled`\n // would take the field out of the tab order, and the reason with it.\n disabled={locked ? undefined : disabled}\n aria-disabled={locked || undefined}\n onClick={(e) => {\n if (inert) {\n e.preventDefault();\n return;\n }\n setOpen((o) => !o);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and Space\n // already do through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is out of the tab order (see `clearable`).\n e.preventDefault();\n clear();\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n // `peer` dims the floating label with a natively disabled trigger, as Select's.\n \"peer pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {flags && code && <CurrencyFlag country={code.toLowerCase()} />}\n {/* A long name (\"Saint Vincent and the Grenadines\") truncates rather than\n widening the field: the kit is checked at 390px. From Large up it wraps and\n the trigger grows instead (text size §10.17, round 0.34 T1). */}\n <span\n id={valueId}\n className={cn(\"truncate-until-large\", selectedName === undefined && \"text-placeholder\")}\n >\n {selectedName ?? emptyText}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could never\n fire; the keys go to the trigger, where Delete and Backspace clear. A pointer\n shortcut, as on EntityCombobox. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={comboText.clear}\n onClick={(e) => {\n e.stopPropagation();\n clear();\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder hover:text-secondary\"\n >\n <X aria-hidden className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n );\n\n const control = locked ? (\n // Button's frame: the line under the trigger, or the Tooltip with a hidden copy for\n // `aria-describedby`. `items-stretch`: in the line's column the trigger keeps the\n // field's full width.\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"flex w-full items-stretch\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\n );\n\n const box =\n label !== undefined ? (\n <FloatingField htmlFor={triggerId} label={label} staticLabel hint={field.labelHint}>\n {control}\n </FloatingField>\n ) : (\n // No label line to ride: a \"?\" goes at the field's end, outside the box, as on an\n // unlabelled Select.\n <EndHintRow hint={field.labelHint}>\n <div className=\"relative\">{control}</div>\n </EndHintRow>\n );\n\n return (\n // `rest` dresses the wrapper — a `data-tour` anchor, a test id. The name, the\n // description and the invalid state are on the trigger above.\n <div {...rest} className={className}>\n {box}\n {field.below}\n <ComboboxPanel\n // The core with the rows ranked above in place of its own filter's.\n core={{ ...core, results }}\n listboxId={listboxId}\n // On a phone the list is a full-screen sheet, which needs the field's own\n // name to say what it is asking for (live #200).\n sheetTitle={label ?? text.country}\n searchPlaceholder={text.search}\n isSelected={(v) => v === code}\n onChoose={choose}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n />\n </div>\n );\n}\n"],"mappings":"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","names":["code"]}
1
+ {"version":3,"sources":["../../src/components/country-select.tsx"],"sourcesContent":["import { useCallback, useId, useMemo } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { COUNTRY_CODES, countryName, normalizeCountryCode } from \"../lib/countries\";\nimport { rankMatch } from \"../lib/text-match\";\nimport {\n FieldChevron,\n FIELD_FLOATING_PAD,\n FIELD_INVALID,\n FIELD_TRIGGER,\n FloatingField,\n} from \"./field-base\";\nimport {\n assignRef,\n EndHintRow,\n LockedReason,\n mergeDescribedBy,\n useFieldMessages,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport { CurrencyFlag } from \"./currency-select\";\nimport { ComboboxPanel, useComboboxCore, type ComboOption } from \"./combobox-core\";\nimport type { CommitScope } from \"./write-lock\";\nimport {\n DEFAULT_COMBOBOX_LABELS,\n DEFAULT_COMMON_LABELS,\n useKitLabels,\n useKitLocale,\n type LabelOverride,\n} from \"../i18n/kit-labels\";\nimport { DEFAULT_COUNTRY_SELECT_LABELS } from \"./country-select-labels\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link CountrySelect} renders on its own behalf. The list's \"no results\",\n * \"loading\" and result-count announcements are the shared `combobox` namespace's,\n * as on every other searchable picker. */\nexport interface CountrySelectLabels {\n /** What the field is, when the caller gives no `label`: the first half of the\n * trigger's accessible name, the phone sheet's title, and the empty trigger's\n * text. */\n country: string;\n /** The search box in the open list. */\n search: string;\n /** The heading between the `preferred` countries and all the others. Shown only\n * when `preferred` puts something above it. */\n others: string;\n}\n\n// Defined in ./country-select-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_COUNTRY_SELECT_LABELS };\n\n// ── Props ─────────────────────────────────────────────────────────────────────\n\n/**\n * `onChange` is the kit's — \"a country was picked\", carrying its code — rather than\n * the div's form event. `id` and the `aria-*` wiring below go to the TRIGGER, which is\n * the control a reader meets; everything else in `rest` dresses the wrapper.\n *\n * `Clearable` follows `clearable`, the way {@link EntityCombobox}'s `C` follows its\n * `clearValue`: it is inferred from the prop, so `onChange` is `(code: string) => void`\n * for the field as it always was (`onChange={setCountry}` over a `string` state still\n * type-checks), and `(code: string | null) => void` once `clearable` is set — the\n * `null` of a clear cannot reach a handler that was never told about it.\n */\nexport interface CountrySelectProps<Clearable extends boolean = false>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\">,\n FieldHelpProps {\n /** The chosen country as an ISO 3166-1 alpha-2 code (\"CH\"), in either case;\n * `null`, `undefined` or `\"\"` when none is. A code outside the list still shows\n * its name — the list restricts what can be PICKED, not what can be shown. */\n value: string | null | undefined;\n /** A country was picked: its alpha-2 code, always upper-case. With `clearable`,\n * also `null`: the \"×\" (or Delete on the trigger) emptied the field. */\n onChange: (code: Clearable extends true ? string | null : string) => void;\n /**\n * Offer a clear \"×\" in place of the chevron while a country is chosen; it hands\n * `onChange` `null` (the kit's later list: an optional country — a contact's\n * nationality, a bank filter's \"any country\" — could be set but never unset again).\n *\n * The combobox family's clear, as {@link EntityCombobox} draws it: a pointer\n * shortcut inside the trigger, out of the tab order so it is not a second stop\n * between this field and the next, named by `clearLabel` (default: the\n * `combobox.clear` label every picker in the family shares). The keyboard way is\n * Delete or Backspace on the closed trigger. Not offered while `disabled` or locked\n * (`commit` / `disabledReason`): a clear is a change like any other.\n */\n clearable?: Clearable;\n /** The clear \"×\"'s accessible name. Wins over `combobox.clear` from the\n * {@link UiKitProvider}, as {@link EntityCombobox}'s `clearLabel` does. */\n clearLabel?: string;\n /**\n * The TRIGGER — the focusable `<button>` a reader meets — as a React 19 ref prop,\n * the way {@link MoneyField} takes its input's (kastlan: react-hook-form's\n * focus-on-error calls `focus()` on whatever `field.ref` was handed, and without a\n * ref the first field in error on an address form was never focused). See\n * `RhfCountrySelect`, which passes it.\n */\n ref?: Ref<HTMLButtonElement>;\n /**\n * Offer only these codes (any case; duplicates and malformed entries dropped).\n * Default: every ISO 3166-1 country, {@link COUNTRY_CODES}. Sorted by name either\n * way — the order given is not kept, because the reader looks a country up by its\n * name in their own language, and any other order is one they cannot predict.\n *\n * keksdose's bank picker offers the fifteen countries its banks are in\n * (DE AT CH FR IT ES NL BE PT PL SE DK FI GB IE). A code that is not in ISO's list\n * is allowed here on purpose: `XK` (Kosovo) is the app's decision, not the kit's.\n */\n countries?: readonly string[];\n /**\n * A short group shown FIRST, in the order given, above the alphabetical rest —\n * kastlan's CH, LI, DE, AT, FR, IT, the six countries most of its customers live\n * in, out of a list of 249. Each code appears once: a preferred country is not\n * repeated in the rest. The rest gets a heading (`labels.others`) so the boundary\n * is visible. Search keeps the split: matching preferred countries stay on top.\n * Codes not in `countries` are ignored — `countries` decides what can be picked.\n */\n preferred?: readonly string[];\n /**\n * Draw each country's flag as a flag-icons span (`fi fi-ch`), in the rows and on\n * the trigger. Default `false`, because the kit ships no flag stylesheet — the same\n * convention as `LanguageMenu`'s `country` and `CurrencySelect`: the host imports\n * `flag-icons/css/flag-icons.min.css`, and without it a flag is an empty box.\n */\n flags?: boolean;\n /** The floating label, as on every field. A string also names the trigger\n * (\"Country: Switzerland\"). */\n label?: ReactNode;\n /** As {@link Select}'s `hint`: plain TEXT is a caption under the field, attached\n * through `aria-describedby`; a {@link FieldHint} rides the label line (or the\n * field's end, when there is no label). */\n hint?: ReactNode;\n /** As {@link Input}'s `error`: rendered under the field, on the trigger's\n * `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Required and unanswered — {@link FIELD_INVALID}, as on every field. */\n invalid?: boolean;\n /** The empty trigger's text. Default: nothing when the field has a `label` or an\n * `aria-labelledby` (the label already says what goes here), else `labels.country`. */\n placeholder?: string;\n /** As on the other pickers: no focus, no list, the settled look. */\n disabled?: boolean;\n /**\n * Why the country cannot be changed — {@link Button}'s `disabledReason`, for a field\n * that saves itself. The trigger stays focusable and `aria-disabled`, the list does\n * not open and `onChange` never fires; the reason is a line under the field at Large\n * and on a touch screen, else in the kit {@link Tooltip} (`disabledReasonDisplay`), and\n * on the trigger's `aria-describedby`. Wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This select SAVES on change (a settings row with no Save button). Under a locked\n * {@link WriteLockProvider} it is locked the `disabledReason` way, with the lock's\n * reason. A country field inside a form that has its own Save stays editable under\n * the lock — leave this off there, and put `commit` on the Save.\n */\n commit?: CommitScope;\n /** The language the names are in. Default: the `<UiKitProvider locale>`, else\n * English (see {@link countryName}). */\n locale?: string;\n /** Per-instance overrides of {@link CountrySelectLabels}, over the provider's\n * `countrySelect` namespace, over English. */\n labels?: LabelOverride<CountrySelectLabels>;\n /**\n * Name the trigger by an element of the caller's — a form's own label above the\n * field, which is how `RhfCountrySelect` names it. The trigger is then named by\n * that element AND the chosen country (two references, as {@link DatePicker}'s\n * trigger is named, so \"Country Switzerland\" needs no punctuation to translate),\n * instead of the composed \"label: country\". An `aria-label` still wins. Before\n * 0.23 this landed on the wrapper `<div>`, which has no role to be named.\n */\n \"aria-labelledby\"?: string;\n}\n\ninterface Entry {\n code: string;\n name: string;\n}\n\n/** Best rank first; a stable sort, so equal ranks keep the collated (or preferred)\n * order they came in. The rank is the kit's one (`rankMatch`, DUP-5 — written here\n * first): the code itself (a two-letter query \"ch\" means Switzerland before it means\n * Chad), then the start of the name, then anywhere in it, with accents and case folded\n * — \"osterreich\" finds Österreich, \"cote\" Côte d'Ivoire. */\nfunction search(entries: Entry[], q: string, locale: string): Entry[] {\n return entries\n .map((e) => ({ e, r: rankMatch(e.name, q, { code: e.code, locale }) }))\n .filter((x) => x.r >= 0)\n .sort((a, b) => a.r - b.r)\n .map((x) => x.e);\n}\n\nfunction toOption(e: Entry, flags: boolean, group?: string): ComboOption<string> {\n return {\n value: e.code,\n label: e.name,\n icon: flags ? <CurrencyFlag country={e.code.toLowerCase()} /> : undefined,\n group,\n };\n}\n\nfunction collator(locale: string): Intl.Collator {\n try {\n return new Intl.Collator(locale);\n } catch {\n return new Intl.Collator();\n }\n}\n\n/**\n * A country picker: the value is the ISO 3166-1 alpha-2 code (\"CH\"), the names are the\n * runtime's own (`Intl.DisplayNames`) in the provider's locale, sorted by that\n * locale's collation. Requested by kastlan (customer and handover addresses, with its\n * six neighbours on top) and keksdose (the bank picker's fifteen countries) — both\n * apps carried a hand-written list of names in two languages.\n *\n * ## Built on the kit's searchable combobox\n *\n * 249 countries is not a menu, it is a lookup, and the list has to work on a phone. So\n * this is the entity pickers' machinery — `useComboboxCore` and `ComboboxPanel`, which\n * {@link EntityCombobox} is also made of — not a native `<select>`: the anchored panel\n * with a search box on a pointer device, the full-screen sheet with its own search box\n * on a phone (live #200), the same keyboard (APG listbox), the same announcements, the\n * same group headings. Only the trigger is this component's own, because the trigger\n * is where the write lock lives: `EntityCombobox`'s is a native `disabled` button,\n * and a locked field has to stay focusable to say why.\n *\n * The ranking is this component's own too: it folds accents and case (\"osterreich\"\n * finds Österreich), matches the code (\"ch\" ranks Switzerland first), and ranks\n * name-starts above name-contains — the kit's `rankMatch`, which began here. The panel\n * is handed those rows in place of the core's filter, which does not rank.\n *\n * Field anatomy as on every field: floating `label`, `hint`, `error`, `invalid`; the\n * trigger is named \"label: value\" as the other trigger pickers are.\n *\n * Since 0.23 it takes a `ref` to the trigger (react-hook-form's focus-on-error, kastlan)\n * and `clearable`, which hands `onChange` `null`.\n */\nexport function CountrySelect<Clearable extends boolean = false>({\n value,\n onChange,\n countries,\n preferred,\n flags = false,\n label,\n hint,\n help,\n error,\n invalid,\n placeholder,\n disabled,\n disabledReason: ownReason,\n disabledReasonDisplay,\n commit,\n clearable,\n clearLabel,\n locale: localeProp,\n labels: labelsProp,\n className,\n id,\n ref,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: CountrySelectProps<Clearable>) {\n const text = useKitLabels(\"countrySelect\", DEFAULT_COUNTRY_SELECT_LABELS, labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // The clear's name is the family's (`combobox.clear`), as on EntityCombobox; a prop\n // left `undefined` falls through to the provider rather than masking it.\n const comboText = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, { clear: clearLabel });\n // English without a locale — see `countryName`.\n const locale = useKitLocale(localeProp) ?? \"en\";\n\n // ── The list ──────────────────────────────────────────────────────────────\n // Keyed on the arrays' CONTENTS: `countries={[\"DE\", \"AT\"]}` written inline is a new\n // array every render, and resorting 249 names on every keystroke elsewhere in the\n // form is work for nothing.\n const countriesKey = countries?.join(\",\");\n const preferredKey = preferred?.join(\",\");\n const { top, rest: others } = useMemo(() => {\n const seen = new Set<string>();\n const pool: string[] = [];\n for (const raw of countriesKey !== undefined ? countriesKey.split(\",\") : COUNTRY_CODES) {\n const code = normalizeCountryCode(raw);\n if (code && !seen.has(code)) {\n seen.add(code);\n pool.push(code);\n }\n }\n const entry = (code: string): Entry => ({ code, name: countryName(code, locale) });\n const firsts: Entry[] = [];\n const picked = new Set<string>();\n for (const raw of preferredKey ? preferredKey.split(\",\") : []) {\n const code = normalizeCountryCode(raw);\n if (code && seen.has(code) && !picked.has(code)) {\n picked.add(code);\n firsts.push(entry(code));\n }\n }\n const compare = collator(locale).compare;\n const sorted = pool\n .filter((c) => !picked.has(c))\n .map(entry)\n .sort((a, b) => compare(a.name, b.name));\n return { top: firsts, rest: sorted };\n }, [countriesKey, preferredKey, locale]);\n\n // `filter: false` — the panel is handed the rows `search` ranks below instead of\n // the core's substring filter; the core still owns open, query, the highlighted row,\n // anchoring and dismissal.\n const allOptions = useMemo(\n () => [...top, ...others].map((e) => toOption(e, flags)),\n [top, others, flags],\n );\n const core = useComboboxCore<string>({ options: allOptions, filter: false });\n const { open, setOpen, query, triggerRef, closeToTrigger } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n\n const q = query.trim();\n const results = useMemo(() => {\n const firsts = q ? search(top, q, locale) : top;\n const below = q ? search(others, q, locale) : others;\n // The heading only where it separates something: with no preferred country in\n // view it would head the whole list and divide nothing.\n const heading = firsts.length > 0 ? text.others : undefined;\n return [...firsts.map((e) => toOption(e, flags)), ...below.map((e) => toOption(e, flags, heading))];\n }, [top, others, q, text.others, flags, locale]);\n\n // ── The lock ──────────────────────────────────────────────────────────────\n const lock = useLockReason(commit, ownReason);\n const locked = lock.locked;\n const inert = locked || Boolean(disabled);\n // A lock (or `disabled`) arriving while the list is up closes it — adjusted while\n // rendering, as the core adjusts its own query, so no frame shows a list that can\n // no longer be used.\n if (inert && open) setOpen(false);\n\n // ── The field ─────────────────────────────────────────────────────────────\n const generated = useId();\n const triggerId = id ?? generated;\n const listboxId = `${generated}-listbox`;\n // Text is a caption under the field; a FieldHint rides the label line, and is that\n // caption at Large and on touch (§4). The caller's ids, the caption, a lock's reason,\n // the error — Select's order. Through useFieldMessages since 0.34 (DUP-2, DUP-8).\n const field = useFieldMessages({\n hint,\n help,\n name: label ?? ariaLabel,\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid,\n reasonId: locked && lock.reasonId,\n });\n\n const code = normalizeCountryCode(value);\n const selectedName = code ? countryName(code, locale) : undefined;\n // A field named by a label of the caller's (`aria-labelledby`) is a labelled field too:\n // the label already says what goes here.\n const emptyText = placeholder ?? (label === undefined && !ariaLabelledBy ? text.country : \"\");\n const fieldName = typeof label === \"string\" ? label : text.country;\n const valueId = `${generated}-value`;\n // Named by the caller's element (a form's label) — then the shown text is the second\n // reference, as the composed name below has the value second; but only when it IS a\n // value or the caller's placeholder, never the `country` fallback word.\n const labelledBy =\n ariaLabel === undefined && ariaLabelledBy\n ? mergeDescribedBy(ariaLabelledBy, (selectedName !== undefined || Boolean(placeholder)) && valueId)\n : undefined;\n const name =\n labelledBy !== undefined\n ? undefined\n : (ariaLabel ??\n (selectedName !== undefined\n ? common.fieldValue(fieldName, selectedName)\n : placeholder\n ? common.fieldValue(fieldName, placeholder)\n : fieldName));\n\n const showClear = Boolean(clearable && code && !inert);\n // `onChange` widened to take the clear's `null` — its type says so once `clearable`\n // is set, and the clear is only ever offered then.\n const emit = onChange as (code: string | null) => void;\n const clear = () => {\n if (!showClear) return;\n emit(null);\n };\n\n const choose = (o: ComboOption<string>) => {\n if (inert) return;\n emit(o.value);\n // Back to the trigger: the panel holding focus is about to unmount, and a keyboard\n // user who has answered this field should stand on it, ready to Tab on.\n closeToTrigger();\n };\n\n const trigger = (\n <button\n ref={setTrigger}\n id={triggerId}\n type=\"button\"\n // A closed choice that expands into the list below — the role every trigger\n // picker in the kit wears (see EntityCombobox), and the one that supports\n // `aria-invalid`.\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-label={name}\n aria-labelledby={labelledBy}\n aria-invalid={field.isInvalid || undefined}\n aria-required={ariaRequired}\n aria-describedby={field.describedBy}\n // The focusable kind of disabled while locked (Button's rule): native `disabled`\n // would take the field out of the tab order, and the reason with it.\n disabled={locked ? undefined : disabled}\n aria-disabled={locked || undefined}\n onClick={(e) => {\n if (inert) {\n e.preventDefault();\n return;\n }\n setOpen((o) => !o);\n }}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and Space\n // already do through the button's own click.\n onKeyDown={(e) => {\n if (inert) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n } else if (showClear && !open && (e.key === \"Delete\" || e.key === \"Backspace\")) {\n // The keyboard's clear: the \"×\" is out of the tab order (see `clearable`).\n e.preventDefault();\n clear();\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n // `peer` dims the floating label with a natively disabled trigger, as Select's.\n \"peer pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n inert && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {flags && code && <CurrencyFlag country={code.toLowerCase()} />}\n {/* A long name (\"Saint Vincent and the Grenadines\") truncates rather than\n widening the field: the kit is checked at 390px. From Large up it wraps and\n the trigger grows instead (text size §10.17, round 0.34 T1). */}\n <span\n id={valueId}\n className={cn(\"truncate-until-large\", selectedName === undefined && \"text-placeholder\")}\n >\n {selectedName ?? emptyText}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could never\n fire; the keys go to the trigger, where Delete and Backspace clear. A pointer\n shortcut, as on EntityCombobox. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={comboText.clear}\n onClick={(e) => {\n e.stopPropagation();\n clear();\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-placeholder hover:text-secondary\"\n >\n <X aria-hidden className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n );\n\n const control = locked ? (\n // Button's frame: the line under the trigger, or the Tooltip with a hidden copy for\n // `aria-describedby`. `items-stretch`: in the line's column the trigger keeps the\n // field's full width.\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"flex w-full items-stretch\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\n );\n\n const box =\n label !== undefined ? (\n <FloatingField htmlFor={triggerId} label={label} staticLabel hint={field.labelHint}>\n {control}\n </FloatingField>\n ) : (\n // No label line to ride: a \"?\" goes at the field's end, outside the box, as on an\n // unlabelled Select.\n <EndHintRow hint={field.labelHint}>\n <div className=\"relative\">{control}</div>\n </EndHintRow>\n );\n\n return (\n // `rest` dresses the wrapper — a `data-tour` anchor, a test id. The name, the\n // description and the invalid state are on the trigger above.\n <div {...rest} className={className}>\n {box}\n {field.below}\n <ComboboxPanel\n // The core with the rows ranked above in place of its own filter's.\n core={{ ...core, results }}\n listboxId={listboxId}\n // On a phone the list is a full-screen sheet, which needs the field's own\n // name to say what it is asking for (live #200).\n sheetTitle={label ?? text.country}\n searchPlaceholder={text.search}\n isSelected={(v) => v === code}\n onChoose={choose}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n />\n </div>\n );\n}\n"],"mappings":"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","names":["code"]}
@@ -1,10 +1,11 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode, Ref } from "react";
2
+ import { type FieldHelpProps } from "./field-parts.js";
2
3
  import { CURRENCIES, CurrencyFlag, currencyName, getCurrency, type CurrencyFlagProps, type CurrencyOption } from "./currency-option-list.js";
3
4
  export { CURRENCIES, CurrencyFlag, currencyName, getCurrency };
4
5
  export type { CurrencyFlagProps, CurrencyOption };
5
6
  /** `onChange` is the kit's — "a currency was picked", carrying the code — rather
6
7
  * than the div's form event. */
7
- export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
8
+ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
8
9
  value: string;
9
10
  onChange: (code: string) => void;
10
11
  placeholder?: string;
@@ -70,4 +71,4 @@ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div"
70
71
  * calls `focus()` on it (0.34, API-3, refactor audit 2026-10). Not the wrapper. */
71
72
  ref?: Ref<HTMLButtonElement>;
72
73
  }
73
- export declare function CurrencySelect({ value, onChange, placeholder, className, label, options, invalid, error, hint, labels, currencyNames, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ref, ...rest }: CurrencySelectProps): import("react").JSX.Element;
74
+ export declare function CurrencySelect({ value, onChange, placeholder, className, label, options, invalid, error, hint, help, labels, currencyNames, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ref, ...rest }: CurrencySelectProps): import("react").JSX.Element;
@@ -29,6 +29,7 @@ function CurrencySelect({
29
29
  invalid,
30
30
  error,
31
31
  hint,
32
+ help,
32
33
  labels,
33
34
  currencyNames,
34
35
  "aria-label": ariaLabel,
@@ -51,7 +52,15 @@ function CurrencySelect({
51
52
  const locale = useKitLocale();
52
53
  const selected = getCurrency(value);
53
54
  const listboxId = `${useId()}-listbox`;
54
- const field = useFieldMessages({ hint, error, invalid, describedBy: ariaDescribedBy, ariaInvalid });
55
+ const field = useFieldMessages({
56
+ hint,
57
+ help,
58
+ name: label ?? ariaLabel,
59
+ error,
60
+ invalid,
61
+ describedBy: ariaDescribedBy,
62
+ ariaInvalid
63
+ });
55
64
  const { labelHint, describedBy, isInvalid } = field;
56
65
  const fieldName = typeof label === "string" ? label : text.currency;
57
66
  const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : placeholder ?? text.currency);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useCallback, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { FieldChevron, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./field-base\";\nimport { FieldLabel } from \"./field-parts\";\nimport { assignRef, EndHintRow, useFieldMessages } from \"./field-parts\";\nimport { cn } from \"../lib/cn\";\nimport { useComboboxCore } from \"./combobox-core\";\nimport {\n CURRENCIES,\n CurrencyFlag,\n CurrencyOptionList,\n currencyName,\n getCurrency,\n type CurrencyFlagProps,\n type CurrencyOption,\n} from \"./currency-option-list\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\n// The data and the flag are declared beside the one list both currency pickers render\n// (./currency-option-list, round 0.34 Wave 2, DUP-4) and are this module's exports, as\n// they always were.\nexport { CURRENCIES, CurrencyFlag, currencyName, getCurrency };\nexport type { CurrencyFlagProps, CurrencyOption };\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /**\n * What is wrong with the choice, in the caller's own words — {@link Input}'s `error`\n * (keksdose K4: of the kit's pickers this one had neither a message nor a caption, so\n * \"the account is in EUR\" sat in a loose `<p>` beside it that nothing pointed at).\n * Rendered under the field, after a text `hint`; merged into the trigger's\n * `aria-describedby`; implies `invalid`. `null`, `false` and `\"\"` are no message.\n */\n error?: ReactNode;\n /**\n * {@link Select}'s `hint`, the same rule (keksdose K4). A {@link FieldHint} \"?\" rides\n * the label's line; on an unlabelled picker it sits at the field's end edge, outside\n * the box. Plain TEXT (a string or a number) is a caption UNDER the field, attached\n * to the trigger through `aria-describedby` — the label line has no room for a\n * sentence.\n */\n hint?: ReactNode;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n /** The TRIGGER — the focusable `<button>` — as a React 19 ref prop, as on\n * {@link MultiSelect} and {@link CountrySelect}: react-hook-form's focus-on-error\n * calls `focus()` on it (0.34, API-3, refactor audit 2026-10). Not the wrapper. */\n ref?: Ref<HTMLButtonElement>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n error,\n hint,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n ref,\n ...rest\n}: CurrencySelectProps) {\n // The picker family's foundation (round 0.34 Wave 2, DUP-4): open state, the query,\n // the highlighted row, portal anchoring, outside press, Escape on the kit's stack and\n // Back — what `useDropdownSearch` gave it, and the rest it did not. `filter: false`:\n // the list's own currency filter hands the panel its rows.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n const { open, setOpen, triggerRef, closeToTrigger } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both, as on MultiSelect.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n\n const listboxId = `${useId()}-listbox`;\n // Text is a caption under the field; anything else (a FieldHint) rides the label\n // line — and a FieldHint is that caption at Large and on touch (§4). See `hint`. On\n // the TRIGGER, which is the control with a role: a caller's description used to land\n // on the wrapper `<div>` through the spread, where nothing reads it. The caller's ids\n // first, then the caption, then the error — Select's order, through useFieldMessages\n // since 0.34 (DUP-2, DUP-8).\n const field = useFieldMessages({ hint, error, invalid, describedBy: ariaDescribedBy, ariaInvalid });\n const { labelHint, describedBy, isInvalid } = field;\n\n // The trigger's NAME. A combobox does not take its name from its content, and the\n // floating label is a <span>, not a <label for> — so without `aria-label` from the\n // caller this trigger had no name at all (keksdose's test suite, 0.5.0). Composed the\n // way MultiSelect and the entity comboboxes compose theirs, \"label: value\", with the\n // `currency` word standing in for a missing label so it is never nameless.\n const fieldName = typeof label === \"string\" ? label : text.currency;\n const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : (placeholder ?? text.currency));\n\n const trigger = (\n <button\n // Where Escape on the open list hands focus back to (round 0.34 BUG-1).\n ref={setTrigger}\n type=\"button\"\n aria-label={name}\n onClick={() => setOpen((v) => !v)}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and Space\n // already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout. And since Wave 2 the popup\n // IS the listbox it promises, of options with the chosen one selected (BUG-12).\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={isInvalid || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_TRIGGER, \"pe-9\", label !== undefined && FIELD_FLOATING_PAD, isInvalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-primary\" : \"text-placeholder\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n );\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined &&\n (labelHint === undefined ? (\n <FieldLabel>{label}</FieldLabel>\n ) : (\n // The label and its \"?\" as one row on the label's line, as FloatingField lays\n // out a static label with a hint: the row places both, the label truncates\n // and the hint keeps its width. `z-10` like FieldLabel's own: the trigger is\n // `relative` and comes later, so it would paint over the strip.\n <div className=\"pointer-events-none absolute inset-x-3 top-1 z-10 flex items-center gap-1\">\n <FieldLabel className=\"static z-auto min-w-0 max-w-none\">{label}</FieldLabel>\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{labelHint}</span>\n </div>\n ))}\n {/* No label line to ride: at the field's end edge, outside the box, as an\n unlabelled Select places its FieldHint. */}\n <EndHintRow hint={label === undefined ? labelHint : undefined}>{trigger}</EndHintRow>\n {field.below}\n {/* Portalled to <body> and anchored to the trigger (dev#548), so where it stands\n in the flow no longer moves it. */}\n <CurrencyOptionList\n core={core}\n listboxId={listboxId}\n value={value}\n codes={options}\n currencyNames={currencyNames}\n locale={locale}\n searchPlaceholder={text.search}\n // On a phone the list is a full-screen sheet, which needs the field's own name\n // to say what it asks for (live #200).\n sheetTitle={label ?? text.currency}\n onChoose={(code) => {\n onChange(code);\n // Back to the trigger: the panel holding focus is about to unmount, and a\n // keyboard user who has answered the field should stand on it, ready to Tab on.\n closeToTrigger();\n }}\n />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/currency-select.tsx"],"sourcesContent":["import { useCallback, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref } from \"react\";\nimport { FieldChevron, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from \"./field-base\";\nimport { FieldLabel } from \"./field-parts\";\nimport { assignRef, EndHintRow, useFieldMessages, type FieldHelpProps } from \"./field-parts\";\nimport { cn } from \"../lib/cn\";\nimport { useComboboxCore } from \"./combobox-core\";\nimport {\n CURRENCIES,\n CurrencyFlag,\n CurrencyOptionList,\n currencyName,\n getCurrency,\n type CurrencyFlagProps,\n type CurrencyOption,\n} from \"./currency-option-list\";\nimport {\n DEFAULT_COMMON_LABELS,\n DEFAULT_CURRENCY_LABELS,\n useKitLabels,\n useKitLocale,\n} from \"../i18n/kit-labels\";\n\n// The data and the flag are declared beside the one list both currency pickers render\n// (./currency-option-list, round 0.34 Wave 2, DUP-4) and are this module's exports, as\n// they always were.\nexport { CURRENCIES, CurrencyFlag, currencyName, getCurrency };\nexport type { CurrencyFlagProps, CurrencyOption };\n\n/** `onChange` is the kit's — \"a currency was picked\", carrying the code — rather\n * than the div's form event. */\nexport interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\">, FieldHelpProps {\n value: string;\n onChange: (code: string) => void;\n placeholder?: string;\n label?: ReactNode;\n /**\n * Restrict the pickable currencies to these ISO codes (e.g. only the\n * currencies actually present in a budget). Order is preserved. Defaults to\n * the full {@link CURRENCIES} list.\n */\n options?: string[];\n /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the\n * native `Select` has worn since feedback #235. Every field-styled control in\n * this package carries it now, so a form can mark any of its fields rather than\n * only the one that happened to have it first. */\n invalid?: boolean;\n /**\n * What is wrong with the choice, in the caller's own words — {@link Input}'s `error`\n * (keksdose K4: of the kit's pickers this one had neither a message nor a caption, so\n * \"the account is in EUR\" sat in a loose `<p>` beside it that nothing pointed at).\n * Rendered under the field, after a text `hint`; merged into the trigger's\n * `aria-describedby`; implies `invalid`. `null`, `false` and `\"\"` are no message.\n */\n error?: ReactNode;\n /**\n * {@link Select}'s `hint`, the same rule (keksdose K4). A {@link FieldHint} \"?\" rides\n * the label's line; on an unlabelled picker it sits at the field's end edge, outside\n * the box. Plain TEXT (a string or a number) is a caption UNDER the field, attached\n * to the trigger through `aria-describedby` — the label line has no room for a\n * sentence.\n */\n hint?: ReactNode;\n /** Every user-facing string this control owns, shaped like `AmountInput`'s\n * `labels` so the two currency pickers in the kit are configured the same way.\n *\n * `search` exists because `placeholder` used to do BOTH jobs: it named the empty\n * field and, for want of anything else, it was also handed to the search box —\n * so a caller who wrote \"Currency\" got \"Currency\" as the prompt for typing a\n * filter, and a caller who wanted to name the filter had to rename the field.\n * `placeholder` still wins for the trigger, so nothing moves for the callers\n * that set it; what changes is that the search box no longer borrows it. */\n labels?: {\n /** The empty trigger, when `placeholder` is not given. */\n currency?: string;\n /** The filter box inside the open panel. */\n search?: string;\n };\n /**\n * Translated currency names, keyed by ISO code — `{ CHF: \"Schweizer Franken\" }`.\n *\n * {@link CURRENCIES} is data the package ships, so its 27 names were the one\n * string in the row a consumer could not translate: the code and the symbol are\n * language-neutral, and the name beside them stayed English in a German UI. This\n * is a sparse OVERRIDE rather than a replacement list, so an app translates the\n * eight currencies it actually uses and keeps the rest; forking `CURRENCIES` to\n * get there would also fork the flags and symbols, which nobody wants to maintain.\n *\n * The search matches what is SHOWN — type \"Franken\" and you find CHF — because a\n * filter that only matches the hidden English name is worse than no filter.\n */\n currencyNames?: Record<string, string>;\n /** The TRIGGER — the focusable `<button>` — as a React 19 ref prop, as on\n * {@link MultiSelect} and {@link CountrySelect}: react-hook-form's focus-on-error\n * calls `focus()` on it (0.34, API-3, refactor audit 2026-10). Not the wrapper. */\n ref?: Ref<HTMLButtonElement>;\n}\n\nexport function CurrencySelect({\n value,\n onChange,\n placeholder,\n className,\n label,\n options,\n invalid,\n error,\n hint,\n help,\n labels,\n currencyNames,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n ref,\n ...rest\n}: CurrencySelectProps) {\n // The picker family's foundation (round 0.34 Wave 2, DUP-4): open state, the query,\n // the highlighted row, portal anchoring, outside press, Escape on the kit's stack and\n // Back — what `useDropdownSearch` gave it, and the rest it did not. `filter: false`:\n // the list's own currency filter hands the panel its rows.\n const core = useComboboxCore<string>({ filter: false, backCloses: true });\n const { open, setOpen, triggerRef, closeToTrigger } = core;\n // The core anchors the panel to the trigger and hands focus back to it; the caller's\n // `ref` wants the same element. One callback feeds both, as on MultiSelect.\n const setTrigger = useCallback(\n (el: HTMLButtonElement | null) => {\n triggerRef.current = el;\n assignRef(ref, el);\n },\n [ref, triggerRef],\n );\n // `labels` > `currency` from the provider > English.\n const text = useKitLabels(\"currency\", DEFAULT_CURRENCY_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n\n const selected = getCurrency(value);\n\n const listboxId = `${useId()}-listbox`;\n // Text is a caption under the field; anything else (a FieldHint) rides the label\n // line — and a FieldHint is that caption at Large and on touch (§4). See `hint`. On\n // the TRIGGER, which is the control with a role: a caller's description used to land\n // on the wrapper `<div>` through the spread, where nothing reads it. The caller's ids\n // first, then the caption, then the error — Select's order, through useFieldMessages\n // since 0.34 (DUP-2, DUP-8).\n const field = useFieldMessages({\n hint,\n help,\n name: label ?? ariaLabel,\n error,\n invalid,\n describedBy: ariaDescribedBy,\n ariaInvalid,\n });\n const { labelHint, describedBy, isInvalid } = field;\n\n // The trigger's NAME. A combobox does not take its name from its content, and the\n // floating label is a <span>, not a <label for> — so without `aria-label` from the\n // caller this trigger had no name at all (keksdose's test suite, 0.5.0). Composed the\n // way MultiSelect and the entity comboboxes compose theirs, \"label: value\", with the\n // `currency` word standing in for a missing label so it is never nameless.\n const fieldName = typeof label === \"string\" ? label : text.currency;\n const name = ariaLabel ?? common.fieldValue(fieldName, selected ? selected.code : (placeholder ?? text.currency));\n\n const trigger = (\n <button\n // Where Escape on the open list hands focus back to (round 0.34 BUG-1).\n ref={setTrigger}\n type=\"button\"\n aria-label={name}\n onClick={() => setOpen((v) => !v)}\n // Down/Up opens the list from the closed trigger, per the APG; Enter and Space\n // already do through the button's own click.\n onKeyDown={(e) => {\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n // The implicit `button` role supports neither `aria-expanded` nor\n // `aria-invalid`, so this trigger previously wore a red border and announced\n // nothing about being invalid or about there being a list behind it. The role\n // that DESCRIBES it is the one that also supports the attributes — the same\n // treatment its siblings (MultiSelect, the entity comboboxes) now carry, so a\n // form row of pickers reads the same way throughout. And since Wave 2 the popup\n // IS the listbox it promises, of options with the chosen one selected (BUG-12).\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-controls={listboxId}\n aria-expanded={open}\n aria-invalid={isInvalid || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_TRIGGER, \"pe-9\", label !== undefined && FIELD_FLOATING_PAD, isInvalid && FIELD_INVALID)}\n >\n {/* The trigger stays COMPACT — flag + code only — so it never clips in a\n narrow field; the full names live in the (wider) popup (feedback\n #308). */}\n <span className={cn(\"flex items-center gap-2 min-w-0\", selected ? \"text-primary\" : \"text-placeholder\")}>\n {selected ? (\n <>\n <CurrencyFlag country={selected.country} />\n <span className=\"font-medium\">{selected.code}</span>\n </>\n ) : (\n placeholder ?? text.currency\n )}\n </span>\n <FieldChevron />\n </button>\n );\n\n return (\n // The wrapper takes `rest`; the trigger takes the name. This field's label is a\n // floating <span> rather than a <label for>, so a caller who wants the control to\n // announce more than the code inside it has nowhere else to put it.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined &&\n (labelHint === undefined ? (\n <FieldLabel>{label}</FieldLabel>\n ) : (\n // The label and its \"?\" as one row on the label's line, as FloatingField lays\n // out a static label with a hint: the row places both, the label truncates\n // and the hint keeps its width. `z-10` like FieldLabel's own: the trigger is\n // `relative` and comes later, so it would paint over the strip.\n <div className=\"pointer-events-none absolute inset-x-3 top-1 z-10 flex items-center gap-1\">\n <FieldLabel className=\"static z-auto min-w-0 max-w-none\">{label}</FieldLabel>\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{labelHint}</span>\n </div>\n ))}\n {/* No label line to ride: at the field's end edge, outside the box, as an\n unlabelled Select places its FieldHint. */}\n <EndHintRow hint={label === undefined ? labelHint : undefined}>{trigger}</EndHintRow>\n {field.below}\n {/* Portalled to <body> and anchored to the trigger (dev#548), so where it stands\n in the flow no longer moves it. */}\n <CurrencyOptionList\n core={core}\n listboxId={listboxId}\n value={value}\n codes={options}\n currencyNames={currencyNames}\n locale={locale}\n searchPlaceholder={text.search}\n // On a phone the list is a full-screen sheet, which needs the field's own name\n // to say what it asks for (live #200).\n sheetTitle={label ?? text.currency}\n onChoose={(code) => {\n onChange(code);\n // Back to the trigger: the panel holding focus is about to unmount, and a\n // keyboard user who has answered the field should stand on it, ready to Tab on.\n closeToTrigger();\n }}\n />\n </div>\n );\n}\n"],"mappings":";AAwMU,mBACE,KADF;AAxMV,SAAS,aAAa,aAAa;AAEnC,SAAS,cAAc,eAAe,eAAe,0BAA0B;AAC/E,SAAS,kBAAkB;AAC3B,SAAS,WAAW,YAAY,wBAA6C;AAC7E,SAAS,UAAU;AACnB,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA6EA,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB;AAAA,EACA,GAAG;AACL,GAAwB;AAKtB,QAAM,OAAO,gBAAwB,EAAE,QAAQ,OAAO,YAAY,KAAK,CAAC;AACxE,QAAM,EAAE,MAAM,SAAS,YAAY,eAAe,IAAI;AAGtD,QAAM,aAAa;AAAA,IACjB,CAAC,OAAiC;AAChC,iBAAW,UAAU;AACrB,gBAAU,KAAK,EAAE;AAAA,IACnB;AAAA,IACA,CAAC,KAAK,UAAU;AAAA,EAClB;AAEA,QAAM,OAAO,aAAa,YAAY,yBAAyB,MAAM;AACrE,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,SAAS,aAAa;AAE5B,QAAM,WAAW,YAAY,KAAK;AAElC,QAAM,YAAY,GAAG,MAAM,CAAC;AAO5B,QAAM,QAAQ,iBAAiB;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,MAAM,SAAS;AAAA,IACf;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,EACF,CAAC;AACD,QAAM,EAAE,WAAW,aAAa,UAAU,IAAI;AAO9C,QAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,KAAK;AAC3D,QAAM,OAAO,aAAa,OAAO,WAAW,WAAW,WAAW,SAAS,OAAQ,eAAe,KAAK,QAAS;AAEhH,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MAEC,KAAK;AAAA,MACL,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,MAGhC,WAAW,CAAC,MAAM;AAChB,YAAI,EAAE,QAAQ,eAAe,EAAE,QAAQ,WAAW;AAChD,YAAE,eAAe;AACjB,kBAAQ,IAAI;AAAA,QACd;AAAA,MACF;AAAA,MAQA,MAAK;AAAA,MACL,iBAAc;AAAA,MACd,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,gBAAc,aAAa;AAAA,MAC3B,oBAAkB;AAAA,MAClB,WAAW,GAAG,eAAe,QAAQ,UAAU,UAAa,oBAAoB,aAAa,aAAa;AAAA,MAK1G;AAAA,4BAAC,UAAK,WAAW,GAAG,mCAAmC,WAAW,iBAAiB,kBAAkB,GAClG,qBACC,iCACE;AAAA,8BAAC,gBAAa,SAAS,SAAS,SAAS;AAAA,UACzC,oBAAC,UAAK,WAAU,eAAe,mBAAS,MAAK;AAAA,WAC/C,IAEA,eAAe,KAAK,UAExB;AAAA,QACA,oBAAC,gBAAa;AAAA;AAAA;AAAA,EAChB;AAGF;AAAA;AAAA;AAAA;AAAA,IAIE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,YAAY,SAAS,GAC/C;AAAA,gBAAU,WACR,cAAc,SACb,oBAAC,cAAY,iBAAM;AAAA;AAAA;AAAA;AAAA;AAAA,QAMnB,qBAAC,SAAI,WAAU,6EACb;AAAA,8BAAC,cAAW,WAAU,oCAAoC,iBAAM;AAAA,UAChE,oBAAC,UAAK,WAAU,kDAAkD,qBAAU;AAAA,WAC9E;AAAA;AAAA,MAIJ,oBAAC,cAAW,MAAM,UAAU,SAAY,YAAY,QAAY,mBAAQ;AAAA,MACvE,MAAM;AAAA,MAGP;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA,mBAAmB,KAAK;AAAA,UAGxB,YAAY,SAAS,KAAK;AAAA,UAC1B,UAAU,CAAC,SAAS;AAClB,qBAAS,IAAI;AAGb,2BAAe;AAAA,UACjB;AAAA;AAAA,MACF;AAAA,OACF;AAAA;AAEJ;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import type { ComponentPropsWithoutRef, ReactNode, Ref, RefObject } from "react";
2
2
  import { type DatePickerLabels, type KitDateFormatContext } from "../i18n/kit-labels.js";
3
+ import { type FieldHelpProps } from "./field-parts.js";
3
4
  import type { RemLength } from "../hooks/use-breakpoint.js";
4
5
  import type { MiniCalendarProps } from "./mini-calendar.js";
5
6
  /**
@@ -11,7 +12,7 @@ import type { MiniCalendarProps } from "./mini-calendar.js";
11
12
  * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own
12
13
  * (date-picker.tsx, date-range-picker.tsx).
13
14
  */
14
- export interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
15
+ export interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange">, FieldHelpProps {
15
16
  /** BCP 47 tag for the trigger's date and the calendar. Falls back to
16
17
  * `<UiKitProvider locale>`, then to the runtime's default. */
17
18
  locale?: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/date-field.tsx"],"sourcesContent":["// The field both date pickers draw: the trigger, the popover or phone sheet it opens,\n// the clear button — and the props, label hook and trigger format the two share.\n// Moved out of date-picker.tsx as it was (MOD-9, refactor audit 2026-10); date-picker\n// still exports the one public name here, `DateTriggerAttributes`.\nimport { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref, RefObject } from \"react\";\nimport { Calendar, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { formatIsoDate } from \"../lib/dates\";\nimport {\n DEFAULT_DATE_PICKER_LABELS,\n DEFAULT_PICKER_SHEET_LABELS,\n useKitDateFormatter,\n useKitLabels,\n type DatePickerLabels,\n type KitDateFormatContext,\n} from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { assignRef, FieldLabelLine, LABEL_ABOVE_BOX } from \"./field-parts\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport type { RemLength } from \"../hooks/use-breakpoint\";\nimport type { MiniCalendarProps } from \"./mini-calendar\";\nimport { Popover } from \"./popover\";\n\n/**\n * What both pickers share — and, through `ComponentPropsWithoutRef<\"div\">`, everything a\n * `<div>` takes, so a `data-tour` anchor, a test id or an `aria-describedby` reaches the\n * field's root element instead of being dropped (audit §api-design).\n *\n * `onChange` is omitted from those attributes: the DOM event of that name carries a\n * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own\n * (date-picker.tsx, date-range-picker.tsx).\n */\nexport interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** BCP 47 tag for the trigger's date and the calendar. Falls back to\n * `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n placeholder?: string;\n /** Inclusive ISO bounds for selectable days. */\n min?: string;\n max?: string;\n /** Show an X to clear the value (emits `\"\"`). */\n clearable?: boolean;\n /** Accessible name of the clear button. Wins over `datePicker.clear` from\n * `<UiKitProvider labels>`; kept as a prop because it predates the provider. */\n clearLabel?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /**\n * Standing advice for the field (keksdose K4, \"uniform field anatomy\"), as on a\n * {@link Select}: plain TEXT is a caption under the field, attached with\n * `aria-describedby`; a {@link FieldHint} rides the label line (or, unlabelled, sits\n * at the field's end, outside the box).\n */\n hint?: ReactNode;\n /**\n * What is wrong with the date, in the caller's words (\"The due date is before the\n * invoice date\") — {@link Input}'s `error` (keksdose K4: DateField errors had no slot).\n * Rendered under the field (under the whole row, with `step` / `today`), attached to\n * the trigger with `aria-describedby` — merged with any the caller passed, after the\n * caption — and implies `invalid`. Passing the key at all keeps the field's box while\n * there is no message, so the trigger is never remounted as one comes and goes.\n */\n error?: ReactNode;\n className?: string;\n /** Intl options for the trigger's formatted date (default: locale short date). Win\n * over the provider's `formatDate`. */\n formatOptions?: Intl.DateTimeFormatOptions;\n /** Render the trigger's text for an ISO date yourself; wins over `formatOptions` and\n * over the provider's `formatDate`.\n * For a host whose date rendering is more than one `Intl` call can say — Keksdose\n * puts the weekday's name in the UI language beside digits ordered by a separate\n * format preference, two locales in one string. `locale` still drives the calendar.\n * Set that rendering once with `<UiKitProvider formatDate>` (keksdose K12) and this\n * is for the one field that differs. */\n formatValue?: (iso: string) => string;\n /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are\n * icon-only, so their `aria-label` is the only name they have — and the package\n * ships English defaults, which a translating host has to be able to replace.\n * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */\n calendarLabels?: MiniCalendarProps[\"labels\"];\n /**\n * Make the panel's caption (\"July 2026\") a button that opens the year's twelve\n * months, with ‹ › stepping a year; picking a month returns to its days. Far dates\n * stop being a march of month arrows.\n *\n * kastlan is why: its service-charge period runs July to June, and with one month on\n * screen and ‹ › the only way across, the end of the period was eleven clicks past\n * the start — so kastlan kept two date fields instead of `RhfDateRangePicker`. With\n * the jump the whole period is open, caption, Jul, 1, caption, next year, Jun, 30.\n *\n * What it adds, for the keyboard and a screen reader:\n * - the caption is a disclosure button (`aria-expanded`). Its name is the month it\n * shows, so speech input can say what it sees; it is `aria-live`, as the\n * calendar's caption was, so paging months still says where you landed; and it is\n * described by `monthPicker.panel` (\"Choose a month\").\n * - the month grid is `MonthPicker`'s grid: one tab stop, arrows walk the months\n * (across the year's edge), Home/End the row, PageUp/PageDown a year, Enter or a\n * click picks. Focus opens on the month on show and comes back on its days — the\n * selected day if it is in that month, else the 1st. Months outside `min`/`max`\n * are `aria-disabled`, and a year arrow is once the whole year beyond is.\n * - Escape in the month grid goes back to the days without closing the panel (focus\n * on the caption); in the days, Escape closes the panel as before.\n *\n * No new strings: the month arrows keep `miniCalendar.previousMonth` / `nextMonth`\n * (`calendarLabels`), and the month grid speaks with `MonthPicker`'s words —\n * `monthPicker.previousYear` / `nextYear` / `month` / `panel` — because it is that\n * grid, and a host that translated it once should not have to again.\n *\n * Off by default: the caption becomes one more stop in the panel's tab order, which a\n * 0.x minor does not spring on a form. Works in the popover and in\n * `DateRangePicker`'s phone sheet alike, beside presets and in `commit=\"apply\"`\n * (a preset picked while the months are open goes back to the days, on its start).\n *\n * Why not a two-month panel (`months={2}`) instead, or beside it: for a twelve-month\n * period it still pages six times where this takes one year step, and two\n * `MiniCalendar`s side by side each follow the value on their own — picking the end\n * in the right-hand month would pull the left one onto it — so the calendar would\n * have to learn to share a month first. Not worth that for a saving of one arrow on a\n * range across a single month boundary.\n */\n monthJump?: boolean;\n /**\n * The TRIGGER — the focusable button that opens the calendar, a `renderTrigger` one\n * too — as a React 19 ref prop (0.34, API-3, refactor audit 2026-10). The field took\n * none, so react-hook-form's focus-on-error reached it only through /rhf's id lookup,\n * and an app not at all. Not the wrapper, which takes the rest of the props.\n */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * The trigger's date renderer, in the kit's order of precedence: the picker's own\n * `formatValue`, then its `formatOptions` (an explicit `Intl` look for this field),\n * then the provider's `formatDate` (keksdose K12), then the locale's short date. A\n * provider formatter that answers `\"\"` falls through to the default for that date.\n */\nexport function useTriggerFormat(\n source: KitDateFormatContext[\"source\"],\n locale: string | undefined,\n formatOptions: Intl.DateTimeFormatOptions | undefined,\n formatValue: ((iso: string) => string) | undefined,\n): (iso: string) => string {\n const fromProvider = useKitDateFormatter();\n if (formatValue) return formatValue;\n return (iso) => {\n if (formatOptions === undefined && fromProvider) {\n // A single date stands alone, where the weekday helps; a range's two ends do not.\n const text = fromProvider(iso, { unit: \"day\", source, locale, weekday: source === \"datePicker\" });\n if (text) return text;\n }\n // No prop and no provider leaves `locale` undefined: the runtime's default (DUP-11).\n return formatIsoDate(iso, locale, formatOptions);\n };\n}\n\n/**\n * Everything the kit's own trigger button carries, handed to a custom one\n * ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread\n * it WHOLE: the ref is how focus comes back when the panel closes, the id pair and\n * `aria-labelledby` are the 0.5.1 naming (an external `<label htmlFor>`, a form\n * library's `aria-describedby`), and `role`/`aria-haspopup`/`aria-controls`/\n * `aria-expanded` are what tell a screen reader this opens a calendar and whether it\n * is open. `className` is the kit's field look (42px with a label, `pe-9` for the\n * glyph the field draws at its end) — merge yours after it with `cn`.\n */\nexport interface DateTriggerAttributes {\n ref: RefObject<HTMLButtonElement | null>;\n type: \"button\";\n id?: string;\n role: \"combobox\";\n \"aria-haspopup\": \"dialog\";\n \"aria-controls\": string;\n \"aria-expanded\": boolean;\n \"aria-labelledby\"?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n /** The field's required state — `Field required` hands it down with the id pair. */\n \"aria-required\"?: boolean | \"true\" | \"false\";\n disabled?: boolean;\n onClick: () => void;\n className: string;\n}\n\n/** A custom trigger, as `DateField` calls it; each picker adapts it to its own props. */\ntype TriggerRenderer = (attrs: DateTriggerAttributes, valueId: string) => ReactNode;\n\n/** A caller's trigger, as an element of its own: the attributes carry the trigger's\n * ref, and a ref is handed to a child as a prop, not passed to a function in the\n * middle of the parent's render. */\nfunction CustomTrigger({\n render,\n attrs,\n valueId,\n}: {\n render: TriggerRenderer;\n attrs: DateTriggerAttributes;\n valueId: string;\n}) {\n return render(attrs, valueId);\n}\n\n/**\n * The trigger's value wraps instead of truncating at Large and Extra large (0.32.1, the\n * 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing truncates\"). A\n * range (\"01/09/2026 – 30/09/2026\") is 312 px at 150 %, and on a 360 px phone it read\n * \"01/09/2026 – 30/…\" — its end, the half a reader needs, was the half cut off. A\n * `large:` class, so Normal keeps its one-line field: since 0.33 (§10.17) the kit's one\n * `truncate-until-large` (tokens.css) in place of this file's own `truncate` +\n * `large:whitespace-normal large:break-words`.\n */\nconst VALUE_TRUNCATE = \"truncate-until-large\";\n\n/**\n * The field-shaped button that opens the calendar.\n *\n * A component of its own rather than JSX inline in `Popover`'s `trigger` render prop,\n * because handing focus back to it is an effect and a render prop is not somewhere a\n * hook can go.\n */\nfunction DateFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n padded,\n disabled,\n invalid,\n required,\n panelId,\n aria,\n renderTrigger,\n fieldRef,\n}: {\n /** The caller's naming/description attributes, routed here from the field. */\n aria: TriggerAria;\n /** Draw the button yourself, with these attributes — see {@link DateTriggerAttributes}. */\n renderTrigger?: TriggerRenderer;\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n /** The picker's own `ref` prop (API-3) — the same element as `triggerRef`. */\n fieldRef?: Ref<HTMLButtonElement>;\n /** The panel's id — `role=\"combobox\"` requires `aria-controls`. */\n panelId: string;\n triggerText: string;\n hasValue: boolean;\n /** ids of the hidden label twin + the value span, in the order they are spoken. */\n labelledBy?: string;\n valueId: string;\n padded: boolean;\n disabled?: boolean;\n invalid?: boolean;\n /** `aria-required`, on the trigger with the rest of the field's ARIA. */\n required?: boolean | \"true\" | \"false\";\n}) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when the panel still HAD focus. The day the user was standing on has just\n // been unmounted with the panel, which drops focus to <body> and leaves a\n // keyboard user back at the top of the document with no idea where they were.\n // But an outside click closes this panel too, and that click has already given\n // focus to whatever it landed on — dragging it back here would be worse than the\n // bug, so a focus that is somewhere real is left alone.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n // Here rather than through `useFocusTrap`'s `restoreFocus`, and not only because\n // `Popover` runs no trap to carry it: that option restores to whatever held focus\n // when the panel opened, and on Safari — and on Firefox on macOS — clicking a\n // button does not focus it, so \"whatever held focus\" is the page. The requirement\n // is this trigger, every time, however the panel was opened.\n }, [open, triggerRef]);\n // The caller's `ref` (API-3, refactor audit 2026-10) is handed the trigger once it is\n // mounted, rather than merged into `attrs.ref`: that one is a `RefObject` by contract\n // (`DateTriggerAttributes`), which a `renderTrigger` button may read, and this way a\n // caller's own trigger is covered as well as the kit's.\n useLayoutEffect(() => {\n if (!fieldRef) return;\n assignRef(fieldRef, triggerRef.current);\n return () => assignRef(fieldRef, null);\n }, [fieldRef, triggerRef]);\n\n // One attribute object, worn either by the kit's own button below or by a caller's\n // (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the\n // naming and state this one announces.\n const isInvalid = Boolean(invalid) || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\";\n const attrs: DateTriggerAttributes = {\n ref: triggerRef,\n type: \"button\",\n disabled,\n onClick: toggle,\n // The name has to carry the VALUE. A bare `aria-label={label}` — which is what\n // this was — replaces the button's text outright, so the one field in the form\n // whose entire job is to show a date announced \"Due date\" and stopped there,\n // and a screen-reader user could not read back what they had picked without\n // opening the calendar and hunting for the selected day.\n //\n // Two ids rather than one composed string (`common.fieldValue`): \"label: value\"\n // is a sentence, and a name reference is spoken as the two texts in order with\n // no punctuation to translate at all. The hidden twin of the FieldLabel lives\n // in DateField, next to the label it copies.\n id: aria.id,\n \"aria-labelledby\": aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy,\n \"aria-label\": aria[\"aria-label\"],\n \"aria-describedby\": aria[\"aria-describedby\"],\n // `role=\"combobox\"` on a button that opens a calendar is the APG date-picker\n // shape, and it is what makes the next two lines legal: `button` supports\n // neither `aria-expanded` nor `aria-invalid`, so the previous markup set an\n // invalid state that announced nothing (ESLint's `role-supports-aria-props`).\n //\n // `aria-haspopup=\"dialog\"` is now honest — it was not when this comment first\n // said so. `Popover` gained a real `role=\"dialog\"` in the same wave, so the\n // trigger's promise and the panel's role finally agree. The phone sheet is a\n // `role=\"dialog\"` too, and carries the same id.\n role: \"combobox\",\n \"aria-haspopup\": \"dialog\",\n \"aria-controls\": panelId,\n \"aria-expanded\": open,\n \"aria-invalid\": isInvalid || undefined,\n \"aria-required\": required,\n className: cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n // An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.\n isInvalid && FIELD_INVALID,\n ),\n };\n if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;\n\n return (\n <button {...attrs}>\n <span id={valueId} className={cn(VALUE_TRUNCATE, !hasValue && \"text-placeholder\")}>\n {/* `|| \" \"` (a NON-BREAKING space) — triggerText is \"\" when there is no value and no\n placeholder was passed. An empty span has no line box, so the\n trigger collapses to its padding and sits shorter than every\n other field beside it.\n\n It has to be a NON-BREAKING space. This read `|| \" \"` first, and\n that fix did nothing at all: an ordinary space is collapsible\n white space, and white space at the start and end of a line is\n removed — so the span rendered with height 0 and the trigger\n stayed 22px against the 42px of the fields either side of it\n (Keksdose live #294, measured in the browser: `span.textContent`\n WAS `\" \"` and `getBoundingClientRect().height` was 0). U+00A0 is\n not collapsible, so it holds the line. */}\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n/**\n * How a panel's body and its action row are put on screen — the popover stacks them,\n * the phone sheet pins the actions in its footer. Handed to the panel rather than\n * applied around it because the actions belong to the panel's own draft state\n * (`RangePanel`), while the frame they sit in belongs to the field.\n */\nexport type PanelFrame = (body: ReactNode, actions?: ReactNode) => ReactNode;\n\nconst popoverFrame: PanelFrame = (body, actions) =>\n actions ? (\n <div className=\"flex flex-col gap-2\">\n {body}\n <div className=\"flex items-center justify-end gap-2 border-t border-subtle pt-2\">{actions}</div>\n </div>\n ) : (\n body\n );\n\n/** A field-styled trigger that opens a portalled MiniCalendar. Shared by the\n * single- and range-date pickers (date-picker.tsx, date-range-picker.tsx). */\nexport function DateField({\n triggerText,\n hasValue,\n label,\n disabled,\n clearable,\n clearLabel,\n panelLabel,\n className,\n width,\n onClear,\n invalid,\n sheet = false,\n sheetBackCloses = true,\n renderTrigger,\n labelHint,\n children,\n // The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id\n // pair, and a required date read as optional while it sat on the role-less div.\n // Taken here rather than in `splitTriggerAria`, which MonthPicker shares.\n \"aria-required\": ariaRequired,\n fieldRef,\n ...rest\n}: Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> & {\n /** The picker's `ref` prop: the trigger (API-3). */\n fieldRef?: Ref<HTMLButtonElement>;\n triggerText: string;\n hasValue: boolean;\n label?: ReactNode;\n /** A `FieldHint` for the label line, beside the label. */\n labelHint?: ReactNode;\n disabled?: boolean;\n clearable?: boolean;\n /** Already resolved by the picker (prop > provider > English). */\n clearLabel: string;\n /** The popover panel's accessible name, resolved likewise. */\n panelLabel: string;\n invalid?: boolean;\n className?: string;\n /** Popover panel width; omit for the default (a bare calendar). In rem so it grows\n * with the text size, like the Popover's own default (§3.2). */\n width?: number | RemLength;\n onClear: () => void;\n /** Open as a full-screen sheet instead of the popover — the picker decides when\n * (in the phone layout, `usePhoneLayout()`). */\n sheet?: boolean;\n /** The sheet's `FullBleedDialog backCloses`. */\n sheetBackCloses?: boolean;\n renderTrigger?: TriggerRenderer;\n children: (close: () => void, frame: PanelFrame) => ReactNode;\n}) {\n const showClear = Boolean(clearable && hasValue && !disabled);\n const [aria, wrapperRest] = splitTriggerAria(rest);\n // The panel is portalled to <body> and leaves the subtree whose `dir` it inherited,\n // so a calendar in an RTL form opened LTR — the grid's columns, its arrow keys and\n // its chevrons all the wrong way round. Read at the moment of opening (the only way\n // the popover opens is through `toggle`) and put back on the panel.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can be reused as a name; a ReactNode may be a whole row with\n // its own interactive \"?\" hint in it, and naming a button with that reads the hint\n // out as part of the field's name.\n const named = typeof label === \"string\";\n // The sheet's open flag. Only the phone branch owns one: on desktop `Popover` owns\n // it, so the two can never disagree about what is on screen.\n const [sheetOpen, setSheetOpen] = useState(false);\n const sheetTriggerRef = useRef<HTMLButtonElement>(null);\n const closeSheet = () => setSheetOpen(false);\n const sheetLabels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS);\n const sheetFrame: PanelFrame = (body, actions) => (\n <FullBleedDialog\n open\n onClose={closeSheet}\n // The same id the popover panel wears, so the trigger's `aria-controls` holds in\n // both presentations; named the way the popover is.\n id={panelId}\n aria-label={panelLabel}\n header={panelLabel}\n // `pickerSheet.close`: the one \"close this full-screen picker\" string the kit\n // already translates, rather than a second key saying the same thing.\n closeLabel={sheetLabels.close}\n backCloses={sheetBackCloses}\n dir={dir}\n // Portalled to <body>, the sheet is not inside the field's wrapper, so a\n // document-level \"outside click\" listener — a Popover or dropdown this field\n // sits in — would read a tap on a day as outside and unmount the cell before\n // its click (Keksdose dev#477, the fix `PickerSheet` carries).\n onMouseDown={(e) => e.stopPropagation()}\n footer={actions}\n >\n {body}\n </FullBleedDialog>\n );\n const fieldTrigger = ({\n open,\n toggle,\n ref,\n }: {\n open: boolean;\n toggle: () => void;\n ref: RefObject<HTMLButtonElement | null>;\n }) => (\n <DateFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={hasValue}\n // ALWAYS a name reference, never `undefined`. A `<button>` takes its\n // accessible name from its contents, so an unlabelled trigger used to\n // announce its own date text for free. `role=\"combobox\"` does not —\n // content is excluded from name computation for that role — so leaving\n // this undefined made an unlabelled date field announce nothing at all.\n // Named: \"label, value\". Unnamed: the value alone, which is what the\n // button was saying before.\n //\n // A caller's own reference comes first. And when the caller gives the field\n // an `id` (so an external `<label htmlFor>` can point at it), the trigger\n // lists ITSELF first: a self-reference in `aria-labelledby` is resolved\n // from the element's native label (accname 2B → 2D), so \"Due date\" from\n // that <label> is spoken before the value, instead of being overridden.\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 panelId={panelId}\n valueId={valueId}\n padded={label !== undefined}\n disabled={disabled}\n invalid={invalid}\n required={ariaRequired}\n renderTrigger={renderTrigger}\n fieldRef={fieldRef}\n />\n );\n return (\n // The caller's attributes land here, on the field's own box — the trigger inside is\n // named by `aria-labelledby` and must keep the id pair it is given.\n // At Large a labelled box is a grid: the label above, the calendar glyph and the\n // clear × measured against the trigger's row (LABEL_ABOVE_BOX, 0.33 §10.17).\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", label !== undefined && LABEL_ABOVE_BOX, className)}>\n {label !== undefined && <FieldLabelLine label={label} hint={labelHint} />}\n {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names\n nothing on its own — this hidden twin is what the trigger is named by.\n `aria-hidden` because a name reference reads a hidden element deliberately\n (accname §4.3.1), and without it the label would be announced twice: once as\n loose text before the button, once inside the button's own name.\n `sr-only-fixed` rather than `sr-only` — see the note on the class in\n tokens.css. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n {sheet ? (\n <>\n {fieldTrigger({ open: sheetOpen, toggle: () => setSheetOpen((v) => !v), ref: sheetTriggerRef })}\n {/* Mounted only while open, like the popover's children: the panel's draft is\n born with it, so every open starts again from the committed value. */}\n {sheetOpen && children(closeSheet, sheetFrame)}\n </>\n ) : (\n <Popover\n width={width}\n panelId={panelId}\n // Named for what it is. Unnamed, it fell back to `popover.panel`, and a date\n // field announced its calendar as \"Popover\" — in English, in every language.\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={fieldTrigger}\n >\n {(close) => children(close, popoverFrame)}\n </Popover>\n )}\n {showClear ? (\n <button\n type=\"button\"\n aria-label={clearLabel}\n onClick={onClear}\n className=\"absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-placeholder hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\"\n />\n )}\n </div>\n );\n}\n\n/**\n * The picker's own label props, folded into one `datePicker` override so they sit at\n * the top of the same precedence chain as everything else: prop > provider > English.\n * They predate the provider and stay public API, which is why they are not simply\n * replaced by a `labels` prop.\n */\nexport function useDatePickerLabels(\n clearLabel: string | undefined,\n stepLabels: { prev: string; next: string } | undefined,\n todayLabel: string | undefined,\n): DatePickerLabels {\n const prev = stepLabels?.prev;\n const next = stepLabels?.next;\n const fromProps = useMemo(() => {\n const out: Partial<DatePickerLabels> = {};\n if (clearLabel !== undefined) out.clear = clearLabel;\n if (prev !== undefined) out.previousDay = prev;\n if (next !== undefined) out.nextDay = next;\n if (todayLabel !== undefined) out.today = todayLabel;\n return out;\n }, [clearLabel, prev, next, todayLabel]);\n return useKitLabels(\"datePicker\", DEFAULT_DATE_PICKER_LABELS, fromProps);\n}\n\n/**\n * The caller's `hint` when it has no label line to ride: at the end of the field,\n * outside the box, as an unlabelled {@link Select} places its FieldHint (0.15.5 P8).\n */\nexport function withEndHint(field: ReactNode, hint: ReactNode): ReactNode {\n if (hint === undefined) return field;\n return (\n <div className=\"flex items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">{field}</div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/date-field.tsx"],"sourcesContent":["// The field both date pickers draw: the trigger, the popover or phone sheet it opens,\n// the clear button — and the props, label hook and trigger format the two share.\n// Moved out of date-picker.tsx as it was (MOD-9, refactor audit 2026-10); date-picker\n// still exports the one public name here, `DateTriggerAttributes`.\nimport { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode, Ref, RefObject } from \"react\";\nimport { Calendar, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { formatIsoDate } from \"../lib/dates\";\nimport {\n DEFAULT_DATE_PICKER_LABELS,\n DEFAULT_PICKER_SHEET_LABELS,\n useKitDateFormatter,\n useKitLabels,\n type DatePickerLabels,\n type KitDateFormatContext,\n} from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./field-base\";\nimport { assignRef, FieldLabelLine, LABEL_ABOVE_BOX, type FieldHelpProps } from \"./field-parts\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport type { RemLength } from \"../hooks/use-breakpoint\";\nimport type { MiniCalendarProps } from \"./mini-calendar\";\nimport { Popover } from \"./popover\";\n\n/**\n * What both pickers share — and, through `ComponentPropsWithoutRef<\"div\">`, everything a\n * `<div>` takes, so a `data-tour` anchor, a test id or an `aria-describedby` reaches the\n * field's root element instead of being dropped (audit §api-design).\n *\n * `onChange` is omitted from those attributes: the DOM event of that name carries a\n * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own\n * (date-picker.tsx, date-range-picker.tsx).\n */\nexport interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\">, FieldHelpProps {\n /** BCP 47 tag for the trigger's date and the calendar. Falls back to\n * `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n placeholder?: string;\n /** Inclusive ISO bounds for selectable days. */\n min?: string;\n max?: string;\n /** Show an X to clear the value (emits `\"\"`). */\n clearable?: boolean;\n /** Accessible name of the clear button. Wins over `datePicker.clear` from\n * `<UiKitProvider labels>`; kept as a prop because it predates the provider. */\n clearLabel?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /**\n * Standing advice for the field (keksdose K4, \"uniform field anatomy\"), as on a\n * {@link Select}: plain TEXT is a caption under the field, attached with\n * `aria-describedby`; a {@link FieldHint} rides the label line (or, unlabelled, sits\n * at the field's end, outside the box).\n */\n hint?: ReactNode;\n /**\n * What is wrong with the date, in the caller's words (\"The due date is before the\n * invoice date\") — {@link Input}'s `error` (keksdose K4: DateField errors had no slot).\n * Rendered under the field (under the whole row, with `step` / `today`), attached to\n * the trigger with `aria-describedby` — merged with any the caller passed, after the\n * caption — and implies `invalid`. Passing the key at all keeps the field's box while\n * there is no message, so the trigger is never remounted as one comes and goes.\n */\n error?: ReactNode;\n className?: string;\n /** Intl options for the trigger's formatted date (default: locale short date). Win\n * over the provider's `formatDate`. */\n formatOptions?: Intl.DateTimeFormatOptions;\n /** Render the trigger's text for an ISO date yourself; wins over `formatOptions` and\n * over the provider's `formatDate`.\n * For a host whose date rendering is more than one `Intl` call can say — Keksdose\n * puts the weekday's name in the UI language beside digits ordered by a separate\n * format preference, two locales in one string. `locale` still drives the calendar.\n * Set that rendering once with `<UiKitProvider formatDate>` (keksdose K12) and this\n * is for the one field that differs. */\n formatValue?: (iso: string) => string;\n /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are\n * icon-only, so their `aria-label` is the only name they have — and the package\n * ships English defaults, which a translating host has to be able to replace.\n * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */\n calendarLabels?: MiniCalendarProps[\"labels\"];\n /**\n * Make the panel's caption (\"July 2026\") a button that opens the year's twelve\n * months, with ‹ › stepping a year; picking a month returns to its days. Far dates\n * stop being a march of month arrows.\n *\n * kastlan is why: its service-charge period runs July to June, and with one month on\n * screen and ‹ › the only way across, the end of the period was eleven clicks past\n * the start — so kastlan kept two date fields instead of `RhfDateRangePicker`. With\n * the jump the whole period is open, caption, Jul, 1, caption, next year, Jun, 30.\n *\n * What it adds, for the keyboard and a screen reader:\n * - the caption is a disclosure button (`aria-expanded`). Its name is the month it\n * shows, so speech input can say what it sees; it is `aria-live`, as the\n * calendar's caption was, so paging months still says where you landed; and it is\n * described by `monthPicker.panel` (\"Choose a month\").\n * - the month grid is `MonthPicker`'s grid: one tab stop, arrows walk the months\n * (across the year's edge), Home/End the row, PageUp/PageDown a year, Enter or a\n * click picks. Focus opens on the month on show and comes back on its days — the\n * selected day if it is in that month, else the 1st. Months outside `min`/`max`\n * are `aria-disabled`, and a year arrow is once the whole year beyond is.\n * - Escape in the month grid goes back to the days without closing the panel (focus\n * on the caption); in the days, Escape closes the panel as before.\n *\n * No new strings: the month arrows keep `miniCalendar.previousMonth` / `nextMonth`\n * (`calendarLabels`), and the month grid speaks with `MonthPicker`'s words —\n * `monthPicker.previousYear` / `nextYear` / `month` / `panel` — because it is that\n * grid, and a host that translated it once should not have to again.\n *\n * Off by default: the caption becomes one more stop in the panel's tab order, which a\n * 0.x minor does not spring on a form. Works in the popover and in\n * `DateRangePicker`'s phone sheet alike, beside presets and in `commit=\"apply\"`\n * (a preset picked while the months are open goes back to the days, on its start).\n *\n * Why not a two-month panel (`months={2}`) instead, or beside it: for a twelve-month\n * period it still pages six times where this takes one year step, and two\n * `MiniCalendar`s side by side each follow the value on their own — picking the end\n * in the right-hand month would pull the left one onto it — so the calendar would\n * have to learn to share a month first. Not worth that for a saving of one arrow on a\n * range across a single month boundary.\n */\n monthJump?: boolean;\n /**\n * The TRIGGER — the focusable button that opens the calendar, a `renderTrigger` one\n * too — as a React 19 ref prop (0.34, API-3, refactor audit 2026-10). The field took\n * none, so react-hook-form's focus-on-error reached it only through /rhf's id lookup,\n * and an app not at all. Not the wrapper, which takes the rest of the props.\n */\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * The trigger's date renderer, in the kit's order of precedence: the picker's own\n * `formatValue`, then its `formatOptions` (an explicit `Intl` look for this field),\n * then the provider's `formatDate` (keksdose K12), then the locale's short date. A\n * provider formatter that answers `\"\"` falls through to the default for that date.\n */\nexport function useTriggerFormat(\n source: KitDateFormatContext[\"source\"],\n locale: string | undefined,\n formatOptions: Intl.DateTimeFormatOptions | undefined,\n formatValue: ((iso: string) => string) | undefined,\n): (iso: string) => string {\n const fromProvider = useKitDateFormatter();\n if (formatValue) return formatValue;\n return (iso) => {\n if (formatOptions === undefined && fromProvider) {\n // A single date stands alone, where the weekday helps; a range's two ends do not.\n const text = fromProvider(iso, { unit: \"day\", source, locale, weekday: source === \"datePicker\" });\n if (text) return text;\n }\n // No prop and no provider leaves `locale` undefined: the runtime's default (DUP-11).\n return formatIsoDate(iso, locale, formatOptions);\n };\n}\n\n/**\n * Everything the kit's own trigger button carries, handed to a custom one\n * ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread\n * it WHOLE: the ref is how focus comes back when the panel closes, the id pair and\n * `aria-labelledby` are the 0.5.1 naming (an external `<label htmlFor>`, a form\n * library's `aria-describedby`), and `role`/`aria-haspopup`/`aria-controls`/\n * `aria-expanded` are what tell a screen reader this opens a calendar and whether it\n * is open. `className` is the kit's field look (42px with a label, `pe-9` for the\n * glyph the field draws at its end) — merge yours after it with `cn`.\n */\nexport interface DateTriggerAttributes {\n ref: RefObject<HTMLButtonElement | null>;\n type: \"button\";\n id?: string;\n role: \"combobox\";\n \"aria-haspopup\": \"dialog\";\n \"aria-controls\": string;\n \"aria-expanded\": boolean;\n \"aria-labelledby\"?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n /** The field's required state — `Field required` hands it down with the id pair. */\n \"aria-required\"?: boolean | \"true\" | \"false\";\n disabled?: boolean;\n onClick: () => void;\n className: string;\n}\n\n/** A custom trigger, as `DateField` calls it; each picker adapts it to its own props. */\ntype TriggerRenderer = (attrs: DateTriggerAttributes, valueId: string) => ReactNode;\n\n/** A caller's trigger, as an element of its own: the attributes carry the trigger's\n * ref, and a ref is handed to a child as a prop, not passed to a function in the\n * middle of the parent's render. */\nfunction CustomTrigger({\n render,\n attrs,\n valueId,\n}: {\n render: TriggerRenderer;\n attrs: DateTriggerAttributes;\n valueId: string;\n}) {\n return render(attrs, valueId);\n}\n\n/**\n * The trigger's value wraps instead of truncating at Large and Extra large (0.32.1, the\n * 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing truncates\"). A\n * range (\"01/09/2026 – 30/09/2026\") is 312 px at 150 %, and on a 360 px phone it read\n * \"01/09/2026 – 30/…\" — its end, the half a reader needs, was the half cut off. A\n * `large:` class, so Normal keeps its one-line field: since 0.33 (§10.17) the kit's one\n * `truncate-until-large` (tokens.css) in place of this file's own `truncate` +\n * `large:whitespace-normal large:break-words`.\n */\nconst VALUE_TRUNCATE = \"truncate-until-large\";\n\n/**\n * The field-shaped button that opens the calendar.\n *\n * A component of its own rather than JSX inline in `Popover`'s `trigger` render prop,\n * because handing focus back to it is an effect and a render prop is not somewhere a\n * hook can go.\n */\nfunction DateFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n padded,\n disabled,\n invalid,\n required,\n panelId,\n aria,\n renderTrigger,\n fieldRef,\n}: {\n /** The caller's naming/description attributes, routed here from the field. */\n aria: TriggerAria;\n /** Draw the button yourself, with these attributes — see {@link DateTriggerAttributes}. */\n renderTrigger?: TriggerRenderer;\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n /** The picker's own `ref` prop (API-3) — the same element as `triggerRef`. */\n fieldRef?: Ref<HTMLButtonElement>;\n /** The panel's id — `role=\"combobox\"` requires `aria-controls`. */\n panelId: string;\n triggerText: string;\n hasValue: boolean;\n /** ids of the hidden label twin + the value span, in the order they are spoken. */\n labelledBy?: string;\n valueId: string;\n padded: boolean;\n disabled?: boolean;\n invalid?: boolean;\n /** `aria-required`, on the trigger with the rest of the field's ARIA. */\n required?: boolean | \"true\" | \"false\";\n}) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when the panel still HAD focus. The day the user was standing on has just\n // been unmounted with the panel, which drops focus to <body> and leaves a\n // keyboard user back at the top of the document with no idea where they were.\n // But an outside click closes this panel too, and that click has already given\n // focus to whatever it landed on — dragging it back here would be worse than the\n // bug, so a focus that is somewhere real is left alone.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n // Here rather than through `useFocusTrap`'s `restoreFocus`, and not only because\n // `Popover` runs no trap to carry it: that option restores to whatever held focus\n // when the panel opened, and on Safari — and on Firefox on macOS — clicking a\n // button does not focus it, so \"whatever held focus\" is the page. The requirement\n // is this trigger, every time, however the panel was opened.\n }, [open, triggerRef]);\n // The caller's `ref` (API-3, refactor audit 2026-10) is handed the trigger once it is\n // mounted, rather than merged into `attrs.ref`: that one is a `RefObject` by contract\n // (`DateTriggerAttributes`), which a `renderTrigger` button may read, and this way a\n // caller's own trigger is covered as well as the kit's.\n useLayoutEffect(() => {\n if (!fieldRef) return;\n assignRef(fieldRef, triggerRef.current);\n return () => assignRef(fieldRef, null);\n }, [fieldRef, triggerRef]);\n\n // One attribute object, worn either by the kit's own button below or by a caller's\n // (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the\n // naming and state this one announces.\n const isInvalid = Boolean(invalid) || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\";\n const attrs: DateTriggerAttributes = {\n ref: triggerRef,\n type: \"button\",\n disabled,\n onClick: toggle,\n // The name has to carry the VALUE. A bare `aria-label={label}` — which is what\n // this was — replaces the button's text outright, so the one field in the form\n // whose entire job is to show a date announced \"Due date\" and stopped there,\n // and a screen-reader user could not read back what they had picked without\n // opening the calendar and hunting for the selected day.\n //\n // Two ids rather than one composed string (`common.fieldValue`): \"label: value\"\n // is a sentence, and a name reference is spoken as the two texts in order with\n // no punctuation to translate at all. The hidden twin of the FieldLabel lives\n // in DateField, next to the label it copies.\n id: aria.id,\n \"aria-labelledby\": aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy,\n \"aria-label\": aria[\"aria-label\"],\n \"aria-describedby\": aria[\"aria-describedby\"],\n // `role=\"combobox\"` on a button that opens a calendar is the APG date-picker\n // shape, and it is what makes the next two lines legal: `button` supports\n // neither `aria-expanded` nor `aria-invalid`, so the previous markup set an\n // invalid state that announced nothing (ESLint's `role-supports-aria-props`).\n //\n // `aria-haspopup=\"dialog\"` is now honest — it was not when this comment first\n // said so. `Popover` gained a real `role=\"dialog\"` in the same wave, so the\n // trigger's promise and the panel's role finally agree. The phone sheet is a\n // `role=\"dialog\"` too, and carries the same id.\n role: \"combobox\",\n \"aria-haspopup\": \"dialog\",\n \"aria-controls\": panelId,\n \"aria-expanded\": open,\n \"aria-invalid\": isInvalid || undefined,\n \"aria-required\": required,\n className: cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n // An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.\n isInvalid && FIELD_INVALID,\n ),\n };\n if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;\n\n return (\n <button {...attrs}>\n <span id={valueId} className={cn(VALUE_TRUNCATE, !hasValue && \"text-placeholder\")}>\n {/* `|| \" \"` (a NON-BREAKING space) — triggerText is \"\" when there is no value and no\n placeholder was passed. An empty span has no line box, so the\n trigger collapses to its padding and sits shorter than every\n other field beside it.\n\n It has to be a NON-BREAKING space. This read `|| \" \"` first, and\n that fix did nothing at all: an ordinary space is collapsible\n white space, and white space at the start and end of a line is\n removed — so the span rendered with height 0 and the trigger\n stayed 22px against the 42px of the fields either side of it\n (Keksdose live #294, measured in the browser: `span.textContent`\n WAS `\" \"` and `getBoundingClientRect().height` was 0). U+00A0 is\n not collapsible, so it holds the line. */}\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n/**\n * How a panel's body and its action row are put on screen — the popover stacks them,\n * the phone sheet pins the actions in its footer. Handed to the panel rather than\n * applied around it because the actions belong to the panel's own draft state\n * (`RangePanel`), while the frame they sit in belongs to the field.\n */\nexport type PanelFrame = (body: ReactNode, actions?: ReactNode) => ReactNode;\n\nconst popoverFrame: PanelFrame = (body, actions) =>\n actions ? (\n <div className=\"flex flex-col gap-2\">\n {body}\n <div className=\"flex items-center justify-end gap-2 border-t border-subtle pt-2\">{actions}</div>\n </div>\n ) : (\n body\n );\n\n/** A field-styled trigger that opens a portalled MiniCalendar. Shared by the\n * single- and range-date pickers (date-picker.tsx, date-range-picker.tsx). */\nexport function DateField({\n triggerText,\n hasValue,\n label,\n disabled,\n clearable,\n clearLabel,\n panelLabel,\n className,\n width,\n onClear,\n invalid,\n sheet = false,\n sheetBackCloses = true,\n renderTrigger,\n labelHint,\n children,\n // The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id\n // pair, and a required date read as optional while it sat on the role-less div.\n // Taken here rather than in `splitTriggerAria`, which MonthPicker shares.\n \"aria-required\": ariaRequired,\n fieldRef,\n ...rest\n}: Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> & {\n /** The picker's `ref` prop: the trigger (API-3). */\n fieldRef?: Ref<HTMLButtonElement>;\n triggerText: string;\n hasValue: boolean;\n label?: ReactNode;\n /** A `FieldHint` for the label line, beside the label. */\n labelHint?: ReactNode;\n disabled?: boolean;\n clearable?: boolean;\n /** Already resolved by the picker (prop > provider > English). */\n clearLabel: string;\n /** The popover panel's accessible name, resolved likewise. */\n panelLabel: string;\n invalid?: boolean;\n className?: string;\n /** Popover panel width; omit for the default (a bare calendar). In rem so it grows\n * with the text size, like the Popover's own default (§3.2). */\n width?: number | RemLength;\n onClear: () => void;\n /** Open as a full-screen sheet instead of the popover — the picker decides when\n * (in the phone layout, `usePhoneLayout()`). */\n sheet?: boolean;\n /** The sheet's `FullBleedDialog backCloses`. */\n sheetBackCloses?: boolean;\n renderTrigger?: TriggerRenderer;\n children: (close: () => void, frame: PanelFrame) => ReactNode;\n}) {\n const showClear = Boolean(clearable && hasValue && !disabled);\n const [aria, wrapperRest] = splitTriggerAria(rest);\n // The panel is portalled to <body> and leaves the subtree whose `dir` it inherited,\n // so a calendar in an RTL form opened LTR — the grid's columns, its arrow keys and\n // its chevrons all the wrong way round. Read at the moment of opening (the only way\n // the popover opens is through `toggle`) and put back on the panel.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can be reused as a name; a ReactNode may be a whole row with\n // its own interactive \"?\" hint in it, and naming a button with that reads the hint\n // out as part of the field's name.\n const named = typeof label === \"string\";\n // The sheet's open flag. Only the phone branch owns one: on desktop `Popover` owns\n // it, so the two can never disagree about what is on screen.\n const [sheetOpen, setSheetOpen] = useState(false);\n const sheetTriggerRef = useRef<HTMLButtonElement>(null);\n const closeSheet = () => setSheetOpen(false);\n const sheetLabels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS);\n const sheetFrame: PanelFrame = (body, actions) => (\n <FullBleedDialog\n open\n onClose={closeSheet}\n // The same id the popover panel wears, so the trigger's `aria-controls` holds in\n // both presentations; named the way the popover is.\n id={panelId}\n aria-label={panelLabel}\n header={panelLabel}\n // `pickerSheet.close`: the one \"close this full-screen picker\" string the kit\n // already translates, rather than a second key saying the same thing.\n closeLabel={sheetLabels.close}\n backCloses={sheetBackCloses}\n dir={dir}\n // Portalled to <body>, the sheet is not inside the field's wrapper, so a\n // document-level \"outside click\" listener — a Popover or dropdown this field\n // sits in — would read a tap on a day as outside and unmount the cell before\n // its click (Keksdose dev#477, the fix `PickerSheet` carries).\n onMouseDown={(e) => e.stopPropagation()}\n footer={actions}\n >\n {body}\n </FullBleedDialog>\n );\n const fieldTrigger = ({\n open,\n toggle,\n ref,\n }: {\n open: boolean;\n toggle: () => void;\n ref: RefObject<HTMLButtonElement | null>;\n }) => (\n <DateFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={hasValue}\n // ALWAYS a name reference, never `undefined`. A `<button>` takes its\n // accessible name from its contents, so an unlabelled trigger used to\n // announce its own date text for free. `role=\"combobox\"` does not —\n // content is excluded from name computation for that role — so leaving\n // this undefined made an unlabelled date field announce nothing at all.\n // Named: \"label, value\". Unnamed: the value alone, which is what the\n // button was saying before.\n //\n // A caller's own reference comes first. And when the caller gives the field\n // an `id` (so an external `<label htmlFor>` can point at it), the trigger\n // lists ITSELF first: a self-reference in `aria-labelledby` is resolved\n // from the element's native label (accname 2B → 2D), so \"Due date\" from\n // that <label> is spoken before the value, instead of being overridden.\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 panelId={panelId}\n valueId={valueId}\n padded={label !== undefined}\n disabled={disabled}\n invalid={invalid}\n required={ariaRequired}\n renderTrigger={renderTrigger}\n fieldRef={fieldRef}\n />\n );\n return (\n // The caller's attributes land here, on the field's own box — the trigger inside is\n // named by `aria-labelledby` and must keep the id pair it is given.\n // At Large a labelled box is a grid: the label above, the calendar glyph and the\n // clear × measured against the trigger's row (LABEL_ABOVE_BOX, 0.33 §10.17).\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", label !== undefined && LABEL_ABOVE_BOX, className)}>\n {label !== undefined && <FieldLabelLine label={label} hint={labelHint} />}\n {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names\n nothing on its own — this hidden twin is what the trigger is named by.\n `aria-hidden` because a name reference reads a hidden element deliberately\n (accname §4.3.1), and without it the label would be announced twice: once as\n loose text before the button, once inside the button's own name.\n `sr-only-fixed` rather than `sr-only` — see the note on the class in\n tokens.css. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n {sheet ? (\n <>\n {fieldTrigger({ open: sheetOpen, toggle: () => setSheetOpen((v) => !v), ref: sheetTriggerRef })}\n {/* Mounted only while open, like the popover's children: the panel's draft is\n born with it, so every open starts again from the committed value. */}\n {sheetOpen && children(closeSheet, sheetFrame)}\n </>\n ) : (\n <Popover\n width={width}\n panelId={panelId}\n // Named for what it is. Unnamed, it fell back to `popover.panel`, and a date\n // field announced its calendar as \"Popover\" — in English, in every language.\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={fieldTrigger}\n >\n {(close) => children(close, popoverFrame)}\n </Popover>\n )}\n {showClear ? (\n <button\n type=\"button\"\n aria-label={clearLabel}\n onClick={onClear}\n className=\"absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-placeholder hover:text-secondary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-placeholder\"\n />\n )}\n </div>\n );\n}\n\n/**\n * The picker's own label props, folded into one `datePicker` override so they sit at\n * the top of the same precedence chain as everything else: prop > provider > English.\n * They predate the provider and stay public API, which is why they are not simply\n * replaced by a `labels` prop.\n */\nexport function useDatePickerLabels(\n clearLabel: string | undefined,\n stepLabels: { prev: string; next: string } | undefined,\n todayLabel: string | undefined,\n): DatePickerLabels {\n const prev = stepLabels?.prev;\n const next = stepLabels?.next;\n const fromProps = useMemo(() => {\n const out: Partial<DatePickerLabels> = {};\n if (clearLabel !== undefined) out.clear = clearLabel;\n if (prev !== undefined) out.previousDay = prev;\n if (next !== undefined) out.nextDay = next;\n if (todayLabel !== undefined) out.today = todayLabel;\n return out;\n }, [clearLabel, prev, next, todayLabel]);\n return useKitLabels(\"datePicker\", DEFAULT_DATE_PICKER_LABELS, fromProps);\n}\n\n/**\n * The caller's `hint` when it has no label line to ride: at the end of the field,\n * outside the box, as an unlabelled {@link Select} places its FieldHint (0.15.5 P8).\n */\nexport function withEndHint(field: ReactNode, hint: ReactNode): ReactNode {\n if (hint === undefined) return field;\n return (\n <div className=\"flex items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">{field}</div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -78,6 +78,7 @@ function DatePicker(props) {
78
78
  disabled,
79
79
  invalid,
80
80
  hint,
81
+ help,
81
82
  error,
82
83
  // Named so it reaches the field (and so its trigger) when a step/today row is the
83
84
  // root and takes the rest of the caller's props.
@@ -91,6 +92,8 @@ function DatePicker(props) {
91
92
  const render = useTriggerFormat("datePicker", locale, formatOptions, formatValue);
92
93
  const messages = useFieldMessages({
93
94
  hint,
95
+ help,
96
+ name: label ?? rest["aria-label"],
94
97
  error,
95
98
  invalid,
96
99
  describedBy: rest["aria-describedby"],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/date-picker.tsx"],"sourcesContent":["import type { ComponentType } from \"react\";\nimport { CalendarClock, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { addDaysIso } from \"../lib/dates\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport { FIELD_BASE } from \"./field-base\";\nimport { FieldBox, useFieldMessages } from \"./field-parts\";\nimport { Tooltip } from \"./tooltip\";\nimport {\n DateField,\n useDatePickerLabels,\n useTriggerFormat,\n withEndHint,\n type DatePickerBaseProps,\n} from \"./date-field\";\nimport { PanelCalendar } from \"./jump-calendar\";\n// The shared field, the month-jump calendar and the range picker live in date-field.tsx,\n// jump-calendar.tsx and date-range-picker.tsx (MOD-9); their public names are still this\n// module's, so the barrel exports them as before.\nexport type { DateTriggerAttributes } from \"./date-field\";\nexport { DateRangePicker } from \"./date-range-picker\";\nexport type {\n DateRangeCommit,\n DateRangeDraftSummary,\n DateRangePickerPreset,\n DateRangePickerProps,\n DateRangeTriggerRenderProps,\n} from \"./date-range-picker\";\n\n/**\n * The ‹ date › (today) row on a phone at Large and Extra large (0.32.1, the 360 px\n * Extra-large sweep; docs/text-size-harmonization.md §4): the field takes the first line\n * on its own, whole and with all four corners, and the buttons share the line under it\n * as one joined strip. Three 2.5rem buttons are 180 px at 150 %, and on a 360 px phone\n * they left the field 84 px — \"Book… 0\" for \"Booking date 07/09/2026\" — where a 360 px\n * row would hold the date only by cutting it. `large:max-sm:`, the scaled phone width,\n * so a tablet or a desktop at Large keeps the one row, and Normal is unchanged.\n */\nconst STEP_ROW_STACK_AT_LARGE = cn(\n \"large:max-sm:flex-wrap large:max-sm:gap-y-2\",\n \"large:max-sm:[&>[data-slot=date-step-field]]:order-first large:max-sm:[&>[data-slot=date-step-field]]:basis-full\",\n \"large:max-sm:[&>[data-slot=date-step-field]]:ms-0 large:max-sm:[&_[role=combobox]]:rounded-md\",\n // The buttons (each in its tooltip's wrapper) share the line under the field.\n \"large:max-sm:[&>span]:flex-1 large:max-sm:[&>span>button]:w-full\",\n);\n\n// The focused part of the ‹ date › (today) row framed 2px wide, not 1 (keksdose live\n// #394, fourth round; kk K5). The row raises the focused part over the neighbour it\n// overlaps (`[&>*:focus-within]:z-20`), so all four of its sides are its own brand\n// border — but at a fractional device-pixel ratio a 1px line covers 1.25 device pixels\n// (at 125 %), and on whichever sides fall across a pixel boundary it is spread over two\n// pixels at partial strength: ‹'s left side and the date field's right read grey beside\n// the crisp ones (dark theme, measured: 82,87,146 against 139,147,248).\n//\n// A 1px INSET ring in the focus colour beside the 1px border doubles the line inward, so\n// every side keeps at least one fully covered device pixel at any scale — FIELD_INVALID's\n// border-plus-ring, for the same reason (live #295). A box-shadow, and inset: nothing\n// outside the part moves or is covered. `focus`, not `focus-visible`, as the row's\n// raise: a tap focuses too.\n//\n// 0.34 drew it here, on the parts — `focus:ring-1 focus:ring-inset` on each step and\n// `[&_[role=combobox]:focus]:…` from the row — and the K5 follow-up then moved that very\n// ring into FIELD_BASE for every field (keksdose run 78). Each part of this row is\n// FIELD_BASE, the steps directly and the trigger through FIELD_TRIGGER, so the copies went\n// (round 0.34 Wave 2). Not on every button in the row: keksdose's local fix\n// (`[&_button:focus]:ring-1 [&_button:focus]:ring-inset` on the row) also reached the\n// label's \"?\" and thinned its 2px focus frame to 1.\n\n/** One ‹ / › day-step button, sized to sit flush beside the field. */\nfunction StepButton({\n icon: Icon,\n label,\n disabled,\n onClick,\n mirror,\n className,\n}: {\n icon: ComponentType<{ className?: string }>;\n label: string;\n disabled: boolean;\n onClick: () => void;\n /** Flip the icon in RTL — for the ‹ › day steps, whose \"previous\" points to the\n * reading start. Not for the today icon, which has no direction. */\n mirror?: boolean;\n /** The button's corners in the joined group (see DatePicker). */\n className?: string;\n}) {\n return (\n <Tooltip label={label} portal>\n <button\n type=\"button\"\n aria-label={label}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n FIELD_BASE,\n // FIELD_BASE is `block w-full` for text inputs; a step button is neither, and\n // px-3 would make it wider than it needs to be. twMerge lets these win.\n \"relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-muted\",\n \"hover:bg-hover hover:text-secondary\",\n // Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border\n // included — faded out of the group and read as a broken, detached control.\n \"disabled:cursor-not-allowed disabled:text-placeholder disabled:opacity-60 disabled:hover:bg-surface\",\n // A part of a joined row: FIELD_BASE's 2px focus line, whole at 125 % (K5, the\n // note above this function).\n className,\n )}\n >\n <Icon className={cn(\"size-4\", mirror && \"rtl:-scale-x-100\")} />\n </button>\n </Tooltip>\n );\n}\n\nexport interface DatePickerProps extends DatePickerBaseProps {\n /** ISO \"YYYY-MM-DD\", or \"\" for empty. */\n value: string;\n onChange: (iso: string) => void;\n /**\n * Flank the field with ‹ › buttons that move the value one day. For fields whose\n * typical edit is a day or two — opening a calendar to go \"yesterday\" is three\n * interactions for what should be one.\n *\n * Off by default: on a filter or a far-away date the buttons are dead weight, and\n * they cost ~5rem of width that a narrow layout may not have.\n */\n step?: boolean;\n /** Accessible names for the step buttons. Optional: they resolve from\n * `datePicker.previousDay` / `nextDay` in `<UiKitProvider labels>`, then from the\n * English defaults. This prop wins over both, for the one field that says it\n * differently. */\n stepLabels?: { prev: string; next: string };\n /**\n * Today's date as ISO \"YYYY-MM-DD\". Adds a jump-to-today button beside the field,\n * disabled once the value already is that day.\n *\n * Passed in rather than read from a clock here: a component that decides what \"today\"\n * is cannot be tested at a fixed date, and the consumer already has one definition of\n * it. The button is always RENDERED — going flat instead of disappearing — because a\n * control that comes and goes with the value reflows whatever sits beside it, which\n * is the report this exists to answer.\n */\n today?: string;\n /** Accessible name for the today button; wins over `datePicker.today`. */\n todayLabel?: string;\n}\n\n/** Single-date picker: a field showing the formatted date, opening a calendar. */\nexport function DatePicker(props: DatePickerProps) {\n const {\n value,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n step,\n stepLabels,\n today,\n todayLabel,\n className,\n calendarLabels,\n monthJump,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n hint,\n error,\n // Named so it reaches the field (and so its trigger) when a step/today row is the\n // root and takes the rest of the caller's props.\n \"aria-required\": ariaRequired,\n // The trigger's, wherever the rest go (API-3).\n ref,\n ...rest\n } = props;\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);\n const render = useTriggerFormat(\"datePicker\", locale, formatOptions, formatValue);\n // keksdose K4: the caption and the error under the field, attached to the TRIGGER\n // (the element a screen reader is on) after any description of the caller's own.\n const messages = useFieldMessages({\n hint,\n error,\n invalid,\n describedBy: rest[\"aria-describedby\"],\n ariaInvalid: rest[\"aria-invalid\"],\n });\n // With a step or a today button beside it the field is no longer the outermost\n // element — the flex row at the bottom is, and a caller's attributes belong on\n // whichever of the two is actually the root. Naming the shared props rather than\n // letting `...rest` carry them is what makes that choice possible.\n const wrapped = Boolean(step || today);\n const [triggerAria, wrapperRest] = splitTriggerAria(rest);\n const field = (\n <DateField\n // With step/today buttons the flex row is the root and takes the caller's props —\n // except the ones that name the field, which still belong on its trigger.\n {...(wrapped ? triggerAria : rest)}\n fieldRef={ref}\n aria-describedby={messages.describedBy}\n aria-required={ariaRequired}\n label={label}\n labelHint={messages.labelHint}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.panel}\n disabled={disabled}\n invalid={messages.isInvalid}\n className={step ? \"min-w-0 flex-1\" : className}\n // Named for STEP_ROW_STACK_AT_LARGE; only in the row, so a caller's own\n // `data-slot` on a bare field is left alone.\n {...(wrapped ? { \"data-slot\": \"date-step-field\" } : {})}\n hasValue={Boolean(value)}\n triggerText={value ? render(value) : (placeholder ?? \"\")}\n onClear={() => onChange(\"\")}\n >\n {(close) => (\n <PanelCalendar\n monthJump={monthJump}\n mode=\"single\"\n focusOnOpen\n from={value}\n to={value}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(iso) => {\n onChange(iso);\n close();\n }}\n />\n )}\n </DateField>\n );\n // Passed at all — even as `undefined` — the field keeps its box; see FieldBox.\n const reserve = \"error\" in props || \"hint\" in props;\n const endHint = label === undefined ? messages.labelHint : undefined;\n if (!wrapped) {\n return (\n <FieldBox below={messages.below} reserve={reserve}>\n {withEndHint(field, endHint)}\n </FieldBox>\n );\n }\n\n // An empty field has nothing to step from, so both buttons are dead until a date\n // is picked. Bounds are compared as strings: \"YYYY-MM-DD\" sorts chronologically.\n const target = (days: number) => (value ? addDaysIso(value, days) : \"\");\n const outOfBounds = (iso: string) => Boolean((min && iso < min) || (max && iso > max));\n const blocked = (days: number) => {\n const next = target(days);\n if (!next || disabled) return true;\n return outOfBounds(next);\n };\n const row = (\n // ONE joined control, not three boxes in a row: the buttons and the field share\n // their borders (each piece overlaps the previous by a pixel) and only the outer\n // ends are rounded — the button-group shape, so ‹ date › reads as one field with\n // controls rather than a field with loose buttons parked beside it, which is what\n // the `gap-1` version looked like.\n //\n // items-stretch, not items-center: the buttons match the field's height, which\n // varies with whether it carries a floating label. The field's trigger is nested\n // inside DateField, so its inner corners are squared from here.\n <div\n {...wrapperRest}\n className={cn(\n \"flex items-stretch [&>*:not(:first-child)]:-ms-px\",\n step && \"[&_[role=combobox]]:rounded-s-none\",\n (step || today) && \"[&_[role=combobox]]:rounded-e-none\",\n // The PART that is focused, pressed or invalid is raised over the neighbour it\n // overlaps by a pixel, so its blue (or red) border shows on all four sides\n // (keksdose live #394). `:focus-within`, not `:focus-visible`: a tap focuses\n // without it, as does the sheet handing focus back, and the next part — later in\n // the DOM — painted its grey left edge over the tapped one's right. Raising the\n // whole part (each is `relative`) keeps the floating label and the icon above\n // the trigger, which raising the trigger alone covered. Hover raises nothing: it\n // changes no border, and would let a hovered › cover a focused field's edge.\n \"[&>*:focus-within]:z-20 [&>*:active]:z-20 [&>*:has([aria-invalid=true])]:z-10\",\n // …and its line is 2px, so no side of it reads half-strength at 125 % (live #394,\n // fourth round): FIELD_BASE's inset ring, which every part of the row carries.\n STEP_ROW_STACK_AT_LARGE,\n className,\n )}\n >\n {step && (\n <StepButton\n icon={ChevronLeft}\n mirror\n label={text.previousDay}\n disabled={blocked(-1)}\n onClick={() => onChange(target(-1))}\n className=\"rounded-e-none\"\n />\n )}\n {field}\n {step && (\n <StepButton\n icon={ChevronRight}\n mirror\n label={text.nextDay}\n disabled={blocked(1)}\n onClick={() => onChange(target(1))}\n className={cn(\"rounded-s-none\", today && \"rounded-e-none\")}\n />\n )}\n {today && (\n <StepButton\n icon={CalendarClock}\n label={text.today}\n disabled={Boolean(disabled) || value === today || outOfBounds(today)}\n onClick={() => onChange(today)}\n className=\"rounded-s-none\"\n />\n )}\n </div>\n );\n // Under the whole row, not under the field between the buttons: the message is about\n // the date, and the steppers are part of how it is entered.\n return (\n <FieldBox below={messages.below} reserve={reserve}>\n {withEndHint(row, endHint)}\n </FieldBox>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/date-picker.tsx"],"sourcesContent":["import type { ComponentType } from \"react\";\nimport { CalendarClock, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { addDaysIso } from \"../lib/dates\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport { FIELD_BASE } from \"./field-base\";\nimport { FieldBox, useFieldMessages } from \"./field-parts\";\nimport { Tooltip } from \"./tooltip\";\nimport {\n DateField,\n useDatePickerLabels,\n useTriggerFormat,\n withEndHint,\n type DatePickerBaseProps,\n} from \"./date-field\";\nimport { PanelCalendar } from \"./jump-calendar\";\n// The shared field, the month-jump calendar and the range picker live in date-field.tsx,\n// jump-calendar.tsx and date-range-picker.tsx (MOD-9); their public names are still this\n// module's, so the barrel exports them as before.\nexport type { DateTriggerAttributes } from \"./date-field\";\nexport { DateRangePicker } from \"./date-range-picker\";\nexport type {\n DateRangeCommit,\n DateRangeDraftSummary,\n DateRangePickerPreset,\n DateRangePickerProps,\n DateRangeTriggerRenderProps,\n} from \"./date-range-picker\";\n\n/**\n * The ‹ date › (today) row on a phone at Large and Extra large (0.32.1, the 360 px\n * Extra-large sweep; docs/text-size-harmonization.md §4): the field takes the first line\n * on its own, whole and with all four corners, and the buttons share the line under it\n * as one joined strip. Three 2.5rem buttons are 180 px at 150 %, and on a 360 px phone\n * they left the field 84 px — \"Book… 0\" for \"Booking date 07/09/2026\" — where a 360 px\n * row would hold the date only by cutting it. `large:max-sm:`, the scaled phone width,\n * so a tablet or a desktop at Large keeps the one row, and Normal is unchanged.\n */\nconst STEP_ROW_STACK_AT_LARGE = cn(\n \"large:max-sm:flex-wrap large:max-sm:gap-y-2\",\n \"large:max-sm:[&>[data-slot=date-step-field]]:order-first large:max-sm:[&>[data-slot=date-step-field]]:basis-full\",\n \"large:max-sm:[&>[data-slot=date-step-field]]:ms-0 large:max-sm:[&_[role=combobox]]:rounded-md\",\n // The buttons (each in its tooltip's wrapper) share the line under the field.\n \"large:max-sm:[&>span]:flex-1 large:max-sm:[&>span>button]:w-full\",\n);\n\n// The focused part of the ‹ date › (today) row framed 2px wide, not 1 (keksdose live\n// #394, fourth round; kk K5). The row raises the focused part over the neighbour it\n// overlaps (`[&>*:focus-within]:z-20`), so all four of its sides are its own brand\n// border — but at a fractional device-pixel ratio a 1px line covers 1.25 device pixels\n// (at 125 %), and on whichever sides fall across a pixel boundary it is spread over two\n// pixels at partial strength: ‹'s left side and the date field's right read grey beside\n// the crisp ones (dark theme, measured: 82,87,146 against 139,147,248).\n//\n// A 1px INSET ring in the focus colour beside the 1px border doubles the line inward, so\n// every side keeps at least one fully covered device pixel at any scale — FIELD_INVALID's\n// border-plus-ring, for the same reason (live #295). A box-shadow, and inset: nothing\n// outside the part moves or is covered. `focus`, not `focus-visible`, as the row's\n// raise: a tap focuses too.\n//\n// 0.34 drew it here, on the parts — `focus:ring-1 focus:ring-inset` on each step and\n// `[&_[role=combobox]:focus]:…` from the row — and the K5 follow-up then moved that very\n// ring into FIELD_BASE for every field (keksdose run 78). Each part of this row is\n// FIELD_BASE, the steps directly and the trigger through FIELD_TRIGGER, so the copies went\n// (round 0.34 Wave 2). Not on every button in the row: keksdose's local fix\n// (`[&_button:focus]:ring-1 [&_button:focus]:ring-inset` on the row) also reached the\n// label's \"?\" and thinned its 2px focus frame to 1.\n\n/** One ‹ / › day-step button, sized to sit flush beside the field. */\nfunction StepButton({\n icon: Icon,\n label,\n disabled,\n onClick,\n mirror,\n className,\n}: {\n icon: ComponentType<{ className?: string }>;\n label: string;\n disabled: boolean;\n onClick: () => void;\n /** Flip the icon in RTL — for the ‹ › day steps, whose \"previous\" points to the\n * reading start. Not for the today icon, which has no direction. */\n mirror?: boolean;\n /** The button's corners in the joined group (see DatePicker). */\n className?: string;\n}) {\n return (\n <Tooltip label={label} portal>\n <button\n type=\"button\"\n aria-label={label}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n FIELD_BASE,\n // FIELD_BASE is `block w-full` for text inputs; a step button is neither, and\n // px-3 would make it wider than it needs to be. twMerge lets these win.\n \"relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-muted\",\n \"hover:bg-hover hover:text-secondary\",\n // Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border\n // included — faded out of the group and read as a broken, detached control.\n \"disabled:cursor-not-allowed disabled:text-placeholder disabled:opacity-60 disabled:hover:bg-surface\",\n // A part of a joined row: FIELD_BASE's 2px focus line, whole at 125 % (K5, the\n // note above this function).\n className,\n )}\n >\n <Icon className={cn(\"size-4\", mirror && \"rtl:-scale-x-100\")} />\n </button>\n </Tooltip>\n );\n}\n\nexport interface DatePickerProps extends DatePickerBaseProps {\n /** ISO \"YYYY-MM-DD\", or \"\" for empty. */\n value: string;\n onChange: (iso: string) => void;\n /**\n * Flank the field with ‹ › buttons that move the value one day. For fields whose\n * typical edit is a day or two — opening a calendar to go \"yesterday\" is three\n * interactions for what should be one.\n *\n * Off by default: on a filter or a far-away date the buttons are dead weight, and\n * they cost ~5rem of width that a narrow layout may not have.\n */\n step?: boolean;\n /** Accessible names for the step buttons. Optional: they resolve from\n * `datePicker.previousDay` / `nextDay` in `<UiKitProvider labels>`, then from the\n * English defaults. This prop wins over both, for the one field that says it\n * differently. */\n stepLabels?: { prev: string; next: string };\n /**\n * Today's date as ISO \"YYYY-MM-DD\". Adds a jump-to-today button beside the field,\n * disabled once the value already is that day.\n *\n * Passed in rather than read from a clock here: a component that decides what \"today\"\n * is cannot be tested at a fixed date, and the consumer already has one definition of\n * it. The button is always RENDERED — going flat instead of disappearing — because a\n * control that comes and goes with the value reflows whatever sits beside it, which\n * is the report this exists to answer.\n */\n today?: string;\n /** Accessible name for the today button; wins over `datePicker.today`. */\n todayLabel?: string;\n}\n\n/** Single-date picker: a field showing the formatted date, opening a calendar. */\nexport function DatePicker(props: DatePickerProps) {\n const {\n value,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n step,\n stepLabels,\n today,\n todayLabel,\n className,\n calendarLabels,\n monthJump,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n hint,\n help,\n error,\n // Named so it reaches the field (and so its trigger) when a step/today row is the\n // root and takes the rest of the caller's props.\n \"aria-required\": ariaRequired,\n // The trigger's, wherever the rest go (API-3).\n ref,\n ...rest\n } = props;\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);\n const render = useTriggerFormat(\"datePicker\", locale, formatOptions, formatValue);\n // keksdose K4: the caption and the error under the field, attached to the TRIGGER\n // (the element a screen reader is on) after any description of the caller's own.\n const messages = useFieldMessages({\n hint,\n help,\n name: label ?? rest[\"aria-label\"],\n error,\n invalid,\n describedBy: rest[\"aria-describedby\"],\n ariaInvalid: rest[\"aria-invalid\"],\n });\n // With a step or a today button beside it the field is no longer the outermost\n // element — the flex row at the bottom is, and a caller's attributes belong on\n // whichever of the two is actually the root. Naming the shared props rather than\n // letting `...rest` carry them is what makes that choice possible.\n const wrapped = Boolean(step || today);\n const [triggerAria, wrapperRest] = splitTriggerAria(rest);\n const field = (\n <DateField\n // With step/today buttons the flex row is the root and takes the caller's props —\n // except the ones that name the field, which still belong on its trigger.\n {...(wrapped ? triggerAria : rest)}\n fieldRef={ref}\n aria-describedby={messages.describedBy}\n aria-required={ariaRequired}\n label={label}\n labelHint={messages.labelHint}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.panel}\n disabled={disabled}\n invalid={messages.isInvalid}\n className={step ? \"min-w-0 flex-1\" : className}\n // Named for STEP_ROW_STACK_AT_LARGE; only in the row, so a caller's own\n // `data-slot` on a bare field is left alone.\n {...(wrapped ? { \"data-slot\": \"date-step-field\" } : {})}\n hasValue={Boolean(value)}\n triggerText={value ? render(value) : (placeholder ?? \"\")}\n onClear={() => onChange(\"\")}\n >\n {(close) => (\n <PanelCalendar\n monthJump={monthJump}\n mode=\"single\"\n focusOnOpen\n from={value}\n to={value}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(iso) => {\n onChange(iso);\n close();\n }}\n />\n )}\n </DateField>\n );\n // Passed at all — even as `undefined` — the field keeps its box; see FieldBox.\n const reserve = \"error\" in props || \"hint\" in props;\n const endHint = label === undefined ? messages.labelHint : undefined;\n if (!wrapped) {\n return (\n <FieldBox below={messages.below} reserve={reserve}>\n {withEndHint(field, endHint)}\n </FieldBox>\n );\n }\n\n // An empty field has nothing to step from, so both buttons are dead until a date\n // is picked. Bounds are compared as strings: \"YYYY-MM-DD\" sorts chronologically.\n const target = (days: number) => (value ? addDaysIso(value, days) : \"\");\n const outOfBounds = (iso: string) => Boolean((min && iso < min) || (max && iso > max));\n const blocked = (days: number) => {\n const next = target(days);\n if (!next || disabled) return true;\n return outOfBounds(next);\n };\n const row = (\n // ONE joined control, not three boxes in a row: the buttons and the field share\n // their borders (each piece overlaps the previous by a pixel) and only the outer\n // ends are rounded — the button-group shape, so ‹ date › reads as one field with\n // controls rather than a field with loose buttons parked beside it, which is what\n // the `gap-1` version looked like.\n //\n // items-stretch, not items-center: the buttons match the field's height, which\n // varies with whether it carries a floating label. The field's trigger is nested\n // inside DateField, so its inner corners are squared from here.\n <div\n {...wrapperRest}\n className={cn(\n \"flex items-stretch [&>*:not(:first-child)]:-ms-px\",\n step && \"[&_[role=combobox]]:rounded-s-none\",\n (step || today) && \"[&_[role=combobox]]:rounded-e-none\",\n // The PART that is focused, pressed or invalid is raised over the neighbour it\n // overlaps by a pixel, so its blue (or red) border shows on all four sides\n // (keksdose live #394). `:focus-within`, not `:focus-visible`: a tap focuses\n // without it, as does the sheet handing focus back, and the next part — later in\n // the DOM — painted its grey left edge over the tapped one's right. Raising the\n // whole part (each is `relative`) keeps the floating label and the icon above\n // the trigger, which raising the trigger alone covered. Hover raises nothing: it\n // changes no border, and would let a hovered › cover a focused field's edge.\n \"[&>*:focus-within]:z-20 [&>*:active]:z-20 [&>*:has([aria-invalid=true])]:z-10\",\n // …and its line is 2px, so no side of it reads half-strength at 125 % (live #394,\n // fourth round): FIELD_BASE's inset ring, which every part of the row carries.\n STEP_ROW_STACK_AT_LARGE,\n className,\n )}\n >\n {step && (\n <StepButton\n icon={ChevronLeft}\n mirror\n label={text.previousDay}\n disabled={blocked(-1)}\n onClick={() => onChange(target(-1))}\n className=\"rounded-e-none\"\n />\n )}\n {field}\n {step && (\n <StepButton\n icon={ChevronRight}\n mirror\n label={text.nextDay}\n disabled={blocked(1)}\n onClick={() => onChange(target(1))}\n className={cn(\"rounded-s-none\", today && \"rounded-e-none\")}\n />\n )}\n {today && (\n <StepButton\n icon={CalendarClock}\n label={text.today}\n disabled={Boolean(disabled) || value === today || outOfBounds(today)}\n onClick={() => onChange(today)}\n className=\"rounded-s-none\"\n />\n )}\n </div>\n );\n // Under the whole row, not under the field between the buttons: the message is about\n // the date, and the steppers are part of how it is entered.\n return (\n <FieldBox below={messages.below} reserve={reserve}>\n {withEndHint(row, endHint)}\n </FieldBox>\n );\n}\n"],"mappings":"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","names":[]}
@@ -177,6 +177,7 @@ function DateRangePicker(props) {
177
177
  disabled,
178
178
  invalid,
179
179
  hint,
180
+ help,
180
181
  error,
181
182
  // The trigger's (API-3).
182
183
  ref,
@@ -187,6 +188,8 @@ function DateRangePicker(props) {
187
188
  const render = useTriggerFormat("dateRangePicker", locale, formatOptions, formatValue);
188
189
  const messages = useFieldMessages({
189
190
  hint,
191
+ help,
192
+ name: label ?? rest["aria-label"],
190
193
  error,
191
194
  invalid,
192
195
  describedBy: rest["aria-describedby"],