@eifi1/ui-kit 0.4.1 → 0.5.1

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