@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,179 @@
1
+ import { useRef } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
4
+ import { X } from "lucide-react";
5
+
6
+ import { cn } from "../lib/cn";
7
+ import { useBackdropClose } from "./modal";
8
+ import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
9
+ import { useOverlayHistory } from "../hooks/use-overlay-history";
10
+ import { useCloseTransition } from "../hooks/use-close-transition";
11
+ import { useFocusTrap } from "../hooks/use-focus-trap";
12
+
13
+ /**
14
+ * Named and exported, because `ComponentProps<typeof FullBleedDialog>` was the only way
15
+ * to say "the props of this" and five call sites across keksdose and kastlan had already
16
+ * written one (audit §api-design).
17
+ *
18
+ * It extends the `<div>` attributes so a caller can reach the OUTER element — the one
19
+ * carrying `role="dialog"`, and therefore the one an `aria-label`, an
20
+ * `aria-describedby` or a `data-tour` anchor belongs on. {@link className} keeps going
21
+ * to the panel inside it, which is what it has always meant here.
22
+ */
23
+ export interface FullBleedDialogProps extends ComponentPropsWithoutRef<"div"> {
24
+ open: boolean;
25
+ /** The X, the backdrop (where one shows) and — unless {@link backCloses} is off —
26
+ * the platform Back gesture all call this. */
27
+ onClose: () => void;
28
+ /** What the header strip shows beside the close button. */
29
+ header?: ReactNode;
30
+ closeLabel: string;
31
+ children: ReactNode;
32
+ /**
33
+ * Push a history entry so Back dismisses this dialog instead of navigating
34
+ * (Keksdose feedback #172). On by default, because a full-screen form that ignores
35
+ * the phone's universal "go back" gesture does not merely fail to close — the
36
+ * navigation lands somewhere else with the form still notionally open.
37
+ *
38
+ * **Off for a caller whose open state is already in the URL.** The transactions
39
+ * create card is `?action=new`, so Back already pops it through the router; a
40
+ * sentinel on top of that would cost two presses to close one dialog. One owner of
41
+ * the Back press, always.
42
+ */
43
+ backCloses?: boolean;
44
+ /** Extra classes for the PANEL (not the backdrop). */
45
+ className?: string;
46
+ }
47
+
48
+ /**
49
+ * The phone's full-screen dialog: a panel that covers the viewport edge to edge.
50
+ *
51
+ * Not {@link Modal}, and that is a decision rather than an omission. Keksdose feedback
52
+ * #32: the inset panel #204 shipped left a strip of page showing beside it on a phone,
53
+ * and tapping that strip dismissed a form mid-edit. With no backdrop exposed there is
54
+ * nothing to mis-tap, so the X is the way out — `backdropClose` stays wired for any
55
+ * layout that does leave a backdrop visible, and costs nothing where none is.
56
+ *
57
+ * ## Why it is a component
58
+ *
59
+ * It was markup inside `data-table.tsx`, where it is the phone's row EDITOR. Keksdose
60
+ * live #307's follow-up needed the same shell for the transactions CREATE card —
61
+ * *"Is the creation window of the tx still different than the edit window? Since the tx
62
+ * list still appears below when scrolling down?"* — and the two answers to that are
63
+ * either one component or two copies that drift. Every previous convergence in that app
64
+ * drifted the moment a second surface was written beside the first, which is what
65
+ * `field-layer-parity` exists to catch one layer down.
66
+ *
67
+ * So the panel, the header strip, the scrolling body, the body-scroll lock and the Back
68
+ * handling are all here, once.
69
+ *
70
+ * ## What the caller still owns
71
+ *
72
+ * The HEADER's content, because the two callers name themselves differently: the row
73
+ * editor shows the row's own first column, the create card a plain title. And `open`,
74
+ * because who decides is the caller's business — see {@link backCloses}.
75
+ */
76
+ export function FullBleedDialog({
77
+ open,
78
+ onClose,
79
+ header,
80
+ closeLabel,
81
+ children,
82
+ backCloses = true,
83
+ className,
84
+ ...rest
85
+ }: FullBleedDialogProps) {
86
+ // Every way OUT goes through `requestClose`, so the panel lowers itself before the
87
+ // caller unmounts it (live #320 rework). `onClose` is still what finally runs — this
88
+ // only delays it by the length of the animation.
89
+ const { closing, requestClose } = useCloseTransition(onClose);
90
+ const backdropClose = useBackdropClose(requestClose);
91
+ // The page behind must not scroll or jump under the overlay (feedback #204).
92
+ useBodyScrollLock(open);
93
+ useOverlayHistory(open && backCloses, requestClose);
94
+
95
+ const dialogRef = useRef<HTMLDivElement>(null);
96
+ // The half of "modal" this dialog was only claiming. It has said
97
+ // `aria-modal="true"` since it was extracted from `data-table.tsx`, which tells
98
+ // assistive technology to hide everything outside it — while the user's focus stayed
99
+ // on the row they tapped, in the part that is now hidden. Tab then walked them
100
+ // through a table the screen reader would not describe, and on a phone this dialog
101
+ // covers the whole screen, so there was nothing on screen to say where they were.
102
+ //
103
+ // `initialFocus` is left at the default — the dialog itself, not the first field.
104
+ // This is the phone's row EDITOR and its first child is usually an input; focusing it
105
+ // would pull the software keyboard up over half the form before the user has said
106
+ // which field they came for. `Modal` made the same call for the same reason.
107
+ useFocusTrap(dialogRef, { active: open });
108
+
109
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
110
+ if (e.key !== "Escape") return;
111
+ // On the panel, the way `Modal` does it, and NOT through `useEscapeKey`: this
112
+ // dialog is the thing a `PickerSheet` opens on top of, and a document-level
113
+ // listener cannot tell which of the two the user meant. One press would dismiss
114
+ // the sheet AND throw away the edit behind it — Keksdose live #309 (*"Mouse Back
115
+ // does not only close the select but also the whole edit or create dialog"*) on a
116
+ // different key. The sheet's own handler stops the event here, so the innermost
117
+ // overlay is the one that closes, which is only true because focus is now in it.
118
+ e.stopPropagation();
119
+ requestClose();
120
+ };
121
+
122
+ if (!open || typeof document === "undefined") return null;
123
+
124
+ return createPortal(
125
+ <div
126
+ // `animate-overlay` on the backdrop and `animate-sheet` on the panel (live #320):
127
+ // the backdrop fades, the panel rises from the bottom edge it is anchored to.
128
+ // Both are no-ops under prefers-reduced-motion — see tokens.css.
129
+ //
130
+ // …and the same two backwards while `closing`, which is the #320 rework. The
131
+ // backdrop keeps taking pointer events on the way out: a tap during those 220ms
132
+ // is a tap on a dialog that is leaving, and letting it through to the row
133
+ // underneath would open a second one.
134
+ //
135
+ // `...rest` first, the dialog's own contract after it. A caller adding a
136
+ // `data-tour` anchor must not be able to take away the role, the modality or the
137
+ // `tabIndex` below by passing one of them too — this is the element the focus
138
+ // trap holds, and each of those failures is invisible on screen.
139
+ {...rest}
140
+ className={cn(
141
+ "fixed inset-0 z-50 flex items-stretch justify-center bg-black/40 outline-none",
142
+ closing ? "animate-overlay-out" : "animate-overlay",
143
+ )}
144
+ role="dialog"
145
+ aria-modal="true"
146
+ // `tabIndex={-1}` is what makes the line above more than a claim: without it the
147
+ // container cannot take focus and `useFocusTrap` silently does nothing.
148
+ tabIndex={-1}
149
+ ref={dialogRef}
150
+ onKeyDown={handleKeyDown}
151
+ {...backdropClose}
152
+ >
153
+ <div
154
+ className={cn(
155
+ "flex h-full w-full flex-col overflow-hidden bg-[var(--bg-surface)] shadow-xl",
156
+ closing ? "animate-sheet-out" : "animate-sheet",
157
+ className,
158
+ )}
159
+ >
160
+ <div className="flex items-center justify-between gap-2 border-b border-[var(--border)] px-3 py-3">
161
+ <div className="min-w-0 font-medium">{header}</div>
162
+ <button
163
+ type="button"
164
+ onClick={requestClose}
165
+ aria-label={closeLabel}
166
+ className="-mr-1 shrink-0 rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
167
+ >
168
+ <X className="size-5" />
169
+ </button>
170
+ </div>
171
+ {/* px-3, not px-4: every pixel of chrome here is width the form fields lose
172
+ on a phone (feedback #32). The body is the only thing that scrolls, so the
173
+ header stays put and the page underneath cannot move at all. */}
174
+ <div className="flex-1 overflow-y-auto overscroll-contain px-3 py-3">{children}</div>
175
+ </div>
176
+ </div>,
177
+ document.body,
178
+ );
179
+ }
@@ -0,0 +1,157 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
+ import { ChevronDown } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { DropdownSearchHeader, useDropdownSearch } from "./dropdown";
6
+ import { useKitLocale } from "../i18n/kit-labels";
7
+
8
+ export interface PickerGroup {
9
+ key: string;
10
+ label: string;
11
+ /** `icon` is an optional leading glyph, drawn before the label — the same slot
12
+ * `ComboOption` has always had, so the two pickers can present one option set
13
+ * the same way. Omitted, a row is exactly what it was: label only, with no
14
+ * reserved space, so a list where nothing has an icon does not indent. */
15
+ items: { key: string; label: string; icon?: ReactNode }[];
16
+ }
17
+
18
+ /**
19
+ * `onSelect` is omitted from the div's own attributes because this component's
20
+ * means "an item was chosen", not the DOM's "text was selected inside me" — two
21
+ * different signatures under one name, and the kit's meaning is the one three apps
22
+ * are written against.
23
+ */
24
+ export interface GroupedPickerProps extends Omit<ComponentPropsWithoutRef<"div">, "onSelect"> {
25
+ groups: PickerGroup[];
26
+ /** Face of the closed picker button. */
27
+ buttonLabel: ReactNode;
28
+ /** Keys of the current selection, if any. */
29
+ selected: { group: string; item: string } | null;
30
+ onSelect: (groupKey: string, itemKey: string) => void;
31
+ /**
32
+ * @deprecated Use `aria-label`, the DOM spelling every control in the kit now
33
+ * takes. Kept working — three apps pass it — and it applies only when
34
+ * `aria-label` is absent; it goes in a later minor.
35
+ */
36
+ ariaLabel?: string;
37
+ filterPlaceholder?: string;
38
+ }
39
+
40
+ /** Panel picker for large grouped option sets: instead of one long flat
41
+ * dropdown, the groups sit side by side as columns with their items listed
42
+ * beneath (feedback #267/#276), narrowed by a filter box at the top (feedback
43
+ * #267 rework). A query that matches a group's name keeps the whole group;
44
+ * otherwise groups are reduced to their matching items. */
45
+ export function GroupedPicker({
46
+ groups,
47
+ buttonLabel,
48
+ selected,
49
+ onSelect,
50
+ ariaLabel,
51
+ "aria-label": ariaLabelAttr,
52
+ filterPlaceholder,
53
+ className,
54
+ ...rest
55
+ }: GroupedPickerProps) {
56
+ const { open, setOpen, wrapperRef, query, setQuery, inputRef } = useDropdownSearch();
57
+
58
+ useEffect(() => {
59
+ if (!open) return;
60
+ const onKey = (e: KeyboardEvent) => {
61
+ if (e.key === "Escape") setOpen(false);
62
+ };
63
+ document.addEventListener("keydown", onKey);
64
+ return () => document.removeEventListener("keydown", onKey);
65
+ }, [open, setOpen]);
66
+
67
+ // Case folding in the provider's locale — see `rowMatches` for the Turkish "İ"
68
+ // that plain `toLowerCase` gets wrong.
69
+ const locale = useKitLocale();
70
+ const filtered = useMemo(() => {
71
+ const fold = (s: string) => s.toLocaleLowerCase(locale);
72
+ const q = fold(query.trim());
73
+ if (!q) return groups;
74
+ return groups
75
+ .map((g) =>
76
+ fold(g.label).includes(q)
77
+ ? g
78
+ : { ...g, items: g.items.filter((i) => fold(i.label).includes(q)) },
79
+ )
80
+ .filter((g) => g.items.length > 0);
81
+ }, [groups, query, locale]);
82
+
83
+ return (
84
+ // `rest` goes on the wrapper, but the accessible NAME does not: this div is a
85
+ // positioning box with no role, so a label parked on it would be announced by
86
+ // nothing. It belongs to the control — the same split every picker in this wave
87
+ // makes, and the reason `aria-label` is destructured out rather than left in
88
+ // `rest`. Spread FIRST so the attributes below, each of them a fix with an
89
+ // incident behind it, cannot be clobbered from outside.
90
+ <div {...rest} ref={wrapperRef} className={cn("relative", className)}>
91
+ <button
92
+ type="button"
93
+ aria-haspopup="dialog"
94
+ aria-expanded={open}
95
+ aria-label={ariaLabelAttr ?? ariaLabel}
96
+ onClick={() => setOpen((o) => !o)}
97
+ className="flex w-full items-center justify-between gap-2 rounded-md border border-[var(--border-strong)] bg-[var(--bg-surface)] px-2.5 py-1.5 text-sm text-[var(--text-primary)] hover:bg-[var(--bg-hover)]"
98
+ >
99
+ <span className="truncate">{buttonLabel}</span>
100
+ <ChevronDown className="size-4 shrink-0 text-[var(--text-placeholder)]" />
101
+ </button>
102
+ {open && (
103
+ <div className="absolute left-0 z-30 mt-1 flex max-h-96 w-[min(56rem,85vw)] flex-col overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg">
104
+ <DropdownSearchHeader
105
+ query={query}
106
+ onQueryChange={setQuery}
107
+ inputRef={inputRef}
108
+ placeholder={filterPlaceholder}
109
+ />
110
+ <div className="overflow-y-auto p-3">
111
+ {filtered.length === 0 ? (
112
+ <p className="text-sm text-[var(--text-muted)]">—</p>
113
+ ) : (
114
+ <div className="columns-2 gap-4 md:columns-3 xl:columns-4">
115
+ {filtered.map((g) => (
116
+ <div key={g.key} className="mb-3 break-inside-avoid">
117
+ <div className="mb-1 text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]">
118
+ {g.label}
119
+ </div>
120
+ <ul className="space-y-0.5">
121
+ {g.items.map((item) => {
122
+ const isSelected =
123
+ selected !== null &&
124
+ g.key === selected.group &&
125
+ item.key === selected.item;
126
+ return (
127
+ <li key={item.key}>
128
+ <button
129
+ type="button"
130
+ onClick={() => {
131
+ onSelect(g.key, item.key);
132
+ setOpen(false);
133
+ }}
134
+ className={cn(
135
+ "flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-sm",
136
+ isSelected
137
+ ? "bg-[var(--brand-bg)] font-medium text-[var(--brand-muted)]"
138
+ : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
139
+ )}
140
+ >
141
+ {item.icon && <span className="shrink-0">{item.icon}</span>}
142
+ <span className="min-w-0 truncate">{item.label}</span>
143
+ </button>
144
+ </li>
145
+ );
146
+ })}
147
+ </ul>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ )}
152
+ </div>
153
+ </div>
154
+ )}
155
+ </div>
156
+ );
157
+ }
@@ -0,0 +1,183 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
+ import { cn } from "../lib/cn";
4
+ import { useOutsideClick } from "../hooks/use-dismiss";
5
+
6
+ /**
7
+ * Exported and `<div>`-shaped: the top bar composes several of these side by side, and
8
+ * telling them apart — in a test, in a guided tour, in an accessibility tree — needs an
9
+ * attribute the kit does not know about. A closed prop list dropped every one of them.
10
+ *
11
+ * `children` is omitted from the div attributes because here it is a render prop taking
12
+ * the panel's `close`, not a `ReactNode`.
13
+ */
14
+ export interface HoverMenuProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
15
+ trigger: (state: { open: boolean; toggle: () => void }) => ReactNode;
16
+ children: (close: () => void) => ReactNode;
17
+ align?: "left" | "right";
18
+ panelClassName?: string;
19
+ /** Extra classes for the WRAPPER (the panel has {@link panelClassName}). */
20
+ className?: string;
21
+ /**
22
+ * @deprecated Use the DOM spelling `aria-label`, which now reaches the wrapper like
23
+ * any other attribute. Kept working because keksdose, kastlan and lenkbank all pass
24
+ * this today and a rename to save one line of resolution is not a patch release; it
25
+ * loses to `aria-label` wherever both are given. Removal is a later minor.
26
+ */
27
+ ariaLabel?: string;
28
+ }
29
+
30
+ // Hovering waits briefly before opening so sweeping the cursor across a row of
31
+ // triggers doesn't flash menus; closing waits so the cursor can travel from
32
+ // trigger to panel. Click/keyboard toggling is instant (no "opening" phase).
33
+ const OPEN_DELAY_MS = 120;
34
+ const CLOSE_DELAY_MS = 120;
35
+
36
+ type Phase = "closed" | "opening" | "open" | "closing";
37
+
38
+ // Only one hover menu may show its panel at a time across the app — opening
39
+ // one closes whichever was open, so adjacent top-bar menus can't overlap
40
+ // (feedback #251). Module-level is fine: there is one cursor per document.
41
+ let activeClose: (() => void) | null = null;
42
+
43
+ export function HoverMenu({
44
+ trigger,
45
+ children,
46
+ align = "right",
47
+ panelClassName,
48
+ className,
49
+ ariaLabel,
50
+ "aria-label": ariaLabelAttr,
51
+ ...rest
52
+ }: HoverMenuProps) {
53
+ const wrapperRef = useRef<HTMLDivElement>(null);
54
+ const [phase, setPhase] = useState<Phase>("closed");
55
+ const open = phase === "open" || phase === "closing";
56
+
57
+ // Whether the panel is open *because hover opened it*, vs. an explicit
58
+ // click/keyboard toggle. A real mouse click on the trigger is always
59
+ // preceded by a real mouseenter — the cursor has to land on the button
60
+ // before it can be clicked — so once hover has opened the panel, the click
61
+ // that inevitably follows is the same physical gesture, not a request to
62
+ // close what was just opened (feedback #19). Toggling closed on click stays
63
+ // for keyboard/touch activation, which reaches "open" without going through
64
+ // the hover path.
65
+ const openedByHoverRef = useRef(false);
66
+
67
+ const close = useCallback(() => {
68
+ openedByHoverRef.current = false;
69
+ setPhase("closed");
70
+ }, []);
71
+
72
+ const toggle = useCallback(
73
+ () =>
74
+ setPhase((p) => {
75
+ if (p === "open" || p === "closing") return openedByHoverRef.current ? p : "closed";
76
+ openedByHoverRef.current = false;
77
+ return "open";
78
+ }),
79
+ [],
80
+ );
81
+
82
+ const handleMouseEnter = useCallback(
83
+ () => setPhase((p) => (p === "closing" ? "open" : p === "closed" ? "opening" : p)),
84
+ [],
85
+ );
86
+
87
+ const handleMouseLeave = useCallback(
88
+ () => setPhase((p) => (p === "opening" ? "closed" : p === "open" ? "closing" : p)),
89
+ [],
90
+ );
91
+
92
+ // Drive the delayed transitions; unmount/phase changes cancel the timer.
93
+ useEffect(() => {
94
+ if (phase === "opening") {
95
+ const id = setTimeout(() => {
96
+ openedByHoverRef.current = true;
97
+ setPhase("open");
98
+ }, OPEN_DELAY_MS);
99
+ return () => clearTimeout(id);
100
+ }
101
+ if (phase === "closing") {
102
+ const id = setTimeout(() => setPhase("closed"), CLOSE_DELAY_MS);
103
+ return () => clearTimeout(id);
104
+ }
105
+ }, [phase]);
106
+
107
+ // Claim the "active menu" slot while our panel is visible, closing the
108
+ // previous holder. Cleanup releases the slot only if we still hold it
109
+ // (a successor that displaced us has already overwritten it).
110
+ useEffect(() => {
111
+ if (!open) return;
112
+ if (activeClose !== close) activeClose?.();
113
+ activeClose = close;
114
+ return () => {
115
+ if (activeClose === close) activeClose = null;
116
+ };
117
+ }, [open, close]);
118
+
119
+ useOutsideClick(wrapperRef, close, open);
120
+
121
+ // Keep the panel inside the viewport (feedback #85): a wide panel anchored to a
122
+ // trigger near a screen edge would otherwise spill off it (on mobile the guided-
123
+ // tours menu ran off the left). Once open, measure and translate it horizontally
124
+ // just enough to sit within an 8px margin. Recomputed on each open; `shiftX` is
125
+ // subtracted first so the measurement is against the natural position.
126
+ const panelRef = useRef<HTMLDivElement>(null);
127
+ const [shiftX, setShiftX] = useState(0);
128
+ useLayoutEffect(() => {
129
+ if (!open) {
130
+ setShiftX(0);
131
+ return;
132
+ }
133
+ const el = panelRef.current;
134
+ if (!el) return;
135
+ const rect = el.getBoundingClientRect();
136
+ if (rect.width === 0) return; // unmeasured (e.g. jsdom) — leave as-is
137
+ const margin = 8;
138
+ const naturalLeft = rect.left - shiftX;
139
+ const naturalRight = rect.right - shiftX;
140
+ let next = 0;
141
+ if (naturalRight > window.innerWidth - margin) next = window.innerWidth - margin - naturalRight;
142
+ if (naturalLeft + next < margin) next = margin - naturalLeft;
143
+ setShiftX(next);
144
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- recompute only on open/close
145
+ }, [open]);
146
+
147
+ return (
148
+ <div
149
+ // `...rest` first: the hover handlers below are the whole component, and a caller
150
+ // passing one of its own must not silently replace them.
151
+ {...rest}
152
+ ref={wrapperRef}
153
+ className={cn("relative", className)}
154
+ onMouseEnter={handleMouseEnter}
155
+ onMouseLeave={handleMouseLeave}
156
+ // The DOM spelling wins; {@link HoverMenuProps.ariaLabel} is the deprecated alias
157
+ // three apps still pass, so it stays as the fallback rather than as the answer.
158
+ aria-label={ariaLabelAttr ?? ariaLabel}
159
+ >
160
+ {trigger({ open, toggle })}
161
+ {open && (
162
+ <div
163
+ ref={panelRef}
164
+ className={cn(
165
+ "absolute top-full z-40 pt-2",
166
+ align === "right" ? "right-0" : "left-0",
167
+ )}
168
+ style={shiftX ? { transform: `translateX(${shiftX}px)` } : undefined}
169
+ >
170
+ <div
171
+ role="menu"
172
+ className={cn(
173
+ "min-w-44 max-w-[calc(100vw-1rem)] rounded-md border border-[var(--border)] bg-[var(--bg-surface)] shadow-lg",
174
+ panelClassName,
175
+ )}
176
+ >
177
+ {children(close)}
178
+ </div>
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }