@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,287 @@
1
+ import { useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import type {
4
+ ComponentPropsWithoutRef,
5
+ KeyboardEvent,
6
+ MouseEvent as ReactMouseEvent,
7
+ PointerEvent as ReactPointerEvent,
8
+ ReactNode,
9
+ } from "react";
10
+ import { cn } from "../lib/cn";
11
+ import { useFocusTrap } from "../hooks/use-focus-trap";
12
+ import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
13
+ import { useOverlayHistory } from "../hooks/use-overlay-history";
14
+ import { useCloseTransition } from "../hooks/use-close-transition";
15
+
16
+ /**
17
+ * Drag the panel by its top strip, in pointer space (dev#460).
18
+ *
19
+ * Deliberately not a library and not persisted: the offset is component state that
20
+ * dies with the dialog. It starts only on a press that lands on the panel's own
21
+ * chrome — never on a field, a button or a link, which is what keeps "drag to select
22
+ * text in the textarea" working. Above `md` only: below it the panel is a
23
+ * full-width bottom sheet with nothing beside it to uncover.
24
+ *
25
+ * The whole gesture stays on the PANEL — captured pointer, React handlers — rather than
26
+ * on `window`. The first shape of this added `pointermove`/`pointerup` to `window` from
27
+ * inside the `pointerdown` handler and took them off again from the `pointerup` handler,
28
+ * so the teardown ran only if the gesture ended: React never sees a listener added from
29
+ * an event handler, and an unmount mid-drag left a live `pointermove` on `window` for
30
+ * the life of the page, pinning this hook's state with it. The one caller that enables
31
+ * dragging is the feedback composer, which closes ITSELF on submit — a Ctrl+Enter sent
32
+ * with the pointer still down is exactly that order of events, once per drag.
33
+ */
34
+ function useDragOffset(enabled: boolean) {
35
+ const [offset, setOffset] = useState<{ x: number; y: number } | null>(null);
36
+ const from = useRef<{ x: number; y: number; ox: number; oy: number } | null>(null);
37
+
38
+ const endDrag = () => {
39
+ from.current = null;
40
+ };
41
+
42
+ return {
43
+ style: offset ? { transform: `translate(${offset.x}px, ${offset.y}px)` } : undefined,
44
+ handlers: !enabled
45
+ ? undefined
46
+ : {
47
+ onPointerDown: (e: ReactPointerEvent<HTMLDivElement>) => {
48
+ if (e.button !== 0 || window.innerWidth < 768) return;
49
+ const target = e.target as HTMLElement;
50
+ // Only the panel itself and its non-interactive chrome start a drag; a
51
+ // press inside any control belongs to that control.
52
+ if (target.closest("input,textarea,select,button,a,[role='button']")) return;
53
+ from.current = { x: e.clientX, y: e.clientY, ox: offset?.x ?? 0, oy: offset?.y ?? 0 };
54
+ try {
55
+ // Capture is what lets the handlers live on the panel: every later
56
+ // pointer event for this pointer retargets here, so the drag keeps
57
+ // working once the pointer outruns the panel's own box — which it always
58
+ // does, since the panel moves out from under it.
59
+ e.currentTarget.setPointerCapture(e.pointerId);
60
+ } catch {
61
+ // jsdom implements no pointer capture, and a browser throws
62
+ // NotFoundError for a pointerId that is already gone. Neither is worth
63
+ // refusing the drag: uncaptured, the panel still gets the moves that
64
+ // land on it.
65
+ }
66
+ },
67
+ onPointerMove: (e: ReactPointerEvent<HTMLDivElement>) => {
68
+ const start = from.current;
69
+ if (!start) return;
70
+ setOffset({ x: start.ox + e.clientX - start.x, y: start.oy + e.clientY - start.y });
71
+ },
72
+ onPointerUp: endDrag,
73
+ // A cancel (the browser taking the pointer for a scroll or a gesture) and a
74
+ // lost capture both mean no `pointerup` is coming. Without them the drag would
75
+ // stay "live" and the next move over the panel would jump it.
76
+ onPointerCancel: endDrag,
77
+ onLostPointerCapture: endDrag,
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Backdrop handlers that close only when a press starts AND ends on the
84
+ * backdrop itself. A plain onClick fires on mouseup, so dragging a text
85
+ * selection that starts inside the panel and releases over the backdrop would
86
+ * dismiss the dialog (feedback #254) — and the reverse drag would too, since
87
+ * click targets the common ancestor of the down/up elements.
88
+ */
89
+ export function useBackdropClose(onClose: () => void) {
90
+ const pressStartedOnBackdrop = useRef(false);
91
+ return {
92
+ onMouseDown: (e: ReactMouseEvent<HTMLDivElement>) => {
93
+ pressStartedOnBackdrop.current = e.target === e.currentTarget;
94
+ },
95
+ onMouseUp: (e: ReactMouseEvent<HTMLDivElement>) => {
96
+ const started = pressStartedOnBackdrop.current;
97
+ pressStartedOnBackdrop.current = false;
98
+ if (started && e.target === e.currentTarget) onClose();
99
+ },
100
+ };
101
+ }
102
+
103
+ // Tab-cycle target set: the interactive descendants the focus trap rotates
104
+ // through. Querying live (per keystroke) means dynamically rendered fields are
105
+ // included automatically.
106
+ /**
107
+ * `extends ComponentPropsWithoutRef<"div">` so anything a `<div>` takes reaches the
108
+ * PANEL — the element with `role="dialog"`, which is the one a consumer means. That is
109
+ * where a `data-tour` anchor, a test id, an `aria-describedby` or an `aria-label` has
110
+ * to land; before this, a closed prop list dropped all four silently (audit §api-design).
111
+ */
112
+ export interface ModalProps extends ComponentPropsWithoutRef<"div"> {
113
+ /** Invoked on backdrop click and on Escape. */
114
+ onClose: () => void;
115
+ children: ReactNode;
116
+ /**
117
+ * Panel max-width: `md` (28rem), `lg` (32rem), `xl` (48rem).
118
+ *
119
+ * `xl` exists because two callers had already written `className="max-w-3xl"` to
120
+ * reach it — the payee backfill's preview table and the price-merge dialog, both
121
+ * of which hold a list and several fields rather than one question. A caller
122
+ * patching a missing size IS the signal that the size is missing, and a
123
+ * hand-written width in a `className` is a width nothing else can line up with.
124
+ *
125
+ * The panel is `w-full` under every one of them, so the cap only bites once the
126
+ * viewport is wider than it: on a phone all three are the same full-width bottom
127
+ * sheet. Nothing here may introduce a `min-w-*`, which is what would break that.
128
+ */
129
+ size?: "md" | "lg" | "xl";
130
+ /** Extra classes for the panel (e.g. `space-y-3` for form content spacing). */
131
+ className?: string;
132
+ /** id of the heading element, wired to `aria-labelledby`. */
133
+ labelledBy?: string;
134
+ /**
135
+ * Extra key handling on the panel (e.g. Ctrl/Cmd+Enter to submit). Runs first;
136
+ * if it calls `preventDefault()` the built-in Escape/Tab handling is skipped
137
+ * for that event.
138
+ */
139
+ onKeyDown?: (e: KeyboardEvent<HTMLDivElement>) => void;
140
+ /**
141
+ * Drop the backdrop's mobile margin so the panel can reach the screen edges
142
+ * (a full-screen sheet on phones). The panel still needs its own
143
+ * `h-[100dvh] max-w-full rounded-none` classes via `className` to fill it; the
144
+ * desktop `md:p-4` margin is kept. Defaults to false (padded on all sizes).
145
+ */
146
+ fullBleed?: boolean;
147
+ /**
148
+ * Let the user drag the panel out of the way by its top edge (Keksdose feedback
149
+ * dev#460: *"Make the feedback dialog draggable so I can see behind it if it
150
+ * blocks something."*).
151
+ *
152
+ * Opt-in, because it only makes sense for a dialog whose content is ABOUT the page
153
+ * behind it — the feedback composer is the case, an ordinary form is not, and a
154
+ * draggable panel that has nothing to reveal is a control that can only get lost.
155
+ *
156
+ * Pointer-only by design: it is a "move this out of the way" gesture, not a layout
157
+ * choice worth persisting, and the position resets when the dialog closes. On a
158
+ * phone the panel is a bottom sheet the width of the screen, so dragging is
159
+ * suppressed there — there is nothing beside it to see.
160
+ */
161
+ draggable?: boolean;
162
+ }
163
+
164
+ /**
165
+ * Centered (bottom-sheet on mobile) modal dialog: dimmed backdrop, single
166
+ * rounded panel. Owns the overlay mechanics that the hand-rolled dialogs each
167
+ * re-implemented or lacked — backdrop-click + Escape to close, the platform Back
168
+ * gesture (Keksdose feedback #172), background scroll lock (feedback #204), focus
169
+ * into the panel on open and back to the trigger on close, a Tab focus trap, and
170
+ * `role="dialog"`/`aria-modal`.
171
+ */
172
+ export function Modal({
173
+ onClose,
174
+ children,
175
+ size = "md",
176
+ className,
177
+ labelledBy,
178
+ onKeyDown,
179
+ fullBleed,
180
+ draggable,
181
+ style,
182
+ ...rest
183
+ }: ModalProps) {
184
+ const panelRef = useRef<HTMLDivElement>(null);
185
+ const drag = useDragOffset(Boolean(draggable));
186
+ // The panel lowers itself before the caller unmounts it (live #320 rework). Every
187
+ // dismissal below goes through `requestClose`; `onClose` still does the closing, one
188
+ // animation later. A caller that closes the dialog ITSELF — after a save, say —
189
+ // unmounts with no exit, which is the documented limit of this.
190
+ const { closing, requestClose } = useCloseTransition(onClose);
191
+ const backdropClose = useBackdropClose(requestClose);
192
+ // Back means the same as Escape here. On a phone Escape doesn't exist, so without
193
+ // this the only way out of a dialog is finding its close button — and Back, the
194
+ // gesture everyone reaches for, navigated the page underneath instead (#172).
195
+ // Mounted only while open, hence the constant `true`.
196
+ useOverlayHistory(true, requestClose);
197
+
198
+ // Mounted only while open, hence the constant `true`. Through the shared hook and
199
+ // not by hand: this component's own save/restore copy was one half of the pair that
200
+ // left the page permanently unscrollable when a dialog containing an open sheet was
201
+ // closed — see the note in use-body-scroll-lock.ts.
202
+ useBodyScrollLock(true);
203
+
204
+ // Focus the panel itself (not the first field) so opening doesn't pop the mobile
205
+ // keyboard, while still moving focus into the dialog for keyboard and screen-reader
206
+ // users. Through the shared hook since this component is where it came from — the
207
+ // hook adds per-keystroke recomputation, a guard on restoring to a detached node,
208
+ // and nesting, none of which this copy had.
209
+ useFocusTrap(panelRef, { active: true, initialFocus: "container" });
210
+
211
+ const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
212
+ onKeyDown?.(e);
213
+ if (e.defaultPrevented) return;
214
+ if (e.key === "Escape") {
215
+ e.stopPropagation();
216
+ requestClose();
217
+ return;
218
+ }
219
+ // Tab containment lives in useFocusTrap, which listens on the panel itself.
220
+ };
221
+
222
+ // Portal to <body> so the `fixed inset-0 z-50` overlay escapes whatever stacking
223
+ // context its caller sits in. Rendered inline, a modal opened from e.g. the top
224
+ // bar (a z-30 context) could paint BELOW a sibling z-30 sticky element like the
225
+ // expanded transaction editor (feedback #320). The tour/command-palette overlays
226
+ // already portal for the same reason.
227
+ return createPortal(
228
+ <div
229
+ className={cn(
230
+ // Live #320. The panel below rises only where it is bottom-anchored: from md up
231
+ // it is centred, and a centred box sliding up from off-screen reads as a
232
+ // different component arriving rather than as the same one settling.
233
+ closing ? "animate-overlay-out" : "animate-overlay",
234
+ "fixed inset-0 z-50 flex items-end justify-center bg-black/40 md:items-center",
235
+ fullBleed ? "p-0 md:p-4" : "p-4",
236
+ )}
237
+ {...backdropClose}
238
+ >
239
+ <div
240
+ // `...rest` FIRST, everything the dialog needs to be a dialog after it. A
241
+ // wrapper that spreads its own props through to this one cannot then take away
242
+ // the role, the modality or the `tabIndex={-1}` that `useFocusTrap` needs —
243
+ // losing any of the three is silent, and the symptom (a dialog a screen reader
244
+ // walks straight out of) shows up nowhere near the call site that caused it.
245
+ {...rest}
246
+ ref={panelRef}
247
+ role="dialog"
248
+ aria-modal="true"
249
+ aria-labelledby={labelledBy}
250
+ tabIndex={-1}
251
+ onKeyDown={handleKeyDown}
252
+ {...drag.handlers}
253
+ // MERGED, not replaced: `drag.style` is undefined until a drag starts, so
254
+ // assigning it outright would drop a caller's own `style` on every render but
255
+ // the ones where the panel is being dragged.
256
+ style={{ ...style, ...drag.style }}
257
+ className={cn(
258
+ closing ? "animate-sheet-out md:animate-none" : "animate-sheet md:animate-none",
259
+ "w-full rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm outline-none",
260
+ { md: "max-w-md", lg: "max-w-lg", xl: "max-w-3xl" }[size],
261
+ // A panel taller than the screen has to scroll ITSELF. The backdrop is
262
+ // `fixed inset-0` and the body is scroll-locked while a dialog is open, so
263
+ // without this the overflow simply had nowhere to go: on a phone the panel
264
+ // is bottom-anchored, so it grew UPWARD past the top edge and the part that
265
+ // went past it could not be reached by any gesture (Keksdose live #254, on
266
+ // the price-merge dialog — but it was every dialog with more than a screen
267
+ // of content). `max-h-full` resolves against the backdrop's content box,
268
+ // which is the viewport minus its own padding, so the panel stops exactly
269
+ // where the screen does; `overscroll-contain` keeps the fling inside it.
270
+ //
271
+ // Three callers had already patched themselves with `max-h-[90vh]` +
272
+ // an inner scroller. Those still win — `cn` is tailwind-merge — and this is
273
+ // the default they should not have needed.
274
+ "max-h-full overflow-y-auto overscroll-contain",
275
+ "p-4",
276
+ // The grab affordance sits on the panel's own top strip: a drag handle of
277
+ // its own would be one more control in a dialog that is mostly one field.
278
+ draggable && "md:[&>*:first-child]:cursor-grab",
279
+ className,
280
+ )}
281
+ >
282
+ {children}
283
+ </div>
284
+ </div>,
285
+ document.body,
286
+ );
287
+ }