@eifi1/ui-kit 0.11.0 → 0.12.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 (342) hide show
  1. package/README.md +57 -32
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +24 -4
  12. package/dist/components/amount-input.js +5 -2
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -0,0 +1,63 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { DataTableLabels } from './data-table-labels.js';
4
+ import { b as DataTableColumn } from '../data-table-Drun8Fk9.js';
5
+ import './data-table-sort.js';
6
+ import './swipeable-row.js';
7
+
8
+ interface BooleanMarkProps {
9
+ /** `true` → a check, `false` → a cross (or a dash, see `falseAs`), `null` /
10
+ * `undefined` → a dash. */
11
+ value: boolean | null | undefined;
12
+ /**
13
+ * How `false` is drawn. `"cross"` (default) when "no" is news worth seeing — a
14
+ * failed check, a missing document. `"dash"` for a column that is mostly `false` and
15
+ * reads as a list of the `true` ones (kastlan's BoolCell): a column of crosses is
16
+ * noise. The screen-reader text says "No" either way.
17
+ */
18
+ falseAs?: "cross" | "dash";
19
+ /** The spoken words, over `dataTable.booleanTrue` / `booleanFalse` / `booleanUnset`
20
+ * from `<UiKitProvider labels>`. */
21
+ labels?: Partial<Pick<DataTableLabels, "booleanTrue" | "booleanFalse" | "booleanUnset">>;
22
+ className?: string;
23
+ }
24
+ /**
25
+ * A yes/no value in a table cell: a green check, a muted cross, or a faint dash for
26
+ * "not set" — with the word for a screen reader, since a glyph alone is announced as
27
+ * nothing at all (or as "image").
28
+ *
29
+ * kastlan's `BoolCell` (shared/components/data-table/bool-cell.tsx) drew the icons with
30
+ * no text, so a reader walking the table heard an empty cell for every row; the apps'
31
+ * Yes/No chips were the other answer, and too loud for a column of them.
32
+ */
33
+ declare function BooleanMark({ value, falseAs, labels: labelsProp, className }: BooleanMarkProps): react.JSX.Element;
34
+ interface BooleanColumnOptions<T> {
35
+ key: string;
36
+ header: ReactNode;
37
+ /** The value of this row. */
38
+ value: (row: T) => boolean | null | undefined;
39
+ /** See {@link BooleanMarkProps.falseAs}. */
40
+ falseAs?: "cross" | "dash";
41
+ /** Sortable, `true` first on the first click (a column is sorted to find the yes
42
+ * rows). Default true. */
43
+ sortable?: boolean;
44
+ /**
45
+ * A select filter over the column, with these option labels — pass them translated.
46
+ * Omitted: no filter. The option values are `"true"` / `"false"` (and `"unset"` when
47
+ * `unset` is given), which is also what `filterHref` takes for this column.
48
+ */
49
+ filterOptions?: {
50
+ true: string;
51
+ false: string;
52
+ unset?: string;
53
+ };
54
+ /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */
55
+ column?: Partial<DataTableColumn<T>>;
56
+ }
57
+ /**
58
+ * A {@link DataTableColumn} for a yes/no field: {@link BooleanMark} cells,
59
+ * sortable with `true` first, and optionally a Yes/No select filter.
60
+ */
61
+ declare function booleanColumn<T>({ key, header, value, falseAs, sortable, filterOptions, column, }: BooleanColumnOptions<T>): DataTableColumn<T>;
62
+
63
+ export { type BooleanColumnOptions, BooleanMark, type BooleanMarkProps, booleanColumn };
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Check, Minus, X } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useKitLabels } from "../i18n/kit-labels.js";
6
+ import { DEFAULT_DATA_TABLE_LABELS } from "./data-table-labels.js";
7
+ function BooleanMark({ value, falseAs = "cross", labels: labelsProp, className }) {
8
+ const labels = useKitLabels("dataTable", DEFAULT_DATA_TABLE_LABELS, labelsProp);
9
+ const state = value === true ? "true" : value === false ? "false" : "unset";
10
+ const text = state === "true" ? labels.booleanTrue : state === "false" ? labels.booleanFalse : labels.booleanUnset;
11
+ const Icon = state === "true" ? Check : state === "false" && falseAs === "cross" ? X : Minus;
12
+ return /* @__PURE__ */ jsxs("span", { "data-value": state, className: cn("inline-flex items-center align-middle", className), children: [
13
+ /* @__PURE__ */ jsx(
14
+ Icon,
15
+ {
16
+ "aria-hidden": true,
17
+ className: cn(
18
+ "size-4",
19
+ state === "true" ? "text-[var(--success)]" : state === "false" && falseAs === "cross" ? "text-[var(--text-muted)]" : "text-[var(--text-placeholder)]"
20
+ )
21
+ }
22
+ ),
23
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: text })
24
+ ] });
25
+ }
26
+ function booleanColumn({
27
+ key,
28
+ header,
29
+ value,
30
+ falseAs,
31
+ sortable = true,
32
+ filterOptions,
33
+ column
34
+ }) {
35
+ const asKey = (row) => {
36
+ const v = value(row);
37
+ return v === true ? "true" : v === false ? "false" : "unset";
38
+ };
39
+ return {
40
+ key,
41
+ header,
42
+ cell: (row) => /* @__PURE__ */ jsx(BooleanMark, { value: value(row), falseAs }),
43
+ // 0 for yes, 1 for no, null for unset (which the table sorts last either way).
44
+ sortBy: sortable ? (row) => {
45
+ const v = value(row);
46
+ return v == null ? null : v ? 0 : 1;
47
+ } : void 0,
48
+ filter: filterOptions ? {
49
+ type: "select",
50
+ getValue: asKey,
51
+ options: [
52
+ { value: "true", label: filterOptions.true },
53
+ { value: "false", label: filterOptions.false },
54
+ ...filterOptions.unset !== void 0 ? [{ value: "unset", label: filterOptions.unset }] : []
55
+ ]
56
+ } : void 0,
57
+ ...column
58
+ };
59
+ }
60
+ export {
61
+ BooleanMark,
62
+ booleanColumn
63
+ };
64
+ //# sourceMappingURL=data-table-cells.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/data-table-cells.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check, Minus, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from \"./data-table-labels\";\nimport type { DataTableColumn } from \"./data-table\";\n\nexport interface BooleanMarkProps {\n /** `true` → a check, `false` → a cross (or a dash, see `falseAs`), `null` /\n * `undefined` → a dash. */\n value: boolean | null | undefined;\n /**\n * How `false` is drawn. `\"cross\"` (default) when \"no\" is news worth seeing — a\n * failed check, a missing document. `\"dash\"` for a column that is mostly `false` and\n * reads as a list of the `true` ones (kastlan's BoolCell): a column of crosses is\n * noise. The screen-reader text says \"No\" either way.\n */\n falseAs?: \"cross\" | \"dash\";\n /** The spoken words, over `dataTable.booleanTrue` / `booleanFalse` / `booleanUnset`\n * from `<UiKitProvider labels>`. */\n labels?: Partial<Pick<DataTableLabels, \"booleanTrue\" | \"booleanFalse\" | \"booleanUnset\">>;\n className?: string;\n}\n\n/**\n * A yes/no value in a table cell: a green check, a muted cross, or a faint dash for\n * \"not set\" — with the word for a screen reader, since a glyph alone is announced as\n * nothing at all (or as \"image\").\n *\n * kastlan's `BoolCell` (shared/components/data-table/bool-cell.tsx) drew the icons with\n * no text, so a reader walking the table heard an empty cell for every row; the apps'\n * Yes/No chips were the other answer, and too loud for a column of them.\n */\nexport function BooleanMark({ value, falseAs = \"cross\", labels: labelsProp, className }: BooleanMarkProps) {\n const labels = useKitLabels(\"dataTable\", DEFAULT_DATA_TABLE_LABELS, labelsProp);\n const state = value === true ? \"true\" : value === false ? \"false\" : \"unset\";\n const text =\n state === \"true\" ? labels.booleanTrue : state === \"false\" ? labels.booleanFalse : labels.booleanUnset;\n const Icon = state === \"true\" ? Check : state === \"false\" && falseAs === \"cross\" ? X : Minus;\n return (\n <span data-value={state} className={cn(\"inline-flex items-center align-middle\", className)}>\n <Icon\n aria-hidden\n className={cn(\n \"size-4\",\n state === \"true\"\n ? \"text-[var(--success)]\"\n : state === \"false\" && falseAs === \"cross\"\n ? \"text-[var(--text-muted)]\"\n : \"text-[var(--text-placeholder)]\",\n )}\n />\n <span className=\"sr-only\">{text}</span>\n </span>\n );\n}\n\nexport interface BooleanColumnOptions<T> {\n key: string;\n header: ReactNode;\n /** The value of this row. */\n value: (row: T) => boolean | null | undefined;\n /** See {@link BooleanMarkProps.falseAs}. */\n falseAs?: \"cross\" | \"dash\";\n /** Sortable, `true` first on the first click (a column is sorted to find the yes\n * rows). Default true. */\n sortable?: boolean;\n /**\n * A select filter over the column, with these option labels — pass them translated.\n * Omitted: no filter. The option values are `\"true\"` / `\"false\"` (and `\"unset\"` when\n * `unset` is given), which is also what `filterHref` takes for this column.\n */\n filterOptions?: { true: string; false: string; unset?: string };\n /** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */\n column?: Partial<DataTableColumn<T>>;\n}\n\n/**\n * A {@link DataTableColumn} for a yes/no field: {@link BooleanMark} cells,\n * sortable with `true` first, and optionally a Yes/No select filter.\n */\nexport function booleanColumn<T>({\n key,\n header,\n value,\n falseAs,\n sortable = true,\n filterOptions,\n column,\n}: BooleanColumnOptions<T>): DataTableColumn<T> {\n const asKey = (row: T) => {\n const v = value(row);\n return v === true ? \"true\" : v === false ? \"false\" : \"unset\";\n };\n return {\n key,\n header,\n cell: (row) => <BooleanMark value={value(row)} falseAs={falseAs} />,\n // 0 for yes, 1 for no, null for unset (which the table sorts last either way).\n sortBy: sortable\n ? (row) => {\n const v = value(row);\n return v == null ? null : v ? 0 : 1;\n }\n : undefined,\n filter: filterOptions\n ? {\n type: \"select\",\n getValue: asKey,\n options: [\n { value: \"true\", label: filterOptions.true },\n { value: \"false\", label: filterOptions.false },\n ...(filterOptions.unset !== undefined ? [{ value: \"unset\", label: filterOptions.unset }] : []),\n ],\n }\n : undefined,\n ...column,\n };\n}\n"],"mappings":";AAwCI,SACE,KADF;AAvCJ,SAAS,OAAO,OAAO,SAAS;AAChC,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,iCAAuD;AA6BzD,SAAS,YAAY,EAAE,OAAO,UAAU,SAAS,QAAQ,YAAY,UAAU,GAAqB;AACzG,QAAM,SAAS,aAAa,aAAa,2BAA2B,UAAU;AAC9E,QAAM,QAAQ,UAAU,OAAO,SAAS,UAAU,QAAQ,UAAU;AACpE,QAAM,OACJ,UAAU,SAAS,OAAO,cAAc,UAAU,UAAU,OAAO,eAAe,OAAO;AAC3F,QAAM,OAAO,UAAU,SAAS,QAAQ,UAAU,WAAW,YAAY,UAAU,IAAI;AACvF,SACE,qBAAC,UAAK,cAAY,OAAO,WAAW,GAAG,yCAAyC,SAAS,GACvF;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,eAAW;AAAA,QACX,WAAW;AAAA,UACT;AAAA,UACA,UAAU,SACN,0BACA,UAAU,WAAW,YAAY,UAC/B,6BACA;AAAA,QACR;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,UAAK,WAAU,WAAW,gBAAK;AAAA,KAClC;AAEJ;AA0BO,SAAS,cAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AACF,GAAgD;AAC9C,QAAM,QAAQ,CAAC,QAAW;AACxB,UAAM,IAAI,MAAM,GAAG;AACnB,WAAO,MAAM,OAAO,SAAS,MAAM,QAAQ,UAAU;AAAA,EACvD;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,MAAM,CAAC,QAAQ,oBAAC,eAAY,OAAO,MAAM,GAAG,GAAG,SAAkB;AAAA;AAAA,IAEjE,QAAQ,WACJ,CAAC,QAAQ;AACP,YAAM,IAAI,MAAM,GAAG;AACnB,aAAO,KAAK,OAAO,OAAO,IAAI,IAAI;AAAA,IACpC,IACA;AAAA,IACJ,QAAQ,gBACJ;AAAA,MACE,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS;AAAA,QACP,EAAE,OAAO,QAAQ,OAAO,cAAc,KAAK;AAAA,QAC3C,EAAE,OAAO,SAAS,OAAO,cAAc,MAAM;AAAA,QAC7C,GAAI,cAAc,UAAU,SAAY,CAAC,EAAE,OAAO,SAAS,OAAO,cAAc,MAAM,CAAC,IAAI,CAAC;AAAA,MAC9F;AAAA,IACF,IACA;AAAA,IACJ,GAAG;AAAA,EACL;AACF;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { b as DataTableColumn, e as FilterValue } from '../data-table-filters-Dh9uF_S-.js';
2
+ import { b as DataTableColumn, f as FilterValue } from '../data-table-Drun8Fk9.js';
3
3
  import { DataTableLabels } from './data-table-labels.js';
