@eifi1/ui-kit 0.48.2 → 0.50.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 (377) hide show
  1. package/README.md +34 -16
  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 +24 -12
  7. package/dist/admin/user-roster.js +21 -6
  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.d.ts +9 -1
  16. package/dist/auth/verify-email.js +5 -3
  17. package/dist/auth/verify-email.js.map +1 -1
  18. package/dist/billing/plan-card.js +2 -2
  19. package/dist/billing/plan-card.js.map +1 -1
  20. package/dist/components/amount-input.d.ts +2 -1
  21. package/dist/components/amount-input.js +3 -2
  22. package/dist/components/amount-input.js.map +1 -1
  23. package/dist/components/autocomplete.js +1 -0
  24. package/dist/components/autocomplete.js.map +1 -1
  25. package/dist/components/breadcrumbs.d.ts +19 -1
  26. package/dist/components/breadcrumbs.js +11 -4
  27. package/dist/components/breadcrumbs.js.map +1 -1
  28. package/dist/components/button-look.d.ts +37 -7
  29. package/dist/components/button-look.js +33 -6
  30. package/dist/components/button-look.js.map +1 -1
  31. package/dist/components/button.d.ts +4 -1
  32. package/dist/components/button.js +2 -2
  33. package/dist/components/button.js.map +1 -1
  34. package/dist/components/calendar-heatmap.js +2 -1
  35. package/dist/components/calendar-heatmap.js.map +1 -1
  36. package/dist/components/chart-edge.d.ts +10 -0
  37. package/dist/components/chart-edge.js +57 -0
  38. package/dist/components/chart-edge.js.map +1 -0
  39. package/dist/components/chart.js +2 -1
  40. package/dist/components/chart.js.map +1 -1
  41. package/dist/components/chip.d.ts +8 -1
  42. package/dist/components/chip.js +4 -1
  43. package/dist/components/chip.js.map +1 -1
  44. package/dist/components/combobox-core.js +1 -0
  45. package/dist/components/combobox-core.js.map +1 -1
  46. package/dist/components/combobox-suggestions.js +1 -0
  47. package/dist/components/combobox-suggestions.js.map +1 -1
  48. package/dist/components/confirm-context.d.ts +5 -0
  49. package/dist/components/confirm-context.js +7 -0
  50. package/dist/components/confirm-context.js.map +1 -0
  51. package/dist/components/confirm-dialog.js +3 -3
  52. package/dist/components/confirm-dialog.js.map +1 -1
  53. package/dist/components/currency-select.d.ts +28 -2
  54. package/dist/components/currency-select.js +56 -10
  55. package/dist/components/currency-select.js.map +1 -1
  56. package/dist/components/danger-confirm.d.ts +2 -2
  57. package/dist/components/danger-confirm.js +1 -1
  58. package/dist/components/danger-confirm.js.map +1 -1
  59. package/dist/components/data-table-fit.js +21 -3
  60. package/dist/components/data-table-fit.js.map +1 -1
  61. package/dist/components/data-table-mobile.d.ts +58 -3
  62. package/dist/components/data-table-mobile.js +147 -16
  63. package/dist/components/data-table-mobile.js.map +1 -1
  64. package/dist/components/data-table-row-actions.d.ts +2 -1
  65. package/dist/components/data-table-row-actions.js +7 -0
  66. package/dist/components/data-table-row-actions.js.map +1 -1
  67. package/dist/components/data-table-row.d.ts +1 -1
  68. package/dist/components/data-table-row.js.map +1 -1
  69. package/dist/components/data-table.d.ts +34 -1
  70. package/dist/components/data-table.js +37 -10
  71. package/dist/components/data-table.js.map +1 -1
  72. package/dist/components/dropdown.js +1 -0
  73. package/dist/components/dropdown.js.map +1 -1
  74. package/dist/components/error-boundary.js +2 -2
  75. package/dist/components/error-boundary.js.map +1 -1
  76. package/dist/components/floating-panel.d.ts +36 -13
  77. package/dist/components/floating-panel.js +27 -7
  78. package/dist/components/floating-panel.js.map +1 -1
  79. package/dist/components/form-actions.d.ts +1 -1
  80. package/dist/components/form-actions.js +1 -1
  81. package/dist/components/form-actions.js.map +1 -1
  82. package/dist/components/full-bleed-dialog.js +1 -0
  83. package/dist/components/full-bleed-dialog.js.map +1 -1
  84. package/dist/components/icon-button.d.ts +11 -0
  85. package/dist/components/icon-button.js +7 -5
  86. package/dist/components/icon-button.js.map +1 -1
  87. package/dist/components/modal.js +1 -0
  88. package/dist/components/modal.js.map +1 -1
  89. package/dist/components/nav-pills.d.ts +8 -1
  90. package/dist/components/nav-pills.js +61 -38
  91. package/dist/components/nav-pills.js.map +1 -1
  92. package/dist/components/number-input.d.ts +2 -1
  93. package/dist/components/number-input.js +6 -2
  94. package/dist/components/number-input.js.map +1 -1
  95. package/dist/components/numpad-sheet.js +2 -0
  96. package/dist/components/numpad-sheet.js.map +1 -1
  97. package/dist/components/page-header-context.d.ts +8 -0
  98. package/dist/components/page-header-context.js +7 -0
  99. package/dist/components/page-header-context.js.map +1 -0
  100. package/dist/components/page-header.d.ts +14 -2
  101. package/dist/components/page-header.js +10 -1
  102. package/dist/components/page-header.js.map +1 -1
  103. package/dist/components/picker-sheet.js +2 -0
  104. package/dist/components/picker-sheet.js.map +1 -1
  105. package/dist/components/pie-chart.js +2 -1
  106. package/dist/components/pie-chart.js.map +1 -1
  107. package/dist/components/popover.js +1 -0
  108. package/dist/components/popover.js.map +1 -1
  109. package/dist/components/progress-bar.js +5 -4
  110. package/dist/components/progress-bar.js.map +1 -1
  111. package/dist/components/reauth-dialog.js +1 -1
  112. package/dist/components/reauth-dialog.js.map +1 -1
  113. package/dist/components/row-actions-labels.js +3 -1
  114. package/dist/components/row-actions-labels.js.map +1 -1
  115. package/dist/components/row-actions.d.ts +43 -9
  116. package/dist/components/row-actions.js +78 -15
  117. package/dist/components/row-actions.js.map +1 -1
  118. package/dist/components/section-nav.d.ts +37 -0
  119. package/dist/components/section-nav.js +33 -0
  120. package/dist/components/section-nav.js.map +1 -0
  121. package/dist/components/series-chart.js +8 -3
  122. package/dist/components/series-chart.js.map +1 -1
  123. package/dist/components/share-card.js +1 -1
  124. package/dist/components/share-card.js.map +1 -1
  125. package/dist/components/signature-pad.js +1 -1
  126. package/dist/components/signature-pad.js.map +1 -1
  127. package/dist/components/sparkline.js +2 -1
  128. package/dist/components/sparkline.js.map +1 -1
  129. package/dist/components/stat-tile.js +14 -7
  130. package/dist/components/stat-tile.js.map +1 -1
  131. package/dist/components/status-dot.d.ts +5 -1
  132. package/dist/components/status-dot.js +4 -1
  133. package/dist/components/status-dot.js.map +1 -1
  134. package/dist/components/tooltip.js +1 -1
  135. package/dist/components/tooltip.js.map +1 -1
  136. package/dist/components/translation-review-editor.js +2 -2
  137. package/dist/components/translation-review-editor.js.map +1 -1
  138. package/dist/components/translation-review.js +2 -2
  139. package/dist/components/translation-review.js.map +1 -1
  140. package/dist/components/treemap.d.ts +5 -2
  141. package/dist/components/treemap.js +26 -6
  142. package/dist/components/treemap.js.map +1 -1
  143. package/dist/components/use-table-state.js +37 -6
  144. package/dist/components/use-table-state.js.map +1 -1
  145. package/dist/data-table.js.map +1 -1
  146. package/dist/demo/demo-banner.js +1 -1
  147. package/dist/demo/demo-banner.js.map +1 -1
  148. package/dist/demo/demo-ended.js +1 -1
  149. package/dist/demo/demo-ended.js.map +1 -1
  150. package/dist/demo/demo-start.js +1 -1
  151. package/dist/demo/demo-start.js.map +1 -1
  152. package/dist/eslint/index.js +1 -1
  153. package/dist/feedback/feedback-inbox.js +1 -1
  154. package/dist/feedback/feedback-inbox.js.map +1 -1
  155. package/dist/hooks/use-dismiss.d.ts +9 -1
  156. package/dist/hooks/use-dismiss.js +25 -5
  157. package/dist/hooks/use-dismiss.js.map +1 -1
  158. package/dist/hooks/use-document-title.d.ts +14 -0
  159. package/dist/hooks/use-document-title.js +22 -0
  160. package/dist/hooks/use-document-title.js.map +1 -0
  161. package/dist/i18n/defaults.js +2 -0
  162. package/dist/i18n/defaults.js.map +1 -1
  163. package/dist/i18n/german.js +20 -3
  164. package/dist/i18n/german.js.map +1 -1
  165. package/dist/i18n/kit-labels.d.ts +34 -1
  166. package/dist/i18n/kit-labels.js +43 -2
  167. package/dist/i18n/kit-labels.js.map +1 -1
  168. package/dist/i18n/locales/en.js +22 -3
  169. package/dist/i18n/locales/en.js.map +3 -3
  170. package/dist/i18n/locales/es.js +20 -3
  171. package/dist/i18n/locales/es.js.map +1 -1
  172. package/dist/i18n/locales/fr.js +20 -3
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.js +20 -3
  175. package/dist/i18n/locales/hu.js.map +1 -1
  176. package/dist/i18n/locales/it.js +20 -3
  177. package/dist/i18n/locales/it.js.map +1 -1
  178. package/dist/i18n/locales/zh.js +20 -3
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.js +3 -0
  181. package/dist/i18n/review.js.map +1 -1
  182. package/dist/i18n/shared-labels.js +1 -0
  183. package/dist/i18n/shared-labels.js.map +1 -1
  184. package/dist/index.d.ts +8 -1
  185. package/dist/index.js +11 -0
  186. package/dist/index.js.map +1 -1
  187. package/dist/landing/landing-actions.js +2 -2
  188. package/dist/landing/landing-actions.js.map +1 -1
  189. package/dist/landing/public-header.js +2 -2
  190. package/dist/landing/public-header.js.map +1 -1
  191. package/dist/lib/clipboard-minus.d.ts +27 -0
  192. package/dist/lib/clipboard-minus.js +63 -0
  193. package/dist/lib/clipboard-minus.js.map +1 -0
  194. package/dist/lib/data-attributes.d.ts +11 -0
  195. package/dist/lib/data-attributes.js +10 -0
  196. package/dist/lib/data-attributes.js.map +1 -0
  197. package/dist/lib/document-title.d.ts +17 -0
  198. package/dist/lib/document-title.js +29 -0
  199. package/dist/lib/document-title.js.map +1 -0
  200. package/dist/lib/float-clearance.js +8 -2
  201. package/dist/lib/float-clearance.js.map +1 -1
  202. package/dist/lib/format.d.ts +36 -10
  203. package/dist/lib/format.js +57 -11
  204. package/dist/lib/format.js.map +1 -1
  205. package/dist/lib/minus.d.ts +32 -0
  206. package/dist/lib/minus.js +14 -0
  207. package/dist/lib/minus.js.map +1 -0
  208. package/dist/lib/status-tone.d.ts +30 -0
  209. package/dist/lib/status-tone.js +17 -0
  210. package/dist/lib/status-tone.js.map +1 -0
  211. package/dist/settings/settings-layout.js +3 -2
  212. package/dist/settings/settings-layout.js.map +1 -1
  213. package/dist/shell/account-menu-language.d.ts +1 -1
  214. package/dist/shell/account-menu-language.js +9 -1
  215. package/dist/shell/account-menu-language.js.map +1 -1
  216. package/dist/shell/app-shell-labels.js +2 -1
  217. package/dist/shell/app-shell-labels.js.map +1 -1
  218. package/dist/shell/app-shell.d.ts +58 -16
  219. package/dist/shell/app-shell.js +151 -43
  220. package/dist/shell/app-shell.js.map +1 -1
  221. package/dist/shell/language-code.d.ts +3 -0
  222. package/dist/shell/language-code.js +8 -0
  223. package/dist/shell/language-code.js.map +1 -0
  224. package/dist/shell/topbar-controls.d.ts +14 -6
  225. package/dist/shell/topbar-controls.js +14 -17
  226. package/dist/shell/topbar-controls.js.map +1 -1
  227. package/dist/testing/css-variables.generated.d.ts +1 -1
  228. package/dist/testing/css-variables.generated.js +1 -1
  229. package/dist/testing/css-variables.generated.js.map +1 -1
  230. package/dist/testing/react.d.ts +25 -0
  231. package/dist/testing/react.js +19 -0
  232. package/dist/testing/react.js.map +1 -0
  233. package/dist/theme/chart-palette.d.ts +82 -15
  234. package/dist/theme/chart-palette.js +51 -24
  235. package/dist/theme/chart-palette.js.map +1 -1
  236. package/dist/theme/palette-presets.d.ts +9 -0
  237. package/dist/theme/palette-presets.js +77 -62
  238. package/dist/theme/palette-presets.js.map +1 -1
  239. package/dist/tour/tour-dock.d.ts +42 -0
  240. package/dist/tour/tour-dock.js +80 -0
  241. package/dist/tour/tour-dock.js.map +1 -0
  242. package/dist/tour/tour-gates.d.ts +71 -0
  243. package/dist/tour/tour-gates.js +114 -0
  244. package/dist/tour/tour-gates.js.map +1 -0
  245. package/dist/tour/tour-guide-button.d.ts +27 -0
  246. package/dist/tour/tour-guide-button.js +45 -0
  247. package/dist/tour/tour-guide-button.js.map +1 -0
  248. package/dist/tour/tour-guide-labels.d.ts +8 -0
  249. package/dist/tour/tour-guide-labels.js +9 -0
  250. package/dist/tour/tour-guide-labels.js.map +1 -0
  251. package/dist/tour/tour-labels.js +9 -1
  252. package/dist/tour/tour-labels.js.map +1 -1
  253. package/dist/tour/tour.d.ts +133 -9
  254. package/dist/tour/tour.js +594 -70
  255. package/dist/tour/tour.js.map +1 -1
  256. package/dist/tour/use-tour-url-sync.d.ts +5 -2
  257. package/dist/tour/use-tour-url-sync.js +7 -1
  258. package/dist/tour/use-tour-url-sync.js.map +1 -1
  259. package/dist/tour.d.ts +2 -0
  260. package/dist/tour.js +3 -0
  261. package/dist/tour.js.map +1 -1
  262. package/dist/wizard/stepper-nav.d.ts +1 -1
  263. package/dist/wizard/stepper-nav.js +18 -5
  264. package/dist/wizard/stepper-nav.js.map +1 -1
  265. package/dist/wizard/types.d.ts +11 -0
  266. package/dist/wizard/types.js.map +1 -1
  267. package/dist/wizard/use-wizard.js +11 -5
  268. package/dist/wizard/use-wizard.js.map +1 -1
  269. package/package.json +9 -1
  270. package/src/admin/admin-action-confirm.tsx +1 -1
  271. package/src/admin/invitations-panel.tsx +7 -3
  272. package/src/admin/user-roster.tsx +41 -14
  273. package/src/auth/accept-invitation.tsx +3 -3
  274. package/src/auth/not-found-page.tsx +1 -1
  275. package/src/auth/reset-password-form.tsx +3 -3
  276. package/src/auth/verify-email.tsx +13 -3
  277. package/src/billing/plan-card.tsx +2 -2
  278. package/src/components/amount-input.tsx +5 -4
  279. package/src/components/autocomplete.tsx +2 -0
  280. package/src/components/breadcrumbs.tsx +32 -4
  281. package/src/components/button-look.ts +86 -15
  282. package/src/components/button.tsx +6 -3
  283. package/src/components/calendar-heatmap.tsx +2 -1
  284. package/src/components/chart-edge.ts +73 -0
  285. package/src/components/chart.tsx +3 -1
  286. package/src/components/chip.tsx +10 -1
  287. package/src/components/combobox-core.tsx +2 -0
  288. package/src/components/combobox-suggestions.tsx +2 -0
  289. package/src/components/confirm-context.ts +7 -0
  290. package/src/components/confirm-dialog.tsx +3 -3
  291. package/src/components/currency-select.tsx +95 -6
  292. package/src/components/danger-confirm.tsx +3 -3
  293. package/src/components/data-table-fit.ts +41 -6
  294. package/src/components/data-table-mobile.tsx +259 -17
  295. package/src/components/data-table-row-actions.tsx +9 -1
  296. package/src/components/data-table-row.tsx +3 -0
  297. package/src/components/data-table.tsx +91 -19
  298. package/src/components/dropdown.tsx +2 -0
  299. package/src/components/error-boundary.tsx +2 -2
  300. package/src/components/floating-panel.tsx +67 -16
  301. package/src/components/form-actions.tsx +2 -2
  302. package/src/components/full-bleed-dialog.tsx +3 -0
  303. package/src/components/icon-button.tsx +21 -8
  304. package/src/components/modal.tsx +3 -0
  305. package/src/components/nav-pills.tsx +29 -2
  306. package/src/components/number-input.tsx +8 -3
  307. package/src/components/numpad-sheet.tsx +4 -0
  308. package/src/components/page-header-context.ts +8 -0
  309. package/src/components/page-header.tsx +34 -2
  310. package/src/components/picker-sheet.tsx +4 -0
  311. package/src/components/pie-chart.tsx +2 -1
  312. package/src/components/popover.tsx +2 -0
  313. package/src/components/progress-bar.tsx +6 -3
  314. package/src/components/reauth-dialog.tsx +1 -1
  315. package/src/components/row-actions-labels.ts +2 -0
  316. package/src/components/row-actions.tsx +154 -33
  317. package/src/components/section-nav.tsx +65 -0
  318. package/src/components/series-chart.tsx +12 -3
  319. package/src/components/share-card.tsx +1 -1
  320. package/src/components/signature-pad.tsx +1 -1
  321. package/src/components/sparkline.tsx +2 -1
  322. package/src/components/stat-tile.tsx +19 -7
  323. package/src/components/status-dot.tsx +7 -1
  324. package/src/components/tooltip.tsx +3 -1
  325. package/src/components/translation-review-editor.tsx +2 -2
  326. package/src/components/translation-review.tsx +2 -2
  327. package/src/components/treemap.tsx +34 -8
  328. package/src/components/use-table-state.ts +63 -8
  329. package/src/data-table.ts +3 -3
  330. package/src/demo/demo-banner.tsx +1 -1
  331. package/src/demo/demo-ended.tsx +1 -1
  332. package/src/demo/demo-start.tsx +1 -1
  333. package/src/feedback/feedback-inbox.tsx +1 -1
  334. package/src/hooks/use-dismiss.ts +43 -4
  335. package/src/hooks/use-document-title.ts +31 -0
  336. package/src/i18n/defaults.ts +2 -0
  337. package/src/i18n/german.ts +17 -0
  338. package/src/i18n/kit-labels.tsx +79 -1
  339. package/src/i18n/locales/es.ts +17 -0
  340. package/src/i18n/locales/fr.ts +17 -0
  341. package/src/i18n/locales/hu.ts +17 -0
  342. package/src/i18n/locales/it.ts +17 -0
  343. package/src/i18n/locales/zh.ts +17 -0
  344. package/src/i18n/review.ts +3 -0
  345. package/src/i18n/shared-labels.ts +1 -0
  346. package/src/index.ts +13 -0
  347. package/src/landing/landing-actions.tsx +2 -2
  348. package/src/landing/public-header.tsx +2 -2
  349. package/src/lib/clipboard-minus.ts +94 -0
  350. package/src/lib/data-attributes.ts +14 -0
  351. package/src/lib/document-title.ts +41 -0
  352. package/src/lib/float-clearance.ts +19 -3
  353. package/src/lib/format.ts +117 -30
  354. package/src/lib/minus.ts +50 -0
  355. package/src/lib/status-tone.ts +43 -0
  356. package/src/settings/settings-layout.tsx +3 -2
  357. package/src/shell/account-menu-language.tsx +14 -2
  358. package/src/shell/app-shell-labels.ts +1 -0
  359. package/src/shell/app-shell.tsx +291 -68
  360. package/src/shell/language-code.ts +5 -0
  361. package/src/shell/topbar-controls.tsx +35 -15
  362. package/src/testing/css-variables.generated.ts +6 -6
  363. package/src/testing/react.ts +59 -0
  364. package/src/theme/chart-palette.ts +102 -20
  365. package/src/theme/palette-presets.ts +121 -84
  366. package/src/tour/tour-dock.ts +132 -0
  367. package/src/tour/tour-gates.ts +195 -0
  368. package/src/tour/tour-guide-button.tsx +64 -0
  369. package/src/tour/tour-guide-labels.ts +15 -0
  370. package/src/tour/tour-labels.ts +8 -0
  371. package/src/tour/tour.tsx +949 -86
  372. package/src/tour/use-tour-url-sync.ts +14 -4
  373. package/src/tour.ts +3 -0
  374. package/src/wizard/stepper-nav.tsx +27 -7
  375. package/src/wizard/types.ts +12 -0
  376. package/src/wizard/use-wizard.ts +16 -9
  377. 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";
