@eifi1/ui-kit 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/hover-menu.d.ts +3 -0
  72. package/dist/components/hover-menu.js +2 -2
  73. package/dist/components/hover-menu.js.map +1 -1
  74. package/dist/components/iban-input.d.ts +66 -0
  75. package/dist/components/iban-input.js +125 -0
  76. package/dist/components/iban-input.js.map +1 -0
  77. package/dist/components/icon-picker.d.ts +12 -2
  78. package/dist/components/icon-picker.js +9 -2
  79. package/dist/components/icon-picker.js.map +1 -1
  80. package/dist/components/inline-edit-field.d.ts +167 -0
  81. package/dist/components/inline-edit-field.js +293 -0
  82. package/dist/components/inline-edit-field.js.map +1 -0
  83. package/dist/components/language-select.d.ts +127 -0
  84. package/dist/components/language-select.js +18 -0
  85. package/dist/components/language-select.js.map +1 -0
  86. package/dist/components/line-items.d.ts +25 -3
  87. package/dist/components/line-items.js +55 -46
  88. package/dist/components/line-items.js.map +1 -1
  89. package/dist/components/list.d.ts +25 -0
  90. package/dist/components/list.js +16 -5
  91. package/dist/components/list.js.map +1 -1
  92. package/dist/components/money-field.d.ts +20 -8
  93. package/dist/components/money-field.js.map +1 -1
  94. package/dist/components/month-picker.d.ts +52 -4
  95. package/dist/components/month-picker.js +290 -86
  96. package/dist/components/month-picker.js.map +1 -1
  97. package/dist/components/multi-select.d.ts +8 -1
  98. package/dist/components/multi-select.js +9 -4
  99. package/dist/components/multi-select.js.map +1 -1
  100. package/dist/components/number-field.d.ts +25 -13
  101. package/dist/components/number-field.js +6 -20
  102. package/dist/components/number-field.js.map +1 -1
  103. package/dist/components/number-input.d.ts +70 -13
  104. package/dist/components/number-input.js +90 -38
  105. package/dist/components/number-input.js.map +1 -1
  106. package/dist/components/numpad-sheet.d.ts +10 -8
  107. package/dist/components/one-time-code-input.d.ts +100 -0
  108. package/dist/components/one-time-code-input.js +137 -0
  109. package/dist/components/one-time-code-input.js.map +1 -0
  110. package/dist/components/phone-input.d.ts +66 -0
  111. package/dist/components/phone-input.js +171 -0
  112. package/dist/components/phone-input.js.map +1 -0
  113. package/dist/components/series-chart.d.ts +10 -8
  114. package/dist/components/settings-fields.d.ts +10 -8
  115. package/dist/components/share-card.d.ts +8 -6
  116. package/dist/components/sign-chip.d.ts +66 -0
  117. package/dist/components/sign-chip.js +39 -0
  118. package/dist/components/sign-chip.js.map +1 -0
  119. package/dist/components/swatch-picker.d.ts +27 -2
  120. package/dist/components/swatch-picker.js +73 -31
  121. package/dist/components/swatch-picker.js.map +1 -1
  122. package/dist/components/switch.d.ts +19 -0
  123. package/dist/components/switch.js +24 -6
  124. package/dist/components/switch.js.map +1 -1
  125. package/dist/components/text-link.d.ts +10 -8
  126. package/dist/components/tile-radio.d.ts +68 -13
  127. package/dist/components/tile-radio.js +3 -3
  128. package/dist/components/tile-radio.js.map +1 -1
  129. package/dist/components/time-input.d.ts +15 -9
  130. package/dist/components/time-input.js.map +1 -1
  131. package/dist/components/toggle-group.d.ts +47 -1
  132. package/dist/components/toggle-group.js +110 -69
  133. package/dist/components/toggle-group.js.map +1 -1
  134. package/dist/components/ui.d.ts +10 -8
  135. package/dist/components/ui.js +280 -78
  136. package/dist/components/ui.js.map +1 -1
  137. package/dist/components/use-table-state.d.ts +1 -2
  138. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  139. package/dist/data-table.d.ts +1 -2
  140. package/dist/data-table.js +2 -1
  141. package/dist/data-table.js.map +1 -1
  142. package/dist/feedback/feedback-attachment.d.ts +1 -1
  143. package/dist/feedback/feedback-attachment.js +210 -3
  144. package/dist/feedback/feedback-attachment.js.map +1 -1
  145. package/dist/feedback/feedback-dialog.d.ts +1 -1
  146. package/dist/feedback/feedback-dialog.js.map +1 -1
  147. package/dist/feedback/feedback-inbox.d.ts +1 -1
  148. package/dist/feedback/feedback-thread.d.ts +37 -3
  149. package/dist/feedback/feedback-thread.js +38 -9
  150. package/dist/feedback/feedback-thread.js.map +1 -1
  151. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  152. package/dist/feedback.d.ts +2 -2
  153. package/dist/hooks/use-file-drop.d.ts +10 -8
  154. package/dist/i18n/defaults.d.ts +10 -8
  155. package/dist/i18n/defaults.js +17 -2
  156. package/dist/i18n/defaults.js.map +1 -1
  157. package/dist/i18n/german.d.ts +10 -8
  158. package/dist/i18n/german.js +50 -5
  159. package/dist/i18n/german.js.map +1 -1
  160. package/dist/i18n/kit-labels.d.ts +10 -8
  161. package/dist/i18n/kit-labels.js +8 -2
  162. package/dist/i18n/kit-labels.js.map +1 -1
  163. package/dist/i18n/languages.d.ts +10 -8
  164. package/dist/i18n/locales/de-CH.d.ts +10 -8
  165. package/dist/i18n/locales/en.d.ts +10 -8
  166. package/dist/i18n/locales/en.js +9 -0
  167. package/dist/i18n/locales/en.js.map +1 -1
  168. package/dist/i18n/locales/es.d.ts +10 -8
  169. package/dist/i18n/locales/es.js +48 -5
  170. package/dist/i18n/locales/es.js.map +1 -1
  171. package/dist/i18n/locales/fr.d.ts +10 -8
  172. package/dist/i18n/locales/fr.js +48 -5
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.d.ts +10 -8
  175. package/dist/i18n/locales/hu.js +49 -5
  176. package/dist/i18n/locales/hu.js.map +1 -1
  177. package/dist/i18n/locales/it.d.ts +10 -8
  178. package/dist/i18n/locales/it.js +49 -5
  179. package/dist/i18n/locales/it.js.map +1 -1
  180. package/dist/i18n/locales/zh.d.ts +10 -8
  181. package/dist/i18n/locales/zh.js +48 -5
  182. package/dist/i18n/locales/zh.js.map +1 -1
  183. package/dist/i18n/review.d.ts +10 -8
  184. package/dist/i18n/review.js +8 -1
  185. package/dist/i18n/review.js.map +1 -1
  186. package/dist/index.d.ts +17 -10
  187. package/dist/index.js +57 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  190. package/dist/lib/countries.d.ts +43 -0
  191. package/dist/lib/countries.js +39 -0
  192. package/dist/lib/countries.js.map +1 -0
  193. package/dist/lib/decimal-marks.d.ts +46 -0
  194. package/dist/lib/decimal-marks.js +40 -0
  195. package/dist/lib/decimal-marks.js.map +1 -0
  196. package/dist/lib/iban.d.ts +98 -0
  197. package/dist/lib/iban.js +154 -0
  198. package/dist/lib/iban.js.map +1 -0
  199. package/dist/lib/isin.d.ts +30 -0
  200. package/dist/lib/isin.js +36 -0
  201. package/dist/lib/isin.js.map +1 -0
  202. package/dist/lib/phone.d.ts +69 -0
  203. package/dist/lib/phone.js +121 -0
  204. package/dist/lib/phone.js.map +1 -0
  205. package/dist/rhf/fields.d.ts +148 -11
  206. package/dist/rhf/fields.js +332 -3
  207. package/dist/rhf/fields.js.map +1 -1
  208. package/dist/rhf/form.d.ts +10 -8
  209. package/dist/rhf.d.ts +13 -9
  210. package/dist/rhf.js.map +1 -1
  211. package/dist/shell/app-shell.d.ts +10 -8
  212. package/dist/shell/top-bar-brand.d.ts +10 -8
  213. package/dist/shell/topbar-action-menu.d.ts +13 -1
  214. package/dist/shell/topbar-action-menu.js +31 -28
  215. package/dist/shell/topbar-action-menu.js.map +1 -1
  216. package/dist/shell.d.ts +10 -8
  217. package/dist/tour/tour.js +20 -24
  218. package/dist/tour/tour.js.map +1 -1
  219. package/dist/wizard/stepper-nav.d.ts +10 -8
  220. package/dist/wizard.d.ts +10 -8
  221. package/package.json +1 -1
  222. package/src/components/account-chips.tsx +17 -3
  223. package/src/components/account-settings.tsx +92 -26
  224. package/src/components/amount-input.tsx +49 -56
  225. package/src/components/autocomplete.tsx +133 -110
  226. package/src/components/checkbox-group.tsx +229 -0
  227. package/src/components/checkbox.tsx +72 -8
  228. package/src/components/chip.tsx +80 -10
  229. package/src/components/combobox.tsx +292 -253
  230. package/src/components/country-select.tsx +443 -0
  231. package/src/components/currency-select.tsx +109 -33
  232. package/src/components/danger-confirm.tsx +345 -77
  233. package/src/components/data-table-labels.ts +16 -1
  234. package/src/components/data-table.tsx +175 -6
  235. package/src/components/date-picker.tsx +173 -62
  236. package/src/components/entity-combobox.tsx +107 -87
  237. package/src/components/field-anatomy.tsx +190 -0
  238. package/src/components/field-parts.tsx +184 -0
  239. package/src/components/file-button.tsx +36 -3
  240. package/src/components/file-dropzone.tsx +106 -38
  241. package/src/components/form-actions.tsx +115 -2
  242. package/src/components/hover-menu.tsx +7 -4
  243. package/src/components/iban-input.tsx +231 -0
  244. package/src/components/icon-picker.tsx +26 -4
  245. package/src/components/inline-edit-field.tsx +533 -0
  246. package/src/components/language-select.tsx +78 -0
  247. package/src/components/line-items.tsx +35 -2
  248. package/src/components/list.tsx +55 -6
  249. package/src/components/money-field.tsx +10 -0
  250. package/src/components/month-picker.tsx +399 -63
  251. package/src/components/multi-select.tsx +80 -58
  252. package/src/components/number-field.tsx +28 -46
  253. package/src/components/number-input.tsx +210 -43
  254. package/src/components/one-time-code-input.tsx +288 -0
  255. package/src/components/phone-input.tsx +299 -0
  256. package/src/components/sign-chip.tsx +103 -0
  257. package/src/components/swatch-picker.tsx +118 -30
  258. package/src/components/switch.tsx +58 -7
  259. package/src/components/tile-radio.tsx +71 -16
  260. package/src/components/time-input.tsx +8 -1
  261. package/src/components/toggle-group.tsx +206 -73
  262. package/src/components/ui.tsx +472 -105
  263. package/src/data-table.ts +2 -1
  264. package/src/feedback/feedback-attachment.tsx +313 -9
  265. package/src/feedback/feedback-dialog.tsx +2 -0
  266. package/src/feedback/feedback-thread.tsx +66 -5
  267. package/src/i18n/defaults.ts +16 -1
  268. package/src/i18n/german.ts +47 -0
  269. package/src/i18n/kit-labels.tsx +78 -2
  270. package/src/i18n/locales/en.ts +10 -0
  271. package/src/i18n/locales/es.ts +46 -0
  272. package/src/i18n/locales/fr.ts +45 -0
  273. package/src/i18n/locales/hu.ts +46 -0
  274. package/src/i18n/locales/it.ts +47 -0
  275. package/src/i18n/locales/zh.ts +44 -0
  276. package/src/i18n/review.ts +7 -0
  277. package/src/index.ts +53 -3
  278. package/src/lib/countries.ts +91 -0
  279. package/src/lib/decimal-marks.ts +99 -0
  280. package/src/lib/iban.ts +171 -0
  281. package/src/lib/isin.ts +62 -0
  282. package/src/lib/phone.ts +211 -0
  283. package/src/rhf/fields.tsx +606 -9
  284. package/src/rhf.ts +2 -1
  285. package/src/shell/topbar-action-menu.tsx +53 -29
  286. package/src/tour/tour.tsx +66 -28
