@eifi1/ui-kit 0.48.2 → 0.49.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 (292) hide show
  1. package/README.md +26 -12
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +11 -5
  7. package/dist/admin/user-roster.js +15 -4
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.js +2 -2
  16. package/dist/auth/verify-email.js.map +1 -1
  17. package/dist/billing/plan-card.js +2 -2
  18. package/dist/billing/plan-card.js.map +1 -1
  19. package/dist/components/breadcrumbs.d.ts +19 -1
  20. package/dist/components/breadcrumbs.js +11 -4
  21. package/dist/components/breadcrumbs.js.map +1 -1
  22. package/dist/components/button-look.d.ts +37 -7
  23. package/dist/components/button-look.js +33 -6
  24. package/dist/components/button-look.js.map +1 -1
  25. package/dist/components/button.d.ts +4 -1
  26. package/dist/components/button.js +2 -2
  27. package/dist/components/button.js.map +1 -1
  28. package/dist/components/calendar-heatmap.js +2 -1
  29. package/dist/components/calendar-heatmap.js.map +1 -1
  30. package/dist/components/chart-edge.d.ts +10 -0
  31. package/dist/components/chart-edge.js +57 -0
  32. package/dist/components/chart-edge.js.map +1 -0
  33. package/dist/components/chart.js +2 -1
  34. package/dist/components/chart.js.map +1 -1
  35. package/dist/components/chip.d.ts +8 -1
  36. package/dist/components/chip.js +4 -1
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/confirm-context.d.ts +5 -0
  39. package/dist/components/confirm-context.js +7 -0
  40. package/dist/components/confirm-context.js.map +1 -0
  41. package/dist/components/confirm-dialog.js +3 -3
  42. package/dist/components/confirm-dialog.js.map +1 -1
  43. package/dist/components/danger-confirm.d.ts +2 -2
  44. package/dist/components/danger-confirm.js +1 -1
  45. package/dist/components/danger-confirm.js.map +1 -1
  46. package/dist/components/data-table-fit.js +21 -3
  47. package/dist/components/data-table-fit.js.map +1 -1
  48. package/dist/components/data-table-mobile.d.ts +48 -2
  49. package/dist/components/data-table-mobile.js +87 -10
  50. package/dist/components/data-table-mobile.js.map +1 -1
  51. package/dist/components/data-table-row-actions.d.ts +2 -1
  52. package/dist/components/data-table-row-actions.js +7 -0
  53. package/dist/components/data-table-row-actions.js.map +1 -1
  54. package/dist/components/data-table-row.d.ts +1 -1
  55. package/dist/components/data-table-row.js.map +1 -1
  56. package/dist/components/data-table.d.ts +34 -1
  57. package/dist/components/data-table.js +34 -9
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/error-boundary.js +2 -2
  60. package/dist/components/error-boundary.js.map +1 -1
  61. package/dist/components/floating-panel.d.ts +36 -13
  62. package/dist/components/floating-panel.js +27 -7
  63. package/dist/components/floating-panel.js.map +1 -1
  64. package/dist/components/form-actions.d.ts +1 -1
  65. package/dist/components/form-actions.js +1 -1
  66. package/dist/components/form-actions.js.map +1 -1
  67. package/dist/components/icon-button.d.ts +11 -0
  68. package/dist/components/icon-button.js +7 -5
  69. package/dist/components/icon-button.js.map +1 -1
  70. package/dist/components/nav-pills.d.ts +8 -1
  71. package/dist/components/nav-pills.js +61 -38
  72. package/dist/components/nav-pills.js.map +1 -1
  73. package/dist/components/page-header-context.d.ts +8 -0
  74. package/dist/components/page-header-context.js +7 -0
  75. package/dist/components/page-header-context.js.map +1 -0
  76. package/dist/components/page-header.d.ts +14 -2
  77. package/dist/components/page-header.js +10 -1
  78. package/dist/components/page-header.js.map +1 -1
  79. package/dist/components/pie-chart.js +2 -1
  80. package/dist/components/pie-chart.js.map +1 -1
  81. package/dist/components/progress-bar.js +5 -4
  82. package/dist/components/progress-bar.js.map +1 -1
  83. package/dist/components/reauth-dialog.js +1 -1
  84. package/dist/components/reauth-dialog.js.map +1 -1
  85. package/dist/components/row-actions-labels.js +3 -1
  86. package/dist/components/row-actions-labels.js.map +1 -1
  87. package/dist/components/row-actions.d.ts +43 -9
  88. package/dist/components/row-actions.js +78 -15
  89. package/dist/components/row-actions.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +37 -0
  91. package/dist/components/section-nav.js +33 -0
  92. package/dist/components/section-nav.js.map +1 -0
  93. package/dist/components/series-chart.js +8 -3
  94. package/dist/components/series-chart.js.map +1 -1
  95. package/dist/components/share-card.js +1 -1
  96. package/dist/components/share-card.js.map +1 -1
  97. package/dist/components/signature-pad.js +1 -1
  98. package/dist/components/signature-pad.js.map +1 -1
  99. package/dist/components/sparkline.js +2 -1
  100. package/dist/components/sparkline.js.map +1 -1
  101. package/dist/components/stat-tile.js +14 -7
  102. package/dist/components/stat-tile.js.map +1 -1
  103. package/dist/components/status-dot.d.ts +5 -1
  104. package/dist/components/status-dot.js +4 -1
  105. package/dist/components/status-dot.js.map +1 -1
  106. package/dist/components/tooltip.js +1 -1
  107. package/dist/components/tooltip.js.map +1 -1
  108. package/dist/components/translation-review-editor.js +2 -2
  109. package/dist/components/translation-review-editor.js.map +1 -1
  110. package/dist/components/translation-review.js +2 -2
  111. package/dist/components/translation-review.js.map +1 -1
  112. package/dist/components/treemap.d.ts +5 -2
  113. package/dist/components/treemap.js +26 -6
  114. package/dist/components/treemap.js.map +1 -1
  115. package/dist/components/use-table-state.js +37 -6
  116. package/dist/components/use-table-state.js.map +1 -1
  117. package/dist/data-table.js.map +1 -1
  118. package/dist/demo/demo-banner.js +1 -1
  119. package/dist/demo/demo-banner.js.map +1 -1
  120. package/dist/demo/demo-ended.js +1 -1
  121. package/dist/demo/demo-ended.js.map +1 -1
  122. package/dist/demo/demo-start.js +1 -1
  123. package/dist/demo/demo-start.js.map +1 -1
  124. package/dist/eslint/index.js +1 -1
  125. package/dist/feedback/feedback-inbox.js +1 -1
  126. package/dist/feedback/feedback-inbox.js.map +1 -1
  127. package/dist/hooks/use-dismiss.d.ts +9 -1
  128. package/dist/hooks/use-dismiss.js +25 -5
  129. package/dist/hooks/use-dismiss.js.map +1 -1
  130. package/dist/hooks/use-document-title.d.ts +14 -0
  131. package/dist/hooks/use-document-title.js +22 -0
  132. package/dist/hooks/use-document-title.js.map +1 -0
  133. package/dist/i18n/german.js +6 -2
  134. package/dist/i18n/german.js.map +1 -1
  135. package/dist/i18n/kit-labels.d.ts +31 -1
  136. package/dist/i18n/kit-labels.js +43 -2
  137. package/dist/i18n/kit-labels.js.map +1 -1
  138. package/dist/i18n/locales/en.js +6 -2
  139. package/dist/i18n/locales/en.js.map +2 -2
  140. package/dist/i18n/locales/es.js +6 -2
  141. package/dist/i18n/locales/es.js.map +1 -1
  142. package/dist/i18n/locales/fr.js +6 -2
  143. package/dist/i18n/locales/fr.js.map +1 -1
  144. package/dist/i18n/locales/hu.js +6 -2
  145. package/dist/i18n/locales/hu.js.map +1 -1
  146. package/dist/i18n/locales/it.js +6 -2
  147. package/dist/i18n/locales/it.js.map +1 -1
  148. package/dist/i18n/locales/zh.js +6 -2
  149. package/dist/i18n/locales/zh.js.map +1 -1
  150. package/dist/i18n/review.js +2 -0
  151. package/dist/i18n/review.js.map +1 -1
  152. package/dist/i18n/shared-labels.js +1 -0
  153. package/dist/i18n/shared-labels.js.map +1 -1
  154. package/dist/index.d.ts +8 -1
  155. package/dist/index.js +11 -0
  156. package/dist/index.js.map +1 -1
  157. package/dist/landing/landing-actions.js +2 -2
  158. package/dist/landing/landing-actions.js.map +1 -1
  159. package/dist/landing/public-header.js +2 -2
  160. package/dist/landing/public-header.js.map +1 -1
  161. package/dist/lib/clipboard-minus.d.ts +27 -0
  162. package/dist/lib/clipboard-minus.js +63 -0
  163. package/dist/lib/clipboard-minus.js.map +1 -0
  164. package/dist/lib/document-title.d.ts +17 -0
  165. package/dist/lib/document-title.js +29 -0
  166. package/dist/lib/document-title.js.map +1 -0
  167. package/dist/lib/float-clearance.js +8 -2
  168. package/dist/lib/float-clearance.js.map +1 -1
  169. package/dist/lib/format.d.ts +35 -10
  170. package/dist/lib/format.js +55 -11
  171. package/dist/lib/format.js.map +1 -1
  172. package/dist/lib/minus.d.ts +22 -0
  173. package/dist/lib/minus.js +14 -0
  174. package/dist/lib/minus.js.map +1 -0
  175. package/dist/lib/status-tone.d.ts +30 -0
  176. package/dist/lib/status-tone.js +17 -0
  177. package/dist/lib/status-tone.js.map +1 -0
  178. package/dist/settings/settings-layout.js +3 -2
  179. package/dist/settings/settings-layout.js.map +1 -1
  180. package/dist/shell/account-menu-language.d.ts +1 -1
  181. package/dist/shell/account-menu-language.js +9 -1
  182. package/dist/shell/account-menu-language.js.map +1 -1
  183. package/dist/shell/app-shell-labels.js +2 -1
  184. package/dist/shell/app-shell-labels.js.map +1 -1
  185. package/dist/shell/app-shell.d.ts +58 -16
  186. package/dist/shell/app-shell.js +151 -43
  187. package/dist/shell/app-shell.js.map +1 -1
  188. package/dist/shell/language-code.d.ts +3 -0
  189. package/dist/shell/language-code.js +8 -0
  190. package/dist/shell/language-code.js.map +1 -0
  191. package/dist/shell/topbar-controls.d.ts +14 -6
  192. package/dist/shell/topbar-controls.js +14 -17
  193. package/dist/shell/topbar-controls.js.map +1 -1
  194. package/dist/testing/css-variables.generated.d.ts +1 -1
  195. package/dist/testing/css-variables.generated.js +1 -1
  196. package/dist/testing/css-variables.generated.js.map +1 -1
  197. package/dist/theme/chart-palette.d.ts +82 -15
  198. package/dist/theme/chart-palette.js +51 -24
  199. package/dist/theme/chart-palette.js.map +1 -1
  200. package/dist/theme/palette-presets.d.ts +9 -0
  201. package/dist/theme/palette-presets.js +77 -62
  202. package/dist/theme/palette-presets.js.map +1 -1
  203. package/dist/tour/tour.js +1 -1
  204. package/dist/tour/tour.js.map +1 -1
  205. package/dist/wizard/stepper-nav.d.ts +1 -1
  206. package/dist/wizard/stepper-nav.js +3 -3
  207. package/dist/wizard/stepper-nav.js.map +1 -1
  208. package/package.json +1 -1
  209. package/src/admin/admin-action-confirm.tsx +1 -1
  210. package/src/admin/invitations-panel.tsx +7 -3
  211. package/src/admin/user-roster.tsx +23 -6
  212. package/src/auth/accept-invitation.tsx +3 -3
  213. package/src/auth/not-found-page.tsx +1 -1
  214. package/src/auth/reset-password-form.tsx +3 -3
  215. package/src/auth/verify-email.tsx +3 -3
  216. package/src/billing/plan-card.tsx +2 -2
  217. package/src/components/breadcrumbs.tsx +32 -4
  218. package/src/components/button-look.ts +86 -15
  219. package/src/components/button.tsx +6 -3
  220. package/src/components/calendar-heatmap.tsx +2 -1
  221. package/src/components/chart-edge.ts +73 -0
  222. package/src/components/chart.tsx +3 -1
  223. package/src/components/chip.tsx +10 -1
  224. package/src/components/confirm-context.ts +7 -0
  225. package/src/components/confirm-dialog.tsx +3 -3
  226. package/src/components/danger-confirm.tsx +3 -3
  227. package/src/components/data-table-fit.ts +41 -6
  228. package/src/components/data-table-mobile.tsx +162 -11
  229. package/src/components/data-table-row-actions.tsx +9 -1
  230. package/src/components/data-table-row.tsx +2 -0
  231. package/src/components/data-table.tsx +82 -18
  232. package/src/components/error-boundary.tsx +2 -2
  233. package/src/components/floating-panel.tsx +67 -16
  234. package/src/components/form-actions.tsx +2 -2
  235. package/src/components/icon-button.tsx +21 -8
  236. package/src/components/nav-pills.tsx +29 -2
  237. package/src/components/page-header-context.ts +8 -0
  238. package/src/components/page-header.tsx +34 -2
  239. package/src/components/pie-chart.tsx +2 -1
  240. package/src/components/progress-bar.tsx +6 -3
  241. package/src/components/reauth-dialog.tsx +1 -1
  242. package/src/components/row-actions-labels.ts +2 -0
  243. package/src/components/row-actions.tsx +154 -33
  244. package/src/components/section-nav.tsx +65 -0
  245. package/src/components/series-chart.tsx +12 -3
  246. package/src/components/share-card.tsx +1 -1
  247. package/src/components/signature-pad.tsx +1 -1
  248. package/src/components/sparkline.tsx +2 -1
  249. package/src/components/stat-tile.tsx +19 -7
  250. package/src/components/status-dot.tsx +7 -1
  251. package/src/components/tooltip.tsx +3 -1
  252. package/src/components/translation-review-editor.tsx +2 -2
  253. package/src/components/translation-review.tsx +2 -2
  254. package/src/components/treemap.tsx +34 -8
  255. package/src/components/use-table-state.ts +63 -8
  256. package/src/data-table.ts +3 -3
  257. package/src/demo/demo-banner.tsx +1 -1
  258. package/src/demo/demo-ended.tsx +1 -1
  259. package/src/demo/demo-start.tsx +1 -1
  260. package/src/feedback/feedback-inbox.tsx +1 -1
  261. package/src/hooks/use-dismiss.ts +43 -4
  262. package/src/hooks/use-document-title.ts +31 -0
  263. package/src/i18n/german.ts +4 -0
  264. package/src/i18n/kit-labels.tsx +76 -1
  265. package/src/i18n/locales/es.ts +4 -0
  266. package/src/i18n/locales/fr.ts +4 -0
  267. package/src/i18n/locales/hu.ts +4 -0
  268. package/src/i18n/locales/it.ts +4 -0
  269. package/src/i18n/locales/zh.ts +4 -0
  270. package/src/i18n/review.ts +2 -0
  271. package/src/i18n/shared-labels.ts +1 -0
  272. package/src/index.ts +13 -0
  273. package/src/landing/landing-actions.tsx +2 -2
  274. package/src/landing/public-header.tsx +2 -2
  275. package/src/lib/clipboard-minus.ts +94 -0
  276. package/src/lib/document-title.ts +41 -0
  277. package/src/lib/float-clearance.ts +19 -3
  278. package/src/lib/format.ts +115 -30
  279. package/src/lib/minus.ts +40 -0
  280. package/src/lib/status-tone.ts +43 -0
  281. package/src/settings/settings-layout.tsx +3 -2
  282. package/src/shell/account-menu-language.tsx +14 -2
  283. package/src/shell/app-shell-labels.ts +1 -0
  284. package/src/shell/app-shell.tsx +291 -68
  285. package/src/shell/language-code.ts +5 -0
  286. package/src/shell/topbar-controls.tsx +35 -15
  287. package/src/testing/css-variables.generated.ts +6 -6
  288. package/src/theme/chart-palette.ts +102 -20
  289. package/src/theme/palette-presets.ts +121 -84
  290. package/src/tour/tour.tsx +1 -1
  291. package/src/wizard/stepper-nav.tsx +4 -4
  292. package/tokens.css +28 -18
