@eifi1/ui-kit 0.4.0 → 0.5.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 (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +91 -20
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +96 -20
  63. package/dist/components/dropdown.js +77 -33
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +38 -6
  75. package/dist/components/file-dropzone.js +34 -9
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +28 -2
  105. package/dist/components/number-input.js +12 -2
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +32 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +15 -4
  176. package/dist/components/user-avatar.js +5 -4
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +27 -5
  270. package/dist/theme/palette-presets.js +231 -33
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +67 -14
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -0,0 +1,549 @@
1
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
3
+ import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { monthKey, pad } from "../lib/dates";
6
+ import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
7
+ import { Popover } from "./popover";
8
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
9
+
10
+ /**
11
+ * Every string the month picker can speak. The month and year names are NOT here —
12
+ * they come from `Intl` in the `locale` prop, so they are already in the user's
13
+ * language. What is left is the two icon-only year arrows, whose `aria-label` is the
14
+ * only name they have, the panel's name, and a month cell's name.
15
+ */
16
+ export interface MonthPickerLabels {
17
+ previousYear: string;
18
+ nextYear: string;
19
+ /** The popover's accessible name (`role="dialog"`). */
20
+ panel: string;
21
+ /**
22
+ * Accessible name of one month cell, given that month and its year formatted in
23
+ * `locale` ("August 2026"). The cell's visible text is the short month alone, and
24
+ * "Aug" read out on its own does not say which year the grid is on.
25
+ */
26
+ month: (monthYear: string) => string;
27
+ }
28
+
29
+ /** The English starting point every override is merged onto. Exported (unlike the
30
+ * calendar's) because it is asked for by name: a host building its own translation
31
+ * table can spread it rather than re-typing four keys to change one. */
32
+ export const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {
33
+ previousYear: "Previous year",
34
+ nextYear: "Next year",
35
+ panel: "Choose a month",
36
+ month: (monthYear) => monthYear,
37
+ };
38
+
39
+ /** Merge caller overrides onto the English defaults. */
40
+ function useMonthPickerLabels(partial?: Partial<MonthPickerLabels>): MonthPickerLabels {
41
+ // Prop > <UiKitProvider> > English, like every labelled component in the kit.
42
+ return useKitLabels("monthPicker", DEFAULT_MONTH_PICKER_LABELS, partial);
43
+ }
44
+
45
+ /**
46
+ * `onChange` is omitted from the `<div>` attributes: the DOM event of that name carries
47
+ * a `FormEvent`, and this one answers with a month key. Everything else reaches the
48
+ * field's root, so a test id or a `data-tour` anchor can find it.
49
+ */
50
+ export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
51
+ /**
52
+ * The selected month as a `"YYYY-MM"` key, or `""` for none.
53
+ *
54
+ * A string rather than `{ year, month }` or a `Date`, for the reason every date in
55
+ * this kit is one: it is what `monthKey()` in `lib/dates` produces, it sorts
56
+ * chronologically so the bounds are plain `<` / `>`, and it crosses a URL or an API
57
+ * without a serialiser. A `{ year, month }` caller converts with
58
+ * `` `${year}-${pad(month)}` `` (month 1–12) and back with `key.split("-").map(Number)`.
59
+ */
60
+ value: string;
61
+ /** Called with the picked month as `"YYYY-MM"`. */
62
+ onChange: (month: string) => void;
63
+ /** Drives the month and year names (`Intl.DateTimeFormat`). Defaults to the
64
+ * `<UiKitProvider>` locale, then the runtime's. */
65
+ locale?: string;
66
+ /**
67
+ * Inclusive bounds, as `"YYYY-MM"`. A full ISO date (`"YYYY-MM-DD"`) is accepted and
68
+ * read as its month, so a host holding "the first day we have data for" can pass it
69
+ * straight in. Months outside are shown but refuse selection; a year arrow is
70
+ * disabled once the whole year beyond it is out of bounds.
71
+ */
72
+ min?: string;
73
+ max?: string;
74
+ /** Embedded top-boundary label, matching the other field-shaped controls. */
75
+ label?: ReactNode;
76
+ /** Trigger text when `value` is `""`. */
77
+ placeholder?: string;
78
+ disabled?: boolean;
79
+ /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
80
+ invalid?: boolean;
81
+ /** Intl options for the trigger's text (default: long month + numeric year). */
82
+ formatOptions?: Intl.DateTimeFormatOptions;
83
+ /**
84
+ * The month to ring as "now", as `"YYYY-MM"`. Defaults to the local calendar's
85
+ * current month; pass one to pin it (a test, a host with its own notion of today).
86
+ */
87
+ currentMonth?: string;
88
+ /** User-facing strings; see {@link MonthPickerLabels}. */
89
+ labels?: Partial<MonthPickerLabels>;
90
+ /** Extra classes for the field's root. */
91
+ className?: string;
92
+ /** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger
93
+ * sitting between two icon buttons in a toolbar. */
94
+ triggerClassName?: string;
95
+ }
96
+
97
+ // ── Month keys ────────────────────────────────────────────────────────────
98
+
99
+ interface YearMonth {
100
+ year: number;
101
+ /** 1–12, as in the key. */
102
+ month: number;
103
+ }
104
+
105
+ const KEY = /^(\d{4})-(\d{2})/;
106
+
107
+ function parseMonth(key: string | undefined): YearMonth | null {
108
+ const m = key ? KEY.exec(key) : null;
109
+ if (!m) return null;
110
+ const month = Number(m[2]);
111
+ return month >= 1 && month <= 12 ? { year: Number(m[1]), month } : null;
112
+ }
113
+
114
+ function toKey({ year, month }: YearMonth): string {
115
+ return `${year}-${pad(month)}`;
116
+ }
117
+
118
+ /** Move a month key by whole months, carrying into the year. */
119
+ function shiftMonth(ym: YearMonth, delta: number): YearMonth {
120
+ const index = ym.year * 12 + (ym.month - 1) + delta;
121
+ return { year: Math.floor(index / 12), month: (index % 12) + 1 };
122
+ }
123
+
124
+ /** A bound, normalised to its month key — so `"2024-03-15"` compares as `"2024-03"`. */
125
+ function boundKey(bound: string | undefined): string | null {
126
+ const ym = parseMonth(bound);
127
+ return ym ? toKey(ym) : null;
128
+ }
129
+
130
+ /** Local-calendar Date for the 1st of a month, for `Intl` to format. */
131
+ function firstOf({ year, month }: YearMonth): Date {
132
+ return new Date(year, month - 1, 1);
133
+ }
134
+
135
+ const DEFAULT_TRIGGER_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "numeric" };
136
+ const MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "numeric" };
137
+
138
+ // ── Trigger ───────────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * The field-shaped button that opens the grid. The same shape as `DatePicker`'s
142
+ * trigger, for the same reasons written down there: `role="combobox"` so it may carry
143
+ * `aria-expanded` / `aria-invalid`, a name made of the label AND the value, and focus
144
+ * handed back here when the panel closes with focus still inside it.
145
+ */
146
+ function MonthFieldTrigger({
147
+ open,
148
+ toggle,
149
+ triggerRef,
150
+ triggerText,
151
+ hasValue,
152
+ labelledBy,
153
+ valueId,
154
+ panelId,
155
+ padded,
156
+ disabled,
157
+ invalid,
158
+ className,
159
+ }: {
160
+ open: boolean;
161
+ toggle: () => void;
162
+ triggerRef: RefObject<HTMLButtonElement | null>;
163
+ triggerText: string;
164
+ hasValue: boolean;
165
+ labelledBy: string;
166
+ valueId: string;
167
+ panelId: string;
168
+ padded: boolean;
169
+ disabled?: boolean;
170
+ invalid?: boolean;
171
+ className?: string;
172
+ }) {
173
+ const wasOpen = useRef(false);
174
+ useEffect(() => {
175
+ const justClosed = wasOpen.current && !open;
176
+ wasOpen.current = open;
177
+ if (!justClosed) return;
178
+ // Only when focus went down with the panel. An outside click has already put it
179
+ // somewhere real, and dragging it back here would be worse than leaving it.
180
+ const active = document.activeElement;
181
+ if (!active || active === document.body) triggerRef.current?.focus();
182
+ }, [open, triggerRef]);
183
+
184
+ return (
185
+ <button
186
+ ref={triggerRef}
187
+ type="button"
188
+ disabled={disabled}
189
+ onClick={toggle}
190
+ role="combobox"
191
+ aria-haspopup="dialog"
192
+ aria-controls={panelId}
193
+ aria-expanded={open}
194
+ aria-invalid={invalid || undefined}
195
+ aria-labelledby={labelledBy}
196
+ className={cn(
197
+ FIELD_TRIGGER,
198
+ "pe-9",
199
+ padded && FIELD_FLOATING_PAD,
200
+ disabled && "cursor-not-allowed opacity-50",
201
+ invalid && FIELD_INVALID,
202
+ className,
203
+ )}
204
+ >
205
+ {/* The U+00A0 fallback keeps the line box when there is no text — see
206
+ DatePicker. The value arrives capitalised (see `upperFirst`); this span does
207
+ NOT use `capitalize`, which upper-cases every WORD — "Choose A Month",
208
+ "Septiembre De 2026". */}
209
+ <span
210
+ id={valueId}
211
+ className={cn("truncate", !hasValue && "text-[var(--text-placeholder)]")}
212
+ >
213
+ {triggerText || " "}
214
+ </span>
215
+ </button>
216
+ );
217
+ }
218
+
219
+ // ── Grid ──────────────────────────────────────────────────────────────────
220
+
221
+ const COLUMNS = 3;
222
+
223
+ /**
224
+ * The panel: a year caption between ‹ › arrows, over a 4×3 grid of months.
225
+ *
226
+ * Mounted only while the popover is open, so every opening starts from the current
227
+ * value's year — a value moved from outside (a prev/next-month button beside the
228
+ * field) is where the grid lands, with no effect needed to snap it back.
229
+ *
230
+ * A grid in the APG sense, like `MiniCalendar`: one tab stop (the roving tabindex),
231
+ * the arrows walk the months — left/right by one, up/down by a row of three — and
232
+ * walking off either end of the year MOVES the year rather than stopping. PageUp /
233
+ * PageDown step a whole year. Out-of-bounds months are `aria-disabled`, not
234
+ * `disabled`, so the roving tab stop never falls into a hole.
235
+ */
236
+ function MonthGrid({
237
+ value,
238
+ onPick,
239
+ locale,
240
+ minKey,
241
+ maxKey,
242
+ currentKey,
243
+ labels,
244
+ }: {
245
+ value: YearMonth | null;
246
+ onPick: (key: string) => void;
247
+ locale: string | undefined;
248
+ minKey: string | null;
249
+ maxKey: string | null;
250
+ currentKey: string;
251
+ labels: MonthPickerLabels;
252
+ }) {
253
+ const outOfBounds = (key: string) =>
254
+ Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
255
+
256
+ // The month holding the tab order, and with it the year on screen — one piece of
257
+ // state, as in MiniCalendar, so the two cannot disagree. Opens on the value, or on
258
+ // the current month, pulled inside the bounds.
259
+ const [active, setActive] = useState<YearMonth>(() => {
260
+ const start = value ? toKey(value) : currentKey;
261
+ if (minKey && start < minKey) return parseMonth(minKey)!;
262
+ if (maxKey && start > maxKey) return parseMonth(maxKey)!;
263
+ return parseMonth(start)!;
264
+ });
265
+ const year = active.year;
266
+
267
+ const gridRef = useRef<HTMLDivElement>(null);
268
+ const captionId = `${useId()}-year`;
269
+
270
+ // Short names for the cells, long "Month YYYY" for their accessible names.
271
+ const shortNames = useMemo(() => {
272
+ const fmt = new Intl.DateTimeFormat(locale, { month: "short" });
273
+ return Array.from({ length: 12 }, (_, i) => fmt.format(new Date(2000, i, 1)));
274
+ }, [locale]);
275
+ const longName = useMemo(() => new Intl.DateTimeFormat(locale, MONTH_NAME_FORMAT), [locale]);
276
+ const yearCaption = new Intl.DateTimeFormat(locale, { year: "numeric" }).format(
277
+ firstOf({ year, month: 1 }),
278
+ );
279
+
280
+ // A month whose cell must take focus after the next commit. Arrowing across a year
281
+ // boundary re-labels the same twelve buttons, so the one to focus is a DIFFERENT
282
+ // button from the one the key landed on.
283
+ const pendingFocus = useRef<string | null>(null);
284
+ useEffect(() => {
285
+ const key = pendingFocus.current;
286
+ if (!key) return;
287
+ pendingFocus.current = null;
288
+ gridRef.current?.querySelector<HTMLElement>(`[data-month="${key}"]`)?.focus();
289
+ });
290
+
291
+ // Opened because the user asked to pick a month: land them on it. Runs before the
292
+ // popover's focus trap (a child's effect fires first), which then keeps this focus.
293
+ useEffect(() => {
294
+ gridRef.current?.querySelector<HTMLElement>('[tabindex="0"]')?.focus();
295
+ }, []);
296
+
297
+ const moveTo = (next: YearMonth, focus: boolean) => {
298
+ if (focus) pendingFocus.current = toKey(next);
299
+ setActive(next);
300
+ };
301
+
302
+ const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
303
+ // Left and right are VISUAL directions: in a right-to-left page the grid runs the
304
+ // other way, so "right" is the previous month there. Read from the nearest `dir`
305
+ // up the tree, which for this portalled panel is in practice <html>'s.
306
+ const rtl = e.currentTarget.closest("[dir]")?.getAttribute("dir") === "rtl";
307
+ const col = (active.month - 1) % COLUMNS;
308
+ let delta: number;
309
+ switch (e.key) {
310
+ case "ArrowLeft":
311
+ delta = rtl ? 1 : -1;
312
+ break;
313
+ case "ArrowRight":
314
+ delta = rtl ? -1 : 1;
315
+ break;
316
+ case "ArrowUp":
317
+ delta = -COLUMNS;
318
+ break;
319
+ case "ArrowDown":
320
+ delta = COLUMNS;
321
+ break;
322
+ case "Home":
323
+ delta = -col;
324
+ break;
325
+ case "End":
326
+ delta = COLUMNS - 1 - col;
327
+ break;
328
+ case "PageUp":
329
+ delta = -12;
330
+ break;
331
+ case "PageDown":
332
+ delta = 12;
333
+ break;
334
+ default:
335
+ return;
336
+ }
337
+ // Before any no-op check: these keys also scroll the panel, and a month that
338
+ // cannot move still must not move the page.
339
+ e.preventDefault();
340
+ if (delta !== 0) moveTo(shiftMonth(active, delta), true);
341
+ };
342
+
343
+ // An arrow is dead once EVERY month of the year it leads to is out of bounds.
344
+ const prevBlocked = Boolean(minKey && toKey({ year: year - 1, month: 12 }) < minKey);
345
+ const nextBlocked = Boolean(maxKey && toKey({ year: year + 1, month: 1 }) > maxKey);
346
+ const selectedKey = value ? toKey(value) : null;
347
+
348
+ const arrow =
349
+ "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";
350
+
351
+ return (
352
+ <div className="select-none">
353
+ <div className="mb-1 flex items-center justify-between">
354
+ <button
355
+ type="button"
356
+ aria-label={labels.previousYear}
357
+ disabled={prevBlocked}
358
+ // Focus stays on the arrow being pressed — only the grid moves.
359
+ onClick={() => moveTo(shiftMonth(active, -12), false)}
360
+ className={arrow}
361
+ >
362
+ {/* Mirrored in RTL, where "previous" points the other way. */}
363
+ <ChevronLeft className="size-4 rtl:-scale-x-100" />
364
+ </button>
365
+ {/* Names the grid, and `aria-live` so stepping a year says which one you are
366
+ on — the user pressing the arrow is not inside the grid to hear it change. */}
367
+ <div
368
+ id={captionId}
369
+ aria-live="polite"
370
+ className="text-sm font-semibold tabular-nums text-[var(--text-primary)]"
371
+ >
372
+ {yearCaption}
373
+ </div>
374
+ <button
375
+ type="button"
376
+ aria-label={labels.nextYear}
377
+ disabled={nextBlocked}
378
+ onClick={() => moveTo(shiftMonth(active, 12), false)}
379
+ className={arrow}
380
+ >
381
+ <ChevronRight className="size-4 rtl:-scale-x-100" />
382
+ </button>
383
+ </div>
384
+ <div ref={gridRef} role="grid" aria-labelledby={captionId} className="grid gap-1">
385
+ {Array.from({ length: 12 / COLUMNS }, (_, row) => (
386
+ <div key={row} role="row" className="grid grid-cols-3 gap-1">
387
+ {Array.from({ length: COLUMNS }, (_, c) => {
388
+ const ym = { year, month: row * COLUMNS + c + 1 };
389
+ const key = toKey(ym);
390
+ const disabled = outOfBounds(key);
391
+ const selected = key === selectedKey;
392
+ const current = key === currentKey;
393
+ return (
394
+ <button
395
+ key={c}
396
+ type="button"
397
+ role="gridcell"
398
+ data-month={key}
399
+ tabIndex={ym.month === active.month ? 0 : -1}
400
+ aria-label={labels.month(longName.format(firstOf(ym)))}
401
+ aria-selected={selected}
402
+ aria-disabled={disabled || undefined}
403
+ aria-current={current ? "date" : undefined}
404
+ onClick={() => {
405
+ // The guard `disabled` would have been; see the component note.
406
+ if (disabled) return;
407
+ setActive(ym);
408
+ onPick(key);
409
+ }}
410
+ onKeyDown={onCellKeyDown}
411
+ className={cn(
412
+ "rounded px-2 py-1.5 text-sm capitalize transition-colors",
413
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset",
414
+ // A --brand ring on a --brand fill exists only in the DOM.
415
+ selected && !disabled
416
+ ? "focus-visible:ring-[var(--brand-contrast)]"
417
+ : "focus-visible:ring-[var(--brand)]",
418
+ disabled
419
+ ? "cursor-not-allowed text-[var(--text-placeholder)]"
420
+ : selected
421
+ ? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
422
+ : "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
423
+ current && !selected && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
424
+ )}
425
+ >
426
+ {shortNames[ym.month - 1]}
427
+ </button>
428
+ );
429
+ })}
430
+ </div>
431
+ ))}
432
+ </div>
433
+ </div>
434
+ );
435
+ }
436
+
437
+ // ── Field ─────────────────────────────────────────────────────────────────
438
+
439
+ /**
440
+ * Month picker: a field showing the selected month ("August 2026"), opening a 12-month
441
+ * grid with year arrows. For a value that IS a month — a budget period, a calendar
442
+ * view, a statement — where a day picker would ask for a day nobody means.
443
+ *
444
+ * One component for the two app-local pickers it replaces: keksdose's budget-header
445
+ * popover (grid + year steppers + a floor) and kastlan's calendar header (a month list
446
+ * and a year list in two popovers). The grid does both jobs — the year arrows and
447
+ * PageUp/PageDown cover what the year list did, without a fixed ±5-year window.
448
+ */
449
+ /** Upper-case the first letter only, in `locale`. Several locales (fr, es, it, pt, …)
450
+ * write month names in lower case, which at the start of a field reads as a typo —
451
+ * but only the FIRST letter is the field's business: "septiembre de 2026" becomes
452
+ * "Septiembre de 2026", not "Septiembre De 2026". */
453
+ function upperFirst(text: string, locale: string | undefined): string {
454
+ return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
455
+ }
456
+
457
+ export function MonthPicker({
458
+ value,
459
+ onChange,
460
+ locale: localeProp,
461
+ min,
462
+ max,
463
+ label,
464
+ placeholder,
465
+ disabled,
466
+ invalid,
467
+ formatOptions,
468
+ currentMonth,
469
+ labels: labelsProp,
470
+ className,
471
+ triggerClassName,
472
+ ...rest
473
+ }: MonthPickerProps) {
474
+ const labels = useMonthPickerLabels(labelsProp);
475
+ const locale = useKitLocale(localeProp);
476
+ const selected = parseMonth(value);
477
+ const minKey = boundKey(min);
478
+ const maxKey = boundKey(max);
479
+ const currentKey = boundKey(currentMonth) ?? monthKey(new Date());
480
+
481
+ const triggerText = selected
482
+ ? upperFirst(
483
+ new Intl.DateTimeFormat(locale, formatOptions ?? DEFAULT_TRIGGER_FORMAT).format(
484
+ firstOf(selected),
485
+ ),
486
+ locale,
487
+ )
488
+ : (placeholder ?? "");
489
+
490
+ const id = useId();
491
+ const labelId = `${id}-label`;
492
+ const valueId = `${id}-value`;
493
+ const panelId = `${id}-panel`;
494
+ // Only a STRING label can double as a name; a ReactNode may hold its own controls.
495
+ const named = typeof label === "string";
496
+
497
+ return (
498
+ <div {...rest} className={cn("relative", className)}>
499
+ {label !== undefined && <FieldLabel>{label}</FieldLabel>}
500
+ {/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,
501
+ and inside this `relative` root either way. */}
502
+ {named && (
503
+ <span id={labelId} aria-hidden className="sr-only-fixed">
504
+ {label}
505
+ </span>
506
+ )}
507
+ <Popover
508
+ width={256}
509
+ panelId={panelId}
510
+ labels={{ panel: labels.panel }}
511
+ trigger={({ open, toggle, ref }) => (
512
+ <MonthFieldTrigger
513
+ open={open}
514
+ toggle={toggle}
515
+ triggerRef={ref}
516
+ triggerText={triggerText}
517
+ hasValue={selected != null}
518
+ labelledBy={named ? `${labelId} ${valueId}` : valueId}
519
+ valueId={valueId}
520
+ panelId={panelId}
521
+ padded={label !== undefined}
522
+ disabled={disabled}
523
+ invalid={invalid}
524
+ className={triggerClassName}
525
+ />
526
+ )}
527
+ >
528
+ {(close) => (
529
+ <MonthGrid
530
+ value={selected}
531
+ locale={locale}
532
+ minKey={minKey}
533
+ maxKey={maxKey}
534
+ currentKey={currentKey}
535
+ labels={labels}
536
+ onPick={(key) => {
537
+ onChange(key);
538
+ close();
539
+ }}
540
+ />
541
+ )}
542
+ </Popover>
543
+ <Calendar
544
+ aria-hidden
545
+ className="pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
546
+ />
547
+ </div>
548
+ );
549
+ }