@@ -2,8 +2,10 @@ import * as react from 'react';
2
2
  import { ComponentProps, Ref } from 'react';
3
3
  import { AmountInput } from './amount-input.js';
4
4
  import './calculator.js';
5
- import '../kit-labels-D1J2_jyD.js';
6
- import './data-table-labels.js';
5
+ import '../kit-labels-R8mIAc9N.js';
6
+ import '../data-table-labels-B7OdnM0S.js';
7
+ import './data-table-sort.js';
8
+ import './swipeable-row.js';
7
9
  import './mini-calendar.js';
8
10
  import './calendar-heatmap.js';
9
11
  import './popover.js';
@@ -26,7 +28,6 @@ import './sparkline.js';
26
28
  import './stat-tile.js';
27
29
  import './signature-pad.js';
28
30
  import './password-strength.js';
29
- import './danger-confirm.js';
30
31
  import './swatch-picker.js';
31
32
  import './tile-radio.js';
32
33
  import './icon-picker.js';
@@ -34,11 +35,10 @@ import './dialog-frame.js';
34
35
  import './modal.js';
35
36
  import '../hooks/use-search-param-state.js';
36
37
  import './measured-grid.js';
37
- import '../feedback-attachment-WFiXJ8We.js';
38
+ import '../feedback-attachment-NhUm7Zga.js';
38
39
  import '../feedback/feedback-thread.js';