@@ -1,4 +1,4 @@
1
- import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ import { Fragment, isValidElement, useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode } from "react";
3
3
  import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, ChevronRight, Filter } from "lucide-react";
4
4
  import { FOCUS_RING } from "./focus-ring";
@@ -13,6 +13,9 @@ import { FilterPopover } from "./data-table-filter-popover";
13
13
  import { SwipeableRow, type SwipeAction } from "./swipeable-row";
14
14
  import { DialogFrame } from "./dialog-frame";
15
15
  import { Checkbox } from "./checkbox";
16
+ import { Chip } from "./chip";
17
+ import { BooleanMark } from "./data-table-cells";
18
+ import { EMPTY_FORMATTED_VALUE } from "../lib/format";
16
19
  import { useLongPress } from "../hooks/use-long-press";
17
20
  import type { DataTableLabels } from "./data-table-labels";
18
21
  import { RowLink, cleanAttrs, columnLabel, fromOutside, fromOwnControl } from "./data-table-shared";
@@ -70,19 +73,21 @@ const CARD_NO_TRUNCATE =
70
73
  * header's `sr-only` text (an actions column's), as every kit container must.
71
74
  */
72
75
  export function StackedFields<T>({ columns, row, compact }: { columns: DataTableColumn<T>[]; row: T; compact: boolean }) {
76
+ const fields = filledFields(columns, row);
77
+ if (fields.length === 0) return null;
73
78
  return (
74
79
  <dl
75
80
  data-card-fields="stacked"
76
81
  className={cn("relative flex min-w-0 flex-col", compact ? "gap-1.5 text-xs" : "gap-2.5 text-sm", CARD_NO_TRUNCATE)}
77
82
  >
78
- {columns.map((col) => (
83
+ {fields.map(({ col, value }) => (
79
84
  <div key={col.key} className="min-w-0">
80
85
  <dt className="text-xs uppercase tracking-wide text-muted">{col.header}</dt>
81
86
  <dd
82
87
  {...dataAttrsOf(col.cellProps?.(row))}
83
88
  className="mt-0.5 min-w-0 break-words text-secondary"
84
89
  >
85
- {col.cell(row)}
90
+ {value}
86
91
  </dd>
87
92
  </div>
88
93
  ))}
@@ -90,6 +95,112 @@ export function StackedFields<T>({ columns, row, compact }: { columns: DataTable
90
95
  );
91
96
  }
92
97
 
98
+ /** The most fields the default phone card shows under its title (0.49 KIT-7). */
99
+ export const MOBILE_CARD_FIELDS = 3;
100
+
101
+ /**
102
+ * The default phone card's columns (0.49 KIT-7), out of the phone's columns (visible,
103
+ * not `mobileHidden`): the title — `mobile: "primary"` or `mobilePrimary`, else the
104
+ * first that is not a status — the `role: "status"` columns for beside it, at most
105
+ * {@link MOBILE_CARD_FIELDS} fields: those marked `mobile: "secondary"` when any is,
106
+ * else the first after the title, and the `role: "actions"` columns, drawn after the
107
+ * fields. An actions column is never the title and never one of the fields, so the
108
+ * fields and the marks leave it on the card; only `mobileHidden` / `mobile: "hidden"`
109
+ * (which keep it out of `columns`) remove it.
110
+ */
111
+ export function mobileCardColumns<T>(columns: DataTableColumn<T>[]): {
112
+ primary: DataTableColumn<T> | null;
113
+ status: DataTableColumn<T>[];
114
+ fields: DataTableColumn<T>[];
115
+ actions: DataTableColumn<T>[];
116
+ } {
117
+ const titles = columns.filter((c) => c.role !== "actions");
118
+ const primary =
119
+ titles.find((c) => c.mobile === "primary") ??
120
+ titles.find((c) => c.mobilePrimary) ??
121
+ titles.find((c) => c.role !== "status") ??
122
+ titles[0] ??
123
+ null;
124
+ const status = columns.filter((c) => c !== primary && c.role === "status");
125
+ const actions = columns.filter((c) => c.role === "actions");
126
+ const rest = titles.filter((c) => c !== primary && c.role !== "status");
127
+ const marked = rest.filter((c) => c.mobile === "secondary");
128
+ return { primary, status, fields: (marked.length > 0 ? marked : rest).slice(0, MOBILE_CARD_FIELDS), actions };
129
+ }
130
+
131
+ /** A column whose cell keeps its own clicks — `noRowLink`, or `role: "actions"`: no row
132
+ * link may wrap it, and a card that draws it is not a link itself. */
133
+ export function keepsOwnClicks<T>(col: DataTableColumn<T>): boolean {
134
+ return !!col.noRowLink || col.role === "actions";
135
+ }
136
+
137
+ /** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
138
+ * production one, never under vitest. Read per call so a test can switch it on. */
139
+ function devWarnings(): boolean {
140
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
141
+ }
142
+
143
+ /**
144
+ * A development build's note on the default phone card (0.49 KIT-7): a `noRowLink`
145
+ * column past its three fields. Such a column holds the cell's own controls — a per-row
146
+ * delete, a link — and the card leaves it out with them, so on a phone the action is
147
+ * gone. Never for a `role: "actions"` column, which the card draws after its fields.
148
+ * Read off the declared `columns` (not the ones a person or the width hid), and only
149
+ * while the table lets the card choose: `enabled` is false with a `mobileCard` or
150
+ * `mobileDetailsInRow` (the column is then in the opened row), and a column marked
151
+ * `mobile: "secondary"` means the app chose. Once per table and column key; `name`
152
+ * (the table's `storageKey` or `urlSync` prefix) says which table, else its title
153
+ * column does.
154
+ */
155
+ export function useCardColumnWarnings<T>(columns: DataTableColumn<T>[], enabled: boolean, name?: string): void {
156
+ const warned = useRef<Set<string> | null>(null);
157
+ useEffect(() => {
158
+ if (!enabled || !devWarnings()) return;
159
+ const phone = columns.filter((c) => !c.mobileHidden && c.mobile !== "hidden");
160
+ if (phone.some((c) => c.mobile === "secondary")) return;
161
+ const { primary, status, fields, actions } = mobileCardColumns(phone);
162
+ const onCard = new Set<DataTableColumn<T> | null>([primary, ...status, ...fields, ...actions]);
163
+ const table = name ? ` "${name}"` : primary ? ` (title column "${primary.key}")` : "";
164
+ warned.current ??= new Set();
165
+ for (const col of phone) {
166
+ if (onCard.has(col) || !col.noRowLink || warned.current.has(col.key)) continue;
167
+ warned.current.add(col.key);
168
+ console.warn(
169
+ `[ui-kit] DataTable${table}: column "${col.key}" (noRowLink) is past the phone card's three fields and ` +
170
+ 'leaves the card — give it role: "actions" (drawn after the fields), or move its action to rowActions.',
171
+ );
172
+ }
173
+ }, [columns, enabled, name]);
174
+ }
175
+
176
+ /**
177
+ * Whether a rendered cell shows nothing worth a line (0.49 KIT-7): `null`, `undefined`,
178
+ * a boolean (React draws neither), `""` or blank text, and the formatters' empty value
179
+ * "—" ({@link EMPTY_FORMATTED_VALUE}), and `booleanColumn`'s mark for an unset value.
180
+ * Any other element is never empty here — its output is the component's — so a column
181
+ * whose empty value should drop off the card returns `null` (or "—") from `cell` for it.
182
+ */
183
+ export function isEmptyCell(value: ReactNode): boolean {
184
+ if (value === null || value === undefined || typeof value === "boolean") return true;
185
+ if (isValidElement<{ value?: unknown }>(value) && value.type === BooleanMark) return value.props.value == null;
186
+ if (typeof value === "string") {
187
+ const text = value.trim();
188
+ return text === "" || text === EMPTY_FORMATTED_VALUE;
189
+ }
190
+ return false;
191
+ }
192
+
193
+ /** Each column's rendered cell, without the empty ones. */
194
+ function filledFields<T>(columns: DataTableColumn<T>[], row: T): { col: DataTableColumn<T>; value: ReactNode }[] {
195
+ return columns.map((col) => ({ col, value: col.cell(row) })).filter(({ value }) => !isEmptyCell(value));
196
+ }
197
+
198
+ /** A status column's value beside the card's title: text in a neutral chip, an element
199
+ * (the app's own Chip or StatusDot) as it is. */
200
+ function StatusChip({ value }: { value: ReactNode }) {
201
+ return typeof value === "string" || typeof value === "number" ? <Chip size="sm">{value}</Chip> : <>{value}</>;
202
+ }
203
+
93
204
  // ---------- Phone sort control (keksdose K10) ----------
94
205
 
95
206
  /**
@@ -199,7 +310,11 @@ export interface MobileRowProps<T> {
199
310
  mobileCardLinkable: boolean;
200
311
  mobileCard: DataTableProps<T>["mobileCard"];
201
312
  mobilePrimaryCol: DataTableColumn<T> | null;
313
+ /** The `role: "status"` columns, drawn as chips beside the title (0.49 KIT-7). */
314
+ mobileStatusColumns: DataTableColumn<T>[];
202
315
  mobileSecondaryColumns: DataTableColumn<T>[];
316
+ /** The `role: "actions"` columns, drawn after the fields at the card's end (0.49 KIT-7). */
317
+ mobileActionColumns: DataTableColumn<T>[];
203
318
  mobileExpandAsDialog: boolean;
204
319
  compact: boolean;
205
320
  /** The text size is Large or Extra large (§4). */
@@ -236,7 +351,9 @@ export function MobileRow<T>({
236
351
  mobileCardLinkable,
237
352
  mobileCard,
238
353
  mobilePrimaryCol,
354
+ mobileStatusColumns,
239
355
  mobileSecondaryColumns,
356
+ mobileActionColumns,
240
357
  mobileExpandAsDialog,
241
358
  compact,
242
359
  large,
@@ -305,6 +422,11 @@ export function MobileRow<T>({
305
422
  // itself is the anchor, and routing it through `linkColumn` too would nest an
306
423
  // <a> inside an <a>. Desktop and mobile are mutually exclusive (`isMdUp`), so
307
424
  // exactly one anchor exists per row.
425
+ // The default card's filled fields and status values (0.49 KIT-7): an empty one
426
+ // gets no line. Not computed for a `mobileCard`, which draws its own.
427
+ const fields = mobileCard ? [] : filledFields(mobileSecondaryColumns, row);
428
+ const statuses = mobileCard ? [] : filledFields(mobileStatusColumns, row);
429
+ const actionCells = mobileCard ? [] : filledFields(mobileActionColumns, row);
308
430
  const cardInner = (
309
431
  <>
310
432
  {/* The card's own content keeps the column it always had; the chevron sits
@@ -323,24 +445,42 @@ export function MobileRow<T>({
323
445
  mobileCard(row)
324
446
  ) : (
325
447
  <>
326
- {/* Only the `data-*` half of `cellProps` — see its note. */}
327
- {mobilePrimaryCol && (
328
- <div {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))} className="min-w-0 break-words font-medium">
329
- {mobilePrimaryCol.cell(row)}
448
+ {/* Only the `data-*` half of `cellProps` — see its note. The title row
449
+ carries the status chips at its end (0.49 KIT-7). */}
450
+ {(mobilePrimaryCol || statuses.length > 0) && (
451
+ <div data-card-head="" className="flex min-w-0 items-start justify-between gap-2">
452
+ {mobilePrimaryCol && (
453
+ <div
454
+ {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))}
455
+ data-card-title=""
456
+ className="min-w-0 flex-1 break-words font-medium"
457
+ >
458
+ {mobilePrimaryCol.cell(row)}
459
+ </div>
460
+ )}
461
+ {statuses.length > 0 && (
462
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
463
+ {statuses.map(({ col, value }) => (
464
+ <span key={col.key} {...dataAttrsOf(col.cellProps?.(row))} data-card-status="" className="inline-flex">
465
+ <StatusChip value={value} />
466
+ </span>
467
+ ))}
468
+ </div>
469
+ )}
330
470
  </div>
331
471
  )}
332
- {mobileSecondaryColumns.length > 0 && large ? (
472
+ {fields.length > 0 && large ? (
333
473
  // §4: each label ABOVE its value at Large. Side by side, a label column
334
474
  // sized to the widest label leaves a value a sliver of a 240 px card.
335
475
  <StackedFields columns={mobileSecondaryColumns} row={row} compact={compact} />
336
- ) : mobileSecondaryColumns.length > 0 && (
476
+ ) : fields.length > 0 && (
337
477
  <dl
338
478
  className={cn(
339
479
  "grid grid-cols-[auto_1fr] gap-x-3",
340
480
  compact ? "gap-y-0.5 text-xs" : "gap-y-1 text-sm",
341
481
  )}
342
482
  >
343
- {mobileSecondaryColumns.map((col) => (
483
+ {fields.map(({ col, value }) => (
344
484
  <Fragment key={col.key}>
345
485
  <dt className="text-xs uppercase tracking-wide text-muted self-center">
346
486
  {col.header}
@@ -349,12 +489,23 @@ export function MobileRow<T>({
349
489
  {...dataAttrsOf(col.cellProps?.(row))}
350
490
  className="min-w-0 text-secondary self-center"
351
491
  >
352
- {col.cell(row)}
492
+ {value}
353
493
  </dd>
354
494
  </Fragment>
355
495
  ))}
356
496
  </dl>
357
497
  )}
498
+ {/* The row's actions after the fields, at the card's end (0.49 KIT-7): its
499
+ buttons keep their own clicks, and RowActions folds into "⋯" at Large. */}
500
+ {actionCells.length > 0 && (
501
+ <div data-card-actions="" className="flex min-w-0 flex-wrap items-center justify-end gap-2">
502
+ {actionCells.map(({ col, value }) => (
503
+ <div key={col.key} {...dataAttrsOf(col.cellProps?.(row))} className="flex min-w-0 justify-end">
504
+ {value}
505
+ </div>
506
+ ))}
507
+ </div>
508
+ )}
358
509
  </>
359
510
  )}
360
511
  </div>
@@ -64,7 +64,8 @@ function swipeTone(action: SwipeAction): RowActionTone {
64
64
  * `end` then `start` — the physical `right` / `left` only where the logical side is
65
65
  * missing, as the card reads them — each action once, told apart by its `id`, else by its
66
66
  * label (two stages with one label would be one button twice). Each button runs the
67
- * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any
67
+ * swipe's own `onCommit` — with no confirmation of the kit's (`confirm: false`), as the
68
+ * swipe has none; a swipe's `danger` / `warning` / `info` tone carries over, any
68
69
  * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a
69
70
  * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.
70
71
  *
@@ -94,6 +95,10 @@ export function swipePlanRowActions<T>(
94
95
  : undefined,
95
96
  tone: swipeTone(action),
96
97
  commit,
98
+ // A swipe commits without the kit asking (the plan's `onCommit` asks, as keksdose's
99
+ // does), so its button does not ask twice (0.49 KIT-12); a danger one still sits in
100
+ // the row's menu.
101
+ confirm: false,
97
102
  onSelect: action.onCommit,
98
103
  });
99
104
  }
@@ -142,6 +147,9 @@ function toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {
142
147
  icon: isValidElement(a.icon) ? a.icon : a.icon != null && a.icon !== false ? <>{a.icon}</> : undefined,
143
148
  tone: a.tone,
144
149
  disabledReason: a.action.disabledReason?.(row),
150
+ // The table's own `confirm` asks in `runRowAction`; RowActions' default question
151
+ // (0.49 KIT-12) is for a delete that brought none.
152
+ confirm: a.action.confirm ? false : undefined,
145
153
  onSelect: () => void runRowAction(a.action, row),
146
154
  };
147
155
  }
@@ -406,7 +406,9 @@ export interface DataTableMobileRowProps<T>
406
406
  | "mobileCardLinkable"
407
407
  | "mobileCard"
408
408
  | "mobilePrimaryCol"
409
+ | "mobileStatusColumns"
409
410
  | "mobileSecondaryColumns"
411
+ | "mobileActionColumns"
410
412
  | "mobileExpandAsDialog"
411
413
  | "compact"
412
414
  | "large"
@@ -6,7 +6,15 @@ import { isTableOwnColumn, useRowActionColumn } from "./data-table-row-actions";
6
6
  import { useColumnResize } from "./data-table-resize";
7
7
  import { useRangeSelection } from "./data-table-selection";
8
8
  import { columnLabel, logicalAlign, resolveDataTableLabels } from "./data-table-shared";
9
- import { MobileFilters, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
9
+ import {
10
+ MobileFilters,
11
+ MobileSelectBar,
12
+ MobileSort,
13
+ StackedFields,
14
+ keepsOwnClicks,
15
+ mobileCardColumns,
16
+ useCardColumnWarnings,
17
+ } from "./data-table-mobile";
10
18
  import { DataTableMobileRow, DataTableRow, type DataTableRowCtx } from "./data-table-row";
11
19
  import { DataTableHead } from "./data-table-head";
12
20
  import { DataTableLeadingRow } from "./data-table-leading-row";
@@ -61,6 +69,36 @@ export interface DataTableColumn<T> {
61
69
  // bookkeeping) that would crowd a card without aiding scanning.
62
70
  mobilePrimary?: boolean;
63
71
  mobileHidden?: boolean;
72
+ /**
73
+ * This column on the phone's default card (0.49 KIT-7; ignored with `mobileCard`):
74
+ * `"primary"` — the card's title, as `mobilePrimary`; `"secondary"` — one of its
75
+ * fields; `"hidden"` — not on the card, as `mobileHidden`. The card shows at most
76
+ * three fields: the columns marked `"secondary"` when any
77
+ * is, else the first three visible columns after the title. A field whose value is
78
+ * empty — the cell returns `null`, `undefined`, `""` or the formatters' empty "—" —
79
+ * is left out of that card. The columns the card does not show go into the opened
80
+ * row with `mobileDetailsInRow`. A `noRowLink` column (the cell's own buttons or link)
81
+ * counts as a field: past the third it leaves the card with its action, and a
82
+ * development build says so once per table and column key. Give a column of row
83
+ * actions `role: "actions"` instead: it is then no field, and the card draws it after
84
+ * them.
85
+ */
86
+ mobile?: "primary" | "secondary" | "hidden";
87
+ /**
88
+ * What the column is (0.49 KIT-7), for the phone's default card; the desktop table is
89
+ * unchanged.
90
+ *
91
+ * - `"status"`: its value sits as a chip beside the title instead of as a label/value
92
+ * line, and takes none of the card's three fields. A cell that returns text is drawn
93
+ * in a neutral `Chip`; one that returns an element (a `Chip` with its own tone, a
94
+ * `StatusDot`) is drawn as it is.
95
+ * - `"actions"`: the row's buttons (`rowActionsColumn` sets it). Never the card's title
96
+ * and never one of its three fields: the card draws it after them, at its end,
97
+ * whether or not other columns are marked `mobile: "secondary"`; only `mobileHidden`
98
+ * / `mobile: "hidden"` take it off. Its cell keeps its own clicks, as `noRowLink`:
99
+ * no row link wraps it, and a card that draws it is not a link.
100
+ */
101
+ role?: "status" | "actions";
64
102
  /**
65
103
  * Set on a column whose cell renders its own `<a>` (or anything else that must
66
104
  * keep its own click): the row anchor of `rowHref` then skips it and lands on
@@ -275,7 +313,10 @@ export interface DataTableProps<T> {
275
313
  * When true, mirror filter/sort/page state into URL search params so views are
276
314
  * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
277
315
  * URL state takes precedence over localStorage on initial load. Requires the
278
- * consuming app to render inside a react-router Router.
316
+ * consuming app to render inside a react-router Router (0.49: only with `urlSync` — a
317
+ * table without it needs none). With `urlSync` and no Router, a development build
318
+ * throws "DataTable urlSync requires a Router"; a production build keeps the view in
319
+ * local state instead.
279
320
  *
280
321
  * `{ prefix }` namespaces them, for two URL-synced tables on one page:
281
322
  * `urlSync={{ prefix: "inv." }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.
@@ -1309,14 +1350,14 @@ export function DataTable<T>({
1309
1350
  // column panel. Resolved from `visibleColumns` for exactly that reason — off a
1310
1351
  // raw `columns` lookup a hidden headline would leave the row with no link at all.
1311
1352
  //
1312
- // `noRowLink` columns are skipped in BOTH steps: wrapping a cell that owns an
1313
- // anchor nests two links and disables the inner one (see the prop's note). If
1314
- // nothing visible is left to wrap, the row gets NO anchor — an anchor over the
1315
- // one cell there is would cancel that cell's own click and hand back nothing,
1316
- // and a row without a link still behaves exactly as it did before #451.
1353
+ // `noRowLink` (and `role: "actions"`) columns are skipped in BOTH steps: wrapping a
1354
+ // cell that owns an anchor nests two links and disables the inner one (see the prop's
1355
+ // note). If nothing visible is left to wrap, the row gets NO anchor — an anchor
1356
+ // over the one cell there is would cancel that cell's own click and hand back
1357
+ // nothing, and a row without a link still behaves exactly as it did before #451.
1317
1358
  const linkColumn = rowHref
1318
- ? (visibleColumns.find((c) => c.mobilePrimary && !c.noRowLink) ??
1319
- visibleColumns.find((c) => !c.noRowLink))
1359
+ ? (visibleColumns.find((c) => c.mobilePrimary && !keepsOwnClicks(c)) ??
1360
+ visibleColumns.find((c) => !keepsOwnClicks(c)))
1320
1361
  : undefined;
1321
1362
 
1322
1363
  // ---- Column resize ---- (data-table-resize.ts: pointer capture, one `setWidths` per
@@ -1387,15 +1428,36 @@ export function DataTable<T>({
1387
1428
  // filters come through a bottom sheet (feedback #299) and the sort through the
1388
1429
  // opt-in `mobileSort` row (keksdose K10), both on the table's own state.
1389
1430
  // Memoised on `visibleColumns`: the cards compare their columns by identity (0.48.2).
1390
- const mobileColumns = useMemo(() => visibleColumns.filter((c) => !c.mobileHidden), [visibleColumns]);
1391
- const mobilePrimaryCol =
1392
- mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;
1393
- const mobileSecondaryColumns = useMemo(
1394
- () => mobileColumns.filter((c) => c !== mobilePrimaryCol),
1395
- [mobileColumns, mobilePrimaryCol],
1431
+ const mobileColumns = useMemo(
1432
+ () => visibleColumns.filter((c) => !c.mobileHidden && c.mobile !== "hidden"),
1433
+ [visibleColumns],
1434
+ );
1435
+ // The default card (0.49 KIT-7): the title, the status chips beside it, at most three
1436
+ // fields — the ones marked `mobile: "secondary"`, else the first after the title — and
1437
+ // the `role: "actions"` columns after them.
1438
+ const {
1439
+ primary: mobilePrimaryCol,
1440
+ status: mobileStatusColumns,
1441
+ fields: mobileSecondaryColumns,
1442
+ actions: mobileActionColumns,
1443
+ } = useMemo(() => mobileCardColumns(mobileColumns), [mobileColumns]);
1444
+ // A development build names a `noRowLink` column (a per-row delete) the card drops.
1445
+ useCardColumnWarnings(
1446
+ columns,
1447
+ !mobileCard && !mobileDetailsInRow,
1448
+ storageKey ?? (typeof urlSync === "object" ? urlSync.prefix : undefined),
1396
1449
  );
1397
- // The columns the card leaves out, for the opened row (`mobileDetailsInRow`).
1398
- const mobileDetailColumns = mobileDetailsInRow ? visibleColumns.filter((c) => c.mobileHidden) : [];
1450
+ // The columns the card leaves out, for the opened row (`mobileDetailsInRow`): the
1451
+ // hidden ones and, without a `mobileCard`, the ones past the card's three fields.
1452
+ const onCard = new Set<DataTableColumn<T>>([
1453
+ ...(mobilePrimaryCol ? [mobilePrimaryCol] : []),
1454
+ ...mobileStatusColumns,
1455
+ ...mobileSecondaryColumns,
1456
+ ...mobileActionColumns,
1457
+ ]);
1458
+ const mobileDetailColumns = mobileDetailsInRow
1459
+ ? visibleColumns.filter((c) => (mobileCard ? c.mobileHidden || c.mobile === "hidden" : !onCard.has(c)))
1460
+ : [];
1399
1461
  // Large and Extra large (§4 "DataTable phone card"): the card's labels go ABOVE their
1400
1462
  // values, nothing in it truncates, and the rows get more room. Most of it is a
1401
1463
  // `large:` class; the label/value grid needs a different STRUCTURE (each pair in a
@@ -1420,7 +1482,7 @@ export function DataTable<T>({
1420
1482
  // opens the row and leaves the cell's own link working. A caller's `mobileCard`
1421
1483
  // renderer is opaque to us; a link inside one is the caller's to declare by
1422
1484
  // marking the column it comes from.
1423
- const mobileCardLinkable = mobileColumns.every((c) => !c.noRowLink);
1485
+ const mobileCardLinkable = (mobileCard ? mobileColumns : [...onCard]).every((c) => !keepsOwnClicks(c));
1424
1486
  // The card list's reading direction, for the one thing on it CSS cannot flip: which
1425
1487
  // physical drag a logical `start`/`end` swipe action means. Read off the DOM through
1426
1488
  // a ref callback (it runs at commit, like a layout effect) because `dir` is an
@@ -1590,7 +1652,9 @@ export function DataTable<T>({
1590
1652
  mobileCardLinkable={mobileCardLinkable}
1591
1653
  mobileCard={mobileCard}
1592
1654
  mobilePrimaryCol={mobilePrimaryCol}
1655
+ mobileStatusColumns={mobileStatusColumns}
1593
1656
  mobileSecondaryColumns={mobileSecondaryColumns}
1657
+ mobileActionColumns={mobileActionColumns}
1594
1658
  mobileExpandAsDialog={mobileExpandAsDialog}
1595
1659
  compact={compact}
1596
1660
  large={large}
@@ -604,14 +604,14 @@ function ErrorFallback({
604
604
  const errorLine = details.message ? `${details.name}: ${details.message}` : details.name;
605
605
 
606
606
  const reloadButton = (
607
- <Button key="reload" variant={mode === "error" ? "secondary" : "primary"} onClick={reloadPage}>
607
+ <Button key="reload" variant={mode === "error" ? "secondary" : "default"} onClick={reloadPage}>
608
608
  {text.reload}
609
609
  </Button>
610
610
  );
611
611
  // Not after a chunk failure: `React.lazy` keeps the rejected import, so Try again
612
612
  // could only throw the same error again.
613
613
  const retryButton = chunk ? null : (
614
- <Button key="retry" variant={mode === "error" ? "primary" : "secondary"} onClick={reset}>
614
+ <Button key="retry" variant={mode === "error" ? "default" : "secondary"} onClick={reset}>
615
615
  {text.retry}
616
616
  </Button>
617
617
  );