@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,54 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ /**
4
+ * The phone's endless scroll, in place of the desktop pager (feedback #232).
5
+ *
6
+ * A client-side table already holds every row, so there is nothing to fetch — it simply
7
+ * reveals more of the sorted list as a sentinel scrolls into view. A SERVER-paginated
8
+ * table keeps its pager instead, because the rest of the rows genuinely are not here.
9
+ *
10
+ * The reveal count only ever grows and self-caps at the (re-filtered) list length, which
11
+ * is why nothing resets it: narrowing a filter cannot leave the limit pointing past the
12
+ * end, and widening one does not have to re-reveal what the user already scrolled past.
13
+ *
14
+ * Lifted out of `DataTable` with the rest of its state; the sentinel ref belongs to the
15
+ * caller's `<li>`.
16
+ */
17
+ export function useMobileReveal<T>({
18
+ rows,
19
+ slice,
20
+ isServer,
21
+ isMdUp,
22
+ defaultPageSize,
23
+ }: {
24
+ /** The whole sorted list — what there is to reveal. */
25
+ rows: T[];
26
+ /** What a server-paginated table shows instead. */
27
+ slice: T[];
28
+ isServer: boolean;
29
+ isMdUp: boolean;
30
+ defaultPageSize: number;
31
+ }) {
32
+ const [mobileLimit, setMobileLimit] = useState(defaultPageSize);
33
+ const mobileSlice = isServer ? slice : rows.slice(0, mobileLimit);
34
+ const canRevealMoreMobile = !isServer && !isMdUp && mobileLimit < rows.length;
35
+ const loadMoreRef = useRef<HTMLLIElement | null>(null);
36
+
37
+ useEffect(() => {
38
+ if (!canRevealMoreMobile) return;
39
+ const el = loadMoreRef.current;
40
+ if (!el || typeof IntersectionObserver === "undefined") return;
41
+ const io = new IntersectionObserver(
42
+ (entries) => {
43
+ if (entries.some((e) => e.isIntersecting)) {
44
+ setMobileLimit((n) => n + (defaultPageSize || 25));
45
+ }
46
+ },
47
+ { rootMargin: "300px" },
48
+ );
49
+ io.observe(el);
50
+ return () => io.disconnect();
51
+ }, [canRevealMoreMobile, mobileLimit, defaultPageSize]);
52
+
53
+ return { mobileSlice, canRevealMoreMobile, loadMoreRef };
54
+ }
@@ -0,0 +1,295 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { useSearchParams } from "react-router";
3
+ import { readStored, writeStored } from "../lib/safe-storage";
4
+ import {
5
+ decodeFilterValue,
6
+ encodeFilterValue,
7
+ isFilterActive,
8
+ resolveFilter,
9
+ } from "./data-table-filters";
10
+ import { decodeSorts, encodeSorts, normalizeSorts, type SortState } from "./data-table-sort";
11
+ import type { DataTableColumn, FilterState } from "./data-table";
12
+
13
+ // ---------- Persisted + URL-encoded view state ----------
14
+ //
15
+ // Moved here with `useTableState` when `DataTable` was split: these are what the hook
16
+ // reads and writes, and nothing else in the table ever called them.
17
+
18
+ interface PersistedState {
19
+ // Multi-sort array; blobs from before the upgrade hold a single {key, dir}
20
+ // object (or null) — read back through normalizeSorts.
21
+ sort: SortState[] | { key: string; dir: string } | null;
22
+ filters: FilterState;
23
+ pageSize: number | "all";
24
+ widths?: Record<string, number>;
25
+ hidden?: string[];
26
+ }
27
+
28
+ export const DEFAULT_PERSIST_PREFIX = "hbui-table:";
29
+
30
+ function loadPersisted(storageKey: string | undefined, prefix: string): Partial<PersistedState> {
31
+ if (!storageKey) return {};
32
+ const raw = readStored(prefix + storageKey);
33
+ if (!raw) return {};
34
+ try {
35
+ const parsed = JSON.parse(raw) as Partial<PersistedState>;
36
+ return parsed && typeof parsed === "object" ? parsed : {};
37
+ } catch {
38
+ return {}; // corrupt JSON — the storage access itself is guarded by readStored
39
+ }
40
+ }
41
+
42
+ /** The blob that would be stored — also the value the effect below compares by, so
43
+ * the comparison and the write can never disagree about what "unchanged" means. */
44
+ function serializePersisted(state: PersistedState): string {
45
+ return JSON.stringify(state);
46
+ }
47
+
48
+ // ---------- URL sync helpers ----------
49
+
50
+ const URL_FILTER_PREFIX = "f.";
51
+ const URL_SORT_KEY = "sort";
52
+ const URL_PAGE_KEY = "p";
53
+ const URL_PAGE_SIZE_KEY = "ps";
54
+
55
+ interface UrlState {
56
+ sorts?: SortState[];
57
+ filters?: FilterState;
58
+ pageSize?: number; // Infinity for "all"
59
+ page?: number; // 0-based
60
+ }
61
+
62
+ function readUrlState<T>(columns: DataTableColumn<T>[]): UrlState {
63
+ if (typeof window === "undefined") return {};
64
+ const sp = new URLSearchParams(window.location.search);
65
+ const out: UrlState = {};
66
+
67
+ const filters: FilterState = {};
68
+ for (const col of columns) {
69
+ const filter = resolveFilter(col);
70
+ if (!filter) continue;
71
+ const raw = sp.get(URL_FILTER_PREFIX + col.key);
72
+ if (raw == null) continue;
73
+ filters[col.key] = decodeFilterValue(filter, raw);
74
+ }
75
+ if (Object.keys(filters).length) out.filters = filters;
76
+
77
+ const sortRaw = sp.get(URL_SORT_KEY);
78
+ if (sortRaw) {
79
+ const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));
80
+ if (decoded.length) out.sorts = decoded;
81
+ }
82
+
83
+ const psRaw = sp.get(URL_PAGE_SIZE_KEY);
84
+ if (psRaw === "all") out.pageSize = Infinity;
85
+ else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);
86
+
87
+ const pRaw = sp.get(URL_PAGE_KEY);
88
+ if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;
89
+
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * The managed part of the query string, as a string, for comparing by VALUE.
95
+ *
96
+ * Everything the table puts in the address and nothing else, in a fixed order.
97
+ * `columns` is walked for its keys only, which is why it can be left out of the
98
+ * effect's dependencies without lying: a new column ARRAY carrying the same keys
99
+ * produces the same signature, and a genuinely different set of filterable columns
100
+ * produces a different one.
101
+ */
102
+ function urlSignature<T>(
103
+ columns: DataTableColumn<T>[],
104
+ sorts: SortState[],
105
+ filters: FilterState,
106
+ pageSize: number,
107
+ page: number,
108
+ defaultPageSize: number,
109
+ ): string {
110
+ const parts: string[] = [];
111
+ for (const col of columns) {
112
+ const state = filters[col.key];
113
+ if (!state || !isFilterActive(state)) continue;
114
+ const enc = encodeFilterValue(state);
115
+ if (enc != null) parts.push(`${URL_FILTER_PREFIX}${col.key}=${enc}`);
116
+ }
117
+ parts.push(`${URL_SORT_KEY}=${encodeSorts(sorts)}`);
118
+ parts.push(
119
+ `${URL_PAGE_SIZE_KEY}=${
120
+ pageSize === Infinity ? "all" : pageSize === defaultPageSize ? "" : String(pageSize)
121
+ }`,
122
+ );
123
+ parts.push(`${URL_PAGE_KEY}=${page > 0 ? String(page + 1) : ""}`);
124
+ return parts.join("&");
125
+ }
126
+
127
+ function writeUrlState<T>(
128
+ setSearchParams: ReturnType<typeof useSearchParams>[1],
129
+ columns: DataTableColumn<T>[],
130
+ sorts: SortState[],
131
+ filters: FilterState,
132
+ pageSize: number,
133
+ page: number,
134
+ defaultPageSize: number,
135
+ ): void {
136
+ setSearchParams(
137
+ (prev) => {
138
+ const next = new URLSearchParams(prev);
139
+ // clear managed keys
140
+ for (const col of columns) next.delete(URL_FILTER_PREFIX + col.key);
141
+ next.delete(URL_SORT_KEY);
142
+ next.delete(URL_PAGE_KEY);
143
+ next.delete(URL_PAGE_SIZE_KEY);
144
+ // write filters
145
+ for (const col of columns) {
146
+ const state = filters[col.key];
147
+ if (!state || !isFilterActive(state)) continue;
148
+ const enc = encodeFilterValue(state);
149
+ if (enc != null) next.set(URL_FILTER_PREFIX + col.key, enc);
150
+ }
151
+ // write sort
152
+ const sortEnc = encodeSorts(sorts);
153
+ if (sortEnc) next.set(URL_SORT_KEY, sortEnc);
154
+ // write page size if non-default
155
+ if (pageSize === Infinity) next.set(URL_PAGE_SIZE_KEY, "all");
156
+ else if (pageSize !== defaultPageSize) next.set(URL_PAGE_SIZE_KEY, String(pageSize));
157
+ // write page if not first
158
+ if (page > 0) next.set(URL_PAGE_KEY, String(page + 1));
159
+ return next;
160
+ },
161
+ { replace: true },
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Where a table's view lives: what it is sorted and filtered by, which page, how wide
167
+ * the columns are and which are hidden — plus the two places that outlives a render.
168
+ *
169
+ * Lifted out of `DataTable`, which was 984 lines and twenty hooks in one function. This
170
+ * is the part that is genuinely about STATE rather than about drawing, and the three
171
+ * rules it encodes are easy to lose among the markup:
172
+ *
173
+ * * **The URL is read once, on mount.** A shared or deep link populates the initial view;
174
+ * after that the state is local, or the browser's own history would fight the user's
175
+ * next click.
176
+ * * **Sorting and filtering are optionally CONTROLLED.** A server-driven table passes
177
+ * state and a callback and owns them; everything else is self-owned. That is the
178
+ * `sortsProp ?? internalSorts` pair, and it is why the setters are exposed separately.
179
+ * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the
180
+ * per-table identity; `urlSync` is opt-in because only some tables want their view in
181
+ * a shareable address.
182
+ */
183
+ export function useTableState<T>({
184
+ columns,
185
+ storageKey,
186
+ storageKeyPrefix,
187
+ urlSync,
188
+ setSearchParams,
189
+ defaultPageSize,
190
+ sortsProp,
191
+ filtersProp,
192
+ }: {
193
+ columns: DataTableColumn<T>[];
194
+ storageKey?: string;
195
+ storageKeyPrefix: string;
196
+ urlSync?: boolean;
197
+ setSearchParams: ReturnType<typeof useSearchParams>[1];
198
+ defaultPageSize: number;
199
+ sortsProp?: SortState[];
200
+ filtersProp?: FilterState;
201
+ }) {
202
+ // Read URL once on mount so URL-encoded views (e.g. shared links, deep links from
203
+ // other pages) populate initial state. After mount, state is local.
204
+ const [urlInitial] = useState<UrlState>(() => (urlSync ? readUrlState(columns) : {}));
205
+ const initial = useMemo(
206
+ () => loadPersisted(storageKey, storageKeyPrefix),
207
+ [storageKey, storageKeyPrefix],
208
+ );
209
+ const [internalSorts, setInternalSorts] = useState<SortState[]>(() =>
210
+ urlInitial.sorts !== undefined ? urlInitial.sorts : normalizeSorts(initial.sort),
211
+ );
212
+ const [internalFilters, setInternalFilters] = useState<FilterState>(
213
+ () => urlInitial.filters ?? initial.filters ?? {},
214
+ );
215
+ // Controlled when the owner passes state + callback (server-driven tables);
216
+ // self-owned otherwise.
217
+ const sorts = sortsProp ?? internalSorts;
218
+ const filters = filtersProp ?? internalFilters;
219
+ const [page, setPage] = useState(() => urlInitial.page ?? 0);
220
+ const [pageSize, setPageSize] = useState<number>(() => {
221
+ if (urlInitial.pageSize !== undefined) return urlInitial.pageSize;
222
+ if (initial.pageSize === "all") return Infinity;
223
+ if (typeof initial.pageSize === "number" && initial.pageSize > 0) return initial.pageSize;
224
+ return defaultPageSize;
225
+ });
226
+ const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});
227
+ const [hiddenCols, setHiddenCols] = useState<Set<string>>(() => new Set(initial.hidden ?? []));
228
+ const [showSettings, setShowSettings] = useState(false);
229
+ const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);
230
+
231
+ // ---- Writing out: both of these compare by VALUE ----
232
+ //
233
+ // A CONTROLLED table (`sorts`/`filters` props plus callbacks — every server-driven
234
+ // one) receives a freshly-built object on every render of the page around it, and
235
+ // `{}` is never `===` the `{}` before it. Keyed on identity, both effects below
236
+ // therefore fired on every unrelated parent render: persistence wrote the same JSON
237
+ // into `localStorage` each time — a synchronous main-thread write, on a page whose
238
+ // other half may be a form being typed into — and the URL one handed react-router a
239
+ // `replace` it answers with a render, which is the loop the audit records. A `ref`
240
+ // holding what was last written is what makes "changed" mean changed.
241
+ const lastPersisted = useRef<string | null>(null);
242
+ const persisted = !storageKey
243
+ ? ""
244
+ : serializePersisted({
245
+ sort: sorts,
246
+ filters,
247
+ pageSize: pageSize === Infinity ? "all" : pageSize,
248
+ widths,
249
+ hidden: Array.from(hiddenCols),
250
+ });
251
+ useEffect(() => {
252
+ if (!storageKey) return;
253
+ // The destination is part of the signature: two tables can hold identical state,
254
+ // and a table that swaps `storageKey` has to write under the new one even though
255
+ // nothing about the view changed.
256
+ const target = storageKeyPrefix + storageKey;
257
+ const sig = `${target}\u0000${persisted}`;
258
+ if (lastPersisted.current === sig) return;
259
+ lastPersisted.current = sig;
260
+ writeStored(target, persisted);
261
+ }, [storageKey, storageKeyPrefix, persisted]);
262
+
263
+ const lastUrl = useRef<string | null>(null);
264
+ const urlSig = urlSync
265
+ ? urlSignature(columns, sorts, filters, pageSize, page, defaultPageSize)
266
+ : "";
267
+ useEffect(() => {
268
+ if (!urlSync) return;
269
+ if (lastUrl.current === urlSig) return;
270
+ lastUrl.current = urlSig;
271
+ // `columns` is deliberately not a dependency and does not need to be: the
272
+ // signature above is computed from it, so a column set that would produce a
273
+ // different address produces a different signature first.
274
+ writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize);
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ }, [urlSync, urlSig]);
277
+
278
+ return {
279
+ sorts,
280
+ filters,
281
+ setInternalSorts,
282
+ setInternalFilters,
283
+ page,
284
+ setPage,
285
+ pageSize,
286
+ setPageSize,
287
+ widths,
288
+ setWidths,
289
+ hiddenCols,
290
+ setHiddenCols,
291
+ showSettings,
292
+ setShowSettings,
293
+ headRefs,
294
+ };
295
+ }
@@ -0,0 +1,61 @@
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ /** Initials from a display name (first + last) or, failing that, an email —
5
+ * e.g. "Marcel Eifert" → "ME", "marcel@x.com" → "MA". App-agnostic.
6
+ *
7
+ * "Failing that" means BLANK, not merely absent. This was `name ?? email ?? "?"`,
8
+ * and `??` falls through on null/undefined alone — so an empty or whitespace-only
9
+ * display name short-circuited the email it had in hand and landed on the guard
10
+ * below, returning "?" for a user whose address says "MA" perfectly well. Keksdose
11
+ * cannot reach it (its `display_name` has `min_length=1`), but this is exported as
12
+ * app-agnostic, and any consumer whose profile name is optional free text got a wall
13
+ * of "?" chips. The guard is what shows the empty case was foreseen; it just
14
+ * answered it at the wrong end. */
15
+ export function avatarInitials(name?: string | null, email?: string | null): string {
16
+ const source = [name, email].map((s) => s?.trim()).find(Boolean) ?? "?";
17
+ const parts = source.split(/\s+/).filter(Boolean);
18
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
19
+ return source.slice(0, 2).toUpperCase();
20
+ }
21
+
22
+ const AVATAR_SIZES = {
23
+ sm: "size-7 text-[11px]",
24
+ md: "size-8 text-xs",
25
+ lg: "size-12 text-base",
26
+ } as const;
27
+
28
+ /** A `<span>`'s props, minus `children`: the content is the initials this computes
29
+ * from `name`/`email`, so there is nothing for a caller to put inside. */
30
+ export interface UserAvatarProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
31
+ name?: string | null;
32
+ email?: string | null;
33
+ size?: keyof typeof AVATAR_SIZES;
34
+ }
35
+
36
+ /**
37
+ * A round initials avatar — the common user chip shared across apps (feedback
38
+ * #333). Purely presentational; wrap it in a button for the account menu trigger.
39
+ */
40
+ export function UserAvatar({ name, email, size = "md", className, ...rest }: UserAvatarProps) {
41
+ return (
42
+ <span
43
+ // `aria-hidden` sits BEFORE the spread, unlike the structural attributes on the
44
+ // other components here, because it is a DEFAULT rather than an invariant. The
45
+ // avatar is decorative next to the name it belongs to — which is where all three
46
+ // apps render it — so hiding it stops a screen reader saying "ME" after the word
47
+ // "Marcel Eifert". A caller who renders one ALONE (an assignee column, a
48
+ // presence dot) needs the opposite, and now has it: `aria-hidden={false}` plus an
49
+ // `aria-label` arrives through `...rest` and wins.
50
+ aria-hidden
51
+ {...rest}
52
+ className={cn(
53
+ "inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--bg-inverse)] font-semibold text-[var(--text-inverse)]",
54
+ AVATAR_SIZES[size],
55
+ className,
56
+ )}
57
+ >
58
+ {avatarInitials(name, email)}
59
+ </span>
60
+ );
61
+ }
@@ -0,0 +1,37 @@
1
+ import { cn } from "../lib/cn";
2
+
3
+ /** Compact step indicator for the import wizards. Past steps are plain, the
4
+ * current one is highlighted, future ones muted. */
5
+ export function WizardStepper({
6
+ steps,
7
+ current,
8
+ ariaLabel,
9
+ }: {
10
+ steps: { key: string; label: string }[];
11
+ current: string;
12
+ ariaLabel?: string;
13
+ }) {
14
+ const currentIndex = steps.findIndex((s) => s.key === current);
15
+ return (
16
+ <ol className="flex items-center gap-1 text-xs" aria-label={ariaLabel}>
17
+ {steps.map((step, i) => (
18
+ <li key={step.key} className="flex items-center gap-1">
19
+ {i > 0 && <span className="w-4 border-t border-[var(--border)]" />}
20
+ <span
21
+ aria-current={step.key === current ? "step" : undefined}
22
+ className={cn(
23
+ "rounded-full px-2 py-0.5",
24
+ i === currentIndex
25
+ ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)] font-medium"
26
+ : i < currentIndex
27
+ ? "text-[var(--text-secondary)]"
28
+ : "text-[var(--text-placeholder)]",
29
+ )}
30
+ >
31
+ {step.label}
32
+ </span>
33
+ </li>
34
+ ))}
35
+ </ol>
36
+ );
37
+ }
@@ -0,0 +1,19 @@
1
+ // `@eifi1/ui-kit/data-table` — the table suite.
2
+ //
3
+ // A re-slicing of the main barrel, not a new API. This entry point requires
4
+ // `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`
5
+ // branch, so a Router is needed even with URL sync switched off.
6
+ export * from "./components/data-table-labels";
7
+ export * from "./components/data-table-sort";
8
+ export * from "./components/data-table-filters";
9
+ export * from "./components/data-table-pagination";
10
+ export { FilterPopover } from "./components/data-table-filter-popover";
11
+ // `SortState` comes from data-table-sort above; re-exporting the module wholesale
12
+ // would collide with it, which is why the main barrel names these explicitly too.
13
+ export { DataTable } from "./components/data-table";
14
+ export type {
15
+ DataTableColumn,
16
+ DataTableProps,
17
+ ServerPagination,
18
+ FilterState,
19
+ } from "./components/data-table";