39
40
  import '../feedback/feedback-inbox.js';
40
41
  import './account-settings-labels.js';
41
- import './confirm-dialog.js';
42
42
  import './floating-panel.js';
43
43
  import './bulk-action-bar.js';
44
44
  import './list.js';
@@ -57,14 +57,16 @@ import './write-lock.js';
57
57
  import './legal.js';
58
58
  import './account-chips.js';
59
59
  import '../lib/format.js';
60
- import '../data-table-3mi2R3so.js';
61
- import './data-table-sort.js';
62
- import './swipeable-row.js';
63
60
  import './reauth-dialog.js';
64
61
  import './server-wake.js';
65
62
  import '../lib/server-wake.js';
66
63
  import './translation-review-labels.js';
67
64
  import '../lib/translation-review.js';
65
+ import './country-select.js';
66
+ import './inline-edit-field.js';
67
+ import '../lib/iban.js';
68
+ import '../lib/phone.js';
69
+ import './sign-chip.js';
68
70
  import './numpad-sheet.js';
69
71
 
70
72
  type AmountInputProps = ComponentProps<typeof AmountInput>;
@@ -93,6 +95,16 @@ interface MoneyFieldProps extends Omit<AmountInputProps, "value" | "onChange" |
93
95
  * rounding to the currency's minor unit and `min` / `max` are AmountInput's. A caller-
94
96
  * owned sign (`negative`) is not offered: the figure here carries its own minus.
95
97
  *
