@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,656 @@
1
+ import { useEffect, useId, useMemo, useRef } from "react";
2
+ import type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from "react";
3
+ import { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { addDaysIso, parseIsoDate } from "../lib/dates";
6
+ import {
7
+ DEFAULT_DATE_PICKER_LABELS,
8
+ useKitLabels,
9
+ useKitLocale,
10
+ type DatePickerLabels,
11
+ } from "../i18n/kit-labels";
12
+ import { splitTriggerAria } from "./trigger-aria";
13
+ import type { TriggerAria } from "./trigger-aria";
14
+ import { FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
15
+ import { MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
16
+ import { Popover } from "./popover";
17
+ import { Tooltip } from "./tooltip";
18
+
19
+ /**
20
+ * What both pickers share — and, through `ComponentPropsWithoutRef<"div">`, everything a
21
+ * `<div>` takes, so a `data-tour` anchor, a test id or an `aria-describedby` reaches the
22
+ * field's root element instead of being dropped (audit §api-design).
23
+ *
24
+ * `onChange` is omitted from those attributes: the DOM event of that name carries a
25
+ * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own
26
+ * below.
27
+ */
28
+ interface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
29
+ /** BCP 47 tag for the trigger's date and the calendar. Falls back to
30
+ * `<UiKitProvider locale>`, then to the runtime's default. */
31
+ locale?: string;
32
+ /** Embedded top-boundary label, matching the native Input/Select fields. */
33
+ label?: ReactNode;
34
+ placeholder?: string;
35
+ /** Inclusive ISO bounds for selectable days. */
36
+ min?: string;
37
+ max?: string;
38
+ /** Show an X to clear the value (emits `""`). */
39
+ clearable?: boolean;
40
+ /** Accessible name of the clear button. Wins over `datePicker.clear` from
41
+ * `<UiKitProvider labels>`; kept as a prop because it predates the provider. */
42
+ clearLabel?: string;
43
+ disabled?: boolean;
44
+ /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
45
+ invalid?: boolean;
46
+ className?: string;
47
+ /** Intl options for the trigger's formatted date (default: locale short date). */
48
+ formatOptions?: Intl.DateTimeFormatOptions;
49
+ /** Render the trigger's text for an ISO date yourself; wins over `formatOptions`.
50
+ * For a host whose date rendering is more than one `Intl` call can say — Keksdose
51
+ * puts the weekday's name in the UI language beside digits ordered by a separate
52
+ * format preference, two locales in one string. `locale` still drives the calendar. */
53
+ formatValue?: (iso: string) => string;
54
+ /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are
55
+ * icon-only, so their `aria-label` is the only name they have — and the package
56
+ * ships English defaults, which a translating host has to be able to replace.
57
+ * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */
58
+ calendarLabels?: MiniCalendarProps["labels"];
59
+ }
60
+
61
+ /** Format an ISO date for the trigger. `lib/dates`' `formatIsoDate` insists on a
62
+ * locale, and here there may be none — no prop and no provider — which `Intl` reads
63
+ * as "the runtime's default", as every other kit formatter does. */
64
+ function formatDate(iso: string, locale: string | undefined, options?: Intl.DateTimeFormatOptions) {
65
+ return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? "";
66
+ }
67
+
68
+ /**
69
+ * The field-shaped button that opens the calendar.
70
+ *
71
+ * A component of its own rather than JSX inline in `Popover`'s `trigger` render prop,
72
+ * because handing focus back to it is an effect and a render prop is not somewhere a
73
+ * hook can go.
74
+ */
75
+ function DateFieldTrigger({
76
+ open,
77
+ toggle,
78
+ triggerRef,
79
+ triggerText,
80
+ hasValue,
81
+ labelledBy,
82
+ valueId,
83
+ padded,
84
+ disabled,
85
+ invalid,
86
+ panelId,
87
+ aria,
88
+ }: {
89
+ /** The caller's naming/description attributes, routed here from the field. */
90
+ aria: TriggerAria;
91
+ open: boolean;
92
+ toggle: () => void;
93
+ triggerRef: RefObject<HTMLButtonElement | null>;
94
+ /** The panel's id — `role="combobox"` requires `aria-controls`. */
95
+ panelId: string;
96
+ triggerText: string;
97
+ hasValue: boolean;
98
+ /** ids of the hidden label twin + the value span, in the order they are spoken. */
99
+ labelledBy?: string;
100
+ valueId: string;
101
+ padded: boolean;
102
+ disabled?: boolean;
103
+ invalid?: boolean;
104
+ }) {
105
+ const wasOpen = useRef(false);
106
+ useEffect(() => {
107
+ const justClosed = wasOpen.current && !open;
108
+ wasOpen.current = open;
109
+ if (!justClosed) return;
110
+ // Only when the panel still HAD focus. The day the user was standing on has just
111
+ // been unmounted with the panel, which drops focus to <body> and leaves a
112
+ // keyboard user back at the top of the document with no idea where they were.
113
+ // But an outside click closes this panel too, and that click has already given
114
+ // focus to whatever it landed on — dragging it back here would be worse than the
115
+ // bug, so a focus that is somewhere real is left alone.
116
+ const active = document.activeElement;
117
+ if (!active || active === document.body) triggerRef.current?.focus();
118
+ // Here rather than through `useFocusTrap`'s `restoreFocus`, and not only because
119
+ // `Popover` runs no trap to carry it: that option restores to whatever held focus
120
+ // when the panel opened, and on Safari — and on Firefox on macOS — clicking a
121
+ // button does not focus it, so "whatever held focus" is the page. The requirement
122
+ // is this trigger, every time, however the panel was opened.
123
+ }, [open, triggerRef]);
124
+
125
+ return (
126
+ <button
127
+ ref={triggerRef}
128
+ type="button"
129
+ disabled={disabled}
130
+ onClick={toggle}
131
+ // The name has to carry the VALUE. A bare `aria-label={label}` — which is what
132
+ // this was — replaces the button's text outright, so the one field in the form
133
+ // whose entire job is to show a date announced "Due date" and stopped there,
134
+ // and a screen-reader user could not read back what they had picked without
135
+ // opening the calendar and hunting for the selected day.
136
+ //
137
+ // Two ids rather than one composed string (`common.fieldValue`): "label: value"
138
+ // is a sentence, and a name reference is spoken as the two texts in order with
139
+ // no punctuation to translate at all. The hidden twin of the FieldLabel lives
140
+ // in DateField, next to the label it copies.
141
+ id={aria.id}
142
+ aria-labelledby={aria["aria-label"] && !aria["aria-labelledby"] ? undefined : labelledBy}
143
+ aria-label={aria["aria-label"]}
144
+ aria-describedby={aria["aria-describedby"]}
145
+ // `role="combobox"` on a button that opens a calendar is the APG date-picker
146
+ // shape, and it is what makes the next two lines legal: `button` supports
147
+ // neither `aria-expanded` nor `aria-invalid`, so the previous markup set an
148
+ // invalid state that announced nothing (ESLint's `role-supports-aria-props`).
149
+ //
150
+ // `aria-haspopup="dialog"` is now honest — it was not when this comment first
151
+ // said so. `Popover` gained a real `role="dialog"` in the same wave, so the
152
+ // trigger's promise and the panel's role finally agree.
153
+ role="combobox"
154
+ aria-haspopup="dialog"
155
+ aria-controls={panelId}
156
+ aria-expanded={open}
157
+ aria-invalid={invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined}
158
+ className={cn(
159
+ FIELD_TRIGGER,
160
+ "pr-9",
161
+ padded && FIELD_FLOATING_PAD,
162
+ disabled && "cursor-not-allowed opacity-50",
163
+ invalid && FIELD_INVALID,
164
+ )}
165
+ >
166
+ <span id={valueId} className={cn("truncate", !hasValue && "text-[var(--text-placeholder)]")}>
167
+ {/* `|| " "` (a NON-BREAKING space) — triggerText is "" when there is no value and no
168
+ placeholder was passed. An empty span has no line box, so the
169
+ trigger collapses to its padding and sits shorter than every
170
+ other field beside it.
171
+
172
+ It has to be a NON-BREAKING space. This read `|| " "` first, and
173
+ that fix did nothing at all: an ordinary space is collapsible
174
+ white space, and white space at the start and end of a line is
175
+ removed — so the span rendered with height 0 and the trigger
176
+ stayed 22px against the 42px of the fields either side of it
177
+ (Keksdose live #294, measured in the browser: `span.textContent`
178
+ WAS `" "` and `getBoundingClientRect().height` was 0). U+00A0 is
179
+ not collapsible, so it holds the line. */}
180
+ {triggerText || " "}
181
+ </span>
182
+ </button>
183
+ );
184
+ }
185
+
186
+ /** A field-styled trigger that opens a portalled MiniCalendar. Shared by the
187
+ * single- and range-date pickers below. */
188
+ function DateField({
189
+ triggerText,
190
+ hasValue,
191
+ label,
192
+ disabled,
193
+ clearable,
194
+ clearLabel,
195
+ panelLabel,
196
+ className,
197
+ width,
198
+ onClear,
199
+ invalid,
200
+ children,
201
+ ...rest
202
+ }: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
203
+ triggerText: string;
204
+ hasValue: boolean;
205
+ label?: ReactNode;
206
+ disabled?: boolean;
207
+ clearable?: boolean;
208
+ /** Already resolved by the picker (prop > provider > English). */
209
+ clearLabel: string;
210
+ /** The popover panel's accessible name, resolved likewise. */
211
+ panelLabel: string;
212
+ invalid?: boolean;
213
+ className?: string;
214
+ /** Popover panel width; omit for the default (a bare calendar). */
215
+ width?: number;
216
+ onClear: () => void;
217
+ children: (close: () => void) => ReactNode;
218
+ }) {
219
+ const showClear = Boolean(clearable && hasValue && !disabled);
220
+ const [aria, wrapperRest] = splitTriggerAria(rest);
221
+ const id = useId();
222
+ const labelId = `${id}-label`;
223
+ const valueId = `${id}-value`;
224
+ const panelId = `${id}-panel`;
225
+ // Only a STRING label can be reused as a name; a ReactNode may be a whole row with
226
+ // its own interactive "?" hint in it, and naming a button with that reads the hint
227
+ // out as part of the field's name.
228
+ const named = typeof label === "string";
229
+ return (
230
+ // The caller's attributes land here, on the field's own box — the trigger inside is
231
+ // named by `aria-labelledby` and must keep the id pair it is given.
232
+ <div {...wrapperRest} className={cn("relative", className)}>
233
+ {label !== undefined && <FieldLabel>{label}</FieldLabel>}
234
+ {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names
235
+ nothing on its own — this hidden twin is what the trigger is named by.
236
+ `aria-hidden` because a name reference reads a hidden element deliberately
237
+ (accname §4.3.1), and without it the label would be announced twice: once as
238
+ loose text before the button, once inside the button's own name.
239
+ `sr-only-fixed` rather than `sr-only` — see the note on the class in
240
+ tokens.css. */}
241
+ {named && (
242
+ <span id={labelId} aria-hidden className="sr-only-fixed">
243
+ {label}
244
+ </span>
245
+ )}
246
+ <Popover
247
+ width={width}
248
+ panelId={panelId}
249
+ // Named for what it is. Unnamed, it fell back to `popover.panel`, and a date
250
+ // field announced its calendar as "Popover" — in English, in every language.
251
+ labels={{ panel: panelLabel }}
252
+ trigger={({ open, toggle, ref }) => (
253
+ <DateFieldTrigger
254
+ open={open}
255
+ toggle={toggle}
256
+ triggerRef={ref}
257
+ triggerText={triggerText}
258
+ hasValue={hasValue}
259
+ // ALWAYS a name reference, never `undefined`. A `<button>` takes its
260
+ // accessible name from its contents, so an unlabelled trigger used to
261
+ // announce its own date text for free. `role="combobox"` does not —
262
+ // content is excluded from name computation for that role — so leaving
263
+ // this undefined made an unlabelled date field announce nothing at all.
264
+ // Named: "label, value". Unnamed: the value alone, which is what the
265
+ // button was saying before.
266
+ //
267
+ // A caller's own reference comes first. And when the caller gives the field
268
+ // an `id` (so an external `<label htmlFor>` can point at it), the trigger
269
+ // lists ITSELF first: a self-reference in `aria-labelledby` is resolved
270
+ // from the element's native label (accname 2B → 2D), so "Due date" from
271
+ // that <label> is spoken before the value, instead of being overridden.
272
+ labelledBy={[
273
+ aria["aria-labelledby"],
274
+ named ? labelId : !aria["aria-labelledby"] && aria.id ? aria.id : undefined,
275
+ valueId,
276
+ ]
277
+ .filter(Boolean)
278
+ .join(" ")}
279
+ aria={aria}
280
+ panelId={panelId}
281
+ valueId={valueId}
282
+ padded={label !== undefined}
283
+ disabled={disabled}
284
+ invalid={invalid}
285
+ />
286
+ )}
287
+ >
288
+ {(close) => children(close)}
289
+ </Popover>
290
+ {showClear ? (
291
+ <button
292
+ type="button"
293
+ aria-label={clearLabel}
294
+ onClick={onClear}
295
+ className="absolute right-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
296
+ >
297
+ <X className="size-4" />
298
+ </button>
299
+ ) : (
300
+ <Calendar
301
+ aria-hidden
302
+ className="pointer-events-none absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
303
+ />
304
+ )}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ /** One ‹ / › day-step button, sized to sit flush beside the field. */
310
+ function StepButton({
311
+ icon: Icon,
312
+ label,
313
+ disabled,
314
+ onClick,
315
+ className,
316
+ }: {
317
+ icon: ComponentType<{ className?: string }>;
318
+ label: string;
319
+ disabled: boolean;
320
+ onClick: () => void;
321
+ /** The button's corners in the joined group (see DatePicker). */
322
+ className?: string;
323
+ }) {
324
+ return (
325
+ <Tooltip label={label} portal>
326
+ <button
327
+ type="button"
328
+ aria-label={label}
329
+ disabled={disabled}
330
+ onClick={onClick}
331
+ className={cn(
332
+ FIELD_BASE,
333
+ // FIELD_BASE is `block w-full` for text inputs; a step button is neither, and
334
+ // px-3 would make it wider than it needs to be. twMerge lets these win.
335
+ "relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-[var(--text-muted)]",
336
+ "hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
337
+ // Raised while hovered or focused, so the shared border and the focus ring are
338
+ // drawn on top of the neighbour they overlap by a pixel.
339
+ "hover:z-10 focus-visible:z-10",
340
+ // Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border
341
+ // included — faded out of the group and read as a broken, detached control.
342
+ "disabled:cursor-not-allowed disabled:text-[var(--text-placeholder)] disabled:opacity-60 disabled:hover:bg-[var(--bg-surface)]",
343
+ className,
344
+ )}
345
+ >
346
+ <Icon className="size-4" />
347
+ </button>
348
+ </Tooltip>
349
+ );
350
+ }
351
+
352
+ export interface DatePickerProps extends DatePickerBaseProps {
353
+ /** ISO "YYYY-MM-DD", or "" for empty. */
354
+ value: string;
355
+ onChange: (iso: string) => void;
356
+ /**
357
+ * Flank the field with ‹ › buttons that move the value one day. For fields whose
358
+ * typical edit is a day or two — opening a calendar to go "yesterday" is three
359
+ * interactions for what should be one.
360
+ *
361
+ * Off by default: on a filter or a far-away date the buttons are dead weight, and
362
+ * they cost ~5rem of width that a narrow layout may not have.
363
+ */
364
+ step?: boolean;
365
+ /** Accessible names for the step buttons. Optional: they resolve from
366
+ * `datePicker.previousDay` / `nextDay` in `<UiKitProvider labels>`, then from the
367
+ * English defaults. This prop wins over both, for the one field that says it
368
+ * differently. */
369
+ stepLabels?: { prev: string; next: string };
370
+ /**
371
+ * Today's date as ISO "YYYY-MM-DD". Adds a jump-to-today button beside the field,
372
+ * disabled once the value already is that day.
373
+ *
374
+ * Passed in rather than read from a clock here: a component that decides what "today"
375
+ * is cannot be tested at a fixed date, and the consumer already has one definition of
376
+ * it. The button is always RENDERED — going flat instead of disappearing — because a
377
+ * control that comes and goes with the value reflows whatever sits beside it, which
378
+ * is the report this exists to answer.
379
+ */
380
+ today?: string;
381
+ /** Accessible name for the today button; wins over `datePicker.today`. */
382
+ todayLabel?: string;
383
+ }
384
+
385
+ /**
386
+ * The picker's own label props, folded into one `datePicker` override so they sit at
387
+ * the top of the same precedence chain as everything else: prop > provider > English.
388
+ * They predate the provider and stay public API, which is why they are not simply
389
+ * replaced by a `labels` prop.
390
+ */
391
+ function useDatePickerLabels(
392
+ clearLabel: string | undefined,
393
+ stepLabels: { prev: string; next: string } | undefined,
394
+ todayLabel: string | undefined,
395
+ ): DatePickerLabels {
396
+ const prev = stepLabels?.prev;
397
+ const next = stepLabels?.next;
398
+ const fromProps = useMemo(() => {
399
+ const out: Partial<DatePickerLabels> = {};
400
+ if (clearLabel !== undefined) out.clear = clearLabel;
401
+ if (prev !== undefined) out.previousDay = prev;
402
+ if (next !== undefined) out.nextDay = next;
403
+ if (todayLabel !== undefined) out.today = todayLabel;
404
+ return out;
405
+ }, [clearLabel, prev, next, todayLabel]);
406
+ return useKitLabels("datePicker", DEFAULT_DATE_PICKER_LABELS, fromProps);
407
+ }
408
+
409
+ /** Single-date picker: a field showing the formatted date, opening a calendar. */
410
+ export function DatePicker({
411
+ value,
412
+ onChange,
413
+ locale: localeProp,
414
+ placeholder,
415
+ min,
416
+ max,
417
+ formatOptions,
418
+ formatValue,
419
+ step,
420
+ stepLabels,
421
+ today,
422
+ todayLabel,
423
+ className,
424
+ calendarLabels,
425
+ label,
426
+ clearable,
427
+ clearLabel,
428
+ disabled,
429
+ invalid,
430
+ ...rest
431
+ }: DatePickerProps) {
432
+ const locale = useKitLocale(localeProp);
433
+ const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);
434
+ const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));
435
+ // With a step or a today button beside it the field is no longer the outermost
436
+ // element — the flex row at the bottom is, and a caller's attributes belong on
437
+ // whichever of the two is actually the root. Naming the shared props rather than
438
+ // letting `...rest` carry them is what makes that choice possible.
439
+ const wrapped = Boolean(step || today);
440
+ const [triggerAria, wrapperRest] = splitTriggerAria(rest);
441
+ const field = (
442
+ <DateField
443
+ // With step/today buttons the flex row is the root and takes the caller's props —
444
+ // except the ones that name the field, which still belong on its trigger.
445
+ {...(wrapped ? triggerAria : rest)}
446
+ label={label}
447
+ clearable={clearable}
448
+ clearLabel={text.clear}
449
+ panelLabel={text.panel}
450
+ disabled={disabled}
451
+ invalid={invalid}
452
+ className={step ? "min-w-0 flex-1" : className}
453
+ hasValue={Boolean(value)}
454
+ triggerText={value ? render(value) : (placeholder ?? "")}
455
+ onClear={() => onChange("")}
456
+ >
457
+ {(close) => (
458
+ <MiniCalendar
459
+ mode="single"
460
+ focusOnOpen
461
+ from={value}
462
+ to={value}
463
+ locale={locale}
464
+ min={min}
465
+ max={max}
466
+ labels={calendarLabels}
467
+ onSelect={(iso) => {
468
+ onChange(iso);
469
+ close();
470
+ }}
471
+ />
472
+ )}
473
+ </DateField>
474
+ );
475
+ if (!wrapped) return field;
476
+
477
+ // An empty field has nothing to step from, so both buttons are dead until a date
478
+ // is picked. Bounds are compared as strings: "YYYY-MM-DD" sorts chronologically.
479
+ const target = (days: number) => (value ? addDaysIso(value, days) : "");
480
+ const outOfBounds = (iso: string) => Boolean((min && iso < min) || (max && iso > max));
481
+ const blocked = (days: number) => {
482
+ const next = target(days);
483
+ if (!next || disabled) return true;
484
+ return outOfBounds(next);
485
+ };
486
+ return (
487
+ // ONE joined control, not three boxes in a row: the buttons and the field share
488
+ // their borders (each piece overlaps the previous by a pixel) and only the outer
489
+ // ends are rounded — the button-group shape, so ‹ date › reads as one field with
490
+ // controls rather than a field with loose buttons parked beside it, which is what
491
+ // the `gap-1` version looked like.
492
+ //
493
+ // items-stretch, not items-center: the buttons match the field's height, which
494
+ // varies with whether it carries a floating label. The field's trigger is nested
495
+ // inside DateField, so its inner corners are squared from here.
496
+ <div
497
+ {...wrapperRest}
498
+ className={cn(
499
+ "flex items-stretch [&>*:not(:first-child)]:-ms-px",
500
+ step && "[&_[role=combobox]]:rounded-s-none",
501
+ (step || today) && "[&_[role=combobox]]:rounded-e-none",
502
+ "[&_[role=combobox]:focus-visible]:relative [&_[role=combobox]:focus-visible]:z-10",
503
+ className,
504
+ )}
505
+ >
506
+ {step && (
507
+ <StepButton
508
+ icon={ChevronLeft}
509
+ label={text.previousDay}
510
+ disabled={blocked(-1)}
511
+ onClick={() => onChange(target(-1))}
512
+ className="rounded-e-none"
513
+ />
514
+ )}
515
+ {field}
516
+ {step && (
517
+ <StepButton
518
+ icon={ChevronRight}
519
+ label={text.nextDay}
520
+ disabled={blocked(1)}
521
+ onClick={() => onChange(target(1))}
522
+ className={cn("rounded-s-none", today && "rounded-e-none")}
523
+ />
524
+ )}
525
+ {today && (
526
+ <StepButton
527
+ icon={CalendarClock}
528
+ label={text.today}
529
+ disabled={Boolean(disabled) || value === today || outOfBounds(today)}
530
+ onClick={() => onChange(today)}
531
+ className="rounded-s-none"
532
+ />
533
+ )}
534
+ </div>
535
+ );
536
+ }
537
+
538
+ export interface DateRangePickerPreset {
539
+ label: ReactNode;
540
+ from: string;
541
+ to: string;
542
+ }
543
+
544
+ export interface DateRangePickerProps extends DatePickerBaseProps {
545
+ from: string;
546
+ to: string;
547
+ onChange: (from: string, to: string) => void;
548
+ /** Separator between the two formatted dates in the trigger (default " – "). */
549
+ separator?: string;
550
+ /** Optional named ranges rendered as a left column (last month / YTD / …). */
551
+ presets?: DateRangePickerPreset[];
552
+ }
553
+
554
+ /** Two-date range picker: click a start then an end; closes once both are set.
555
+ * With `presets`, a left column of named ranges is shown beside the calendar. */
556
+ export function DateRangePicker({
557
+ from,
558
+ to,
559
+ onChange,
560
+ locale: localeProp,
561
+ placeholder,
562
+ min,
563
+ max,
564
+ formatOptions,
565
+ formatValue,
566
+ separator = " – ",
567
+ presets,
568
+ calendarLabels,
569
+ label,
570
+ clearable,
571
+ clearLabel,
572
+ disabled,
573
+ invalid,
574
+ ...rest
575
+ }: DateRangePickerProps) {
576
+ const locale = useKitLocale(localeProp);
577
+ const text = useDatePickerLabels(clearLabel, undefined, undefined);
578
+ const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));
579
+ const a = from ? render(from) : "";
580
+ const b = to ? render(to) : "";
581
+ const triggerText = from
582
+ ? to
583
+ ? `${a}${separator}${b}`
584
+ : `${a}${separator}…`
585
+ : (placeholder ?? "");
586
+ const hasPresets = Boolean(presets && presets.length > 0);
587
+ const calendar = (close: () => void) => (
588
+ <MiniCalendar
589
+ focusOnOpen
590
+ from={from}
591
+ to={to}
592
+ locale={locale}
593
+ min={min}
594
+ max={max}
595
+ labels={calendarLabels}
596
+ onSelect={(f, t) => {
597
+ onChange(f, t);
598
+ // Two-click range: only dismiss once both ends are chosen.
599
+ if (f && t) close();
600
+ }}
601
+ />
602
+ );
603
+ return (
604
+ <DateField
605
+ {...rest}
606
+ label={label}
607
+ clearable={clearable}
608
+ clearLabel={text.clear}
609
+ panelLabel={text.rangePanel}
610
+ disabled={disabled}
611
+ invalid={invalid}
612
+ // Widen so the preset column sits beside the calendar (default otherwise).
613
+ width={hasPresets ? 440 : undefined}
614
+ hasValue={Boolean(from || to)}
615
+ triggerText={triggerText}
616
+ onClear={() => onChange("", "")}
617
+ >
618
+ {(close) =>
619
+ hasPresets ? (
620
+ <div className="flex gap-3">
621
+ {/* `border-e`/`pe`, not `-r`: the preset column is on the START side, and the
622
+ rule between it and the calendar has to follow it in a right-to-left UI. */}
623
+ <div className="flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2">
624
+ {presets!.map((p, i) => {
625
+ const selected = p.from === from && p.to === to;
626
+ return (
627
+ <button
628
+ key={i}
629
+ type="button"
630
+ onClick={() => {
631
+ onChange(p.from, p.to);
632
+ close();
633
+ }}
634
+ className={cn(
635
+ "rounded px-2 py-1.5 text-start text-xs",
636
+ selected
637
+ ? "bg-[var(--bg-active)] font-medium text-[var(--text-primary)]"
638
+ : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
639
+ )}
640
+ >
641
+ {p.label}
642
+ </button>
643
+ );
644
+ })}
645
+ </div>
646
+ {/* `flex-1`: the calendar takes the rest of the panel. Content-sized it was
647
+ seven tiny cells beside a wide preset column. */}
648
+ <div className="min-w-0 flex-1">{calendar(close)}</div>
649
+ </div>
650
+ ) : (
651
+ calendar(close)
652
+ )
653
+ }
654
+ </DateField>
655
+ );
656
+ }