@eifi1/ui-kit 0.4.1 → 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 +89 -19
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +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 +74 -19
  63. package/dist/components/dropdown.js +72 -22
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +21 -7
  75. package/dist/components/file-dropzone.js +28 -7
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +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 +21 -2
  105. package/dist/components/number-input.js +2 -1
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +25 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +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 +5 -3
  176. package/dist/components/user-avatar.js +4 -2
  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 +14 -1
  270. package/dist/theme/palette-presets.js +231 -3
  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 +66 -13
  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,176 @@
1
+ import type { DataTableColumn } from "./data-table";
2
+
3
+ /** How a column is filtered. `text`/`select`/`date`/`number` each read a value
4
+ * off the row and compare it against the user's {@link FilterValue}. */
5
+ export type ColumnFilter<T> =
6
+ | { type: "text"; getValue: (row: T) => string }
7
+ | { type: "select"; getValue: (row: T) => string; options?: { value: string; label?: string }[] }
8
+ | { type: "date"; getValue: (row: T) => string | null | undefined }
9
+ | { type: "number"; getValue: (row: T) => number | null | undefined };
10
+
11
+ /** The current state of a column's filter (what the user has entered). */
12
+ export type FilterValue =
13
+ | { type: "text"; q: string }
14
+ | { type: "select"; values: string[] }
15
+ | { type: "date"; from: string; to: string }
16
+ | { type: "number"; min: string; max: string; abs: boolean };
17
+
18
+ /** A column's filter config, falling back to a plain text filter over `filterBy`. */
19
+ export function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null {
20
+ if (col.filter) return col.filter;
21
+ if (col.filterBy) return { type: "text", getValue: col.filterBy };
22
+ return null;
23
+ }
24
+
25
+ /** Whether a filter value is actually narrowing anything (vs its empty default). */
26
+ export function isFilterActive(v: FilterValue | undefined): boolean {
27
+ if (!v) return false;
28
+ switch (v.type) {
29
+ case "text":
30
+ return v.q.trim().length > 0;
31
+ case "select":
32
+ return v.values.length > 0;
33
+ case "date":
34
+ return !!v.from || !!v.to;
35
+ case "number":
36
+ return v.min !== "" || v.max !== "" || v.abs;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Whether `row` passes column `col`'s filter given the user's `state`.
42
+ *
43
+ * `locale` is for the text filter's case folding. Plain `toLowerCase` folds by the
44
+ * root rules, under which a Turkish user typing "istanbul" does not find "İSTANBUL";
45
+ * `toLocaleLowerCase(locale)` does. Optional, and `undefined` is the runtime's default.
46
+ */
47
+ export function rowMatches<T>(
48
+ col: DataTableColumn<T>,
49
+ row: T,
50
+ state: FilterValue,
51
+ locale?: string,
52
+ ): boolean {
53
+ const filter = resolveFilter(col);
54
+ if (!filter) return true;
55
+ switch (filter.type) {
56
+ case "text": {
57
+ if (state.type !== "text" || !state.q.trim()) return true;
58
+ return filter
59
+ .getValue(row)
60
+ .toLocaleLowerCase(locale)
61
+ .includes(state.q.trim().toLocaleLowerCase(locale));
62
+ }
63
+ case "select": {
64
+ if (state.type !== "select" || state.values.length === 0) return true;
65
+ return state.values.includes(filter.getValue(row));
66
+ }
67
+ case "date": {
68
+ if (state.type !== "date") return true;
69
+ const raw = filter.getValue(row);
70
+ if (!raw) return !state.from && !state.to;
71
+ const v = raw.slice(0, 10);
72
+ if (state.from && v < state.from) return false;
73
+ if (state.to && v > state.to) return false;
74
+ return true;
75
+ }
76
+ case "number": {
77
+ if (state.type !== "number") return true;
78
+ const raw = filter.getValue(row);
79
+ if (raw == null || Number.isNaN(raw)) return !state.min && !state.max;
80
+ const x = state.abs ? Math.abs(raw) : raw;
81
+ if (state.min !== "" && x < Number(state.min)) return false;
82
+ if (state.max !== "" && x > Number(state.max)) return false;
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+
88
+ /** The empty filter state for a column's filter type. */
89
+ export function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue {
90
+ switch (filter.type) {
91
+ case "text":
92
+ return { type: "text", q: "" };
93
+ case "select":
94
+ return { type: "select", values: [] };
95
+ case "date":
96
+ return { type: "date", from: "", to: "" };
97
+ case "number":
98
+ return { type: "number", min: "", max: "", abs: false };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Escape the ONE character the select codec uses as its separator, plus the escape
104
+ * marker itself so the transform is reversible.
105
+ *
106
+ * A minimal escape rather than `encodeURIComponent`, deliberately: these strings sit
107
+ * inside a query-parameter value that `URLSearchParams` already encodes, so a full
108
+ * percent-encode would double-encode every space into `%2520` and make shared links
109
+ * unreadable for no gain. Only `,` can be misread here.
110
+ *
111
+ * Backwards compatible with every link written before this existed: a value with
112
+ * neither `,` nor `%` passes through both directions untouched, and `%` on its own
113
+ * never matched `%25` either.
114
+ */
115
+ function escapeSelectValue(v: string): string {
116
+ return v.replace(/%/g, "%25").replace(/,/g, "%2C");
117
+ }
118
+
119
+ /** Inverse of {@link escapeSelectValue}. The order matters and is the reverse of the
120
+ * escape: undoing `%2C` first cannot see the `2C` inside an escaped `%252C`. */
121
+ function unescapeSelectValue(v: string): string {
122
+ return v.replace(/%2C/g, ",").replace(/%25/g, "%");
123
+ }
124
+
125
+ /** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */
126
+ export function encodeFilterValue(v: FilterValue): string | null {
127
+ switch (v.type) {
128
+ case "text":
129
+ return v.q.trim() ? v.q : null;
130
+ case "select":
131
+ // Escaped before joining: an option value may legitimately contain the
132
+ // separator (a free-text title, a payee or category name), and without this it
133
+ // came back as two values and the filter silently matched nothing.
134
+ return v.values.length ? v.values.map(escapeSelectValue).join(",") : null;
135
+ case "date":
136
+ return v.from || v.to ? `${v.from}..${v.to}` : null;
137
+ case "number": {
138
+ const hasRange = v.min !== "" || v.max !== "";
139
+ if (!hasRange && !v.abs) return null;
140
+ return `${v.min}..${v.max}${v.abs ? "!" : ""}`;
141
+ }
142
+ }
143
+ }
144
+
145
+ /** Inverse of {@link encodeFilterValue} given just the filter type — for
146
+ * callers that know a column's type without holding its full config. */
147
+ export function decodeFilterValueOfType(type: FilterValue["type"], raw: string): FilterValue {
148
+ switch (type) {
149
+ case "text":
150
+ return { type: "text", q: raw };
151
+ case "select":
152
+ return {
153
+ type: "select",
154
+ values: raw
155
+ .split(",")
156
+ .filter((v) => v.length > 0)
157
+ .map(unescapeSelectValue),
158
+ };
159
+ case "date": {
160
+ const [from = "", to = ""] = raw.split("..");
161
+ return { type: "date", from, to };
162
+ }
163
+ case "number": {
164
+ let body = raw;
165
+ const abs = body.endsWith("!");
166
+ if (abs) body = body.slice(0, -1);
167
+ const [min = "", max = ""] = body.split("..");
168
+ return { type: "number", min, max, abs };
169
+ }
170
+ }
171
+ }
172
+
173
+ /** Inverse of {@link encodeFilterValue}, typed by the column's filter config. */
174
+ export function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue {
175
+ return decodeFilterValueOfType(filter.type, raw);
176
+ }
@@ -0,0 +1,190 @@
1
+ // All user-facing strings for the DataTable system — the `dataTable` namespace of
2
+ // `<UiKitProvider labels>`, and per table the `labels` prop over that. The package
3
+ // ships no translations; the English defaults below make the table usable with no
4
+ // wiring.
5
+
6
+ export interface DataTableLabels {
7
+ // Table chrome
8
+ columns: string;
9
+ selectAllRows: string;
10
+ sortHint: string;
11
+ filter: string;
12
+ close: string;
13
+ selectRow: string;
14
+ autoSize: string;
15
+ loading: string;
16
+ filters: string;
17
+ clearAll: string;
18
+ done: string;
19
+ // Pagination
20
+ pageSize: string;
21
+ pageSizeAll: string;
22
+ prevPage: string;
23
+ nextPage: string;
24
+ // Filter popover
25
+ clearFilter: string;
26
+ filterPlaceholder: string;
27
+ selectFilter: string;
28
+ selectAll: string;
29
+ selectNone: string;
30
+ dateFrom: string;
31
+ dateTo: string;
32
+ numberMin: string;
33
+ numberMax: string;
34
+ numberAbs: string;
35
+ // Date-range preset labels, keyed by preset key (see dateRangePresets()).
36
+ presets: Record<string, string>;
37
+
38
+ /**
39
+ * The table's own accessible name. Deliberately a label rather than a second,
40
+ * parallel `tableLabel` prop: the audit already records "three different prop
41
+ * names for the accessible name" as a finding, and a table that is not "Data
42
+ * table" says so with `labels={{ table: "Transactions" }}` at the call site.
43
+ */
44
+ table: string;
45
+
46
+ // ---- Announcements ----
47
+ // Spoken through the live region, never rendered. Each takes its numbers as
48
+ // ARGUMENTS: a count glued into an English template literal is untranslatable,
49
+ // and the grammar around it moves with the number in most languages.
50
+
51
+ /** After a filter changes: "12 of 137 rows". */
52
+ filterResults: (shown: number, total: number) => string;
53
+ /** After a header is activated: "Sorted by Name, ascending". */
54
+ sortedAscending: (column: string) => string;
55
+ sortedDescending: (column: string) => string;
56
+ /** The third click, which removes the column from the sort entirely. */
57
+ sortCleared: (column: string) => string;
58
+ /** After paging: "Page 3 of 14". `page` is 1-BASED, as spoken. */
59
+ pageChanged: (page: number, totalPages: number) => string;
60
+
61
+ // ---- Counted chrome ----
62
+ // Visible text that interpolates numbers. These were template literals until the
63
+ // audit flagged them: a consumer could translate every key in this file and still
64
+ // be left with English (and with ASCII digit grouping) in the footer.
65
+
66
+ /** The pagination range summary: "1–25 / 137". */
67
+ pageRange: (from: number, to: number, total: number) => string;
68
+ /** The same summary when the page size is "all" — just the row count. */
69
+ rowCount: (total: number) => string;
70
+ /** The column-settings rail and its heading: "Columns (4/9)". */
71
+ columnsCount: (visible: number, total: number) => string;
72
+ }
73
+
74
+ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
75
+ columns: "Columns",
76
+ selectAllRows: "Select all rows",
77
+ sortHint: "Click to sort · Shift-click to add a sort",
78
+ filter: "Filter",
79
+ close: "Close",
80
+ selectRow: "Select row",
81
+ autoSize: "Auto-size columns",
82
+ loading: "Loading…",
83
+ filters: "Filters",
84
+ clearAll: "Clear all",
85
+ done: "Done",
86
+ pageSize: "Rows per page",
87
+ pageSizeAll: "All",
88
+ prevPage: "Previous page",
89
+ nextPage: "Next page",
90
+ clearFilter: "Clear filter",
91
+ filterPlaceholder: "Filter…",
92
+ selectFilter: "Select",
93
+ selectAll: "All",
94
+ selectNone: "None",
95
+ dateFrom: "From",
96
+ dateTo: "To",
97
+ numberMin: "Min",
98
+ numberMax: "Max",
99
+ numberAbs: "Absolute value",
100
+ presets: {
101
+ today: "Today",
102
+ yesterday: "Yesterday",
103
+ this_week: "This week",
104
+ last_week: "Last week",
105
+ last_7_days: "Last 7 days",
106
+ last_30_days: "Last 30 days",
107
+ this_month: "This month",
108
+ last_month: "Last month",
109
+ last_3_months: "Last 3 months",
110
+ ytd: "Year to date",
111
+ last_year: "Last year",
112
+ },
113
+ table: "Data table",
114
+ filterResults: (shown, total) => `${shown} of ${total} rows`,
115
+ sortedAscending: (column) => `Sorted by ${column}, ascending`,
116
+ sortedDescending: (column) => `Sorted by ${column}, descending`,
117
+ sortCleared: (column) => `Sort cleared on ${column}`,
118
+ pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,
119
+ // The separators are the ones that shipped, so adding the seam changes no screen.
120
+ pageRange: (from, to, total) => `${from}–${to} / ${total}`,
121
+ rowCount: (total) => `${total}`,
122
+ columnsCount: (visible, total) => `Columns (${visible}/${total})`,
123
+ };
124
+
125
+ /**
126
+ * Merge caller overrides onto the English defaults (presets merged too).
127
+ *
128
+ * The parameter is `Partial` on purpose — a table with no labels at all still
129
+ * renders — but that is also how a translated app ends up half English and never
130
+ * hears about it: lenkbank ships nineteen of these keys in English in a German UI
131
+ * today, because every one of them is backfilled silently. The kit will not start
132
+ * throwing over it (three apps depend on the lenient merge), so
133
+ * {@link missingDataTableLabels} is the other half: it answers which keys fell back,
134
+ * and an app asserts on that in its OWN test, where the answer is actionable.
135
+ */
136
+ export function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels {
137
+ if (!partial) return DEFAULT_DATA_TABLE_LABELS;
138
+ const merged: DataTableLabels = {
139
+ ...DEFAULT_DATA_TABLE_LABELS,
140
+ ...partial,
141
+ presets: { ...DEFAULT_DATA_TABLE_LABELS.presets, ...(partial.presets ?? {}) },
142
+ };
143
+ // `columnsCount` was split out of `columns` + a template literal. Consumers have
144
+ // been translating `columns` since 0.1, and backfilling the English default over
145
+ // that would hand them "Columns (4/9)" back for having translated the key that
146
+ // existed at the time — the same silent-English failure the audit records against
147
+ // this file. So the count is derived from the RESOLVED `columns` unless the caller
148
+ // states the whole sentence, which is the escape hatch for a language where the
149
+ // parenthetical does not fit.
150
+ if (partial.columns !== undefined && partial.columnsCount === undefined) {
151
+ merged.columnsCount = (visible, total) => `${merged.columns} (${visible}/${total})`;
152
+ }
153
+ return merged;
154
+ }
155
+
156
+ /**
157
+ * The label keys `labels` does not supply, and which therefore come back English
158
+ * from {@link resolveDataTableLabels}.
159
+ *
160
+ * For a consumer's own test:
161
+ *
162
+ * ```ts
163
+ * expect(missingDataTableLabels(de.dataTable)).toEqual([]);
164
+ * ```
165
+ *
166
+ * which fails the day the kit adds a key — which is the point. Adding a label is
167
+ * additive here and a silent regression in a translated app, and nothing else in
168
+ * either repository can tell the two apart.
169
+ *
170
+ * Preset labels are reported one at a time (`presets.yesterday`), because `presets`
171
+ * is a `Record` merged key by key: answering `"presets"` would say nothing about
172
+ * which of the eleven are still English.
173
+ */
174
+ export function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[] {
175
+ const missing: string[] = [];
176
+ for (const key of Object.keys(DEFAULT_DATA_TABLE_LABELS) as Array<keyof DataTableLabels>) {
177
+ if (key === "presets") continue;
178
+ if (labels?.[key] !== undefined) continue;
179
+ // `columnsCount` is DERIVED from a translated `columns` by the resolver above,
180
+ // so a caller who set that one is not leaking English here. Reporting it would
181
+ // send them to translate a key they have already covered, and an `toEqual([])`
182
+ // assertion built on this would never go green.
183
+ if (key === "columnsCount" && labels?.columns !== undefined) continue;
184
+ missing.push(key);
185
+ }
186
+ for (const preset of Object.keys(DEFAULT_DATA_TABLE_LABELS.presets)) {
187
+ if (labels?.presets?.[preset] === undefined) missing.push(`presets.${preset}`);
188
+ }
189
+ return missing;
190
+ }
@@ -0,0 +1,126 @@
1
+ import { useMemo } from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+ import { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from "./data-table-labels";
5
+ import { useKitLocale } from "../i18n/kit-labels";
6
+
7
+ export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 200] as const;
8
+
9
+ interface PaginationProps {
10
+ page: number;
11
+ totalPages: number;
12
+ pageSize: number;
13
+ total: number;
14
+ onPage: (p: number) => void;
15
+ onPageSize: (n: number) => void;
16
+ labels?: DataTableLabels;
17
+ /** For the page numbers and page-size options; falls back to the provider's. */
18
+ locale?: string;
19
+ }
20
+
21
+ /** Footer for {@link DataTable}: range summary, page-size select, and a
22
+ * windowed page-number strip (first/last + ±2 around the current page). */
23
+ export function Pagination({
24
+ page,
25
+ totalPages,
26
+ pageSize,
27
+ total,
28
+ onPage,
29
+ onPageSize,
30
+ labels = DEFAULT_DATA_TABLE_LABELS,
31
+ locale: localeProp,
32
+ }: PaginationProps) {
33
+ const locale = useKitLocale(localeProp);
34
+ // The bare numbers on the strip and in the select are text too: `String(n)` is
35
+ // always ASCII digits, which is wrong for a locale that writes its own. The range
36
+ // summary is not formatted here — `pageRange` / `rowCount` take NUMBERS so the
37
+ // translation can format them itself, alongside the words around them.
38
+ const num = useMemo(() => new Intl.NumberFormat(locale), [locale]);
39
+ const visible = useMemo(() => {
40
+ if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i);
41
+ const set = new Set<number>([0, totalPages - 1, page]);
42
+ for (let d = 1; d <= 2; d++) {
43
+ if (page - d >= 0) set.add(page - d);
44
+ if (page + d <= totalPages - 1) set.add(page + d);
45
+ }
46
+ return Array.from(set).sort((a, b) => a - b);
47
+ }, [page, totalPages]);
48
+
49
+ const items: (number | "ellipsis")[] = [];
50
+ visible.forEach((p, idx) => {
51
+ if (idx > 0 && p - visible[idx - 1] > 1) items.push("ellipsis");
52
+ items.push(p);
53
+ });
54
+
55
+ return (
56
+ <div className="flex flex-wrap items-center justify-between gap-2 border-t border-[var(--border)] px-3 py-2 text-xs text-[var(--text-secondary)]">
57
+ <div className="flex items-center gap-2">
58
+ <span>
59
+ {pageSize === Infinity
60
+ ? labels.rowCount(total)
61
+ : labels.pageRange(page * pageSize + 1, Math.min(total, (page + 1) * pageSize), total)}
62
+ </span>
63
+ <select
64
+ value={pageSize === Infinity ? "all" : pageSize}
65
+ onChange={(e) => onPageSize(e.target.value === "all" ? Infinity : Number(e.target.value))}
66
+ className="rounded border border-[var(--border)] bg-[var(--bg-surface)] px-1 py-0.5 text-xs"
67
+ aria-label={labels.pageSize}
68
+ >
69
+ {PAGE_SIZE_OPTIONS.map((n) => (
70
+ <option key={n} value={n}>
71
+ {num.format(n)}
72
+ </option>
73
+ ))}
74
+ <option value="all">{labels.pageSizeAll}</option>
75
+ </select>
76
+ </div>
77
+ {pageSize !== Infinity && totalPages > 1 && (
78
+ <div className="flex items-center gap-1">
79
+ <button
80
+ type="button"
81
+ onClick={() => onPage(Math.max(0, page - 1))}
82
+ disabled={page === 0}
83
+ className="rounded p-1 hover:bg-[var(--bg-hover)] disabled:opacity-40"
84
+ aria-label={labels.prevPage}
85
+ >
86
+ <ChevronLeft className="size-4" />
87
+ </button>
88
+ {items.map((it, i) =>
89
+ it === "ellipsis" ? (
90
+ <span key={`e${i}`} className="px-1 text-[var(--text-placeholder)]">
91
+ …
92
+ </span>
93
+ ) : (
94
+ <button
95
+ key={it}
96
+ type="button"
97
+ onClick={() => onPage(it)}
98
+ // Which page you are on is carried only by a background colour
99
+ // otherwise, and the strip is a row of bare numbers with nothing
100
+ // else to tell them apart.
101
+ aria-current={it === page ? "page" : undefined}
102
+ className={cn(
103
+ "min-w-7 rounded px-2 py-0.5",
104
+ it === page
105
+ ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)]"
106
+ : "hover:bg-[var(--bg-hover)]",
107
+ )}
108
+ >
109
+ {num.format(it + 1)}
110
+ </button>
111
+ ),
112
+ )}
113
+ <button
114
+ type="button"
115
+ onClick={() => onPage(Math.min(totalPages - 1, page + 1))}
116
+ disabled={page >= totalPages - 1}
117
+ className="rounded p-1 hover:bg-[var(--bg-hover)] disabled:opacity-40"
118
+ aria-label={labels.nextPage}
119
+ >
120
+ <ChevronRight className="size-4" />
121
+ </button>
122
+ </div>
123
+ )}
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,70 @@
1
+ /** Multi-column sort state for DataTable: ordered by priority (index 0 is the
2
+ * primary sort). Encoded as `sort=date.desc,amount` in URLs and the server's
3
+ * transactions `sort` param — the pre-multi-sort single-value format
4
+ * (`sort=date.desc`) decodes as a one-element list. */
5
+
6
+ export type SortDir = "asc" | "desc";
7
+ export interface SortState {
8
+ key: string;
9
+ dir: SortDir;
10
+ }
11
+
12
+ /** Coerce any persisted/URL-derived shape into a `SortState[]`. The
13
+ * pre-multi-sort localStorage format stored one `{key, dir}` object (or
14
+ * null); arrays pass through with junk entries dropped. */
15
+ export function normalizeSorts(raw: unknown): SortState[] {
16
+ const one = (v: unknown): SortState | null => {
17
+ if (!v || typeof v !== "object") return null;
18
+ const { key, dir } = v as { key?: unknown; dir?: unknown };
19
+ if (typeof key !== "string" || !key) return null;
20
+ return { key, dir: dir === "desc" ? "desc" : "asc" };
21
+ };
22
+ if (Array.isArray(raw)) {
23
+ const out: SortState[] = [];
24
+ for (const entry of raw) {
25
+ const s = one(entry);
26
+ if (s && !out.some((e) => e.key === s.key)) out.push(s);
27
+ }
28
+ return out;
29
+ }
30
+ const single = one(raw);
31
+ return single ? [single] : [];
32
+ }
33
+
34
+ export function encodeSorts(sorts: SortState[]): string | null {
35
+ if (!sorts.length) return null;
36
+ return sorts.map((s) => (s.dir === "desc" ? `${s.key}.desc` : s.key)).join(",");
37
+ }
38
+
39
+ /** Decode a `sort` URL param, dropping unknown and duplicated keys. */
40
+ export function decodeSorts(raw: string | null, validKeys: ReadonlySet<string>): SortState[] {
41
+ if (!raw) return [];
42
+ const out: SortState[] = [];
43
+ for (const part of raw.split(",")) {
44
+ const desc = part.endsWith(".desc");
45
+ const asc = part.endsWith(".asc");
46
+ const key = desc ? part.slice(0, -".desc".length) : asc ? part.slice(0, -".asc".length) : part;
47
+ if (!validKeys.has(key) || out.some((s) => s.key === key)) continue;
48
+ out.push({ key, dir: desc ? "desc" : "asc" });
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /** Header-click semantics. Plain click: make `key` the only sort — cycling
54
+ * asc → desc → none when it already is the sole criterion. Shift-click
55
+ * (`additive`): append `key` as the next-priority sort, or cycle an existing
56
+ * entry's direction in place (asc → desc → removed). */
57
+ export function nextSorts(prev: SortState[], key: string, additive: boolean): SortState[] {
58
+ const idx = prev.findIndex((s) => s.key === key);
59
+ if (!additive) {
60
+ if (prev.length === 1 && idx === 0) {
61
+ return prev[0].dir === "asc" ? [{ key, dir: "desc" }] : [];
62
+ }
63
+ return [{ key, dir: "asc" }];
64
+ }
65
+ if (idx === -1) return [...prev, { key, dir: "asc" }];
66
+ if (prev[idx].dir === "asc") {
67
+ return prev.map((s, i) => (i === idx ? { key, dir: "desc" as SortDir } : s));
68
+ }
69
+ return prev.filter((_, i) => i !== idx);
70
+ }