98
+ * So are two props a table cell needs (keksdose K4, K6): `error`, the message under the
99
+ * field that paints and describes it like {@link Input}'s, and `calculator={false}`,
100
+ * for an amount in a close-on-blur inline editor, where a click on the calculator icon
101
+ * blurs the field and tears the editor down before the popover can open (typed
102
+ * calculations still evaluate).
103
+ *
104
+ * This, not a `NumberField` with `digits={2}`, is the number-valued field for money:
105
+ * it settles to the CURRENCY's minor unit (JPY 0, CHF 2) and `digits` takes a unit
106
+ * price's finer scale (kastlan 5).
107
+ *
96
108
  * ```tsx
97
109
  * <MoneyField ariaLabel="Amount" currency="CHF" value={row.amount}
98
110
  * onCommit={(amount) => updateRow(row.id, { amount })} />
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/money-field.tsx"],"sourcesContent":["import { useState, type ComponentProps, type Ref } from \"react\";\nimport { AmountInput } from \"./amount-input\";\n\ntype AmountInputProps = ComponentProps<typeof AmountInput>;\n\nexport interface MoneyFieldProps\n extends Omit<AmountInputProps, \"value\" | \"onChange\" | \"onCommit\" | \"negative\" | \"onNegativeChange\" | \"ref\"> {\n /** The amount, or `null` for an empty field. */\n value: number | null;\n /**\n * The settled amount — on blur, on Enter, on the numpad's \"=\" and on a calculator\n * result; never a half-typed \"1200+\". Rounded to the currency's minor unit (or\n * `digits`) and clamped to `min` / `max`, like {@link AmountInput}. `null` when the\n * field was emptied. Not fired when the settled amount equals `value`, so an\n * Enter-then-blur saves once.\n */\n onCommit: (value: number | null) => void;\n ref?: Ref<HTMLInputElement>;\n}\n\n/** The number a settled text reads as: `null` for empty, `undefined` for no number\n * (a lone \"-\" or \".\"). */\nfunction parseSettled(text: string): number | null | undefined {\n const t = text.trim();\n if (t === \"\") return null;\n const n = Number(t);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/**\n * {@link AmountInput} with a NUMBER in and a number out, for money edited outside a\n * form — a budget row, a table cell (kastlan 52: new-budget-page edited a CHF amount per\n * row with `NumberField` because AmountInput is string-valued and reports every\n * keystroke, \"1200+\" included, so a number-state row needed its own parse-and-settle\n * glue). This is that glue, the same one `RhfMoneyField` has: the text being typed\n * lives here, and only a settled figure reaches `onCommit`.\n *\n * The currency chip, the calculator, the numpad, the locale's decimal mark, the\n * rounding to the currency's minor unit and `min` / `max` are AmountInput's. A caller-\n * owned sign (`negative`) is not offered: the figure here carries its own minus.\n *\n * ```tsx\n * <MoneyField ariaLabel=\"Amount\" currency=\"CHF\" value={row.amount}\n * onCommit={(amount) => updateRow(row.id, { amount })} />\n * ```\n */\nexport function MoneyField({ value, onCommit, ref, ...props }: MoneyFieldProps) {\n const external = value === null ? \"\" : String(value);\n // The draft is re-read from `value` only when `value` moves away from what the draft\n // already says (a reset, another row's save), so a commit's own echo keeps the\n // typist's spelling (\"12.50\" stays \"12.50\" when 12.5 comes back).\n const [draft, setDraft] = useState(external);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (parseSettled(draft) !== value) setDraft(external);\n }\n return (\n <AmountInput\n {...props}\n ref={ref}\n value={draft}\n onChange={setDraft}\n onCommit={(text) => {\n const next = parseSettled(text);\n // No number (\"-\" alone): back to the stored amount rather than a guess.\n if (next === undefined) {\n setDraft(external);\n return;\n }\n if (next !== value) onCommit(next);\n }}\n />\n );\n}\n"],"mappings":";AA0DI;AA1DJ,SAAS,gBAA+C;AACxD,SAAS,mBAAmB;AAqB5B,SAAS,aAAa,MAAyC;AAC7D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAmBO,SAAS,WAAW,EAAE,OAAO,UAAU,KAAK,GAAG,MAAM,GAAoB;AAC9E,QAAM,WAAW,UAAU,OAAO,KAAK,OAAO,KAAK;AAInD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,QAAQ;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,MAAI,SAAS,OAAO;AAClB,YAAQ,KAAK;AACb,QAAI,aAAa,KAAK,MAAM,MAAO,UAAS,QAAQ;AAAA,EACtD;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU,CAAC,SAAS;AAClB,cAAM,OAAO,aAAa,IAAI;AAE9B,YAAI,SAAS,QAAW;AACtB,mBAAS,QAAQ;AACjB;AAAA,QACF;AACA,YAAI,SAAS,MAAO,UAAS,IAAI;AAAA,MACnC;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/money-field.tsx"],"sourcesContent":["import { useState, type ComponentProps, type Ref } from \"react\";\nimport { AmountInput } from \"./amount-input\";\n\ntype AmountInputProps = ComponentProps<typeof AmountInput>;\n\nexport interface MoneyFieldProps\n extends Omit<AmountInputProps, \"value\" | \"onChange\" | \"onCommit\" | \"negative\" | \"onNegativeChange\" | \"ref\"> {\n /** The amount, or `null` for an empty field. */\n value: number | null;\n /**\n * The settled amount — on blur, on Enter, on the numpad's \"=\" and on a calculator\n * result; never a half-typed \"1200+\". Rounded to the currency's minor unit (or\n * `digits`) and clamped to `min` / `max`, like {@link AmountInput}. `null` when the\n * field was emptied. Not fired when the settled amount equals `value`, so an\n * Enter-then-blur saves once.\n */\n onCommit: (value: number | null) => void;\n ref?: Ref<HTMLInputElement>;\n}\n\n/** The number a settled text reads as: `null` for empty, `undefined` for no number\n * (a lone \"-\" or \".\"). */\nfunction parseSettled(text: string): number | null | undefined {\n const t = text.trim();\n if (t === \"\") return null;\n const n = Number(t);\n return Number.isFinite(n) ? n : undefined;\n}\n\n/**\n * {@link AmountInput} with a NUMBER in and a number out, for money edited outside a\n * form — a budget row, a table cell (kastlan 52: new-budget-page edited a CHF amount per\n * row with `NumberField` because AmountInput is string-valued and reports every\n * keystroke, \"1200+\" included, so a number-state row needed its own parse-and-settle\n * glue). This is that glue, the same one `RhfMoneyField` has: the text being typed\n * lives here, and only a settled figure reaches `onCommit`.\n *\n * The currency chip, the calculator, the numpad, the locale's decimal mark, the\n * rounding to the currency's minor unit and `min` / `max` are AmountInput's. A caller-\n * owned sign (`negative`) is not offered: the figure here carries its own minus.\n *\n * So are two props a table cell needs (keksdose K4, K6): `error`, the message under the\n * field that paints and describes it like {@link Input}'s, and `calculator={false}`,\n * for an amount in a close-on-blur inline editor, where a click on the calculator icon\n * blurs the field and tears the editor down before the popover can open (typed\n * calculations still evaluate).\n *\n * This, not a `NumberField` with `digits={2}`, is the number-valued field for money:\n * it settles to the CURRENCY's minor unit (JPY 0, CHF 2) and `digits` takes a unit\n * price's finer scale (kastlan 5).\n *\n * ```tsx\n * <MoneyField ariaLabel=\"Amount\" currency=\"CHF\" value={row.amount}\n * onCommit={(amount) => updateRow(row.id, { amount })} />\n * ```\n */\nexport function MoneyField({ value, onCommit, ref, ...props }: MoneyFieldProps) {\n const external = value === null ? \"\" : String(value);\n // The draft is re-read from `value` only when `value` moves away from what the draft\n // already says (a reset, another row's save), so a commit's own echo keeps the\n // typist's spelling (\"12.50\" stays \"12.50\" when 12.5 comes back).\n const [draft, setDraft] = useState(external);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (parseSettled(draft) !== value) setDraft(external);\n }\n return (\n <AmountInput\n {...props}\n ref={ref}\n value={draft}\n onChange={setDraft}\n onCommit={(text) => {\n const next = parseSettled(text);\n // No number (\"-\" alone): back to the stored amount rather than a guess.\n if (next === undefined) {\n setDraft(external);\n return;\n }\n if (next !== value) onCommit(next);\n }}\n />\n );\n}\n"],"mappings":";AAoEI;AApEJ,SAAS,gBAA+C;AACxD,SAAS,mBAAmB;AAqB5B,SAAS,aAAa,MAAyC;AAC7D,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AA6BO,SAAS,WAAW,EAAE,OAAO,UAAU,KAAK,GAAG,MAAM,GAAoB;AAC9E,QAAM,WAAW,UAAU,OAAO,KAAK,OAAO,KAAK;AAInD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,QAAQ;AAC3C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,MAAI,SAAS,OAAO;AAClB,YAAQ,KAAK;AACb,QAAI,aAAa,KAAK,MAAM,MAAO,UAAS,QAAQ;AAAA,EACtD;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU,CAAC,SAAS;AAClB,cAAM,OAAO,aAAa,IAAI;AAE9B,YAAI,SAAS,QAAW;AACtB,mBAAS,QAAQ;AACjB;AAAA,QACF;AACA,YAAI,SAAS,MAAO,UAAS,IAAI;AAAA,MACnC;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -23,6 +23,15 @@ interface MonthPickerLabels {
23
23
  nextMonth: string;
24
24
  /** `variant="stepper"`: the button that jumps to the current month. Visible text. */
25
25
  today: string;