@@ -69,20 +72,33 @@ const CARD_NO_TRUNCATE =
69
72
  * BETWEEN facts rather than between a label and its own value. `relative` holds a
70
73
  * header's `sr-only` text (an actions column's), as every kit container must.
71
74
  */
72
- export function StackedFields<T>({ columns, row, compact }: { columns: DataTableColumn<T>[]; row: T; compact: boolean }) {
75
+ export function StackedFields<T>({
76
+ columns,
77
+ row,
78
+ compact,
79
+ liftOwnClicks = false,
80
+ }: {
81
+ columns: DataTableColumn<T>[];
82
+ row: T;
83
+ compact: boolean;
84
+ /** On a stretched card (0.50 K4): lift the own-click fields above its title link. */
85
+ liftOwnClicks?: boolean;
86
+ }) {
87
+ const fields = filledFields(columns, row);
88
+ if (fields.length === 0) return null;
73
89
  return (
74
90
  <dl
75
91
  data-card-fields="stacked"
76
92
  className={cn("relative flex min-w-0 flex-col", compact ? "gap-1.5 text-xs" : "gap-2.5 text-sm", CARD_NO_TRUNCATE)}
77
93
  >
78
- {columns.map((col) => (
94
+ {fields.map(({ col, value }) => (
79
95
  <div key={col.key} className="min-w-0">
80
96
  <dt className="text-xs uppercase tracking-wide text-muted">{col.header}</dt>
81
97
  <dd
82
98
  {...dataAttrsOf(col.cellProps?.(row))}
83
- className="mt-0.5 min-w-0 break-words text-secondary"
99
+ className={cn("mt-0.5 min-w-0 break-words text-secondary", liftOwnClicks && keepsOwnClicks(col) && LIFTED_FIELD)}
84
100
  >
85
- {col.cell(row)}
101
+ {value}
86
102
  </dd>
87
103
  </div>
88
104
  ))}
@@ -90,6 +106,127 @@ export function StackedFields<T>({ columns, row, compact }: { columns: DataTable
90
106
  );
91
107
  }
92
108
 
109
+ /**
110
+ * A stretched card's own-click cell (0.50 K4): above the title control's overlay
111
+ * (`after:z-[1]`), so its link or buttons take their own taps. `w-fit` keeps the lifted
112
+ * box to the cell's content, so the field's empty rest still opens the row.
113
+ */
114
+ const LIFTED_CELL = "relative z-[2]";
115
+ const LIFTED_FIELD = cn(LIFTED_CELL, "w-fit max-w-full justify-self-start");
116
+
117
+ /**
118
+ * The keyboard frame of a stretched card (0.50 K4): FOCUS_RING's, inside the card,
119
+ * while its title control (`data-card-link`) has the focus.
120
+ */
121
+ const STRETCHED_RING =
122
+ "has-[[data-card-link]:focus-visible]:ring-inset has-[[data-card-link]:focus-visible]:ring-focus has-[[data-card-link]:focus-visible]:ring-brand";
123
+
124
+ /** The most fields the default phone card shows under its title (0.49 KIT-7). */
125
+ export const MOBILE_CARD_FIELDS = 3;
126
+
127
+ /**
128
+ * The default phone card's columns (0.49 KIT-7), out of the phone's columns (visible,
129
+ * not `mobileHidden`): the title — `mobile: "primary"` or `mobilePrimary`, else the
130
+ * first that is not a status — the `role: "status"` columns for beside it, at most
131
+ * {@link MOBILE_CARD_FIELDS} fields: those marked `mobile: "secondary"` when any is,
132
+ * else the first after the title, and the `role: "actions"` columns, drawn after the
133
+ * fields. An actions column is never the title and never one of the fields, so the
134
+ * fields and the marks leave it on the card; only `mobileHidden` / `mobile: "hidden"`
135
+ * (which keep it out of `columns`) remove it.
136
+ */
137
+ export function mobileCardColumns<T>(columns: DataTableColumn<T>[]): {
138
+ primary: DataTableColumn<T> | null;
139
+ status: DataTableColumn<T>[];
140
+ fields: DataTableColumn<T>[];
141
+ actions: DataTableColumn<T>[];
142
+ } {
143
+ const titles = columns.filter((c) => c.role !== "actions");
144
+ const primary =
145
+ titles.find((c) => c.mobile === "primary") ??
146
+ titles.find((c) => c.mobilePrimary) ??
147
+ titles.find((c) => c.role !== "status") ??
148
+ titles[0] ??
149
+ null;
150
+ const status = columns.filter((c) => c !== primary && c.role === "status");
151
+ const actions = columns.filter((c) => c.role === "actions");
152
+ const rest = titles.filter((c) => c !== primary && c.role !== "status");
153
+ const marked = rest.filter((c) => c.mobile === "secondary");
154
+ return { primary, status, fields: (marked.length > 0 ? marked : rest).slice(0, MOBILE_CARD_FIELDS), actions };
155
+ }
156
+
157
+ /** A column whose cell keeps its own clicks — `noRowLink`, or `role: "actions"`: no row
158
+ * link may wrap it, and a card that draws it is not a link itself. */
159
+ export function keepsOwnClicks<T>(col: DataTableColumn<T>): boolean {
160
+ return !!col.noRowLink || col.role === "actions";
161
+ }
162
+
163
+ /** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
164
+ * production one, never under vitest. Read per call so a test can switch it on. */
165
+ function devWarnings(): boolean {
166
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
167
+ }
168
+
169
+ /**
170
+ * A development build's note on the default phone card (0.49 KIT-7): a `noRowLink`
171
+ * column past its three fields. Such a column holds the cell's own controls — a per-row
172
+ * delete, a link — and the card leaves it out with them, so on a phone the action is
173
+ * gone. Never for a `role: "actions"` column, which the card draws after its fields.
174
+ * Read off the declared `columns` (not the ones a person or the width hid), and only
175
+ * while the table lets the card choose: `enabled` is false with a `mobileCard` or
176
+ * `mobileDetailsInRow` (the column is then in the opened row), and a column marked
177
+ * `mobile: "secondary"` means the app chose. Once per table and column key; `name`
178
+ * (the table's `storageKey` or `urlSync` prefix) says which table, else its title
179
+ * column does.
180
+ */
181
+ export function useCardColumnWarnings<T>(columns: DataTableColumn<T>[], enabled: boolean, name?: string): void {
182
+ const warned = useRef<Set<string> | null>(null);
183
+ useEffect(() => {
184
+ if (!enabled || !devWarnings()) return;
185
+ const phone = columns.filter((c) => !c.mobileHidden && c.mobile !== "hidden");
186
+ if (phone.some((c) => c.mobile === "secondary")) return;
187
+ const { primary, status, fields, actions } = mobileCardColumns(phone);
188
+ const onCard = new Set<DataTableColumn<T> | null>([primary, ...status, ...fields, ...actions]);
189
+ const table = name ? ` "${name}"` : primary ? ` (title column "${primary.key}")` : "";
190
+ warned.current ??= new Set();
191
+ for (const col of phone) {
192
+ if (onCard.has(col) || !col.noRowLink || warned.current.has(col.key)) continue;
193
+ warned.current.add(col.key);
194
+ console.warn(
195
+ `[ui-kit] DataTable${table}: column "${col.key}" (noRowLink) is past the phone card's three fields and ` +
196
+ 'leaves the card — give it role: "actions" (drawn after the fields), or move its action to rowActions.',
197
+ );
198
+ }
199
+ }, [columns, enabled, name]);
200
+ }
201
+
202
+ /**
203
+ * Whether a rendered cell shows nothing worth a line (0.49 KIT-7): `null`, `undefined`,
204
+ * a boolean (React draws neither), `""` or blank text, and the formatters' empty value
205
+ * "—" ({@link EMPTY_FORMATTED_VALUE}), and `booleanColumn`'s mark for an unset value.
206
+ * Any other element is never empty here — its output is the component's — so a column
207
+ * whose empty value should drop off the card returns `null` (or "—") from `cell` for it.
208
+ */
209
+ export function isEmptyCell(value: ReactNode): boolean {
210
+ if (value === null || value === undefined || typeof value === "boolean") return true;
211
+ if (isValidElement<{ value?: unknown }>(value) && value.type === BooleanMark) return value.props.value == null;
212
+ if (typeof value === "string") {
213
+ const text = value.trim();
214
+ return text === "" || text === EMPTY_FORMATTED_VALUE;
215
+ }
216
+ return false;
217
+ }
218
+
219
+ /** Each column's rendered cell, without the empty ones. */
220
+ function filledFields<T>(columns: DataTableColumn<T>[], row: T): { col: DataTableColumn<T>; value: ReactNode }[] {
221
+ return columns.map((col) => ({ col, value: col.cell(row) })).filter(({ value }) => !isEmptyCell(value));
222
+ }
223
+
224
+ /** A status column's value beside the card's title: text in a neutral chip, an element
225
+ * (the app's own Chip or StatusDot) as it is. */
226
+ function StatusChip({ value }: { value: ReactNode }) {
227
+ return typeof value === "string" || typeof value === "number" ? <Chip size="sm">{value}</Chip> : <>{value}</>;
228
+ }
229
+
93
230
  // ---------- Phone sort control (keksdose K10) ----------
94
231
 
95
232
  /**
@@ -197,9 +334,20 @@ export interface MobileRowProps<T> {
197
334
  mobileDir: Direction;
198
335
  /** No column the card renders owns a link, so the card itself may be one. */
199
336
  mobileCardLinkable: boolean;
337
+ /**
338
+ * The card draws cells that keep their own clicks, so it cannot be one control (0.50
339
+ * K4): its TITLE is the row's link (`rowHref`) or button (`onRowClick`), stretched over
340
+ * the card, and those cells sit above it. Only for the default card, never a
341
+ * `mobileCard`, and never when the title itself keeps its own clicks.
342
+ */
343
+ mobileCardStretched: boolean;
200
344
  mobileCard: DataTableProps<T>["mobileCard"];
201
345
  mobilePrimaryCol: DataTableColumn<T> | null;
346
+ /** The `role: "status"` columns, drawn as chips beside the title (0.49 KIT-7). */
347
+ mobileStatusColumns: DataTableColumn<T>[];
202
348
  mobileSecondaryColumns: DataTableColumn<T>[];
349
+ /** The `role: "actions"` columns, drawn after the fields at the card's end (0.49 KIT-7). */
350
+ mobileActionColumns: DataTableColumn<T>[];
203
351
  mobileExpandAsDialog: boolean;
204
352
  compact: boolean;
205
353
  /** The text size is Large or Extra large (§4). */
@@ -234,9 +382,12 @@ export function MobileRow<T>({
234
382
  swipeActionsFor,
235
383
  mobileDir,
236
384
  mobileCardLinkable,
385
+ mobileCardStretched,
237
386
  mobileCard,
238
387
  mobilePrimaryCol,
388
+ mobileStatusColumns,
239
389
  mobileSecondaryColumns,
390
+ mobileActionColumns,
240
391
  mobileExpandAsDialog,
241
392
  compact,
242
393
  large,
@@ -272,7 +423,10 @@ export function MobileRow<T>({
272
423
  // scrolling never meet the gesture. Until 0.40 an open card silently lost it.
273
424
  // In selection mode a sideways drag would be a second meaning for a selecting touch.
274
425
  const swipeEnabled = !!swipe && !selecting;
275
- const href = mobileCardLinkable ? rowHref?.(row) : undefined;
426
+ // The stretched card (0.50 K4): the title is the control, the card a plain box.
427
+ // Selection mode keeps its own card (the whole card is the toggle there).
428
+ const stretched = mobileCardStretched && interactive && !selecting && !mobileCard && !!mobilePrimaryCol;
429
+ const href = mobileCardLinkable || stretched ? rowHref?.(row) : undefined;
276
430
  const cardClass = cn(
277
431
  "w-full text-start flex items-center",
278
432
  // §4: more room per row at Large — a taller row is a bigger tap target too.
@@ -292,7 +446,13 @@ export function MobileRow<T>({
292
446
  // AppShell main's `overflow-x-clip` — so the frame went missing on one side. A press
293
447
  // keeps the fill and the squeeze above; an outline on every tap of a long list would
294
448
  // be louder than the acknowledgement it replaces.
295
- (interactive || selecting) && cn("[-webkit-tap-highlight-color:transparent] focus-visible:ring-inset", FOCUS_RING),
449
+ (interactive || selecting) &&
450
+ !stretched &&
451
+ cn("[-webkit-tap-highlight-color:transparent] focus-visible:ring-inset", FOCUS_RING),
452
+ // A stretched card (0.50 K4) draws the same frame, inside the card, while its TITLE
453
+ // control has the keyboard: the ring outlines the whole tap target, as on every other
454
+ // card, not the title's line alone. The own-click cells draw their own.
455
+ stretched && cn("relative [-webkit-tap-highlight-color:transparent]", STRETCHED_RING),
296
456
  selecting && "cursor-pointer",
297
457
  select?.selected && selecting && "bg-brand-soft",
298
458
  // A 500 ms hold would otherwise start a text selection, and iOS's link callout —
@@ -305,6 +465,50 @@ export function MobileRow<T>({
305
465
  // itself is the anchor, and routing it through `linkColumn` too would nest an
306
466
  // <a> inside an <a>. Desktop and mobile are mutually exclusive (`isMdUp`), so
307
467
  // exactly one anchor exists per row.
468
+ // The default card's filled fields and status values (0.49 KIT-7): an empty one
469
+ // gets no line. Not computed for a `mobileCard`, which draws its own.
470
+ // The stretched card's title control (0.50 K4): the row's link, or a real <button>
471
+ // when the table only has `onRowClick` — a plain control, since nothing interactive
472
+ // sits inside it any more. Its `::after` covers the whole card, so a tap anywhere
473
+ // but on an own-click cell (lifted above it) lands on it, and a long press on the
474
+ // card is a long press on the link ("open in new tab"). Its accessible name is the
475
+ // title alone; the fields read as text after it. `data-row-link` marks it as the
476
+ // row itself for `fromOwnControl`, so the long press still arms on it.
477
+ const stretchedClass =
478
+ "block w-full text-start focus-visible:outline-none after:absolute after:inset-0 after:z-[1] after:content-['']";
479
+ const titleControl = (title: ReactNode) =>
480
+ href ? (
481
+ <RowLink
482
+ href={href}
483
+ data-card-link=""
484
+ data-row-link=""
485
+ onActivate={() => onRowClick!(row)}
486
+ aria-expanded={opensRow ? expanded : undefined}
487
+ className={stretchedClass}
488
+ // Enter follows RowLink's own onClick; Space opens the row too, not the list's scroll.
489
+ onKeyDown={(e) => {
490
+ if (e.key !== " ") return;
491
+ e.preventDefault();
492
+ onRowClick!(row);
493
+ }}
494
+ >
495
+ {title}
496
+ </RowLink>
497
+ ) : (
498
+ <button
499
+ type="button"
500
+ data-card-link=""
501
+ data-row-link=""
502
+ aria-expanded={opensRow ? expanded : undefined}
503
+ onClick={() => onRowClick!(row)}
504
+ className={cn(stretchedClass, "font-[inherit]")}
505
+ >
506
+ {title}
507
+ </button>
508
+ );
509
+ const fields = mobileCard ? [] : filledFields(mobileSecondaryColumns, row);
510
+ const statuses = mobileCard ? [] : filledFields(mobileStatusColumns, row);
511
+ const actionCells = mobileCard ? [] : filledFields(mobileActionColumns, row);
308
512
  const cardInner = (
309
513
  <>
310
514
  {/* The card's own content keeps the column it always had; the chevron sits
@@ -323,38 +527,71 @@ export function MobileRow<T>({
323
527
  mobileCard(row)
324
528
  ) : (
325
529
  <>
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)}
530
+ {/* Only the `data-*` half of `cellProps` — see its note. The title row
531
+ carries the status chips at its end (0.49 KIT-7). */}
532
+ {(mobilePrimaryCol || statuses.length > 0) && (
533
+ <div data-card-head="" className="flex min-w-0 items-start justify-between gap-2">
534
+ {mobilePrimaryCol && (
535
+ <div
536
+ {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))}
537
+ data-card-title=""
538
+ className="min-w-0 flex-1 break-words font-medium"
539
+ >
540
+ {stretched ? titleControl(mobilePrimaryCol.cell(row)) : mobilePrimaryCol.cell(row)}
541
+ </div>
542
+ )}
543
+ {statuses.length > 0 && (
544
+ <div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
545
+ {statuses.map(({ col, value }) => (
546
+ <span key={col.key} {...dataAttrsOf(col.cellProps?.(row))} data-card-status="" className="inline-flex">
547
+ <StatusChip value={value} />
548
+ </span>
549
+ ))}
550
+ </div>
551
+ )}
330
552
  </div>
331
553
  )}
332
- {mobileSecondaryColumns.length > 0 && large ? (
554
+ {fields.length > 0 && large ? (
333
555
  // §4: each label ABOVE its value at Large. Side by side, a label column
334
556
  // sized to the widest label leaves a value a sliver of a 240 px card.
335
- <StackedFields columns={mobileSecondaryColumns} row={row} compact={compact} />
336
- ) : mobileSecondaryColumns.length > 0 && (
557
+ <StackedFields columns={mobileSecondaryColumns} row={row} compact={compact} liftOwnClicks={stretched} />
558
+ ) : fields.length > 0 && (
337
559
  <dl
338
560
  className={cn(
339
561
  "grid grid-cols-[auto_1fr] gap-x-3",
340
562
  compact ? "gap-y-0.5 text-xs" : "gap-y-1 text-sm",
341
563
  )}
342
564
  >
343
- {mobileSecondaryColumns.map((col) => (
565
+ {fields.map(({ col, value }) => (
344
566
  <Fragment key={col.key}>
345
567
  <dt className="text-xs uppercase tracking-wide text-muted self-center">
346
568
  {col.header}
347
569
  </dt>
348
570
  <dd
349
571
  {...dataAttrsOf(col.cellProps?.(row))}
350
- className="min-w-0 text-secondary self-center"
572
+ className={cn("min-w-0 text-secondary self-center", stretched && keepsOwnClicks(col) && LIFTED_FIELD)}
351
573
  >
352
- {col.cell(row)}
574
+ {value}
353
575
  </dd>
354
576
  </Fragment>
355
577
  ))}
356
578
  </dl>
357
579
  )}
580
+ {/* The row's actions after the fields, at the card's end (0.49 KIT-7): its
581
+ buttons keep their own clicks, and RowActions folds into "⋯" at Large. */}
582
+ {actionCells.length > 0 && (
583
+ <div data-card-actions="" className="flex min-w-0 flex-wrap items-center justify-end gap-2">
584
+ {actionCells.map(({ col, value }) => (
585
+ <div
586
+ key={col.key}
587
+ {...dataAttrsOf(col.cellProps?.(row))}
588
+ className={cn("flex min-w-0 justify-end", stretched && LIFTED_CELL)}
589
+ >
590
+ {value}
591
+ </div>
592
+ ))}
593
+ </div>
594
+ )}
358
595
  </>
359
596
  )}
360
597
  </div>
@@ -441,6 +678,11 @@ export function MobileRow<T>({
441
678
  />
442
679
  {cardInner}
443
680
  </div>
681
+ ) : stretched ? (
682
+ // The stretched card (0.50 K4): a plain box; its title is the control (above).
683
+ <div {...press} data-card-stretched="" className={cardClass}>
684
+ {cardInner}
685
+ </div>
444
686
  ) : interactive && href ? (
445
687
  // A card with a URL of its own is a LINK, not a div wearing role="button"
446
688
  // (feedback #451): the phone's long-press "open in new tab" and a screen
@@ -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
  }
@@ -404,9 +404,12 @@ export interface DataTableMobileRowProps<T>
404
404
  | "opensRow"
405
405
  | "mobileDir"
406
406
  | "mobileCardLinkable"
407
+ | "mobileCardStretched"
407
408
  | "mobileCard"
408
409
  | "mobilePrimaryCol"
410
+ | "mobileStatusColumns"
409
411
  | "mobileSecondaryColumns"
412
+ | "mobileActionColumns"
410
413
  | "mobileExpandAsDialog"
411
414
  | "compact"
412
415
  | "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,14 @@ 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));
1486
+ // ...except on the default card (0.50 K4, kastlan): its title becomes the row's link
1487
+ // (or, with only `onRowClick`, a button), stretched over the card, and the own-click
1488
+ // cells — a tel: link, the actions row — sit above it, so "tap to open" stays and no
1489
+ // control nests in another. A `mobileCard` stays the fallback above (its controls
1490
+ // cannot be lifted), and so does a card whose title keeps its own clicks.
1491
+ const mobileCardStretched =
1492
+ !mobileCard && !mobileCardLinkable && !!mobilePrimaryCol && !keepsOwnClicks(mobilePrimaryCol);
1424
1493
  // The card list's reading direction, for the one thing on it CSS cannot flip: which
1425
1494
  // physical drag a logical `start`/`end` swipe action means. Read off the DOM through
1426
1495
  // a ref callback (it runs at commit, like a layout effect) because `dir` is an
@@ -1584,13 +1653,16 @@ export function DataTable<T>({
1584
1653
  className={rowClassName?.(row)}
1585
1654
  attrs={rowAttributes?.(row)}
1586
1655
  interactive={!!onRowClick}
1587
- href={mobileCardLinkable ? rowHref?.(row) : undefined}
1656
+ href={mobileCardLinkable || mobileCardStretched ? rowHref?.(row) : undefined}
1588
1657
  swipeActionsFor={swipeActionsFor}
1589
1658
  mobileDir={mobileDir}
1590
1659
  mobileCardLinkable={mobileCardLinkable}
1660
+ mobileCardStretched={mobileCardStretched}
1591
1661
  mobileCard={mobileCard}
1592
1662
  mobilePrimaryCol={mobilePrimaryCol}
1663
+ mobileStatusColumns={mobileStatusColumns}
1593
1664
  mobileSecondaryColumns={mobileSecondaryColumns}
1665
+ mobileActionColumns={mobileActionColumns}
1594
1666
  mobileExpandAsDialog={mobileExpandAsDialog}
1595
1667
  compact={compact}
1596
1668
  large={large}
@@ -432,6 +432,8 @@ export function DropdownPanel({
432
432
  <div
433
433
  {...rest}
434
434
  ref={setPanel}
435
+ // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).
436
+ data-kit-layer=""
435
437
  dir={dir}
436
438
  onKeyDown={handleKeyDown}
437
439
  onPointerDownCapture={handlePointerDownCapture}
@@ -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
  );