@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 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { Card } from \"./card\";\nimport { Spinner } from \"./spinner\";\nimport { isTableOwnColumn, useRowActionColumn } from \"./data-table-row-actions\";\nimport { useColumnResize } from \"./data-table-resize\";\nimport { useRangeSelection } from \"./data-table-selection\";\nimport { columnLabel, logicalAlign, resolveDataTableLabels } from \"./data-table-shared\";\nimport { MobileFilters, MobileSelectBar, MobileSort, StackedFields } from \"./data-table-mobile\";\nimport { DataTableMobileRow, DataTableRow, type DataTableRowCtx } from \"./data-table-row\";\nimport { DataTableHead } from \"./data-table-head\";\nimport { DataTableLeadingRow } from \"./data-table-leading-row\";\nimport {\n DataTableGroupBand,\n DataTableLoadMoreFooter,\n groupLevels,\n groupRows,\n useGroupBandTops,\n type DataTableGroupBy,\n type DataTableLoadMore,\n type RowGroup,\n} from \"./data-table-groups\";\nexport type { DataTableGroupBy, DataTableLoadMore } from \"./data-table-groups\";\nimport { VIRTUAL_SELECT_COL, rowNavTarget, useRowWindow, type DataTableVirtualize, type VirtualGroup } from \"./data-table-virtual\";\nexport type { DataTableVirtualize } from \"./data-table-virtual\";\nimport { ColumnSettingsPanel } from \"./data-table-column-settings\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { cn } from \"../lib/cn\";\nimport { isFilterActive, resolveFilter, rowMatches } from \"./data-table-filters\";\nimport type { ColumnFilter, FilterValue } from \"./data-table-filters\";\nimport { nextSorts } from \"./data-table-sort\";\nimport type { SortCycle, SortDir, SortState } from \"./data-table-sort\";\nimport { Pagination } from \"./data-table-pagination\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { RowActionsCollapse } from \"./row-actions\";\nimport type { CommitScope } from \"./write-lock\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale, useKitTableStorageKeyPrefix, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { dirOf, type Direction } from \"../lib/direction\";\n\n// ---------- Types ----------\n\nexport interface DataTableColumn<T> {\n key: string;\n header: ReactNode;\n cell: (row: T) => ReactNode;\n sortBy?: (row: T) => string | number | null | undefined;\n filter?: ColumnFilter<T>;\n filterBy?: (row: T) => string;\n className?: string;\n headClassName?: string;\n // Mobile card layout: when no `mobilePrimary` column is set the first\n // visible non-hidden column acts as the primary. The primary cell renders\n // bold at the top of the card without a label; the rest stack below as\n // labelled key/value pairs. `mobileHidden` skips a column entirely on\n // narrow viewports — useful for noisy details (IDs, raw URLs, internal\n // bookkeeping) that would crowd a card without aiding scanning.\n mobilePrimary?: boolean;\n mobileHidden?: boolean;\n /**\n * Set on a column whose cell renders its own `<a>` (or anything else that must\n * keep its own click): the row anchor of `rowHref` then skips it and lands on\n * the next column that allows wrapping (Keksdose feedback #451). Without it the\n * fallback can wrap such a cell the moment the headline column is hidden from\n * the settings panel — `<a>` inside `<a>`, which React warns about and which\n * kills the inner link outright, since RowLink cancels the click on the capture\n * phase and the cell's own `stopPropagation` then blocks the row handler too.\n * The rejected alternative was sniffing the rendered output for an anchor: a\n * cell that renders a link is a fact its author knows and the renderer cannot\n * see without reaching into React elements it deliberately treats as opaque.\n */\n noRowLink?: boolean;\n /**\n * The direction this column sorts in on its FIRST click. `\"desc\"` for money and\n * count columns, where the rows worth reading are the large ones and an ascending\n * first click opens on the zero rows (keksdose's aggregated report tables). The\n * rest of the cycle follows from it — see {@link DataTableProps.sortCycle}.\n * Default `\"asc\"`.\n */\n firstSort?: SortDir;\n /**\n * The column's name as plain text, for the places a ReactNode `header` cannot go:\n * the phone sort control's options ({@link DataTableProps.mobileSort}), the spoken\n * \"Sorted by …\" and the column-settings checklist. Only needed when `header` is not\n * a string (an icon, a unit suffix); without it those places fall back to the\n * header string, then to `key`.\n */\n headerText?: string;\n /**\n * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a\n * `title`, an `aria-describedby`, a class. keksdose's accounts page\n * (accounts-page.tsx, the name and IBAN columns) is why it exists: its demo mode\n * blurs everything marked `data-private`, and with nowhere to put the marker on the\n * cell it wrapped every value in an inner `<span data-private>` — which blurs the\n * text but not the cell, and has to be remembered in every `cell` renderer.\n *\n * MERGED under the kit's own, which win: `className` is joined before the column's\n * `className` (so the column's alignment is still the alignment), `style` is laid\n * under a resized column's pinned width, and `data-col` and a `role` stay the\n * table's — `data-col` is how a resize finds the column's cells, and a `<td>` that\n * claims another role leaves its row's cell count wrong for a screen reader.\n *\n * On a phone the card field (`<dd>`, or the primary line) gets the `data-*`\n * attributes only: the rest — padding classes, `colSpan`, a width — describe a\n * table cell and would mean something else, or nothing, on a card.\n */\n cellProps?: (row: T) => DataTableCellProps | undefined;\n /** The same for the column's `<th>`, once. `scope`, `aria-sort`, `data-col` and the\n * width a resize pins stay the table's, and the class list goes before\n * `headClassName` for the same reason as in {@link cellProps}. */\n headProps?: DataTableHeadProps;\n /**\n * This column's cell in the table's totals row — a `<tfoot>` on a wide screen, a\n * labelled summary card under the list on a phone. kastlan's trial balance is why\n * it exists: it hand-built a debit/credit/balance strip in a `div` UNDER the table,\n * which lined up with no column, dropped out of the table's semantics, and stayed\n * put when the user hid or resized the columns it was summing.\n *\n * `rows` is every row that passes the current filters (in the current sort order),\n * NOT just the page on screen: a total that changes when the user pages is not a\n * total, and the trial balance has to balance on page 1 as much as on page 7. The\n * page is the second argument for the rare \"subtotal of this page\" footer.\n *\n * Under `serverPagination` the table only ever holds one page, so both arguments are\n * that page — a server-side table computes its totals on the server and returns\n * them here (`footer: () => fmt(response.totalDebit)`), ignoring the arguments.\n *\n * The cell takes the column's `className` (so a `text-end` amount column totals\n * end-aligned), its resized width, and drops out with the column when it is hidden.\n */\n footer?: (rows: T[], page: T[]) => ReactNode;\n /**\n * This column takes the width the others leave, and its text ellipsises (0.48,\n * keksdose live #451): `w-full max-w-0` on its cells, so in the table's auto layout it\n * is \"the rest of the row\" and one long title can no longer widen the table. One per\n * table. It has no resize handle, and a width stored for it (before it grew) is\n * ignored: it always takes the rest. Resize the others to change it. Opt-in: without it nothing\n * changes. It keeps at least 10rem: with {@link fitPriority} columns the table leaves\n * those out first, and scrolls sideways once none is left. Every other cell keeps one\n * line at Normal text size (`whitespace-nowrap`; a column's own\n * `not-large:whitespace-normal` lets its prose wrap), so a figure or a name is never\n * broken in two to make room for it.\n */\n grow?: boolean;\n /**\n * Leave this column out while the TABLE is too narrow for it (0.48, keksdose live\n * #451) — measured on the table's own scroller, so a sidebar, the window and Large\n * text count alike. The lowest number goes first (ties: the later column first), and\n * comes back last, once the scroller is as wide as the table needed with it. Columns\n * without it never go. Desktop only: the phone's card list is not narrowed.\n *\n * The Columns panel counts only what is drawn (\"Columns (8/10)\") and marks a column\n * left out this way \"hidden for width\". Ticking it there keeps it in for as long as\n * the table is mounted (not persisted): the table makes room by leaving out the next\n * one instead, and scrolls sideways once none is left. Unticking hides it as any column.\n */\n fitPriority?: number;\n /**\n * Start this column HIDDEN (0.48, keksdose dev #617) — for as long as the view saved\n * under `storageKey` holds no entry for it: on a first visit, on a device whose saved\n * view predates the column (or the flag), and for a column the app adds later. The\n * user's own choice in the Columns panel always wins: ticking it is remembered\n * (`shown` in the saved view), unticking it again stores it as any hidden column. The\n * default itself is never written into the saved view, so dropping the flag brings the\n * column back on every device where nobody chose. In the panel it is simply unticked —\n * not \"hidden for width\", which `fitPriority` alone says. Ignored, like every hidden\n * column, with `columnSettings={false}`: nothing could bring it back.\n */\n defaultHidden?: boolean;\n}\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** What {@link DataTableColumn.cellProps} may return. */\nexport type DataTableCellProps = TdHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\n/** What {@link DataTableColumn.headProps} takes. */\nexport type DataTableHeadProps = ThHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\nexport interface ServerPagination {\n page: number; // 0-based\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n /** Without it the page size is the server's to choose, and the pager shows no\n * page-size select at all — one that changed nothing was a control that lied. */\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * A page is being fetched. The table marks itself `aria-busy`, dims the rows it is\n * still showing (they are the PREVIOUS page's, and about to go), and replaces the\n * empty state with a loading row when there is nothing to show yet — \"no results\"\n * while the first page is still in flight is a claim the table cannot make.\n *\n * The pager stays usable. Disabling it would drop focus from the very button the\n * user just pressed (a disabled button cannot hold focus), and a second click\n * during a fetch is a request the owner already has to handle for a slow network.\n */\n isLoading?: boolean;\n}\n\nexport interface DataTableProps<T> {\n rows: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string | number;\n defaultPageSize?: number;\n onRowClick?: (row: T) => void;\n /**\n * The URL that shows this row, when one exists — the table then renders a REAL\n * anchor for it (Keksdose feedback #451, \"to be able to middle click with the\n * mouse and open in a new tab\"). A row that opens through `onRowClick` alone is\n * invisible to the browser: no middle click, no ⌘/Ctrl-click, no \"copy link\n * address\", no status-bar preview, no long-press menu on a phone. The\n * alternative was an `onAuxClick` that calls `window.open` — it buys back only\n * the middle click and loses the other four, none of which JavaScript can fake.\n *\n * Return `undefined` for a row that has no address of its own. Only wire this up\n * where the OPEN row is genuinely in the URL (a `?row=` param, a detail route);\n * a row whose expansion lives in local component state has no link to hand out,\n * and a link the target page cannot honour is worse than no link.\n *\n * The anchor covers the row's primary cell, not the whole `<tr>`: an `<a>` may\n * not wrap table cells, and the absolutely-positioned row overlay that is the\n * usual workaround would sit on top of the per-cell controls these rows carry\n * (status toggles, the selection checkbox, nested links) and swallow them.\n */\n rowHref?: (row: T) => string | undefined;\n expandedRow?: (row: T) => ReactNode | null;\n isExpanded?: (row: T) => boolean;\n /**\n * Extra classes for a row — the `<tr>` on desktop, the card on mobile. Merged last,\n * so a `hover:` fill here replaces the row's own `hover:bg-hover`.\n *\n * **A row you paint has to pick its own hover** (0.33, docs/colour-roles-harmonization.md\n * §12.2): the kit cannot see your rest colour, and `--bg-hover` is mixed against the\n * card. The rule: a hover differs from its rest by 1.08:1 or more, and every text role\n * on it keeps 4.5:1.\n * - A row that rests on the well (`bg-surface-2`) hovers to the CARD,\n * `hover:bg-surface`: in light the well is the page, and `--bg-hover` sits 1.04:1\n * from it.\n * - A row whose well MARKS A STATE (selected) keeps its fill and draws a 1 px inside\n * line instead, since the card would read as \"not selected\":\n * `bg-surface-2 hover:bg-surface-2 hover:outline hover:-outline-offset-1 hover:outline-strong`.\n * An outline, not a shadow: a shadow on a `<tr>` is not painted by every engine.\n * - Not `--bg-active` for a selected row: its muted and money text fall to 3.8–4.7:1.\n */\n rowClassName?: (row: T) => string | undefined;\n /**\n * Extra DOM attributes for a row's container — the `<tr>` on desktop, the\n * `<li>` on mobile. For hooks that have to sit on the row itself rather than\n * inside a cell: guided-tour anchors, analytics ids, e2e selectors. Undefined\n * values are dropped, so a per-row conditional needs no filtering at the call\n * site. Not a styling escape hatch — that is `rowClassName`.\n */\n rowAttributes?: (row: T) => Record<string, string | undefined> | undefined;\n /**\n * Set false for a table that is short by construction — an invoice's line\n * items, a wizard's picks — where the pager is chrome around a list that can\n * never need one. Renders every row and drops the footer entirely. Ignored in\n * `serverPagination` mode, where the pager is the only way to reach the rest\n * of the data.\n */\n paginated?: boolean;\n empty?: ReactNode;\n /**\n * If set, the table persists filter / sort / page-size state to localStorage under this key.\n * Use a stable, unique key per table (e.g. \"transactions-table\").\n */\n storageKey?: string;\n /**\n * When true, mirror filter/sort/page state into URL search params so views are\n * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.\n * URL state takes precedence over localStorage on initial load. Requires the\n * consuming app to render inside a react-router Router.\n *\n * `{ prefix }` namespaces them, for two URL-synced tables on one page:\n * `urlSync={{ prefix: \"inv.\" }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.\n */\n urlSync?: TableUrlSync;\n /**\n * When true, the table fills its parent's height and scrolls INTERNALLY, with the\n * header pinned — so the page itself doesn't add a second, outer scrollbar. The parent\n * must give it a bounded height (e.g. a flex column inside the viewport-locked app\n * shell). See feedback #207.\n *\n * **The list page** (0.40, docs/page-layout-harmonization.md §1): the root carries\n * `data-fill-height`, and from `md` up AppShell's `<main>` turns into a flex column\n * while such a table is inside — the shell's banners keep their height, the page takes\n * the rest. Wrap every element between `<main>` and the table in `FillHeight` (or merge\n * `FILL_HEIGHT_CLASS`); nothing else to wire. The body never shrinks below `basis-48`\n * (12rem, about five rows): on a window too short for that, `<main>` scrolls again.\n *\n * **On a phone** the card list becomes the scroller ONLY when its parent really bounds\n * it — measured, not assumed (0.40, keksdose live #420): the list takes the flex\n * room it is given, and only when its rows overflow that room does it turn\n * `overflow-y-auto overscroll-contain`, with the filter button pinned above. Under an\n * unbounded parent — `AppShell` on a phone, where the DOCUMENT scrolls — it stays in\n * the page's flow and grows to its content. Until 0.40 it was a scroller either way,\n * and a scroller with nothing to scroll and `overscroll-contain` swallowed the page's\n * own scroll: a swipe that started on a card moved nothing. So `fillHeight` is safe\n * to pass at every width; `fillHeight={useBreakpoint(\"md\")}` is no longer needed.\n *\n * The cure for TWO SCROLLBARS (round 0.34 C14, kastlan feedback 55): the default\n * `maxBodyHeight` scrolls the body inside a page that scrolls too. How a page fills\n * the height is in {@link DataTableProps.maxBodyHeight}'s note.\n */\n fillHeight?: boolean;\n /** How tall the scrolling body may get before it scrolls, as a CSS length.\n *\n * Default `calc(100dvh - 12rem)`, which is what every caller had before this\n * existed: a bound is what makes the sticky header work, because a header\n * can only stick to a wrapper that actually scrolls. The cap assumes the\n * table has page chrome above and below it, and a table that is the last\n * thing on its page can afford more — ASPICE Atlas report #12, \"for the 25\n * elements there should in best case be no scrolling necessary\".\n *\n * Ignored under `fillHeight`, which bounds the body by its flex parent\n * instead and is the stronger statement of the same intent.\n *\n * **Two scrollbars** (round 0.34 C14, kastlan feedback 55). The cap scrolls the\n * body INSIDE a page that, in most shells, scrolls itself: a table taller than\n * the cap on a page with anything else on it shows two scrollbars side by side,\n * and the wheel moves whichever is under the pointer. For a list page whose table\n * is the point, fill the height instead, from `md` up (a phone scrolls the page\n * and its card list as one) — docs/page-layout-harmonization.md §1.2 says which\n * pages fill:\n *\n * 1. `<DataTable fillHeight>`: the table marks itself (`data-fill-height`), and\n * AppShell's `<main>` becomes a flex column while it is inside;\n * 2. each element between `<main>` and the table is a `FillHeight` (the page's\n * root, any wrapper on the way), or merges `FILL_HEIGHT_CLASS`.\n *\n * Only the body then scrolls, under its pinned header, and every other page grows\n * with its content as before (kastlan e3985c9, its leases list). A short table that\n * is the last thing on its page can instead drop the cap: `maxBodyHeight=\"none\"`. */\n maxBodyHeight?: string;\n /**\n * When provided, the table renders the supplied rows as-is (no client-side\n * filtering/sorting/slicing) and the pagination footer is driven by these\n * server-controlled values. Column-level filters and sorting are hidden\n * UNLESS the caller takes them over via `onFiltersChange`/`onSortsChange` —\n * the caller is then expected to translate the state into the server query\n * that produces `rows`.\n */\n serverPagination?: ServerPagination;\n /**\n * 0.48.1 (keksdose K-052-1): the rows are already filtered and sorted by the server —\n * render them as given. No client filter, no client sort, no client paging (every row\n * shows, as with `paginated={false}`). The column filters and sort headers behave as\n * under `serverPagination`: hidden unless the owner takes them over with\n * `onFiltersChange` / `onSortsChange`, and then a click only REPORTS the new state (a\n * header click calls `onSortsChange` with the next sort and the rows keep their order\n * until the owner re-queries and passes new ones).\n *\n * Defaults to `true` when {@link loadMore} is set — the slices it loads come from the\n * server in its order, and re-filtering them dropped rows (the other leg of each\n * transfer). Pass `serverRows={false}` for a client table that loads more chunks but\n * filters and sorts what it holds itself. `serverPagination` implies it regardless.\n */\n serverRows?: boolean;\n /**\n * Controlled filter/sort state. When the `on*Change` callback is provided\n * the table stops owning that piece of state: it renders `filters`/`sorts`\n * as given and reports user interactions through the callback (including\n * in serverPagination mode, where the UI is otherwise disabled).\n *\n * Page reset: on a client-side table, a change in the VALUE of `filters` or\n * `sorts` sends the table back to page 1 — whoever made the change, the user\n * through the header or the owner from outside (a saved view, a chip). Compared\n * by value, so passing a fresh but equal object on every render does not reset\n * anything. In `serverPagination` mode the owner's `page` is the page, and\n * resetting it on a filter change stays the owner's job.\n */\n filters?: FilterState;\n onFiltersChange?: (next: FilterState) => void;\n sorts?: SortState[];\n onSortsChange?: (next: SortState[]) => void;\n /**\n * Controlled page size, as `sorts` above: the table pages by it and hands its pager's\n * choice to `onPageSizeChange` (`Infinity` is \"All\"). For tables that page ALIKE —\n * `TranslationReviewPanel`'s groups share one page size as they share one sort\n * (0.35). Left out, the table keeps its own, from `defaultPageSize`, the URL and\n * `storageKey`. A change of value sends a client-side table back to page 1. In\n * `serverPagination` mode the page size is `serverPagination.pageSize`.\n */\n pageSize?: number;\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * Opt-in multi-row selection. When provided, a leading\n * checkbox column is rendered with a select-all box in the header; the owner\n * holds the selected set and reacts via the callbacks. Rows for which\n * `isSelectable` returns false render no checkbox and are excluded from\n * select-all. See feedback #285 (bulk edit).\n *\n * **On a phone** (0.40, keksdose live #421) the cards enter a selection MODE: a long\n * press on a card (500 ms, keksdose's register gesture) selects it and turns every\n * card into a toggle with its box showing; a tap then selects or deselects. A\n * keyboard or screen reader enters through \"Select rows\" (`labels.selectRows`),\n * visually hidden until focused; in the mode a bar above the cards holds \"Select all\n * rows\" and Cancel (`labels.cancelSelection`). The mode lasts while anything is\n * selected — the bulk bar's Clear, the last box unticked or Cancel ends it — and swipe\n * actions rest while it is on. Render the owner's `BulkActionBar` as on desktop; it\n * appears with the first selected row. With `selection` the cards' text is not\n * selectable on a phone, and a linked card's long press selects instead of opening\n * the browser's link menu.\n */\n selection?: {\n isSelectable?: (row: T) => boolean;\n isSelected: (row: T) => boolean;\n onToggle: (row: T, checked: boolean) => void;\n /**\n * Commit a whole Shift+click range in ONE call (audit 2026-09-22).\n *\n * Optional and purely additive: without it the range is still walked row by\n * row, exactly as before. With it, an owner holding a `Set` copies that Set\n * once instead of once per row — a 400-row range was 400 `onToggle` calls,\n * 400 Set copies and 400 renders of this table, all to reach a state the\n * owner could have reached in one.\n *\n * `rows` is already filtered by `isSelectable` and is in the order the table\n * is showing, so it can be applied as-is.\n */\n onToggleMany?: (rows: T[], checked: boolean) => void;\n allSelected: boolean;\n someSelected: boolean;\n onToggleAll: (checked: boolean) => void;\n };\n /** User-facing strings (English defaults); pass translated overrides. Merged over\n * `dataTable` from `<UiKitProvider labels>`, so a call site only states what\n * differs for THIS table — typically `table`, its accessible name. */\n labels?: LabelOverride<DataTableLabels>;\n /** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls\n * back to `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** localStorage namespace prefix for `storageKey` persistence: the stored key is this\n * plus `storageKey`. Falls back to `<UiKitProvider tableStorageKeyPrefix>` (0.34, set\n * your app's prefix there once), then to \"hbui-table:\". This prop wins over both. */\n storageKeyPrefix?: string;\n /**\n * On phones, surface the expanded row's detail in a full-screen dialog instead\n * of unfolding it inline (feedback #204). Desktop always uses inline expansion.\n */\n mobileExpandAsDialog?: boolean;\n /**\n * Show the `mobileHidden` columns in the OPENED row on the phone layout — inline\n * under the card, or in the `mobileExpandAsDialog` dialog — so a column dropped from\n * the card to keep it short is not lost (0.32, docs/text-size-harmonization.md §4:\n * \"a new `mobileDetailsInRow` puts the `mobileHidden` columns into the opened row, so\n * nothing is lost\"). At Large a card holds fewer facts per screen, and dropping one\n * from the card must not drop it from the phone.\n *\n * Each column is a label above its value, BEFORE what `expandedRow` renders: the\n * row's own facts first, then the app's detail or editor. A row opens as it always\n * does (`onRowClick` toggling `isExpanded`); with this set it may open without an\n * `expandedRow` at all. `rowActions`' column is one of them — the actions, which the\n * card otherwise offers only as swipes, reachable by a tap. Desktop is unaffected.\n */\n mobileDetailsInRow?: boolean;\n /**\n * The row's name, as plain text — for the \"⋯\" menu its `rowActions` collapse into at\n * Large (\"Actions for Ada Example\", §10.8). Without it every row's menu button is\n * named \"Actions\", which a screen reader cannot tell apart down the list.\n */\n rowName?: (row: T) => string | undefined;\n /**\n * The phone row dialog's `backCloses` ({@link FullBleedDialog}): whether it pushes\n * its own history entry so Back dismisses it. On by default — right for a row whose\n * expansion is component state, where nothing else would answer Back.\n *\n * **Set it `false` when the open row is in the URL** and opening it PUSHES (a\n * `?row=` written with `useSearchParamState(…, { history: \"replace-on-clear\" })` or\n * `\"push\"`): Back already closes the row through the router, and a second entry on\n * top of the page's is two owners of one Back press (keksdose dev #584). Keep it on\n * when opening the row REPLACES the URL — then nothing was pushed, and the dialog's\n * own entry is the only thing Back can pop.\n *\n * The table cannot decide this for you: `isExpanded` is a predicate, and neither it\n * nor `rowHref` says whether opening pushed or replaced (keksdose's feedback page\n * has `rowHref` with a REPLACING `?row=`, and needs the entry). Ignored unless\n * `mobileExpandAsDialog`.\n */\n mobileDialogBackCloses?: boolean;\n /**\n * Group the mobile card list into sections, each with a sticky header — the\n * \"assistance\"-style list (like the transactions list grouped by date, but by\n * whatever key you return, e.g. a name's first letter). The returned string is\n * the section a row belongs to; rows must already be ordered so equal keys are\n * contiguous (pair with a matching default sort). `mobileGroupLabel` formats\n * the header. Desktop is unaffected. Feedback #317.\n */\n mobileGroupBy?: ((row: T) => string) | DataTableGroupBy<T>;\n /** The section header's words for `mobileGroupBy`'s key. Wins over a level's own\n * `label` when `mobileGroupBy` is a {@link DataTableGroupBy}. */\n mobileGroupLabel?: (key: string) => ReactNode;\n /**\n * Fully replace the default mobile card body (bold primary + labelled\n * key/value rows) with a compact custom layout, while keeping the shared\n * clickable/expandable row wrapper. Use when the stacked label/value grid\n * wastes space (feedback #317). Desktop is unaffected.\n *\n * It replaces the WHOLE body, `mobilePrimary` included: a column's\n * `mobilePrimary` cell is not drawn when `mobileCard` is set, so anything that\n * cell carried (keksdose's environment and rework chips, 0.27) must be drawn by\n * the card itself — as `FeedbackMobileCard` does.\n */\n mobileCard?: (row: T) => ReactNode;\n /**\n * A \"Sort by\" select and a direction button above the phone card list (keksdose\n * K10). The card list has no header row, so without it a phone user is stuck with\n * whatever order the table opened in. It drives the same sort state as the column\n * headers — `sorts`/`onSortsChange` when controlled, the URL and `storageKey` when\n * not — and offers the columns a header click could sort (those with a `sortBy`).\n * Each option is named by the column's `headerText`, else its string `header`.\n *\n * Off by default, so no existing phone layout gains a row. Strings:\n * {@link DataTableSortLabels}. Phones only; the desktop table is unchanged.\n */\n mobileSort?: boolean;\n /**\n * Swipe actions for a mobile row, revealed by dragging it horizontally. Return\n * `null` (or empty sides) for rows that should not move — a locked row, one whose\n * mutation is in flight, one the user has expanded.\n *\n * Prefer `start`/`end`: actions committed by dragging the row toward the reading\n * START or END of the line. In a left-to-right layout `end` is a drag to the\n * right, and in a right-to-left one it is a drag to the left — so a \"drag forward\n * to archive\" row stays forward in Arabic or Hebrew without a second definition.\n * `left`/`right` are the physical directions and keep meaning exactly that; each\n * is used only where the logical side for that direction is not given.\n *\n * Mobile only, and deliberately so: the desktop table already has room for an\n * actions column, and a drag gesture on a pointer device is a worse version of a\n * button. Wraps only the row body, so an expansion panel below stays put while the\n * row above it slides. The desktop's buttons for the same actions come with it — see\n * {@link DataTableProps.desktopSwipeActions}.\n */\n mobileSwipeActions?: (row: T) => MobileSwipeActions | null;\n /**\n * The desktop half of `mobileSwipeActions` (0.46, keksdose dev#598 — Marcel: every\n * table whose phone cards swipe gets a desktop actions column with the same actions):\n * an actions column DERIVED from the very plan the card swipes with, so the two\n * viewports offer the same functions and cannot drift. Each button runs the swipe's own\n * `onCommit` — the same confirm, the same undo toast. The reading-end side's actions\n * first, then the start side's, each once (by `SwipeAction.id`, else by label); a row\n * whose plan is `null` gets an empty cell. The column is hidden on the phone, where the\n * actions are the swipes, kept off the column settings, and its tooltips open toward\n * the row's start.\n *\n * On by default wherever `mobileSwipeActions` is given, so a new swipeable table cannot\n * forget it — except beside `rowActions`, whose column is the desktop's already (pass\n * `true` or options to add this one too). `false` where the desktop row carries every\n * one of those actions as its own buttons; say so at the call site. Options:\n * {@link DataTableDesktopSwipeActions}.\n */\n desktopSwipeActions?: boolean | DataTableDesktopSwipeActions<T>;\n /**\n * Renders as a full-width row above the data rows in the desktop table\n * (mobile has no equivalent list-row slot, so it's desktop-only). For a\n * toggle that shows/hides a slice of rows, style it as an expand/collapse\n * disclosure (chevron + label) so it reads as part of the table rather\n * than a floating control above it. See feedback #10/#11 (transactions'\n * \"hide upcoming/scheduled\" toggle).\n */\n leadingRow?: ReactNode;\n /**\n * 0.48 (keksdose dev #603: \"it should rather be sticky, so the user can show hide them\n * everywhere on click\"): pin the {@link leadingRow} under the sticky header while the\n * rows scroll beneath both — its `top` is the header's measured height. Desktop only,\n * as `leadingRow` is; it sticks within the table's own scroller (a bounded body:\n * `fillHeight` or `maxBodyHeight`). Default: it scrolls with the rows.\n */\n leadingRowSticky?: boolean;\n /**\n * 0.48 (keksdose dev #605): group the desktop table's rows under full-width bands, one\n * level or two (period → day). Each band is a row header spanning the visible columns\n * (selection column included, hidden and fit-hidden columns not) with a label and an\n * end slot (counts, totals), and opens its own `<tbody>`, so a screen reader announces\n * it as the reader enters the group; the period band opens the first day's. Rows must\n * already be ordered so equal keys are contiguous (as for `mobileGroupBy`); grouping\n * runs over the rows on screen, page by page.\n *\n * The bands are sticky: the period under the header (and under a sticky\n * {@link leadingRow}), the day under the period — measured offsets, inside the table's\n * own scroller (`fillHeight` or `maxBodyHeight`). The `leadingRow` stays above the first\n * group. On a phone pass the same level to `mobileGroupBy`:\n * `groupBy={[period, day]} mobileGroupBy={period}`. See {@link DataTableGroupBy}.\n */\n groupBy?: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?];\n /**\n * 0.48 (keksdose dev #605): a \"load more\" footer in place of the pager. The table\n * renders every row it is given (as `paginated={false}` does — pass that too, to say\n * so), and the footer asks the owner for the next slice: as it scrolls into view (an\n * IntersectionObserver, guarded against double calls while one is in flight) and with\n * a button the keyboard reaches, named by the app (\"Load August 2026\"). With\n * `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the\n * card list's last item. Since 0.48.1 it implies {@link serverRows} — the rows are\n * rendered in the order given, unfiltered — unless `serverRows={false}`. See\n * {@link DataTableLoadMore}.\n */\n loadMore?: DataTableLoadMore;\n /**\n * 0.48.2 (keksdose dev #632, Marcel: \"The table is extremely slow. Is it virtualised…?\"):\n * render only the desktop rows near the visible part of the scroller, plus `overscan`\n * rows each side ({@link DataTableVirtualize}), with spacer rows standing in for the\n * rest. For long lists — keksdose's register under `loadMore` holds 1000+ rows. Opt-in;\n * without it every row renders, as before.\n *\n * - **Scroller:** the bounded body (`fillHeight` or `maxBodyHeight`), or the page when the\n * body is unbounded (`maxBodyHeight=\"none\"`): the window is what of the table both the\n * scroller and the browser window show.\n * - **Kept rendered:** the header, the `leadingRow`, the group bands (sticky and stacked\n * as before; a period's band while any of its rows is in view), every OPEN row (so an\n * inline editor in `expandedRow` keeps its state), the roving tab stop and a row\n * holding the focus. The `loadMore` footer follows the last spacer and loads once.\n * - **Heights:** each rendered row is measured and remembered by its key; a row not yet\n * seen counts as the measured average (`estimateRowHeight` before any). The scroll\n * height settles as the rows are seen; the browser's scroll anchoring holds the rows in\n * view while a spacer above is corrected (Safari has none: a jump of the difference\n * is possible there on the first pass).\n * - **Columns:** the header widths are measured once and locked in a `<colgroup>`, so the\n * columns do not move as rows come and go; a wider cell scrolling in widens its column\n * (once). Re-measured when the rows, the columns, a width or the scroller's width\n * change. A `grow` column and a resized one are not locked.\n * - **Keyboard and screen reader:** Arrow/Home/End move over every row — the target is\n * rendered, then focused (End reaches the last row); Shift+click ranges and select-all\n * work on the data, not the DOM. The table has `aria-rowcount`, every rendered row its\n * `aria-rowindex` (bands, details and the totals row counted); spacers are\n * `aria-hidden`.\n * - **Trade-offs:** the browser's find-in-page sees only the rendered rows, and so does\n * print; an `id` from `rowAttributes` (an anchor, an `aria-controls` target, a\n * `scrollIntoView` on a fresh row) exists only while its row is rendered. Memoise\n * `columns` (and `mobileCard`): the rows bail out of a render only then, and a scroll\n * renders the rows that come in, not the whole window.\n * - **Phone:** desktop only. The card list renders every row it holds (`loadMore`,\n * `paginated={false}`) or reveals them in chunks as it is scrolled (client paging).\n */\n virtualize?: boolean | DataTableVirtualize;\n /**\n * Row height. `\"compact\"` is lenkbank's in-card tables: five `text-xs` tables with\n * `px-2 py-1` cells sitting inside cards, which at the default padding came out at\n * twice their height and pushed the card's own content below the fold — the whole\n * reason they were still hand-rolled `<table>`s. It tightens the header, the cells,\n * the selection checkboxes, the pager and the phone cards together, so a compact\n * table is compact everywhere rather than a dense body under a roomy header.\n *\n * Defaults to `\"comfortable\"`, which is the table every caller has today.\n */\n density?: DataTableDensity;\n /**\n * What a repeated click on a sorted header does. `\"tri\"` (the default, and the only\n * behaviour before 0.8.0): first direction → the other → unsorted. `\"toggle\"`: first\n * direction ⇄ the other, never unsorted — for a report table that is always ranked\n * by something. Each column's first direction is its own `firstSort`. `aria-sort`\n * and the spoken \"Sorted by …\" follow whatever the click produced, so they are\n * right under either cycle.\n */\n sortCycle?: SortCycle;\n /**\n * The desktop table's power-user chrome, as one switch. `\"full\"` (default) is the\n * table as it has always been. `\"minimal\"` is a compact report table in a half-width\n * card: no column-settings rail, no resize handles, no multi-sort (and so no\n * \"Shift-click to add a sort\" tooltip) — the three pieces that cost width and\n * attention and that a ten-row summary never needs. Each can still be set on its\n * own below; an explicit boolean wins over the preset.\n */\n chrome?: DataTableChrome;\n /**\n * The vertical \"Columns (n/m)\" rail and its show/hide + auto-size panel. Default:\n * on, unless `chrome=\"minimal\"`. With it off, every column is shown and a hidden\n * set persisted under `storageKey` is ignored — there would be no way to get a\n * column back.\n */\n columnSettings?: boolean;\n /**\n * The drag handles on the header's trailing edges. Default: on, unless\n * `chrome=\"minimal\"`. With it off, widths persisted under `storageKey` are ignored\n * for the same reason as above: nothing could reset them.\n */\n resizable?: boolean;\n /**\n * Shift-click adds a secondary sort criterion. Default: on, unless\n * `chrome=\"minimal\"`. With it off, a shift-click sorts like a plain click and the\n * header drops the tooltip that advertises the gesture.\n */\n multiSort?: boolean;\n /**\n * The card around the table — border, surface, radius, shadow. Set false to sit the\n * table inside the app's own card, under its own caption and CSV button, without a\n * frame inside a frame. The table's layout is unchanged; only the chrome goes.\n * Default true.\n */\n frame?: boolean;\n /**\n * Fade out an edge of the desktop table's scroller while columns are hidden behind\n * it (0.25) — the kit {@link Table}'s `edgeFade`, on the scroller this table draws\n * itself (DataTable renders its own `<table>`, not through Table, so the prop is\n * its own too). Measured: nothing is drawn while every column fits; the end with\n * columns behind it fades, both ends once scrolled into the middle, the reading\n * direction's ends in RTL. A `mask-image`, so no layout moves and no colour is\n * assumed; never animated. The scroller's own scrollbars stay out of it — its\n * vertical one sits at the inline end, in the very band that fades. Focus landing\n * in a cell behind the fade (a row action, a link, the selection box) is scrolled\n * clear of it. The scroller carries `data-overflow` (`\"start\"`, `\"end\"`, `\"both\"`).\n *\n * The phone layout is a card list that never scrolls sideways; it is unaffected.\n *\n * Off by default. It is visual only, but not neutral: on the bump it would change\n * how every overflowing desktop table in an app looks, and the mask covers\n * everything the scroller paints — the sticky header and totals rows, an app's own\n * marks at the scroller's edges. Opt in per table, or app-wide from your own wrapper.\n */\n edgeFade?: boolean;\n /** Extra classes for the table's root element (the card, or with `frame={false}`\n * the plain wrapper). Merged last, so a caller's margin or width wins. */\n className?: string;\n /**\n * Per-row actions: an actions column of icon buttons at the row's end on desktop,\n * and the same actions as swipe actions on a phone card (where the column is not\n * shown; with `mobileDetailsInRow` also as buttons in the opened row). At Large and\n * Extra large two or more collapse into one \"⋯\" menu with each action's words\n * (0.32, §10.8, `RowActions`) — name the rows with `rowName` so each menu button is\n * \"Actions for …\". kastlan's `ResourceListPage` appended this column by hand to every CRUD\n * list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a\n * `stopPropagation` on each so a press did not also open the row.\n *\n * `kind: \"edit\"` / `\"delete\"` bring the icon, the label (`labels.edit` /\n * `labels.delete`), the tone and the swipe side; any other action states its own\n * `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's\n * `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.\n *\n * const confirm = useConfirm();\n * rowActions={[\n * { kind: \"edit\", onAction: openEditor },\n * { kind: \"delete\", onAction: (r) => remove(r.id),\n * confirm: (r) => confirm({ title: t(\"delete\", { name: r.name }), tone: \"danger\" }) },\n * ]}\n *\n * A function receives the row, for actions that differ per row. An explicit\n * `mobileSwipeActions` wins over the swipes derived from here.\n */\n rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);\n /**\n * The `rowActions` column's heading (0.46.1, keksdose dev#598 rework — Marcel: a\n * VISIBLE \"Actions\" heading on every table with an actions column). Default: the\n * provider's `dataTable.actions` (\"Actions\"), shown, end-aligned over the buttons. A\n * node replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader\n * only.\n */\n rowActionsHeader?: ReactNode | false;\n /**\n * The bar above the table — a search field, filter chips, a create button. Inside\n * the table's frame, above the header, on desktop and phone alike (resource-list\n * pages set a search box in a `div` above the card, outside the frame it filters).\n */\n toolbar?: ReactNode;\n /**\n * The totals row's name — \"Total\", \"Summe\", \"Totals as of 30 June\". It is the row's\n * header: rendered as a `<th scope=\"row\">` in the first visible column that has no\n * `footer` of its own (falling back to the first visible column if every one has a\n * value), so a screen reader reads \"Total, Debit, 12 400.00\" for the cell beside it.\n * On a phone it heads the summary card, which is also named by it.\n *\n * There is no built-in default word: a new kit label key is a silent English string\n * in every translated app (see `missingKitLabels`), and the caller has the\n * word in its own catalogue already. Without it the row is unlabelled on screen —\n * pass one.\n */\n footerLabel?: ReactNode;\n /**\n * Show the totals row. Default: shown when any column declares a `footer`. Set\n * false to drop it without touching the column definitions (a \"show totals\"\n * switch). The row is also left out while there are no rows to total — a row of\n * zeroes under \"No results\" says nothing the empty state has not.\n */\n footer?: boolean;\n}\n\n/** One entry of {@link DataTableProps.rowActions}. */\nexport interface DataTableRowAction<T> {\n /** A preset: `\"edit\"` (pencil, `labels.edit`, swipes toward the END) or `\"delete\"`\n * (bin, `labels.delete`, danger tone, swipes toward the START). */\n kind?: \"edit\" | \"delete\";\n /** The action's name — its tooltip, accessible name and swipe label. Required\n * without a `kind`. */\n label?: string;\n /** The glyph. Required without a `kind`. */\n icon?: ReactNode;\n /** Runs after `confirm` (when given) has said yes. */\n onAction: (row: T) => void;\n /** Asked before `onAction`; a `false` (or a promise of one) cancels. */\n confirm?: (row: T) => boolean | Promise<boolean>;\n /** Default: `\"danger\"` for `kind: \"delete\"`, else `\"default\"`. */\n tone?: \"default\" | \"danger\";\n /** Leave the action out for this row. */\n hidden?: (row: T) => boolean;\n /** Why the action is locked for this row — the button stays focusable and says so,\n * and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */\n disabledReason?: (row: T) => ReactNode;\n /** Which way the phone card is dragged to commit it, or `false` for no swipe.\n * Default: `\"start\"` for a danger action, `\"end\"` otherwise. */\n swipe?: \"start\" | \"end\" | false;\n}\n\n/** See {@link DataTableProps.chrome}. */\nexport type DataTableChrome = \"full\" | \"minimal\";\n\n/** See {@link DataTableProps.density}. */\nexport type DataTableDensity = \"comfortable\" | \"compact\";\n\nexport type FilterState = Record<string, FilterValue>;\nexport type { SortState };\n\n/** What {@link DataTableProps.mobileSwipeActions} returns for one row. */\nexport interface MobileSwipeActions {\n /** Committed by dragging toward the reading start (left in LTR, right in RTL). */\n start?: SwipeAction[];\n /** Committed by dragging toward the reading end (right in LTR, left in RTL). */\n end?: SwipeAction[];\n /** Committed by dragging physically LEFT, whatever the direction. */\n left?: SwipeAction[];\n /** Committed by dragging physically RIGHT, whatever the direction. */\n right?: SwipeAction[];\n}\n\n/** How {@link DataTableProps.desktopSwipeActions} draws the column (0.46, dev#598). */\nexport interface DataTableDesktopSwipeActions<T> {\n /** Leave an action out because the row already has its own button for it — keksdose's\n * feedback inbox, whose status cell offers `advance` and `wont_do`\n * (`feedbackSwipeOmitShown`). Match on `action.id` rather than the translated label. */\n omit?: (action: SwipeAction, row: T) => boolean;\n /** The row's name for the \"⋯\" menu (\"Actions for …\"). Default: the table's `rowName`. */\n name?: (row: T) => string | null | undefined;\n /** {@link RowActionsCollapse}; default `\"auto\"`. A table whose rows offer more than two\n * actions passes `\"menu\"`, so the column does not switch between icons and a menu. */\n collapse?: RowActionsCollapse;\n /**\n * Whether an action COMMITS, refused with the reason under a locked\n * `WriteLockProvider` ({@link CommitScope}) — a swipe has no such state, so a plan\n * usually returns nothing for a locked row and the desktop shows nothing either; with\n * this the plan can stay and the desktop says why. Per action as a function. Default:\n * no commit scope (the plan decides).\n */\n commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);\n /**\n * The column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE \"Actions\"\n * heading on every table with an actions column). Default: the provider's\n * `dataTable.actions` (\"Actions\"), shown, end-aligned over the buttons. A node\n * replaces it; `false` keeps the 0.46.0 heading, named for a screen reader only.\n */\n header?: ReactNode | false;\n}\n\n/**\n * A totals-row cell: pinned to the scroller's bottom edge, on the header's surface.\n * The rule above it is an inset shadow rather than a `border-t`: the table collapses\n * its borders, and a collapsed border belongs to the TABLE, not the cell, so it stays\n * behind at the bottom of the rows while the sticky cell rides over them.\n */\nconst FOOTER_CELL =\n \"sticky bottom-0 z-10 bg-surface-2 shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n// ---------- Phone sort control (keksdose K10) ----------\n\n/**\n * The phone sort control's strings (keksdose K10). They are `dataTable` strings —\n * read from the table's resolved labels, so the `labels` prop and the provider's\n * `dataTable` namespace reach them — and are declared here, with the table's other\n * public types (the control itself, `MobileSort`, is in data-table-mobile.tsx), with\n * English defaults until `DataTableLabels` declares them itself.\n */\nexport interface DataTableSortLabels {\n /** The control's visible label: \"Sort by\". */\n sortBy: string;\n /** The \"no sort\" option — the rows in the order they were given: \"Default order\". */\n sortDefault: string;\n /** The direction button's name while the sort ascends: \"Ascending\". */\n sortAscending: string;\n /** …and while it descends: \"Descending\". */\n sortDescending: string;\n}\n\n/** The phone sort control's words, as they sit in {@link DEFAULT_DATA_TABLE_LABELS}\n * (DataTableLabels extends DataTableSortLabels; one source for the English). */\nexport const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {\n sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,\n sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,\n sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,\n sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending,\n};\n\nconst flipDir = (d: SortDir): SortDir => (d === \"asc\" ? \"desc\" : \"asc\");\n\n// `virtualize`'s constants (0.48.2): the band levels a group's `<tbody>` opens with.\nconst NO_BANDS: readonly (1 | 2)[] = [];\nconst BAND_1: readonly (1 | 2)[] = [1];\nconst BAND_2: readonly (1 | 2)[] = [2];\nconst BAND_1_2: readonly (1 | 2)[] = [1, 2];\nconst NO_VIRTUAL_OPTIONS: DataTableVirtualize = {};\n\n// `moveRowFocus` (the roving rows' Arrow/Home/End) lives with the row in data-table-row.tsx.\n\n// ---------- Main DataTable ----------\n\n\n\nimport { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from \"./use-table-state\";\nimport { useMobileReveal } from \"./use-mobile-reveal\";\nimport { GROW_CELL_CLASS, GROW_PEER_CELL_CLASS, useColumnFit } from \"./data-table-fit\";\n\nexport function DataTable<T>({\n rows,\n columns: columnsProp,\n rowKey,\n defaultPageSize = 25,\n onRowClick,\n rowHref,\n expandedRow,\n isExpanded,\n rowClassName,\n rowAttributes,\n paginated = true,\n empty,\n storageKey,\n urlSync = false,\n fillHeight = false,\n maxBodyHeight = \"calc(100dvh - 12rem)\",\n serverPagination,\n serverRows: serverRowsProp,\n filters: filtersProp,\n onFiltersChange,\n sorts: sortsProp,\n onSortsChange,\n pageSize: pageSizeProp,\n onPageSizeChange,\n selection,\n labels: labelsProp,\n locale: localeProp,\n storageKeyPrefix: storageKeyPrefixProp,\n mobileExpandAsDialog = false,\n mobileDetailsInRow = false,\n rowName,\n mobileDialogBackCloses = true,\n mobileGroupBy,\n mobileGroupLabel,\n mobileCard,\n mobileSort = false,\n mobileSwipeActions,\n desktopSwipeActions,\n leadingRow,\n leadingRowSticky = false,\n groupBy,\n loadMore,\n virtualize = false,\n density = \"comfortable\",\n sortCycle = \"tri\",\n chrome = \"full\",\n columnSettings: columnSettingsProp,\n resizable: resizableProp,\n multiSort: multiSortProp,\n frame = true,\n edgeFade = false,\n className,\n rowActions,\n rowActionsHeader,\n toolbar,\n footerLabel,\n footer: footerProp,\n}: DataTableProps<T>) {\n const compact = density === \"compact\";\n const minimal = chrome === \"minimal\";\n const columnSettings = columnSettingsProp ?? !minimal;\n const resizable = resizableProp ?? !minimal;\n const multiSort = multiSortProp ?? !minimal;\n const isServer = !!serverPagination;\n // The server filtered and sorted these rows (0.48.1, K-052-1): no client filter, sort\n // or paging, and the sort/filter UI only reports to the owner. `isServer` keeps\n // meaning \"the server pages\" (its pager, its page and page size).\n const serverRows = isServer || (serverRowsProp ?? !!loadMore);\n const isLoading = !!serverPagination?.isLoading;\n // prop > provider > English, through the table's own resolver rather than\n // `useKitLabels`: that resolver derives `columnsCount` from a translated `columns`,\n // and a plain merge would skip it. `presets` is a record and is merged key by key,\n // as the provider merges it, so a call site renaming one preset keeps the provider's\n // other ten. Memoised because the announcement effects below list `labels` as a\n // dependency, and a fresh object every render would re-run them for nothing.\n const labelOverrides = useKitLabelOverrides(\"dataTable\");\n // prop > provider > the kit's default, the order every kit setting resolves in (round\n // 0.34 C5, KS U4): one prefix on the provider reaches the tables an app cannot pass a\n // prop to, `TranslationReviewPanel`'s among them.\n const providerStorageKeyPrefix = useKitTableStorageKeyPrefix();\n const storageKeyPrefix = storageKeyPrefixProp ?? providerStorageKeyPrefix ?? DEFAULT_PERSIST_PREFIX;\n const labels = useMemo(() => resolveDataTableLabels(labelOverrides, labelsProp), [labelOverrides, labelsProp]);\n const locale = useKitLocale(localeProp);\n\n // ---- Row actions ---- (data-table-row-actions.tsx)\n // The actions column joins the caller's columns; on a phone the same actions are the\n // card's swipes, unless the caller's own `mobileSwipeActions` says otherwise.\n const { columns, swipesFromActions } = useRowActionColumn({\n columns: columnsProp,\n rowActions,\n rowActionsHeader,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n });\n const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);\n\n // Sorting, filtering and paging all change WHICH rows are on screen without\n // moving focus — the header button the user pressed is still the header button\n // they are on — so there is no other channel to say it on. See use-announce.ts.\n const { announce, regionProps } = useAnnounce();\n const {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths: storedWidths,\n setWidths,\n hiddenCols: storedHiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n } = useTableState({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync,\n defaultPageSize,\n sortsProp,\n filtersProp,\n // Ignored in `serverPagination` mode, where the owner's is the page size.\n pageSizeProp: isServer ? undefined : pageSizeProp,\n });\n\n // With the controls that change them switched off, persisted widths and hidden\n // columns are ignored rather than applied: a table cannot be left in a state the\n // user has no control to get out of. They stay in storage untouched, so turning\n // the control back on brings them back.\n // A `grow` column takes the rest of the row by design (0.48): a width stored for it\n // (a drag or \"Auto-size columns\" before it grew, KS N2) is ignored, and it has no\n // handle, so it can never pin it again. Ignored rather than deleted from storage, as\n // the widths above; the next resize or auto-size writes a blob without it.\n const widths = useMemo(() => {\n if (!resizable) return {};\n const grow = columns.find((c) => c.grow)?.key;\n if (!grow || !(grow in storedWidths)) return storedWidths;\n const { [grow]: _ignored, ...rest } = storedWidths;\n return rest;\n }, [resizable, storedWidths, columns]);\n const hiddenCols = useMemo(\n () => (columnSettings ? storedHiddenCols : new Set<string>()),\n [columnSettings, storedHiddenCols],\n );\n\n // Every call site builds `columns` inline — `columns={[{ key: \"name\", … }]}` right\n // there in the JSX — so the array is a new object on every render of the page around\n // the table, and a `useMemo` keyed on its identity memoises nothing. The three below\n // walk all the rows; a sibling field being typed into re-filtered and re-sorted the\n // whole dataset on every keystroke.\n //\n // What they actually need from `columns` is WHICH columns there are, so that is what\n // they are keyed on. The trade-off is deliberate and narrow: swapping a column's\n // `sortBy`/`filter`/`filterBy` FUNCTION while its `key` stays the same no longer\n // invalidates on its own. A rule that changes is a different column — give it a\n // different key — and anything the rule reads (rows, filters, sorts) is a dependency\n // in its own right.\n const columnSig = columns.map((c) => c.key).join(\"\\u0000\");\n // The select filters' declared option lists are the one part of a column definition\n // the memo below reads as DATA rather than as a rule, and a caller may recompute\n // them (a lookup that arrived, a saved view applied) without the column set moving.\n const selectOptionsSig = columns\n .map((c) => {\n const f = resolveFilter(c);\n if (!f || f.type !== \"select\" || !f.options) return \"\";\n return `${c.key}:${f.options.map((o) => `${o.value}=${o.label ?? \"\"}`).join(\",\")}`;\n })\n .join(\"\\u0000\");\n\n const selectOptionsByKey = useMemo(() => {\n const map: Record<string, { value: string; label: string }[]> = {};\n for (const col of columns) {\n const f = resolveFilter(col);\n if (!f || f.type !== \"select\") continue;\n if (f.options && f.options.length > 0) {\n map[col.key] = f.options.map((o) => ({ value: o.value, label: o.label ?? o.value }));\n continue;\n }\n const seen = new Set<string>();\n for (const row of rows) {\n const v = f.getValue(row);\n if (v && !seen.has(v)) seen.add(v);\n }\n map[col.key] = Array.from(seen)\n .sort()\n .map((v) => ({ value: v, label: v }));\n }\n return map;\n // `columns` by signature — see the note above the signatures.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [columnSig, selectOptionsSig, rows]);\n\n const filtered = useMemo(() => {\n if (serverRows) return rows;\n let result = rows;\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n result = result.filter((row) => rowMatches(col, row, state, locale));\n }\n return result;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rows, columnSig, filters, serverRows, locale]);\n\n const sorted = useMemo(() => {\n if (serverRows) return filtered;\n // Priority chain: the first sort key that distinguishes two rows wins;\n // nulls sort last for that key regardless of direction (as before).\n const chain = sorts\n .map((s) => {\n const col = columns.find((c) => c.key === s.key);\n return col?.sortBy ? { sortBy: col.sortBy, dir: s.dir } : null;\n })\n .filter((c): c is { sortBy: (row: T) => string | number | null | undefined; dir: \"asc\" | \"desc\" } => c !== null);\n if (!chain.length) return filtered;\n const copy = [...filtered];\n copy.sort((a, b) => {\n for (const { sortBy, dir } of chain) {\n const av = sortBy(a);\n const bv = sortBy(b);\n if (av == null && bv == null) continue;\n if (av == null) return 1;\n if (bv == null) return -1;\n if (av < bv) return dir === \"asc\" ? -1 : 1;\n if (av > bv) return dir === \"asc\" ? 1 : -1;\n }\n return 0;\n });\n return copy;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [filtered, sorts, columnSig, serverRows]);\n\n // `paginated={false}` is expressed as \"page size = everything\" rather than as a\n // second code path, so slicing, the page clamp and the range summary all keep\n // exactly one definition.\n const unpaged = !isServer && (!paginated || !!loadMore || serverRows);\n const effectivePageSize = isServer\n ? serverPagination!.pageSize\n : unpaged || pageSize === Infinity\n ? sorted.length || 1\n : pageSize;\n const paginationTotal = isServer ? serverPagination!.total : sorted.length;\n const paginationPageSize = isServer ? serverPagination!.pageSize : pageSize;\n const totalPages = isServer\n ? Math.max(1, Math.ceil(serverPagination!.total / Math.max(1, serverPagination!.pageSize)))\n : Math.max(1, Math.ceil(sorted.length / effectivePageSize));\n const safePage = isServer\n ? Math.min(Math.max(0, serverPagination!.page), totalPages - 1)\n : Math.min(page, totalPages - 1);\n const slice =\n isServer || unpaged || pageSize === Infinity\n ? sorted\n : sorted.slice(safePage * effectivePageSize, safePage * effectivePageSize + effectivePageSize);\n\n const toggleSort = (key: string, shiftKey: boolean) => {\n const col = columns.find((c) => c.key === key);\n // The cycle is the table's, the first direction the column's. The announcement\n // below reads the RESULT, so it can never describe a step the cycle did not take.\n const next = nextSorts(sorts, key, multiSort && shiftKey, {\n firstDir: col?.firstSort,\n cycle: sortCycle,\n });\n commitSorts(next, key);\n };\n\n // Header clicks and the phone sort control (`mobileSort`) land here alike, so both\n // go through the same owner and say the same thing.\n const commitSorts = (next: SortState[], key: string) => {\n const col = columns.find((c) => c.key === key);\n if (onSortsChange) onSortsChange(next);\n else setInternalSorts(next);\n\n // Announce the column the user just pressed, not `next[0]`: a shift-click adds a\n // criterion BEHIND the existing one, and answering \"Sorted by Date\" to a click on\n // Name misreports what the click did. The third click drops the column entirely,\n // which is the state a user is most likely to have reached by accident.\n const name = col ? columnLabel(col, labels) : key;\n const entry = next.find((s) => s.key === key);\n announce(\n !entry\n ? labels.sortCleared(name)\n : entry.dir === \"asc\"\n ? labels.sortedAscending(name)\n : labels.sortedDescending(name),\n );\n };\n\n const setFilterValue = (key: string, value: FilterValue) => {\n if (onFiltersChange) {\n onFiltersChange({ ...filters, [key]: value });\n // No `setPage(0)` here: the page resets when the new value comes back in\n // through the prop — see the controlled reset in useTableState. Resetting now\n // would move to page 1 for a change the owner may yet refuse.\n return;\n }\n setInternalFilters((s) => ({ ...s, [key]: value }));\n setPage(0);\n };\n\n const clearFilter = (key: string) => {\n if (onFiltersChange) {\n const rest = { ...filters };\n delete rest[key];\n onFiltersChange(rest);\n return;\n }\n setInternalFilters((s) => {\n const rest = { ...s };\n delete rest[key];\n return rest;\n });\n setPage(0);\n };\n\n // Clear every column filter at once (drives the mobile filter sheet's\n // \"clear all\"). Only touches keys the table actually exposes as filters.\n const clearAllFilters = () => {\n const keys = columns\n .filter((col) => ((!serverRows || onFiltersChange) ? resolveFilter(col) : null))\n .map((col) => col.key);\n if (!keys.length) return;\n if (onFiltersChange) {\n const rest = { ...filters };\n for (const k of keys) delete rest[k];\n onFiltersChange(rest);\n return;\n }\n setInternalFilters((s) => {\n const rest = { ...s };\n for (const k of keys) delete rest[k];\n return rest;\n });\n setPage(0);\n };\n\n // The result count only exists AFTER the table has re-filtered, so the sentence is\n // spoken from the render that produced the new number rather than from the handler\n // that changed the filter. Keyed on a serialised copy of the filter state, for two\n // reasons: a controlled `filters` prop arrives with a fresh identity on every\n // parent render, and a change the OWNER makes (a saved view applied, a filter chip\n // dismissed outside the table) moves the same rows and deserves the same sentence.\n const filterKey = useMemo(() => JSON.stringify(filters), [filters]);\n // Seeded with the mount value: a live region that speaks on first paint talks over\n // the page the user has just opened, and every table on that page does it at once.\n const announcedFilterKey = useRef(filterKey);\n useEffect(() => {\n if (announcedFilterKey.current === filterKey) return;\n announcedFilterKey.current = filterKey;\n // A server-paginated table does not do the filtering: the matching rows arrive on\n // some later render from the caller, and `serverPagination.total` is ALREADY the\n // filtered total — so the \"n of m\" sentence has no second number to give here.\n // Saying nothing is better than announcing the page size as a result count.\n if (serverRows) return;\n announce(labels.filterResults(filtered.length, rows.length));\n }, [filterKey, filtered.length, rows.length, serverRows, announce, labels]);\n\n // 1-based, because that is what the page strip shows and what a person says.\n const goToPage = (p: number) => {\n if (isServer) serverPagination!.onPageChange(p);\n else setPage(p);\n announce(labels.pageChanged(p + 1, totalPages));\n };\n\n // 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out\n // (data-table-fit.ts). Desktop only; the scroller is read after render.\n // Large and Extra large: read here, as the fit re-measures when it changes.\n const large = useLargeText();\n const { autoHidden, keep: keepColumn } = useColumnFit({\n columns,\n hiddenCols,\n headRefs,\n scroller: () => desktopScroller.current,\n enabled: !usePhoneLayout(),\n // Measured only when one of these changed (0.48.2): not on a ticked box.\n content: [slice, columns, hiddenCols, widths, density, large],\n });\n const visibleColumns = useMemo(\n () => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),\n [columns, hiddenCols, autoHidden],\n );\n // With a `grow` column drawn, every other cell keeps one line at Normal (kastlan S2,\n // 0.48): the `w-full` column squeezed them to their min-content width.\n const growDrawn = visibleColumns.some((c) => c.grow);\n\n // Which cell carries the row's link when `rowHref` is set (feedback #451). The\n // `mobilePrimary` column is the row's headline by declaration, so it is both the\n // widest target and the one the pointer is already over; falls back to the first\n // column, and falls back again if the user has hidden the primary one from the\n // column panel. Resolved from `visibleColumns` for exactly that reason — off a\n // raw `columns` lookup a hidden headline would leave the row with no link at all.\n //\n // `noRowLink` columns are skipped in BOTH steps: wrapping a cell that owns an\n // anchor nests two links and disables the inner one (see the prop's note). If\n // nothing visible is left to wrap, the row gets NO anchor — an anchor over the\n // one cell there is would cancel that cell's own click and hand back nothing,\n // and a row without a link still behaves exactly as it did before #451.\n const linkColumn = rowHref\n ? (visibleColumns.find((c) => c.mobilePrimary && !c.noRowLink) ??\n visibleColumns.find((c) => !c.noRowLink))\n : undefined;\n\n // ---- Column resize ---- (data-table-resize.ts: pointer capture, one `setWidths` per\n // drag, and the page's cursor and selection handed back on an unmount mid-drag)\n const { startResize, moveResize, finishResize, autoSizeAll } = useColumnResize({\n headRefs,\n widths,\n setWidths,\n columns,\n // The columns left out for width have no header to measure either.\n hiddenCols: autoHidden.size > 0 ? new Set([...hiddenCols, ...autoHidden]) : hiddenCols,\n });\n\n const visibleCount = visibleColumns.length;\n const totalCount = columns.length;\n // Spans the full row width including the optional leading selection column.\n const totalColSpan = visibleCount + (selection ? 1 : 0);\n\n // ---- Totals row ----\n // `sorted`, not `slice`: the totals are of everything the filters let through, so\n // paging does not move them — see `DataTableColumn.footer`. In server mode `sorted`\n // IS the page (the table holds nothing else), which the prop's note spells out.\n const hasFooterColumns = columns.some((c) => c.footer);\n const showFooter = footerProp !== false && hasFooterColumns && sorted.length > 0;\n // The row header sits in the first visible column that has nothing of its own to\n // show — so hiding the \"Account\" column moves \"Total\" into \"Name\" rather than\n // dropping it — and on the first visible column when every one carries a value.\n const footerLabelCol = showFooter\n ? (visibleColumns.find((c) => !c.footer && !isTableOwnColumn(c.key)) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // Each open row's detail row, for its disclosure button's `aria-controls`.\n const detailIdBase = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const ownColumns = columns.filter((c) => isTableOwnColumn(c.key)).length;\n const ownVisible = visibleColumns.filter((c) => isTableOwnColumn(c.key)).length;\n const columnsCountLabel = labels.columnsCount(visibleCount - ownVisible, totalCount - ownColumns);\n // Match the `md:` breakpoint at the text size in force: we render either the table or\n // the card list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin\n // `useBreakpoint(\"md\")`: the same query an app's own phone checks ask (keksdose's\n // sticky editor footer), so the card list and the full-screen row dialog switch with\n // them and never a frame apart (§10.4 \"usePhoneLayout() is the single answer\"; 0.32.1,\n // keksdose's 0.32 report, where `md` and `max-md` both missed at 767.5 px). Without\n // `matchMedia` (SSR, jsdom) it is the table, as before.\n const isMdUp = !usePhoneLayout();\n // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.\n const desktopScroller = useRef<HTMLDivElement | null>(null);\n // 0.47 (keksdose S4): the scroller's visible width, as `--dt-visible-w` on it, for the\n // expansion row's pinned content (below). Set on the element, not in state: a resize\n // re-renders nothing. Only a table that can expand a row measures.\n // The group bands and the load-more footer pin their content the same way (0.48).\n const levels = groupLevels(groupBy);\n const measuresVisibleWidth = isMdUp && (!!expandedRow || levels.length > 0 || !!loadMore);\n useEffect(() => {\n const el = desktopScroller.current;\n if (!measuresVisibleWidth || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => el.style.setProperty(\"--dt-visible-w\", `${el.clientWidth}px`));\n ro.observe(el);\n return () => ro.disconnect();\n }, [measuresVisibleWidth]);\n const edgeFadeProps = useEdgeFade(desktopScroller, edgeFade && isMdUp);\n\n // ---- Mobile card list (md:hidden) ----\n // Renders the same paged/filtered/sorted slice but as stacked cards instead\n // of a horizontally-scrolling table. Column settings stay desktop-only; the\n // filters come through a bottom sheet (feedback #299) and the sort through the\n // opt-in `mobileSort` row (keksdose K10), both on the table's own state.\n // Memoised on `visibleColumns`: the cards compare their columns by identity (0.48.2).\n const mobileColumns = useMemo(() => visibleColumns.filter((c) => !c.mobileHidden), [visibleColumns]);\n const mobilePrimaryCol =\n mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;\n const mobileSecondaryColumns = useMemo(\n () => mobileColumns.filter((c) => c !== mobilePrimaryCol),\n [mobileColumns, mobilePrimaryCol],\n );\n // The columns the card leaves out, for the opened row (`mobileDetailsInRow`).\n const mobileDetailColumns = mobileDetailsInRow ? visibleColumns.filter((c) => c.mobileHidden) : [];\n // Large and Extra large (§4 \"DataTable phone card\"): the card's labels go ABOVE their\n // values, nothing in it truncates, and the rows get more room. Most of it is a\n // `large:` class; the label/value grid needs a different STRUCTURE (each pair in a\n // box of its own), which a class cannot write. (`large` is read above, by the fit.)\n // What an opened phone row shows: the card's left-out columns first (when asked\n // for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.\n const opensRow = !!expandedRow || mobileDetailColumns.length > 0;\n const openedRowContent = (row: T): ReactNode => {\n const detail = expandedRow?.(row) ?? null;\n if (mobileDetailColumns.length === 0) return detail;\n return (\n <div data-row-details=\"\" className={cn(\"flex flex-col\", compact ? \"gap-2\" : \"gap-3\")}>\n <StackedFields columns={mobileDetailColumns} row={row} compact={compact} />\n {detail}\n </div>\n );\n };\n // The phone card IS the anchor, so unlike the desktop table there is no other\n // cell to move the row link into: if any cell the card renders owns a link, the\n // card itself cannot be one (feedback #451 — same nesting rule as `linkColumn`\n // above). It then falls back to the pre-#451 role=\"button\" card, which still\n // opens the row and leaves the cell's own link working. A caller's `mobileCard`\n // renderer is opaque to us; a link inside one is the caller's to declare by\n // marking the column it comes from.\n const mobileCardLinkable = mobileColumns.every((c) => !c.noRowLink);\n // The card list's reading direction, for the one thing on it CSS cannot flip: which\n // physical drag a logical `start`/`end` swipe action means. Read off the DOM through\n // a ref callback (it runs at commit, like a layout effect) because `dir` is an\n // attribute of some ancestor the table does not own; LTR until the first commit,\n // which is also before any finger can have touched a row.\n const [mobileDir, setMobileDir] = useState<Direction>(\"ltr\");\n const mobileRootRef = (el: HTMLElement | null) => {\n if (!el) return;\n const d = dirOf(el);\n if (d !== mobileDir) setMobileDir(d);\n };\n\n // `fillHeight` on a phone (0.40, keksdose live #420): the card list is a scroller only\n // while its parent bounds it — its rows overflow the flex room it was given. Under\n // AppShell's phone layout the DOCUMENT scrolls, the list's room is its own content,\n // and a scroller there (with `overscroll-contain`) only swallowed the page's scroll.\n // Measured before paint on every render (rows revealed, filtered, opened), and on a\n // resize of the list or its parent; one read of two heights.\n const phoneFill = fillHeight && !isMdUp;\n const phoneListRef = useRef<HTMLUListElement | null>(null);\n const [phoneListScrolls, setPhoneListScrolls] = useState(false);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose: what overflows is the rows, and React bails out on an unchanged boolean\n useLayoutEffect(() => {\n const el = phoneListRef.current;\n if (!phoneFill || !el) return;\n setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1);\n });\n useEffect(() => {\n const el = phoneListRef.current;\n if (!phoneFill || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1));\n ro.observe(el);\n if (el.parentElement) ro.observe(el.parentElement);\n return () => ro.disconnect();\n }, [phoneFill]);\n\n const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // ---- Phone selection mode (0.40, keksdose live #421) ----\n // The cards had no boxes, so `selection`'s bulk actions were unreachable on a phone.\n // A long press on a card (kk's register gesture) — or \"Select rows\", the keyboard's\n // and screen reader's way — enters the mode: every card shows its box and a tap\n // toggles it. The mode lasts while anything is selected, so emptying the selection\n // (the bulk bar's Clear, the last box unticked, Cancel) leaves it, as kk's does.\n const anySelected =\n !!selection && (selection.someSelected || (selection.allSelected && slice.length > 0));\n const [phoneSelectMode, setPhoneSelectMode] = useState(false);\n const [hadSelection, setHadSelection] = useState(anySelected);\n if (anySelected !== hadSelection) {\n // Adjusted while rendering, so the frame the selection empties is already out of\n // the mode (an effect would paint a frame of boxes first).\n setHadSelection(anySelected);\n if (!anySelected) setPhoneSelectMode(false);\n }\n const phoneSelecting = !!selection && !isMdUp && (phoneSelectMode || anySelected);\n // ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,\n // anchored on the row's KEY so a sort, filter or page change cannot move it)\n const { canSelect, selectRange, setAnchor, markRangeClick, takeRangeClick } = useRangeSelection({\n selection,\n slice,\n rowKey,\n });\n // ---- Keyboard-activatable rows ----\n //\n // A desktop row with `onRowClick` opened on a mouse click and on nothing else: no\n // tab stop, no Enter, no focus ring (keksdose's report tables hand-rolled all three,\n // which is half of why they could not move onto this table). The row now takes\n // focus itself, as a ROVING tab stop: one row of the table is in the tab order\n // (the last one focused, else the first), ArrowUp/ArrowDown/Home/End move between\n // rows, Enter and Space activate.\n //\n // Why the `<tr>` rather than the two alternatives:\n // * `role=\"button\"` on the row would replace its `row` role, and the cells with\n // it — a screen reader would stop reading the table as a table.\n // * A button or link in one cell is what `rowHref` already is where the row HAS a\n // URL. Without one, a synthetic button would have to wrap caller-rendered cell\n // content, which nests the controls those cells carry (status toggles, links)\n // inside it — illegal markup, and the reason the mobile card is a div.\n // A focused `<tr>` keeps its role and reads out its cells. Roving rather than a tab\n // stop per row, so a 100-row page does not put 100 stops between the header and\n // the pager.\n const rowsFocusable = !!onRowClick;\n const [focusRowKey, setFocusRowKey] = useState<string | number | null>(null);\n const tabRowKey =\n focusRowKey !== null && slice.some((r) => rowKey(r) === focusRowKey)\n ? focusRowKey\n : slice.length\n ? rowKey(slice[0])\n : null;\n\n // What the memoised rows' handlers read at event time (0.48.2, K-052-2): the latest\n // props, refreshed every commit, so a row that bailed out of a render still toggles\n // through the current `selection` and opens through the current `onRowClick`.\n // `virtualize` (0.48.2): desktop only, and only with rows to window. Arrow/Home/End then\n // move over every row, rendered or not: the target is rendered, then focused.\n const virtualOn = !!virtualize && isMdUp && slice.length > 0;\n const focusRowRef = useRef<((index: number) => void) | undefined>(undefined);\n const latestRowCtx: DataTableRowCtx<T> = {\n moveFocus: virtualOn\n ? (from, key) => {\n const target = rowNavTarget(from, key, slice.length);\n if (target === null) return false;\n focusRowRef.current?.(target);\n return true;\n }\n : undefined,\n onRowClick,\n onToggle: selection?.onToggle,\n isSelected: (row) => !!selection?.isSelected(row),\n canSelect,\n selectRange,\n setAnchor,\n markRangeClick,\n takeRangeClick,\n setFocusRowKey,\n onLongPress: (row) => {\n setPhoneSelectMode(true);\n setAnchor(rowKey(row));\n if (selection && !selection.isSelected(row)) selection.onToggle(row, true);\n },\n };\n const rowCtx = useRef(latestRowCtx);\n useLayoutEffect(() => {\n rowCtx.current = latestRowCtx;\n });\n\n // When dialog-mode is on, the expanded row's detail goes into a bottom-sheet\n // modal instead of unfolding inline. Only one row is ever expanded at a time.\n const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;\n const mobileDialogContent = mobileDialogRow ? openedRowContent(mobileDialogRow) : null;\n // The full-screen row dialog. Its panel, header, scroll lock and Back handling all\n // live in {@link FullBleedDialog} now — this was the only copy of that shell until\n // the transactions create card needed the same one (Keksdose live #307's follow-up),\n // and two copies of a dialog is how the row editor and the create form come to look\n // like two different products.\n const dialogOpen = !!(mobileDialogRow && mobileDialogContent);\n const closeDialog = () => {\n if (mobileDialogRow) onRowClick?.(mobileDialogRow);\n };\n\n // One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list\n // and the grouped list (below) share identical row markup.\n const renderMobileRow = (row: T) => {\n const key = rowKey(row);\n const expanded = isExpanded?.(row) ?? false;\n return (\n <DataTableMobileRow<T>\n key={key}\n ctx={rowCtx}\n rowKey={key}\n row={row}\n expanded={expanded}\n expansion={expanded ? openedRowContent(row) : null}\n opensRow={opensRow}\n className={rowClassName?.(row)}\n attrs={rowAttributes?.(row)}\n interactive={!!onRowClick}\n href={mobileCardLinkable ? rowHref?.(row) : undefined}\n swipeActionsFor={swipeActionsFor}\n mobileDir={mobileDir}\n mobileCardLinkable={mobileCardLinkable}\n mobileCard={mobileCard}\n mobilePrimaryCol={mobilePrimaryCol}\n mobileSecondaryColumns={mobileSecondaryColumns}\n mobileExpandAsDialog={mobileExpandAsDialog}\n compact={compact}\n large={large}\n selectionOn={!!selection}\n selectActive={phoneSelecting}\n selectable={canSelect(row)}\n selected={!!selection && selection.isSelected(row)}\n />\n );\n };\n\n // Consecutive grouping of the (already-sorted) mobile slice into labelled\n // sections for the \"assistance\"-style list (feedback #317). Null unless the\n // caller opts in via `mobileGroupBy`.\n // The desktop's bands (0.48, keksdose dev #605), over the rows on screen.\n const tableRef = useRef<HTMLTableElement | null>(null);\n const desktopGroups = isMdUp && levels.length > 0 ? groupRows(slice, levels) : null;\n const bandTops = useGroupBandTops(\n tableRef,\n !!desktopGroups,\n `${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? \"\"}`,\n );\n // ---- Row windowing ---- (0.48.2, keksdose dev #632; data-table-virtual.ts)\n // One run per innermost group, with its bands (`virtualOn` is above, with the rows' context).\n const virtualGroups: VirtualGroup[] = [];\n const virtualBands: { g1: RowGroup<T>; g2: RowGroup<T> | null }[] = [];\n if (virtualOn) {\n if (!desktopGroups) virtualGroups.push({ start: 0, end: slice.length, bands: NO_BANDS, first: 0 });\n else\n for (const g1 of desktopGroups) {\n const first = virtualGroups.length;\n if (!g1.children) {\n virtualGroups.push({ start: g1.start, end: g1.start + g1.rows.length, bands: BAND_1, first });\n virtualBands.push({ g1, g2: null });\n continue;\n }\n g1.children.forEach((g2, j) => {\n virtualGroups.push({ start: g2.start, end: g2.start + g2.rows.length, bands: j === 0 ? BAND_1_2 : BAND_2, first });\n virtualBands.push({ g1, g2 });\n });\n }\n }\n const expandedIndices: number[] = [];\n if (virtualOn && isExpanded && expandedRow) slice.forEach((r, i) => isExpanded(r) && expandedIndices.push(i));\n const virtualColKeys = [...(selection ? [VIRTUAL_SELECT_COL] : []), ...visibleColumns.map((c) => c.key)];\n const virtualLockable = new Set(\n virtualColKeys.filter((k) => !widths[k] && !visibleColumns.some((c) => c.key === k && c.grow)),\n );\n const rowWindow = useRowWindow({\n enabled: virtualOn,\n rows: slice,\n rowKey,\n groups: virtualGroups,\n expanded: expandedIndices,\n pinnedKeys: [tabRowKey],\n scrollerRef: desktopScroller,\n tableRef,\n options: typeof virtualize === \"object\" ? virtualize : NO_VIRTUAL_OPTIONS,\n compact,\n rowsBefore: 1 + (leadingRow ? 1 : 0),\n rowsAfter: showFooter ? 1 : 0,\n colKeys: virtualColKeys,\n lockable: virtualLockable,\n resetKey: `${Object.entries(widths).join(\";\")}|${density}|${large}|${slice.length}:${\n slice.length ? `${rowKey(slice[0])}:${rowKey(slice[slice.length - 1])}` : \"\"\n }`,\n });\n // What `moveFocus` (in the rows' context, above) reaches: the window's focus mover.\n const focusRowAt = rowWindow?.focusRow;\n useLayoutEffect(() => {\n focusRowRef.current = focusRowAt;\n }, [focusRowAt]);\n\n // `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.\n const mobileLevel = typeof mobileGroupBy === \"function\" ? null : (mobileGroupBy ?? null);\n const mobileKey = typeof mobileGroupBy === \"function\" ? mobileGroupBy : mobileGroupBy?.key;\n const mobileGroups: { key: string; rows: T[] }[] | null = mobileKey\n ? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {\n const key = mobileKey(row);\n const last = acc[acc.length - 1];\n if (last && last.key === key) last.rows.push(row);\n else acc.push({ key, rows: [row] });\n return acc;\n }, [])\n : null;\n\n // `frame={false}` swaps the Card for a bare wrapper carrying the same hooks and\n // layout classes, so everything inside renders identically either way.\n const Root = frame ? FramedRoot : PlainRoot;\n\n return (\n <Root\n // A styling and testing hook: a host's own cell content can follow the table's\n // density (`group-data-[density=compact]/table:…`) without being told twice.\n data-density={density}\n data-frame={frame ? undefined : \"none\"}\n // The frame clips (`overflow-clip`), so it is a clipping container for a\n // Tooltip's auto-portal — said as an attribute as well, because jsdom computes no\n // classes and a table-cell tooltip must portal under test just as it does in the\n // browser (see `CLIPS_ATTRIBUTE`). The bare root clips nothing and is not marked.\n data-clips={frame ? \"\" : undefined}\n // The fill's marker (0.40, docs/page-layout-harmonization.md §1.3): AppShell's\n // `<main>` and every `FillHeight` between it and this table key off it with\n // `has-[[data-fill-height]]`, so a page asks for the fill by passing the prop.\n data-fill-height={fillHeight ? \"\" : undefined}\n className={cn(\n \"group/table\",\n frame ? \"overflow-clip\" : \"min-w-0\",\n // Both layouts, not just the table: the phone card list overflowing a bounded\n // pane drew itself over whatever followed it, and in a viewport-locked shell\n // the rows below the fold could not be reached at all. From `md` the minimum is\n // the content's again (`md:min-h-auto`): the body's floor below then holds the\n // whole chain up to `<main>`, which scrolls instead of leaving a slit.\n fillHeight && \"flex flex-1 flex-col min-h-0 md:min-h-auto\",\n className,\n )}\n >\n {/* Outside both viewport branches on purpose. A screen reader subscribes to a\n live region when it encounters it, so one that appears together with its\n first message is usually missed — and crossing the md breakpoint swaps the\n card list for the table wholesale, which would remount a region living\n inside either one. */}\n <span {...regionProps} />\n {toolbar != null && toolbar !== false && (\n <div\n data-table-toolbar=\"\"\n className={cn(\n \"flex flex-wrap items-center gap-2\",\n frame\n ? cn(\"border-b border-subtle\", compact ? \"px-2 py-1.5\" : \"px-3 py-2\")\n : \"pb-3\",\n )}\n >\n {toolbar}\n </div>\n )}\n {!isMdUp && (\n <div ref={mobileRootRef} className={cn(fillHeight && \"flex min-h-0 flex-1 flex-col\")}>\n {/* Mobile filter access (feedback #299): the per-column filter popovers\n live in the desktop header, which the card list doesn't render — so\n expose the same filters through a bottom sheet here. */}\n {(!serverRows || onFiltersChange) && (\n <MobileFilters\n columns={columns}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n isServer={serverRows}\n hasFilterCallback={!!onFiltersChange}\n onSetFilter={setFilterValue}\n onClearFilter={clearFilter}\n onClearAll={clearAllFilters}\n labels={labels}\n locale={locale}\n />\n )}\n {mobileSort && (\n <MobileSort\n columns={visibleColumns.filter((c) => (!serverRows || !!onSortsChange) && !!c.sortBy)}\n sorts={sorts}\n sortCycle={sortCycle}\n compact={compact}\n labels={labels}\n onPick={(key) => {\n if (key === null) {\n if (sorts.length > 0) commitSorts([], sorts[0].key);\n return;\n }\n if (sorts[0]?.key === key) return;\n // A newly picked column starts where its header's first click would.\n const col = columns.find((c) => c.key === key);\n commitSorts([{ key, dir: col?.firstSort ?? \"asc\" }], key);\n }}\n onFlip={() => {\n if (sorts.length === 0) return;\n commitSorts(\n sorts.map((s, i) => (i === 0 ? { ...s, dir: flipDir(s.dir) } : s)),\n sorts[0].key,\n );\n }}\n />\n )}\n {selection && (\n <MobileSelectBar\n active={phoneSelecting}\n allSelected={selection.allSelected}\n someSelected={selection.someSelected}\n compact={compact}\n labels={labels}\n onEnter={() => setPhoneSelectMode(true)}\n onToggleAll={selection.onToggleAll}\n onCancel={() => {\n setPhoneSelectMode(false);\n if (anySelected) selection.onToggleAll(false);\n }}\n />\n )}\n <ul\n ref={phoneListRef}\n className={cn(\n \"divide-y divide-subtle\",\n // Under `fillHeight` the list takes the room its parent gives it, and in a\n // BOUNDED parent it is the scroller, as the table body is on a wide screen:\n // the filter button stays above it and a server pager below. Only then —\n // see `phoneListScrolls` (live #420). The endless-scroll sentinel still\n // fires in here — an IntersectionObserver on the viewport sees through the\n // clipping ancestor.\n fillHeight && \"min-h-0 flex-1\",\n fillHeight && phoneListScrolls && \"overflow-y-auto overscroll-contain\",\n // Same busy treatment as the desktop body; see `isLoading` below.\n isLoading && mobileSlice.length > 0 && \"opacity-60 transition-opacity\",\n )}\n aria-busy={isLoading || undefined}\n >\n {mobileGroups\n ? mobileGroups.map((g) => (\n <Fragment key={`hb-group:${g.key}`}>\n <li className=\"sticky top-0 z-10 flex flex-wrap items-baseline justify-between gap-x-3 bg-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted backdrop-blur\">\n <span className=\"min-w-0\">\n {mobileGroupLabel\n ? mobileGroupLabel(g.key)\n : mobileLevel?.label\n ? mobileLevel.label(g.key, g.rows)\n : g.key}\n </span>\n {mobileLevel?.end && <span className=\"ms-auto tabular-nums\">{mobileLevel.end(g.key, g.rows)}</span>}\n </li>\n {g.rows.map(renderMobileRow)}\n </Fragment>\n ))\n : mobileSlice.map(renderMobileRow)}\n {mobileSlice.length === 0 && (\n <li\n className={cn(\n \"text-center text-muted\",\n compact ? \"px-3 py-3 text-xs\" : \"px-4 py-6 text-sm\",\n )}\n >\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </li>\n )}\n {/* Endless-scroll sentinel: observed by IntersectionObserver to pull in\n the next chunk as it nears the viewport (feedback #232). */}\n {canRevealMoreMobile && (\n <li\n ref={loadMoreRef}\n className=\"px-4 py-4 text-center text-xs text-placeholder\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\n )}\n {/* The owner's next slice (0.48), once every row held is revealed. */}\n {loadMore && !canRevealMoreMobile && (\n <DataTableLoadMoreFooter\n as=\"li\"\n loadMore={loadMore}\n rowCount={sorted.length}\n labels={labels}\n compact={compact}\n />\n )}\n </ul>\n {/* The phone's totals: a summary card after the list rather than one more\n `<li>` in it. The list reveals rows in chunks as it is scrolled, so a last\n item would sit mid-list until the end was reached; outside it, and under\n `fillHeight` outside the scroller too, the totals stay in view the way the\n sticky `<tfoot>` keeps them in view on a wide screen. Named by\n `footerLabel` so it is announced as what it is, not as a stray list of\n numbers; `mobileHidden` columns stay hidden here as on the cards. */}\n {showFooter && mobileColumns.some((c) => c.footer) && (\n <section\n data-table-footer=\"\"\n aria-labelledby={footerLabel != null ? footerHeadingId : undefined}\n className={cn(\n \"border-t-2 border-strong bg-surface-2\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n {footerLabel != null && (\n <div id={footerHeadingId} className={cn(\"font-semibold\", compact ? \"mb-1 text-sm\" : \"mb-2\")}>\n {footerLabel}\n </div>\n )}\n <dl\n className={cn(\n \"grid grid-cols-[auto_1fr] gap-x-3\",\n compact ? \"gap-y-0.5 text-xs\" : \"gap-y-1 text-sm\",\n )}\n >\n {mobileColumns\n .filter((c) => c.footer)\n .map((col) => (\n <Fragment key={col.key}>\n <dt className=\"self-center text-xs uppercase tracking-wide text-muted\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-primary\">\n {footerValue(col)}\n </dd>\n </Fragment>\n ))}\n </dl>\n </section>\n )}\n {/* Client-side mobile lists scroll endlessly (sentinel above); only\n server-paginated tables keep the pager here. */}\n {isServer && !loadMore && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n pageSize={paginationPageSize}\n total={paginationTotal}\n onPage={goToPage}\n onPageSize={serverPagination!.onPageSizeChange}\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n {/* The row's own first column is this dialog's title — the same cell the card\n behind it shows, so the panel names the row it opened from. */}\n <FullBleedDialog\n open={dialogOpen}\n onClose={closeDialog}\n closeLabel={labels.close}\n header={mobileDialogRow ? mobilePrimaryCol?.cell(mobileDialogRow) : null}\n backCloses={mobileDialogBackCloses}\n >\n {mobileDialogContent}\n </FullBleedDialog>\n </div>\n )}\n {isMdUp && (\n <div className={cn(\"flex\", fillHeight && \"flex-1\")}>\n {/* Under `fillHeight` neither wrapper is `min-h-0`: a flex item's automatic\n minimum is its content's, and the content's height counts the body at its\n basis (`basis-48`, below), not at its rows — so the floor carries up. */}\n <div className={cn(\"relative min-w-0 flex-1\", fillHeight && \"flex flex-col\")}>\n {/* scrollbar-gutter:stable reserves the vertical-scrollbar space up\n front. Expanding a row adds height, which can bring the scrollbar\n in, which narrows the content box — and in an `auto` table layout\n that re-computes every column width. Reserving the gutter is the\n other half of the `w-0 min-w-full` fix on the expansion cell below\n (feedback #104: \"expanding an item resizes the columns\"). */}\n <div\n ref={desktopScroller}\n // The scroller: marked for the Tooltip's auto-portal (see the root above).\n data-clips=\"\"\n // Under `fillHeight` the body grows into the room from a 12rem basis (0.40,\n // §1.3): about five rows is its floor. A window too short for the page's\n // header and that much body scrolls `<main>` instead (keksdose's review).\n className={cn(\"overflow-auto [scrollbar-gutter:stable]\", fillHeight && \"min-h-0 grow basis-48\")}\n // `edgeFade`: the mask, `data-overflow` and the focus handling; nothing\n // while off. The height bound is merged over the mask, not replaced by it.\n {...edgeFadeProps}\n style={\n fillHeight\n ? edgeFadeProps.style\n : { ...edgeFadeProps.style, maxHeight: maxBodyHeight }\n }\n >\n {/* A table with no name is \"table\" in a screen reader's list of tables, and\n an app renders several. `labels.table` is how a call site says which. */}\n {/* `aria-busy` while a server page is in flight: a reader holds off announcing\n the rows that are about to be replaced, and the dimmed body says the same\n to the eye. The pager stays live — see `ServerPagination.isLoading`. */}\n <table\n ref={tableRef}\n className={cn(\"w-full\", compact ? \"text-xs\" : \"text-sm\")}\n data-fit-grow={growDrawn ? \"\" : undefined}\n aria-label={labels.table}\n aria-busy={isLoading || undefined}\n // `virtualize` (0.48.2): every row counted, rendered or not.\n aria-rowcount={rowWindow?.rowCount}\n data-virtualized={rowWindow ? \"\" : undefined}\n >\n {/* The locked column widths (0.48.2, `virtualize`): rows coming and going no\n longer move the columns. */}\n {rowWindow && (\n <colgroup>\n {virtualColKeys.map((key) => {\n const w = rowWindow.colWidths?.[key];\n return <col key={key} style={w !== undefined ? { width: w } : undefined} />;\n })}\n </colgroup>\n )}\n {/* Sticky header. position:sticky pins to the nearest scroll-container\n ancestor — so the header can only stick to THIS wrapper, never the\n page. For that to actually hold the header in place, the wrapper\n must be the thing that scrolls vertically: hence overflow-auto +\n a bounded max-height (rather than overflow-x-auto, which scrolls\n only sideways and lets the header ride away on page scroll). The\n header then stays put as you scroll the rows. position:sticky on\n <thead> isn't reliable across browsers, so we put it on each <th>\n (see DataTableHead, data-table-head.tsx). */}\n <DataTableHead\n visibleColumns={visibleColumns}\n selection={selection}\n compact={compact}\n labels={labels}\n locale={locale}\n isServer={serverRows}\n hasSortCallback={!!onSortsChange}\n hasFilterCallback={!!onFiltersChange}\n sorts={sorts}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n multiSort={multiSort}\n resizable={resizable}\n widths={widths}\n setWidths={setWidths}\n headRefs={headRefs}\n toggleSort={toggleSort}\n setFilterValue={setFilterValue}\n clearFilter={clearFilter}\n startResize={startResize}\n moveResize={moveResize}\n finishResize={finishResize}\n ariaRowIndex={rowWindow ? 1 : undefined}\n lockedWidths={rowWindow?.colWidths}\n selectionLockKey={VIRTUAL_SELECT_COL}\n />\n {(() => {\n const bodyClass = cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\");\n const leading = leadingRow ? (\n <DataTableLeadingRow\n colSpan={totalColSpan}\n sticky={leadingRowSticky}\n ariaRowIndex={rowWindow ? 2 : undefined}\n >\n {leadingRow}\n </DataTableLeadingRow>\n ) : null;\n // One memoised row each (0.48.2, K-052-2; data-table-row.tsx): the table\n // derives each row's own inputs here, as primitives, so a tick re-renders\n // only the rows whose inputs changed.\n const renderRow = (row: T, rowIndex: number) => {\n const expanded = isExpanded?.(row) ?? false;\n const key = rowKey(row);\n return (\n <DataTableRow<T>\n key={key}\n row={row}\n rowIndex={rowIndex}\n rowKey={key}\n ctx={rowCtx}\n columns={visibleColumns}\n widths={widths}\n growDrawn={growDrawn}\n linkColumn={linkColumn}\n href={rowHref?.(row)}\n selectionColumn={!!selection}\n selectable={canSelect(row)}\n selected={!!selection && selection.isSelected(row)}\n expanded={expanded}\n expansion={expanded ? expandedRow?.(row) : null}\n disclosure={rowsFocusable && !!expandedRow}\n detailId={`${detailIdBase}-${rowIndex}`}\n rowsFocusable={rowsFocusable}\n rowInteractive={!!onRowClick || !!selection}\n tabbable={key === tabRowKey}\n className={rowClassName?.(row)}\n attrs={rowAttributes?.(row)}\n compact={compact}\n totalColSpan={totalColSpan}\n rowDetailsLabel={labels.rowDetails}\n selectRowLabel={labels.selectRow}\n ariaRowIndex={rowWindow?.ariaRow(rowIndex)}\n />\n );\n };\n const emptyRow =\n slice.length === 0 ? (\n <tr>\n <td\n colSpan={totalColSpan}\n className={cn(\"text-center text-muted\", compact ? \"px-2 py-2\" : \"px-3 py-4\")}\n >\n {/* Nothing is not the same as not-yet: \"no results\" while the first\n page is still in flight is a claim the table cannot make. */}\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </td>\n </tr>\n ) : null;\n const band = (\n level: 1 | 2,\n def: DataTableGroupBy<T>,\n g: { key: string; rows: T[] },\n ariaRowIndex?: number,\n ) => (\n <DataTableGroupBand\n level={level}\n colSpan={totalColSpan}\n top={level === 1 ? bandTops[0] : bandTops[1]}\n compact={compact}\n label={def.label ? def.label(g.key, g.rows) : g.key}\n end={def.end?.(g.key, g.rows)}\n ariaRowIndex={ariaRowIndex}\n />\n );\n if (rowWindow) {\n // Windowed (0.48.2): each group's own `<tbody>` with its bands and the\n // rendered rows, spacer rows between; one spacer `<tbody>` per run of groups\n // with nothing rendered. Spacers are hidden from the accessibility tree.\n const spacer = (key: string, height: number) => (\n <tr key={key} aria-hidden=\"true\" data-vt-spacer=\"\">\n <td colSpan={totalColSpan} style={{ height, padding: 0, border: 0 }} />\n </tr>\n );\n return (\n <>\n {leading && <tbody className={bodyClass}>{leading}</tbody>}\n {rowWindow.bodies.map((b) => {\n if (b.kind === \"spacer\")\n return (\n <tbody key={b.key} data-vt-body=\"\" aria-hidden=\"true\">\n {spacer(\"s\", b.height)}\n </tbody>\n );\n const g = virtualGroups[b.group];\n const vb = virtualBands[b.group];\n return (\n <tbody\n key={vb ? `vg:${vb.g1.key}:${vb.g2?.key ?? \"\"}:${g.start}` : \"vg\"}\n data-vt-body=\"\"\n className={bodyClass}\n >\n {vb && g.bands.includes(1) && band(1, levels[0], vb.g1, rowWindow.ariaBand(b.group, 1))}\n {vb?.g2 && band(2, levels[1], vb.g2, rowWindow.ariaBand(b.group, 2))}\n {b.items.map((it) =>\n it.kind === \"gap\" ? spacer(it.key, it.height) : renderRow(slice[it.index], it.index),\n )}\n </tbody>\n );\n })}\n </>\n );\n }\n if (!desktopGroups) {\n return (\n <tbody className={bodyClass}>\n {leading}\n {slice.map(renderRow)}\n {emptyRow}\n </tbody>\n );\n }\n // Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its\n // band the row header; the leading row (and the empty state) in their own\n // before the first group.\n const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));\n return (\n <>\n {(leading || emptyRow) && (\n <tbody className={bodyClass}>\n {leading}\n {emptyRow}\n </tbody>\n )}\n {desktopGroups.map((g1) =>\n g1.children ? (\n g1.children.map((g2, j) => (\n <tbody key={`g:${g1.key}:${g2.key}:${g2.start}`} className={bodyClass}>\n {j === 0 && band(1, levels[0], g1)}\n {band(2, levels[1], g2)}\n {rowsOf(g2)}\n </tbody>\n ))\n ) : (\n <tbody key={`g:${g1.key}:${g1.start}`} className={bodyClass}>\n {band(1, levels[0], g1)}\n {rowsOf(g1)}\n </tbody>\n ),\n )}\n </>\n );\n })()}\n {showFooter && (\n // Sticky to the scroller's bottom edge for the same reason the header is\n // sticky to its top: the body scrolls inside a bounded wrapper, and a\n // total that scrolls away with the last row is a total nobody sees on a\n // 25-row page. Each cell carries its own background and its own `sticky`,\n // as the header's do — `position: sticky` on a `<tfoot>` is not reliable.\n <tfoot\n data-table-footer=\"\"\n className={cn(\n \"font-semibold text-primary\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n <tr aria-rowindex={rowWindow?.rowCount}>\n {/* The selection column has nothing to total: an empty cell keeps the\n values under their own headers. */}\n {selection && (\n <td\n className={cn(\n FOOTER_CELL,\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n />\n )}\n {visibleColumns.map((col) => {\n const width = widths[col.key];\n const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;\n const isLabel = col === footerLabelCol;\n const Cell = isLabel ? \"th\" : \"td\";\n return (\n <Cell\n key={col.key}\n data-col={col.key}\n scope={isLabel ? \"row\" : undefined}\n style={pinned}\n className={cn(\n FOOTER_CELL,\n \"align-top\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n width && \"overflow-hidden text-ellipsis\",\n // A `<th>` centres by default; the label reads from the start\n // like the cells above it, unless the column says otherwise.\n isLabel && \"text-start\",\n col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,\n logicalAlign(col.className),\n )}\n >\n {col.footer ? footerValue(col) : isLabel ? footerLabel : null}\n </Cell>\n );\n })}\n </tr>\n </tfoot>\n )}\n </table>\n {/* In the scroller, after the rows (0.48): it comes into view as the last row\n does, under `fillHeight` too, where the body is the scroller. */}\n {loadMore && (\n <DataTableLoadMoreFooter loadMore={loadMore} rowCount={sorted.length} labels={labels} compact={compact} />\n )}\n </div>\n {!unpaged && !loadMore && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n pageSize={paginationPageSize}\n total={paginationTotal}\n onPage={goToPage}\n onPageSize={\n isServer\n ? serverPagination!.onPageSizeChange\n : (n) => {\n // To the owner when it holds the page size, as `commitSorts` does.\n if (onPageSizeChange) onPageSizeChange(n);\n else setPageSize(n);\n setPage(0);\n }\n }\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n </div>\n {columnSettings && (\n <ColumnSettingsPanel\n columns={columns}\n hiddenCols={hiddenCols}\n setHiddenCols={setHiddenCols}\n autoHidden={autoHidden}\n keepColumn={keepColumn}\n showSettings={showSettings}\n setShowSettings={setShowSettings}\n columnsCountLabel={columnsCountLabel}\n labels={labels}\n autoSizeAll={autoSizeAll}\n />\n )}\n </div>\n )}\n </Root>\n );\n}\n\n/** The framed root: the kit's flush Card, as every table had before `frame`. */\nfunction FramedRoot({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <Card flush {...rest}>\n {children}\n </Card>\n );\n}\n\n/** The unframed root (`frame={false}`): no border, surface, radius or shadow — the\n * host's own card supplies them. */\nfunction PlainRoot(props: React.HTMLAttributes<HTMLDivElement>) {\n return <div {...props} />;\n}\n\n/** The loading row's content: a spinner beside the word, the word for everyone. */\nfunction LoadingText({ label }: { label: string }) {\n // `<bdi>`: the label takes its direction from its own letters, not the table's. The\n // English default inside a `dir=\"rtl\"` table otherwise had its trailing \"…\" — a\n // neutral with no letter after it — resolved right-to-left and drawn in front of\n // the word: \"…Loading\". An Arabic or Hebrew label still reads right-to-left.\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{label}</bdi>\n </span>\n );\n}\n"],"mappings":";AAi4CM,SAurBU,UAtrBR,KADF;AAj4CN,SAAS,YAAAA,WAAU,WAAW,OAAO,iBAAiB,SAAS,QAAQ,gBAAgB;AAEvF,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,kBAAkB,0BAA0B;AACrD,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,aAAa,cAAc,8BAA8B;AAClE,SAAS,eAAe,iBAAiB,YAAY,qBAAqB;AAC1E,SAAS,oBAAoB,oBAA0C;AACvE,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAEP,SAAS,oBAAoB,cAAc,oBAAiE;AAE5G,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,gBAAgB,eAAe,kBAAkB;AAE1D,SAAS,iBAAiB;AAE1B,SAAS,kBAAkB;AAI3B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,iCAAuD;AAChE,SAAS,sBAAsB,cAAc,mCAAuD;AACpG,SAAS,mBAAmB;AAC5B,SAAS,aAA6B;AAozBtC,MAAM,cACJ;AAwBK,MAAM,iCAAsD;AAAA,EACjE,QAAQ,0BAA0B;AAAA,EAClC,aAAa,0BAA0B;AAAA,EACvC,eAAe,0BAA0B;AAAA,EACzC,gBAAgB,0BAA0B;AAC5C;AAEA,MAAM,UAAU,CAAC,MAAyB,MAAM,QAAQ,SAAS;AAGjE,MAAM,WAA+B,CAAC;AACtC,MAAM,SAA6B,CAAC,CAAC;AACrC,MAAM,SAA6B,CAAC,CAAC;AACrC,MAAM,WAA+B,CAAC,GAAG,CAAC;AAC1C,MAAM,qBAA0C,CAAC;AAQjD,SAAS,wBAAwB,qBAAwC;AACzE,SAAS,uBAAuB;AAChC,SAAS,iBAAiB,sBAAsB,oBAAoB;AAE7D,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB;AAAA,EACA,YAAY;AAAA,EACZ,SAAS;AAAA,EACT;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,kBAAkB;AAAA,EAClB,uBAAuB;AAAA,EACvB,qBAAqB;AAAA,EACrB;AAAA,EACA,yBAAyB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAsB;AACpB,QAAM,UAAU,YAAY;AAC5B,QAAM,UAAU,WAAW;AAC3B,QAAM,iBAAiB,sBAAsB,CAAC;AAC9C,QAAM,YAAY,iBAAiB,CAAC;AACpC,QAAM,YAAY,iBAAiB,CAAC;AACpC,QAAM,WAAW,CAAC,CAAC;AAInB,QAAM,aAAa,aAAa,kBAAkB,CAAC,CAAC;AACpD,QAAM,YAAY,CAAC,CAAC,kBAAkB;AAOtC,QAAM,iBAAiB,qBAAqB,WAAW;AAIvD,QAAM,2BAA2B,4BAA4B;AAC7D,QAAM,mBAAmB,wBAAwB,4BAA4B;AAC7E,QAAM,SAAS,QAAQ,MAAM,uBAAuB,gBAAgB,UAAU,GAAG,CAAC,gBAAgB,UAAU,CAAC;AAC7G,QAAM,SAAS,aAAa,UAAU;AAKtC,QAAM,EAAE,SAAS,kBAAkB,IAAI,mBAAmB;AAAA,IACxD,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,kBAAkB,uBAAuB,aAAa,oBAAoB;AAKhF,QAAM,EAAE,UAAU,YAAY,IAAI,YAAY;AAC9C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,cAAc;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA,cAAc,WAAW,SAAY;AAAA,EACvC,CAAC;AAUD,QAAM,SAAS,QAAQ,MAAM;AAC3B,QAAI,CAAC,UAAW,QAAO,CAAC;AACxB,UAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,IAAI,GAAG;AAC1C,QAAI,CAAC,QAAQ,EAAE,QAAQ,cAAe,QAAO;AAC7C,UAAM,EAAE,CAAC,IAAI,GAAG,UAAU,GAAG,KAAK,IAAI;AACtC,WAAO;AAAA,EACT,GAAG,CAAC,WAAW,cAAc,OAAO,CAAC;AACrC,QAAM,aAAa;AAAA,IACjB,MAAO,iBAAiB,mBAAmB,oBAAI,IAAY;AAAA,IAC3D,CAAC,gBAAgB,gBAAgB;AAAA,EACnC;AAcA,QAAM,YAAY,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,IAAQ;AAIzD,QAAM,mBAAmB,QACtB,IAAI,CAAC,MAAM;AACV,UAAM,IAAI,cAAc,CAAC;AACzB,QAAI,CAAC,KAAK,EAAE,SAAS,YAAY,CAAC,EAAE,QAAS,QAAO;AACpD,WAAO,GAAG,EAAE,GAAG,IAAI,EAAE,QAAQ,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,EAClF,CAAC,EACA,KAAK,IAAQ;AAEhB,QAAM,qBAAqB,QAAQ,MAAM;AACvC,UAAM,MAA0D,CAAC;AACjE,eAAW,OAAO,SAAS;AACzB,YAAM,IAAI,cAAc,GAAG;AAC3B,UAAI,CAAC,KAAK,EAAE,SAAS,SAAU;AAC/B,UAAI,EAAE,WAAW,EAAE,QAAQ,SAAS,GAAG;AACrC,YAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE;AACnF;AAAA,MACF;AACA,YAAM,OAAO,oBAAI,IAAY;AAC7B,iBAAW,OAAO,MAAM;AACtB,cAAM,IAAI,EAAE,SAAS,GAAG;AACxB,YAAI,KAAK,CAAC,KAAK,IAAI,CAAC,EAAG,MAAK,IAAI,CAAC;AAAA,MACnC;AACA,UAAI,IAAI,GAAG,IAAI,MAAM,KAAK,IAAI,EAC3B,KAAK,EACL,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,IACxC;AACA,WAAO;AAAA,EAGT,GAAG,CAAC,WAAW,kBAAkB,IAAI,CAAC;AAEtC,QAAM,WAAW,QAAQ,MAAM;AAC7B,QAAI,WAAY,QAAO;AACvB,QAAI,SAAS;AACb,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,QAAQ,IAAI,GAAG;AAC7B,UAAI,CAAC,SAAS,CAAC,eAAe,KAAK,EAAG;AACtC,eAAS,OAAO,OAAO,CAAC,QAAQ,WAAW,KAAK,KAAK,OAAO,MAAM,CAAC;AAAA,IACrE;AACA,WAAO;AAAA,EAET,GAAG,CAAC,MAAM,WAAW,SAAS,YAAY,MAAM,CAAC;AAEjD,QAAM,SAAS,QAAQ,MAAM;AAC3B,QAAI,WAAY,QAAO;AAGvB,UAAM,QAAQ,MACX,IAAI,CAAC,MAAM;AACV,YAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG;AAC/C,aAAO,KAAK,SAAS,EAAE,QAAQ,IAAI,QAAQ,KAAK,EAAE,IAAI,IAAI;AAAA,IAC5D,CAAC,EACA,OAAO,CAAC,MAA4F,MAAM,IAAI;AACjH,QAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,UAAM,OAAO,CAAC,GAAG,QAAQ;AACzB,SAAK,KAAK,CAAC,GAAG,MAAM;AAClB,iBAAW,EAAE,QAAQ,IAAI,KAAK,OAAO;AACnC,cAAM,KAAK,OAAO,CAAC;AACnB,cAAM,KAAK,OAAO,CAAC;AACnB,YAAI,MAAM,QAAQ,MAAM,KAAM;AAC9B,YAAI,MAAM,KAAM,QAAO;AACvB,YAAI,MAAM,KAAM,QAAO;AACvB,YAAI,KAAK,GAAI,QAAO,QAAQ,QAAQ,KAAK;AACzC,YAAI,KAAK,GAAI,QAAO,QAAQ,QAAQ,IAAI;AAAA,MAC1C;AACA,aAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EAET,GAAG,CAAC,UAAU,OAAO,WAAW,UAAU,CAAC;AAK3C,QAAM,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,YAAY;AAC1D,QAAM,oBAAoB,WACtB,iBAAkB,WAClB,WAAW,aAAa,WACtB,OAAO,UAAU,IACjB;AACN,QAAM,kBAAkB,WAAW,iBAAkB,QAAQ,OAAO;AACpE,QAAM,qBAAqB,WAAW,iBAAkB,WAAW;AACnE,QAAM,aAAa,WACf,KAAK,IAAI,GAAG,KAAK,KAAK,iBAAkB,QAAQ,KAAK,IAAI,GAAG,iBAAkB,QAAQ,CAAC,CAAC,IACxF,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,SAAS,iBAAiB,CAAC;AAC5D,QAAM,WAAW,WACb,KAAK,IAAI,KAAK,IAAI,GAAG,iBAAkB,IAAI,GAAG,aAAa,CAAC,IAC5D,KAAK,IAAI,MAAM,aAAa,CAAC;AACjC,QAAM,QACJ,YAAY,WAAW,aAAa,WAChC,SACA,OAAO,MAAM,WAAW,mBAAmB,WAAW,oBAAoB,iBAAiB;AAEjG,QAAM,aAAa,CAAC,KAAa,aAAsB;AACrD,UAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAG7C,UAAM,OAAO,UAAU,OAAO,KAAK,aAAa,UAAU;AAAA,MACxD,UAAU,KAAK;AAAA,MACf,OAAO;AAAA,IACT,CAAC;AACD,gBAAY,MAAM,GAAG;AAAA,EACvB;AAIA,QAAM,cAAc,CAAC,MAAmB,QAAgB;AACtD,UAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAC7C,QAAI,cAAe,eAAc,IAAI;AAAA,QAChC,kBAAiB,IAAI;AAM1B,UAAM,OAAO,MAAM,YAAY,KAAK,MAAM,IAAI;AAC9C,UAAM,QAAQ,KAAK,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAC5C;AAAA,MACE,CAAC,QACG,OAAO,YAAY,IAAI,IACvB,MAAM,QAAQ,QACZ,OAAO,gBAAgB,IAAI,IAC3B,OAAO,iBAAiB,IAAI;AAAA,IACpC;AAAA,EACF;AAEA,QAAM,iBAAiB,CAAC,KAAa,UAAuB;AAC1D,QAAI,iBAAiB;AACnB,sBAAgB,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,CAAC;AAI5C;AAAA,IACF;AACA,uBAAmB,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,MAAM,EAAE;AAClD,YAAQ,CAAC;AAAA,EACX;AAEA,QAAM,cAAc,CAAC,QAAgB;AACnC,QAAI,iBAAiB;AACnB,YAAM,OAAO,EAAE,GAAG,QAAQ;AAC1B,aAAO,KAAK,GAAG;AACf,sBAAgB,IAAI;AACpB;AAAA,IACF;AACA,uBAAmB,CAAC,MAAM;AACxB,YAAM,OAAO,EAAE,GAAG,EAAE;AACpB,aAAO,KAAK,GAAG;AACf,aAAO;AAAA,IACT,CAAC;AACD,YAAQ,CAAC;AAAA,EACX;AAIA,QAAM,kBAAkB,MAAM;AAC5B,UAAM,OAAO,QACV,OAAO,CAAC,QAAU,CAAC,cAAc,kBAAmB,cAAc,GAAG,IAAI,IAAK,EAC9E,IAAI,CAAC,QAAQ,IAAI,GAAG;AACvB,QAAI,CAAC,KAAK,OAAQ;AAClB,QAAI,iBAAiB;AACnB,YAAM,OAAO,EAAE,GAAG,QAAQ;AAC1B,iBAAW,KAAK,KAAM,QAAO,KAAK,CAAC;AACnC,sBAAgB,IAAI;AACpB;AAAA,IACF;AACA,uBAAmB,CAAC,MAAM;AACxB,YAAM,OAAO,EAAE,GAAG,EAAE;AACpB,iBAAW,KAAK,KAAM,QAAO,KAAK,CAAC;AACnC,aAAO;AAAA,IACT,CAAC;AACD,YAAQ,CAAC;AAAA,EACX;AAQA,QAAM,YAAY,QAAQ,MAAM,KAAK,UAAU,OAAO,GAAG,CAAC,OAAO,CAAC;AAGlE,QAAM,qBAAqB,OAAO,SAAS;AAC3C,YAAU,MAAM;AACd,QAAI,mBAAmB,YAAY,UAAW;AAC9C,uBAAmB,UAAU;AAK7B,QAAI,WAAY;AAChB,aAAS,OAAO,cAAc,SAAS,QAAQ,KAAK,MAAM,CAAC;AAAA,EAC7D,GAAG,CAAC,WAAW,SAAS,QAAQ,KAAK,QAAQ,YAAY,UAAU,MAAM,CAAC;AAG1E,QAAM,WAAW,CAAC,MAAc;AAC9B,QAAI,SAAU,kBAAkB,aAAa,CAAC;AAAA,QACzC,SAAQ,CAAC;AACd,aAAS,OAAO,YAAY,IAAI,GAAG,UAAU,CAAC;AAAA,EAChD;AAKA,QAAM,QAAQ,aAAa;AAC3B,QAAM,EAAE,YAAY,MAAM,WAAW,IAAI,aAAa;AAAA,IACpD;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,MAAM,gBAAgB;AAAA,IAChC,SAAS,CAAC,eAAe;AAAA;AAAA,IAEzB,SAAS,CAAC,OAAO,SAAS,YAAY,QAAQ,SAAS,KAAK;AAAA,EAC9D,CAAC;AACD,QAAM,iBAAiB;AAAA,IACrB,MAAM,QAAQ,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,GAAG,KAAK,CAAC,WAAW,IAAI,EAAE,GAAG,CAAC;AAAA,IAC5E,CAAC,SAAS,YAAY,UAAU;AAAA,EAClC;AAGA,QAAM,YAAY,eAAe,KAAK,CAAC,MAAM,EAAE,IAAI;AAcnD,QAAM,aAAa,UACd,eAAe,KAAK,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,SAAS,KAC3D,eAAe,KAAK,CAAC,MAAM,CAAC,EAAE,SAAS,IACvC;AAIJ,QAAM,EAAE,aAAa,YAAY,cAAc,YAAY,IAAI,gBAAgB;AAAA,IAC7E;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAEA,YAAY,WAAW,OAAO,IAAI,oBAAI,IAAI,CAAC,GAAG,YAAY,GAAG,UAAU,CAAC,IAAI;AAAA,EAC9E,CAAC;AAED,QAAM,eAAe,eAAe;AACpC,QAAM,aAAa,QAAQ;AAE3B,QAAM,eAAe,gBAAgB,YAAY,IAAI;AAMrD,QAAM,mBAAmB,QAAQ,KAAK,CAAC,MAAM,EAAE,MAAM;AACrD,QAAM,aAAa,eAAe,SAAS,oBAAoB,OAAO,SAAS;AAI/E,QAAM,iBAAiB,aAClB,eAAe,KAAK,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC,KAAK,eAAe,CAAC,IACtF;AACJ,QAAM,cAAc,CAAC,QAA4B,IAAI,SAAS,QAAQ,KAAK;AAC3E,QAAM,kBAAkB,MAAM;AAE9B,QAAM,eAAe,MAAM;AAE3B,QAAM,aAAa,QAAQ,OAAO,CAAC,MAAM,iBAAiB,EAAE,GAAG,CAAC,EAAE;AAClE,QAAM,aAAa,eAAe,OAAO,CAAC,MAAM,iBAAiB,EAAE,GAAG,CAAC,EAAE;AACzE,QAAM,oBAAoB,OAAO,aAAa,eAAe,YAAY,aAAa,UAAU;AAShG,QAAM,SAAS,CAAC,eAAe;AAE/B,QAAM,kBAAkB,OAA8B,IAAI;AAK1D,QAAM,SAAS,YAAY,OAAO;AAClC,QAAM,uBAAuB,WAAW,CAAC,CAAC,eAAe,OAAO,SAAS,KAAK,CAAC,CAAC;AAChF,YAAU,MAAM;AACd,UAAM,KAAK,gBAAgB;AAC3B,QAAI,CAAC,wBAAwB,CAAC,MAAM,OAAO,mBAAmB,YAAa;AAC3E,UAAM,KAAK,IAAI,eAAe,MAAM,GAAG,MAAM,YAAY,kBAAkB,GAAG,GAAG,WAAW,IAAI,CAAC;AACjG,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,oBAAoB,CAAC;AACzB,QAAM,gBAAgB,YAAY,iBAAiB,YAAY,MAAM;AAQrE,QAAM,gBAAgB,QAAQ,MAAM,eAAe,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,CAAC,cAAc,CAAC;AACnG,QAAM,mBACJ,cAAc,KAAK,CAAC,MAAM,EAAE,aAAa,KAAK,cAAc,CAAC,KAAK;AACpE,QAAM,yBAAyB;AAAA,IAC7B,MAAM,cAAc,OAAO,CAAC,MAAM,MAAM,gBAAgB;AAAA,IACxD,CAAC,eAAe,gBAAgB;AAAA,EAClC;AAEA,QAAM,sBAAsB,qBAAqB,eAAe,OAAO,CAAC,MAAM,EAAE,YAAY,IAAI,CAAC;AAOjG,QAAM,WAAW,CAAC,CAAC,eAAe,oBAAoB,SAAS;AAC/D,QAAM,mBAAmB,CAAC,QAAsB;AAC9C,UAAM,SAAS,cAAc,GAAG,KAAK;AACrC,QAAI,oBAAoB,WAAW,EAAG,QAAO;AAC7C,WACE,qBAAC,SAAI,oBAAiB,IAAG,WAAW,GAAG,iBAAiB,UAAU,UAAU,OAAO,GACjF;AAAA,0BAAC,iBAAc,SAAS,qBAAqB,KAAU,SAAkB;AAAA,MACxE;AAAA,OACH;AAAA,EAEJ;AAQA,QAAM,qBAAqB,cAAc,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS;AAMlE,QAAM,CAAC,WAAW,YAAY,IAAI,SAAoB,KAAK;AAC3D,QAAM,gBAAgB,CAAC,OAA2B;AAChD,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,MAAM,EAAE;AAClB,QAAI,MAAM,UAAW,cAAa,CAAC;AAAA,EACrC;AAQA,QAAM,YAAY,cAAc,CAAC;AACjC,QAAM,eAAe,OAAgC,IAAI;AACzD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,KAAK;AAE9D,kBAAgB,MAAM;AACpB,UAAM,KAAK,aAAa;AACxB,QAAI,CAAC,aAAa,CAAC,GAAI;AACvB,wBAAoB,GAAG,eAAe,GAAG,eAAe,CAAC;AAAA,EAC3D,CAAC;AACD,YAAU,MAAM;AACd,UAAM,KAAK,aAAa;AACxB,QAAI,CAAC,aAAa,CAAC,MAAM,OAAO,mBAAmB,YAAa;AAChE,UAAM,KAAK,IAAI,eAAe,MAAM,oBAAoB,GAAG,eAAe,GAAG,eAAe,CAAC,CAAC;AAC9F,OAAG,QAAQ,EAAE;AACb,QAAI,GAAG,cAAe,IAAG,QAAQ,GAAG,aAAa;AACjD,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,EAAE,aAAa,qBAAqB,YAAY,IAAI,gBAAgB;AAAA,IACxE,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAOD,QAAM,cACJ,CAAC,CAAC,cAAc,UAAU,gBAAiB,UAAU,eAAe,MAAM,SAAS;AACrF,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAS,KAAK;AAC5D,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,MAAI,gBAAgB,cAAc;AAGhC,oBAAgB,WAAW;AAC3B,QAAI,CAAC,YAAa,oBAAmB,KAAK;AAAA,EAC5C;AACA,QAAM,iBAAiB,CAAC,CAAC,aAAa,CAAC,WAAW,mBAAmB;AAGrE,QAAM,EAAE,WAAW,aAAa,WAAW,gBAAgB,eAAe,IAAI,kBAAkB;AAAA,IAC9F;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAoBD,QAAM,gBAAgB,CAAC,CAAC;AACxB,QAAM,CAAC,aAAa,cAAc,IAAI,SAAiC,IAAI;AAC3E,QAAM,YACJ,gBAAgB,QAAQ,MAAM,KAAK,CAAC,MAAM,OAAO,CAAC,MAAM,WAAW,IAC/D,cACA,MAAM,SACJ,OAAO,MAAM,CAAC,CAAC,IACf;AAOR,QAAM,YAAY,CAAC,CAAC,cAAc,UAAU,MAAM,SAAS;AAC3D,QAAM,cAAc,OAA8C,MAAS;AAC3E,QAAM,eAAmC;AAAA,IACvC,WAAW,YACP,CAAC,MAAM,QAAQ;AACb,YAAM,SAAS,aAAa,MAAM,KAAK,MAAM,MAAM;AACnD,UAAI,WAAW,KAAM,QAAO;AAC5B,kBAAY,UAAU,MAAM;AAC5B,aAAO;AAAA,IACT,IACA;AAAA,IACJ;AAAA,IACA,UAAU,WAAW;AAAA,IACrB,YAAY,CAAC,QAAQ,CAAC,CAAC,WAAW,WAAW,GAAG;AAAA,IAChD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,CAAC,QAAQ;AACpB,yBAAmB,IAAI;AACvB,gBAAU,OAAO,GAAG,CAAC;AACrB,UAAI,aAAa,CAAC,UAAU,WAAW,GAAG,EAAG,WAAU,SAAS,KAAK,IAAI;AAAA,IAC3E;AAAA,EACF;AACA,QAAM,SAAS,OAAO,YAAY;AAClC,kBAAgB,MAAM;AACpB,WAAO,UAAU;AAAA,EACnB,CAAC;AAID,QAAM,kBAAkB,wBAAwB,CAAC,SAAS,YAAY,KAAK,CAAC,MAAM,aAAa,CAAC,CAAC,IAAI;AACrG,QAAM,sBAAsB,kBAAkB,iBAAiB,eAAe,IAAI;AAMlF,QAAM,aAAa,CAAC,EAAE,mBAAmB;AACzC,QAAM,cAAc,MAAM;AACxB,QAAI,gBAAiB,cAAa,eAAe;AAAA,EACnD;AAIA,QAAM,kBAAkB,CAAC,QAAW;AAClC,UAAM,MAAM,OAAO,GAAG;AACtB,UAAM,WAAW,aAAa,GAAG,KAAK;AACtC,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,KAAK;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,WAAW,WAAW,iBAAiB,GAAG,IAAI;AAAA,QAC9C;AAAA,QACA,WAAW,eAAe,GAAG;AAAA,QAC7B,OAAO,gBAAgB,GAAG;AAAA,QAC1B,aAAa,CAAC,CAAC;AAAA,QACf,MAAM,qBAAqB,UAAU,GAAG,IAAI;AAAA,QAC5C;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAa,CAAC,CAAC;AAAA,QACf,cAAc;AAAA,QACd,YAAY,UAAU,GAAG;AAAA,QACzB,UAAU,CAAC,CAAC,aAAa,UAAU,WAAW,GAAG;AAAA;AAAA,MAvB5C;AAAA,IAwBP;AAAA,EAEJ;AAMA,QAAM,WAAW,OAAgC,IAAI;AACrD,QAAM,gBAAgB,UAAU,OAAO,SAAS,IAAI,UAAU,OAAO,MAAM,IAAI;AAC/E,QAAM,WAAW;AAAA,IACf;AAAA,IACA,CAAC,CAAC;AAAA,IACF,GAAG,OAAO,MAAM,IAAI,oBAAoB,CAAC,CAAC,UAAU,IAAI,gBAAgB,CAAC,GAAG,OAAO,EAAE;AAAA,EACvF;AAGA,QAAM,gBAAgC,CAAC;AACvC,QAAM,eAA8D,CAAC;AACrE,MAAI,WAAW;AACb,QAAI,CAAC,cAAe,eAAc,KAAK,EAAE,OAAO,GAAG,KAAK,MAAM,QAAQ,OAAO,UAAU,OAAO,EAAE,CAAC;AAAA;AAE/F,iBAAW,MAAM,eAAe;AAC9B,cAAM,QAAQ,cAAc;AAC5B,YAAI,CAAC,GAAG,UAAU;AAChB,wBAAc,KAAK,EAAE,OAAO,GAAG,OAAO,KAAK,GAAG,QAAQ,GAAG,KAAK,QAAQ,OAAO,QAAQ,MAAM,CAAC;AAC5F,uBAAa,KAAK,EAAE,IAAI,IAAI,KAAK,CAAC;AAClC;AAAA,QACF;AACA,WAAG,SAAS,QAAQ,CAAC,IAAI,MAAM;AAC7B,wBAAc,KAAK,EAAE,OAAO,GAAG,OAAO,KAAK,GAAG,QAAQ,GAAG,KAAK,QAAQ,OAAO,MAAM,IAAI,WAAW,QAAQ,MAAM,CAAC;AACjH,uBAAa,KAAK,EAAE,IAAI,GAAG,CAAC;AAAA,QAC9B,CAAC;AAAA,MACH;AAAA,EACJ;AACA,QAAM,kBAA4B,CAAC;AACnC,MAAI,aAAa,cAAc,YAAa,OAAM,QAAQ,CAAC,GAAG,MAAM,WAAW,CAAC,KAAK,gBAAgB,KAAK,CAAC,CAAC;AAC5G,QAAM,iBAAiB,CAAC,GAAI,YAAY,CAAC,kBAAkB,IAAI,CAAC,GAAI,GAAG,eAAe,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;AACvG,QAAM,kBAAkB,IAAI;AAAA,IAC1B,eAAe,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,EAC/F;AACA,QAAM,YAAY,aAAa;AAAA,IAC7B,SAAS;AAAA,IACT,MAAM;AAAA,IACN;AAAA,IACA,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,YAAY,CAAC,SAAS;AAAA,IACtB,aAAa;AAAA,IACb;AAAA,IACA,SAAS,OAAO,eAAe,WAAW,aAAa;AAAA,IACvD;AAAA,IACA,YAAY,KAAK,aAAa,IAAI;AAAA,IAClC,WAAW,aAAa,IAAI;AAAA,IAC5B,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU,GAAG,OAAO,QAAQ,MAAM,EAAE,KAAK,GAAG,CAAC,IAAI,OAAO,IAAI,KAAK,IAAI,MAAM,MAAM,IAC/E,MAAM,SAAS,GAAG,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,OAAO,MAAM,MAAM,SAAS,CAAC,CAAC,CAAC,KAAK,EAC5E;AAAA,EACF,CAAC;AAED,QAAM,aAAa,WAAW;AAC9B,kBAAgB,MAAM;AACpB,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,UAAU,CAAC;AAGf,QAAM,cAAc,OAAO,kBAAkB,aAAa,OAAQ,iBAAiB;AACnF,QAAM,YAAY,OAAO,kBAAkB,aAAa,gBAAgB,eAAe;AACvF,QAAM,eAAoD,YACtD,YAAY,OAAqC,CAAC,KAAK,QAAQ;AAC7D,UAAM,MAAM,UAAU,GAAG;AACzB,UAAM,OAAO,IAAI,IAAI,SAAS,CAAC;AAC/B,QAAI,QAAQ,KAAK,QAAQ,IAAK,MAAK,KAAK,KAAK,GAAG;AAAA,QAC3C,KAAI,KAAK,EAAE,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC;AAClC,WAAO;AAAA,EACT,GAAG,CAAC,CAAC,IACL;AAIJ,QAAM,OAAO,QAAQ,aAAa;AAElC,SACE;AAAA,IAAC;AAAA;AAAA,MAGC,gBAAc;AAAA,MACd,cAAY,QAAQ,SAAY;AAAA,MAKhC,cAAY,QAAQ,KAAK;AAAA,MAIzB,oBAAkB,aAAa,KAAK;AAAA,MACpC,WAAW;AAAA,QACT;AAAA,QACA,QAAQ,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAM1B,cAAc;AAAA,QACd;AAAA,MACF;AAAA,MAOA;AAAA,4BAAC,UAAM,GAAG,aAAa;AAAA,QACtB,WAAW,QAAQ,YAAY,SAC9B;AAAA,UAAC;AAAA;AAAA,YACC,sBAAmB;AAAA,YACnB,WAAW;AAAA,cACT;AAAA,cACA,QACI,GAAG,0BAA0B,UAAU,gBAAgB,WAAW,IAClE;AAAA,YACN;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QAED,CAAC,UACF,qBAAC,SAAI,KAAK,eAAe,WAAW,GAAG,cAAc,8BAA8B,GAI/E;AAAA,YAAC,cAAc,oBACf;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA,UAAU;AAAA,cACV,mBAAmB,CAAC,CAAC;AAAA,cACrB,aAAa;AAAA,cACb,eAAe;AAAA,cACf,YAAY;AAAA,cACZ;AAAA,cACA;AAAA;AAAA,UACF;AAAA,UAED,cACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,eAAe,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,kBAAkB,CAAC,CAAC,EAAE,MAAM;AAAA,cACpF;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,QAAQ,CAAC,QAAQ;AACf,oBAAI,QAAQ,MAAM;AAChB,sBAAI,MAAM,SAAS,EAAG,aAAY,CAAC,GAAG,MAAM,CAAC,EAAE,GAAG;AAClD;AAAA,gBACF;AACA,oBAAI,MAAM,CAAC,GAAG,QAAQ,IAAK;AAE3B,sBAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,GAAG;AAC7C,4BAAY,CAAC,EAAE,KAAK,KAAK,KAAK,aAAa,MAAM,CAAC,GAAG,GAAG;AAAA,cAC1D;AAAA,cACA,QAAQ,MAAM;AACZ,oBAAI,MAAM,WAAW,EAAG;AACxB;AAAA,kBACE,MAAM,IAAI,CAAC,GAAG,MAAO,MAAM,IAAI,EAAE,GAAG,GAAG,KAAK,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAE;AAAA,kBACjE,MAAM,CAAC,EAAE;AAAA,gBACX;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UAED,aACC;AAAA,YAAC;AAAA;AAAA,cACC,QAAQ;AAAA,cACR,aAAa,UAAU;AAAA,cACvB,cAAc,UAAU;AAAA,cACxB;AAAA,cACA;AAAA,cACA,SAAS,MAAM,mBAAmB,IAAI;AAAA,cACtC,aAAa,UAAU;AAAA,cACvB,UAAU,MAAM;AACd,mCAAmB,KAAK;AACxB,oBAAI,YAAa,WAAU,YAAY,KAAK;AAAA,cAC9C;AAAA;AAAA,UACF;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,WAAW;AAAA,gBACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAOA,cAAc;AAAA,gBACd,cAAc,oBAAoB;AAAA;AAAA,gBAElC,aAAa,YAAY,SAAS,KAAK;AAAA,cACzC;AAAA,cACA,aAAW,aAAa;AAAA,cAEvB;AAAA,+BACG,aAAa,IAAI,CAAC,MAChB,qBAACA,WAAA,EACC;AAAA,uCAAC,QAAG,WAAU,2KACZ;AAAA,wCAAC,UAAK,WAAU,WACb,6BACG,iBAAiB,EAAE,GAAG,IACtB,aAAa,QACX,YAAY,MAAM,EAAE,KAAK,EAAE,IAAI,IAC/B,EAAE,KACV;AAAA,oBACC,aAAa,OAAO,oBAAC,UAAK,WAAU,wBAAwB,sBAAY,IAAI,EAAE,KAAK,EAAE,IAAI,GAAE;AAAA,qBAC9F;AAAA,kBACC,EAAE,KAAK,IAAI,eAAe;AAAA,qBAXd,YAAY,EAAE,GAAG,EAYhC,CACD,IACD,YAAY,IAAI,eAAe;AAAA,gBAClC,YAAY,WAAW,KACtB;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW;AAAA,sBACT;AAAA,sBACA,UAAU,sBAAsB;AAAA,oBAClC;AAAA,oBAEC,sBAAY,oBAAC,eAAY,OAAO,OAAO,SAAS,IAAM,SAAS,OAAO;AAAA;AAAA,gBACzE;AAAA,gBAID,uBACC;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK;AAAA,oBACL,WAAU;AAAA,oBAGV,8BAAC,SAAK,iBAAO,SAAQ;AAAA;AAAA,gBACvB;AAAA,gBAGD,YAAY,CAAC,uBACZ;AAAA,kBAAC;AAAA;AAAA,oBACC,IAAG;AAAA,oBACH;AAAA,oBACA,UAAU,OAAO;AAAA,oBACjB;AAAA,oBACA;AAAA;AAAA,gBACF;AAAA;AAAA;AAAA,UAEJ;AAAA,UAQC,cAAc,cAAc,KAAK,CAAC,MAAM,EAAE,MAAM,KAC/C;AAAA,YAAC;AAAA;AAAA,cACC,qBAAkB;AAAA,cAClB,mBAAiB,eAAe,OAAO,kBAAkB;AAAA,cACzD,WAAW;AAAA,gBACT;AAAA,gBACA,UAAU,cAAc;AAAA,gBACxB,aAAa;AAAA,cACf;AAAA,cAEC;AAAA,+BAAe,QACd,oBAAC,SAAI,IAAI,iBAAiB,WAAW,GAAG,iBAAiB,UAAU,iBAAiB,MAAM,GACvF,uBACH;AAAA,gBAEF;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW;AAAA,sBACT;AAAA,sBACA,UAAU,sBAAsB;AAAA,oBAClC;AAAA,oBAEC,wBACE,OAAO,CAAC,MAAM,EAAE,MAAM,EACtB,IAAI,CAAC,QACJ,qBAACA,WAAA,EACC;AAAA,0CAAC,QAAG,WAAU,0DACX,cAAI,QACP;AAAA,sBACA,oBAAC,QAAG,WAAU,kDACX,sBAAY,GAAG,GAClB;AAAA,yBANa,IAAI,GAOnB,CACD;AAAA;AAAA,gBACL;AAAA;AAAA;AAAA,UACF;AAAA,UAID,YAAY,CAAC,YACZ;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN;AAAA,cACA,UAAU;AAAA,cACV,OAAO;AAAA,cACP,QAAQ;AAAA,cACR,YAAY,iBAAkB;AAAA,cAC9B;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,UAIF;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,SAAS;AAAA,cACT,YAAY,OAAO;AAAA,cACnB,QAAQ,kBAAkB,kBAAkB,KAAK,eAAe,IAAI;AAAA,cACpE,YAAY;AAAA,cAEX;AAAA;AAAA,UACH;AAAA,WACF;AAAA,QAEC,UACD,qBAAC,SAAI,WAAW,GAAG,QAAQ,cAAc,QAAQ,GAI/C;AAAA,+BAAC,SAAI,WAAW,GAAG,2BAA2B,cAAc,eAAe,GAOzE;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,KAAK;AAAA,gBAEL,cAAW;AAAA,gBAIX,WAAW,GAAG,2CAA2C,cAAc,uBAAuB;AAAA,gBAG7F,GAAG;AAAA,gBACJ,OACE,aACI,cAAc,QACd,EAAE,GAAG,cAAc,OAAO,WAAW,cAAc;AAAA,gBAQ7D;AAAA;AAAA,oBAAC;AAAA;AAAA,sBACC,KAAK;AAAA,sBACL,WAAW,GAAG,UAAU,UAAU,YAAY,SAAS;AAAA,sBACvD,iBAAe,YAAY,KAAK;AAAA,sBAChC,cAAY,OAAO;AAAA,sBACnB,aAAW,aAAa;AAAA,sBAExB,iBAAe,WAAW;AAAA,sBAC1B,oBAAkB,YAAY,KAAK;AAAA,sBAIlC;AAAA,qCACC,oBAAC,cACE,yBAAe,IAAI,CAAC,QAAQ;AAC3B,gCAAM,IAAI,UAAU,YAAY,GAAG;AACnC,iCAAO,oBAAC,SAAc,OAAO,MAAM,SAAY,EAAE,OAAO,EAAE,IAAI,UAA7C,GAAwD;AAAA,wBAC3E,CAAC,GACH;AAAA,wBAWF;AAAA,0BAAC;AAAA;AAAA,4BACC;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA,UAAU;AAAA,4BACV,iBAAiB,CAAC,CAAC;AAAA,4BACnB,mBAAmB,CAAC,CAAC;AAAA,4BACrB;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA;AAAA,4BACA,cAAc,YAAY,IAAI;AAAA,4BAC9B,cAAc,WAAW;AAAA,4BACzB,kBAAkB;AAAA;AAAA,wBACpB;AAAA,yBACE,MAAM;AACN,gCAAM,YAAY,GAAG,aAAa,MAAM,SAAS,KAAK,+BAA+B;AACrF,gCAAM,UAAU,aACd;AAAA,4BAAC;AAAA;AAAA,8BACC,SAAS;AAAA,8BACT,QAAQ;AAAA,8BACR,cAAc,YAAY,IAAI;AAAA,8BAE7B;AAAA;AAAA,0BACH,IACE;AAIJ,gCAAM,YAAY,CAAC,KAAQ,aAAqB;AAC9C,kCAAM,WAAW,aAAa,GAAG,KAAK;AACtC,kCAAM,MAAM,OAAO,GAAG;AACtB,mCACE;AAAA,8BAAC;AAAA;AAAA,gCAEC;AAAA,gCACA;AAAA,gCACA,QAAQ;AAAA,gCACR,KAAK;AAAA,gCACL,SAAS;AAAA,gCACT;AAAA,gCACA;AAAA,gCACA;AAAA,gCACA,MAAM,UAAU,GAAG;AAAA,gCACnB,iBAAiB,CAAC,CAAC;AAAA,gCACnB,YAAY,UAAU,GAAG;AAAA,gCACzB,UAAU,CAAC,CAAC,aAAa,UAAU,WAAW,GAAG;AAAA,gCACjD;AAAA,gCACA,WAAW,WAAW,cAAc,GAAG,IAAI;AAAA,gCAC3C,YAAY,iBAAiB,CAAC,CAAC;AAAA,gCAC/B,UAAU,GAAG,YAAY,IAAI,QAAQ;AAAA,gCACrC;AAAA,gCACA,gBAAgB,CAAC,CAAC,cAAc,CAAC,CAAC;AAAA,gCAClC,UAAU,QAAQ;AAAA,gCAClB,WAAW,eAAe,GAAG;AAAA,gCAC7B,OAAO,gBAAgB,GAAG;AAAA,gCAC1B;AAAA,gCACA;AAAA,gCACA,iBAAiB,OAAO;AAAA,gCACxB,gBAAgB,OAAO;AAAA,gCACvB,cAAc,WAAW,QAAQ,QAAQ;AAAA;AAAA,8BA1BpC;AAAA,4BA2BP;AAAA,0BAEJ;AACA,gCAAM,WACJ,MAAM,WAAW,IACf,oBAAC,QACC;AAAA,4BAAC;AAAA;AAAA,8BACC,SAAS;AAAA,8BACT,WAAW,GAAG,0BAA0B,UAAU,cAAc,WAAW;AAAA,8BAI1E,sBAAY,oBAAC,eAAY,OAAO,OAAO,SAAS,IAAM,SAAS,OAAO;AAAA;AAAA,0BACzE,GACF,IACE;AACN,gCAAM,OAAO,CACX,OACA,KACA,GACA,iBAEA;AAAA,4BAAC;AAAA;AAAA,8BACC;AAAA,8BACA,SAAS;AAAA,8BACT,KAAK,UAAU,IAAI,SAAS,CAAC,IAAI,SAAS,CAAC;AAAA,8BAC3C;AAAA,8BACA,OAAO,IAAI,QAAQ,IAAI,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE;AAAA,8BAChD,KAAK,IAAI,MAAM,EAAE,KAAK,EAAE,IAAI;AAAA,8BAC5B;AAAA;AAAA,0BACF;AAEF,8BAAI,WAAW;AAIb,kCAAM,SAAS,CAAC,KAAa,WAC3B,oBAAC,QAAa,eAAY,QAAO,kBAAe,IAC9C,8BAAC,QAAG,SAAS,cAAc,OAAO,EAAE,QAAQ,SAAS,GAAG,QAAQ,EAAE,GAAG,KAD9D,GAET;AAEF,mCACE,iCACG;AAAA,yCAAW,oBAAC,WAAM,WAAW,WAAY,mBAAQ;AAAA,8BACjD,UAAU,OAAO,IAAI,CAAC,MAAM;AAC3B,oCAAI,EAAE,SAAS;AACb,yCACE,oBAAC,WAAkB,gBAAa,IAAG,eAAY,QAC5C,iBAAO,KAAK,EAAE,MAAM,KADX,EAAE,GAEd;AAEJ,sCAAM,IAAI,cAAc,EAAE,KAAK;AAC/B,sCAAM,KAAK,aAAa,EAAE,KAAK;AAC/B,uCACE;AAAA,kCAAC;AAAA;AAAA,oCAEC,gBAAa;AAAA,oCACb,WAAW;AAAA,oCAEV;AAAA,4CAAM,EAAE,MAAM,SAAS,CAAC,KAAK,KAAK,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,UAAU,SAAS,EAAE,OAAO,CAAC,CAAC;AAAA,sCACrF,IAAI,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,UAAU,SAAS,EAAE,OAAO,CAAC,CAAC;AAAA,sCAClE,EAAE,MAAM;AAAA,wCAAI,CAAC,OACZ,GAAG,SAAS,QAAQ,OAAO,GAAG,KAAK,GAAG,MAAM,IAAI,UAAU,MAAM,GAAG,KAAK,GAAG,GAAG,KAAK;AAAA,sCACrF;AAAA;AAAA;AAAA,kCARK,KAAK,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,OAAO,EAAE,IAAI,EAAE,KAAK,KAAK;AAAA,gCAS/D;AAAA,8BAEJ,CAAC;AAAA,+BACH;AAAA,0BAEJ;AACA,8BAAI,CAAC,eAAe;AAClB,mCACE,qBAAC,WAAM,WAAW,WACf;AAAA;AAAA,8BACA,MAAM,IAAI,SAAS;AAAA,8BACnB;AAAA,+BACH;AAAA,0BAEJ;AAIA,gCAAM,SAAS,CAAC,MAAoC,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM,UAAU,GAAG,EAAE,QAAQ,CAAC,CAAC;AAClG,iCACE,iCACI;AAAA,wCAAW,aACX,qBAAC,WAAM,WAAW,WACf;AAAA;AAAA,8BACA;AAAA,+BACH;AAAA,4BAED,cAAc;AAAA,8BAAI,CAAC,OAClB,GAAG,WACD,GAAG,SAAS,IAAI,CAAC,IAAI,MACnB,qBAAC,WAAgD,WAAW,WACzD;AAAA,sCAAM,KAAK,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE;AAAA,gCAChC,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE;AAAA,gCACrB,OAAO,EAAE;AAAA,mCAHA,KAAK,GAAG,GAAG,IAAI,GAAG,GAAG,IAAI,GAAG,KAAK,EAI7C,CACD,IAED,qBAAC,WAAsC,WAAW,WAC/C;AAAA,qCAAK,GAAG,OAAO,CAAC,GAAG,EAAE;AAAA,gCACrB,OAAO,EAAE;AAAA,mCAFA,KAAK,GAAG,GAAG,IAAI,GAAG,KAAK,EAGnC;AAAA,4BAEJ;AAAA,6BACF;AAAA,wBAEJ,GAAG;AAAA,wBACF;AAAA;AAAA;AAAA;AAAA;AAAA,wBAMC;AAAA,0BAAC;AAAA;AAAA,4BACC,qBAAkB;AAAA,4BAClB,WAAW;AAAA,8BACT;AAAA,8BACA,aAAa;AAAA,4BACf;AAAA,4BAEA,+BAAC,QAAG,iBAAe,WAAW,UAG3B;AAAA,2CACC;AAAA,gCAAC;AAAA;AAAA,kCACC,WAAW;AAAA,oCACT;AAAA,oCACA,UAAU,kBAAkB;AAAA,kCAC9B;AAAA;AAAA,8BACF;AAAA,8BAED,eAAe,IAAI,CAAC,QAAQ;AAC3B,sCAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,sCAAM,SAAS,QAAQ,EAAE,OAAO,UAAU,OAAO,UAAU,MAAM,IAAI;AACrE,sCAAM,UAAU,QAAQ;AACxB,sCAAM,OAAO,UAAU,OAAO;AAC9B,uCACE;AAAA,kCAAC;AAAA;AAAA,oCAEC,YAAU,IAAI;AAAA,oCACd,OAAO,UAAU,QAAQ;AAAA,oCACzB,OAAO;AAAA,oCACP,WAAW;AAAA,sCACT;AAAA,sCACA;AAAA,sCACA,UAAU,cAAc;AAAA,sCACxB,SAAS;AAAA;AAAA;AAAA,sCAGT,WAAW;AAAA,sCACX,IAAI,OAAO,kBAAkB,aAAa;AAAA,sCAC1C,aAAa,IAAI,SAAS;AAAA,oCAC5B;AAAA,oCAEC,cAAI,SAAS,YAAY,GAAG,IAAI,UAAU,cAAc;AAAA;AAAA,kCAhBpD,IAAI;AAAA,gCAiBX;AAAA,8BAEJ,CAAC;AAAA,+BACH;AAAA;AAAA,wBACF;AAAA;AAAA;AAAA,kBAEJ;AAAA,kBAGC,YACC,oBAAC,2BAAwB,UAAoB,UAAU,OAAO,QAAQ,QAAgB,SAAkB;AAAA;AAAA;AAAA,YAE5G;AAAA,YACK,CAAC,WAAW,CAAC,YACZ;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN;AAAA,gBACA,UAAU;AAAA,gBACV,OAAO;AAAA,gBACP,QAAQ;AAAA,gBACR,YACE,WACI,iBAAkB,mBAClB,CAAC,MAAM;AAEL,sBAAI,iBAAkB,kBAAiB,CAAC;AAAA,sBACnC,aAAY,CAAC;AAClB,0BAAQ,CAAC;AAAA,gBACX;AAAA,gBAEN;AAAA,gBACA;AAAA,gBACA;AAAA;AAAA,YACF;AAAA,aAEJ;AAAA,UACC,kBACC;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,WAEJ;AAAA;AAAA;AAAA,EAEF;AAEJ;AAGA,SAAS,WAAW,EAAE,UAAU,GAAG,KAAK,GAAyC;AAC/E,SACE,oBAAC,QAAK,OAAK,MAAE,GAAG,MACb,UACH;AAEJ;AAIA,SAAS,UAAU,OAA6C;AAC9D,SAAO,oBAAC,SAAK,GAAG,OAAO;AACzB;AAGA,SAAS,YAAY,EAAE,MAAM,GAAsB;AAKjD,SACE,qBAAC,UAAK,WAAU,kCACd;AAAA,wBAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,IACzC,oBAAC,SAAK,iBAAM;AAAA,KACd;AAEJ;","names":["Fragment"]}
1
+ {"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { Card } from \"./card\";\nimport { Spinner } from \"./spinner\";\nimport { isTableOwnColumn, useRowActionColumn } from \"./data-table-row-actions\";\nimport { useColumnResize } from \"./data-table-resize\";\nimport { useRangeSelection } from \"./data-table-selection\";\nimport { columnLabel, logicalAlign, resolveDataTableLabels } from \"./data-table-shared\";\nimport {\n MobileFilters,\n MobileSelectBar,\n MobileSort,\n StackedFields,\n keepsOwnClicks,\n mobileCardColumns,\n useCardColumnWarnings,\n} from \"./data-table-mobile\";\nimport { DataTableMobileRow, DataTableRow, type DataTableRowCtx } from \"./data-table-row\";\nimport { DataTableHead } from \"./data-table-head\";\nimport { DataTableLeadingRow } from \"./data-table-leading-row\";\nimport {\n DataTableGroupBand,\n DataTableLoadMoreFooter,\n groupLevels,\n groupRows,\n useGroupBandTops,\n type DataTableGroupBy,\n type DataTableLoadMore,\n type RowGroup,\n} from \"./data-table-groups\";\nexport type { DataTableGroupBy, DataTableLoadMore } from \"./data-table-groups\";\nimport { VIRTUAL_SELECT_COL, rowNavTarget, useRowWindow, type DataTableVirtualize, type VirtualGroup } from \"./data-table-virtual\";\nexport type { DataTableVirtualize } from \"./data-table-virtual\";\nimport { ColumnSettingsPanel } from \"./data-table-column-settings\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { cn } from \"../lib/cn\";\nimport { isFilterActive, resolveFilter, rowMatches } from \"./data-table-filters\";\nimport type { ColumnFilter, FilterValue } from \"./data-table-filters\";\nimport { nextSorts } from \"./data-table-sort\";\nimport type { SortCycle, SortDir, SortState } from \"./data-table-sort\";\nimport { Pagination } from \"./data-table-pagination\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { RowActionsCollapse } from \"./row-actions\";\nimport type { CommitScope } from \"./write-lock\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale, useKitTableStorageKeyPrefix, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { dirOf, type Direction } from \"../lib/direction\";\n\n// ---------- Types ----------\n\nexport interface DataTableColumn<T> {\n key: string;\n header: ReactNode;\n cell: (row: T) => ReactNode;\n sortBy?: (row: T) => string | number | null | undefined;\n filter?: ColumnFilter<T>;\n filterBy?: (row: T) => string;\n className?: string;\n headClassName?: string;\n // Mobile card layout: when no `mobilePrimary` column is set the first\n // visible non-hidden column acts as the primary. The primary cell renders\n // bold at the top of the card without a label; the rest stack below as\n // labelled key/value pairs. `mobileHidden` skips a column entirely on\n // narrow viewports — useful for noisy details (IDs, raw URLs, internal\n // bookkeeping) that would crowd a card without aiding scanning.\n mobilePrimary?: boolean;\n mobileHidden?: boolean;\n /**\n * This column on the phone's default card (0.49 KIT-7; ignored with `mobileCard`):\n * `\"primary\"` — the card's title, as `mobilePrimary`; `\"secondary\"` — one of its\n * fields; `\"hidden\"` — not on the card, as `mobileHidden`. The card shows at most\n * three fields: the columns marked `\"secondary\"` when any\n * is, else the first three visible columns after the title. A field whose value is\n * empty — the cell returns `null`, `undefined`, `\"\"` or the formatters' empty \"—\" —\n * is left out of that card. The columns the card does not show go into the opened\n * row with `mobileDetailsInRow`. A `noRowLink` column (the cell's own buttons or link)\n * counts as a field: past the third it leaves the card with its action, and a\n * development build says so once per table and column key. Give a column of row\n * actions `role: \"actions\"` instead: it is then no field, and the card draws it after\n * them.\n */\n mobile?: \"primary\" | \"secondary\" | \"hidden\";\n /**\n * What the column is (0.49 KIT-7), for the phone's default card; the desktop table is\n * unchanged.\n *\n * - `\"status\"`: its value sits as a chip beside the title instead of as a label/value\n * line, and takes none of the card's three fields. A cell that returns text is drawn\n * in a neutral `Chip`; one that returns an element (a `Chip` with its own tone, a\n * `StatusDot`) is drawn as it is.\n * - `\"actions\"`: the row's buttons (`rowActionsColumn` sets it). Never the card's title\n * and never one of its three fields: the card draws it after them, at its end,\n * whether or not other columns are marked `mobile: \"secondary\"`; only `mobileHidden`\n * / `mobile: \"hidden\"` take it off. Its cell keeps its own clicks, as `noRowLink`:\n * no row link wraps it, and a card that draws it is not a link.\n */\n role?: \"status\" | \"actions\";\n /**\n * Set on a column whose cell renders its own `<a>` (or anything else that must\n * keep its own click): the row anchor of `rowHref` then skips it and lands on\n * the next column that allows wrapping (Keksdose feedback #451). Without it the\n * fallback can wrap such a cell the moment the headline column is hidden from\n * the settings panel — `<a>` inside `<a>`, which React warns about and which\n * kills the inner link outright, since RowLink cancels the click on the capture\n * phase and the cell's own `stopPropagation` then blocks the row handler too.\n * The rejected alternative was sniffing the rendered output for an anchor: a\n * cell that renders a link is a fact its author knows and the renderer cannot\n * see without reaching into React elements it deliberately treats as opaque.\n */\n noRowLink?: boolean;\n /**\n * The direction this column sorts in on its FIRST click. `\"desc\"` for money and\n * count columns, where the rows worth reading are the large ones and an ascending\n * first click opens on the zero rows (keksdose's aggregated report tables). The\n * rest of the cycle follows from it — see {@link DataTableProps.sortCycle}.\n * Default `\"asc\"`.\n */\n firstSort?: SortDir;\n /**\n * The column's name as plain text, for the places a ReactNode `header` cannot go:\n * the phone sort control's options ({@link DataTableProps.mobileSort}), the spoken\n * \"Sorted by …\" and the column-settings checklist. Only needed when `header` is not\n * a string (an icon, a unit suffix); without it those places fall back to the\n * header string, then to `key`.\n */\n headerText?: string;\n /**\n * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a\n * `title`, an `aria-describedby`, a class. keksdose's accounts page\n * (accounts-page.tsx, the name and IBAN columns) is why it exists: its demo mode\n * blurs everything marked `data-private`, and with nowhere to put the marker on the\n * cell it wrapped every value in an inner `<span data-private>` — which blurs the\n * text but not the cell, and has to be remembered in every `cell` renderer.\n *\n * MERGED under the kit's own, which win: `className` is joined before the column's\n * `className` (so the column's alignment is still the alignment), `style` is laid\n * under a resized column's pinned width, and `data-col` and a `role` stay the\n * table's — `data-col` is how a resize finds the column's cells, and a `<td>` that\n * claims another role leaves its row's cell count wrong for a screen reader.\n *\n * On a phone the card field (`<dd>`, or the primary line) gets the `data-*`\n * attributes only: the rest — padding classes, `colSpan`, a width — describe a\n * table cell and would mean something else, or nothing, on a card.\n */\n cellProps?: (row: T) => DataTableCellProps | undefined;\n /** The same for the column's `<th>`, once. `scope`, `aria-sort`, `data-col` and the\n * width a resize pins stay the table's, and the class list goes before\n * `headClassName` for the same reason as in {@link cellProps}. */\n headProps?: DataTableHeadProps;\n /**\n * This column's cell in the table's totals row — a `<tfoot>` on a wide screen, a\n * labelled summary card under the list on a phone. kastlan's trial balance is why\n * it exists: it hand-built a debit/credit/balance strip in a `div` UNDER the table,\n * which lined up with no column, dropped out of the table's semantics, and stayed\n * put when the user hid or resized the columns it was summing.\n *\n * `rows` is every row that passes the current filters (in the current sort order),\n * NOT just the page on screen: a total that changes when the user pages is not a\n * total, and the trial balance has to balance on page 1 as much as on page 7. The\n * page is the second argument for the rare \"subtotal of this page\" footer.\n *\n * Under `serverPagination` the table only ever holds one page, so both arguments are\n * that page — a server-side table computes its totals on the server and returns\n * them here (`footer: () => fmt(response.totalDebit)`), ignoring the arguments.\n *\n * The cell takes the column's `className` (so a `text-end` amount column totals\n * end-aligned), its resized width, and drops out with the column when it is hidden.\n */\n footer?: (rows: T[], page: T[]) => ReactNode;\n /**\n * This column takes the width the others leave, and its text ellipsises (0.48,\n * keksdose live #451): `w-full max-w-0` on its cells, so in the table's auto layout it\n * is \"the rest of the row\" and one long title can no longer widen the table. One per\n * table. It has no resize handle, and a width stored for it (before it grew) is\n * ignored: it always takes the rest. Resize the others to change it. Opt-in: without it nothing\n * changes. It keeps at least 10rem: with {@link fitPriority} columns the table leaves\n * those out first, and scrolls sideways once none is left. Every other cell keeps one\n * line at Normal text size (`whitespace-nowrap`; a column's own\n * `not-large:whitespace-normal` lets its prose wrap), so a figure or a name is never\n * broken in two to make room for it.\n */\n grow?: boolean;\n /**\n * Leave this column out while the TABLE is too narrow for it (0.48, keksdose live\n * #451) — measured on the table's own scroller, so a sidebar, the window and Large\n * text count alike. The lowest number goes first (ties: the later column first), and\n * comes back last, once the scroller is as wide as the table needed with it. Columns\n * without it never go. Desktop only: the phone's card list is not narrowed.\n *\n * The Columns panel counts only what is drawn (\"Columns (8/10)\") and marks a column\n * left out this way \"hidden for width\". Ticking it there keeps it in for as long as\n * the table is mounted (not persisted): the table makes room by leaving out the next\n * one instead, and scrolls sideways once none is left. Unticking hides it as any column.\n */\n fitPriority?: number;\n /**\n * Start this column HIDDEN (0.48, keksdose dev #617) — for as long as the view saved\n * under `storageKey` holds no entry for it: on a first visit, on a device whose saved\n * view predates the column (or the flag), and for a column the app adds later. The\n * user's own choice in the Columns panel always wins: ticking it is remembered\n * (`shown` in the saved view), unticking it again stores it as any hidden column. The\n * default itself is never written into the saved view, so dropping the flag brings the\n * column back on every device where nobody chose. In the panel it is simply unticked —\n * not \"hidden for width\", which `fitPriority` alone says. Ignored, like every hidden\n * column, with `columnSettings={false}`: nothing could bring it back.\n */\n defaultHidden?: boolean;\n}\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** What {@link DataTableColumn.cellProps} may return. */\nexport type DataTableCellProps = TdHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\n/** What {@link DataTableColumn.headProps} takes. */\nexport type DataTableHeadProps = ThHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\nexport interface ServerPagination {\n page: number; // 0-based\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n /** Without it the page size is the server's to choose, and the pager shows no\n * page-size select at all — one that changed nothing was a control that lied. */\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * A page is being fetched. The table marks itself `aria-busy`, dims the rows it is\n * still showing (they are the PREVIOUS page's, and about to go), and replaces the\n * empty state with a loading row when there is nothing to show yet — \"no results\"\n * while the first page is still in flight is a claim the table cannot make.\n *\n * The pager stays usable. Disabling it would drop focus from the very button the\n * user just pressed (a disabled button cannot hold focus), and a second click\n * during a fetch is a request the owner already has to handle for a slow network.\n */\n isLoading?: boolean;\n}\n\nexport interface DataTableProps<T> {\n rows: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string | number;\n defaultPageSize?: number;\n onRowClick?: (row: T) => void;\n /**\n * The URL that shows this row, when one exists — the table then renders a REAL\n * anchor for it (Keksdose feedback #451, \"to be able to middle click with the\n * mouse and open in a new tab\"). A row that opens through `onRowClick` alone is\n * invisible to the browser: no middle click, no ⌘/Ctrl-click, no \"copy link\n * address\", no status-bar preview, no long-press menu on a phone. The\n * alternative was an `onAuxClick` that calls `window.open` — it buys back only\n * the middle click and loses the other four, none of which JavaScript can fake.\n *\n * Return `undefined` for a row that has no address of its own. Only wire this up\n * where the OPEN row is genuinely in the URL (a `?row=` param, a detail route);\n * a row whose expansion lives in local component state has no link to hand out,\n * and a link the target page cannot honour is worse than no link.\n *\n * The anchor covers the row's primary cell, not the whole `<tr>`: an `<a>` may\n * not wrap table cells, and the absolutely-positioned row overlay that is the\n * usual workaround would sit on top of the per-cell controls these rows carry\n * (status toggles, the selection checkbox, nested links) and swallow them.\n */\n rowHref?: (row: T) => string | undefined;\n expandedRow?: (row: T) => ReactNode | null;\n isExpanded?: (row: T) => boolean;\n /**\n * Extra classes for a row — the `<tr>` on desktop, the card on mobile. Merged last,\n * so a `hover:` fill here replaces the row's own `hover:bg-hover`.\n *\n * **A row you paint has to pick its own hover** (0.33, docs/colour-roles-harmonization.md\n * §12.2): the kit cannot see your rest colour, and `--bg-hover` is mixed against the\n * card. The rule: a hover differs from its rest by 1.08:1 or more, and every text role\n * on it keeps 4.5:1.\n * - A row that rests on the well (`bg-surface-2`) hovers to the CARD,\n * `hover:bg-surface`: in light the well is the page, and `--bg-hover` sits 1.04:1\n * from it.\n * - A row whose well MARKS A STATE (selected) keeps its fill and draws a 1 px inside\n * line instead, since the card would read as \"not selected\":\n * `bg-surface-2 hover:bg-surface-2 hover:outline hover:-outline-offset-1 hover:outline-strong`.\n * An outline, not a shadow: a shadow on a `<tr>` is not painted by every engine.\n * - Not `--bg-active` for a selected row: its muted and money text fall to 3.8–4.7:1.\n */\n rowClassName?: (row: T) => string | undefined;\n /**\n * Extra DOM attributes for a row's container — the `<tr>` on desktop, the\n * `<li>` on mobile. For hooks that have to sit on the row itself rather than\n * inside a cell: guided-tour anchors, analytics ids, e2e selectors. Undefined\n * values are dropped, so a per-row conditional needs no filtering at the call\n * site. Not a styling escape hatch — that is `rowClassName`.\n */\n rowAttributes?: (row: T) => Record<string, string | undefined> | undefined;\n /**\n * Set false for a table that is short by construction — an invoice's line\n * items, a wizard's picks — where the pager is chrome around a list that can\n * never need one. Renders every row and drops the footer entirely. Ignored in\n * `serverPagination` mode, where the pager is the only way to reach the rest\n * of the data.\n */\n paginated?: boolean;\n empty?: ReactNode;\n /**\n * If set, the table persists filter / sort / page-size state to localStorage under this key.\n * Use a stable, unique key per table (e.g. \"transactions-table\").\n */\n storageKey?: string;\n /**\n * When true, mirror filter/sort/page state into URL search params so views are\n * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.\n * URL state takes precedence over localStorage on initial load. Requires the\n * consuming app to render inside a react-router Router (0.49: only with `urlSync` — a\n * table without it needs none). With `urlSync` and no Router, a development build\n * throws \"DataTable urlSync requires a Router\"; a production build keeps the view in\n * local state instead.\n *\n * `{ prefix }` namespaces them, for two URL-synced tables on one page:\n * `urlSync={{ prefix: \"inv.\" }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.\n */\n urlSync?: TableUrlSync;\n /**\n * When true, the table fills its parent's height and scrolls INTERNALLY, with the\n * header pinned — so the page itself doesn't add a second, outer scrollbar. The parent\n * must give it a bounded height (e.g. a flex column inside the viewport-locked app\n * shell). See feedback #207.\n *\n * **The list page** (0.40, docs/page-layout-harmonization.md §1): the root carries\n * `data-fill-height`, and from `md` up AppShell's `<main>` turns into a flex column\n * while such a table is inside — the shell's banners keep their height, the page takes\n * the rest. Wrap every element between `<main>` and the table in `FillHeight` (or merge\n * `FILL_HEIGHT_CLASS`); nothing else to wire. The body never shrinks below `basis-48`\n * (12rem, about five rows): on a window too short for that, `<main>` scrolls again.\n *\n * **On a phone** the card list becomes the scroller ONLY when its parent really bounds\n * it — measured, not assumed (0.40, keksdose live #420): the list takes the flex\n * room it is given, and only when its rows overflow that room does it turn\n * `overflow-y-auto overscroll-contain`, with the filter button pinned above. Under an\n * unbounded parent — `AppShell` on a phone, where the DOCUMENT scrolls — it stays in\n * the page's flow and grows to its content. Until 0.40 it was a scroller either way,\n * and a scroller with nothing to scroll and `overscroll-contain` swallowed the page's\n * own scroll: a swipe that started on a card moved nothing. So `fillHeight` is safe\n * to pass at every width; `fillHeight={useBreakpoint(\"md\")}` is no longer needed.\n *\n * The cure for TWO SCROLLBARS (round 0.34 C14, kastlan feedback 55): the default\n * `maxBodyHeight` scrolls the body inside a page that scrolls too. How a page fills\n * the height is in {@link DataTableProps.maxBodyHeight}'s note.\n */\n fillHeight?: boolean;\n /** How tall the scrolling body may get before it scrolls, as a CSS length.\n *\n * Default `calc(100dvh - 12rem)`, which is what every caller had before this\n * existed: a bound is what makes the sticky header work, because a header\n * can only stick to a wrapper that actually scrolls. The cap assumes the\n * table has page chrome above and below it, and a table that is the last\n * thing on its page can afford more — ASPICE Atlas report #12, \"for the 25\n * elements there should in best case be no scrolling necessary\".\n *\n * Ignored under `fillHeight`, which bounds the body by its flex parent\n * instead and is the stronger statement of the same intent.\n *\n * **Two scrollbars** (round 0.34 C14, kastlan feedback 55). The cap scrolls the\n * body INSIDE a page that, in most shells, scrolls itself: a table taller than\n * the cap on a page with anything else on it shows two scrollbars side by side,\n * and the wheel moves whichever is under the pointer. For a list page whose table\n * is the point, fill the height instead, from `md` up (a phone scrolls the page\n * and its card list as one) — docs/page-layout-harmonization.md §1.2 says which\n * pages fill:\n *\n * 1. `<DataTable fillHeight>`: the table marks itself (`data-fill-height`), and\n * AppShell's `<main>` becomes a flex column while it is inside;\n * 2. each element between `<main>` and the table is a `FillHeight` (the page's\n * root, any wrapper on the way), or merges `FILL_HEIGHT_CLASS`.\n *\n * Only the body then scrolls, under its pinned header, and every other page grows\n * with its content as before (kastlan e3985c9, its leases list). A short table that\n * is the last thing on its page can instead drop the cap: `maxBodyHeight=\"none\"`. */\n maxBodyHeight?: string;\n /**\n * When provided, the table renders the supplied rows as-is (no client-side\n * filtering/sorting/slicing) and the pagination footer is driven by these\n * server-controlled values. Column-level filters and sorting are hidden\n * UNLESS the caller takes them over via `onFiltersChange`/`onSortsChange` —\n * the caller is then expected to translate the state into the server query\n * that produces `rows`.\n */\n serverPagination?: ServerPagination;\n /**\n * 0.48.1 (keksdose K-052-1): the rows are already filtered and sorted by the server —\n * render them as given. No client filter, no client sort, no client paging (every row\n * shows, as with `paginated={false}`). The column filters and sort headers behave as\n * under `serverPagination`: hidden unless the owner takes them over with\n * `onFiltersChange` / `onSortsChange`, and then a click only REPORTS the new state (a\n * header click calls `onSortsChange` with the next sort and the rows keep their order\n * until the owner re-queries and passes new ones).\n *\n * Defaults to `true` when {@link loadMore} is set — the slices it loads come from the\n * server in its order, and re-filtering them dropped rows (the other leg of each\n * transfer). Pass `serverRows={false}` for a client table that loads more chunks but\n * filters and sorts what it holds itself. `serverPagination` implies it regardless.\n */\n serverRows?: boolean;\n /**\n * Controlled filter/sort state. When the `on*Change` callback is provided\n * the table stops owning that piece of state: it renders `filters`/`sorts`\n * as given and reports user interactions through the callback (including\n * in serverPagination mode, where the UI is otherwise disabled).\n *\n * Page reset: on a client-side table, a change in the VALUE of `filters` or\n * `sorts` sends the table back to page 1 — whoever made the change, the user\n * through the header or the owner from outside (a saved view, a chip). Compared\n * by value, so passing a fresh but equal object on every render does not reset\n * anything. In `serverPagination` mode the owner's `page` is the page, and\n * resetting it on a filter change stays the owner's job.\n */\n filters?: FilterState;\n onFiltersChange?: (next: FilterState) => void;\n sorts?: SortState[];\n onSortsChange?: (next: SortState[]) => void;\n /**\n * Controlled page size, as `sorts` above: the table pages by it and hands its pager's\n * choice to `onPageSizeChange` (`Infinity` is \"All\"). For tables that page ALIKE —\n * `TranslationReviewPanel`'s groups share one page size as they share one sort\n * (0.35). Left out, the table keeps its own, from `defaultPageSize`, the URL and\n * `storageKey`. A change of value sends a client-side table back to page 1. In\n * `serverPagination` mode the page size is `serverPagination.pageSize`.\n */\n pageSize?: number;\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * Opt-in multi-row selection. When provided, a leading\n * checkbox column is rendered with a select-all box in the header; the owner\n * holds the selected set and reacts via the callbacks. Rows for which\n * `isSelectable` returns false render no checkbox and are excluded from\n * select-all. See feedback #285 (bulk edit).\n *\n * **On a phone** (0.40, keksdose live #421) the cards enter a selection MODE: a long\n * press on a card (500 ms, keksdose's register gesture) selects it and turns every\n * card into a toggle with its box showing; a tap then selects or deselects. A\n * keyboard or screen reader enters through \"Select rows\" (`labels.selectRows`),\n * visually hidden until focused; in the mode a bar above the cards holds \"Select all\n * rows\" and Cancel (`labels.cancelSelection`). The mode lasts while anything is\n * selected — the bulk bar's Clear, the last box unticked or Cancel ends it — and swipe\n * actions rest while it is on. Render the owner's `BulkActionBar` as on desktop; it\n * appears with the first selected row. With `selection` the cards' text is not\n * selectable on a phone, and a linked card's long press selects instead of opening\n * the browser's link menu.\n */\n selection?: {\n isSelectable?: (row: T) => boolean;\n isSelected: (row: T) => boolean;\n onToggle: (row: T, checked: boolean) => void;\n /**\n * Commit a whole Shift+click range in ONE call (audit 2026-09-22).\n *\n * Optional and purely additive: without it the range is still walked row by\n * row, exactly as before. With it, an owner holding a `Set` copies that Set\n * once instead of once per row — a 400-row range was 400 `onToggle` calls,\n * 400 Set copies and 400 renders of this table, all to reach a state the\n * owner could have reached in one.\n *\n * `rows` is already filtered by `isSelectable` and is in the order the table\n * is showing, so it can be applied as-is.\n */\n onToggleMany?: (rows: T[], checked: boolean) => void;\n allSelected: boolean;\n someSelected: boolean;\n onToggleAll: (checked: boolean) => void;\n };\n /** User-facing strings (English defaults); pass translated overrides. Merged over\n * `dataTable` from `<UiKitProvider labels>`, so a call site only states what\n * differs for THIS table — typically `table`, its accessible name. */\n labels?: LabelOverride<DataTableLabels>;\n /** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls\n * back to `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** localStorage namespace prefix for `storageKey` persistence: the stored key is this\n * plus `storageKey`. Falls back to `<UiKitProvider tableStorageKeyPrefix>` (0.34, set\n * your app's prefix there once), then to \"hbui-table:\". This prop wins over both. */\n storageKeyPrefix?: string;\n /**\n * On phones, surface the expanded row's detail in a full-screen dialog instead\n * of unfolding it inline (feedback #204). Desktop always uses inline expansion.\n */\n mobileExpandAsDialog?: boolean;\n /**\n * Show the `mobileHidden` columns in the OPENED row on the phone layout — inline\n * under the card, or in the `mobileExpandAsDialog` dialog — so a column dropped from\n * the card to keep it short is not lost (0.32, docs/text-size-harmonization.md §4:\n * \"a new `mobileDetailsInRow` puts the `mobileHidden` columns into the opened row, so\n * nothing is lost\"). At Large a card holds fewer facts per screen, and dropping one\n * from the card must not drop it from the phone.\n *\n * Each column is a label above its value, BEFORE what `expandedRow` renders: the\n * row's own facts first, then the app's detail or editor. A row opens as it always\n * does (`onRowClick` toggling `isExpanded`); with this set it may open without an\n * `expandedRow` at all. `rowActions`' column is one of them — the actions, which the\n * card otherwise offers only as swipes, reachable by a tap. Desktop is unaffected.\n */\n mobileDetailsInRow?: boolean;\n /**\n * The row's name, as plain text — for the \"⋯\" menu its `rowActions` collapse into at\n * Large (\"Actions for Ada Example\", §10.8). Without it every row's menu button is\n * named \"Actions\", which a screen reader cannot tell apart down the list.\n */\n rowName?: (row: T) => string | undefined;\n /**\n * The phone row dialog's `backCloses` ({@link FullBleedDialog}): whether it pushes\n * its own history entry so Back dismisses it. On by default — right for a row whose\n * expansion is component state, where nothing else would answer Back.\n *\n * **Set it `false` when the open row is in the URL** and opening it PUSHES (a\n * `?row=` written with `useSearchParamState(…, { history: \"replace-on-clear\" })` or\n * `\"push\"`): Back already closes the row through the router, and a second entry on\n * top of the page's is two owners of one Back press (keksdose dev #584). Keep it on\n * when opening the row REPLACES the URL — then nothing was pushed, and the dialog's\n * own entry is the only thing Back can pop.\n *\n * The table cannot decide this for you: `isExpanded` is a predicate, and neither it\n * nor `rowHref` says whether opening pushed or replaced (keksdose's feedback page\n * has `rowHref` with a REPLACING `?row=`, and needs the entry). Ignored unless\n * `mobileExpandAsDialog`.\n */\n mobileDialogBackCloses?: boolean;\n /**\n * Group the mobile card list into sections, each with a sticky header — the\n * \"assistance\"-style list (like the transactions list grouped by date, but by\n * whatever key you return, e.g. a name's first letter). The returned string is\n * the section a row belongs to; rows must already be ordered so equal keys are\n * contiguous (pair with a matching default sort). `mobileGroupLabel` formats\n * the header. Desktop is unaffected. Feedback #317.\n */\n mobileGroupBy?: ((row: T) => string) | DataTableGroupBy<T>;\n /** The section header's words for `mobileGroupBy`'s key. Wins over a level's own\n * `label` when `mobileGroupBy` is a {@link DataTableGroupBy}. */\n mobileGroupLabel?: (key: string) => ReactNode;\n /**\n * Fully replace the default mobile card body (bold primary + labelled\n * key/value rows) with a compact custom layout, while keeping the shared\n * clickable/expandable row wrapper. Use when the stacked label/value grid\n * wastes space (feedback #317). Desktop is unaffected.\n *\n * It replaces the WHOLE body, `mobilePrimary` included: a column's\n * `mobilePrimary` cell is not drawn when `mobileCard` is set, so anything that\n * cell carried (keksdose's environment and rework chips, 0.27) must be drawn by\n * the card itself — as `FeedbackMobileCard` does.\n */\n mobileCard?: (row: T) => ReactNode;\n /**\n * A \"Sort by\" select and a direction button above the phone card list (keksdose\n * K10). The card list has no header row, so without it a phone user is stuck with\n * whatever order the table opened in. It drives the same sort state as the column\n * headers — `sorts`/`onSortsChange` when controlled, the URL and `storageKey` when\n * not — and offers the columns a header click could sort (those with a `sortBy`).\n * Each option is named by the column's `headerText`, else its string `header`.\n *\n * Off by default, so no existing phone layout gains a row. Strings:\n * {@link DataTableSortLabels}. Phones only; the desktop table is unchanged.\n */\n mobileSort?: boolean;\n /**\n * Swipe actions for a mobile row, revealed by dragging it horizontally. Return\n * `null` (or empty sides) for rows that should not move — a locked row, one whose\n * mutation is in flight, one the user has expanded.\n *\n * Prefer `start`/`end`: actions committed by dragging the row toward the reading\n * START or END of the line. In a left-to-right layout `end` is a drag to the\n * right, and in a right-to-left one it is a drag to the left — so a \"drag forward\n * to archive\" row stays forward in Arabic or Hebrew without a second definition.\n * `left`/`right` are the physical directions and keep meaning exactly that; each\n * is used only where the logical side for that direction is not given.\n *\n * Mobile only, and deliberately so: the desktop table already has room for an\n * actions column, and a drag gesture on a pointer device is a worse version of a\n * button. Wraps only the row body, so an expansion panel below stays put while the\n * row above it slides. The desktop's buttons for the same actions come with it — see\n * {@link DataTableProps.desktopSwipeActions}.\n */\n mobileSwipeActions?: (row: T) => MobileSwipeActions | null;\n /**\n * The desktop half of `mobileSwipeActions` (0.46, keksdose dev#598 — Marcel: every\n * table whose phone cards swipe gets a desktop actions column with the same actions):\n * an actions column DERIVED from the very plan the card swipes with, so the two\n * viewports offer the same functions and cannot drift. Each button runs the swipe's own\n * `onCommit` — the same confirm, the same undo toast. The reading-end side's actions\n * first, then the start side's, each once (by `SwipeAction.id`, else by label); a row\n * whose plan is `null` gets an empty cell. The column is hidden on the phone, where the\n * actions are the swipes, kept off the column settings, and its tooltips open toward\n * the row's start.\n *\n * On by default wherever `mobileSwipeActions` is given, so a new swipeable table cannot\n * forget it — except beside `rowActions`, whose column is the desktop's already (pass\n * `true` or options to add this one too). `false` where the desktop row carries every\n * one of those actions as its own buttons; say so at the call site. Options:\n * {@link DataTableDesktopSwipeActions}.\n */\n desktopSwipeActions?: boolean | DataTableDesktopSwipeActions<T>;\n /**\n * Renders as a full-width row above the data rows in the desktop table\n * (mobile has no equivalent list-row slot, so it's desktop-only). For a\n * toggle that shows/hides a slice of rows, style it as an expand/collapse\n * disclosure (chevron + label) so it reads as part of the table rather\n * than a floating control above it. See feedback #10/#11 (transactions'\n * \"hide upcoming/scheduled\" toggle).\n */\n leadingRow?: ReactNode;\n /**\n * 0.48 (keksdose dev #603: \"it should rather be sticky, so the user can show hide them\n * everywhere on click\"): pin the {@link leadingRow} under the sticky header while the\n * rows scroll beneath both — its `top` is the header's measured height. Desktop only,\n * as `leadingRow` is; it sticks within the table's own scroller (a bounded body:\n * `fillHeight` or `maxBodyHeight`). Default: it scrolls with the rows.\n */\n leadingRowSticky?: boolean;\n /**\n * 0.48 (keksdose dev #605): group the desktop table's rows under full-width bands, one\n * level or two (period → day). Each band is a row header spanning the visible columns\n * (selection column included, hidden and fit-hidden columns not) with a label and an\n * end slot (counts, totals), and opens its own `<tbody>`, so a screen reader announces\n * it as the reader enters the group; the period band opens the first day's. Rows must\n * already be ordered so equal keys are contiguous (as for `mobileGroupBy`); grouping\n * runs over the rows on screen, page by page.\n *\n * The bands are sticky: the period under the header (and under a sticky\n * {@link leadingRow}), the day under the period — measured offsets, inside the table's\n * own scroller (`fillHeight` or `maxBodyHeight`). The `leadingRow` stays above the first\n * group. On a phone pass the same level to `mobileGroupBy`:\n * `groupBy={[period, day]} mobileGroupBy={period}`. See {@link DataTableGroupBy}.\n */\n groupBy?: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?];\n /**\n * 0.48 (keksdose dev #605): a \"load more\" footer in place of the pager. The table\n * renders every row it is given (as `paginated={false}` does — pass that too, to say\n * so), and the footer asks the owner for the next slice: as it scrolls into view (an\n * IntersectionObserver, guarded against double calls while one is in flight) and with\n * a button the keyboard reaches, named by the app (\"Load August 2026\"). With\n * `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the\n * card list's last item. Since 0.48.1 it implies {@link serverRows} — the rows are\n * rendered in the order given, unfiltered — unless `serverRows={false}`. See\n * {@link DataTableLoadMore}.\n */\n loadMore?: DataTableLoadMore;\n /**\n * 0.48.2 (keksdose dev #632, Marcel: \"The table is extremely slow. Is it virtualised…?\"):\n * render only the desktop rows near the visible part of the scroller, plus `overscan`\n * rows each side ({@link DataTableVirtualize}), with spacer rows standing in for the\n * rest. For long lists — keksdose's register under `loadMore` holds 1000+ rows. Opt-in;\n * without it every row renders, as before.\n *\n * - **Scroller:** the bounded body (`fillHeight` or `maxBodyHeight`), or the page when the\n * body is unbounded (`maxBodyHeight=\"none\"`): the window is what of the table both the\n * scroller and the browser window show.\n * - **Kept rendered:** the header, the `leadingRow`, the group bands (sticky and stacked\n * as before; a period's band while any of its rows is in view), every OPEN row (so an\n * inline editor in `expandedRow` keeps its state), the roving tab stop and a row\n * holding the focus. The `loadMore` footer follows the last spacer and loads once.\n * - **Heights:** each rendered row is measured and remembered by its key; a row not yet\n * seen counts as the measured average (`estimateRowHeight` before any). The scroll\n * height settles as the rows are seen; the browser's scroll anchoring holds the rows in\n * view while a spacer above is corrected (Safari has none: a jump of the difference\n * is possible there on the first pass).\n * - **Columns:** the header widths are measured once and locked in a `<colgroup>`, so the\n * columns do not move as rows come and go; a wider cell scrolling in widens its column\n * (once). Re-measured when the rows, the columns, a width or the scroller's width\n * change. A `grow` column and a resized one are not locked.\n * - **Keyboard and screen reader:** Arrow/Home/End move over every row — the target is\n * rendered, then focused (End reaches the last row); Shift+click ranges and select-all\n * work on the data, not the DOM. The table has `aria-rowcount`, every rendered row its\n * `aria-rowindex` (bands, details and the totals row counted); spacers are\n * `aria-hidden`.\n * - **Trade-offs:** the browser's find-in-page sees only the rendered rows, and so does\n * print; an `id` from `rowAttributes` (an anchor, an `aria-controls` target, a\n * `scrollIntoView` on a fresh row) exists only while its row is rendered. Memoise\n * `columns` (and `mobileCard`): the rows bail out of a render only then, and a scroll\n * renders the rows that come in, not the whole window.\n * - **Phone:** desktop only. The card list renders every row it holds (`loadMore`,\n * `paginated={false}`) or reveals them in chunks as it is scrolled (client paging).\n */\n virtualize?: boolean | DataTableVirtualize;\n /**\n * Row height. `\"compact\"` is lenkbank's in-card tables: five `text-xs` tables with\n * `px-2 py-1` cells sitting inside cards, which at the default padding came out at\n * twice their height and pushed the card's own content below the fold — the whole\n * reason they were still hand-rolled `<table>`s. It tightens the header, the cells,\n * the selection checkboxes, the pager and the phone cards together, so a compact\n * table is compact everywhere rather than a dense body under a roomy header.\n *\n * Defaults to `\"comfortable\"`, which is the table every caller has today.\n */\n density?: DataTableDensity;\n /**\n * What a repeated click on a sorted header does. `\"tri\"` (the default, and the only\n * behaviour before 0.8.0): first direction → the other → unsorted. `\"toggle\"`: first\n * direction ⇄ the other, never unsorted — for a report table that is always ranked\n * by something. Each column's first direction is its own `firstSort`. `aria-sort`\n * and the spoken \"Sorted by …\" follow whatever the click produced, so they are\n * right under either cycle.\n */\n sortCycle?: SortCycle;\n /**\n * The desktop table's power-user chrome, as one switch. `\"full\"` (default) is the\n * table as it has always been. `\"minimal\"` is a compact report table in a half-width\n * card: no column-settings rail, no resize handles, no multi-sort (and so no\n * \"Shift-click to add a sort\" tooltip) — the three pieces that cost width and\n * attention and that a ten-row summary never needs. Each can still be set on its\n * own below; an explicit boolean wins over the preset.\n */\n chrome?: DataTableChrome;\n /**\n * The vertical \"Columns (n/m)\" rail and its show/hide + auto-size panel. Default:\n * on, unless `chrome=\"minimal\"`. With it off, every column is shown and a hidden\n * set persisted under `storageKey` is ignored — there would be no way to get a\n * column back.\n */\n columnSettings?: boolean;\n /**\n * The drag handles on the header's trailing edges. Default: on, unless\n * `chrome=\"minimal\"`. With it off, widths persisted under `storageKey` are ignored\n * for the same reason as above: nothing could reset them.\n */\n resizable?: boolean;\n /**\n * Shift-click adds a secondary sort criterion. Default: on, unless\n * `chrome=\"minimal\"`. With it off, a shift-click sorts like a plain click and the\n * header drops the tooltip that advertises the gesture.\n */\n multiSort?: boolean;\n /**\n * The card around the table — border, surface, radius, shadow. Set false to sit the\n * table inside the app's own card, under its own caption and CSV button, without a\n * frame inside a frame. The table's layout is unchanged; only the chrome goes.\n * Default true.\n */\n frame?: boolean;\n /**\n * Fade out an edge of the desktop table's scroller while columns are hidden behind\n * it (0.25) — the kit {@link Table}'s `edgeFade`, on the scroller this table draws\n * itself (DataTable renders its own `<table>`, not through Table, so the prop is\n * its own too). Measured: nothing is drawn while every column fits; the end with\n * columns behind it fades, both ends once scrolled into the middle, the reading\n * direction's ends in RTL. A `mask-image`, so no layout moves and no colour is\n * assumed; never animated. The scroller's own scrollbars stay out of it — its\n * vertical one sits at the inline end, in the very band that fades. Focus landing\n * in a cell behind the fade (a row action, a link, the selection box) is scrolled\n * clear of it. The scroller carries `data-overflow` (`\"start\"`, `\"end\"`, `\"both\"`).\n *\n * The phone layout is a card list that never scrolls sideways; it is unaffected.\n *\n * Off by default. It is visual only, but not neutral: on the bump it would change\n * how every overflowing desktop table in an app looks, and the mask covers\n * everything the scroller paints — the sticky header and totals rows, an app's own\n * marks at the scroller's edges. Opt in per table, or app-wide from your own wrapper.\n */\n edgeFade?: boolean;\n /** Extra classes for the table's root element (the card, or with `frame={false}`\n * the plain wrapper). Merged last, so a caller's margin or width wins. */\n className?: string;\n /**\n * Per-row actions: an actions column of icon buttons at the row's end on desktop,\n * and the same actions as swipe actions on a phone card (where the column is not\n * shown; with `mobileDetailsInRow` also as buttons in the opened row). At Large and\n * Extra large two or more collapse into one \"⋯\" menu with each action's words\n * (0.32, §10.8, `RowActions`) — name the rows with `rowName` so each menu button is\n * \"Actions for …\". kastlan's `ResourceListPage` appended this column by hand to every CRUD\n * list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a\n * `stopPropagation` on each so a press did not also open the row.\n *\n * `kind: \"edit\"` / `\"delete\"` bring the icon, the label (`labels.edit` /\n * `labels.delete`), the tone and the swipe side; any other action states its own\n * `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's\n * `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.\n *\n * const confirm = useConfirm();\n * rowActions={[\n * { kind: \"edit\", onAction: openEditor },\n * { kind: \"delete\", onAction: (r) => remove(r.id),\n * confirm: (r) => confirm({ title: t(\"delete\", { name: r.name }), tone: \"danger\" }) },\n * ]}\n *\n * A function receives the row, for actions that differ per row. An explicit\n * `mobileSwipeActions` wins over the swipes derived from here.\n */\n rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);\n /**\n * The `rowActions` column's heading (0.46.1, keksdose dev#598 rework — Marcel: a\n * VISIBLE \"Actions\" heading on every table with an actions column). Default: the\n * provider's `dataTable.actions` (\"Actions\"), shown, end-aligned over the buttons. A\n * node replaces it; `false` keeps the pre-0.46.1 heading, named for a screen reader\n * only.\n */\n rowActionsHeader?: ReactNode | false;\n /**\n * The bar above the table — a search field, filter chips, a create button. Inside\n * the table's frame, above the header, on desktop and phone alike (resource-list\n * pages set a search box in a `div` above the card, outside the frame it filters).\n */\n toolbar?: ReactNode;\n /**\n * The totals row's name — \"Total\", \"Summe\", \"Totals as of 30 June\". It is the row's\n * header: rendered as a `<th scope=\"row\">` in the first visible column that has no\n * `footer` of its own (falling back to the first visible column if every one has a\n * value), so a screen reader reads \"Total, Debit, 12 400.00\" for the cell beside it.\n * On a phone it heads the summary card, which is also named by it.\n *\n * There is no built-in default word: a new kit label key is a silent English string\n * in every translated app (see `missingKitLabels`), and the caller has the\n * word in its own catalogue already. Without it the row is unlabelled on screen —\n * pass one.\n */\n footerLabel?: ReactNode;\n /**\n * Show the totals row. Default: shown when any column declares a `footer`. Set\n * false to drop it without touching the column definitions (a \"show totals\"\n * switch). The row is also left out while there are no rows to total — a row of\n * zeroes under \"No results\" says nothing the empty state has not.\n */\n footer?: boolean;\n}\n\n/** One entry of {@link DataTableProps.rowActions}. */\nexport interface DataTableRowAction<T> {\n /** A preset: `\"edit\"` (pencil, `labels.edit`, swipes toward the END) or `\"delete\"`\n * (bin, `labels.delete`, danger tone, swipes toward the START). */\n kind?: \"edit\" | \"delete\";\n /** The action's name — its tooltip, accessible name and swipe label. Required\n * without a `kind`. */\n label?: string;\n /** The glyph. Required without a `kind`. */\n icon?: ReactNode;\n /** Runs after `confirm` (when given) has said yes. */\n onAction: (row: T) => void;\n /** Asked before `onAction`; a `false` (or a promise of one) cancels. */\n confirm?: (row: T) => boolean | Promise<boolean>;\n /** Default: `\"danger\"` for `kind: \"delete\"`, else `\"default\"`. */\n tone?: \"default\" | \"danger\";\n /** Leave the action out for this row. */\n hidden?: (row: T) => boolean;\n /** Why the action is locked for this row — the button stays focusable and says so,\n * and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */\n disabledReason?: (row: T) => ReactNode;\n /** Which way the phone card is dragged to commit it, or `false` for no swipe.\n * Default: `\"start\"` for a danger action, `\"end\"` otherwise. */\n swipe?: \"start\" | \"end\" | false;\n}\n\n/** See {@link DataTableProps.chrome}. */\nexport type DataTableChrome = \"full\" | \"minimal\";\n\n/** See {@link DataTableProps.density}. */\nexport type DataTableDensity = \"comfortable\" | \"compact\";\n\nexport type FilterState = Record<string, FilterValue>;\nexport type { SortState };\n\n/** What {@link DataTableProps.mobileSwipeActions} returns for one row. */\nexport interface MobileSwipeActions {\n /** Committed by dragging toward the reading start (left in LTR, right in RTL). */\n start?: SwipeAction[];\n /** Committed by dragging toward the reading end (right in LTR, left in RTL). */\n end?: SwipeAction[];\n /** Committed by dragging physically LEFT, whatever the direction. */\n left?: SwipeAction[];\n /** Committed by dragging physically RIGHT, whatever the direction. */\n right?: SwipeAction[];\n}\n\n/** How {@link DataTableProps.desktopSwipeActions} draws the column (0.46, dev#598). */\nexport interface DataTableDesktopSwipeActions<T> {\n /** Leave an action out because the row already has its own button for it — keksdose's\n * feedback inbox, whose status cell offers `advance` and `wont_do`\n * (`feedbackSwipeOmitShown`). Match on `action.id` rather than the translated label. */\n omit?: (action: SwipeAction, row: T) => boolean;\n /** The row's name for the \"⋯\" menu (\"Actions for …\"). Default: the table's `rowName`. */\n name?: (row: T) => string | null | undefined;\n /** {@link RowActionsCollapse}; default `\"auto\"`. A table whose rows offer more than two\n * actions passes `\"menu\"`, so the column does not switch between icons and a menu. */\n collapse?: RowActionsCollapse;\n /**\n * Whether an action COMMITS, refused with the reason under a locked\n * `WriteLockProvider` ({@link CommitScope}) — a swipe has no such state, so a plan\n * usually returns nothing for a locked row and the desktop shows nothing either; with\n * this the plan can stay and the desktop says why. Per action as a function. Default:\n * no commit scope (the plan decides).\n */\n commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);\n /**\n * The column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE \"Actions\"\n * heading on every table with an actions column). Default: the provider's\n * `dataTable.actions` (\"Actions\"), shown, end-aligned over the buttons. A node\n * replaces it; `false` keeps the 0.46.0 heading, named for a screen reader only.\n */\n header?: ReactNode | false;\n}\n\n/**\n * A totals-row cell: pinned to the scroller's bottom edge, on the header's surface.\n * The rule above it is an inset shadow rather than a `border-t`: the table collapses\n * its borders, and a collapsed border belongs to the TABLE, not the cell, so it stays\n * behind at the bottom of the rows while the sticky cell rides over them.\n */\nconst FOOTER_CELL =\n \"sticky bottom-0 z-10 bg-surface-2 shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n// ---------- Phone sort control (keksdose K10) ----------\n\n/**\n * The phone sort control's strings (keksdose K10). They are `dataTable` strings —\n * read from the table's resolved labels, so the `labels` prop and the provider's\n * `dataTable` namespace reach them — and are declared here, with the table's other\n * public types (the control itself, `MobileSort`, is in data-table-mobile.tsx), with\n * English defaults until `DataTableLabels` declares them itself.\n */\nexport interface DataTableSortLabels {\n /** The control's visible label: \"Sort by\". */\n sortBy: string;\n /** The \"no sort\" option — the rows in the order they were given: \"Default order\". */\n sortDefault: string;\n /** The direction button's name while the sort ascends: \"Ascending\". */\n sortAscending: string;\n /** …and while it descends: \"Descending\". */\n sortDescending: string;\n}\n\n/** The phone sort control's words, as they sit in {@link DEFAULT_DATA_TABLE_LABELS}\n * (DataTableLabels extends DataTableSortLabels; one source for the English). */\nexport const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {\n sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,\n sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,\n sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,\n sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending,\n};\n\nconst flipDir = (d: SortDir): SortDir => (d === \"asc\" ? \"desc\" : \"asc\");\n\n// `virtualize`'s constants (0.48.2): the band levels a group's `<tbody>` opens with.\nconst NO_BANDS: readonly (1 | 2)[] = [];\nconst BAND_1: readonly (1 | 2)[] = [1];\nconst BAND_2: readonly (1 | 2)[] = [2];\nconst BAND_1_2: readonly (1 | 2)[] = [1, 2];\nconst NO_VIRTUAL_OPTIONS: DataTableVirtualize = {};\n\n// `moveRowFocus` (the roving rows' Arrow/Home/End) lives with the row in data-table-row.tsx.\n\n// ---------- Main DataTable ----------\n\n\n\nimport { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from \"./use-table-state\";\nimport { useMobileReveal } from \"./use-mobile-reveal\";\nimport { GROW_CELL_CLASS, GROW_PEER_CELL_CLASS, useColumnFit } from \"./data-table-fit\";\n\nexport function DataTable<T>({\n rows,\n columns: columnsProp,\n rowKey,\n defaultPageSize = 25,\n onRowClick,\n rowHref,\n expandedRow,\n isExpanded,\n rowClassName,\n rowAttributes,\n paginated = true,\n empty,\n storageKey,\n urlSync = false,\n fillHeight = false,\n maxBodyHeight = \"calc(100dvh - 12rem)\",\n serverPagination,\n serverRows: serverRowsProp,\n filters: filtersProp,\n onFiltersChange,\n sorts: sortsProp,\n onSortsChange,\n pageSize: pageSizeProp,\n onPageSizeChange,\n selection,\n labels: labelsProp,\n locale: localeProp,\n storageKeyPrefix: storageKeyPrefixProp,\n mobileExpandAsDialog = false,\n mobileDetailsInRow = false,\n rowName,\n mobileDialogBackCloses = true,\n mobileGroupBy,\n mobileGroupLabel,\n mobileCard,\n mobileSort = false,\n mobileSwipeActions,\n desktopSwipeActions,\n leadingRow,\n leadingRowSticky = false,\n groupBy,\n loadMore,\n virtualize = false,\n density = \"comfortable\",\n sortCycle = \"tri\",\n chrome = \"full\",\n columnSettings: columnSettingsProp,\n resizable: resizableProp,\n multiSort: multiSortProp,\n frame = true,\n edgeFade = false,\n className,\n rowActions,\n rowActionsHeader,\n toolbar,\n footerLabel,\n footer: footerProp,\n}: DataTableProps<T>) {\n const compact = density === \"compact\";\n const minimal = chrome === \"minimal\";\n const columnSettings = columnSettingsProp ?? !minimal;\n const resizable = resizableProp ?? !minimal;\n const multiSort = multiSortProp ?? !minimal;\n const isServer = !!serverPagination;\n // The server filtered and sorted these rows (0.48.1, K-052-1): no client filter, sort\n // or paging, and the sort/filter UI only reports to the owner. `isServer` keeps\n // meaning \"the server pages\" (its pager, its page and page size).\n const serverRows = isServer || (serverRowsProp ?? !!loadMore);\n const isLoading = !!serverPagination?.isLoading;\n // prop > provider > English, through the table's own resolver rather than\n // `useKitLabels`: that resolver derives `columnsCount` from a translated `columns`,\n // and a plain merge would skip it. `presets` is a record and is merged key by key,\n // as the provider merges it, so a call site renaming one preset keeps the provider's\n // other ten. Memoised because the announcement effects below list `labels` as a\n // dependency, and a fresh object every render would re-run them for nothing.\n const labelOverrides = useKitLabelOverrides(\"dataTable\");\n // prop > provider > the kit's default, the order every kit setting resolves in (round\n // 0.34 C5, KS U4): one prefix on the provider reaches the tables an app cannot pass a\n // prop to, `TranslationReviewPanel`'s among them.\n const providerStorageKeyPrefix = useKitTableStorageKeyPrefix();\n const storageKeyPrefix = storageKeyPrefixProp ?? providerStorageKeyPrefix ?? DEFAULT_PERSIST_PREFIX;\n const labels = useMemo(() => resolveDataTableLabels(labelOverrides, labelsProp), [labelOverrides, labelsProp]);\n const locale = useKitLocale(localeProp);\n\n // ---- Row actions ---- (data-table-row-actions.tsx)\n // The actions column joins the caller's columns; on a phone the same actions are the\n // card's swipes, unless the caller's own `mobileSwipeActions` says otherwise.\n const { columns, swipesFromActions } = useRowActionColumn({\n columns: columnsProp,\n rowActions,\n rowActionsHeader,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n });\n const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);\n\n // Sorting, filtering and paging all change WHICH rows are on screen without\n // moving focus — the header button the user pressed is still the header button\n // they are on — so there is no other channel to say it on. See use-announce.ts.\n const { announce, regionProps } = useAnnounce();\n const {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths: storedWidths,\n setWidths,\n hiddenCols: storedHiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n } = useTableState({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync,\n defaultPageSize,\n sortsProp,\n filtersProp,\n // Ignored in `serverPagination` mode, where the owner's is the page size.\n pageSizeProp: isServer ? undefined : pageSizeProp,\n });\n\n // With the controls that change them switched off, persisted widths and hidden\n // columns are ignored rather than applied: a table cannot be left in a state the\n // user has no control to get out of. They stay in storage untouched, so turning\n // the control back on brings them back.\n // A `grow` column takes the rest of the row by design (0.48): a width stored for it\n // (a drag or \"Auto-size columns\" before it grew, KS N2) is ignored, and it has no\n // handle, so it can never pin it again. Ignored rather than deleted from storage, as\n // the widths above; the next resize or auto-size writes a blob without it.\n const widths = useMemo(() => {\n if (!resizable) return {};\n const grow = columns.find((c) => c.grow)?.key;\n if (!grow || !(grow in storedWidths)) return storedWidths;\n const { [grow]: _ignored, ...rest } = storedWidths;\n return rest;\n }, [resizable, storedWidths, columns]);\n const hiddenCols = useMemo(\n () => (columnSettings ? storedHiddenCols : new Set<string>()),\n [columnSettings, storedHiddenCols],\n );\n\n // Every call site builds `columns` inline — `columns={[{ key: \"name\", … }]}` right\n // there in the JSX — so the array is a new object on every render of the page around\n // the table, and a `useMemo` keyed on its identity memoises nothing. The three below\n // walk all the rows; a sibling field being typed into re-filtered and re-sorted the\n // whole dataset on every keystroke.\n //\n // What they actually need from `columns` is WHICH columns there are, so that is what\n // they are keyed on. The trade-off is deliberate and narrow: swapping a column's\n // `sortBy`/`filter`/`filterBy` FUNCTION while its `key` stays the same no longer\n // invalidates on its own. A rule that changes is a different column — give it a\n // different key — and anything the rule reads (rows, filters, sorts) is a dependency\n // in its own right.\n const columnSig = columns.map((c) => c.key).join(\"\\u0000\");\n // The select filters' declared option lists are the one part of a column definition\n // the memo below reads as DATA rather than as a rule, and a caller may recompute\n // them (a lookup that arrived, a saved view applied) without the column set moving.\n const selectOptionsSig = columns\n .map((c) => {\n const f = resolveFilter(c);\n if (!f || f.type !== \"select\" || !f.options) return \"\";\n return `${c.key}:${f.options.map((o) => `${o.value}=${o.label ?? \"\"}`).join(\",\")}`;\n })\n .join(\"\\u0000\");\n\n const selectOptionsByKey = useMemo(() => {\n const map: Record<string, { value: string; label: string }[]> = {};\n for (const col of columns) {\n const f = resolveFilter(col);\n if (!f || f.type !== \"select\") continue;\n if (f.options && f.options.length > 0) {\n map[col.key] = f.options.map((o) => ({ value: o.value, label: o.label ?? o.value }));\n continue;\n }\n const seen = new Set<string>();\n for (const row of rows) {\n const v = f.getValue(row);\n if (v && !seen.has(v)) seen.add(v);\n }\n map[col.key] = Array.from(seen)\n .sort()\n .map((v) => ({ value: v, label: v }));\n }\n return map;\n // `columns` by signature — see the note above the signatures.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [columnSig, selectOptionsSig, rows]);\n\n const filtered = useMemo(() => {\n if (serverRows) return rows;\n let result = rows;\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n result = result.filter((row) => rowMatches(col, row, state, locale));\n }\n return result;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rows, columnSig, filters, serverRows, locale]);\n\n const sorted = useMemo(() => {\n if (serverRows) return filtered;\n // Priority chain: the first sort key that distinguishes two rows wins;\n // nulls sort last for that key regardless of direction (as before).\n const chain = sorts\n .map((s) => {\n const col = columns.find((c) => c.key === s.key);\n return col?.sortBy ? { sortBy: col.sortBy, dir: s.dir } : null;\n })\n .filter((c): c is { sortBy: (row: T) => string | number | null | undefined; dir: \"asc\" | \"desc\" } => c !== null);\n if (!chain.length) return filtered;\n const copy = [...filtered];\n copy.sort((a, b) => {\n for (const { sortBy, dir } of chain) {\n const av = sortBy(a);\n const bv = sortBy(b);\n if (av == null && bv == null) continue;\n if (av == null) return 1;\n if (bv == null) return -1;\n if (av < bv) return dir === \"asc\" ? -1 : 1;\n if (av > bv) return dir === \"asc\" ? 1 : -1;\n }\n return 0;\n });\n return copy;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [filtered, sorts, columnSig, serverRows]);\n\n // `paginated={false}` is expressed as \"page size = everything\" rather than as a\n // second code path, so slicing, the page clamp and the range summary all keep\n // exactly one definition.\n const unpaged = !isServer && (!paginated || !!loadMore || serverRows);\n const effectivePageSize = isServer\n ? serverPagination!.pageSize\n : unpaged || pageSize === Infinity\n ? sorted.length || 1\n : pageSize;\n const paginationTotal = isServer ? serverPagination!.total : sorted.length;\n const paginationPageSize = isServer ? serverPagination!.pageSize : pageSize;\n const totalPages = isServer\n ? Math.max(1, Math.ceil(serverPagination!.total / Math.max(1, serverPagination!.pageSize)))\n : Math.max(1, Math.ceil(sorted.length / effectivePageSize));\n const safePage = isServer\n ? Math.min(Math.max(0, serverPagination!.page), totalPages - 1)\n : Math.min(page, totalPages - 1);\n const slice =\n isServer || unpaged || pageSize === Infinity\n ? sorted\n : sorted.slice(safePage * effectivePageSize, safePage * effectivePageSize + effectivePageSize);\n\n const toggleSort = (key: string, shiftKey: boolean) => {\n const col = columns.find((c) => c.key === key);\n // The cycle is the table's, the first direction the column's. The announcement\n // below reads the RESULT, so it can never describe a step the cycle did not take.\n const next = nextSorts(sorts, key, multiSort && shiftKey, {\n firstDir: col?.firstSort,\n cycle: sortCycle,\n });\n commitSorts(next, key);\n };\n\n // Header clicks and the phone sort control (`mobileSort`) land here alike, so both\n // go through the same owner and say the same thing.\n const commitSorts = (next: SortState[], key: string) => {\n const col = columns.find((c) => c.key === key);\n if (onSortsChange) onSortsChange(next);\n else setInternalSorts(next);\n\n // Announce the column the user just pressed, not `next[0]`: a shift-click adds a\n // criterion BEHIND the existing one, and answering \"Sorted by Date\" to a click on\n // Name misreports what the click did. The third click drops the column entirely,\n // which is the state a user is most likely to have reached by accident.\n const name = col ? columnLabel(col, labels) : key;\n const entry = next.find((s) => s.key === key);\n announce(\n !entry\n ? labels.sortCleared(name)\n : entry.dir === \"asc\"\n ? labels.sortedAscending(name)\n : labels.sortedDescending(name),\n );\n };\n\n const setFilterValue = (key: string, value: FilterValue) => {\n if (onFiltersChange) {\n onFiltersChange({ ...filters, [key]: value });\n // No `setPage(0)` here: the page resets when the new value comes back in\n // through the prop — see the controlled reset in useTableState. Resetting now\n // would move to page 1 for a change the owner may yet refuse.\n return;\n }\n setInternalFilters((s) => ({ ...s, [key]: value }));\n setPage(0);\n };\n\n const clearFilter = (key: string) => {\n if (onFiltersChange) {\n const rest = { ...filters };\n delete rest[key];\n onFiltersChange(rest);\n return;\n }\n setInternalFilters((s) => {\n const rest = { ...s };\n delete rest[key];\n return rest;\n });\n setPage(0);\n };\n\n // Clear every column filter at once (drives the mobile filter sheet's\n // \"clear all\"). Only touches keys the table actually exposes as filters.\n const clearAllFilters = () => {\n const keys = columns\n .filter((col) => ((!serverRows || onFiltersChange) ? resolveFilter(col) : null))\n .map((col) => col.key);\n if (!keys.length) return;\n if (onFiltersChange) {\n const rest = { ...filters };\n for (const k of keys) delete rest[k];\n onFiltersChange(rest);\n return;\n }\n setInternalFilters((s) => {\n const rest = { ...s };\n for (const k of keys) delete rest[k];\n return rest;\n });\n setPage(0);\n };\n\n // The result count only exists AFTER the table has re-filtered, so the sentence is\n // spoken from the render that produced the new number rather than from the handler\n // that changed the filter. Keyed on a serialised copy of the filter state, for two\n // reasons: a controlled `filters` prop arrives with a fresh identity on every\n // parent render, and a change the OWNER makes (a saved view applied, a filter chip\n // dismissed outside the table) moves the same rows and deserves the same sentence.\n const filterKey = useMemo(() => JSON.stringify(filters), [filters]);\n // Seeded with the mount value: a live region that speaks on first paint talks over\n // the page the user has just opened, and every table on that page does it at once.\n const announcedFilterKey = useRef(filterKey);\n useEffect(() => {\n if (announcedFilterKey.current === filterKey) return;\n announcedFilterKey.current = filterKey;\n // A server-paginated table does not do the filtering: the matching rows arrive on\n // some later render from the caller, and `serverPagination.total` is ALREADY the\n // filtered total — so the \"n of m\" sentence has no second number to give here.\n // Saying nothing is better than announcing the page size as a result count.\n if (serverRows) return;\n announce(labels.filterResults(filtered.length, rows.length));\n }, [filterKey, filtered.length, rows.length, serverRows, announce, labels]);\n\n // 1-based, because that is what the page strip shows and what a person says.\n const goToPage = (p: number) => {\n if (isServer) serverPagination!.onPageChange(p);\n else setPage(p);\n announce(labels.pageChanged(p + 1, totalPages));\n };\n\n // 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out\n // (data-table-fit.ts). Desktop only; the scroller is read after render.\n // Large and Extra large: read here, as the fit re-measures when it changes.\n const large = useLargeText();\n const { autoHidden, keep: keepColumn } = useColumnFit({\n columns,\n hiddenCols,\n headRefs,\n scroller: () => desktopScroller.current,\n enabled: !usePhoneLayout(),\n // Measured only when one of these changed (0.48.2): not on a ticked box.\n content: [slice, columns, hiddenCols, widths, density, large],\n });\n const visibleColumns = useMemo(\n () => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),\n [columns, hiddenCols, autoHidden],\n );\n // With a `grow` column drawn, every other cell keeps one line at Normal (kastlan S2,\n // 0.48): the `w-full` column squeezed them to their min-content width.\n const growDrawn = visibleColumns.some((c) => c.grow);\n\n // Which cell carries the row's link when `rowHref` is set (feedback #451). The\n // `mobilePrimary` column is the row's headline by declaration, so it is both the\n // widest target and the one the pointer is already over; falls back to the first\n // column, and falls back again if the user has hidden the primary one from the\n // column panel. Resolved from `visibleColumns` for exactly that reason — off a\n // raw `columns` lookup a hidden headline would leave the row with no link at all.\n //\n // `noRowLink` (and `role: \"actions\"`) columns are skipped in BOTH steps: wrapping a\n // cell that owns an anchor nests two links and disables the inner one (see the prop's\n // note). If nothing visible is left to wrap, the row gets NO anchor — an anchor\n // over the one cell there is would cancel that cell's own click and hand back\n // nothing, and a row without a link still behaves exactly as it did before #451.\n const linkColumn = rowHref\n ? (visibleColumns.find((c) => c.mobilePrimary && !keepsOwnClicks(c)) ??\n visibleColumns.find((c) => !keepsOwnClicks(c)))\n : undefined;\n\n // ---- Column resize ---- (data-table-resize.ts: pointer capture, one `setWidths` per\n // drag, and the page's cursor and selection handed back on an unmount mid-drag)\n const { startResize, moveResize, finishResize, autoSizeAll } = useColumnResize({\n headRefs,\n widths,\n setWidths,\n columns,\n // The columns left out for width have no header to measure either.\n hiddenCols: autoHidden.size > 0 ? new Set([...hiddenCols, ...autoHidden]) : hiddenCols,\n });\n\n const visibleCount = visibleColumns.length;\n const totalCount = columns.length;\n // Spans the full row width including the optional leading selection column.\n const totalColSpan = visibleCount + (selection ? 1 : 0);\n\n // ---- Totals row ----\n // `sorted`, not `slice`: the totals are of everything the filters let through, so\n // paging does not move them — see `DataTableColumn.footer`. In server mode `sorted`\n // IS the page (the table holds nothing else), which the prop's note spells out.\n const hasFooterColumns = columns.some((c) => c.footer);\n const showFooter = footerProp !== false && hasFooterColumns && sorted.length > 0;\n // The row header sits in the first visible column that has nothing of its own to\n // show — so hiding the \"Account\" column moves \"Total\" into \"Name\" rather than\n // dropping it — and on the first visible column when every one carries a value.\n const footerLabelCol = showFooter\n ? (visibleColumns.find((c) => !c.footer && !isTableOwnColumn(c.key)) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // Each open row's detail row, for its disclosure button's `aria-controls`.\n const detailIdBase = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const ownColumns = columns.filter((c) => isTableOwnColumn(c.key)).length;\n const ownVisible = visibleColumns.filter((c) => isTableOwnColumn(c.key)).length;\n const columnsCountLabel = labels.columnsCount(visibleCount - ownVisible, totalCount - ownColumns);\n // Match the `md:` breakpoint at the text size in force: we render either the table or\n // the card list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin\n // `useBreakpoint(\"md\")`: the same query an app's own phone checks ask (keksdose's\n // sticky editor footer), so the card list and the full-screen row dialog switch with\n // them and never a frame apart (§10.4 \"usePhoneLayout() is the single answer\"; 0.32.1,\n // keksdose's 0.32 report, where `md` and `max-md` both missed at 767.5 px). Without\n // `matchMedia` (SSR, jsdom) it is the table, as before.\n const isMdUp = !usePhoneLayout();\n // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.\n const desktopScroller = useRef<HTMLDivElement | null>(null);\n // 0.47 (keksdose S4): the scroller's visible width, as `--dt-visible-w` on it, for the\n // expansion row's pinned content (below). Set on the element, not in state: a resize\n // re-renders nothing. Only a table that can expand a row measures.\n // The group bands and the load-more footer pin their content the same way (0.48).\n const levels = groupLevels(groupBy);\n const measuresVisibleWidth = isMdUp && (!!expandedRow || levels.length > 0 || !!loadMore);\n useEffect(() => {\n const el = desktopScroller.current;\n if (!measuresVisibleWidth || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => el.style.setProperty(\"--dt-visible-w\", `${el.clientWidth}px`));\n ro.observe(el);\n return () => ro.disconnect();\n }, [measuresVisibleWidth]);\n const edgeFadeProps = useEdgeFade(desktopScroller, edgeFade && isMdUp);\n\n // ---- Mobile card list (md:hidden) ----\n // Renders the same paged/filtered/sorted slice but as stacked cards instead\n // of a horizontally-scrolling table. Column settings stay desktop-only; the\n // filters come through a bottom sheet (feedback #299) and the sort through the\n // opt-in `mobileSort` row (keksdose K10), both on the table's own state.\n // Memoised on `visibleColumns`: the cards compare their columns by identity (0.48.2).\n const mobileColumns = useMemo(\n () => visibleColumns.filter((c) => !c.mobileHidden && c.mobile !== \"hidden\"),\n [visibleColumns],\n );\n // The default card (0.49 KIT-7): the title, the status chips beside it, at most three\n // fields — the ones marked `mobile: \"secondary\"`, else the first after the title — and\n // the `role: \"actions\"` columns after them.\n const {\n primary: mobilePrimaryCol,\n status: mobileStatusColumns,\n fields: mobileSecondaryColumns,\n actions: mobileActionColumns,\n } = useMemo(() => mobileCardColumns(mobileColumns), [mobileColumns]);\n // A development build names a `noRowLink` column (a per-row delete) the card drops.\n useCardColumnWarnings(\n columns,\n !mobileCard && !mobileDetailsInRow,\n storageKey ?? (typeof urlSync === \"object\" ? urlSync.prefix : undefined),\n );\n // The columns the card leaves out, for the opened row (`mobileDetailsInRow`): the\n // hidden ones and, without a `mobileCard`, the ones past the card's three fields.\n const onCard = new Set<DataTableColumn<T>>([\n ...(mobilePrimaryCol ? [mobilePrimaryCol] : []),\n ...mobileStatusColumns,\n ...mobileSecondaryColumns,\n ...mobileActionColumns,\n ]);\n const mobileDetailColumns = mobileDetailsInRow\n ? visibleColumns.filter((c) => (mobileCard ? c.mobileHidden || c.mobile === \"hidden\" : !onCard.has(c)))\n : [];\n // Large and Extra large (§4 \"DataTable phone card\"): the card's labels go ABOVE their\n // values, nothing in it truncates, and the rows get more room. Most of it is a\n // `large:` class; the label/value grid needs a different STRUCTURE (each pair in a\n // box of its own), which a class cannot write. (`large` is read above, by the fit.)\n // What an opened phone row shows: the card's left-out columns first (when asked\n // for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.\n const opensRow = !!expandedRow || mobileDetailColumns.length > 0;\n const openedRowContent = (row: T): ReactNode => {\n const detail = expandedRow?.(row) ?? null;\n if (mobileDetailColumns.length === 0) return detail;\n return (\n <div data-row-details=\"\" className={cn(\"flex flex-col\", compact ? \"gap-2\" : \"gap-3\")}>\n <StackedFields columns={mobileDetailColumns} row={row} compact={compact} />\n {detail}\n </div>\n );\n };\n // The phone card IS the anchor, so unlike the desktop table there is no other\n // cell to move the row link into: if any cell the card renders owns a link, the\n // card itself cannot be one (feedback #451 — same nesting rule as `linkColumn`\n // above). It then falls back to the pre-#451 role=\"button\" card, which still\n // opens the row and leaves the cell's own link working. A caller's `mobileCard`\n // renderer is opaque to us; a link inside one is the caller's to declare by\n // marking the column it comes from.\n const mobileCardLinkable = (mobileCard ? mobileColumns : [...onCard]).every((c) => !keepsOwnClicks(c));\n // ...except on the default card (0.50 K4, kastlan): its title becomes the row's link\n // (or, with only `onRowClick`, a button), stretched over the card, and the own-click\n // cells — a tel: link, the actions row — sit above it, so \"tap to open\" stays and no\n // control nests in another. A `mobileCard` stays the fallback above (its controls\n // cannot be lifted), and so does a card whose title keeps its own clicks.\n const mobileCardStretched =\n !mobileCard && !mobileCardLinkable && !!mobilePrimaryCol && !keepsOwnClicks(mobilePrimaryCol);\n // The card list's reading direction, for the one thing on it CSS cannot flip: which\n // physical drag a logical `start`/`end` swipe action means. Read off the DOM through\n // a ref callback (it runs at commit, like a layout effect) because `dir` is an\n // attribute of some ancestor the table does not own; LTR until the first commit,\n // which is also before any finger can have touched a row.\n const [mobileDir, setMobileDir] = useState<Direction>(\"ltr\");\n const mobileRootRef = (el: HTMLElement | null) => {\n if (!el) return;\n const d = dirOf(el);\n if (d !== mobileDir) setMobileDir(d);\n };\n\n // `fillHeight` on a phone (0.40, keksdose live #420): the card list is a scroller only\n // while its parent bounds it — its rows overflow the flex room it was given. Under\n // AppShell's phone layout the DOCUMENT scrolls, the list's room is its own content,\n // and a scroller there (with `overscroll-contain`) only swallowed the page's scroll.\n // Measured before paint on every render (rows revealed, filtered, opened), and on a\n // resize of the list or its parent; one read of two heights.\n const phoneFill = fillHeight && !isMdUp;\n const phoneListRef = useRef<HTMLUListElement | null>(null);\n const [phoneListScrolls, setPhoneListScrolls] = useState(false);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose: what overflows is the rows, and React bails out on an unchanged boolean\n useLayoutEffect(() => {\n const el = phoneListRef.current;\n if (!phoneFill || !el) return;\n setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1);\n });\n useEffect(() => {\n const el = phoneListRef.current;\n if (!phoneFill || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1));\n ro.observe(el);\n if (el.parentElement) ro.observe(el.parentElement);\n return () => ro.disconnect();\n }, [phoneFill]);\n\n const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // ---- Phone selection mode (0.40, keksdose live #421) ----\n // The cards had no boxes, so `selection`'s bulk actions were unreachable on a phone.\n // A long press on a card (kk's register gesture) — or \"Select rows\", the keyboard's\n // and screen reader's way — enters the mode: every card shows its box and a tap\n // toggles it. The mode lasts while anything is selected, so emptying the selection\n // (the bulk bar's Clear, the last box unticked, Cancel) leaves it, as kk's does.\n const anySelected =\n !!selection && (selection.someSelected || (selection.allSelected && slice.length > 0));\n const [phoneSelectMode, setPhoneSelectMode] = useState(false);\n const [hadSelection, setHadSelection] = useState(anySelected);\n if (anySelected !== hadSelection) {\n // Adjusted while rendering, so the frame the selection empties is already out of\n // the mode (an effect would paint a frame of boxes first).\n setHadSelection(anySelected);\n if (!anySelected) setPhoneSelectMode(false);\n }\n const phoneSelecting = !!selection && !isMdUp && (phoneSelectMode || anySelected);\n // ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,\n // anchored on the row's KEY so a sort, filter or page change cannot move it)\n const { canSelect, selectRange, setAnchor, markRangeClick, takeRangeClick } = useRangeSelection({\n selection,\n slice,\n rowKey,\n });\n // ---- Keyboard-activatable rows ----\n //\n // A desktop row with `onRowClick` opened on a mouse click and on nothing else: no\n // tab stop, no Enter, no focus ring (keksdose's report tables hand-rolled all three,\n // which is half of why they could not move onto this table). The row now takes\n // focus itself, as a ROVING tab stop: one row of the table is in the tab order\n // (the last one focused, else the first), ArrowUp/ArrowDown/Home/End move between\n // rows, Enter and Space activate.\n //\n // Why the `<tr>` rather than the two alternatives:\n // * `role=\"button\"` on the row would replace its `row` role, and the cells with\n // it — a screen reader would stop reading the table as a table.\n // * A button or link in one cell is what `rowHref` already is where the row HAS a\n // URL. Without one, a synthetic button would have to wrap caller-rendered cell\n // content, which nests the controls those cells carry (status toggles, links)\n // inside it — illegal markup, and the reason the mobile card is a div.\n // A focused `<tr>` keeps its role and reads out its cells. Roving rather than a tab\n // stop per row, so a 100-row page does not put 100 stops between the header and\n // the pager.\n const rowsFocusable = !!onRowClick;\n const [focusRowKey, setFocusRowKey] = useState<string | number | null>(null);\n const tabRowKey =\n focusRowKey !== null && slice.some((r) => rowKey(r) === focusRowKey)\n ? focusRowKey\n : slice.length\n ? rowKey(slice[0])\n : null;\n\n // What the memoised rows' handlers read at event time (0.48.2, K-052-2): the latest\n // props, refreshed every commit, so a row that bailed out of a render still toggles\n // through the current `selection` and opens through the current `onRowClick`.\n // `virtualize` (0.48.2): desktop only, and only with rows to window. Arrow/Home/End then\n // move over every row, rendered or not: the target is rendered, then focused.\n const virtualOn = !!virtualize && isMdUp && slice.length > 0;\n const focusRowRef = useRef<((index: number) => void) | undefined>(undefined);\n const latestRowCtx: DataTableRowCtx<T> = {\n moveFocus: virtualOn\n ? (from, key) => {\n const target = rowNavTarget(from, key, slice.length);\n if (target === null) return false;\n focusRowRef.current?.(target);\n return true;\n }\n : undefined,\n onRowClick,\n onToggle: selection?.onToggle,\n isSelected: (row) => !!selection?.isSelected(row),\n canSelect,\n selectRange,\n setAnchor,\n markRangeClick,\n takeRangeClick,\n setFocusRowKey,\n onLongPress: (row) => {\n setPhoneSelectMode(true);\n setAnchor(rowKey(row));\n if (selection && !selection.isSelected(row)) selection.onToggle(row, true);\n },\n };\n const rowCtx = useRef(latestRowCtx);\n useLayoutEffect(() => {\n rowCtx.current = latestRowCtx;\n });\n\n // When dialog-mode is on, the expanded row's detail goes into a bottom-sheet\n // modal instead of unfolding inline. Only one row is ever expanded at a time.\n const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;\n const mobileDialogContent = mobileDialogRow ? openedRowContent(mobileDialogRow) : null;\n // The full-screen row dialog. Its panel, header, scroll lock and Back handling all\n // live in {@link FullBleedDialog} now — this was the only copy of that shell until\n // the transactions create card needed the same one (Keksdose live #307's follow-up),\n // and two copies of a dialog is how the row editor and the create form come to look\n // like two different products.\n const dialogOpen = !!(mobileDialogRow && mobileDialogContent);\n const closeDialog = () => {\n if (mobileDialogRow) onRowClick?.(mobileDialogRow);\n };\n\n // One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list\n // and the grouped list (below) share identical row markup.\n const renderMobileRow = (row: T) => {\n const key = rowKey(row);\n const expanded = isExpanded?.(row) ?? false;\n return (\n <DataTableMobileRow<T>\n key={key}\n ctx={rowCtx}\n rowKey={key}\n row={row}\n expanded={expanded}\n expansion={expanded ? openedRowContent(row) : null}\n opensRow={opensRow}\n className={rowClassName?.(row)}\n attrs={rowAttributes?.(row)}\n interactive={!!onRowClick}\n href={mobileCardLinkable || mobileCardStretched ? rowHref?.(row) : undefined}\n swipeActionsFor={swipeActionsFor}\n mobileDir={mobileDir}\n mobileCardLinkable={mobileCardLinkable}\n mobileCardStretched={mobileCardStretched}\n mobileCard={mobileCard}\n mobilePrimaryCol={mobilePrimaryCol}\n mobileStatusColumns={mobileStatusColumns}\n mobileSecondaryColumns={mobileSecondaryColumns}\n mobileActionColumns={mobileActionColumns}\n mobileExpandAsDialog={mobileExpandAsDialog}\n compact={compact}\n large={large}\n selectionOn={!!selection}\n selectActive={phoneSelecting}\n selectable={canSelect(row)}\n selected={!!selection && selection.isSelected(row)}\n />\n );\n };\n\n // Consecutive grouping of the (already-sorted) mobile slice into labelled\n // sections for the \"assistance\"-style list (feedback #317). Null unless the\n // caller opts in via `mobileGroupBy`.\n // The desktop's bands (0.48, keksdose dev #605), over the rows on screen.\n const tableRef = useRef<HTMLTableElement | null>(null);\n const desktopGroups = isMdUp && levels.length > 0 ? groupRows(slice, levels) : null;\n const bandTops = useGroupBandTops(\n tableRef,\n !!desktopGroups,\n `${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? \"\"}`,\n );\n // ---- Row windowing ---- (0.48.2, keksdose dev #632; data-table-virtual.ts)\n // One run per innermost group, with its bands (`virtualOn` is above, with the rows' context).\n const virtualGroups: VirtualGroup[] = [];\n const virtualBands: { g1: RowGroup<T>; g2: RowGroup<T> | null }[] = [];\n if (virtualOn) {\n if (!desktopGroups) virtualGroups.push({ start: 0, end: slice.length, bands: NO_BANDS, first: 0 });\n else\n for (const g1 of desktopGroups) {\n const first = virtualGroups.length;\n if (!g1.children) {\n virtualGroups.push({ start: g1.start, end: g1.start + g1.rows.length, bands: BAND_1, first });\n virtualBands.push({ g1, g2: null });\n continue;\n }\n g1.children.forEach((g2, j) => {\n virtualGroups.push({ start: g2.start, end: g2.start + g2.rows.length, bands: j === 0 ? BAND_1_2 : BAND_2, first });\n virtualBands.push({ g1, g2 });\n });\n }\n }\n const expandedIndices: number[] = [];\n if (virtualOn && isExpanded && expandedRow) slice.forEach((r, i) => isExpanded(r) && expandedIndices.push(i));\n const virtualColKeys = [...(selection ? [VIRTUAL_SELECT_COL] : []), ...visibleColumns.map((c) => c.key)];\n const virtualLockable = new Set(\n virtualColKeys.filter((k) => !widths[k] && !visibleColumns.some((c) => c.key === k && c.grow)),\n );\n const rowWindow = useRowWindow({\n enabled: virtualOn,\n rows: slice,\n rowKey,\n groups: virtualGroups,\n expanded: expandedIndices,\n pinnedKeys: [tabRowKey],\n scrollerRef: desktopScroller,\n tableRef,\n options: typeof virtualize === \"object\" ? virtualize : NO_VIRTUAL_OPTIONS,\n compact,\n rowsBefore: 1 + (leadingRow ? 1 : 0),\n rowsAfter: showFooter ? 1 : 0,\n colKeys: virtualColKeys,\n lockable: virtualLockable,\n resetKey: `${Object.entries(widths).join(\";\")}|${density}|${large}|${slice.length}:${\n slice.length ? `${rowKey(slice[0])}:${rowKey(slice[slice.length - 1])}` : \"\"\n }`,\n });\n // What `moveFocus` (in the rows' context, above) reaches: the window's focus mover.\n const focusRowAt = rowWindow?.focusRow;\n useLayoutEffect(() => {\n focusRowRef.current = focusRowAt;\n }, [focusRowAt]);\n\n // `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.\n const mobileLevel = typeof mobileGroupBy === \"function\" ? null : (mobileGroupBy ?? null);\n const mobileKey = typeof mobileGroupBy === \"function\" ? mobileGroupBy : mobileGroupBy?.key;\n const mobileGroups: { key: string; rows: T[] }[] | null = mobileKey\n ? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {\n const key = mobileKey(row);\n const last = acc[acc.length - 1];\n if (last && last.key === key) last.rows.push(row);\n else acc.push({ key, rows: [row] });\n return acc;\n }, [])\n : null;\n\n // `frame={false}` swaps the Card for a bare wrapper carrying the same hooks and\n // layout classes, so everything inside renders identically either way.\n const Root = frame ? FramedRoot : PlainRoot;\n\n return (\n <Root\n // A styling and testing hook: a host's own cell content can follow the table's\n // density (`group-data-[density=compact]/table:…`) without being told twice.\n data-density={density}\n data-frame={frame ? undefined : \"none\"}\n // The frame clips (`overflow-clip`), so it is a clipping container for a\n // Tooltip's auto-portal — said as an attribute as well, because jsdom computes no\n // classes and a table-cell tooltip must portal under test just as it does in the\n // browser (see `CLIPS_ATTRIBUTE`). The bare root clips nothing and is not marked.\n data-clips={frame ? \"\" : undefined}\n // The fill's marker (0.40, docs/page-layout-harmonization.md §1.3): AppShell's\n // `<main>` and every `FillHeight` between it and this table key off it with\n // `has-[[data-fill-height]]`, so a page asks for the fill by passing the prop.\n data-fill-height={fillHeight ? \"\" : undefined}\n className={cn(\n \"group/table\",\n frame ? \"overflow-clip\" : \"min-w-0\",\n // Both layouts, not just the table: the phone card list overflowing a bounded\n // pane drew itself over whatever followed it, and in a viewport-locked shell\n // the rows below the fold could not be reached at all. From `md` the minimum is\n // the content's again (`md:min-h-auto`): the body's floor below then holds the\n // whole chain up to `<main>`, which scrolls instead of leaving a slit.\n fillHeight && \"flex flex-1 flex-col min-h-0 md:min-h-auto\",\n className,\n )}\n >\n {/* Outside both viewport branches on purpose. A screen reader subscribes to a\n live region when it encounters it, so one that appears together with its\n first message is usually missed — and crossing the md breakpoint swaps the\n card list for the table wholesale, which would remount a region living\n inside either one. */}\n <span {...regionProps} />\n {toolbar != null && toolbar !== false && (\n <div\n data-table-toolbar=\"\"\n className={cn(\n \"flex flex-wrap items-center gap-2\",\n frame\n ? cn(\"border-b border-subtle\", compact ? \"px-2 py-1.5\" : \"px-3 py-2\")\n : \"pb-3\",\n )}\n >\n {toolbar}\n </div>\n )}\n {!isMdUp && (\n <div ref={mobileRootRef} className={cn(fillHeight && \"flex min-h-0 flex-1 flex-col\")}>\n {/* Mobile filter access (feedback #299): the per-column filter popovers\n live in the desktop header, which the card list doesn't render — so\n expose the same filters through a bottom sheet here. */}\n {(!serverRows || onFiltersChange) && (\n <MobileFilters\n columns={columns}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n isServer={serverRows}\n hasFilterCallback={!!onFiltersChange}\n onSetFilter={setFilterValue}\n onClearFilter={clearFilter}\n onClearAll={clearAllFilters}\n labels={labels}\n locale={locale}\n />\n )}\n {mobileSort && (\n <MobileSort\n columns={visibleColumns.filter((c) => (!serverRows || !!onSortsChange) && !!c.sortBy)}\n sorts={sorts}\n sortCycle={sortCycle}\n compact={compact}\n labels={labels}\n onPick={(key) => {\n if (key === null) {\n if (sorts.length > 0) commitSorts([], sorts[0].key);\n return;\n }\n if (sorts[0]?.key === key) return;\n // A newly picked column starts where its header's first click would.\n const col = columns.find((c) => c.key === key);\n commitSorts([{ key, dir: col?.firstSort ?? \"asc\" }], key);\n }}\n onFlip={() => {\n if (sorts.length === 0) return;\n commitSorts(\n sorts.map((s, i) => (i === 0 ? { ...s, dir: flipDir(s.dir) } : s)),\n sorts[0].key,\n );\n }}\n />\n )}\n {selection && (\n <MobileSelectBar\n active={phoneSelecting}\n allSelected={selection.allSelected}\n someSelected={selection.someSelected}\n compact={compact}\n labels={labels}\n onEnter={() => setPhoneSelectMode(true)}\n onToggleAll={selection.onToggleAll}\n onCancel={() => {\n setPhoneSelectMode(false);\n if (anySelected) selection.onToggleAll(false);\n }}\n />\n )}\n <ul\n ref={phoneListRef}\n className={cn(\n \"divide-y divide-subtle\",\n // Under `fillHeight` the list takes the room its parent gives it, and in a\n // BOUNDED parent it is the scroller, as the table body is on a wide screen:\n // the filter button stays above it and a server pager below. Only then —\n // see `phoneListScrolls` (live #420). The endless-scroll sentinel still\n // fires in here — an IntersectionObserver on the viewport sees through the\n // clipping ancestor.\n fillHeight && \"min-h-0 flex-1\",\n fillHeight && phoneListScrolls && \"overflow-y-auto overscroll-contain\",\n // Same busy treatment as the desktop body; see `isLoading` below.\n isLoading && mobileSlice.length > 0 && \"opacity-60 transition-opacity\",\n )}\n aria-busy={isLoading || undefined}\n >\n {mobileGroups\n ? mobileGroups.map((g) => (\n <Fragment key={`hb-group:${g.key}`}>\n <li className=\"sticky top-0 z-10 flex flex-wrap items-baseline justify-between gap-x-3 bg-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted backdrop-blur\">\n <span className=\"min-w-0\">\n {mobileGroupLabel\n ? mobileGroupLabel(g.key)\n : mobileLevel?.label\n ? mobileLevel.label(g.key, g.rows)\n : g.key}\n </span>\n {mobileLevel?.end && <span className=\"ms-auto tabular-nums\">{mobileLevel.end(g.key, g.rows)}</span>}\n </li>\n {g.rows.map(renderMobileRow)}\n </Fragment>\n ))\n : mobileSlice.map(renderMobileRow)}\n {mobileSlice.length === 0 && (\n <li\n className={cn(\n \"text-center text-muted\",\n compact ? \"px-3 py-3 text-xs\" : \"px-4 py-6 text-sm\",\n )}\n >\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </li>\n )}\n {/* Endless-scroll sentinel: observed by IntersectionObserver to pull in\n the next chunk as it nears the viewport (feedback #232). */}\n {canRevealMoreMobile && (\n <li\n ref={loadMoreRef}\n className=\"px-4 py-4 text-center text-xs text-placeholder\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\n )}\n {/* The owner's next slice (0.48), once every row held is revealed. */}\n {loadMore && !canRevealMoreMobile && (\n <DataTableLoadMoreFooter\n as=\"li\"\n loadMore={loadMore}\n rowCount={sorted.length}\n labels={labels}\n compact={compact}\n />\n )}\n </ul>\n {/* The phone's totals: a summary card after the list rather than one more\n `<li>` in it. The list reveals rows in chunks as it is scrolled, so a last\n item would sit mid-list until the end was reached; outside it, and under\n `fillHeight` outside the scroller too, the totals stay in view the way the\n sticky `<tfoot>` keeps them in view on a wide screen. Named by\n `footerLabel` so it is announced as what it is, not as a stray list of\n numbers; `mobileHidden` columns stay hidden here as on the cards. */}\n {showFooter && mobileColumns.some((c) => c.footer) && (\n <section\n data-table-footer=\"\"\n aria-labelledby={footerLabel != null ? footerHeadingId : undefined}\n className={cn(\n \"border-t-2 border-strong bg-surface-2\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n {footerLabel != null && (\n <div id={footerHeadingId} className={cn(\"font-semibold\", compact ? \"mb-1 text-sm\" : \"mb-2\")}>\n {footerLabel}\n </div>\n )}\n <dl\n className={cn(\n \"grid grid-cols-[auto_1fr] gap-x-3\",\n compact ? \"gap-y-0.5 text-xs\" : \"gap-y-1 text-sm\",\n )}\n >\n {mobileColumns\n .filter((c) => c.footer)\n .map((col) => (\n <Fragment key={col.key}>\n <dt className=\"self-center text-xs uppercase tracking-wide text-muted\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-primary\">\n {footerValue(col)}\n </dd>\n </Fragment>\n ))}\n </dl>\n </section>\n )}\n {/* Client-side mobile lists scroll endlessly (sentinel above); only\n server-paginated tables keep the pager here. */}\n {isServer && !loadMore && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n pageSize={paginationPageSize}\n total={paginationTotal}\n onPage={goToPage}\n onPageSize={serverPagination!.onPageSizeChange}\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n {/* The row's own first column is this dialog's title — the same cell the card\n behind it shows, so the panel names the row it opened from. */}\n <FullBleedDialog\n open={dialogOpen}\n onClose={closeDialog}\n closeLabel={labels.close}\n header={mobileDialogRow ? mobilePrimaryCol?.cell(mobileDialogRow) : null}\n backCloses={mobileDialogBackCloses}\n >\n {mobileDialogContent}\n </FullBleedDialog>\n </div>\n )}\n {isMdUp && (\n <div className={cn(\"flex\", fillHeight && \"flex-1\")}>\n {/* Under `fillHeight` neither wrapper is `min-h-0`: a flex item's automatic\n minimum is its content's, and the content's height counts the body at its\n basis (`basis-48`, below), not at its rows — so the floor carries up. */}\n <div className={cn(\"relative min-w-0 flex-1\", fillHeight && \"flex flex-col\")}>\n {/* scrollbar-gutter:stable reserves the vertical-scrollbar space up\n front. Expanding a row adds height, which can bring the scrollbar\n in, which narrows the content box — and in an `auto` table layout\n that re-computes every column width. Reserving the gutter is the\n other half of the `w-0 min-w-full` fix on the expansion cell below\n (feedback #104: \"expanding an item resizes the columns\"). */}\n <div\n ref={desktopScroller}\n // The scroller: marked for the Tooltip's auto-portal (see the root above).\n data-clips=\"\"\n // Under `fillHeight` the body grows into the room from a 12rem basis (0.40,\n // §1.3): about five rows is its floor. A window too short for the page's\n // header and that much body scrolls `<main>` instead (keksdose's review).\n className={cn(\"overflow-auto [scrollbar-gutter:stable]\", fillHeight && \"min-h-0 grow basis-48\")}\n // `edgeFade`: the mask, `data-overflow` and the focus handling; nothing\n // while off. The height bound is merged over the mask, not replaced by it.\n {...edgeFadeProps}\n style={\n fillHeight\n ? edgeFadeProps.style\n : { ...edgeFadeProps.style, maxHeight: maxBodyHeight }\n }\n >\n {/* A table with no name is \"table\" in a screen reader's list of tables, and\n an app renders several. `labels.table` is how a call site says which. */}\n {/* `aria-busy` while a server page is in flight: a reader holds off announcing\n the rows that are about to be replaced, and the dimmed body says the same\n to the eye. The pager stays live — see `ServerPagination.isLoading`. */}\n <table\n ref={tableRef}\n className={cn(\"w-full\", compact ? \"text-xs\" : \"text-sm\")}\n data-fit-grow={growDrawn ? \"\" : undefined}\n aria-label={labels.table}\n aria-busy={isLoading || undefined}\n // `virtualize` (0.48.2): every row counted, rendered or not.\n aria-rowcount={rowWindow?.rowCount}\n data-virtualized={rowWindow ? \"\" : undefined}\n >\n {/* The locked column widths (0.48.2, `virtualize`): rows coming and going no\n longer move the columns. */}\n {rowWindow && (\n <colgroup>\n {virtualColKeys.map((key) => {\n const w = rowWindow.colWidths?.[key];\n return <col key={key} style={w !== undefined ? { width: w } : undefined} />;\n })}\n </colgroup>\n )}\n {/* Sticky header. position:sticky pins to the nearest scroll-container\n ancestor — so the header can only stick to THIS wrapper, never the\n page. For that to actually hold the header in place, the wrapper\n must be the thing that scrolls vertically: hence overflow-auto +\n a bounded max-height (rather than overflow-x-auto, which scrolls\n only sideways and lets the header ride away on page scroll). The\n header then stays put as you scroll the rows. position:sticky on\n <thead> isn't reliable across browsers, so we put it on each <th>\n (see DataTableHead, data-table-head.tsx). */}\n <DataTableHead\n visibleColumns={visibleColumns}\n selection={selection}\n compact={compact}\n labels={labels}\n locale={locale}\n isServer={serverRows}\n hasSortCallback={!!onSortsChange}\n hasFilterCallback={!!onFiltersChange}\n sorts={sorts}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n multiSort={multiSort}\n resizable={resizable}\n widths={widths}\n setWidths={setWidths}\n headRefs={headRefs}\n toggleSort={toggleSort}\n setFilterValue={setFilterValue}\n clearFilter={clearFilter}\n startResize={startResize}\n moveResize={moveResize}\n finishResize={finishResize}\n ariaRowIndex={rowWindow ? 1 : undefined}\n lockedWidths={rowWindow?.colWidths}\n selectionLockKey={VIRTUAL_SELECT_COL}\n />\n {(() => {\n const bodyClass = cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\");\n const leading = leadingRow ? (\n <DataTableLeadingRow\n colSpan={totalColSpan}\n sticky={leadingRowSticky}\n ariaRowIndex={rowWindow ? 2 : undefined}\n >\n {leadingRow}\n </DataTableLeadingRow>\n ) : null;\n // One memoised row each (0.48.2, K-052-2; data-table-row.tsx): the table\n // derives each row's own inputs here, as primitives, so a tick re-renders\n // only the rows whose inputs changed.\n const renderRow = (row: T, rowIndex: number) => {\n const expanded = isExpanded?.(row) ?? false;\n const key = rowKey(row);\n return (\n <DataTableRow<T>\n key={key}\n row={row}\n rowIndex={rowIndex}\n rowKey={key}\n ctx={rowCtx}\n columns={visibleColumns}\n widths={widths}\n growDrawn={growDrawn}\n linkColumn={linkColumn}\n href={rowHref?.(row)}\n selectionColumn={!!selection}\n selectable={canSelect(row)}\n selected={!!selection && selection.isSelected(row)}\n expanded={expanded}\n expansion={expanded ? expandedRow?.(row) : null}\n disclosure={rowsFocusable && !!expandedRow}\n detailId={`${detailIdBase}-${rowIndex}`}\n rowsFocusable={rowsFocusable}\n rowInteractive={!!onRowClick || !!selection}\n tabbable={key === tabRowKey}\n className={rowClassName?.(row)}\n attrs={rowAttributes?.(row)}\n compact={compact}\n totalColSpan={totalColSpan}\n rowDetailsLabel={labels.rowDetails}\n selectRowLabel={labels.selectRow}\n ariaRowIndex={rowWindow?.ariaRow(rowIndex)}\n />\n );\n };\n const emptyRow =\n slice.length === 0 ? (\n <tr>\n <td\n colSpan={totalColSpan}\n className={cn(\"text-center text-muted\", compact ? \"px-2 py-2\" : \"px-3 py-4\")}\n >\n {/* Nothing is not the same as not-yet: \"no results\" while the first\n page is still in flight is a claim the table cannot make. */}\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </td>\n </tr>\n ) : null;\n const band = (\n level: 1 | 2,\n def: DataTableGroupBy<T>,\n g: { key: string; rows: T[] },\n ariaRowIndex?: number,\n ) => (\n <DataTableGroupBand\n level={level}\n colSpan={totalColSpan}\n top={level === 1 ? bandTops[0] : bandTops[1]}\n compact={compact}\n label={def.label ? def.label(g.key, g.rows) : g.key}\n end={def.end?.(g.key, g.rows)}\n ariaRowIndex={ariaRowIndex}\n />\n );\n if (rowWindow) {\n // Windowed (0.48.2): each group's own `<tbody>` with its bands and the\n // rendered rows, spacer rows between; one spacer `<tbody>` per run of groups\n // with nothing rendered. Spacers are hidden from the accessibility tree.\n const spacer = (key: string, height: number) => (\n <tr key={key} aria-hidden=\"true\" data-vt-spacer=\"\">\n <td colSpan={totalColSpan} style={{ height, padding: 0, border: 0 }} />\n </tr>\n );\n return (\n <>\n {leading && <tbody className={bodyClass}>{leading}</tbody>}\n {rowWindow.bodies.map((b) => {\n if (b.kind === \"spacer\")\n return (\n <tbody key={b.key} data-vt-body=\"\" aria-hidden=\"true\">\n {spacer(\"s\", b.height)}\n </tbody>\n );\n const g = virtualGroups[b.group];\n const vb = virtualBands[b.group];\n return (\n <tbody\n key={vb ? `vg:${vb.g1.key}:${vb.g2?.key ?? \"\"}:${g.start}` : \"vg\"}\n data-vt-body=\"\"\n className={bodyClass}\n >\n {vb && g.bands.includes(1) && band(1, levels[0], vb.g1, rowWindow.ariaBand(b.group, 1))}\n {vb?.g2 && band(2, levels[1], vb.g2, rowWindow.ariaBand(b.group, 2))}\n {b.items.map((it) =>\n it.kind === \"gap\" ? spacer(it.key, it.height) : renderRow(slice[it.index], it.index),\n )}\n </tbody>\n );\n })}\n </>\n );\n }\n if (!desktopGroups) {\n return (\n <tbody className={bodyClass}>\n {leading}\n {slice.map(renderRow)}\n {emptyRow}\n </tbody>\n );\n }\n // Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its\n // band the row header; the leading row (and the empty state) in their own\n // before the first group.\n const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));\n return (\n <>\n {(leading || emptyRow) && (\n <tbody className={bodyClass}>\n {leading}\n {emptyRow}\n </tbody>\n )}\n {desktopGroups.map((g1) =>\n g1.children ? (\n g1.children.map((g2, j) => (\n <tbody key={`g:${g1.key}:${g2.key}:${g2.start}`} className={bodyClass}>\n {j === 0 && band(1, levels[0], g1)}\n {band(2, levels[1], g2)}\n {rowsOf(g2)}\n </tbody>\n ))\n ) : (\n <tbody key={`g:${g1.key}:${g1.start}`} className={bodyClass}>\n {band(1, levels[0], g1)}\n {rowsOf(g1)}\n </tbody>\n ),\n )}\n </>\n );\n })()}\n {showFooter && (\n // Sticky to the scroller's bottom edge for the same reason the header is\n // sticky to its top: the body scrolls inside a bounded wrapper, and a\n // total that scrolls away with the last row is a total nobody sees on a\n // 25-row page. Each cell carries its own background and its own `sticky`,\n // as the header's do — `position: sticky` on a `<tfoot>` is not reliable.\n <tfoot\n data-table-footer=\"\"\n className={cn(\n \"font-semibold text-primary\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n <tr aria-rowindex={rowWindow?.rowCount}>\n {/* The selection column has nothing to total: an empty cell keeps the\n values under their own headers. */}\n {selection && (\n <td\n className={cn(\n FOOTER_CELL,\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n />\n )}\n {visibleColumns.map((col) => {\n const width = widths[col.key];\n const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;\n const isLabel = col === footerLabelCol;\n const Cell = isLabel ? \"th\" : \"td\";\n return (\n <Cell\n key={col.key}\n data-col={col.key}\n scope={isLabel ? \"row\" : undefined}\n style={pinned}\n className={cn(\n FOOTER_CELL,\n \"align-top\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n width && \"overflow-hidden text-ellipsis\",\n // A `<th>` centres by default; the label reads from the start\n // like the cells above it, unless the column says otherwise.\n isLabel && \"text-start\",\n col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,\n logicalAlign(col.className),\n )}\n >\n {col.footer ? footerValue(col) : isLabel ? footerLabel : null}\n </Cell>\n );\n })}\n </tr>\n </tfoot>\n )}\n </table>\n {/* In the scroller, after the rows (0.48): it comes into view as the last row\n does, under `fillHeight` too, where the body is the scroller. */}\n {loadMore && (\n <DataTableLoadMoreFooter loadMore={loadMore} rowCount={sorted.length} labels={labels} compact={compact} />\n )}\n </div>\n {!unpaged && !loadMore && (\n <Pagination\n page={safePage}\n totalPages={totalPages}\n pageSize={paginationPageSize}\n total={paginationTotal}\n onPage={goToPage}\n onPageSize={\n isServer\n ? serverPagination!.onPageSizeChange\n : (n) => {\n // To the owner when it holds the page size, as `commitSorts` does.\n if (onPageSizeChange) onPageSizeChange(n);\n else setPageSize(n);\n setPage(0);\n }\n }\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n </div>\n {columnSettings && (\n <ColumnSettingsPanel\n columns={columns}\n hiddenCols={hiddenCols}\n setHiddenCols={setHiddenCols}\n autoHidden={autoHidden}\n keepColumn={keepColumn}\n showSettings={showSettings}\n setShowSettings={setShowSettings}\n columnsCountLabel={columnsCountLabel}\n labels={labels}\n autoSizeAll={autoSizeAll}\n />\n )}\n </div>\n )}\n </Root>\n );\n}\n\n/** The framed root: the kit's flush Card, as every table had before `frame`. */\nfunction FramedRoot({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <Card flush {...rest}>\n {children}\n </Card>\n );\n}\n\n/** The unframed root (`frame={false}`): no border, surface, radius or shadow — the\n * host's own card supplies them. */\nfunction PlainRoot(props: React.HTMLAttributes<HTMLDivElement>) {\n return <div {...props} />;\n}\n\n/** The loading row's content: a spinner beside the word, the word for everyone. */\nfunction LoadingText({ label }: { label: string }) {\n // `<bdi>`: the label takes its direction from its own letters, not the table's. The\n // English default inside a `dir=\"rtl\"` table otherwise had its trailing \"…\" — a\n // neutral with no letter after it — resolved right-to-left and drawn in front of\n // the word: \"…Loading\". An Arabic or Hebrew label still reads right-to-left.\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{label}</bdi>\n </span>\n );\n}\n"],"mappings":"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","names":["Fragment"]}