26
+ /** `mode="year"`: the popover's accessible name. */
27
+ yearPanel: string;
28
+ /** `mode="year"`: the icon-only arrows that page the year grid back and forward by a
29
+ * page of twelve years. */
30
+ earlierYears: string;
31
+ laterYears: string;
32
+ /** `mode="year"`, `variant="stepper"`: the button that jumps to the current year.
33
+ * Visible text. */
34
+ thisYear: string;
26
35
  }
27
36
  /** The English starting point every override is merged onto. Exported (unlike the
28
37
  * calendar's) because it is asked for by name: a host building its own translation
@@ -64,7 +73,45 @@ interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChan
64
73
  disabled?: boolean;
65
74
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
66
75
  invalid?: boolean;
67
- /** Intl options for the trigger's text (default: long month + numeric year). */
76
+ /**
77
+ * What is wrong with the month, in the caller's words — {@link Input}'s `error`
78
+ * (keksdose K4): under the field, attached to the trigger with `aria-describedby`
79
+ * (merged, after any caption), and implies `invalid`. Passing the key keeps the
80
+ * field's box while there is no message, so the trigger is never remounted.
81
+ */
82
+ error?: ReactNode;
83
+ /** Standing advice, as on a {@link Select}: text is a caption under the field; a
84
+ * {@link FieldHint} rides the label line. */
85
+ hint?: ReactNode;
86
+ /**
87
+ * `"md"` (default): the form field. `"sm"`: the toolbar trigger — 36px tall (an
88
+ * `IconButton`'s height) and as wide as the month it shows, for a picker that sits
89
+ * between two icon buttons in a page header (keksdose K13: the budget header wrote
90
+ * `triggerClassName="h-9 w-auto … py-0"` by hand, live #260 — 34px between two 36px
91
+ * buttons, *"the kind of 2px that reads as sloppy"*). Unlabelled pickers only, as on
92
+ * {@link Select}: a floating label needs the tall box to float in. The width follows
93
+ * the month's name; add a `min-w-*` in `triggerClassName` if the buttons beside it
94
+ * must not move as it changes.
95
+ */
96
+ size?: "sm" | "md";
97
+ /**
98
+ * `"month"` (default): the value is a month, `"YYYY-MM"`.
99
+ *
100
+ * `"year"`: the value is a YEAR, `"YYYY"`, and the panel is a grid of years — kastlan's
101
+ * budget fiscal year (a `Select` of 2020 to three years ahead) and keksdose's tax tab
102
+ * ("this year and four before", tax-tab.tsx:33). `min` / `max` are read as their year
103
+ * (`"2022"`, or any longer key). A bounded span of twelve years or fewer is shown
104
+ * whole, with no paging; otherwise the grid pages twelve years at a time (PageUp /
105
+ * PageDown, or the arrows), starting from `min` when there is one. The stepper
106
+ * variant steps a year, and its Today button reads "This year".
107
+ *
108
+ * A string rather than a number, for the reason the month key is one: it is the
109
+ * same type across both modes, it crosses a URL as is, and it sorts. A numeric
110
+ * caller converts with `String(year)` and `Number(key)`.
111
+ */
112
+ mode?: "month" | "year";
113
+ /** Intl options for the trigger's text (default: long month + numeric year, or the
114
+ * numeric year in `mode="year"`). Win over the provider's `formatDate`. */
68
115
  formatOptions?: Intl.DateTimeFormatOptions;
69
116
  /**
70
117
  * The month to ring as "now", as `"YYYY-MM"`. Defaults to the local calendar's
@@ -84,8 +131,9 @@ interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChan
84
131
  * `"stepper"`: the header of a month PAGE — the month as a heading that opens the
85
132
  * grid, then Today and ‹ › stepping the value by one month. What a calendar page
86
133
  * assembled by hand around the field (overriding its trigger to look like a
87
- * heading). `label`, `placeholder`, `invalid` and `triggerClassName` are the field's
88
- * and are not drawn here; with no `value` the arrows step from the current month.
134
+ * heading). `label`, `placeholder`, `invalid`, `hint`, `error`, `size` and
135
+ * `triggerClassName` are the field's and are not drawn here; with no `value` the
136
+ * arrows step from the current month (or year).
89
137
  */
90
138
  variant?: "field" | "stepper";
91
139
  /** `variant="stepper"`: the heading level the month is set in (`<h2>` by default), or
@@ -94,6 +142,6 @@ interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChan
94
142
  /** `variant="stepper"`: draw the Today button. Default `true`. */
95
143
  showToday?: boolean;
96
144
  }
97
- declare function MonthPicker({ value, onChange, locale: localeProp, min, max, label, placeholder, disabled, invalid, formatOptions, currentMonth, labels: labelsProp, className, triggerClassName, variant, headingLevel, showToday, ...rest }: MonthPickerProps): react.JSX.Element;
145
+ declare function MonthPicker(props: MonthPickerProps): react.JSX.Element;
98
146
 
99
147
  export { DEFAULT_MONTH_PICKER_LABELS, MonthPicker, type MonthPickerLabels, type MonthPickerProps };