4
4
  import './data-table-sort.js';
5
5
  import './swipeable-row.js';
@@ -1,4 +1,4 @@
1
- export { C as ColumnFilter, e as FilterValue, f as decodeFilterValue, g as decodeFilterValueOfType, h as defaultFilterState, i as encodeFilterValue, j as isFilterActive, r as resolveFilter, k as rowMatches } from '../data-table-filters-Dh9uF_S-.js';
1
+ export { C as ColumnFilter, f as FilterValue, j as dateFilter, k as decodeFilterValue, l as decodeFilterValueOfType, m as defaultFilterState, n as encodeFilterValue, o as filterHref, p as isFilterActive, q as numberFilter, s as resolveFilter, t as rowMatches, u as selectFilter, v as textFilter } from '../data-table-Drun8Fk9.js';
2
2
  import 'react';
3
3
  import './data-table-sort.js';
4
4
  import './swipeable-row.js';
@@ -107,13 +107,43 @@ function decodeFilterValueOfType(type, raw) {
107
107
  function decodeFilterValue(filter, raw) {
108
108
  return decodeFilterValueOfType(filter.type, raw);
109
109
  }
110
+ function textFilter(getValue) {
111
+ return { type: "text", getValue };
112
+ }
113
+ function selectFilter(getValue, options) {
114
+ const opts = Array.isArray(options) ? options : options ? Object.entries(options).map(([value, label]) => ({ value, label })) : void 0;
115
+ return { type: "select", getValue, options: opts };
116
+ }
117
+ function dateFilter(getValue) {
118
+ return { type: "date", getValue };
119
+ }
120
+ function numberFilter(getValue) {
121
+ return { type: "number", getValue };
122
+ }
123
+ function filterHref(path, column, value, options) {
124
+ const state = typeof value === "string" ? { type: "text", q: value } : Array.isArray(value) ? { type: "select", values: value } : value;
125
+ const encoded = encodeFilterValue(state);
126
+ if (encoded == null) return path;
127
+ const hashAt = path.indexOf("#");
128
+ const base = hashAt >= 0 ? path.slice(0, hashAt) : path;
129
+ const hash = hashAt >= 0 ? path.slice(hashAt) : "";
130
+ const queryAt = base.indexOf("?");
131
+ const params = new URLSearchParams(queryAt >= 0 ? base.slice(queryAt + 1) : "");
132
+ params.set(`${options?.prefix ?? ""}f.${column}`, encoded);
133
+ return `${queryAt >= 0 ? base.slice(0, queryAt) : base}?${params.toString()}${hash}`;
134
+ }
110
135
  export {
136
+ dateFilter,
111
137
  decodeFilterValue,
112
138
  decodeFilterValueOfType,
113
139
  defaultFilterState,
114
140
  encodeFilterValue,
141
+ filterHref,
115
142
  isFilterActive,
143
+ numberFilter,
116
144
  resolveFilter,
117
- rowMatches
145
+ rowMatches,
146
+ selectFilter,
147
+ textFilter
118
148
  };
119
149
  //# sourceMappingURL=data-table-filters.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-filters.ts"],"sourcesContent":["import type { DataTableColumn } from \"./data-table\";\n\n/** How a column is filtered. `text`/`select`/`date`/`number` each read a value\n * off the row and compare it against the user's {@link FilterValue}. */\nexport type ColumnFilter<T> =\n | { type: \"text\"; getValue: (row: T) => string }\n | { type: \"select\"; getValue: (row: T) => string; options?: { value: string; label?: string }[] }\n | { type: \"date\"; getValue: (row: T) => string | null | undefined }\n | { type: \"number\"; getValue: (row: T) => number | null | undefined };\n\n/** The current state of a column's filter (what the user has entered). */\nexport type FilterValue =\n | { type: \"text\"; q: string }\n | { type: \"select\"; values: string[] }\n | { type: \"date\"; from: string; to: string }\n | { type: \"number\"; min: string; max: string; abs: boolean };\n\n/** A column's filter config, falling back to a plain text filter over `filterBy`. */\nexport function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null {\n if (col.filter) return col.filter;\n if (col.filterBy) return { type: \"text\", getValue: col.filterBy };\n return null;\n}\n\n/** Whether a filter value is actually narrowing anything (vs its empty default). */\nexport function isFilterActive(v: FilterValue | undefined): boolean {\n if (!v) return false;\n switch (v.type) {\n case \"text\":\n return v.q.trim().length > 0;\n case \"select\":\n return v.values.length > 0;\n case \"date\":\n return !!v.from || !!v.to;\n case \"number\":\n return v.min !== \"\" || v.max !== \"\" || v.abs;\n }\n}\n\n/**\n * Whether `row` passes column `col`'s filter given the user's `state`.\n *\n * `locale` is for the text filter's case folding. Plain `toLowerCase` folds by the\n * root rules, under which a Turkish user typing \"istanbul\" does not find \"İSTANBUL\";\n * `toLocaleLowerCase(locale)` does. Optional, and `undefined` is the runtime's default.\n */\nexport function rowMatches<T>(\n col: DataTableColumn<T>,\n row: T,\n state: FilterValue,\n locale?: string,\n): boolean {\n const filter = resolveFilter(col);\n if (!filter) return true;\n switch (filter.type) {\n case \"text\": {\n if (state.type !== \"text\" || !state.q.trim()) return true;\n return filter\n .getValue(row)\n .toLocaleLowerCase(locale)\n .includes(state.q.trim().toLocaleLowerCase(locale));\n }\n case \"select\": {\n if (state.type !== \"select\" || state.values.length === 0) return true;\n return state.values.includes(filter.getValue(row));\n }\n case \"date\": {\n if (state.type !== \"date\") return true;\n const raw = filter.getValue(row);\n if (!raw) return !state.from && !state.to;\n const v = raw.slice(0, 10);\n if (state.from && v < state.from) return false;\n if (state.to && v > state.to) return false;\n return true;\n }\n case \"number\": {\n if (state.type !== \"number\") return true;\n const raw = filter.getValue(row);\n if (raw == null || Number.isNaN(raw)) return !state.min && !state.max;\n const x = state.abs ? Math.abs(raw) : raw;\n if (state.min !== \"\" && x < Number(state.min)) return false;\n if (state.max !== \"\" && x > Number(state.max)) return false;\n return true;\n }\n }\n}\n\n/** The empty filter state for a column's filter type. */\nexport function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue {\n switch (filter.type) {\n case \"text\":\n return { type: \"text\", q: \"\" };\n case \"select\":\n return { type: \"select\", values: [] };\n case \"date\":\n return { type: \"date\", from: \"\", to: \"\" };\n case \"number\":\n return { type: \"number\", min: \"\", max: \"\", abs: false };\n }\n}\n\n/**\n * Escape the ONE character the select codec uses as its separator, plus the escape\n * marker itself so the transform is reversible.\n *\n * A minimal escape rather than `encodeURIComponent`, deliberately: these strings sit\n * inside a query-parameter value that `URLSearchParams` already encodes, so a full\n * percent-encode would double-encode every space into `%2520` and make shared links\n * unreadable for no gain. Only `,` can be misread here.\n *\n * Backwards compatible with every link written before this existed: a value with\n * neither `,` nor `%` passes through both directions untouched, and `%` on its own\n * never matched `%25` either.\n */\nfunction escapeSelectValue(v: string): string {\n return v.replace(/%/g, \"%25\").replace(/,/g, \"%2C\");\n}\n\n/** Inverse of {@link escapeSelectValue}. The order matters and is the reverse of the\n * escape: undoing `%2C` first cannot see the `2C` inside an escaped `%252C`. */\nfunction unescapeSelectValue(v: string): string {\n return v.replace(/%2C/g, \",\").replace(/%25/g, \"%\");\n}\n\n/** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */\nexport function encodeFilterValue(v: FilterValue): string | null {\n switch (v.type) {\n case \"text\":\n return v.q.trim() ? v.q : null;\n case \"select\":\n // Escaped before joining: an option value may legitimately contain the\n // separator (a free-text title, a payee or category name), and without this it\n // came back as two values and the filter silently matched nothing.\n return v.values.length ? v.values.map(escapeSelectValue).join(\",\") : null;\n case \"date\":\n return v.from || v.to ? `${v.from}..${v.to}` : null;\n case \"number\": {\n const hasRange = v.min !== \"\" || v.max !== \"\";\n if (!hasRange && !v.abs) return null;\n return `${v.min}..${v.max}${v.abs ? \"!\" : \"\"}`;\n }\n }\n}\n\n/** Inverse of {@link encodeFilterValue} given just the filter type — for\n * callers that know a column's type without holding its full config. */\nexport function decodeFilterValueOfType(type: FilterValue[\"type\"], raw: string): FilterValue {\n switch (type) {\n case \"text\":\n return { type: \"text\", q: raw };\n case \"select\":\n return {\n type: \"select\",\n values: raw\n .split(\",\")\n .filter((v) => v.length > 0)\n .map(unescapeSelectValue),\n };\n case \"date\": {\n const [from = \"\", to = \"\"] = raw.split(\"..\");\n return { type: \"date\", from, to };\n }\n case \"number\": {\n let body = raw;\n const abs = body.endsWith(\"!\");\n if (abs) body = body.slice(0, -1);\n const [min = \"\", max = \"\"] = body.split(\"..\");\n return { type: \"number\", min, max, abs };\n }\n }\n}\n\n/** Inverse of {@link encodeFilterValue}, typed by the column's filter config. */\nexport function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue {\n return decodeFilterValueOfType(filter.type, raw);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-filters.ts"],"sourcesContent":["import type { DataTableColumn } from \"./data-table\";\n\n/** How a column is filtered. `text`/`select`/`date`/`number` each read a value\n * off the row and compare it against the user's {@link FilterValue}. */\nexport type ColumnFilter<T> =\n | { type: \"text\"; getValue: (row: T) => string }\n | { type: \"select\"; getValue: (row: T) => string; options?: { value: string; label?: string }[] }\n | { type: \"date\"; getValue: (row: T) => string | null | undefined }\n | { type: \"number\"; getValue: (row: T) => number | null | undefined };\n\n/** The current state of a column's filter (what the user has entered). */\nexport type FilterValue =\n | { type: \"text\"; q: string }\n | { type: \"select\"; values: string[] }\n | { type: \"date\"; from: string; to: string }\n | { type: \"number\"; min: string; max: string; abs: boolean };\n\n/** A column's filter config, falling back to a plain text filter over `filterBy`. */\nexport function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null {\n if (col.filter) return col.filter;\n if (col.filterBy) return { type: \"text\", getValue: col.filterBy };\n return null;\n}\n\n/** Whether a filter value is actually narrowing anything (vs its empty default). */\nexport function isFilterActive(v: FilterValue | undefined): boolean {\n if (!v) return false;\n switch (v.type) {\n case \"text\":\n return v.q.trim().length > 0;\n case \"select\":\n return v.values.length > 0;\n case \"date\":\n return !!v.from || !!v.to;\n case \"number\":\n return v.min !== \"\" || v.max !== \"\" || v.abs;\n }\n}\n\n/**\n * Whether `row` passes column `col`'s filter given the user's `state`.\n *\n * `locale` is for the text filter's case folding. Plain `toLowerCase` folds by the\n * root rules, under which a Turkish user typing \"istanbul\" does not find \"İSTANBUL\";\n * `toLocaleLowerCase(locale)` does. Optional, and `undefined` is the runtime's default.\n */\nexport function rowMatches<T>(\n col: DataTableColumn<T>,\n row: T,\n state: FilterValue,\n locale?: string,\n): boolean {\n const filter = resolveFilter(col);\n if (!filter) return true;\n switch (filter.type) {\n case \"text\": {\n if (state.type !== \"text\" || !state.q.trim()) return true;\n return filter\n .getValue(row)\n .toLocaleLowerCase(locale)\n .includes(state.q.trim().toLocaleLowerCase(locale));\n }\n case \"select\": {\n if (state.type !== \"select\" || state.values.length === 0) return true;\n return state.values.includes(filter.getValue(row));\n }\n case \"date\": {\n if (state.type !== \"date\") return true;\n const raw = filter.getValue(row);\n if (!raw) return !state.from && !state.to;\n const v = raw.slice(0, 10);\n if (state.from && v < state.from) return false;\n if (state.to && v > state.to) return false;\n return true;\n }\n case \"number\": {\n if (state.type !== \"number\") return true;\n const raw = filter.getValue(row);\n if (raw == null || Number.isNaN(raw)) return !state.min && !state.max;\n const x = state.abs ? Math.abs(raw) : raw;\n if (state.min !== \"\" && x < Number(state.min)) return false;\n if (state.max !== \"\" && x > Number(state.max)) return false;\n return true;\n }\n }\n}\n\n/** The empty filter state for a column's filter type. */\nexport function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue {\n switch (filter.type) {\n case \"text\":\n return { type: \"text\", q: \"\" };\n case \"select\":\n return { type: \"select\", values: [] };\n case \"date\":\n return { type: \"date\", from: \"\", to: \"\" };\n case \"number\":\n return { type: \"number\", min: \"\", max: \"\", abs: false };\n }\n}\n\n/**\n * Escape the ONE character the select codec uses as its separator, plus the escape\n * marker itself so the transform is reversible.\n *\n * A minimal escape rather than `encodeURIComponent`, deliberately: these strings sit\n * inside a query-parameter value that `URLSearchParams` already encodes, so a full\n * percent-encode would double-encode every space into `%2520` and make shared links\n * unreadable for no gain. Only `,` can be misread here.\n *\n * Backwards compatible with every link written before this existed: a value with\n * neither `,` nor `%` passes through both directions untouched, and `%` on its own\n * never matched `%25` either.\n */\nfunction escapeSelectValue(v: string): string {\n return v.replace(/%/g, \"%25\").replace(/,/g, \"%2C\");\n}\n\n/** Inverse of {@link escapeSelectValue}. The order matters and is the reverse of the\n * escape: undoing `%2C` first cannot see the `2C` inside an escaped `%252C`. */\nfunction unescapeSelectValue(v: string): string {\n return v.replace(/%2C/g, \",\").replace(/%25/g, \"%\");\n}\n\n/** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */\nexport function encodeFilterValue(v: FilterValue): string | null {\n switch (v.type) {\n case \"text\":\n return v.q.trim() ? v.q : null;\n case \"select\":\n // Escaped before joining: an option value may legitimately contain the\n // separator (a free-text title, a payee or category name), and without this it\n // came back as two values and the filter silently matched nothing.\n return v.values.length ? v.values.map(escapeSelectValue).join(\",\") : null;\n case \"date\":\n return v.from || v.to ? `${v.from}..${v.to}` : null;\n case \"number\": {\n const hasRange = v.min !== \"\" || v.max !== \"\";\n if (!hasRange && !v.abs) return null;\n return `${v.min}..${v.max}${v.abs ? \"!\" : \"\"}`;\n }\n }\n}\n\n/** Inverse of {@link encodeFilterValue} given just the filter type — for\n * callers that know a column's type without holding its full config. */\nexport function decodeFilterValueOfType(type: FilterValue[\"type\"], raw: string): FilterValue {\n switch (type) {\n case \"text\":\n return { type: \"text\", q: raw };\n case \"select\":\n return {\n type: \"select\",\n values: raw\n .split(\",\")\n .filter((v) => v.length > 0)\n .map(unescapeSelectValue),\n };\n case \"date\": {\n const [from = \"\", to = \"\"] = raw.split(\"..\");\n return { type: \"date\", from, to };\n }\n case \"number\": {\n let body = raw;\n const abs = body.endsWith(\"!\");\n if (abs) body = body.slice(0, -1);\n const [min = \"\", max = \"\"] = body.split(\"..\");\n return { type: \"number\", min, max, abs };\n }\n }\n}\n\n/** Inverse of {@link encodeFilterValue}, typed by the column's filter config. */\nexport function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue {\n return decodeFilterValueOfType(filter.type, raw);\n}\n\n// ---------- Filter builders ----------\n//\n// Terse builders for `DataTableColumn.filter`, lifted from kastlan's column-filters.ts,\n// which every converted list page imports. The one that earns its keep is `selectFilter`:\n// in `serverPagination` mode a select filter MUST declare its options (the table would\n// otherwise derive them from the rows of the page on screen), and the options an app has\n// to hand are usually a translated `{ value: label }` map.\n\n/** A free-text filter over `getValue`. */\nexport function textFilter<T>(getValue: (row: T) => string): ColumnFilter<T> {\n return { type: \"text\", getValue };\n}\n\n/**\n * A multi-select filter. `options` is an options array, or a `{ value: label }` record\n * (a translated status map) — kept in its key order. Left out, the table derives the\n * options from the rows it holds.\n */\nexport function selectFilter<T>(\n getValue: (row: T) => string,\n options?: { value: string; label?: string }[] | Record<string, string>,\n): ColumnFilter<T> {\n const opts = Array.isArray(options)\n ? options\n : options\n ? Object.entries(options).map(([value, label]) => ({ value, label }))\n : undefined;\n return { type: \"select\", getValue, options: opts };\n}\n\n/** A from/to date filter over an ISO date (or date-time) string. */\nexport function dateFilter<T>(getValue: (row: T) => string | null | undefined): ColumnFilter<T> {\n return { type: \"date\", getValue };\n}\n\n/** A min/max (and absolute-value) filter over a number. */\nexport function numberFilter<T>(getValue: (row: T) => number | null | undefined): ColumnFilter<T> {\n return { type: \"number\", getValue };\n}\n\n/**\n * A link to a list page with one column filter already applied — in the table's own URL\n * scheme (`f.<column>=<encoded>`), which a table with `urlSync` (or\n * {@link useTableUrlState}) reads on mount. kastlan's `buildListUrl`, which had to\n * restate the `f.` prefix to do it.\n *\n * `value` is a string for a text filter, an array for a select filter, or any\n * {@link FilterValue}. A filter that narrows nothing returns `path` unchanged. A `path`\n * that already carries a query keeps it; a `#fragment` stays at the end.\n *\n * filterHref(\"/units\", \"status\", [\"vacant\"]) // \"/units?f.status=vacant\"\n */\nexport function filterHref(\n path: string,\n column: string,\n value: string | string[] | FilterValue,\n /** The target table's `urlSync` prefix, if it has one (`{ prefix: \"inv.\" }` →\n * `inv.f.<column>`). */\n options?: { prefix?: string },\n): string {\n const state: FilterValue =\n typeof value === \"string\"\n ? { type: \"text\", q: value }\n : Array.isArray(value)\n ? { type: \"select\", values: value }\n : value;\n const encoded = encodeFilterValue(state);\n if (encoded == null) return path;\n const hashAt = path.indexOf(\"#\");\n const base = hashAt >= 0 ? path.slice(0, hashAt) : path;\n const hash = hashAt >= 0 ? path.slice(hashAt) : \"\";\n const queryAt = base.indexOf(\"?\");\n const params = new URLSearchParams(queryAt >= 0 ? base.slice(queryAt + 1) : \"\");\n params.set(`${options?.prefix ?? \"\"}f.${column}`, encoded);\n return `${queryAt >= 0 ? base.slice(0, queryAt) : base}?${params.toString()}${hash}`;\n}\n"],"mappings":"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","names":[]}
@@ -47,6 +47,19 @@ interface DataTableLabels {
47
47
  rowCount: (total: number) => string;
48
48
  /** The column-settings rail and its heading: "Columns (4/9)". */
49
49
  columnsCount: (visible: number, total: number) => string;
50
+ /** The body of a table with no rows and no `empty` prop of its own. A sentence, not
51
+ * "—": a dash in the only row of a table reads as one row with a missing value. */
52
+ empty: string;
53
+ /** The accessible name of the `rowActions` column (its header is visually empty). */
54
+ actions: string;
55
+ /** The `kind: "edit"` row action's name — its tooltip and its swipe label. */
56
+ edit: string;
57
+ /** The `kind: "delete"` row action's name. */
58
+ delete: string;
59
+ /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */
60
+ booleanTrue: string;
61
+ booleanFalse: string;
62
+ booleanUnset: string;
50
63
  }
51
64
  declare const DEFAULT_DATA_TABLE_LABELS: DataTableLabels;
52
65
  /**
@@ -47,7 +47,14 @@ const DEFAULT_DATA_TABLE_LABELS = {
47
47
  // The separators are the ones that shipped, so adding the seam changes no screen.
48
48
  pageRange: (from, to, total) => `${from}\u2013${to} / ${total}`,
49
49
  rowCount: (total) => `${total}`,
50
- columnsCount: (visible, total) => `Columns (${visible}/${total})`
50
+ columnsCount: (visible, total) => `Columns (${visible}/${total})`,
51
+ empty: "No entries",
52
+ actions: "Actions",
53
+ edit: "Edit",
54
+ delete: "Delete",
55
+ booleanTrue: "Yes",
56
+ booleanFalse: "No",
57
+ booleanUnset: "Not set"
51
58
  };
52
59
  function resolveDataTableLabels(partial) {
53
60
  if (!partial) return DEFAULT_DATA_TABLE_LABELS;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The package\n// ships no translations; the English defaults below make the table usable with no\n// wiring.\n\nexport interface DataTableLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n autoSize: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n autoSize: \"Auto-size columns\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n};\n\n/**\n * Merge caller overrides onto the English defaults (presets merged too).\n *\n * The parameter is `Partial` on purpose — a table with no labels at all still\n * renders — but that is also how a translated app ends up half English and never\n * hears about it: lenkbank ships nineteen of these keys in English in a German UI\n * today, because every one of them is backfilled silently. The kit will not start\n * throwing over it (three apps depend on the lenient merge), so\n * {@link missingDataTableLabels} is the other half: it answers which keys fell back,\n * and an app asserts on that in its OWN test, where the answer is actionable.\n */\nexport function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels {\n if (!partial) return DEFAULT_DATA_TABLE_LABELS;\n const merged: DataTableLabels = {\n ...DEFAULT_DATA_TABLE_LABELS,\n ...partial,\n presets: { ...DEFAULT_DATA_TABLE_LABELS.presets, ...(partial.presets ?? {}) },\n };\n // `columnsCount` was split out of `columns` + a template literal. Consumers have\n // been translating `columns` since 0.1, and backfilling the English default over\n // that would hand them \"Columns (4/9)\" back for having translated the key that\n // existed at the time — the same silent-English failure the audit records against\n // this file. So the count is derived from the RESOLVED `columns` unless the caller\n // states the whole sentence, which is the escape hatch for a language where the\n // parenthetical does not fit.\n if (partial.columns !== undefined && partial.columnsCount === undefined) {\n merged.columnsCount = (visible, total) => `${merged.columns} (${visible}/${total})`;\n }\n return merged;\n}\n\n/**\n * The label keys `labels` does not supply, and which therefore come back English\n * from {@link resolveDataTableLabels}.\n *\n * For a consumer's own test:\n *\n * ```ts\n * expect(missingDataTableLabels(de.dataTable)).toEqual([]);\n * ```\n *\n * which fails the day the kit adds a key — which is the point. Adding a label is\n * additive here and a silent regression in a translated app, and nothing else in\n * either repository can tell the two apart.\n *\n * Preset labels are reported one at a time (`presets.yesterday`), because `presets`\n * is a `Record` merged key by key: answering `\"presets\"` would say nothing about\n * which of the eleven are still English.\n */\nexport function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[] {\n const missing: string[] = [];\n for (const key of Object.keys(DEFAULT_DATA_TABLE_LABELS) as Array<keyof DataTableLabels>) {\n if (key === \"presets\") continue;\n if (labels?.[key] !== undefined) continue;\n // `columnsCount` is DERIVED from a translated `columns` by the resolver above,\n // so a caller who set that one is not leaking English here. Reporting it would\n // send them to translate a key they have already covered, and an `toEqual([])`\n // assertion built on this would never go green.\n if (key === \"columnsCount\" && labels?.columns !== undefined) continue;\n missing.push(key);\n }\n for (const preset of Object.keys(DEFAULT_DATA_TABLE_LABELS.presets)) {\n if (labels?.presets?.[preset] === undefined) missing.push(`presets.${preset}`);\n }\n return missing;\n}\n"],"mappings":";AAyEO,MAAM,4BAA6C;AAAA,EACxD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,KAAK;AAAA,IACL,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,EACP,eAAe,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,KAAK;AAAA,EACrD,iBAAiB,CAAC,WAAW,aAAa,MAAM;AAAA,EAChD,kBAAkB,CAAC,WAAW,aAAa,MAAM;AAAA,EACjD,aAAa,CAAC,WAAW,mBAAmB,MAAM;AAAA,EAClD,aAAa,CAAC,MAAM,eAAe,QAAQ,IAAI,OAAO,UAAU;AAAA;AAAA,EAEhE,WAAW,CAAC,MAAM,IAAI,UAAU,GAAG,IAAI,SAAI,EAAE,MAAM,KAAK;AAAA,EACxD,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,cAAc,CAAC,SAAS,UAAU,YAAY,OAAO,IAAI,KAAK;AAChE;AAaO,SAAS,uBAAuB,SAAqD;AAC1F,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAA0B;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,0BAA0B,SAAS,GAAI,QAAQ,WAAW,CAAC,EAAG;AAAA,EAC9E;AAQA,MAAI,QAAQ,YAAY,UAAa,QAAQ,iBAAiB,QAAW;AACvE,WAAO,eAAe,CAAC,SAAS,UAAU,GAAG,OAAO,OAAO,KAAK,OAAO,IAAI,KAAK;AAAA,EAClF;AACA,SAAO;AACT;AAoBO,SAAS,uBAAuB,QAA6C;AAClF,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,OAAO,KAAK,yBAAyB,GAAmC;AACxF,QAAI,QAAQ,UAAW;AACvB,QAAI,SAAS,GAAG,MAAM,OAAW;AAKjC,QAAI,QAAQ,kBAAkB,QAAQ,YAAY,OAAW;AAC7D,YAAQ,KAAK,GAAG;AAAA,EAClB;AACA,aAAW,UAAU,OAAO,KAAK,0BAA0B,OAAO,GAAG;AACnE,QAAI,QAAQ,UAAU,MAAM,MAAM,OAAW,SAAQ,KAAK,WAAW,MAAM,EAAE;AAAA,EAC/E;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-labels.ts"],"sourcesContent":["// All user-facing strings for the DataTable system — the `dataTable` namespace of\n// `<UiKitProvider labels>`, and per table the `labels` prop over that. The package\n// ships no translations; the English defaults below make the table usable with no\n// wiring.\n\nexport interface DataTableLabels {\n // Table chrome\n columns: string;\n selectAllRows: string;\n sortHint: string;\n filter: string;\n close: string;\n selectRow: string;\n autoSize: string;\n loading: string;\n filters: string;\n clearAll: string;\n done: string;\n // Pagination\n pageSize: string;\n pageSizeAll: string;\n prevPage: string;\n nextPage: string;\n // Filter popover\n clearFilter: string;\n filterPlaceholder: string;\n selectFilter: string;\n selectAll: string;\n selectNone: string;\n dateFrom: string;\n dateTo: string;\n numberMin: string;\n numberMax: string;\n numberAbs: string;\n // Date-range preset labels, keyed by preset key (see dateRangePresets()).\n presets: Record<string, string>;\n\n /**\n * The table's own accessible name. Deliberately a label rather than a second,\n * parallel `tableLabel` prop: the audit already records \"three different prop\n * names for the accessible name\" as a finding, and a table that is not \"Data\n * table\" says so with `labels={{ table: \"Transactions\" }}` at the call site.\n */\n table: string;\n\n // ---- Announcements ----\n // Spoken through the live region, never rendered. Each takes its numbers as\n // ARGUMENTS: a count glued into an English template literal is untranslatable,\n // and the grammar around it moves with the number in most languages.\n\n /** After a filter changes: \"12 of 137 rows\". */\n filterResults: (shown: number, total: number) => string;\n /** After a header is activated: \"Sorted by Name, ascending\". */\n sortedAscending: (column: string) => string;\n sortedDescending: (column: string) => string;\n /** The third click, which removes the column from the sort entirely. */\n sortCleared: (column: string) => string;\n /** After paging: \"Page 3 of 14\". `page` is 1-BASED, as spoken. */\n pageChanged: (page: number, totalPages: number) => string;\n\n // ---- Counted chrome ----\n // Visible text that interpolates numbers. These were template literals until the\n // audit flagged them: a consumer could translate every key in this file and still\n // be left with English (and with ASCII digit grouping) in the footer.\n\n /** The pagination range summary: \"1–25 / 137\". */\n pageRange: (from: number, to: number, total: number) => string;\n /** The same summary when the page size is \"all\" — just the row count. */\n rowCount: (total: number) => string;\n /** The column-settings rail and its heading: \"Columns (4/9)\". */\n columnsCount: (visible: number, total: number) => string;\n\n // ---- Empty state, row actions, boolean cells (0.12.0) ----\n\n /** The body of a table with no rows and no `empty` prop of its own. A sentence, not\n * \"—\": a dash in the only row of a table reads as one row with a missing value. */\n empty: string;\n /** The accessible name of the `rowActions` column (its header is visually empty). */\n actions: string;\n /** The `kind: \"edit\"` row action's name — its tooltip and its swipe label. */\n edit: string;\n /** The `kind: \"delete\"` row action's name. */\n delete: string;\n /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */\n booleanTrue: string;\n booleanFalse: string;\n booleanUnset: string;\n}\n\nexport const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {\n columns: \"Columns\",\n selectAllRows: \"Select all rows\",\n sortHint: \"Click to sort · Shift-click to add a sort\",\n filter: \"Filter\",\n close: \"Close\",\n selectRow: \"Select row\",\n autoSize: \"Auto-size columns\",\n loading: \"Loading…\",\n filters: \"Filters\",\n clearAll: \"Clear all\",\n done: \"Done\",\n pageSize: \"Rows per page\",\n pageSizeAll: \"All\",\n prevPage: \"Previous page\",\n nextPage: \"Next page\",\n clearFilter: \"Clear filter\",\n filterPlaceholder: \"Filter…\",\n selectFilter: \"Select\",\n selectAll: \"All\",\n selectNone: \"None\",\n dateFrom: \"From\",\n dateTo: \"To\",\n numberMin: \"Min\",\n numberMax: \"Max\",\n numberAbs: \"Absolute value\",\n presets: {\n today: \"Today\",\n yesterday: \"Yesterday\",\n this_week: \"This week\",\n last_week: \"Last week\",\n last_7_days: \"Last 7 days\",\n last_30_days: \"Last 30 days\",\n this_month: \"This month\",\n last_month: \"Last month\",\n last_3_months: \"Last 3 months\",\n ytd: \"Year to date\",\n last_year: \"Last year\",\n },\n table: \"Data table\",\n filterResults: (shown, total) => `${shown} of ${total} rows`,\n sortedAscending: (column) => `Sorted by ${column}, ascending`,\n sortedDescending: (column) => `Sorted by ${column}, descending`,\n sortCleared: (column) => `Sort cleared on ${column}`,\n pageChanged: (page, totalPages) => `Page ${page} of ${totalPages}`,\n // The separators are the ones that shipped, so adding the seam changes no screen.\n pageRange: (from, to, total) => `${from}–${to} / ${total}`,\n rowCount: (total) => `${total}`,\n columnsCount: (visible, total) => `Columns (${visible}/${total})`,\n empty: \"No entries\",\n actions: \"Actions\",\n edit: \"Edit\",\n delete: \"Delete\",\n booleanTrue: \"Yes\",\n booleanFalse: \"No\",\n booleanUnset: \"Not set\",\n};\n\n/**\n * Merge caller overrides onto the English defaults (presets merged too).\n *\n * The parameter is `Partial` on purpose — a table with no labels at all still\n * renders — but that is also how a translated app ends up half English and never\n * hears about it: lenkbank ships nineteen of these keys in English in a German UI\n * today, because every one of them is backfilled silently. The kit will not start\n * throwing over it (three apps depend on the lenient merge), so\n * {@link missingDataTableLabels} is the other half: it answers which keys fell back,\n * and an app asserts on that in its OWN test, where the answer is actionable.\n */\nexport function resolveDataTableLabels(partial?: Partial<DataTableLabels>): DataTableLabels {\n if (!partial) return DEFAULT_DATA_TABLE_LABELS;\n const merged: DataTableLabels = {\n ...DEFAULT_DATA_TABLE_LABELS,\n ...partial,\n presets: { ...DEFAULT_DATA_TABLE_LABELS.presets, ...(partial.presets ?? {}) },\n };\n // `columnsCount` was split out of `columns` + a template literal. Consumers have\n // been translating `columns` since 0.1, and backfilling the English default over\n // that would hand them \"Columns (4/9)\" back for having translated the key that\n // existed at the time — the same silent-English failure the audit records against\n // this file. So the count is derived from the RESOLVED `columns` unless the caller\n // states the whole sentence, which is the escape hatch for a language where the\n // parenthetical does not fit.\n if (partial.columns !== undefined && partial.columnsCount === undefined) {\n merged.columnsCount = (visible, total) => `${merged.columns} (${visible}/${total})`;\n }\n return merged;\n}\n\n/**\n * The label keys `labels` does not supply, and which therefore come back English\n * from {@link resolveDataTableLabels}.\n *\n * For a consumer's own test:\n *\n * ```ts\n * expect(missingDataTableLabels(de.dataTable)).toEqual([]);\n * ```\n *\n * which fails the day the kit adds a key — which is the point. Adding a label is\n * additive here and a silent regression in a translated app, and nothing else in\n * either repository can tell the two apart.\n *\n * Preset labels are reported one at a time (`presets.yesterday`), because `presets`\n * is a `Record` merged key by key: answering `\"presets\"` would say nothing about\n * which of the eleven are still English.\n */\nexport function missingDataTableLabels(labels?: Partial<DataTableLabels>): string[] {\n const missing: string[] = [];\n for (const key of Object.keys(DEFAULT_DATA_TABLE_LABELS) as Array<keyof DataTableLabels>) {\n if (key === \"presets\") continue;\n if (labels?.[key] !== undefined) continue;\n // `columnsCount` is DERIVED from a translated `columns` by the resolver above,\n // so a caller who set that one is not leaking English here. Reporting it would\n // send them to translate a key they have already covered, and an `toEqual([])`\n // assertion built on this would never go green.\n if (key === \"columnsCount\" && labels?.columns !== undefined) continue;\n missing.push(key);\n }\n for (const preset of Object.keys(DEFAULT_DATA_TABLE_LABELS.presets)) {\n if (labels?.presets?.[preset] === undefined) missing.push(`presets.${preset}`);\n }\n return missing;\n}\n"],"mappings":";AAyFO,MAAM,4BAA6C;AAAA,EACxD,SAAS;AAAA,EACT,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,IACP,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,eAAe;AAAA,IACf,KAAK;AAAA,IACL,WAAW;AAAA,EACb;AAAA,EACA,OAAO;AAAA,EACP,eAAe,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,KAAK;AAAA,EACrD,iBAAiB,CAAC,WAAW,aAAa,MAAM;AAAA,EAChD,kBAAkB,CAAC,WAAW,aAAa,MAAM;AAAA,EACjD,aAAa,CAAC,WAAW,mBAAmB,MAAM;AAAA,EAClD,aAAa,CAAC,MAAM,eAAe,QAAQ,IAAI,OAAO,UAAU;AAAA;AAAA,EAEhE,WAAW,CAAC,MAAM,IAAI,UAAU,GAAG,IAAI,SAAI,EAAE,MAAM,KAAK;AAAA,EACxD,UAAU,CAAC,UAAU,GAAG,KAAK;AAAA,EAC7B,cAAc,CAAC,SAAS,UAAU,YAAY,OAAO,IAAI,KAAK;AAAA,EAC9D,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,cAAc;AAChB;AAaO,SAAS,uBAAuB,SAAqD;AAC1F,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAA0B;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,0BAA0B,SAAS,GAAI,QAAQ,WAAW,CAAC,EAAG;AAAA,EAC9E;AAQA,MAAI,QAAQ,YAAY,UAAa,QAAQ,iBAAiB,QAAW;AACvE,WAAO,eAAe,CAAC,SAAS,UAAU,GAAG,OAAO,OAAO,KAAK,OAAO,IAAI,KAAK;AAAA,EAClF;AACA,SAAO;AACT;AAoBO,SAAS,uBAAuB,QAA6C;AAClF,QAAM,UAAoB,CAAC;AAC3B,aAAW,OAAO,OAAO,KAAK,yBAAyB,GAAmC;AACxF,QAAI,QAAQ,UAAW;AACvB,QAAI,SAAS,GAAG,MAAM,OAAW;AAKjC,QAAI,QAAQ,kBAAkB,QAAQ,YAAY,OAAW;AAC7D,YAAQ,KAAK,GAAG;AAAA,EAClB;AACA,aAAW,UAAU,OAAO,KAAK,0BAA0B,OAAO,GAAG;AACnE,QAAI,QAAQ,UAAU,MAAM,MAAM,OAAW,SAAQ,KAAK,WAAW,MAAM,EAAE;AAAA,EAC/E;AACA,SAAO;AACT;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import 'react';
2
- export { D as DataTable, l as DataTableCellProps, a as DataTableChrome, b as DataTableColumn, c as DataTableDensity, m as DataTableHeadProps, d as DataTableProps, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-filters-Dh9uF_S-.js';
2
+ export { D as DataTable, x as DataTableCellProps, a as DataTableChrome, b as DataTableColumn, c as DataTableDensity, y as DataTableHeadProps, d as DataTableProps, e as DataTableRowAction, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-Drun8Fk9.js';
3
3
  export { SortState } from './data-table-sort.js';
4
4
  import './swipeable-row.js';
5
5
  import './data-table-labels.js';
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { createElement } from "react";
4
- import { Fragment as Fragment2, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as Fragment2, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { createPortal } from "react-dom";
6
6
  import {
7
7
  ArrowDown,
@@ -10,9 +10,11 @@ import {
10
10
  ChevronDown,
11
11
  ChevronRight,
12
12
  Filter,
13
+ Pencil,
14
+ Trash2,
13
15
  X
14
16
  } from "lucide-react";
15
- import { Card, Spinner } from "./ui.js";
17
+ import { Card, IconButton, Spinner } from "./ui.js";
16
18
  import { cn } from "../lib/cn.js";
17
19
  import {
18
20
  defaultFilterState,
@@ -52,6 +54,22 @@ function dataAttrsOf(props) {
52
54
  for (const [k, v] of Object.entries(props)) if (k.startsWith("data-") && k !== "data-col") out[k] = v;
53
55
  return out;
54
56
  }
57
+ const ROW_ACTIONS_KEY = "__rowActions";
58
+ function resolveRowAction(action, index, labels) {
59
+ const tone = action.tone ?? (action.kind === "delete" ? "danger" : "default");
60
+ return {
61
+ key: action.kind ?? `${index}:${action.label ?? ""}`,
62
+ label: action.label ?? (action.kind === "edit" ? labels.edit : action.kind === "delete" ? labels.delete : ""),
63
+ icon: action.icon ?? (action.kind === "edit" ? /* @__PURE__ */ jsx(Pencil, {}) : action.kind === "delete" ? /* @__PURE__ */ jsx(Trash2, {}) : null),
64
+ tone,
65
+ swipe: action.swipe ?? (tone === "danger" ? "start" : "end"),
66
+ action
67
+ };
68
+ }
69
+ async function runRowAction(action, row) {
70
+ if (action.confirm && !await action.confirm(row)) return;
71
+ action.onAction(row);
72
+ }
55
73
  function physicalSwipe(actions, dir) {
56
74
  const rtl = dir === "rtl";
57
75
  return {
@@ -76,10 +94,14 @@ function cleanAttrs(attrs) {
76
94
  for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
77
95
  return out;
78
96
  }
79
- function columnLabel(col) {
97
+ function columnLabel(col, labels) {
98
+ if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
80
99
  return typeof col.header === "string" ? col.header : col.key;
81
100
  }
82
101
  const OWN_CONTROL = 'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
102
+ function fromOutside(target, row) {
103
+ return !(target instanceof Node) || !row.contains(target);
104
+ }
83
105
  function fromOwnControl(target, row) {
84
106
  if (!(target instanceof Element)) return false;
85
107
  const hit = target.closest(OWN_CONTROL);
@@ -114,9 +136,11 @@ function RowLink({
114
136
  draggable: false,
115
137
  className,
116
138
  onClickCapture: (e) => {
139
+ if (fromOutside(e.target, e.currentTarget)) return;
117
140
  if (isPlainLeftClick(e)) e.preventDefault();
118
141
  },
119
142
  onClick: (e) => {
143
+ if (fromOutside(e.target, e.currentTarget)) return;
120
144
  if (!isPlainLeftClick(e)) {
121
145
  e.stopPropagation();
122
146
  return;
@@ -131,7 +155,7 @@ import { DEFAULT_PERSIST_PREFIX, useTableState } from "./use-table-state.js";
131
155
  import { useMobileReveal } from "./use-mobile-reveal.js";
132
156
  function DataTable({
133
157
  rows,
134
- columns,
158
+ columns: columnsProp,
135
159
  rowKey,
136
160
  defaultPageSize = 25,
137
161
  onRowClick,
@@ -168,7 +192,9 @@ function DataTable({
168
192
  resizable: resizableProp,
169
193
  multiSort: multiSortProp,
170
194
  frame = true,
171
- className
195
+ className,
196
+ rowActions,
197
+ toolbar
172
198
  }) {
173
199
  const compact = density === "compact";
174
200
  const minimal = chrome === "minimal";
@@ -189,6 +215,54 @@ function DataTable({
189
215
  [labelOverrides, labelsProp]
190
216
  );
191
217
  const locale = useKitLocale(localeProp);
218
+ const rowActionsFor = useCallback(
219
+ (row) => rowActions ? (typeof rowActions === "function" ? rowActions(row) : rowActions).filter((a) => !a.hidden?.(row)).map((a, i) => resolveRowAction(a, i, labels)) : [],
220
+ [rowActions, labels]
221
+ );
222
+ const columns = useMemo(
223
+ () => rowActions ? [
224
+ ...columnsProp,
225
+ {
226
+ key: ROW_ACTIONS_KEY,
227
+ header: /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.actions }),
228
+ cell: (row) => /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-0.5", children: rowActionsFor(row).map((a) => /* @__PURE__ */ jsx(
229
+ IconButton,
230
+ {
231
+ size: density === "compact" ? "2xs" : "xs",
232
+ label: a.label,
233
+ tone: a.tone === "danger" ? "danger" : void 0,
234
+ disabledReason: a.action.disabledReason?.(row),
235
+ stopPropagation: true,
236
+ onClick: () => void runRowAction(a.action, row),
237
+ children: a.icon
238
+ },
239
+ a.key
240
+ )) }),
241
+ className: "w-px whitespace-nowrap text-end",
242
+ headClassName: "w-px",
243
+ // On a phone the same actions are the card's swipes.
244
+ mobileHidden: true,
245
+ noRowLink: true
246
+ }
247
+ ] : columnsProp,
248
+ [columnsProp, rowActions, rowActionsFor, labels.actions, density]
249
+ );
250
+ const swipesFromActions = (row) => {
251
+ const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
252
+ if (!swipes.length) return null;
253
+ const toSwipe = (a) => ({
254
+ label: a.label,
255
+ icon: a.icon,
256
+ onCommit: () => void runRowAction(a.action, row),
257
+ className: a.tone === "danger" ? "bg-[var(--danger)]" : "bg-[var(--brand)]",
258
+ armedClassName: a.tone === "danger" ? "bg-[var(--danger-hover)]" : "bg-[var(--brand-hover)]"
259
+ });
260
+ return {
261
+ start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
262
+ end: swipes.filter((a) => a.swipe === "end").map(toSwipe)
263
+ };
264
+ };
265
+ const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : void 0);
192
266
  const { announce, regionProps } = useAnnounce();
193
267
  const {
194
268
  sorts,
@@ -291,7 +365,7 @@ function DataTable({
291
365
  });
292
366
  if (onSortsChange) onSortsChange(next);
293
367
  else setInternalSorts(next);
294
- const name = col ? columnLabel(col) : key;
368
+ const name = col ? columnLabel(col, labels) : key;
295
369
  const entry = next.find((s) => s.key === key);
296
370
  announce(
297
371
  !entry ? labels.sortCleared(name) : entry.dir === "asc" ? labels.sortedAscending(name) : labels.sortedDescending(name)
@@ -443,7 +517,7 @@ function DataTable({
443
517
  const visibleCount = visibleColumns.length;
444
518
  const totalCount = columns.length;
445
519
  const totalColSpan = visibleCount + (selection ? 1 : 0);
446
- const columnsCountLabel = labels.columnsCount(visibleCount, totalCount);
520
+ const columnsCountLabel = rowActions ? labels.columnsCount(visibleCount - 1, totalCount - 1) : labels.columnsCount(visibleCount, totalCount);
447
521
  const isMdUp = useMediaQuery("(min-width: 768px)", true);
448
522
  const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);
449
523
  const mobilePrimaryCol = mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;
@@ -495,7 +569,7 @@ function DataTable({
495
569
  const tint = rowClassName?.(row);
496
570
  const interactive = !!onRowClick;
497
571
  const opensDetail = interactive && !!expandedRow;
498
- const swipeActions = mobileSwipeActions?.(row);
572
+ const swipeActions = swipeActionsFor?.(row);
499
573
  const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
500
574
  const swipeEnabled = !!swipe && !expanded;
501
575
  const href = mobileCardLinkable ? rowHref?.(row) : void 0;
@@ -581,7 +655,10 @@ function DataTable({
581
655
  {
582
656
  role: interactive ? "button" : void 0,
583
657
  tabIndex: interactive ? 0 : void 0,
584
- onClick: interactive ? () => onRowClick(row) : void 0,
658
+ onClick: interactive ? (e) => {
659
+ if (fromOutside(e.target, e.currentTarget)) return;
660
+ onRowClick(row);
661
+ } : void 0,
585
662
  onKeyDown: interactive ? (e) => {
586
663
  if (e.target !== e.currentTarget) return;
587
664
  if (e.key === "Enter" || e.key === " ") {
@@ -630,6 +707,17 @@ function DataTable({
630
707
  ),
631
708
  children: [
632
709
  /* @__PURE__ */ jsx("span", { ...regionProps }),
710
+ toolbar != null && toolbar !== false && /* @__PURE__ */ jsx(
711
+ "div",
712
+ {
713
+ "data-table-toolbar": "",
714
+ className: cn(
715
+ "flex flex-wrap items-center gap-2",
716
+ frame ? cn("border-b border-[var(--border)]", compact ? "px-2 py-1.5" : "px-3 py-2") : "pb-3"
717
+ ),
718
+ children: toolbar
719
+ }
720
+ ),
633
721
  !isMdUp && /* @__PURE__ */ jsxs("div", { ref: mobileRootRef, children: [
634
722
  (!isServer || onFiltersChange) && /* @__PURE__ */ jsx(
635
723
  MobileFilters,
@@ -667,7 +755,7 @@ function DataTable({
667
755
  "text-center text-[var(--text-muted)]",
668
756
  compact ? "px-3 py-3 text-xs" : "px-4 py-6 text-sm"
669
757
  ),
670
- children: isLoading ? /* @__PURE__ */ jsx(LoadingText, { label: labels.loading }) : empty ?? "\u2014"
758
+ children: isLoading ? /* @__PURE__ */ jsx(LoadingText, { label: labels.loading }) : empty ?? labels.empty
671
759
  }
672
760
  ),
673
761
  canRevealMoreMobile && /* @__PURE__ */ jsx(
@@ -842,7 +930,7 @@ function DataTable({
842
930
  ]
843
931
  }
844
932
  ),
845
- resizable && /* @__PURE__ */ jsx(
933
+ resizable && col.key !== ROW_ACTIONS_KEY && /* @__PURE__ */ jsx(
846
934
  "span",
847
935
  {
848
936
  role: "separator",
@@ -907,6 +995,7 @@ function DataTable({
907
995
  ),
908
996
  onClick: rowInteractive ? (e) => {
909
997
  if (fromOwnControl(e.target, e.currentTarget)) return;
998
+ if (fromOutside(e.target, e.currentTarget)) return;
910
999
  if (selection && canSelect(row)) {
911
1000
  if (e.shiftKey) {
912
1001
  window.getSelection?.()?.removeAllRanges();
@@ -992,7 +1081,7 @@ function DataTable({
992
1081
  {
993
1082
  colSpan: totalColSpan,
994
1083
  className: cn("text-center text-[var(--text-muted)]", compact ? "px-2 py-2" : "px-3 py-4"),
995
- children: isLoading ? /* @__PURE__ */ jsx(LoadingText, { label: labels.loading }) : empty ?? "\u2014"
1084
+ children: isLoading ? /* @__PURE__ */ jsx(LoadingText, { label: labels.loading }) : empty ?? labels.empty
996
1085
  }
997
1086
  ) })
998
1087
  ]
@@ -1077,6 +1166,7 @@ function DataTable({
1077
1166
  }
1078
1167
  ),
1079
1168
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: columns.map((col) => {
1169
+ if (col.key === ROW_ACTIONS_KEY) return null;
1080
1170
  const checked = !hiddenCols.has(col.key);
1081
1171
  const headerLabel = columnLabel(col);
1082
1172
  return /* @__PURE__ */ jsxs(