@eifi1/ui-kit 0.52.0 → 0.54.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 (290) hide show
  1. package/README.md +52 -59
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/auth/auth-errors.d.ts +36 -0
  5. package/dist/auth/auth-errors.js +20 -0
  6. package/dist/auth/auth-errors.js.map +1 -1
  7. package/dist/billing/cancel-subscription.d.ts +5 -14
  8. package/dist/billing/cancel-subscription.js +1 -3
  9. package/dist/billing/cancel-subscription.js.map +1 -1
  10. package/dist/billing/plan-card.d.ts +5 -13
  11. package/dist/billing/plan-card.js +3 -11
  12. package/dist/billing/plan-card.js.map +1 -1
  13. package/dist/billing/plan-switch.d.ts +5 -14
  14. package/dist/billing/plan-switch.js +1 -3
  15. package/dist/billing/plan-switch.js.map +1 -1
  16. package/dist/chart.d.ts +3 -14
  17. package/dist/chart.js +64 -11
  18. package/dist/chart.js.map +1 -1
  19. package/dist/components/amount-input.d.ts +2 -1
  20. package/dist/components/amount-input.js.map +1 -1
  21. package/dist/components/calendar-heatmap.js +4 -2
  22. package/dist/components/calendar-heatmap.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +2 -1
  24. package/dist/components/currency-option-list.js +10 -3
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/currency-select.d.ts +3 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/danger-confirm.d.ts +22 -1
  29. package/dist/components/danger-confirm.js +50 -28
  30. package/dist/components/danger-confirm.js.map +1 -1
  31. package/dist/components/data-table-filter-popover.d.ts +2 -1
  32. package/dist/components/data-table-filter-popover.js +3 -2
  33. package/dist/components/data-table-filter-popover.js.map +1 -1
  34. package/dist/components/data-table-filters.d.ts +33 -3
  35. package/dist/components/data-table-filters.js +6 -4
  36. package/dist/components/data-table-filters.js.map +1 -1
  37. package/dist/components/date-field.d.ts +1 -1
  38. package/dist/components/date-field.js.map +1 -1
  39. package/dist/components/date-picker.d.ts +1 -1
  40. package/dist/components/date-picker.js.map +1 -1
  41. package/dist/components/date-range-picker.d.ts +4 -17
  42. package/dist/components/date-range-picker.js +2 -2
  43. package/dist/components/date-range-picker.js.map +1 -1
  44. package/dist/components/dialog-frame.d.ts +3 -1
  45. package/dist/components/dialog-frame.js +6 -2
  46. package/dist/components/dialog-frame.js.map +1 -1
  47. package/dist/components/mini-calendar.js +3 -2
  48. package/dist/components/mini-calendar.js.map +1 -1
  49. package/dist/components/modal.d.ts +27 -0
  50. package/dist/components/modal.js +5 -3
  51. package/dist/components/modal.js.map +1 -1
  52. package/dist/components/nav-pills.d.ts +1 -1
  53. package/dist/components/nav-pills.js.map +1 -1
  54. package/dist/components/number-input.js.map +1 -1
  55. package/dist/components/series-chart-axes.d.ts +1 -1
  56. package/dist/components/series-chart-axes.js.map +1 -1
  57. package/dist/components/series-chart.d.ts +64 -19
  58. package/dist/components/series-chart.js +2 -1
  59. package/dist/components/series-chart.js.map +1 -1
  60. package/dist/components/stat-tile.d.ts +21 -2
  61. package/dist/components/stat-tile.js +4 -2
  62. package/dist/components/stat-tile.js.map +1 -1
  63. package/dist/components/treemap-labels.d.ts +6 -1
  64. package/dist/components/treemap-labels.js +2 -2
  65. package/dist/components/treemap-labels.js.map +1 -1
  66. package/dist/components/treemap.d.ts +29 -4
  67. package/dist/components/treemap.js +22 -9
  68. package/dist/components/treemap.js.map +1 -1
  69. package/dist/currencies.d.ts +16 -2
  70. package/dist/currencies.js +2 -1
  71. package/dist/currencies.js.map +1 -1
  72. package/dist/data-table.d.ts +1 -6
  73. package/dist/data-table.js +1 -9
  74. package/dist/data-table.js.map +1 -1
  75. package/dist/demo/demo-countdown.d.ts +0 -5
  76. package/dist/demo/demo-countdown.js +0 -4
  77. package/dist/demo/demo-countdown.js.map +1 -1
  78. package/dist/eslint/index.js +1 -1
  79. package/dist/feedback/feedback-context-labels.js +2 -1
  80. package/dist/feedback/feedback-context-labels.js.map +1 -1
  81. package/dist/feedback/feedback-context.d.ts +19 -3
  82. package/dist/feedback/feedback-context.js +12 -1
  83. package/dist/feedback/feedback-context.js.map +1 -1
  84. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  85. package/dist/feedback/feedback-diagnostics.js +27 -0
  86. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  87. package/dist/feedback/feedback-dialog.d.ts +10 -0
  88. package/dist/feedback/feedback-dialog.js +6 -4
  89. package/dist/feedback/feedback-dialog.js.map +1 -1
  90. package/dist/feedback/feedback-mutation.d.ts +11 -23
  91. package/dist/feedback/feedback-mutation.js +1 -9
  92. package/dist/feedback/feedback-mutation.js.map +1 -1
  93. package/dist/feedback/feedback-public-list.d.ts +1 -6
  94. package/dist/feedback/feedback-public-list.js +1 -2
  95. package/dist/feedback/feedback-public-list.js.map +1 -1
  96. package/dist/feedback/feedback-record.d.ts +5 -0
  97. package/dist/feedback/feedback-record.js.map +1 -1
  98. package/dist/feedback/feedback-row-detail-labels.js +1 -8
  99. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  100. package/dist/feedback/feedback-row-detail.d.ts +6 -50
  101. package/dist/feedback/feedback-row-detail.js +3 -8
  102. package/dist/feedback/feedback-row-detail.js.map +1 -1
  103. package/dist/feedback/feedback-status-undo.d.ts +3 -19
  104. package/dist/feedback/feedback-status-undo.js.map +1 -1
  105. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  106. package/dist/feedback/feedback-submit-dialog.js +15 -3
  107. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  108. package/dist/feedback/feedback-submit.d.ts +30 -2
  109. package/dist/feedback/feedback-submit.js +5 -2
  110. package/dist/feedback/feedback-submit.js.map +1 -1
  111. package/dist/feedback/feedback-thread.d.ts +0 -9
  112. package/dist/feedback/feedback-thread.js.map +1 -1
  113. package/dist/feedback.d.ts +6 -40
  114. package/dist/feedback.js +88 -46
  115. package/dist/feedback.js.map +1 -1
  116. package/dist/hooks/use-hotkey.js +11 -1
  117. package/dist/hooks/use-hotkey.js.map +1 -1
  118. package/dist/i18n/defaults.js +0 -4
  119. package/dist/i18n/defaults.js.map +1 -1
  120. package/dist/i18n/german.js +5 -20
  121. package/dist/i18n/german.js.map +1 -1
  122. package/dist/i18n/kit-labels.d.ts +5 -14
  123. package/dist/i18n/kit-labels.js.map +1 -1
  124. package/dist/i18n/locales/en.js +5 -31
  125. package/dist/i18n/locales/en.js.map +3 -3
  126. package/dist/i18n/locales/es.js +5 -20
  127. package/dist/i18n/locales/es.js.map +1 -1
  128. package/dist/i18n/locales/fr.js +5 -20
  129. package/dist/i18n/locales/fr.js.map +1 -1
  130. package/dist/i18n/locales/hu.js +5 -20
  131. package/dist/i18n/locales/hu.js.map +1 -1
  132. package/dist/i18n/locales/it.js +5 -21
  133. package/dist/i18n/locales/it.js.map +1 -1
  134. package/dist/i18n/locales/zh.js +5 -20
  135. package/dist/i18n/locales/zh.js.map +1 -1
  136. package/dist/i18n/review.js +9 -4
  137. package/dist/i18n/review.js.map +1 -1
  138. package/dist/i18n/shared-labels.js +0 -1
  139. package/dist/i18n/shared-labels.js.map +1 -1
  140. package/dist/index.d.ts +18 -67
  141. package/dist/index.js +28 -103
  142. package/dist/index.js.map +1 -1
  143. package/dist/lib/dates.d.ts +7 -1
  144. package/dist/lib/dates.js +8 -1
  145. package/dist/lib/dates.js.map +1 -1
  146. package/dist/lib/format.d.ts +37 -2
  147. package/dist/lib/format.js +25 -1
  148. package/dist/lib/format.js.map +1 -1
  149. package/dist/lib/intl-cache.d.ts +4 -2
  150. package/dist/lib/intl-cache.js.map +1 -1
  151. package/dist/lib/translation-review.d.ts +26 -0
  152. package/dist/lib/translation-review.js +14 -0
  153. package/dist/lib/translation-review.js.map +1 -1
  154. package/dist/rhf/fields.d.ts +2 -2
  155. package/dist/rhf/fields.js.map +1 -1
  156. package/dist/search/search-index.d.ts +0 -6
  157. package/dist/search/search-index.js +0 -4
  158. package/dist/search/search-index.js.map +1 -1
  159. package/dist/search.d.ts +1 -8
  160. package/dist/search.js +2 -7
  161. package/dist/search.js.map +1 -1
  162. package/dist/shell/account-menu-language.d.ts +7 -21
  163. package/dist/shell/account-menu-language.js +1 -3
  164. package/dist/shell/account-menu-language.js.map +1 -1
  165. package/dist/shell/account-menu-text-size.d.ts +9 -23
  166. package/dist/shell/account-menu-text-size.js +2 -3
  167. package/dist/shell/account-menu-text-size.js.map +1 -1
  168. package/dist/shell/app-shell.d.ts +0 -10
  169. package/dist/shell/app-shell.js +1 -7
  170. package/dist/shell/app-shell.js.map +1 -1
  171. package/dist/shell/company-switcher.d.ts +1 -1
  172. package/dist/shell/company-switcher.js.map +1 -1
  173. package/dist/shell/dev-menu.d.ts +1 -1
  174. package/dist/shell/dev-menu.js.map +1 -1
  175. package/dist/shell/top-bar-parts-labels.d.ts +1 -1
  176. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  177. package/dist/shell.d.ts +7 -29
  178. package/dist/shell.js +12 -42
  179. package/dist/shell.js.map +1 -1
  180. package/dist/sync.d.ts +2 -6
  181. package/dist/sync.js +2 -15
  182. package/dist/sync.js.map +1 -1
  183. package/dist/theme/chart-palette.d.ts +0 -5
  184. package/dist/theme/chart-palette.js +0 -2
  185. package/dist/theme/chart-palette.js.map +1 -1
  186. package/package.json +1 -1
  187. package/src/account/delete-account-setting.tsx +3 -2
  188. package/src/auth/auth-errors.ts +69 -0
  189. package/src/billing/cancel-subscription.tsx +5 -16
  190. package/src/billing/plan-card.tsx +7 -24
  191. package/src/billing/plan-switch.tsx +5 -16
  192. package/src/chart.ts +62 -13
  193. package/src/components/amount-input.tsx +2 -1
  194. package/src/components/calendar-heatmap.tsx +8 -2
  195. package/src/components/currency-option-list.tsx +12 -4
  196. package/src/components/currency-select.tsx +3 -1
  197. package/src/components/danger-confirm.tsx +80 -27
  198. package/src/components/data-table-filter-popover.tsx +19 -11
  199. package/src/components/data-table-filters.ts +36 -8
  200. package/src/components/date-field.tsx +1 -1
  201. package/src/components/date-picker.tsx +0 -1
  202. package/src/components/date-range-picker.tsx +6 -22
  203. package/src/components/dialog-frame.tsx +12 -4
  204. package/src/components/mini-calendar.tsx +5 -2
  205. package/src/components/modal.tsx +36 -7
  206. package/src/components/nav-pills.tsx +1 -1
  207. package/src/components/number-input.tsx +4 -3
  208. package/src/components/series-chart-axes.ts +6 -1
  209. package/src/components/series-chart.tsx +78 -21
  210. package/src/components/stat-tile.tsx +32 -3
  211. package/src/components/treemap-labels.ts +11 -3
  212. package/src/components/treemap.tsx +65 -12
  213. package/src/currencies.ts +23 -3
  214. package/src/data-table.ts +1 -11
  215. package/src/demo/demo-countdown.ts +3 -8
  216. package/src/feedback/feedback-context-labels.ts +1 -0
  217. package/src/feedback/feedback-context.tsx +34 -2
  218. package/src/feedback/feedback-diagnostics.tsx +41 -0
  219. package/src/feedback/feedback-dialog.tsx +16 -4
  220. package/src/feedback/feedback-mutation.ts +12 -33
  221. package/src/feedback/feedback-public-list.tsx +1 -7
  222. package/src/feedback/feedback-record.ts +5 -0
  223. package/src/feedback/feedback-row-detail-labels.ts +1 -8
  224. package/src/feedback/feedback-row-detail.tsx +21 -58
  225. package/src/feedback/feedback-status-undo.ts +4 -18
  226. package/src/feedback/feedback-submit-dialog.tsx +15 -2
  227. package/src/feedback/feedback-submit.tsx +36 -3
  228. package/src/feedback/feedback-thread.tsx +0 -10
  229. package/src/feedback.ts +89 -48
  230. package/src/hooks/use-hotkey.ts +24 -1
  231. package/src/i18n/defaults.ts +0 -4
  232. package/src/i18n/german.ts +4 -19
  233. package/src/i18n/kit-labels.tsx +5 -14
  234. package/src/i18n/locales/es.ts +4 -19
  235. package/src/i18n/locales/fr.ts +4 -19
  236. package/src/i18n/locales/hu.ts +4 -19
  237. package/src/i18n/locales/it.ts +4 -20
  238. package/src/i18n/locales/zh.ts +4 -19
  239. package/src/i18n/review.ts +9 -4
  240. package/src/i18n/shared-labels.ts +0 -1
  241. package/src/index.ts +44 -114
  242. package/src/lib/dates.ts +24 -2
  243. package/src/lib/format.ts +66 -3
  244. package/src/lib/intl-cache.ts +4 -2
  245. package/src/lib/translation-review.ts +52 -0
  246. package/src/rhf/fields.tsx +2 -2
  247. package/src/search/search-index.ts +0 -15
  248. package/src/search.ts +8 -8
  249. package/src/shell/account-menu-language.tsx +7 -27
  250. package/src/shell/account-menu-text-size.tsx +13 -32
  251. package/src/shell/app-shell.tsx +0 -13
  252. package/src/shell/company-switcher.tsx +1 -1
  253. package/src/shell/dev-menu.tsx +1 -1
  254. package/src/shell/top-bar-parts-labels.ts +1 -1
  255. package/src/shell.ts +17 -44
  256. package/src/sync.ts +2 -15
  257. package/src/theme/chart-palette.ts +0 -2
  258. package/tokens.css +4 -4
  259. package/dist/billing/dialog-close.d.ts +0 -29
  260. package/dist/billing/dialog-close.js +0 -8
  261. package/dist/billing/dialog-close.js.map +0 -1
  262. package/dist/components/field-sync-labels.d.ts +0 -3
  263. package/dist/components/field-sync-labels.js +0 -12
  264. package/dist/components/field-sync-labels.js.map +0 -1
  265. package/dist/components/field-sync.d.ts +0 -186
  266. package/dist/components/field-sync.js +0 -269
  267. package/dist/components/field-sync.js.map +0 -1
  268. package/dist/components/section-nav.d.ts +0 -44
  269. package/dist/components/section-nav.js +0 -33
  270. package/dist/components/section-nav.js.map +0 -1
  271. package/dist/components/sign-chip-labels.d.ts +0 -3
  272. package/dist/components/sign-chip-labels.js +0 -11
  273. package/dist/components/sign-chip-labels.js.map +0 -1
  274. package/dist/components/sign-chip.d.ts +0 -77
  275. package/dist/components/sign-chip.js +0 -47
  276. package/dist/components/sign-chip.js.map +0 -1
  277. package/dist/lib/isin.d.ts +0 -38
  278. package/dist/lib/isin.js +0 -36
  279. package/dist/lib/isin.js.map +0 -1
  280. package/dist/shell/role-switcher.d.ts +0 -28
  281. package/dist/shell/role-switcher.js +0 -31
  282. package/dist/shell/role-switcher.js.map +0 -1
  283. package/src/billing/dialog-close.ts +0 -34
  284. package/src/components/field-sync-labels.ts +0 -14
  285. package/src/components/field-sync.tsx +0 -504
  286. package/src/components/section-nav.tsx +0 -72
  287. package/src/components/sign-chip-labels.ts +0 -13
  288. package/src/components/sign-chip.tsx +0 -133
  289. package/src/lib/isin.ts +0 -72
  290. package/src/shell/role-switcher.tsx +0 -53
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { useKitLabels, useKitLocale, useKitWeekStart, type LabelOverride } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\nimport { DEFAULT_CALENDAR_HEATMAP_LABELS } from \"./calendar-heatmap-labels\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n// Defined in ./calendar-heatmap-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CALENDAR_HEATMAP_LABELS };\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: LabelOverride<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => formatWithMinus(numberFormat, v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n const formatDay = (d: Date) => d.toLocaleDateString(locale, DAY_NAME_FORMAT);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-brand\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-caption leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = {\n gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))`,\n } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return d.toLocaleDateString(locale, { month: \"short\" });\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-micro leading-3 text-muted\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-micro leading-3 text-muted\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-micro uppercase tracking-wide text-muted\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && <div className=\"mb-2 text-xs text-muted\">{labels.truncated(dropped)}</div>}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-micro text-muted\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span key={l} data-legend-level={l} className=\"size-2.5 rounded-sm\" style={{ background: legendFill(l) }} />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
1
+ {"version":3,"sources":["../../src/components/calendar-heatmap.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, horizontalStep } from \"../lib/direction\";\nimport { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from \"../lib/dates\";\nimport { dateFormatter } from \"../lib/intl-cache\";\nimport { useKitLabels, useKitLocale, useKitWeekStart, type LabelOverride } from \"../i18n/kit-labels\";\nimport { lerpHex, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { Tooltip } from \"./tooltip\";\nimport type { WeekDay } from \"./mini-calendar\";\nimport { DEFAULT_CALENDAR_HEATMAP_LABELS } from \"./calendar-heatmap-labels\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/**\n * Every string the heatmap speaks — the `calendarHeatmap` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. Dates and\n * values arrive already formatted (in `locale`, and through `formatValue`), so the\n * functions only arrange them.\n */\nexport interface CalendarHeatmapLabels {\n /** Accessible name of the grid. */\n grid: string;\n /**\n * One day: its accessible name, and the tooltip unless `tooltip` is given. `date`\n * is the whole date in `locale` (\"Monday, 14 September 2026\"), `value` the day's\n * figure through `formatValue`.\n */\n day: (date: string, value: string) => string;\n /** The two ends of the legend's scale. */\n less: string;\n more: string;\n /** `maxDays` cut the window: how many of its earliest days are not drawn. */\n truncated: (count: number) => string;\n}\n\n// Defined in ./calendar-heatmap-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_CALENDAR_HEATMAP_LABELS };\n\n/** One day's figure. Several entries for one date are added up. */\nexport interface CalendarHeatmapDatum {\n /** \"YYYY-MM-DD\". */\n date: string;\n value: number;\n}\n\n/** What `tooltip` is told about the day under the pointer. */\nexport interface CalendarHeatmapDay {\n iso: string;\n date: Date;\n /** The day's figure; `0` when `data` has no entry for it (see `hasData`). */\n value: number;\n hasData: boolean;\n /** `0` = empty … `levels` = the busiest. */\n level: number;\n /** The whole date in `locale`. */\n formattedDate: string;\n /** `value` through `formatValue`. */\n formattedValue: string;\n}\n\n/**\n * `onSelect` is omitted from the `<div>` attributes for the reason `MiniCalendar` gives:\n * the DOM event of that name is a text-selection event, and this one answers with a date.\n */\nexport interface CalendarHeatmapProps extends Omit<ComponentPropsWithRef<\"div\">, \"onSelect\"> {\n data: readonly CalendarHeatmapDatum[];\n /** The window, inclusive, as \"YYYY-MM-DD\". */\n from: string;\n to: string;\n /**\n * `\"weeks\"` (default): the contribution-graph shape — one COLUMN per week, the\n * weekdays down the side, month names along the top; a year fits in ~53 small\n * squares and scrolls sideways where it does not. `\"month\"`: a 7-column calendar\n * with day numbers in large square cells, for a single month on a phone (switch on\n * `useMediaQuery` and narrow `from`/`to` to the month).\n */\n layout?: \"weeks\" | \"month\";\n /** The day shown as selected (\"YYYY-MM-DD\"). */\n selected?: string;\n /** Makes each day a button: a click (or Enter/Space) selects it — a drill-down into\n * that day's rows, a day panel. Without it the days are focusable but inert. */\n onSelect?: (iso: string) => void;\n /** How a value is written in the tooltip and the day's name. Default: `Intl.NumberFormat`\n * in `locale`. Pass a currency formatter for money. */\n formatValue?: (value: number) => string;\n /** Tooltip content for a day. Default: `labels.day(date, value)`. */\n tooltip?: (day: CalendarHeatmapDay) => ReactNode;\n /**\n * Tag each tooltip `data-private`, so the host's demo-mode rule blurs it (the\n * attribute `Tooltip redact` and `StatTile sensitive` carry). ON by default: a heatmap\n * is almost always the user's own figures, and a missing tag is silent — the grid\n * looks anonymous and the bubble spells the amount out on hover.\n */\n sensitive?: boolean;\n /** The value that reaches the top of the scale. Default: the largest value ON SCREEN,\n * so a peak outside the window does not wash the drawn days out to empty. */\n max?: number;\n /** How many non-empty intensity steps. Default 4. */\n levels?: number;\n /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this\n * mixed into `--bg-surface`, so the ramp follows the theme. */\n color?: string;\n /**\n * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest\n * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,\n * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`\n * colours are interpolated in JS, so the fill is a concrete hex and the day number's\n * ink (in `\"month\"`) is MEASURED against it; anything else (a `var()`) is mixed with\n * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.\n */\n colorFrom?: string;\n colorTo?: string;\n /**\n * A fill of the caller's own for a day with a value: handed the value and the\n * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.\n * Also paints the legend, at `max × level / levels`.\n */\n fill?: (value: number, max: number) => string;\n /** The fill of a day with no value (or a value of 0). Default: a wash of the text\n * colour over the surface, which reads as a cell on both themes. */\n emptyColor?: string;\n /**\n * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:\n *\n * - `\"to\"` (default): on `to`.\n * - `\"today\"`: on today.\n * - `\"latest\"`: on the last day that can carry a figure — today, or a later day that\n * actually has a value. A window running into the future (a report's \"all\" preset\n * ends a year ahead) then spends its cells on the days a person is looking for,\n * not on a year of empty ones.\n * - a `\"YYYY-MM-DD\"`: on that day.\n *\n * Always kept inside `from`…`to`.\n */\n anchor?: \"to\" | \"today\" | \"latest\" | (string & {});\n /**\n * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many\n * it left out (`labels.truncated`) rather than silently ending. Default: no cap.\n */\n maxDays?: number;\n /** Draw the Less … More legend. Default `true`. */\n legend?: boolean;\n /** BCP 47 tag for the dates, month and weekday names and numbers. */\n locale?: string;\n /** `0` = Sunday … `6` = Saturday. Default: the provider's, else the locale's. */\n weekStartsOn?: WeekDay;\n labels?: LabelOverride<CalendarHeatmapLabels>;\n className?: string;\n}\n\nconst DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {\n weekday: \"long\",\n day: \"numeric\",\n month: \"long\",\n year: \"numeric\",\n};\n/** A month's name over its first column (\"Sep\"). */\nconst MONTH_LABEL_FORMAT: Intl.DateTimeFormatOptions = { month: \"short\" };\n\n/** The empty day: a wash of the text colour, like `--bg-hover`, so it reads as a cell\n * on both themes without claiming any value. */\nconst EMPTY_FILL = \"color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface))\";\n\nconst weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;\n\n/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.\n * `latestData` is the latest ISO date carrying a value, or `\"\"`. */\nexport function heatmapWindowEnd(\n from: string,\n to: string,\n anchor: string,\n latestData: string,\n today: string = toLocalIso(new Date()),\n): string {\n let end: string;\n if (anchor === \"today\") end = today;\n else if (anchor === \"latest\") end = latestData > today ? latestData : today;\n else if (anchor === \"to\" || !parseIsoDate(anchor)) end = to;\n else end = anchor;\n // ISO dates sort as strings, so the clamp is plain comparison.\n if (end > to) end = to;\n if (end < from) end = from;\n return end;\n}\n\n/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */\nexport function heatmapLevel(value: number, max: number, levels: number): number {\n if (!(value > 0) || !(max > 0) || levels < 1) return 0;\n return Math.min(levels, Math.max(1, Math.ceil((value / max) * levels)));\n}\n\n/** How much of `color` a step mixes into the surface: 20% for the first step, 100% for\n * the last — the floor keeps the smallest value visibly apart from an empty day. */\nfunction levelPercent(level: number, levels: number): number {\n if (levels <= 1) return 100;\n return Math.round(20 + (80 * (level - 1)) / (levels - 1));\n}\n\nfunction levelFill(level: number, levels: number, color: string): string {\n if (level === 0) return EMPTY_FILL;\n return `color-mix(in oklab, ${color} ${levelPercent(level, levels)}%, var(--bg-surface))`;\n}\n\n/**\n * Days as a grid of shaded squares — how much happened on each, at a glance\n * (a contribution graph, a spending calendar).\n *\n * **Every day is a cell you can reach.** The grid is a `role=\"grid\"` with ONE tab\n * stop (the roving tabindex, as in `MiniCalendar`); the arrows move by the grid's own\n * geometry — in `\"weeks\"` ↑/↓ are the previous/next day and ←/→ a week, in `\"month\"`\n * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go\n * to the window's first/last day. Each cell is named with its whole date AND its value,\n * because the colour is not something a screen reader can say, and carries\n * `data-day=\"YYYY-MM-DD\"` as its identity for tests and for the host. Note that in\n * `\"weeks\"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all\n * Tuesdays…), not by date — query by `data-day`, not by position.\n *\n * The tooltip is portalled (a year of squares always sits in a sideways scroller) and\n * `data-private` unless `sensitive={false}`. Cell size in `\"weeks\"` is the CSS variable\n * `--heatmap-cell` (default `0.75rem`): `style={{ \"--heatmap-cell\": \"1rem\" }}`.\n */\nexport function CalendarHeatmap({\n data,\n from,\n to,\n layout = \"weeks\",\n selected,\n onSelect,\n formatValue: formatValueProp,\n tooltip,\n sensitive = true,\n max: maxProp,\n levels = 4,\n color = \"var(--brand)\",\n colorFrom,\n colorTo,\n fill: fillProp,\n emptyColor,\n anchor = \"to\",\n maxDays,\n legend = true,\n locale: localeProp,\n weekStartsOn,\n labels: labelsProp,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: CalendarHeatmapProps) {\n const labels = useKitLabels(\"calendarHeatmap\", DEFAULT_CALENDAR_HEATMAP_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const providerWeekStart = useKitWeekStart();\n const weekStart: WeekDay = weekStartsOn ?? providerWeekStart ?? localeWeekStart(locale);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const formatValue = formatValueProp ?? ((v: number) => formatWithMinus(numberFormat, v));\n\n const values = useMemo(() => {\n const m = new Map<string, number>();\n for (const p of data) m.set(p.date, (m.get(p.date) ?? 0) + p.value);\n return m;\n }, [data]);\n\n // The window, trimmed from the FRONT: the days a person looks for are the recent\n // ones, and a cap that kept the oldest would end the calendar before today.\n const latestData = useMemo(() => {\n let latest = \"\";\n for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;\n return latest;\n }, [values]);\n const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);\n\n const { days, dropped } = useMemo(() => {\n const start = parseIsoDate(from);\n const end = parseIsoDate(windowEnd);\n const list: Date[] = [];\n if (!start || !end || end < start) return { days: list, dropped: 0 };\n const cursor = new Date(end);\n const cap = maxDays && maxDays > 0 ? maxDays : Infinity;\n while (cursor >= start && list.length < cap) {\n list.unshift(new Date(cursor));\n cursor.setDate(cursor.getDate() - 1);\n }\n const first = list[0];\n const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;\n return { days: list, dropped: Math.max(0, trimmed) };\n }, [from, windowEnd, maxDays]);\n\n const firstIso = days.length ? toLocalIso(days[0]) : \"\";\n const lastIso = days.length ? toLocalIso(days[days.length - 1]) : \"\";\n\n const max = useMemo(() => {\n if (maxProp !== undefined) return maxProp;\n let m = 0;\n for (const d of days) m = Math.max(m, values.get(toLocalIso(d)) ?? 0);\n return m;\n }, [maxProp, days, values]);\n\n const clampIso = (iso: string) => (iso < firstIso ? firstIso : iso > lastIso ? lastIso : iso);\n\n // The roving tab stop: the selected day, else the latest one.\n const [activeState, setActiveIso] = useState(() => selected || lastIso);\n const [lastSelected, setLastSelected] = useState(selected);\n if (lastSelected !== selected) {\n setLastSelected(selected);\n if (selected) setActiveIso(selected);\n }\n const activeIso = days.length ? clampIso(activeState || lastIso) : \"\";\n\n const gridRef = useRef<HTMLDivElement>(null);\n const scrollerRef = useRef<HTMLDivElement>(null);\n const pendingFocus = useRef<string | null>(null);\n useEffect(() => {\n const iso = pendingFocus.current;\n if (!iso) return;\n pendingFocus.current = null;\n gridRef.current?.querySelector<HTMLElement>(`[data-day=\"${iso}\"]`)?.focus();\n });\n\n // Open the sideways scroller on the latest weeks, not the oldest. In RTL the end is\n // on the left, where browsers count scrollLeft down from 0.\n //\n // And KEEP it there while the box changes size — a sidebar or a contents rail that\n // appears after mount narrows the scroller, which leaves scrollLeft where it was and\n // so shows older weeks than it opened on. The pin holds only while the view sits at\n // the end: a user who scrolls back through the year unpins it, and a resize then\n // leaves their position alone. (Scrolling back to the end pins it again.)\n useLayoutEffect(() => {\n const el = scrollerRef.current;\n if (!el || layout !== \"weeks\") return;\n const toEnd = () => {\n el.scrollLeft = dirOf(el) === \"rtl\" ? -el.scrollWidth : el.scrollWidth;\n };\n toEnd();\n if (typeof ResizeObserver === \"undefined\") return;\n // |scrollLeft| covers both directions: 0 → max in LTR, 0 → -max in RTL.\n let pinned = true;\n const onScroll = () => {\n pinned = Math.abs(el.scrollLeft) >= el.scrollWidth - el.clientWidth - 1;\n };\n const ro = new ResizeObserver(() => {\n if (pinned) toEnd();\n });\n el.addEventListener(\"scroll\", onScroll, { passive: true });\n ro.observe(el);\n return () => {\n ro.disconnect();\n el.removeEventListener(\"scroll\", onScroll);\n };\n }, [layout, firstIso, lastIso]);\n\n // Every day cell names its date, so through the kit's formatter cache, as the month\n // labels are: `toLocaleDateString` built one per cell and render, 365 for a year's grid\n // (keksdose audit follow-up, 2026-11-01).\n const formatDay = (d: Date) => dateFormatter(locale, DAY_NAME_FORMAT).format(d);\n\n const weekdayLabels = useMemo(() => {\n const ref = new Date(2024, 0, 7 + weekStart); // 7 January 2024 was a Sunday\n const week = Array.from({ length: 7 }, (_, i) => {\n const d = new Date(ref);\n d.setDate(ref.getDate() + i);\n return d;\n });\n const name = (d: Date, weekday: \"short\" | \"narrow\" | \"long\") => d.toLocaleDateString(locale, { weekday });\n // The label column is 2rem at 10px type, room for about four characters. Most\n // locales' \"short\" is three or four (\"Thu\", \"jeu.\", \"czw.\"), but some have no\n // abbreviation at all — Arabic's short Thursday IS \"الخميس\", which overran the box\n // and was cut. Such a locale takes the \"narrow\" form (\"خ\") for ALL seven, so the\n // column never mixes two forms; the full name is on the aria-label either way.\n const tooLong = week.some((d) => [...name(d, \"short\")].length > 4);\n return week.map((d) => ({ short: name(d, tooLong ? \"narrow\" : \"short\"), long: name(d, \"long\") }));\n }, [locale, weekStart]);\n\n const lead = days.length ? weekdayIndex(days[0], weekStart) : 0;\n const weekCount = Math.ceil((lead + days.length) / 7);\n /** The day at position `i` of the week-aligned sequence (blanks before the first). */\n const dayAt = (i: number): Date | null => (i >= lead && i - lead < days.length ? days[i - lead] : null);\n\n const move = (e: KeyboardEvent<HTMLElement>) => {\n const current = parseIsoDate(activeIso);\n if (!current) return;\n const next = new Date(current);\n const step = horizontalStep(e.key, e.currentTarget);\n // In \"weeks\" a row is a weekday, so sideways is a week and up/down is a day.\n const across = layout === \"weeks\" ? 7 : 1;\n const down = layout === \"weeks\" ? 1 : 7;\n switch (step ? \"horizontal\" : e.key) {\n case \"horizontal\":\n next.setDate(next.getDate() + step * across);\n break;\n case \"ArrowUp\":\n next.setDate(next.getDate() - down);\n break;\n case \"ArrowDown\":\n next.setDate(next.getDate() + down);\n break;\n case \"PageUp\":\n next.setTime(addMonthsClamped(next, -1).getTime());\n break;\n case \"PageDown\":\n next.setTime(addMonthsClamped(next, 1).getTime());\n break;\n case \"Home\":\n next.setTime(days[0].getTime());\n break;\n case \"End\":\n next.setTime(days[days.length - 1].getTime());\n break;\n default:\n return;\n }\n e.preventDefault();\n const iso = clampIso(toLocalIso(next));\n if (iso === activeIso) return;\n pendingFocus.current = iso;\n setActiveIso(iso);\n };\n\n const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);\n const empty = emptyColor ?? EMPTY_FILL;\n const continuous = colorFrom !== undefined && colorTo !== undefined;\n const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;\n\n /** A day's fill, and the ink its number is set in (`\"month\"`). */\n const paint = (value: number, level: number): { fill: string; ink: string } => {\n if (level === 0) return { fill: empty, ink: \"var(--text-primary)\" };\n if (fillProp || continuous) {\n const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;\n const fill = fillProp\n ? fillProp(value, max)\n : hexRamp\n ? lerpHex(colorFrom!, colorTo!, t)\n : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;\n // Measured where the fill is a colour; a mix of references cannot be measured,\n // and the page's own text colour is the safe answer there.\n const measured = textOn(fill);\n return { fill, ink: measured === \"currentColor\" ? \"var(--text-primary)\" : measured };\n }\n // Light text once the fill is the stronger half of the mix — `--bg-surface` is the\n // colour the scale is mixed INTO, so it is the one that contrasts with its far end\n // on either theme.\n const strong = levelPercent(level, levels) >= 45;\n return {\n fill: levelFill(level, levels, color),\n ink: strong ? \"var(--bg-surface)\" : \"var(--text-primary)\",\n };\n };\n // The legend's swatches: a value that lands on each step (the top one on `max`).\n const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;\n\n const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {\n const iso = toLocalIso(d);\n const raw = values.get(iso);\n const value = raw ?? 0;\n const level = heatmapLevel(value, max, levels);\n const { fill, ink } = paint(value, level);\n const formattedDate = formatDay(d);\n const formattedValue = formatValue(value);\n const name = labels.day(formattedDate, formattedValue);\n const label = tooltip\n ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })\n : name;\n const isSelected = selected === iso;\n const shared = {\n role: \"gridcell\",\n \"data-day\": iso,\n \"data-level\": level,\n tabIndex: iso === activeIso ? 0 : -1,\n \"aria-label\": name,\n \"aria-selected\": onSelect ? isSelected : undefined,\n onKeyDown: move,\n onFocus: () => {\n if (iso !== activeIso) setActiveIso(iso);\n },\n style: { background: fill, color: ink },\n className: cn(\n cellClass,\n isSelected && \"ring-2 ring-[var(--text-primary)]\",\n onSelect && \"cursor-pointer hover:ring-1 hover:ring-brand\",\n ),\n } as const;\n const inner = withNumber ? (\n <span aria-hidden className=\"text-caption leading-none tabular-nums\">\n {numberFormat.format(d.getDate())}\n </span>\n ) : null;\n return (\n <Tooltip key={iso} label={label} portal redact={sensitive} role=\"none\" className={wrapClass}>\n {onSelect ? (\n <button type=\"button\" {...shared} onClick={() => onSelect(iso)}>\n {inner}\n </button>\n ) : (\n <div {...shared}>{inner}</div>\n )}\n </Tooltip>\n );\n };\n\n const gridName = ariaLabel ?? labels.grid;\n let body: ReactNode;\n if (layout === \"weeks\") {\n const columns = {\n gridTemplateColumns: `var(--heatmap-label, 2rem) repeat(${weekCount}, var(--heatmap-cell, 0.75rem))`,\n } as CSSProperties;\n const cellSize = \"size-[var(--heatmap-cell,0.75rem)]\";\n // A month's name over the first column that holds its 1st — or over the first\n // column, when the window opens far enough before the next month for it to fit.\n const monthLabels = Array.from({ length: weekCount }, (_, w) => {\n for (let r = 0; r < 7; r++) {\n const d = dayAt(w * 7 + r);\n if (d && (d.getDate() === 1 || (w === 0 && r === lead && d.getDate() <= 14))) {\n return dateFormatter(locale, MONTH_LABEL_FORMAT).format(d);\n }\n }\n return \"\";\n });\n body = (\n <div ref={scrollerRef} className=\"overflow-x-auto pb-1\">\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"inline-grid gap-0.5\">\n {/* Decoration: every cell's name already carries its month. */}\n <div aria-hidden className=\"grid gap-0.5 text-micro leading-3 text-muted\" style={columns}>\n <span />\n {monthLabels.map((m, w) => (\n <span key={w} className=\"overflow-visible whitespace-nowrap\">\n {m}\n </span>\n ))}\n </div>\n {Array.from({ length: 7 }, (_, r) => (\n <div key={r} role=\"row\" className=\"grid gap-0.5\" style={columns}>\n <div\n role=\"rowheader\"\n aria-label={weekdayLabels[r].long}\n className=\"overflow-hidden pe-1 text-micro leading-3 text-muted\"\n >\n {/* Every other weekday, as a contribution graph does: seven labels\n at this size are a smear. The name is there for all seven. */}\n {r % 2 === 1 ? weekdayLabels[r].short : \"\"}\n </div>\n {Array.from({ length: weekCount }, (_, w) => {\n const d = dayAt(w * 7 + r);\n return d ? (\n cell(d, cellSize, cn(cellSize, \"block rounded-sm\"), false)\n ) : (\n <div key={`pad-${w}`} role=\"gridcell\" className={cellSize} />\n );\n })}\n </div>\n ))}\n </div>\n </div>\n );\n } else {\n const rows = Array.from({ length: weekCount }, (_, w) => w);\n body = (\n <div ref={gridRef} role=\"grid\" aria-label={gridName} className=\"grid gap-1\">\n <div role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {weekdayLabels.map((w, i) => (\n <div\n key={i}\n role=\"columnheader\"\n aria-label={w.long}\n className=\"pb-0.5 text-center text-micro uppercase tracking-wide text-muted\"\n >\n {w.short}\n </div>\n ))}\n </div>\n {rows.map((w) => (\n <div key={w} role=\"row\" className=\"grid grid-cols-7 gap-1\">\n {Array.from({ length: 7 }, (_, c) => {\n const d = dayAt(w * 7 + c);\n return d ? (\n cell(d, \"aspect-square w-full\", \"flex h-full w-full items-center justify-center rounded\", true)\n ) : (\n <div key={`pad-${c}`} role=\"gridcell\" className=\"aspect-square\" />\n );\n })}\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"min-w-0\", className)}>\n {dropped > 0 && <div className=\"mb-2 text-xs text-muted\">{labels.truncated(dropped)}</div>}\n {body}\n {legend && (\n <div aria-hidden className=\"mt-2 flex items-center justify-end gap-1 text-micro text-muted\">\n <span className=\"me-0.5\">{labels.less}</span>\n {legendLevels.map((l) => (\n <span key={l} data-legend-level={l} className=\"size-2.5 rounded-sm\" style={{ background: legendFill(l) }} />\n ))}\n <span className=\"ms-0.5\">{labels.more}</span>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["_"]}
@@ -28,7 +28,8 @@ export interface CurrencyOptionListProps {
28
28
  /** A currency was picked. The host closes the list. */
29
29
  onChoose: (code: string) => void;
30
30
  /** Only these ISO codes, in this order; codes the kit has no definition for are
31
- * dropped. Default every one of {@link CURRENCIES}. */
31
+ * dropped. Default every one of {@link CURRENCIES} that is not `retired` (the chosen
32
+ * one too, if it is). A retired code named here stays: the list is the caller's. */
32
33
  codes?: readonly string[];
33
34
  /** Translated names by code — see {@link currencyName}. */
34
35
  currencyNames?: Record<string, string>;
@@ -46,13 +46,21 @@ function CurrencyOptionList({
46
46
  searchPlaceholder,
47
47
  sheetTitle
48
48
  }) {
49
+ const held = getCurrency(value);
50
+ const chosen = held?.code;
51
+ const heldRetired = held?.retired ? held.code : void 0;
49
52
  const codesKey = codes?.join(",");
50
53
  const pool = useMemo(
51
- () => codesKey === void 0 ? CURRENCIES : (
54
+ () => codesKey === void 0 ? (
55
+ // No currency that was replaced (the lev, since Bulgaria's euro: keksdose audit
56
+ // follow-up, 2026-11-01) — unless the field holds it, which then opens on its
57
+ // own row, checked, rather than on a list where nothing is chosen.
58
+ CURRENCIES.filter((c) => !c.retired || c.code === heldRetired)
59
+ ) : (
52
60
  // The caller's order; codes the kit has no definition for are dropped.
53
61
  codesKey.split(",").map((code) => getCurrency(code)).filter((c) => c !== void 0)
54
62
  ),
55
- [codesKey]
63
+ [codesKey, heldRetired]
56
64
  );
57
65
  const named = useMemo(
58
66
  () => pool.map((c) => ({ c, name: currencyName(c, currencyNames, locale) })),
@@ -69,7 +77,6 @@ function CurrencyOptionList({
69
77
  icon: /* @__PURE__ */ jsx(CurrencyFlag, { country: c.country })
70
78
  }));
71
79
  }, [named, query, locale]);
72
- const chosen = getCurrency(value)?.code;
73
80
  return /* @__PURE__ */ jsx(
74
81
  ComboboxPanel,
75
82
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/currency-option-list.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { matchesSearch, searchWords } from \"../lib/text-match\";\nimport { ComboboxPanel, type ComboboxCore, type ComboOption } from \"./combobox-core\";\nimport { CURRENCIES, type CurrencyOption } from \"../currencies\";\n\n// The one list both currency pickers render (round 0.34 Wave 2, DUP-4). CurrencySelect\n// re-exports the data under its own name, as it always has; the list is package-private.\n// Its own module, so AmountInput can render the list without a cycle through\n// currency-select. The data itself is ../currencies since 0.35: an entry of its own that\n// imports nothing, which an app's build can read in Node (keksdose's flag subset).\nexport { CURRENCIES };\nexport type { CurrencyOption };\n\nexport interface CurrencyFlagProps extends ComponentPropsWithRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\n \"fi inline-block w-5 h-[0.9375rem] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\",\n `fi-${country}`,\n className,\n )}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(c: CurrencyOption, names?: Record<string, string>, locale?: string): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\nexport interface CurrencyOptionListProps {\n /** The host's `useComboboxCore`: its trigger, its open state, its query. */\n core: ComboboxCore<string>;\n /** What the host's trigger names in `aria-controls`. */\n listboxId: string;\n /** The chosen code, if any — selected and checked in the list. */\n value: string | null | undefined;\n /** A currency was picked. The host closes the list. */\n onChoose: (code: string) => void;\n /** Only these ISO codes, in this order; codes the kit has no definition for are\n * dropped. Default every one of {@link CURRENCIES}. */\n codes?: readonly string[];\n /** Translated names by code — see {@link currencyName}. */\n currencyNames?: Record<string, string>;\n locale?: string;\n searchPlaceholder: string;\n /** The phone sheet's title — the field's name. */\n sheetTitle: ReactNode;\n}\n\n/**\n * The currency list — its filter and its rows — that CurrencySelect and AmountInput's\n * currency chip both render, on the picker family's `ComboboxPanel` (round 0.34 Wave 2,\n * DUP-4 + BUG-12).\n *\n * The chip's list was a hand copy of CurrencySelect's, and it had drifted: the name kept a\n * plain `truncate` where T1 made CurrencySelect's wrap from Large up, it took no `options`\n * subset, it had no check mark and no listbox id, and it was a fixed 16rem where #308 had\n * given CurrencySelect a panel that grows to the full names. And both were lists of plain\n * buttons under a trigger that promised a listbox. On the shared panel they are a listbox\n * of options, the chosen one selected, with the arrow keys, the live result count, the\n * phone sheet (live #200) and portal anchoring (dev#548: an `overflow` ancestor clipped\n * the absolute panel's first third away).\n *\n * The panel hangs from the trigger's END edge and grows toward the start, as wide as the\n * names need up to 20rem and the glass (feedback #308: a field with content after it is\n * not pushed past the page).\n */\nexport function CurrencyOptionList({\n core,\n listboxId,\n value,\n onChoose,\n codes,\n currencyNames,\n locale,\n searchPlaceholder,\n sheetTitle,\n}: CurrencyOptionListProps) {\n // Keyed on the codes' CONTENTS: an inline `options={[\"EUR\", \"CHF\"]}` is a new array\n // every render.\n const codesKey = codes?.join(\",\");\n const pool = useMemo(\n () =>\n codesKey === undefined\n ? CURRENCIES\n : // The caller's order; codes the kit has no definition for are dropped.\n codesKey\n .split(\",\")\n .map((code) => getCurrency(code))\n .filter((c): c is CurrencyOption => c !== undefined),\n [codesKey],\n );\n const named = useMemo(\n () => pool.map((c) => ({ c, name: currencyName(c, currencyNames, locale) })),\n [pool, currencyNames, locale],\n );\n const byCode = useMemo(() => new Map(named.map((n) => [n.c.code, n])), [named]);\n\n // The code, the name the row SHOWS (so a translated list is searchable in the language\n // it is written in) and the symbol — folded as every kit list folds (DUP-5).\n const query = core.query;\n const results = useMemo(() => {\n // Every word of the query, in the code, the name or the symbol (0.51.4).\n const words = searchWords(query, locale);\n const hits =\n words.length > 0 ? named.filter(({ c, name }) => matchesSearch(words, [c.code, name, c.symbol], locale)) : named;\n return hits.map(({ c, name }): ComboOption<string> => ({\n value: c.code,\n label: name,\n icon: <CurrencyFlag country={c.country} />,\n }));\n }, [named, query, locale]);\n\n const chosen = getCurrency(value)?.code;\n return (\n <ComboboxPanel\n // The core with the currency filter's rows in place of its own.\n core={{ ...core, results }}\n listboxId={listboxId}\n searchPlaceholder={searchPlaceholder}\n sheetTitle={sheetTitle}\n isSelected={(code) => code === chosen}\n onChoose={(o) => onChoose(o.value)}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n width={{ fit: true, maxRem: 20 }}\n align=\"end\"\n renderOption={(o) => {\n const row = byCode.get(o.value);\n if (!row) return o.label;\n return (\n <>\n <span className=\"w-8 shrink-0 font-mono text-muted\">{row.c.symbol}</span>{\" \"}\n <span className=\"shrink-0 font-medium text-primary\">{row.c.code}</span>{\" \"}\n {/* The name wraps from Large up (round 0.34 T1); the code beside it is what a\n reader scans for, and it never shrinks. */}\n <span className=\"min-w-0 truncate-until-large text-muted\">{row.name}</span>\n </>\n );\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/currency-option-list.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { matchesSearch, searchWords } from \"../lib/text-match\";\nimport { ComboboxPanel, type ComboboxCore, type ComboOption } from \"./combobox-core\";\nimport { CURRENCIES, type CurrencyOption } from \"../currencies\";\n\n// The one list both currency pickers render (round 0.34 Wave 2, DUP-4). CurrencySelect\n// re-exports the data under its own name, as it always has; the list is package-private.\n// Its own module, so AmountInput can render the list without a cycle through\n// currency-select. The data itself is ../currencies since 0.35: an entry of its own that\n// imports nothing, which an app's build can read in Node (keksdose's flag subset).\nexport { CURRENCIES };\nexport type { CurrencyOption };\n\nexport interface CurrencyFlagProps extends ComponentPropsWithRef<\"span\"> {\n /** ISO 3166-1 alpha-2, lowercase — the class flag-icons keys its sprite by. */\n country: string;\n}\n\nexport function CurrencyFlag({ country, className, ...rest }: CurrencyFlagProps) {\n return (\n <span\n {...rest}\n // Stays `aria-hidden` whatever a caller passes: the flag is a picture of the\n // code printed next to it, and a reader that met both would hear the currency\n // twice.\n aria-hidden\n className={cn(\n \"fi inline-block w-5 h-[0.9375rem] rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)] shrink-0\",\n `fi-${country}`,\n className,\n )}\n />\n );\n}\n\nconst CURRENCY_BY_CODE = new Map(CURRENCIES.map((c) => [c.code, c]));\n\nexport function getCurrency(code: string | null | undefined): CurrencyOption | undefined {\n return code ? CURRENCY_BY_CODE.get(code.toUpperCase()) : undefined;\n}\n\n/** The name to SHOW for a currency: the caller's translation if they have one for\n * that code; else, when a `locale` is known, the name `Intl.DisplayNames` has for\n * it in that locale; else the English name {@link CURRENCIES} carries. Shared with\n * AmountInput, which renders the same list from the same data.\n *\n * `Intl` only when a locale was actually GIVEN (by prop or `<UiKitProvider>`): an\n * app that never set one keeps the shipped English names it has always shown,\n * rather than whatever language the test runner or the browser happens to be in. */\nexport function currencyName(c: CurrencyOption, names?: Record<string, string>, locale?: string): string {\n const own = names?.[c.code];\n if (own !== undefined) return own;\n if (locale) {\n try {\n const shown = new Intl.DisplayNames([locale], { type: \"currency\" }).of(c.code);\n if (shown && shown !== c.code) return shown;\n } catch {\n // A malformed tag, or a runtime without `DisplayNames`: the shipped name.\n }\n }\n return c.name;\n}\n\nexport interface CurrencyOptionListProps {\n /** The host's `useComboboxCore`: its trigger, its open state, its query. */\n core: ComboboxCore<string>;\n /** What the host's trigger names in `aria-controls`. */\n listboxId: string;\n /** The chosen code, if any — selected and checked in the list. */\n value: string | null | undefined;\n /** A currency was picked. The host closes the list. */\n onChoose: (code: string) => void;\n /** Only these ISO codes, in this order; codes the kit has no definition for are\n * dropped. Default every one of {@link CURRENCIES} that is not `retired` (the chosen\n * one too, if it is). A retired code named here stays: the list is the caller's. */\n codes?: readonly string[];\n /** Translated names by code — see {@link currencyName}. */\n currencyNames?: Record<string, string>;\n locale?: string;\n searchPlaceholder: string;\n /** The phone sheet's title — the field's name. */\n sheetTitle: ReactNode;\n}\n\n/**\n * The currency list — its filter and its rows — that CurrencySelect and AmountInput's\n * currency chip both render, on the picker family's `ComboboxPanel` (round 0.34 Wave 2,\n * DUP-4 + BUG-12).\n *\n * The chip's list was a hand copy of CurrencySelect's, and it had drifted: the name kept a\n * plain `truncate` where T1 made CurrencySelect's wrap from Large up, it took no `options`\n * subset, it had no check mark and no listbox id, and it was a fixed 16rem where #308 had\n * given CurrencySelect a panel that grows to the full names. And both were lists of plain\n * buttons under a trigger that promised a listbox. On the shared panel they are a listbox\n * of options, the chosen one selected, with the arrow keys, the live result count, the\n * phone sheet (live #200) and portal anchoring (dev#548: an `overflow` ancestor clipped\n * the absolute panel's first third away).\n *\n * The panel hangs from the trigger's END edge and grows toward the start, as wide as the\n * names need up to 20rem and the glass (feedback #308: a field with content after it is\n * not pushed past the page).\n */\nexport function CurrencyOptionList({\n core,\n listboxId,\n value,\n onChoose,\n codes,\n currencyNames,\n locale,\n searchPlaceholder,\n sheetTitle,\n}: CurrencyOptionListProps) {\n const held = getCurrency(value);\n const chosen = held?.code;\n // A retired currency the field holds — the one the default list keeps for it. Its own\n // key, so a change of an ordinary value does not rebuild the rows and their names.\n const heldRetired = held?.retired ? held.code : undefined;\n // Keyed on the codes' CONTENTS: an inline `options={[\"EUR\", \"CHF\"]}` is a new array\n // every render.\n const codesKey = codes?.join(\",\");\n const pool = useMemo(\n () =>\n codesKey === undefined\n ? // No currency that was replaced (the lev, since Bulgaria's euro: keksdose audit\n // follow-up, 2026-11-01) — unless the field holds it, which then opens on its\n // own row, checked, rather than on a list where nothing is chosen.\n CURRENCIES.filter((c) => !c.retired || c.code === heldRetired)\n : // The caller's order; codes the kit has no definition for are dropped.\n codesKey\n .split(\",\")\n .map((code) => getCurrency(code))\n .filter((c): c is CurrencyOption => c !== undefined),\n [codesKey, heldRetired],\n );\n const named = useMemo(\n () => pool.map((c) => ({ c, name: currencyName(c, currencyNames, locale) })),\n [pool, currencyNames, locale],\n );\n const byCode = useMemo(() => new Map(named.map((n) => [n.c.code, n])), [named]);\n\n // The code, the name the row SHOWS (so a translated list is searchable in the language\n // it is written in) and the symbol — folded as every kit list folds (DUP-5).\n const query = core.query;\n const results = useMemo(() => {\n // Every word of the query, in the code, the name or the symbol (0.51.4).\n const words = searchWords(query, locale);\n const hits =\n words.length > 0 ? named.filter(({ c, name }) => matchesSearch(words, [c.code, name, c.symbol], locale)) : named;\n return hits.map(({ c, name }): ComboOption<string> => ({\n value: c.code,\n label: name,\n icon: <CurrencyFlag country={c.country} />,\n }));\n }, [named, query, locale]);\n\n return (\n <ComboboxPanel\n // The core with the currency filter's rows in place of its own.\n core={{ ...core, results }}\n listboxId={listboxId}\n searchPlaceholder={searchPlaceholder}\n sheetTitle={sheetTitle}\n isSelected={(code) => code === chosen}\n onChoose={(o) => onChoose(o.value)}\n showCreate={false}\n onCreate={() => {}}\n createContent={null}\n width={{ fit: true, maxRem: 20 }}\n align=\"end\"\n renderOption={(o) => {\n const row = byCode.get(o.value);\n if (!row) return o.label;\n return (\n <>\n <span className=\"w-8 shrink-0 font-mono text-muted\">{row.c.symbol}</span>{\" \"}\n <span className=\"shrink-0 font-medium text-primary\">{row.c.code}</span>{\" \"}\n {/* The name wraps from Large up (round 0.34 T1); the code beside it is what a\n reader scans for, and it never shrinks. */}\n <span className=\"min-w-0 truncate-until-large text-muted\">{row.name}</span>\n </>\n );\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -14,7 +14,9 @@ export interface CurrencySelectProps extends Omit<ComponentPropsWithoutRef<"div"
14
14
  /**
15
15
  * Restrict the pickable currencies to these ISO codes (e.g. only the
16
16
  * currencies actually present in a budget). Order is preserved. Defaults to
17
- * the full {@link CURRENCIES} list.
17
+ * the full {@link CURRENCIES} list but a `retired` currency (the lev, since
18
+ * Bulgaria's euro), which only a field that holds it lists. A retired code named
19
+ * here is offered: the list is the caller's.
18
20
  */
19
21
  options?: string[];
20
22
  /** Required and unanswered — {@link FIELD_INVALID}, the same rose border the
@@ -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 {\n assignRef,\n EndHintRow,\n LockedReason,\n useFieldMessages,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\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 { DEFAULT_COMMON_LABELS, DEFAULT_CURRENCY_LABELS, useKitLabels, useKitLocale } 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 /** As {@link Select}'s: no focus, no list, the settled look, no chevron. */\n disabled?: boolean;\n /**\n * Why the currency cannot be changed — {@link Select}'s `disabledReason` (keksdose,\n * 2026-10-23: a budget's currency for a guest or a viewer, and while offline, which\n * keksdose drew as a one-option `Select` to keep the reason and lost the flag). The\n * trigger stays focusable and `aria-disabled` (never the `disabled` attribute), keeps\n * its flag and code, opens nothing, and `onChange` is never called; the reason is a\n * line under the field at Large and on a touch screen, else in the kit\n * {@link Tooltip} (`disabledReasonDisplay`), and on the trigger's `aria-describedby`\n * (after a caption, before an error). It wears the settled look of a disabled field\n * and drops its chevron, as a locked Select does. It wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Select}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise. */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. {@link Select}'s `commit`. A currency\n * field inside a form with its own Save stays editable under the lock — leave this off\n * there, and put `commit` on the Save.\n */\n commit?: CommitScope;\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 disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\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 // The lock — Select's (`useLockReason`): the WriteLock's reason under `commit`, else\n // the caller's own. Locked or `disabled`, nothing opens the list or picks a value.\n const lock = useLockReason(commit, disabledReason);\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 CountrySelect does, so no frame shows a list that cannot be used.\n if (inert && open) setOpen(false);\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 // A lock's reason sits between the caption and the error, as on Select.\n reasonId: locked && lock.reasonId,\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 // The focusable kind of disabled while locked (Select'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((v) => !v);\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 }\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(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_BASE's `disabled:` look, for a trigger locked without being `disabled`\n // (the `:disabled` variants cannot match it) — Select's FIELD_LOCKED. No hover:\n // nothing here answers a press.\n locked && \"cursor-not-allowed bg-surface-2 text-muted hover:bg-surface-2\",\n isInvalid && FIELD_INVALID,\n )}\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\n className={cn(\n \"flex items-center gap-2 min-w-0\",\n // The settled look reaches the code too: `text-primary` here would outweigh it.\n !selected ? \"text-placeholder\" : inert ? \"text-muted\" : \"text-primary\",\n )}\n >\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 {/* No menu to drop while disabled or locked, so no chevron — the arrow is the one\n thing on the control that promises a choice (dev#474, as on Select). */}\n {!inert && <FieldChevron />}\n </button>\n );\n\n // Select's frame: the line under the trigger, or the Tooltip with a hidden copy for\n // `aria-describedby`. The wrapper `<div>` keeps `className`, so the layout holds.\n // `flex w-full items-stretch`: the trigger keeps the field's full width in it.\n const control = locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"flex w-full items-stretch\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\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}>{control}</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 if (inert) return;\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 {\n assignRef,\n EndHintRow,\n LockedReason,\n useFieldMessages,\n useLockReason,\n type DisabledReasonDisplay,\n type FieldHelpProps,\n} from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\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 { DEFAULT_COMMON_LABELS, DEFAULT_CURRENCY_LABELS, useKitLabels, useKitLocale } 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 but a `retired` currency (the lev, since\n * Bulgaria's euro), which only a field that holds it lists. A retired code named\n * here is offered: the list is the caller's.\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 /** As {@link Select}'s: no focus, no list, the settled look, no chevron. */\n disabled?: boolean;\n /**\n * Why the currency cannot be changed — {@link Select}'s `disabledReason` (keksdose,\n * 2026-10-23: a budget's currency for a guest or a viewer, and while offline, which\n * keksdose drew as a one-option `Select` to keep the reason and lost the flag). The\n * trigger stays focusable and `aria-disabled` (never the `disabled` attribute), keeps\n * its flag and code, opens nothing, and `onChange` is never called; the reason is a\n * line under the field at Large and on a touch screen, else in the kit\n * {@link Tooltip} (`disabledReasonDisplay`), and on the trigger's `aria-describedby`\n * (after a caption, before an error). It wears the settled look of a disabled field\n * and drops its chevron, as a locked Select does. It wins over `disabled`.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Select}'s `disabledReasonDisplay`: a line under the\n * field at Large and on a touch screen by default, the tooltip otherwise. */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * This picker COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is\n * locked the `disabledReason` way with the lock's reason (which wins over its own).\n * No provider, or an unlocked one: no effect. {@link Select}'s `commit`. A currency\n * field inside a form with its own Save stays editable under the lock — leave this off\n * there, and put `commit` on the Save.\n */\n commit?: CommitScope;\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 disabled,\n disabledReason,\n disabledReasonDisplay,\n commit,\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 // The lock — Select's (`useLockReason`): the WriteLock's reason under `commit`, else\n // the caller's own. Locked or `disabled`, nothing opens the list or picks a value.\n const lock = useLockReason(commit, disabledReason);\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 CountrySelect does, so no frame shows a list that cannot be used.\n if (inert && open) setOpen(false);\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 // A lock's reason sits between the caption and the error, as on Select.\n reasonId: locked && lock.reasonId,\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 // The focusable kind of disabled while locked (Select'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((v) => !v);\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 }\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(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n // FIELD_BASE's `disabled:` look, for a trigger locked without being `disabled`\n // (the `:disabled` variants cannot match it) — Select's FIELD_LOCKED. No hover:\n // nothing here answers a press.\n locked && \"cursor-not-allowed bg-surface-2 text-muted hover:bg-surface-2\",\n isInvalid && FIELD_INVALID,\n )}\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\n className={cn(\n \"flex items-center gap-2 min-w-0\",\n // The settled look reaches the code too: `text-primary` here would outweigh it.\n !selected ? \"text-placeholder\" : inert ? \"text-muted\" : \"text-primary\",\n )}\n >\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 {/* No menu to drop while disabled or locked, so no chevron — the arrow is the one\n thing on the control that promises a choice (dev#474, as on Select). */}\n {!inert && <FieldChevron />}\n </button>\n );\n\n // Select's frame: the line under the trigger, or the Tooltip with a hidden copy for\n // `aria-describedby`. The wrapper `<div>` keeps `className`, so the layout holds.\n // `flex w-full items-stretch`: the trigger keeps the field's full width in it.\n const control = locked ? (\n <LockedReason lock={lock} display={disabledReasonDisplay} className=\"flex w-full items-stretch\">\n {trigger}\n </LockedReason>\n ) : (\n trigger\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}>{control}</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 if (inert) return;\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":[]}
@@ -386,6 +386,27 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithRef<"div">, "
386
386
  * confirm, so a keyboard user can retry or Shift+Tab to the field to correct (0.24).
387
387
  */
388
388
  busy?: boolean;
389
+ /**
390
+ * Why the last confirm failed ("Wrong password"), in the tile beside the field it is
391
+ * about rather than in a toast away from it (keksdose audit follow-up, 2026-11-01:
392
+ * PasswordDangerCard toasted its `password_incorrect` for want of this). The app sets
393
+ * it from the failure and words it — a 400 is a wrong password, a 500 is not.
394
+ *
395
+ * Drawn as the field's own `error`, as {@link ReauthDialog} draws its own: under the
396
+ * password with `requirePassword`, else under the typed phrase — the field painted,
397
+ * `aria-invalid`, the message its `aria-describedby` — and in a tile with neither, as
398
+ * a line above the buttons. Announced as it appears (`role="alert"`): it arrives
399
+ * after the press, with focus on the confirm, where a field's description alone is
400
+ * not heard. Passed at all — even as `undefined` — the field keeps its box, as with
401
+ * Input's `error`, so the message going away does not rebuild the field mid-edit.
402
+ *
403
+ * Hidden while the next attempt runs, once the user edits the tick, the phrase or the
404
+ * password (it is about what was submitted), and when the tile arms or disarms, until
405
+ * the next confirm; shown again when a confirm fails with it still set. So a
406
+ * mutation's error can be handed over as it is:
407
+ * `error={mut.error ? message(mut.error) : undefined}`.
408
+ */
409
+ error?: ReactNode;
389
410
  /** The arm button is disabled. */
390
411
  disabled?: boolean;
391
412
  /**
@@ -445,4 +466,4 @@ export interface DangerConfirmProps extends Omit<ComponentPropsWithRef<"div">, "
445
466
  * destructive button itself), and disarming moves it back to the arm button, so the
446
467
  * keyboard user is never left on an element that just vanished.
447
468
  */
448
- export declare function DangerConfirm({ onConfirm, requirePassword, requireAcknowledge, consequences, phrase, phraseMatch, prompt, tone, confirmVariant, confirmDisabledReason, children, armLabel, confirmLabel, busy: busyProp, disabled, lockedReason: ownLockedReason, commit, armed: armedProp, onArmedChange, labels: labelsProp, className, ...rest }: DangerConfirmProps): import("react").JSX.Element;
469
+ export declare function DangerConfirm(props: DangerConfirmProps): import("react").JSX.Element;
@@ -7,6 +7,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
7
7
  import { Button } from "./button.js";
8
8
  import { Input } from "./input.js";
9
9
  import { Label } from "./field-base.js";
10
+ import { FIELD_ERROR_CLASS } from "./field-parts.js";
10
11
  import { Spinner } from "./spinner.js";
11
12
  import { Checkbox } from "./checkbox.js";
12
13
  import { Tooltip } from "./tooltip.js";
@@ -148,30 +149,32 @@ function firstSlotField(slot) {
148
149
  }
149
150
  return null;
150
151
  }
151
- function DangerConfirm({
152
- onConfirm,
153
- requirePassword,
154
- requireAcknowledge,
155
- consequences,
156
- phrase,
157
- phraseMatch = "trim",
158
- prompt,
159
- tone = "danger",
160
- confirmVariant,
161
- confirmDisabledReason,
162
- children,
163
- armLabel,
164
- confirmLabel,
165
- busy: busyProp,
166
- disabled,
167
- lockedReason: ownLockedReason,
168
- commit,
169
- armed: armedProp,
170
- onArmedChange,
171
- labels: labelsProp,
172
- className,
173
- ...rest
174
- }) {
152
+ function DangerConfirm(props) {
153
+ const {
154
+ onConfirm,
155
+ requirePassword,
156
+ requireAcknowledge,
157
+ consequences,
158
+ phrase,
159
+ phraseMatch = "trim",
160
+ prompt,
161
+ tone = "danger",
162
+ confirmVariant,
163
+ confirmDisabledReason,
164
+ children,
165
+ armLabel,
166
+ confirmLabel,
167
+ busy: busyProp,
168
+ error,
169
+ disabled,
170
+ lockedReason: ownLockedReason,
171
+ commit,
172
+ armed: armedProp,
173
+ onArmedChange,
174
+ labels: labelsProp,
175
+ className,
176
+ ...rest
177
+ } = props;
175
178
  const labels = useKitLabels("dangerConfirm", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);
176
179
  const lockedReason = useCommitReason(commit, ownLockedReason);
177
180
  const [armedState, setArmedState] = useState(false);
@@ -179,6 +182,7 @@ function DangerConfirm({
179
182
  const [password, setPassword] = useState("");
180
183
  const [typed, setTyped] = useState("");
181
184
  const [acknowledged, setAcknowledged] = useState(false);
185
+ const [errorStale, setErrorStale] = useState(false);
182
186
  const { pending, run } = usePromisePending();
183
187
  const busy = Boolean(busyProp) || pending;
184
188
  const promptId = useId();
@@ -194,6 +198,7 @@ function DangerConfirm({
194
198
  const [wasArmed, setWasArmed] = useState(armed);
195
199
  if (wasArmed !== armed) {
196
200
  setWasArmed(armed);
201
+ setErrorStale(true);
197
202
  if (!armed) {
198
203
  setPassword("");
199
204
  setTyped("");
@@ -254,8 +259,14 @@ function DangerConfirm({
254
259
  ...asksAcknowledge && { acknowledged: true }
255
260
  };
256
261
  confirmed.current = true;
262
+ setErrorStale(false);
257
263
  run(onConfirm(values.password, values), () => setArmed(false));
258
264
  };
265
+ const shownError = errorStale || busy ? void 0 : error;
266
+ const errorAlert = hasContent(shownError) ? /* @__PURE__ */ jsx("span", { role: "alert", children: shownError }) : void 0;
267
+ const errorOn = requirePassword ? "password" : phrase !== void 0 ? "phrase" : "line";
268
+ const fieldError = (field) => "error" in props && errorOn === field ? { error: errorAlert } : {};
269
+ const edited = () => setErrorStale(true);
259
270
  const firstField = asksAcknowledge ? "acknowledge" : phrase !== void 0 ? "phrase" : "password";
260
271
  const lines = (consequences ?? []).map(
261
272
  (item, index) => typeof item === "string" ? { key: item, text: item, severe: false } : { key: item.key ?? String(index), text: item.text, severe: Boolean(item.severe) }
@@ -312,7 +323,9 @@ function DangerConfirm({
312
323
  label: acknowledgeLabel,
313
324
  checked: acknowledged,
314
325
  onCheckedChange: (next) => {
315
- if (!busy) setAcknowledged(next);
326
+ if (busy) return;
327
+ setAcknowledged(next);
328
+ edited();
316
329
  }
317
330
  }
318
331
  ),
@@ -323,9 +336,13 @@ function DangerConfirm({
323
336
  target: phrase,
324
337
  match: phraseMatch,
325
338
  value: typed,
326
- onValueChange: setTyped,
339
+ onValueChange: (next) => {
340
+ setTyped(next);
341
+ edited();
342
+ },
327
343
  busy,
328
- labels: { phrase: labels.phrase, phrasePlaceholder: labels.phrasePlaceholder }
344
+ labels: { phrase: labels.phrase, phrasePlaceholder: labels.phrasePlaceholder },
345
+ ...fieldError("phrase")
329
346
  }
330
347
  ),
331
348
  requirePassword && /* @__PURE__ */ jsx(
@@ -335,9 +352,14 @@ function DangerConfirm({
335
352
  label: labels.password,
336
353
  value: password,
337
354
  busy,
338
- onValueChange: setPassword
355
+ onValueChange: (next) => {
356
+ setPassword(next);
357
+ edited();
358
+ },
359
+ ...fieldError("password")
339
360
  }
340
361
  ),
362
+ errorOn === "line" && errorAlert !== void 0 && /* @__PURE__ */ jsx("p", { className: cn(FIELD_ERROR_CLASS, "mt-0"), children: errorAlert }),
341
363
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
342
364
  /* @__PURE__ */ jsx(Button, { id: cancelId, type: "button", variant: "ghost", disabled: busy, onClick: () => setArmed(false), children: labels.cancel }),
343
365
  /* @__PURE__ */ jsxs(