@@ -5,10 +5,11 @@ import { Calendar, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { dirOf, horizontalStep } from "../lib/direction.js";
7
7
  import { monthKey, pad } from "../lib/dates.js";
8
- import { Button, FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from "./ui.js";
8
+ import { Button, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from "./ui.js";
9
9
  import { Popover } from "./popover.js";
10
10
  import { splitTriggerAria } from "./trigger-aria.js";
11
- import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
11
+ import { useKitDateFormatter, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
12
+ import { FieldBox, FieldLabelLine, hasContent, useFieldMessages } from "./field-anatomy.js";
12
13
  const DEFAULT_MONTH_PICKER_LABELS = {
13
14
  previousYear: "Previous year",
14
15
  nextYear: "Next year",
@@ -16,7 +17,11 @@ const DEFAULT_MONTH_PICKER_LABELS = {
16
17
  month: (monthYear) => monthYear,
17
18
  previousMonth: "Previous month",
18
19
  nextMonth: "Next month",
19
- today: "Today"
20
+ today: "Today",
21
+ yearPanel: "Choose a year",
22
+ earlierYears: "Earlier years",
23
+ laterYears: "Later years",
24
+ thisYear: "This year"
20
25
  };
21
26
  function useMonthPickerLabels(partial) {
22
27
  return useKitLabels("monthPicker", DEFAULT_MONTH_PICKER_LABELS, partial);
@@ -44,6 +49,12 @@ function firstOf({ year, month }) {
44
49
  }
45
50
  const DEFAULT_TRIGGER_FORMAT = { month: "long", year: "numeric" };
46
51
  const MONTH_NAME_FORMAT = { month: "long", year: "numeric" };
52
+ const YEAR_FORMAT = { year: "numeric" };
53
+ const YEAR = /^(\d{4})/;
54
+ function parseYear(key) {
55
+ const m = key ? YEAR.exec(key) : null;
56
+ return m ? Number(m[1]) : null;
57
+ }
47
58
  function useFocusBackOnClose(open, triggerRef) {
48
59
  const wasOpen = useRef(false);
49
60
  useEffect(() => {
@@ -100,6 +111,7 @@ function MonthFieldTrigger({
100
111
  valueId,
101
112
  panelId,
102
113
  padded,
114
+ small,
103
115
  disabled,
104
116
  invalid,
105
117
  className,
@@ -126,6 +138,10 @@ function MonthFieldTrigger({
126
138
  FIELD_TRIGGER,
127
139
  "pe-9",
128
140
  padded && FIELD_FLOATING_PAD,
141
+ // keksdose K13 (live #260): an IconButton's 36px, and only as wide as the
142
+ // month — `py-0` with a fixed height so the box is 36px whatever the type's
143
+ // line height. `pe-9` stays: the calendar glyph still sits at the end.
144
+ small && "h-9 w-auto py-0",
129
145
  disabled && "cursor-not-allowed opacity-50",
130
146
  invalid && FIELD_INVALID,
131
147
  className
@@ -291,50 +307,212 @@ function MonthGrid({
291
307
  }) }, row)) })
292
308
  ] });
293
309
  }
310
+ const YEAR_PAGE = 12;
311
+ function YearGrid({
312
+ value,
313
+ onPick,
314
+ locale,
315
+ minYear,
316
+ maxYear,
317
+ currentYear,
318
+ labels
319
+ }) {
320
+ const clamp = (y) => Math.min(maxYear ?? y, Math.max(minYear ?? y, y));
321
+ const whole = minYear !== null && maxYear !== null && maxYear >= minYear && maxYear - minYear < YEAR_PAGE ? { first: minYear, count: maxYear - minYear + 1 } : null;
322
+ const [active, setActive] = useState(() => clamp(value ?? currentYear));
323
+ const anchor = minYear ?? (maxYear !== null ? maxYear - (YEAR_PAGE - 1) : 0);
324
+ const first = whole ? whole.first : anchor + Math.floor((active - anchor) / YEAR_PAGE) * YEAR_PAGE;
325
+ const count = whole ? whole.count : YEAR_PAGE;
326
+ const years = Array.from({ length: count }, (_, i) => first + i);
327
+ const outOfBounds = (y) => minYear !== null && y < minYear || maxYear !== null && y > maxYear;
328
+ const gridRef = useRef(null);
329
+ const captionId = `${useId()}-years`;
330
+ const yearFormat = useMemo(() => new Intl.DateTimeFormat(locale, YEAR_FORMAT), [locale]);
331
+ const caption = yearFormat.formatRange(new Date(first, 0, 1), new Date(first + count - 1, 0, 1));
332
+ const pendingFocus = useRef(null);
333
+ useEffect(() => {
334
+ const year = pendingFocus.current;
335
+ if (year === null) return;
336
+ pendingFocus.current = null;
337
+ gridRef.current?.querySelector(`[data-year="${year}"]`)?.focus();
338
+ });
339
+ useEffect(() => {
340
+ gridRef.current?.querySelector('[tabindex="0"]')?.focus();
341
+ }, []);
342
+ const moveTo = (next, focus) => {
343
+ const year = clamp(next);
344
+ if (focus) pendingFocus.current = year;
345
+ setActive(year);
346
+ };
347
+ const onCellKeyDown = (e) => {
348
+ const step = horizontalStep(e.key, e.currentTarget);
349
+ const col = (active - first) % COLUMNS;
350
+ let delta;
351
+ switch (step ? "horizontal" : e.key) {
352
+ case "horizontal":
353
+ delta = step;
354
+ break;
355
+ case "ArrowUp":
356
+ delta = -COLUMNS;
357
+ break;
358
+ case "ArrowDown":
359
+ delta = COLUMNS;
360
+ break;
361
+ case "Home":
362
+ delta = -col;
363
+ break;
364
+ case "End":
365
+ delta = Math.min(COLUMNS - 1 - col, first + count - 1 - active);
366
+ break;
367
+ case "PageUp":
368
+ delta = -YEAR_PAGE;
369
+ break;
370
+ case "PageDown":
371
+ delta = YEAR_PAGE;
372
+ break;
373
+ default:
374
+ return;
375
+ }
376
+ e.preventDefault();
377
+ if (delta !== 0) moveTo(active + delta, true);
378
+ };
379
+ const prevBlocked = minYear !== null && first - 1 < minYear;
380
+ const nextBlocked = maxYear !== null && first + count > maxYear;
381
+ const arrow = "flex size-7 items-center justify-center rounded text-[var(--text-muted)] hover:bg-[var(--bg-hover)] disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent";
382
+ return /* @__PURE__ */ jsxs("div", { className: "select-none", children: [
383
+ /* @__PURE__ */ jsxs("div", { className: cn("mb-1 flex items-center", whole ? "h-7 justify-center" : "justify-between"), children: [
384
+ !whole && /* @__PURE__ */ jsx(
385
+ "button",
386
+ {
387
+ type: "button",
388
+ "aria-label": labels.earlierYears,
389
+ disabled: prevBlocked,
390
+ onClick: () => moveTo(active - YEAR_PAGE, false),
391
+ className: arrow,
392
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4 rtl:-scale-x-100" })
393
+ }
394
+ ),
395
+ /* @__PURE__ */ jsx("div", { id: captionId, "aria-live": "polite", className: "text-sm font-semibold tabular-nums text-[var(--text-primary)]", children: caption }),
396
+ !whole && /* @__PURE__ */ jsx(
397
+ "button",
398
+ {
399
+ type: "button",
400
+ "aria-label": labels.laterYears,
401
+ disabled: nextBlocked,
402
+ onClick: () => moveTo(active + YEAR_PAGE, false),
403
+ className: arrow,
404
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4 rtl:-scale-x-100" })
405
+ }
406
+ )
407
+ ] }),
408
+ /* @__PURE__ */ jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": captionId, className: "grid gap-1", children: Array.from({ length: Math.ceil(count / COLUMNS) }, (_, row) => /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-3 gap-1", children: years.slice(row * COLUMNS, row * COLUMNS + COLUMNS).map((year) => {
409
+ const disabled = outOfBounds(year);
410
+ const selected = year === value;
411
+ const current = year === currentYear;
412
+ return /* @__PURE__ */ jsx(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ role: "gridcell",
417
+ "data-year": year,
418
+ tabIndex: year === active ? 0 : -1,
419
+ "aria-selected": selected,
420
+ "aria-disabled": disabled || void 0,
421
+ "aria-current": current ? "date" : void 0,
422
+ onClick: () => {
423
+ if (disabled) return;
424
+ setActive(year);
425
+ onPick(String(year));
426
+ },
427
+ onKeyDown: onCellKeyDown,
428
+ className: cn(
429
+ "rounded px-2 py-1.5 text-sm tabular-nums transition-colors",
430
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset",
431
+ selected && !disabled ? "focus-visible:ring-[var(--brand-contrast)]" : "focus-visible:ring-[var(--brand)]",
432
+ disabled ? "cursor-not-allowed text-[var(--text-placeholder)]" : selected ? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]" : "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
433
+ current && !selected && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]"
434
+ ),
435
+ children: yearFormat.format(new Date(year, 0, 1))
436
+ },
437
+ year
438
+ );
439
+ }) }, row)) })
440
+ ] });
441
+ }
294
442
  function upperFirst(text, locale) {
295
443
  return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
296
444
  }
297
- function MonthPicker({
298
- value,
299
- onChange,
300
- locale: localeProp,
301
- min,
302
- max,
303
- label,
304
- placeholder,
305
- disabled,
306
- invalid,
307
- formatOptions,
308
- currentMonth,
309
- labels: labelsProp,
310
- className,
311
- triggerClassName,
312
- variant = "field",
313
- headingLevel = 2,
314
- showToday = true,
315
- ...rest
316
- }) {
445
+ function MonthPicker(props) {
446
+ const {
447
+ value,
448
+ onChange,
449
+ locale: localeProp,
450
+ min,
451
+ max,
452
+ label,
453
+ placeholder,
454
+ disabled,
455
+ invalid,
456
+ error,
457
+ hint,
458
+ size = "md",
459
+ mode = "month",
460
+ formatOptions,
461
+ currentMonth,
462
+ labels: labelsProp,
463
+ className,
464
+ triggerClassName,
465
+ variant = "field",
466
+ headingLevel = 2,
467
+ showToday = true,
468
+ ...rest
469
+ } = props;
317
470
  const labels = useMonthPickerLabels(labelsProp);
318
471
  const locale = useKitLocale(localeProp);
319
- const selected = parseMonth(value);
472
+ const fromProvider = useKitDateFormatter();
473
+ const years = mode === "year";
474
+ const selected = years ? null : parseMonth(value);
475
+ const selectedYear = years ? parseYear(value) : null;
320
476
  const minKey = boundKey(min);
321
477
  const maxKey = boundKey(max);
478
+ const minYear = parseYear(min);
479
+ const maxYear = parseYear(max);
322
480
  const currentKey = boundKey(currentMonth) ?? monthKey(/* @__PURE__ */ new Date());
323
- const triggerText = selected ? upperFirst(
324
- new Intl.DateTimeFormat(locale, formatOptions ?? DEFAULT_TRIGGER_FORMAT).format(
325
- firstOf(selected)
481
+ const currentYear = parseYear(currentKey);
482
+ const shownKey = years ? selectedYear !== null ? String(selectedYear) : null : selected ? toKey(selected) : null;
483
+ const triggerText = shownKey ? formatOptions === void 0 && fromProvider?.(shownKey, { unit: years ? "year" : "month", source: "monthPicker", locale, weekday: false }) || upperFirst(
484
+ new Intl.DateTimeFormat(locale, formatOptions ?? (years ? YEAR_FORMAT : DEFAULT_TRIGGER_FORMAT)).format(
485
+ years ? new Date(selectedYear, 0, 1) : firstOf(selected)
326
486
  ),
327
487
  locale
328
488
  ) : placeholder ?? "";
329
489
  const id = useId();
330
- const [aria, wrapperRest] = splitTriggerAria(rest);
490
+ const messages = useFieldMessages({ hint, error, describedBy: rest["aria-describedby"] });
491
+ const [ownAria, wrapperRest] = splitTriggerAria(rest);
492
+ const aria = { ...ownAria, "aria-describedby": messages.describedBy };
331
493
  const labelId = `${id}-label`;
332
494
  const valueId = `${id}-value`;
333
495
  const panelId = `${id}-panel`;
334
496
  const named = typeof label === "string";
497
+ const small = size === "sm" && label === void 0;
335
498
  const rootRef = useRef(null);
336
499
  const [dir, setDir] = useState("ltr");
337
- const grid = (close) => /* @__PURE__ */ jsx(
500
+ const panelLabel = years ? labels.yearPanel : labels.panel;
501
+ const grid = (close) => years ? /* @__PURE__ */ jsx(
502
+ YearGrid,
503
+ {
504
+ value: selectedYear,
505
+ locale,
506
+ minYear,
507
+ maxYear,
508
+ currentYear,
509
+ labels,
510
+ onPick: (key) => {
511
+ onChange(key);
512
+ close();
513
+ }
514
+ }
515
+ ) : /* @__PURE__ */ jsx(
338
516
  MonthGrid,
339
517
  {
340
518
  value: selected,
@@ -350,17 +528,33 @@ function MonthPicker({
350
528
  }
351
529
  );
352
530
  if (variant === "stepper") {
353
- const base = selected ?? parseMonth(currentKey);
354
- const prevKey = toKey(shiftMonth(base, -1));
355
- const nextKey = toKey(shiftMonth(base, 1));
356
- const outside = (key) => Boolean(minKey && key < minKey || maxKey && key > maxKey);
531
+ let prevKey;
532
+ let nextKey;
533
+ let nowKey;
534
+ let outside;
535
+ if (years) {
536
+ const base = selectedYear ?? currentYear;
537
+ prevKey = String(base - 1);
538
+ nextKey = String(base + 1);
539
+ nowKey = String(currentYear);
540
+ outside = (key) => {
541
+ const y = Number(key);
542
+ return minYear !== null && y < minYear || maxYear !== null && y > maxYear;
543
+ };
544
+ } else {
545
+ const base = selected ?? parseMonth(currentKey);
546
+ prevKey = toKey(shiftMonth(base, -1));
547
+ nextKey = toKey(shiftMonth(base, 1));
548
+ nowKey = currentKey;
549
+ outside = (key) => Boolean(minKey && key < minKey || maxKey && key > maxKey);
550
+ }
357
551
  const Heading = headingLevel ? `h${headingLevel}` : null;
358
552
  const trigger = /* @__PURE__ */ jsx(
359
553
  Popover,
360
554
  {
361
555
  width: 256,
362
556
  panelId,
363
- labels: { panel: labels.panel },
557
+ labels: { panel: panelLabel },
364
558
  dir,
365
559
  trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
366
560
  MonthHeadingTrigger,
@@ -374,7 +568,7 @@ function MonthPicker({
374
568
  text: triggerText,
375
569
  panelId,
376
570
  disabled,
377
- aria
571
+ aria: ownAria
378
572
  }
379
573
  ),
380
574
  children: grid
@@ -388,17 +582,17 @@ function MonthPicker({
388
582
  {
389
583
  variant: "secondary",
390
584
  size: "sm",
391
- disabled: disabled || outside(currentKey),
585
+ disabled: disabled || outside(nowKey),
392
586
  onClick: () => {
393
- if (currentKey !== value) onChange(currentKey);
587
+ if (nowKey !== shownKey) onChange(nowKey);
394
588
  },
395
- children: labels.today
589
+ children: years ? labels.thisYear : labels.today
396
590
  }
397
591
  ),
398
592
  /* @__PURE__ */ jsx(
399
593
  IconButton,
400
594
  {
401
- "aria-label": labels.previousMonth,
595
+ "aria-label": years ? labels.previousYear : labels.previousMonth,
402
596
  disabled: disabled || outside(prevKey),
403
597
  onClick: () => onChange(prevKey),
404
598
  children: /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" })
@@ -407,7 +601,7 @@ function MonthPicker({
407
601
  /* @__PURE__ */ jsx(
408
602
  IconButton,
409
603
  {
410
- "aria-label": labels.nextMonth,
604
+ "aria-label": years ? labels.nextYear : labels.nextMonth,
411
605
  disabled: disabled || outside(nextKey),
412
606
  onClick: () => onChange(nextKey),
413
607
  children: /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
@@ -416,52 +610,62 @@ function MonthPicker({
416
610
  ] })
417
611
  ] });
418
612
  }
419
- return /* @__PURE__ */ jsxs("div", { ...wrapperRest, ref: rootRef, className: cn("relative", className), children: [
420
- label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
421
- named && /* @__PURE__ */ jsx("span", { id: labelId, "aria-hidden": true, className: "sr-only-fixed", children: label }),
422
- /* @__PURE__ */ jsx(
423
- Popover,
424
- {
425
- width: 256,
426
- panelId,
427
- labels: { panel: labels.panel },
428
- dir,
429
- trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
430
- MonthFieldTrigger,
431
- {
432
- open,
433
- toggle: () => {
434
- setDir(dirOf(rootRef.current));
435
- toggle();
436
- },
437
- triggerRef: ref,
438
- triggerText,
439
- hasValue: selected != null,
440
- labelledBy: [
441
- aria["aria-labelledby"],
442
- named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : void 0,
443
- valueId
444
- ].filter(Boolean).join(" "),
445
- aria,
446
- valueId,
447
- panelId,
448
- padded: label !== void 0,
449
- disabled,
450
- invalid,
451
- className: triggerClassName
452
- }
453
- ),
454
- children: grid
455
- }
456
- ),
457
- /* @__PURE__ */ jsx(
458
- Calendar,
459
- {
460
- "aria-hidden": true,
461
- className: "pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
462
- }
463
- )
464
- ] });
613
+ const field = (
614
+ // `w-fit` for the toolbar size: the calendar glyph is placed against this box, and a
615
+ // full-width box would park it at the far end of the row, away from its trigger.
616
+ /* @__PURE__ */ jsxs("div", { ...wrapperRest, ref: rootRef, className: cn("relative", small && "w-fit max-w-full", className), children: [
617
+ label !== void 0 && /* @__PURE__ */ jsx(FieldLabelLine, { label, hint: messages.labelHint }),
618
+ named && /* @__PURE__ */ jsx("span", { id: labelId, "aria-hidden": true, className: "sr-only-fixed", children: label }),
619
+ /* @__PURE__ */ jsx(
620
+ Popover,
621
+ {
622
+ width: 256,
623
+ panelId,
624
+ labels: { panel: panelLabel },
625
+ dir,
626
+ trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
627
+ MonthFieldTrigger,
628
+ {
629
+ open,
630
+ toggle: () => {
631
+ setDir(dirOf(rootRef.current));
632
+ toggle();
633
+ },
634
+ triggerRef: ref,
635
+ triggerText,
636
+ hasValue: shownKey !== null,
637
+ labelledBy: [
638
+ aria["aria-labelledby"],
639
+ named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : void 0,
640
+ valueId
641
+ ].filter(Boolean).join(" "),
642
+ aria,
643
+ valueId,
644
+ panelId,
645
+ padded: label !== void 0,
646
+ small,
647
+ disabled,
648
+ invalid: invalid || hasContent(error),
649
+ className: triggerClassName
650
+ }
651
+ ),
652
+ children: grid
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx(
656
+ Calendar,
657
+ {
658
+ "aria-hidden": true,
659
+ className: "pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
660
+ }
661
+ )
662
+ ] })
663
+ );
664
+ const endHint = label === void 0 ? messages.labelHint : void 0;
665
+ return /* @__PURE__ */ jsx(FieldBox, { below: messages.below, reserve: "error" in props || "hint" in props, children: endHint === void 0 ? field : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
666
+ /* @__PURE__ */ jsx("div", { className: cn("min-w-0", !small && "flex-1"), children: field }),
667
+ /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: endHint })
668
+ ] }) });
465
669
  }
466
670
  export {
467
671
  DEFAULT_MONTH_PICKER_LABELS,