@eifi1/ui-kit 0.24.1 → 0.25.1

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 (148) hide show
  1. package/README.md +6 -6
  2. package/dist/chart.d.ts +2 -2
  3. package/dist/components/account-chips.d.ts +1 -1
  4. package/dist/components/amount-input.d.ts +2 -2
  5. package/dist/components/button-group.d.ts +2 -2
  6. package/dist/components/calculator.d.ts +2 -2
  7. package/dist/components/column-mapper.d.ts +28 -9
  8. package/dist/components/column-mapper.js +63 -77
  9. package/dist/components/column-mapper.js.map +1 -1
  10. package/dist/components/confirm-dialog.d.ts +2 -2
  11. package/dist/components/copy-button.d.ts +2 -2
  12. package/dist/components/danger-confirm.d.ts +2 -2
  13. package/dist/components/data-table-cells.d.ts +1 -1
  14. package/dist/components/data-table-filter-popover.d.ts +1 -1
  15. package/dist/components/data-table-filters.d.ts +1 -1
  16. package/dist/components/data-table-labels.d.ts +1 -1
  17. package/dist/components/data-table-pagination.d.ts +1 -1
  18. package/dist/components/data-table-pagination.js +6 -1
  19. package/dist/components/data-table-pagination.js.map +1 -1
  20. package/dist/components/data-table.d.ts +1 -1
  21. package/dist/components/data-table.js +7 -1
  22. package/dist/components/data-table.js.map +1 -1
  23. package/dist/components/facing-pair.d.ts +2 -2
  24. package/dist/components/file-button.d.ts +2 -2
  25. package/dist/components/file-dropzone.d.ts +2 -2
  26. package/dist/components/form-actions.d.ts +2 -2
  27. package/dist/components/iban-input.d.ts +2 -2
  28. package/dist/components/language-select.d.ts +2 -2
  29. package/dist/components/money-field.d.ts +2 -2
  30. package/dist/components/number-field.d.ts +2 -2
  31. package/dist/components/number-input.d.ts +2 -2
  32. package/dist/components/numpad-sheet.d.ts +2 -2
  33. package/dist/components/phone-input.d.ts +2 -2
  34. package/dist/components/series-chart.d.ts +2 -2
  35. package/dist/components/settings-fields.d.ts +2 -2
  36. package/dist/components/share-card.d.ts +2 -2
  37. package/dist/components/swipeable-row.js +1 -1
  38. package/dist/components/swipeable-row.js.map +1 -1
  39. package/dist/components/table.d.ts +35 -1
  40. package/dist/components/table.js +4 -0
  41. package/dist/components/table.js.map +1 -1
  42. package/dist/components/text-link.d.ts +2 -2
  43. package/dist/components/time-input.d.ts +2 -2
  44. package/dist/components/tooltip.d.ts +103 -14
  45. package/dist/components/tooltip.js +194 -57
  46. package/dist/components/tooltip.js.map +1 -1
  47. package/dist/components/translation-review-editor.d.ts +9 -1
  48. package/dist/components/translation-review-editor.js +11 -1
  49. package/dist/components/translation-review-editor.js.map +1 -1
  50. package/dist/components/translation-review-labels.d.ts +10 -0
  51. package/dist/components/translation-review-labels.js +5 -1
  52. package/dist/components/translation-review-labels.js.map +1 -1
  53. package/dist/components/translation-review.d.ts +145 -8
  54. package/dist/components/translation-review.js +372 -91
  55. package/dist/components/translation-review.js.map +1 -1
  56. package/dist/components/ui.d.ts +2 -2
  57. package/dist/components/ui.js +55 -18
  58. package/dist/components/ui.js.map +1 -1
  59. package/dist/components/use-table-state.d.ts +1 -1
  60. package/dist/{data-table-labels-B7OdnM0S.d.ts → data-table-labels-Cdn1QS2k.d.ts} +21 -1
  61. package/dist/data-table.d.ts +1 -1
  62. package/dist/feedback/feedback-attachment.d.ts +2 -2
  63. package/dist/feedback/feedback-dialog.d.ts +2 -2
  64. package/dist/feedback/feedback-dialog.js +6 -1
  65. package/dist/feedback/feedback-dialog.js.map +1 -1
  66. package/dist/feedback/feedback-inbox.d.ts +2 -2
  67. package/dist/feedback/feedback-inbox.js +2 -0
  68. package/dist/feedback/feedback-inbox.js.map +1 -1
  69. package/dist/feedback/feedback-thread.d.ts +2 -2
  70. package/dist/{feedback-DOwPu-Il.d.ts → feedback-BXKH-ToU.d.ts} +32 -3
  71. package/dist/feedback.d.ts +2 -2
  72. package/dist/hooks/use-body-scroll-lock.js +16 -0
  73. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  74. package/dist/hooks/use-file-drop.d.ts +2 -2
  75. package/dist/hooks/use-search-param-state.d.ts +58 -3
  76. package/dist/hooks/use-search-param-state.js +107 -35
  77. package/dist/hooks/use-search-param-state.js.map +1 -1
  78. package/dist/i18n/defaults.d.ts +2 -2
  79. package/dist/i18n/german.d.ts +2 -2
  80. package/dist/i18n/german.js +8 -1
  81. package/dist/i18n/german.js.map +1 -1
  82. package/dist/i18n/kit-labels.d.ts +2 -2
  83. package/dist/i18n/languages.d.ts +2 -2
  84. package/dist/i18n/locales/de-CH.d.ts +2 -2
  85. package/dist/i18n/locales/en.d.ts +2 -2
  86. package/dist/i18n/locales/en.js +5 -1
  87. package/dist/i18n/locales/en.js.map +1 -1
  88. package/dist/i18n/locales/es.d.ts +2 -2
  89. package/dist/i18n/locales/es.js +7 -1
  90. package/dist/i18n/locales/es.js.map +1 -1
  91. package/dist/i18n/locales/fr.d.ts +2 -2
  92. package/dist/i18n/locales/fr.js +6 -1
  93. package/dist/i18n/locales/fr.js.map +1 -1
  94. package/dist/i18n/locales/hu.d.ts +2 -2
  95. package/dist/i18n/locales/hu.js +9 -1
  96. package/dist/i18n/locales/hu.js.map +1 -1
  97. package/dist/i18n/locales/it.d.ts +2 -2
  98. package/dist/i18n/locales/it.js +6 -1
  99. package/dist/i18n/locales/it.js.map +1 -1
  100. package/dist/i18n/locales/zh.d.ts +2 -2
  101. package/dist/i18n/locales/zh.js +6 -1
  102. package/dist/i18n/locales/zh.js.map +1 -1
  103. package/dist/i18n/review.d.ts +2 -2
  104. package/dist/i18n/review.js +11 -0
  105. package/dist/i18n/review.js.map +1 -1
  106. package/dist/index.d.ts +6 -6
  107. package/dist/index.js +9 -1
  108. package/dist/index.js.map +1 -1
  109. package/dist/lib/strip-fade.d.ts +84 -5
  110. package/dist/lib/strip-fade.js +109 -11
  111. package/dist/lib/strip-fade.js.map +1 -1
  112. package/dist/lib/translation-review.d.ts +45 -1
  113. package/dist/lib/translation-review.js +38 -1
  114. package/dist/lib/translation-review.js.map +1 -1
  115. package/dist/rhf/fields.d.ts +2 -2
  116. package/dist/rhf/form.d.ts +2 -2
  117. package/dist/rhf.d.ts +2 -2
  118. package/dist/shell/app-shell.d.ts +2 -2
  119. package/dist/shell/top-bar-brand.d.ts +2 -2
  120. package/dist/shell.d.ts +2 -2
  121. package/dist/wizard/stepper-nav.d.ts +2 -2
  122. package/dist/wizard.d.ts +2 -2
  123. package/package.json +1 -1
  124. package/src/components/column-mapper.tsx +120 -113
  125. package/src/components/data-table-pagination.tsx +10 -1
  126. package/src/components/data-table.tsx +34 -1
  127. package/src/components/swipeable-row.tsx +1 -1
  128. package/src/components/table.tsx +39 -0
  129. package/src/components/tooltip.tsx +487 -104
  130. package/src/components/translation-review-editor.tsx +21 -1
  131. package/src/components/translation-review-labels.ts +17 -0
  132. package/src/components/translation-review.tsx +657 -109
  133. package/src/components/ui.tsx +104 -4
  134. package/src/feedback/feedback-dialog.tsx +21 -0
  135. package/src/feedback/feedback-inbox.tsx +19 -2
  136. package/src/hooks/use-body-scroll-lock.ts +25 -0
  137. package/src/hooks/use-search-param-state.ts +283 -41
  138. package/src/i18n/german.ts +10 -0
  139. package/src/i18n/locales/en.ts +7 -0
  140. package/src/i18n/locales/es.ts +9 -0
  141. package/src/i18n/locales/fr.ts +9 -0
  142. package/src/i18n/locales/hu.ts +11 -0
  143. package/src/i18n/locales/it.ts +9 -0
  144. package/src/i18n/locales/zh.ts +8 -0
  145. package/src/i18n/review.ts +11 -0
  146. package/src/index.ts +12 -0
  147. package/src/lib/strip-fade.ts +220 -15
  148. package/src/lib/translation-review.ts +85 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n ArrowDown,\n ArrowDownWideNarrow,\n ArrowUp,\n ArrowUpDown,\n ArrowUpNarrowWide,\n ChevronDown,\n ChevronRight,\n Filter,\n Pencil,\n Trash2,\n X,\n} from \"lucide-react\";\nimport { Card, IconButton, Select, Spinner } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport {\n defaultFilterState,\n isFilterActive,\n resolveFilter,\n rowMatches,\n} 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 { FilterPopover } from \"./data-table-filter-popover\";\nimport { Pagination } from \"./data-table-pagination\";\nimport { SwipeableRow, type SwipeAction } from \"./swipeable-row\";\nimport { useBackdropClose } from \"./modal\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { Popover } from \"./popover\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale } from \"../i18n/kit-labels\";\nimport { Tooltip } from \"./tooltip\";\nimport { dirOf, isRtl, 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\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\n/**\n * Split a caller's cell attributes into what may reach the element and what the kit\n * owns. `role` and `data-col` are dropped outright (see `cellProps`); `className` and\n * `style` come back separately so the kit can merge rather than replace them.\n */\nfunction splitCellProps(\n props: DataTableCellProps | DataTableHeadProps | undefined,\n): { attrs: Record<string, unknown>; className?: string; style?: CSSProperties } {\n if (!props) return { attrs: {} };\n const {\n className,\n style,\n role: _role,\n \"data-col\": _col,\n ...attrs\n } = props as DataTableCellProps & { \"data-col\"?: unknown };\n return { attrs, className, style };\n}\n\n/** Only the `data-*` attributes of a cell's props — what a phone card field takes. */\nfunction dataAttrsOf(props: DataTableCellProps | undefined): Record<string, unknown> | undefined {\n if (!props) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(props)) if (k.startsWith(\"data-\") && k !== \"data-col\") out[k] = v;\n return out;\n}\n\n/** A column resize in flight. Held in a ref rather than in state: the live width is\n * written straight onto the column's cells, so a drag costs zero renders and the\n * one `setWidths` happens when the pointer comes up. */\ninterface Resize {\n key: string;\n pointerId: number;\n startX: number;\n startWidth: number;\n /** +1 when a drag to the right widens the column, -1 in a right-to-left table,\n * where the handle sits on the column's LEFT edge and widening is a drag left. */\n sign: 1 | -1;\n /** What React last rendered this column at, so a CANCELLED drag can put the header\n * back — React cannot, it never saw the inline width the drag wrote. */\n hadWidth: number | undefined;\n /** The header and the body cells under it, collected ONCE when the drag starts.\n * A `<th>` alone cannot shrink a column whose `<td>`s already carry a pinned\n * width — and nothing re-renders during a drag, so the list cannot go stale. */\n cells: HTMLElement[];\n width: number;\n /** A press with no drag in it is not a resize — the handle is also the\n * double-click target that CLEARS a width, and committing the header's current\n * size on the way past would pin every column the user ever brushed. */\n moved: boolean;\n}\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 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. On a\n * phone the card list is the scroller instead, with the filter button pinned above\n * it; an unbounded parent simply lets either layout grow to its content. The parent must give it a bounded height (e.g. a flex\n * column inside the viewport-locked app shell). See feedback #207.\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 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 * 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 * Opt-in multi-row selection (desktop table only). 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 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?: Partial<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. Defaults to\n * \"hbui-table:\"; pass your app's own prefix to keep a stable namespace. */\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 * 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;\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 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.\n */\n mobileSwipeActions?: (row: T) => MobileSwipeActions | null;\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 * 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 /** 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). 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 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 `missingDataTableLabels`), 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/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nconst ROW_ACTIONS_KEY = \"__rowActions\";\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\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/** The logical sides onto the physical drag directions {@link SwipeableRow} takes. */\nfunction physicalSwipe(\n actions: MobileSwipeActions,\n dir: Direction,\n): { left?: SwipeAction[]; right?: SwipeAction[] } {\n const rtl = dir === \"rtl\";\n return {\n left: (rtl ? actions.end : actions.start) ?? actions.left,\n right: (rtl ? actions.start : actions.end) ?? actions.right,\n };\n}\n\n// A bare `text-right` / `text-left` token, with or without variant prefixes\n// (`md:text-right`). Deliberately NOT `text-right-…` or an arbitrary value.\nconst PHYSICAL_ALIGN = /(^|\\s)((?:[\\w-]+:)*)text-(left|right)(?=\\s|$)/g;\n\n/**\n * A column's `className`/`headClassName` with physical text alignment rewritten to\n * logical: `text-right` → `text-end`, `text-left` → `text-start`.\n *\n * Every numeric column written before 0.7.0 says `text-right`, and that is what it\n * MEANT: the end of the line, where digits line up. Rendered as written, a\n * right-to-left table puts those numbers on the far side from their own header's\n * sort button. Rewriting keeps each existing call site correct in both directions\n * with no change on its side; a caller who genuinely wants the physical side in RTL\n * too can say `ltr:text-right rtl:text-right`, which this leaves alone.\n */\nfunction logicalAlign(className: string | undefined): string | undefined {\n return className?.replace(\n PHYSICAL_ALIGN,\n (token: string, lead: string, variants: string, side: string) =>\n // Already scoped to one direction: that is a caller choosing the physical side.\n /(^|:)(ltr|rtl):/.test(variants)\n ? token\n : `${lead}${variants}text-${side === \"right\" ? \"end\" : \"start\"}`,\n );\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-[var(--bg-surface-2)] shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** Drop the undefined entries of a {@link DataTableProps.rowAttributes} map, so a\n * caller can write `{ \"data-x\": cond ? \"y\" : undefined }` and get no attribute\n * at all rather than the string \"undefined\" in the DOM. */\nfunction cleanAttrs(\n attrs: Record<string, string | undefined> | undefined,\n): Record<string, string> | undefined {\n if (!attrs) return undefined;\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(attrs)) if (v !== undefined) out[k] = v;\n return out;\n}\n\n/**\n * A column's name as a plain string.\n *\n * `header` is a ReactNode — an icon, a unit suffix, a tooltip-wrapped span — and\n * neither a live region nor the column-settings checklist can speak a React element.\n * The `key` fallback is the one the settings panel has always used; a column whose\n * header is not a string and whose key is not human-readable is a call site that\n * should pass a string header, and that is visible in what gets announced.\n */\nfunction columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {\n if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;\n return col.headerText ?? (typeof col.header === \"string\" ? col.header : col.key);\n}\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, beside the control, 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/**\n * The card list's sort control: a \"Sort by\" select and a direction button.\n *\n * keksdose K10 (inputs audit 2026-10-02 §5; its aggregated report tables, #258): a\n * table is sorted by its header row and the phone card list has none, so the two\n * decisions the header carries — which column, which direction — were unreachable\n * below the breakpoint. keksdose hand-built this row over its DataTable to answer\n * \"who did I pay the most\" on the device it was asked on; this is that row, driven by\n * the SAME sort state as the headers (controlled `sorts` included), so turning the\n * phone sideways keeps the order and a sort chosen here is the one the URL and the\n * storage key hold.\n *\n * The rules are the header's: a newly picked column starts in its `firstSort`, the\n * direction button flips the primary sort (secondary ones stay), and \"Default order\"\n * — the third click of a `\"tri\"` cycle — is offered only under `\"tri\"`, since a\n * `\"toggle\"` table promises it is always ranked by something. The select's label is\n * a real `<label>` beside it, and the button sits outside that label: a click inside\n * a label is forwarded to the control it names, so a nested button would flip AND\n * open the select.\n */\nfunction MobileSort<T>({\n columns,\n sorts,\n sortCycle,\n compact,\n labels,\n onPick,\n onFlip,\n}: {\n columns: DataTableColumn<T>[];\n sorts: SortState[];\n sortCycle: SortCycle;\n compact: boolean;\n labels: DataTableLabels;\n onPick: (key: string | null) => void;\n onFlip: () => void;\n}) {\n const selectId = useId();\n const sortLabels: DataTableSortLabels = labels;\n if (columns.length === 0) return null;\n // A persisted or URL sort can name a column with no `sortBy` (or a hidden one): the\n // sort chain drops it, and so does the control rather than claiming it.\n const primary = sorts[0] && columns.some((c) => c.key === sorts[0].key) ? sorts[0] : null;\n const offerDefault = sortCycle === \"tri\" || primary === null;\n const desc = primary?.dir === \"desc\";\n return (\n <div\n data-table-mobile-sort=\"\"\n className={cn(\n \"flex items-center gap-2 border-b border-[var(--border)]\",\n compact ? \"px-3 py-1.5\" : \"px-4 py-2\",\n )}\n >\n <label htmlFor={selectId} className=\"shrink-0 text-xs text-[var(--text-muted)]\">\n {sortLabels.sortBy}\n </label>\n <Select\n id={selectId}\n size={compact ? \"sm\" : \"md\"}\n className=\"min-w-0 flex-1\"\n value={primary?.key ?? \"\"}\n onChange={(e) => onPick(e.target.value || null)}\n >\n {offerDefault && <option value=\"\">{sortLabels.sortDefault}</option>}\n {columns.map((col) => (\n <option key={col.key} value={col.key}>\n {columnLabel(col, labels)}\n </option>\n ))}\n </Select>\n <IconButton\n size={compact ? \"xs\" : \"md\"}\n variant=\"secondary\"\n label={desc ? sortLabels.sortDescending : sortLabels.sortAscending}\n disabled={primary === null}\n onClick={onFlip}\n >\n {desc ? <ArrowDownWideNarrow /> : <ArrowUpNarrowWide />}\n </IconButton>\n </div>\n );\n}\n\n/**\n * What counts as a control of its own inside a clickable row: a click or key press\n * that lands on one of these belongs to it, not to the row's `onRowClick`. The row's\n * own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.\n */\nconst OWN_CONTROL =\n 'a[href],button,input,select,textarea,label,summary,[role=\"button\"],[role=\"link\"],[role=\"checkbox\"],[role=\"switch\"],[role=\"menuitem\"],[role=\"option\"],[contenteditable=\"\"],[contenteditable=\"true\"],[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Whether a click that reached `row`'s handler did not start in the row's own DOM.\n *\n * React bubbles an event through the COMPONENT tree, portals included: a popover, a\n * menu or a dialog a cell (or the expansion under it) opens renders at `<body>` but\n * still reports its clicks to every React ancestor — the row among them. So a click\n * in a cell's portalled date picker opened the row it came from, and a page wrapped\n * such content in `<div onClick={(e) => e.stopPropagation()}>` to stop it (kastlan's\n * feedback page did, round its comments). The row owns the clicks on the row.\n */\nfunction fromOutside(target: EventTarget | null, row: HTMLElement): boolean {\n return !(target instanceof Node) || !row.contains(target);\n}\n\n/** Whether an event on `row` started on a control nested inside it. */\nfunction fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {\n if (!(target instanceof Element)) return false;\n const hit = target.closest(OWN_CONTROL);\n return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute(\"data-row-link\");\n}\n\n/** Arrow/Home/End between the focusable rows of one `<tbody>` — the roving half of\n * the keyboard-activatable rows (see the row's `onKeyDown`). */\nfunction moveRowFocus(from: HTMLElement, key: string): boolean {\n if (!(key in ROW_NAV_KEYS)) return false;\n const body = from.parentElement;\n if (!body) return false;\n const rows = Array.from(body.children).filter(\n (el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute(\"data-row-nav\"),\n );\n const i = rows.indexOf(from);\n const target =\n key === \"ArrowDown\"\n ? rows[i + 1]\n : key === \"ArrowUp\"\n ? rows[i - 1]\n : key === \"Home\"\n ? rows[0]\n : key === \"End\"\n ? rows[rows.length - 1]\n : undefined;\n // At either end the key is still consumed, or ArrowDown on the last row would\n // scroll the page out from under a focused row.\n target?.focus();\n return true;\n}\nconst ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;\n\n/** A click the APP owns. Anything else — middle, ⌘/Ctrl, Shift, Alt — belongs to\n * the browser, and the whole feature is not touching it. */\nconst isPlainLeftClick = (e: React.MouseEvent) =>\n e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;\n\n/**\n * The anchor a linkable row is opened through (Keksdose feedback #451).\n *\n * The plain left click is OURS: cancelled, then handed to the row's own handler, so\n * clicking a row still expands it in place instead of reloading the page. Every\n * other click — middle, ⌘/Ctrl, Shift, Alt — is left completely untouched, which is\n * the entire point of using an anchor rather than an `onAuxClick` + `window.open`:\n * that imitation buys back the middle click and still loses ⌘/Ctrl-click, \"copy link\n * address\", the status-bar preview and the phone's long-press menu.\n *\n * Modified clicks DO stop propagating: on a table with `selection`, ⌘/Ctrl-click and\n * Shift-click on a row mean \"select\" (feedback #289), and a row must not change its\n * selection while the browser is opening a tab. The rest of the row still selects —\n * the link is one cell, not the row.\n *\n * `draggable={false}` because an anchor otherwise hijacks a horizontal mouse drag as\n * a link-drag: that is both drag-to-select-text on the desktop table and the mouse\n * path through SwipeableRow's gesture on the mobile card.\n */\nfunction RowLink({\n href,\n onActivate,\n className,\n children,\n ...rest\n}: {\n href: string;\n /** Runs on a plain left click. Omit it where an ANCESTOR already handles the\n * click (the desktop `<tr>`), or the row would toggle twice. */\n onActivate?: () => void;\n className?: string;\n children: ReactNode;\n} & Omit<\n React.AnchorHTMLAttributes<HTMLAnchorElement>,\n \"href\" | \"className\" | \"children\" | \"onClick\" | \"onClickCapture\"\n>) {\n return (\n <a\n {...rest}\n href={href}\n draggable={false}\n className={className}\n // CAPTURE phase, and it has to be: on the mobile card the whole card is the\n // anchor, so a nested control sits INSIDE it — and the ones that matter\n // (the feedback row's status toggles) stop the click propagating so the row\n // won't also expand. A bubble-phase handler would never run for those, and\n // the browser would follow the link out from under the button press. Cancelling\n // here happens before any descendant can silence the event; `stopPropagation`\n // does not undo a `preventDefault`.\n onClickCapture={(e) => {\n // A click in a portal a descendant opened is not a click on the link: leave\n // it (and a link inside it) alone. See `fromOutside`.\n if (fromOutside(e.target, e.currentTarget)) return;\n if (isPlainLeftClick(e)) e.preventDefault();\n }}\n onClick={(e) => {\n if (fromOutside(e.target, e.currentTarget)) return;\n if (!isPlainLeftClick(e)) {\n e.stopPropagation();\n return;\n }\n // Already cancelled above; this phase only decides what opens. A nested\n // control that stopped propagation deliberately never reaches it, which is\n // exactly how the row behaved before it became a link.\n onActivate?.();\n }}\n >\n {children}\n </a>\n );\n}\n\n// ---------- Main DataTable ----------\n\n\n\nimport { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from \"./use-table-state\";\nimport { useMobileReveal } from \"./use-mobile-reveal\";\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 filters: filtersProp,\n onFiltersChange,\n sorts: sortsProp,\n onSortsChange,\n selection,\n labels: labelsProp,\n locale: localeProp,\n storageKeyPrefix = DEFAULT_PERSIST_PREFIX,\n mobileExpandAsDialog = false,\n mobileDialogBackCloses = true,\n mobileGroupBy,\n mobileGroupLabel,\n mobileCard,\n mobileSort = false,\n mobileSwipeActions,\n leadingRow,\n density = \"comfortable\",\n sortCycle = \"tri\",\n chrome = \"full\",\n columnSettings: columnSettingsProp,\n resizable: resizableProp,\n multiSort: multiSortProp,\n frame = true,\n className,\n rowActions,\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 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 const labels = useMemo(\n () =>\n resolveDataTableLabels(\n labelOverrides || labelsProp\n ? {\n ...labelOverrides,\n ...labelsProp,\n presets: { ...labelOverrides?.presets, ...labelsProp?.presets },\n }\n : undefined,\n ),\n [labelOverrides, labelsProp],\n );\n const locale = useKitLocale(localeProp);\n\n // ---- Row actions ----\n // The actions column is appended to the caller's columns rather than drawn as a\n // second special case beside the selection column: it then gets the cell padding,\n // the widths and the phone rules (`mobileHidden`) every other column gets.\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n const columns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: <span className=\"sr-only\">{labels.actions}</span>,\n cell: (row) => (\n <div className=\"flex items-center justify-end gap-0.5\">\n {rowActionsFor(row).map((a) => (\n <IconButton\n key={a.key}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n label={a.label}\n tone={a.tone === \"danger\" ? \"danger\" : undefined}\n disabledReason={a.action.disabledReason?.(row)}\n // The row's click handler already skips its own controls; this is for\n // the caller who wraps the table in a clickable element of their own.\n stopPropagation\n onClick={() => void runRowAction(a.action, row)}\n >\n {a.icon}\n </IconButton>\n ))}\n </div>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsFor, labels.actions, density],\n );\n const swipesFromActions = (row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n className: a.tone === \"danger\" ? \"bg-[var(--danger)]\" : \"bg-[var(--brand)]\",\n armedClassName: a.tone === \"danger\" ? \"bg-[var(--danger-hover)]\" : \"bg-[var(--brand-hover)]\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\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 });\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 const widths = useMemo(() => (resizable ? storedWidths : {}), [resizable, storedWidths]);\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 (isServer) 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, isServer, locale]);\n\n const sorted = useMemo(() => {\n if (isServer) 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, isServer]);\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;\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) => ((!isServer || 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 (isServer) return;\n announce(labels.filterResults(filtered.length, rows.length));\n }, [filterKey, filtered.length, rows.length, isServer, 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 const visibleColumns = useMemo(\n () => columns.filter((c) => !hiddenCols.has(c.key)),\n [columns, hiddenCols],\n );\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 ----\n //\n // Pointer events with capture, rather than a `mousemove`/`mouseup` pair on `window`.\n // Three defects in one eleven-line handler (audit 2026-09-22):\n //\n // * a `setWidths` per `mousemove` re-rendered every row of the table on every pixel\n // of the drag — on a 500-row table that is the whole body, sixty times a second;\n // * the two `window` listeners were added from inside an event handler, so nothing\n // removed them if the table unmounted mid-drag (the row beneath opens a route),\n // and `document.body.style` stayed at `user-select: none; cursor: col-resize` for\n // the rest of the session — the whole page unselectable, with the wrong cursor;\n // * `mousedown` is a mouse. Under a finger the handle did nothing at all.\n //\n // `setPointerCapture` answers all three: the handle itself receives every move and\n // the terminating `pointerup`/`pointercancel` wherever the pointer travels, so there\n // is nothing global to leak, and a touch drag works because a finger is a pointer.\n // The live width is written straight onto the column's own cells — the same three\n // properties the render sets, so the drag and the commit cannot disagree — and\n // `setWidths` is called once, when the drag ends.\n const resizing = useRef<Resize | null>(null);\n\n /** Every cell in one column, header first, totals row included. `data-col` rather than a cell index:\n * an expanded row holds a single `colSpan` cell, so the nth `<td>` of a row is\n * not reliably the nth column. */\n const columnCells = (key: string): HTMLElement[] => {\n const th = headRefs.get(key);\n if (!th) return [];\n const table = th.closest(\"table\");\n if (!table) return [th];\n return [\n th,\n ...Array.from(table.querySelectorAll<HTMLElement>(\"tbody [data-col], tfoot [data-col]\")).filter(\n (el) => el.dataset.col === key,\n ),\n ];\n };\n\n const applyLiveWidth = (cells: HTMLElement[], width: number | undefined) => {\n const px = width == null ? \"\" : `${width}px`;\n for (const el of cells) {\n el.style.width = px;\n el.style.minWidth = px;\n el.style.maxWidth = px;\n }\n };\n\n const releaseResizeChrome = (handle: Element, pointerId: number) => {\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n // Optional calls: jsdom implements neither, and the handlers sit on the handle\n // itself, so a run without capture behaves the same for anything that stays\n // inside the element.\n if (handle.hasPointerCapture?.(pointerId)) handle.releasePointerCapture?.(pointerId);\n };\n\n const startResize = (e: React.PointerEvent<HTMLElement>, key: string) => {\n // Secondary buttons open menus; only a primary press starts a drag.\n if (e.button !== 0) return;\n e.preventDefault();\n e.stopPropagation();\n const th = headRefs.get(key);\n const startWidth = th?.getBoundingClientRect().width ?? 100;\n resizing.current = {\n key,\n pointerId: e.pointerId,\n startX: e.clientX,\n startWidth,\n sign: isRtl(e.currentTarget) ? -1 : 1,\n hadWidth: widths[key],\n cells: columnCells(key),\n width: startWidth,\n moved: false,\n };\n e.currentTarget.setPointerCapture?.(e.pointerId);\n document.body.style.userSelect = \"none\";\n document.body.style.cursor = \"col-resize\";\n };\n\n const moveResize = (e: React.PointerEvent<HTMLElement>) => {\n const drag = resizing.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n drag.width = Math.max(40, Math.round(drag.startWidth + drag.sign * (e.clientX - drag.startX)));\n drag.moved = true;\n applyLiveWidth(drag.cells, drag.width);\n };\n\n const finishResize = (e: React.PointerEvent<HTMLElement>, commit: boolean) => {\n const drag = resizing.current;\n // Before the early return: the page gets its selection and its cursor back even\n // for a pointer we were not tracking.\n releaseResizeChrome(e.currentTarget, e.pointerId);\n if (!drag || drag.pointerId !== e.pointerId) return;\n resizing.current = null;\n if (commit && drag.moved) {\n setWidths((w) => ({ ...w, [drag.key]: drag.width }));\n return;\n }\n // Cancelled — the browser took the pointer for a system gesture — or the press\n // never became a drag. Either way the column goes back to the width React last\n // rendered it at, which nothing else can do: the inline width the drag wrote is\n // invisible to React, so a re-render with an unchanged `widths` would keep it.\n applyLiveWidth(drag.cells, drag.hadWidth);\n };\n\n // The last exit path. A table that unmounts mid-drag takes the handle, and with it\n // every handler above, out of the document — this is the only place left to hand the\n // page back its text selection and its cursor.\n useEffect(\n () => () => {\n if (!resizing.current) return;\n resizing.current = null;\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n },\n [],\n );\n\n const autoSizeAll = () => {\n // Clear widths so the table reflows to natural sizes, then snapshot each\n // column's width and write it back — except for the last visible column,\n // which is left unconstrained so it absorbs any remaining horizontal space.\n setWidths({});\n requestAnimationFrame(() => {\n const next: Record<string, number> = {};\n const cols = columns.filter((c) => !hiddenCols.has(c.key));\n cols.forEach((col, idx) => {\n if (idx === cols.length - 1) return;\n const th = headRefs.get(col.key);\n if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);\n });\n setWidths(next);\n });\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 && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const columnsCountLabel = rowActions\n ? labels.columnsCount(visibleCount - 1, totalCount - 1)\n : labels.columnsCount(visibleCount, totalCount);\n // Match Tailwind's `md` breakpoint: we render either the table or the card\n // list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate.\n const isMdUp = useMediaQuery(\"(min-width: 768px)\", true);\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 const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);\n const mobilePrimaryCol =\n mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;\n const mobileSecondaryColumns = mobileColumns.filter((c) => c !== mobilePrimaryCol);\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 const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // The row whose selection was last toggled, so Shift+click can select the range up\n // to it (feedback #289). Held as its KEY, not as an index into `slice`: the slice is\n // re-derived from the current sort, filter and page, so an index taken before a\n // header was clicked addresses a different row afterwards — and the range then ran\n // from a row the user had never touched.\n const selectionAnchor = useRef<string | number | null>(null);\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 /** The checkbox click that drew a range, so its `onChange` can be told apart. */\n const rangeClick = useRef<Event | null>(null);\n const canSelect = (row: T) => !!selection && (selection.isSelectable?.(row) ?? true);\n const selectRange = (toIndex: number) => {\n if (!selection || selectionAnchor.current === null) return false;\n const fromIndex = slice.findIndex((r) => rowKey(r) === selectionAnchor.current);\n // The anchor has been filtered, sorted or paged off the screen: there is no range\n // on display to draw, so the click falls through to the plain single-row toggle.\n if (fromIndex < 0) return false;\n const [lo, hi] = [fromIndex, toIndex].sort((a, b) => a - b);\n const range: T[] = [];\n for (let i = lo; i <= hi; i++) {\n const r = slice[i];\n if (r && canSelect(r)) range.push(r);\n }\n // One call where the owner can take one. See `selection.onToggleMany`: the row-by-\n // row loop below is the fallback for an owner that has not adopted it, not the\n // shape this should be in.\n if (selection.onToggleMany) selection.onToggleMany(range, true);\n else for (const r of range) selection.onToggle(r, true);\n return true;\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 ? expandedRow?.(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. Extracted so the flat list and the grouped list (below)\n // share identical row markup.\n const renderMobileRow = (row: T) => {\n const expanded = isExpanded?.(row) ?? false;\n const expansion = expanded ? expandedRow?.(row) : null;\n const tint = rowClassName?.(row);\n // Use div+role=\"button\" rather than a real <button> so cells that\n // contain their own interactive controls (status toggles, action\n // icons) don't end up as illegal nested buttons.\n const interactive = !!onRowClick;\n // Say that the card OPENS something (Keksdose feedback #163). A phone card had\n // only a cursor and a tap-tint to advertise its editor — on the invoice review\n // screen, whose entire purpose is correcting mis-read lines, that read as \"these\n // values are not editable\". A chevron is the affordance every list on a phone\n // uses for exactly this, and it points the way the row actually opens: right into\n // a sheet, or down into an inline panel that flips it when expanded.\n const opensDetail = interactive && !!expandedRow;\n const swipeActions = swipeActionsFor?.(row);\n const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;\n // An expanded row never swipes: the editor below it owns the horizontal space,\n // and dragging the header away from its own form reads as a glitch.\n const swipeEnabled = !!swipe && !expanded;\n const href = mobileCardLinkable ? rowHref?.(row) : undefined;\n const cardClass = cn(\n \"w-full text-start flex items-center\",\n compact ? \"px-3 py-2 gap-2 text-sm\" : \"px-4 py-3 gap-3\",\n // Live #320's other half: the card acknowledges the touch before the sheet\n // arrives. On a cold route the data can take a beat, and an unacknowledged tap\n // reads as \"did that register?\" — which is most of what \"abrupt\" means here.\n // `origin-center` + a 0.5% squeeze is deliberately almost subliminal: this fires\n // on every row of a long list, so anything larger becomes the list's personality\n // rather than feedback. `transition-transform` alone, so the existing background\n // flip stays instant.\n interactive &&\n \"cursor-pointer transition-transform duration-100 active:scale-[0.995] active:bg-[var(--bg-active)] motion-reduce:transition-none motion-reduce:active:scale-100\",\n );\n // The card's content is written once and worn by either tag below. Note that\n // the primary cell is rendered RAW here, never through `linkColumn` — the card\n // itself is the anchor, and routing it through `linkColumn` too would nest an\n // <a> inside an <a>. Desktop and mobile are mutually exclusive (`isMdUp`), so\n // exactly one anchor exists per row.\n const cardInner = (\n <>\n {/* The card's own content keeps the column it always had; the chevron sits\n beside it rather than inside, so a caller's `mobileCard` is untouched. */}\n <div className={cn(\"flex min-w-0 flex-1 flex-col\", compact ? \"gap-1\" : \"gap-2\")}>\n {mobileCard ? (\n mobileCard(row)\n ) : (\n <>\n {/* Only the `data-*` half of `cellProps` — see its note. */}\n {mobilePrimaryCol && (\n <div {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))} className=\"font-medium\">\n {mobilePrimaryCol.cell(row)}\n </div>\n )}\n {mobileSecondaryColumns.length > 0 && (\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 {mobileSecondaryColumns.map((col) => (\n <Fragment key={col.key}>\n <dt className=\"text-xs uppercase tracking-wide text-[var(--text-muted)] self-center\">\n {col.header}\n </dt>\n <dd\n {...dataAttrsOf(col.cellProps?.(row))}\n className=\"min-w-0 text-[var(--text-secondary)] self-center\"\n >\n {col.cell(row)}\n </dd>\n </Fragment>\n ))}\n </dl>\n )}\n </>\n )}\n </div>\n {opensDetail &&\n // `aria-hidden`: the row already announces itself as a button with\n // `aria-expanded`, so the icon would only add a second, wordless stop.\n (mobileExpandAsDialog ? (\n // Points INTO the sheet, which is the reading-forward direction.\n <ChevronRight\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-placeholder)] rtl:-scale-x-100\"\n />\n ) : (\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-[var(--text-placeholder)] transition-transform\",\n expanded && \"rotate-180\",\n )}\n />\n ))}\n </>\n );\n const body =\n interactive && href ? (\n // A card with a URL of its own is a LINK, not a div wearing role=\"button\"\n // (feedback #451): the phone's long-press \"open in new tab\" and a screen\n // reader's link semantics both come free, and the fake role goes away.\n <RowLink\n href={href}\n onActivate={() => onRowClick!(row)}\n aria-expanded={expandedRow ? expanded : undefined}\n className={cardClass}\n // Enter already activates a link natively, and that routes through\n // RowLink's own onClick — handling it here too would open the row twice.\n // Space is the only key left, and only so it opens the row instead of\n // scrolling the list.\n onKeyDown={(e) => {\n if (e.key !== \" \") return;\n e.preventDefault();\n onRowClick!(row);\n }}\n >\n {cardInner}\n </RowLink>\n ) : (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are\n only attached when `interactive`, and then so are role=\"button\", tabIndex 0 and the\n Enter/Space handler below; the rule cannot follow the conditional role. */\n <div\n role={interactive ? \"button\" : undefined}\n tabIndex={interactive ? 0 : undefined}\n onClick={\n interactive\n ? (e) => {\n if (fromOutside(e.target, e.currentTarget)) return;\n onRowClick!(row);\n }\n : undefined\n }\n onKeyDown={\n interactive\n ? (e) => {\n // A key press inside a nested control is that control's.\n if (e.target !== e.currentTarget) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick!(row);\n }\n }\n : undefined\n }\n aria-expanded={expandedRow ? expanded : undefined}\n className={cardClass}\n >\n {cardInner}\n </div>\n );\n return (\n <li key={rowKey(row)} className={cn(tint)} {...cleanAttrs(rowAttributes?.(row))}>\n {swipeEnabled ? (\n <SwipeableRow enabled left={swipe!.left} right={swipe!.right}>\n {body}\n </SwipeableRow>\n ) : (\n body\n )}\n {expansion && !mobileExpandAsDialog && (\n <div\n className={cn(\n \"bg-[var(--bg-surface-2)] border-t border-[var(--border)]\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n )}\n >\n {expansion}\n </div>\n )}\n </li>\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 const mobileGroups: { key: string; rows: T[] }[] | null = mobileGroupBy\n ? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {\n const key = mobileGroupBy(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 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.\n fillHeight && \"flex flex-1 flex-col min-h-0\",\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-[var(--border)]\", 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 {(!isServer || onFiltersChange) && (\n <MobileFilters\n columns={columns}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n isServer={isServer}\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) => (!isServer || !!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 <ul\n className={cn(\n \"divide-y divide-[var(--border)]\",\n // Under `fillHeight` the list is the scroller, as the table body is on a\n // wide screen: the filter button stays above it and a server pager below.\n // The endless-scroll sentinel still fires in here — an\n // IntersectionObserver on the viewport sees through the clipping ancestor.\n fillHeight && \"min-h-0 flex-1 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 bg-[var(--bg-surface-2)] px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)] backdrop-blur\">\n {mobileGroupLabel ? mobileGroupLabel(g.key) : g.key}\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-[var(--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-[var(--text-placeholder)]\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\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-[var(--border-strong)] bg-[var(--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-[var(--text-muted)]\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-[var(--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 && (\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 && \"min-h-0 flex-1\")}>\n <div className={cn(\"relative min-w-0 flex-1\", fillHeight && \"flex flex-col min-h-0\")}>\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 // The scroller: marked for the Tooltip's auto-portal (see the root above).\n data-clips=\"\"\n className={cn(\"overflow-auto [scrollbar-gutter:stable]\", fillHeight && \"flex-1 min-h-0\")}\n style={fillHeight ? undefined : { maxHeight: maxBodyHeight }}\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 className={cn(\"w-full\", compact ? \"text-xs\" : \"text-sm\")}\n aria-label={labels.table}\n aria-busy={isLoading || undefined}\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 headClassName below). */}\n <thead className=\"bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-[var(--bg-surface-2)] align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || !!onSortsChange) && !!col.sortBy;\n const filter = !isServer || onFiltersChange ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-[var(--bg-surface-2)] backdrop-blur-sm\",\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-[var(--text-primary)]\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-[9px] font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n width={filter.type === \"date\" ? 420 : undefined}\n // The panel is the column's filter, and says so. Unnamed it\n // was announced as the kit-wide fallback, \"Popover\".\n labels={{ panel: labels.filter }}\n trigger={({ toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n aria-label={labels.filter}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-[var(--brand-bg)] text-[var(--brand-muted)]\"\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {resizable && col.key !== ROW_ACTIONS_KEY && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-[var(--brand-bg-hover)]\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody\n className={cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\")}\n >\n {leadingRow && (\n <tr className=\"border-t border-[var(--border)]\">\n <td colSpan={totalColSpan} className=\"p-0\">\n {leadingRow}\n </td>\n </tr>\n )}\n {slice.map((row, rowIndex) => {\n const expanded = isExpanded?.(row) ?? false;\n const expansion = expanded ? expandedRow?.(row) : null;\n const rowInteractive = !!onRowClick || !!selection;\n const href = rowHref?.(row);\n const key = rowKey(row);\n return (\n <Fragment key={key}>\n <tr\n {...cleanAttrs(rowAttributes?.(row))}\n data-row-nav={rowsFocusable ? \"\" : undefined}\n tabIndex={rowsFocusable ? (key === tabRowKey ? 0 : -1) : undefined}\n aria-expanded={rowsFocusable && expandedRow ? expanded : undefined}\n onFocus={\n rowsFocusable\n ? (e) => {\n if (e.target === e.currentTarget) setFocusRowKey(key);\n }\n : undefined\n }\n onKeyDown={\n rowsFocusable\n ? (e) => {\n // Only the row's own key presses: Enter on a nested button\n // or link is that control's, and Space in a nested input\n // is a space.\n if (e.target !== e.currentTarget) return;\n if (e.altKey || e.ctrlKey || e.metaKey) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n // Space would otherwise scroll the table.\n e.preventDefault();\n onRowClick!(row);\n return;\n }\n if (e.shiftKey) return;\n if (moveRowFocus(e.currentTarget, e.key)) e.preventDefault();\n }\n : undefined\n }\n className={cn(\n \"border-t border-[var(--border)]\",\n rowInteractive && \"cursor-pointer hover:bg-[var(--bg-hover)]\",\n rowsFocusable &&\n \"focus-visible:bg-[var(--bg-hover)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n rowClassName?.(row),\n )}\n onClick={\n rowInteractive\n ? (e) => {\n // A click on a control inside the row (a button, a link,\n // an input the cell renders) is that control's — it must\n // not also open the row. Cells no longer have to\n // `stopPropagation` to get that.\n if (fromOwnControl(e.target, e.currentTarget)) return;\n if (fromOutside(e.target, e.currentTarget)) return;\n // Modifier-clicks anywhere in the row drive\n // selection instead of expanding it (feedback #289):\n // Ctrl/Cmd toggles a single row, Shift selects the\n // range from the last-toggled row (inclusive of this\n // one) — the same as the checkbox, but for the whole\n // row, which is what the checkbox-only version was\n // missing.\n if (selection && canSelect(row)) {\n if (e.shiftKey) {\n // Drop the text selection a shift-click would\n // otherwise smear across the rows.\n window.getSelection?.()?.removeAllRanges();\n if (selectRange(rowIndex)) return;\n // No anchor yet: treat it as the first pick.\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, !selection.isSelected(row));\n return;\n }\n if (e.metaKey || e.ctrlKey) {\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, !selection.isSelected(row));\n return;\n }\n }\n onRowClick?.(row);\n }\n : undefined\n }\n >\n {selection && (\n <td\n className={cn(\"align-top\", compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\")}\n // Don't let selecting a row also trigger the row click\n // (which expands/edits it).\n onClick={(e) => e.stopPropagation()}\n >\n {canSelect(row) && (\n <input\n type=\"checkbox\"\n className={cn(\"accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.isSelected(row)}\n // Shift+click selects the range from the last toggled\n // row. `preventDefault` puts the box back, but it does NOT\n // stop React's `onChange`: React derives a checkbox's\n // change from the `click` itself, after the browser has\n // already flipped `checked` and before the cancellation\n // reverts it — so the same click also reported itself as\n // a single toggle, and the owner got `onToggleMany` and\n // then `onToggle` for the clicked row. The click is\n // remembered by identity and its change is dropped; a\n // boolean flag would outlive a click whose change never\n // came and swallow the user's next real one.\n onClick={(e) => {\n if (e.shiftKey && selectRange(rowIndex)) {\n e.preventDefault();\n rangeClick.current = e.nativeEvent;\n }\n }}\n onChange={(e) => {\n if (rangeClick.current && e.nativeEvent === rangeClick.current) {\n rangeClick.current = null;\n return;\n }\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, e.target.checked);\n }}\n aria-label={labels.selectRow}\n />\n )}\n </td>\n )}\n {visibleColumns.map((col) => {\n const width = widths[col.key];\n // The caller's first, the table's over it — see `cellProps`.\n const own = splitCellProps(col.cellProps?.(row));\n const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <td\n {...own.attrs}\n key={col.key}\n data-col={col.key}\n style={own.style || pinned ? { ...own.style, ...pinned } : undefined}\n className={cn(\n \"align-top\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n width && \"overflow-hidden text-ellipsis\",\n logicalAlign(own.className),\n logicalAlign(col.className),\n )}\n >\n {col === linkColumn && href ? (\n // No `onActivate`: the click is left to bubble to the\n // <tr> handler above, which already owns expand-vs-select.\n // Calling it here as well would toggle the row twice.\n <RowLink href={href} className=\"block\" data-row-link=\"\">\n {col.cell(row)}\n </RowLink>\n ) : (\n col.cell(row)\n )}\n </td>\n );\n })}\n </tr>\n {expansion && (\n <tr className=\"bg-[var(--bg-surface-2)] border-t border-[var(--border)]\">\n {/* The detail panel spans every column. In an `auto` table\n layout (the default here — columns only get fixed style\n widths once the user resizes/auto-sizes them) the cell's\n content preferred-width is redistributed across the\n spanned columns, so a long expansion visibly re-sizes the\n columns above it — and one-line content does not, since\n its preferred width is small (feedback #104). Wrapping the\n content in `w-0 min-w-full` pins the cell's preferred\n width to ~0 (the explicit width:0 child), so it can never\n perturb the columns; the div then fills to the full row\n width only at paint time, and long text wraps within it. */}\n <td colSpan={totalColSpan} className=\"p-0\">\n <div className={cn(\"w-0 min-w-full\", compact ? \"px-2 py-2\" : \"px-3 py-3\")}>\n {expansion}\n </div>\n </td>\n </tr>\n )}\n </Fragment>\n );\n })}\n {slice.length === 0 && (\n <tr>\n <td\n colSpan={totalColSpan}\n className={cn(\"text-center text-[var(--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 )}\n </tbody>\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-[var(--text-primary)]\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n <tr>\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 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 </div>\n {!unpaged && (\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 setPageSize(n);\n setPage(0);\n }\n }\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n </div>\n {columnSettings && (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-[var(--border)]\",\n \"hover:bg-[var(--bg-hover)]\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-[var(--text-muted)] group-hover:text-[var(--text-secondary)]\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-[var(--border)] p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => setShowSettings(false)}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-[var(--border)] px-2 py-1 text-xs font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (col.key === ROW_ACTIONS_KEY) return null;\n const checked = !hiddenCols.has(col.key);\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-[var(--bg-hover)]\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5\"\n checked={checked}\n onChange={(e) => {\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n <span className=\"truncate\">{headerLabel}</span>\n </label>\n );\n })}\n </div>\n </div>\n </div>\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\n// ---------- Mobile filter sheet ----------\n\n/**\n * Filter access for the mobile card list (feedback #299). The desktop table\n * houses per-column filters in its header; the card list has no header, so the\n * same filters were unreachable on phones. This renders a compact \"Filters\" bar\n * that opens a bottom sheet listing each filterable column as a collapsible\n * section, reusing the very same {@link FilterPopover} and filter state the\n * desktop table drives — so it filters identically and stays URL-synced.\n */\nfunction MobileFilters<T>({\n columns,\n filters,\n selectOptionsByKey,\n isServer,\n hasFilterCallback,\n onSetFilter,\n onClearFilter,\n onClearAll,\n labels,\n locale,\n}: {\n columns: DataTableColumn<T>[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n isServer: boolean;\n hasFilterCallback: boolean;\n onSetFilter: (key: string, value: FilterValue) => void;\n onClearFilter: (key: string) => void;\n onClearAll: () => void;\n labels: DataTableLabels;\n locale?: string;\n}) {\n const [open, setOpen] = useState(false);\n // The sheet is portalled to <body>, out from under whatever `dir` the table sits\n // in, so it carries the trigger's direction with it — read at the moment of opening,\n // from the element the user actually pressed.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const [expanded, setExpanded] = useState<string | null>(null);\n const backdropClose = useBackdropClose(() => setOpen(false));\n useBodyScrollLock(open);\n // Back dismisses the filter sheet too (Keksdose feedback #172) — it is the same\n // full-screen surface, and the filters it sets are already in the URL, so closing\n // it never loses anything.\n useOverlayHistory(open, () => setOpen(false));\n\n const filterable = useMemo(\n () =>\n columns\n .map((col) => ({ col, filter: !isServer || hasFilterCallback ? resolveFilter(col) : null }))\n .filter((x): x is { col: DataTableColumn<T>; filter: ColumnFilter<T> } => !!x.filter),\n [columns, isServer, hasFilterCallback],\n );\n\n if (filterable.length === 0) return null;\n\n const activeCount = filterable.filter(({ col }) => {\n const s = filters[col.key];\n return s && isFilterActive(s);\n }).length;\n\n return (\n <div className=\"flex items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n setDir(dirOf(e.currentTarget));\n setOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\"\n >\n <Filter className=\"size-4\" />\n <span>{labels.filters}</span>\n {activeCount > 0 && (\n <span className=\"inline-flex min-w-4 items-center justify-center rounded-full bg-[var(--info-bg)] px-1 text-[11px] font-semibold text-[var(--info)]\">\n {activeCount}\n </span>\n )}\n </button>\n {activeCount > 0 && (\n <button\n type=\"button\"\n onClick={onClearAll}\n className=\"text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)]\"\n >\n {labels.clearAll}\n </button>\n )}\n {open &&\n createPortal(\n <div\n className=\"fixed inset-0 z-50 flex items-end justify-center bg-black/40\"\n role=\"dialog\"\n aria-modal=\"true\"\n dir={dir}\n {...backdropClose}\n >\n <div className=\"flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-2xl bg-[var(--bg-surface)] shadow-xl\">\n <div className=\"flex items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-3\">\n <div className=\"font-medium\">{labels.filters}</div>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n aria-label={labels.close}\n className=\"-me-1 rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <div className=\"flex-1 divide-y divide-[var(--border)] overflow-y-auto overscroll-contain\">\n {filterable.map(({ col, filter }) => {\n const state = filters[col.key];\n const active = !!state && isFilterActive(state);\n const isItemOpen = expanded === col.key;\n return (\n <div key={col.key}>\n <button\n type=\"button\"\n onClick={() => setExpanded(isItemOpen ? null : col.key)}\n aria-expanded={isItemOpen}\n className=\"flex w-full items-center justify-between gap-2 px-4 py-3 text-start\"\n >\n <span className=\"flex items-center gap-2 text-sm font-medium text-[var(--text-secondary)]\">\n {col.header}\n {active && <span className=\"size-1.5 rounded-full bg-brand\" />}\n </span>\n <ChevronDown\n className={cn(\n \"size-4 shrink-0 text-[var(--text-placeholder)] transition-transform\",\n isItemOpen && \"rotate-180\",\n )}\n />\n </button>\n {isItemOpen && (\n <div className=\"px-4 pb-3\">\n <FilterPopover\n column={col}\n state={state ?? defaultFilterState(filter)}\n onChange={(next) => onSetFilter(col.key, next)}\n onClear={() => onClearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"flex items-center justify-between gap-2 border-t border-[var(--border)] px-4 py-3\">\n <button\n type=\"button\"\n onClick={onClearAll}\n disabled={activeCount === 0}\n className=\"text-sm text-[var(--text-muted)] hover:text-[var(--text-secondary)] disabled:opacity-40\"\n >\n {labels.clearAll}\n </button>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"rounded-md bg-[var(--bg-inverse)] px-4 py-2 text-sm font-medium text-[var(--text-inverse)]\"\n >\n {labels.done}\n </button>\n </div>\n </div>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment"]}
1
+ {"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n ArrowDown,\n ArrowDownWideNarrow,\n ArrowUp,\n ArrowUpDown,\n ArrowUpNarrowWide,\n ChevronDown,\n ChevronRight,\n Filter,\n Pencil,\n Trash2,\n X,\n} from \"lucide-react\";\nimport { Card, IconButton, Select, Spinner } from \"./ui\";\nimport { cn } from \"../lib/cn\";\nimport {\n defaultFilterState,\n isFilterActive,\n resolveFilter,\n rowMatches,\n} 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 { FilterPopover } from \"./data-table-filter-popover\";\nimport { Pagination } from \"./data-table-pagination\";\nimport { SwipeableRow, type SwipeAction } from \"./swipeable-row\";\nimport { useBackdropClose } from \"./modal\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { Popover } from \"./popover\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale } from \"../i18n/kit-labels\";\nimport { Tooltip } from \"./tooltip\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { dirOf, isRtl, 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\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\n/**\n * Split a caller's cell attributes into what may reach the element and what the kit\n * owns. `role` and `data-col` are dropped outright (see `cellProps`); `className` and\n * `style` come back separately so the kit can merge rather than replace them.\n */\nfunction splitCellProps(\n props: DataTableCellProps | DataTableHeadProps | undefined,\n): { attrs: Record<string, unknown>; className?: string; style?: CSSProperties } {\n if (!props) return { attrs: {} };\n const {\n className,\n style,\n role: _role,\n \"data-col\": _col,\n ...attrs\n } = props as DataTableCellProps & { \"data-col\"?: unknown };\n return { attrs, className, style };\n}\n\n/** Only the `data-*` attributes of a cell's props — what a phone card field takes. */\nfunction dataAttrsOf(props: DataTableCellProps | undefined): Record<string, unknown> | undefined {\n if (!props) return undefined;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(props)) if (k.startsWith(\"data-\") && k !== \"data-col\") out[k] = v;\n return out;\n}\n\n/** A column resize in flight. Held in a ref rather than in state: the live width is\n * written straight onto the column's cells, so a drag costs zero renders and the\n * one `setWidths` happens when the pointer comes up. */\ninterface Resize {\n key: string;\n pointerId: number;\n startX: number;\n startWidth: number;\n /** +1 when a drag to the right widens the column, -1 in a right-to-left table,\n * where the handle sits on the column's LEFT edge and widening is a drag left. */\n sign: 1 | -1;\n /** What React last rendered this column at, so a CANCELLED drag can put the header\n * back — React cannot, it never saw the inline width the drag wrote. */\n hadWidth: number | undefined;\n /** The header and the body cells under it, collected ONCE when the drag starts.\n * A `<th>` alone cannot shrink a column whose `<td>`s already carry a pinned\n * width — and nothing re-renders during a drag, so the list cannot go stale. */\n cells: HTMLElement[];\n width: number;\n /** A press with no drag in it is not a resize — the handle is also the\n * double-click target that CLEARS a width, and committing the header's current\n * size on the way past would pin every column the user ever brushed. */\n moved: boolean;\n}\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 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. On a\n * phone the card list is the scroller instead, with the filter button pinned above\n * it; an unbounded parent simply lets either layout grow to its content. The parent must give it a bounded height (e.g. a flex\n * column inside the viewport-locked app shell). See feedback #207.\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 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 * 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 * Opt-in multi-row selection (desktop table only). 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 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?: Partial<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. Defaults to\n * \"hbui-table:\"; pass your app's own prefix to keep a stable namespace. */\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 * 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;\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 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.\n */\n mobileSwipeActions?: (row: T) => MobileSwipeActions | null;\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 * 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). 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 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 `missingDataTableLabels`), 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/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nconst ROW_ACTIONS_KEY = \"__rowActions\";\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\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/** The logical sides onto the physical drag directions {@link SwipeableRow} takes. */\nfunction physicalSwipe(\n actions: MobileSwipeActions,\n dir: Direction,\n): { left?: SwipeAction[]; right?: SwipeAction[] } {\n const rtl = dir === \"rtl\";\n return {\n left: (rtl ? actions.end : actions.start) ?? actions.left,\n right: (rtl ? actions.start : actions.end) ?? actions.right,\n };\n}\n\n// A bare `text-right` / `text-left` token, with or without variant prefixes\n// (`md:text-right`). Deliberately NOT `text-right-…` or an arbitrary value.\nconst PHYSICAL_ALIGN = /(^|\\s)((?:[\\w-]+:)*)text-(left|right)(?=\\s|$)/g;\n\n/**\n * A column's `className`/`headClassName` with physical text alignment rewritten to\n * logical: `text-right` → `text-end`, `text-left` → `text-start`.\n *\n * Every numeric column written before 0.7.0 says `text-right`, and that is what it\n * MEANT: the end of the line, where digits line up. Rendered as written, a\n * right-to-left table puts those numbers on the far side from their own header's\n * sort button. Rewriting keeps each existing call site correct in both directions\n * with no change on its side; a caller who genuinely wants the physical side in RTL\n * too can say `ltr:text-right rtl:text-right`, which this leaves alone.\n */\nfunction logicalAlign(className: string | undefined): string | undefined {\n return className?.replace(\n PHYSICAL_ALIGN,\n (token: string, lead: string, variants: string, side: string) =>\n // Already scoped to one direction: that is a caller choosing the physical side.\n /(^|:)(ltr|rtl):/.test(variants)\n ? token\n : `${lead}${variants}text-${side === \"right\" ? \"end\" : \"start\"}`,\n );\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-[var(--bg-surface-2)] shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** Drop the undefined entries of a {@link DataTableProps.rowAttributes} map, so a\n * caller can write `{ \"data-x\": cond ? \"y\" : undefined }` and get no attribute\n * at all rather than the string \"undefined\" in the DOM. */\nfunction cleanAttrs(\n attrs: Record<string, string | undefined> | undefined,\n): Record<string, string> | undefined {\n if (!attrs) return undefined;\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(attrs)) if (v !== undefined) out[k] = v;\n return out;\n}\n\n/**\n * A column's name as a plain string.\n *\n * `header` is a ReactNode — an icon, a unit suffix, a tooltip-wrapped span — and\n * neither a live region nor the column-settings checklist can speak a React element.\n * The `key` fallback is the one the settings panel has always used; a column whose\n * header is not a string and whose key is not human-readable is a call site that\n * should pass a string header, and that is visible in what gets announced.\n */\nfunction columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {\n if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;\n return col.headerText ?? (typeof col.header === \"string\" ? col.header : col.key);\n}\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, beside the control, 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/**\n * The card list's sort control: a \"Sort by\" select and a direction button.\n *\n * keksdose K10 (inputs audit 2026-10-02 §5; its aggregated report tables, #258): a\n * table is sorted by its header row and the phone card list has none, so the two\n * decisions the header carries — which column, which direction — were unreachable\n * below the breakpoint. keksdose hand-built this row over its DataTable to answer\n * \"who did I pay the most\" on the device it was asked on; this is that row, driven by\n * the SAME sort state as the headers (controlled `sorts` included), so turning the\n * phone sideways keeps the order and a sort chosen here is the one the URL and the\n * storage key hold.\n *\n * The rules are the header's: a newly picked column starts in its `firstSort`, the\n * direction button flips the primary sort (secondary ones stay), and \"Default order\"\n * — the third click of a `\"tri\"` cycle — is offered only under `\"tri\"`, since a\n * `\"toggle\"` table promises it is always ranked by something. The select's label is\n * a real `<label>` beside it, and the button sits outside that label: a click inside\n * a label is forwarded to the control it names, so a nested button would flip AND\n * open the select.\n */\nfunction MobileSort<T>({\n columns,\n sorts,\n sortCycle,\n compact,\n labels,\n onPick,\n onFlip,\n}: {\n columns: DataTableColumn<T>[];\n sorts: SortState[];\n sortCycle: SortCycle;\n compact: boolean;\n labels: DataTableLabels;\n onPick: (key: string | null) => void;\n onFlip: () => void;\n}) {\n const selectId = useId();\n const sortLabels: DataTableSortLabels = labels;\n if (columns.length === 0) return null;\n // A persisted or URL sort can name a column with no `sortBy` (or a hidden one): the\n // sort chain drops it, and so does the control rather than claiming it.\n const primary = sorts[0] && columns.some((c) => c.key === sorts[0].key) ? sorts[0] : null;\n const offerDefault = sortCycle === \"tri\" || primary === null;\n const desc = primary?.dir === \"desc\";\n return (\n <div\n data-table-mobile-sort=\"\"\n className={cn(\n \"flex items-center gap-2 border-b border-[var(--border)]\",\n compact ? \"px-3 py-1.5\" : \"px-4 py-2\",\n )}\n >\n <label htmlFor={selectId} className=\"shrink-0 text-xs text-[var(--text-muted)]\">\n {sortLabels.sortBy}\n </label>\n <Select\n id={selectId}\n size={compact ? \"sm\" : \"md\"}\n className=\"min-w-0 flex-1\"\n value={primary?.key ?? \"\"}\n onChange={(e) => onPick(e.target.value || null)}\n >\n {offerDefault && <option value=\"\">{sortLabels.sortDefault}</option>}\n {columns.map((col) => (\n <option key={col.key} value={col.key}>\n {columnLabel(col, labels)}\n </option>\n ))}\n </Select>\n <IconButton\n size={compact ? \"xs\" : \"md\"}\n variant=\"secondary\"\n label={desc ? sortLabels.sortDescending : sortLabels.sortAscending}\n disabled={primary === null}\n onClick={onFlip}\n >\n {desc ? <ArrowDownWideNarrow /> : <ArrowUpNarrowWide />}\n </IconButton>\n </div>\n );\n}\n\n/**\n * What counts as a control of its own inside a clickable row: a click or key press\n * that lands on one of these belongs to it, not to the row's `onRowClick`. The row's\n * own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.\n */\nconst OWN_CONTROL =\n 'a[href],button,input,select,textarea,label,summary,[role=\"button\"],[role=\"link\"],[role=\"checkbox\"],[role=\"switch\"],[role=\"menuitem\"],[role=\"option\"],[contenteditable=\"\"],[contenteditable=\"true\"],[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Whether a click that reached `row`'s handler did not start in the row's own DOM.\n *\n * React bubbles an event through the COMPONENT tree, portals included: a popover, a\n * menu or a dialog a cell (or the expansion under it) opens renders at `<body>` but\n * still reports its clicks to every React ancestor — the row among them. So a click\n * in a cell's portalled date picker opened the row it came from, and a page wrapped\n * such content in `<div onClick={(e) => e.stopPropagation()}>` to stop it (kastlan's\n * feedback page did, round its comments). The row owns the clicks on the row.\n */\nfunction fromOutside(target: EventTarget | null, row: HTMLElement): boolean {\n return !(target instanceof Node) || !row.contains(target);\n}\n\n/** Whether an event on `row` started on a control nested inside it. */\nfunction fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {\n if (!(target instanceof Element)) return false;\n const hit = target.closest(OWN_CONTROL);\n return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute(\"data-row-link\");\n}\n\n/** Arrow/Home/End between the focusable rows of one `<tbody>` — the roving half of\n * the keyboard-activatable rows (see the row's `onKeyDown`). */\nfunction moveRowFocus(from: HTMLElement, key: string): boolean {\n if (!(key in ROW_NAV_KEYS)) return false;\n const body = from.parentElement;\n if (!body) return false;\n const rows = Array.from(body.children).filter(\n (el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute(\"data-row-nav\"),\n );\n const i = rows.indexOf(from);\n const target =\n key === \"ArrowDown\"\n ? rows[i + 1]\n : key === \"ArrowUp\"\n ? rows[i - 1]\n : key === \"Home\"\n ? rows[0]\n : key === \"End\"\n ? rows[rows.length - 1]\n : undefined;\n // At either end the key is still consumed, or ArrowDown on the last row would\n // scroll the page out from under a focused row.\n target?.focus();\n return true;\n}\nconst ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;\n\n/** A click the APP owns. Anything else — middle, ⌘/Ctrl, Shift, Alt — belongs to\n * the browser, and the whole feature is not touching it. */\nconst isPlainLeftClick = (e: React.MouseEvent) =>\n e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;\n\n/**\n * The anchor a linkable row is opened through (Keksdose feedback #451).\n *\n * The plain left click is OURS: cancelled, then handed to the row's own handler, so\n * clicking a row still expands it in place instead of reloading the page. Every\n * other click — middle, ⌘/Ctrl, Shift, Alt — is left completely untouched, which is\n * the entire point of using an anchor rather than an `onAuxClick` + `window.open`:\n * that imitation buys back the middle click and still loses ⌘/Ctrl-click, \"copy link\n * address\", the status-bar preview and the phone's long-press menu.\n *\n * Modified clicks DO stop propagating: on a table with `selection`, ⌘/Ctrl-click and\n * Shift-click on a row mean \"select\" (feedback #289), and a row must not change its\n * selection while the browser is opening a tab. The rest of the row still selects —\n * the link is one cell, not the row.\n *\n * `draggable={false}` because an anchor otherwise hijacks a horizontal mouse drag as\n * a link-drag: that is both drag-to-select-text on the desktop table and the mouse\n * path through SwipeableRow's gesture on the mobile card.\n */\nfunction RowLink({\n href,\n onActivate,\n className,\n children,\n ...rest\n}: {\n href: string;\n /** Runs on a plain left click. Omit it where an ANCESTOR already handles the\n * click (the desktop `<tr>`), or the row would toggle twice. */\n onActivate?: () => void;\n className?: string;\n children: ReactNode;\n} & Omit<\n React.AnchorHTMLAttributes<HTMLAnchorElement>,\n \"href\" | \"className\" | \"children\" | \"onClick\" | \"onClickCapture\"\n>) {\n return (\n <a\n {...rest}\n href={href}\n draggable={false}\n className={className}\n // CAPTURE phase, and it has to be: on the mobile card the whole card is the\n // anchor, so a nested control sits INSIDE it — and the ones that matter\n // (the feedback row's status toggles) stop the click propagating so the row\n // won't also expand. A bubble-phase handler would never run for those, and\n // the browser would follow the link out from under the button press. Cancelling\n // here happens before any descendant can silence the event; `stopPropagation`\n // does not undo a `preventDefault`.\n onClickCapture={(e) => {\n // A click in a portal a descendant opened is not a click on the link: leave\n // it (and a link inside it) alone. See `fromOutside`.\n if (fromOutside(e.target, e.currentTarget)) return;\n if (isPlainLeftClick(e)) e.preventDefault();\n }}\n onClick={(e) => {\n if (fromOutside(e.target, e.currentTarget)) return;\n if (!isPlainLeftClick(e)) {\n e.stopPropagation();\n return;\n }\n // Already cancelled above; this phase only decides what opens. A nested\n // control that stopped propagation deliberately never reaches it, which is\n // exactly how the row behaved before it became a link.\n onActivate?.();\n }}\n >\n {children}\n </a>\n );\n}\n\n// ---------- Main DataTable ----------\n\n\n\nimport { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from \"./use-table-state\";\nimport { useMobileReveal } from \"./use-mobile-reveal\";\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 filters: filtersProp,\n onFiltersChange,\n sorts: sortsProp,\n onSortsChange,\n selection,\n labels: labelsProp,\n locale: localeProp,\n storageKeyPrefix = DEFAULT_PERSIST_PREFIX,\n mobileExpandAsDialog = false,\n mobileDialogBackCloses = true,\n mobileGroupBy,\n mobileGroupLabel,\n mobileCard,\n mobileSort = false,\n mobileSwipeActions,\n leadingRow,\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 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 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 const labels = useMemo(\n () =>\n resolveDataTableLabels(\n labelOverrides || labelsProp\n ? {\n ...labelOverrides,\n ...labelsProp,\n presets: { ...labelOverrides?.presets, ...labelsProp?.presets },\n }\n : undefined,\n ),\n [labelOverrides, labelsProp],\n );\n const locale = useKitLocale(localeProp);\n\n // ---- Row actions ----\n // The actions column is appended to the caller's columns rather than drawn as a\n // second special case beside the selection column: it then gets the cell padding,\n // the widths and the phone rules (`mobileHidden`) every other column gets.\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n const columns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: <span className=\"sr-only\">{labels.actions}</span>,\n cell: (row) => (\n <div className=\"flex items-center justify-end gap-0.5\">\n {rowActionsFor(row).map((a) => (\n <IconButton\n key={a.key}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n label={a.label}\n tone={a.tone === \"danger\" ? \"danger\" : undefined}\n disabledReason={a.action.disabledReason?.(row)}\n // The row's click handler already skips its own controls; this is for\n // the caller who wraps the table in a clickable element of their own.\n stopPropagation\n onClick={() => void runRowAction(a.action, row)}\n >\n {a.icon}\n </IconButton>\n ))}\n </div>\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsFor, labels.actions, density],\n );\n const swipesFromActions = (row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n className: a.tone === \"danger\" ? \"bg-[var(--danger)]\" : \"bg-[var(--brand)]\",\n armedClassName: a.tone === \"danger\" ? \"bg-[var(--danger-hover)]\" : \"bg-[var(--brand-hover)]\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\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 });\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 const widths = useMemo(() => (resizable ? storedWidths : {}), [resizable, storedWidths]);\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 (isServer) 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, isServer, locale]);\n\n const sorted = useMemo(() => {\n if (isServer) 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, isServer]);\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;\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) => ((!isServer || 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 (isServer) return;\n announce(labels.filterResults(filtered.length, rows.length));\n }, [filterKey, filtered.length, rows.length, isServer, 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 const visibleColumns = useMemo(\n () => columns.filter((c) => !hiddenCols.has(c.key)),\n [columns, hiddenCols],\n );\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 ----\n //\n // Pointer events with capture, rather than a `mousemove`/`mouseup` pair on `window`.\n // Three defects in one eleven-line handler (audit 2026-09-22):\n //\n // * a `setWidths` per `mousemove` re-rendered every row of the table on every pixel\n // of the drag — on a 500-row table that is the whole body, sixty times a second;\n // * the two `window` listeners were added from inside an event handler, so nothing\n // removed them if the table unmounted mid-drag (the row beneath opens a route),\n // and `document.body.style` stayed at `user-select: none; cursor: col-resize` for\n // the rest of the session — the whole page unselectable, with the wrong cursor;\n // * `mousedown` is a mouse. Under a finger the handle did nothing at all.\n //\n // `setPointerCapture` answers all three: the handle itself receives every move and\n // the terminating `pointerup`/`pointercancel` wherever the pointer travels, so there\n // is nothing global to leak, and a touch drag works because a finger is a pointer.\n // The live width is written straight onto the column's own cells — the same three\n // properties the render sets, so the drag and the commit cannot disagree — and\n // `setWidths` is called once, when the drag ends.\n const resizing = useRef<Resize | null>(null);\n\n /** Every cell in one column, header first, totals row included. `data-col` rather than a cell index:\n * an expanded row holds a single `colSpan` cell, so the nth `<td>` of a row is\n * not reliably the nth column. */\n const columnCells = (key: string): HTMLElement[] => {\n const th = headRefs.get(key);\n if (!th) return [];\n const table = th.closest(\"table\");\n if (!table) return [th];\n return [\n th,\n ...Array.from(table.querySelectorAll<HTMLElement>(\"tbody [data-col], tfoot [data-col]\")).filter(\n (el) => el.dataset.col === key,\n ),\n ];\n };\n\n const applyLiveWidth = (cells: HTMLElement[], width: number | undefined) => {\n const px = width == null ? \"\" : `${width}px`;\n for (const el of cells) {\n el.style.width = px;\n el.style.minWidth = px;\n el.style.maxWidth = px;\n }\n };\n\n const releaseResizeChrome = (handle: Element, pointerId: number) => {\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n // Optional calls: jsdom implements neither, and the handlers sit on the handle\n // itself, so a run without capture behaves the same for anything that stays\n // inside the element.\n if (handle.hasPointerCapture?.(pointerId)) handle.releasePointerCapture?.(pointerId);\n };\n\n const startResize = (e: React.PointerEvent<HTMLElement>, key: string) => {\n // Secondary buttons open menus; only a primary press starts a drag.\n if (e.button !== 0) return;\n e.preventDefault();\n e.stopPropagation();\n const th = headRefs.get(key);\n const startWidth = th?.getBoundingClientRect().width ?? 100;\n resizing.current = {\n key,\n pointerId: e.pointerId,\n startX: e.clientX,\n startWidth,\n sign: isRtl(e.currentTarget) ? -1 : 1,\n hadWidth: widths[key],\n cells: columnCells(key),\n width: startWidth,\n moved: false,\n };\n e.currentTarget.setPointerCapture?.(e.pointerId);\n document.body.style.userSelect = \"none\";\n document.body.style.cursor = \"col-resize\";\n };\n\n const moveResize = (e: React.PointerEvent<HTMLElement>) => {\n const drag = resizing.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n drag.width = Math.max(40, Math.round(drag.startWidth + drag.sign * (e.clientX - drag.startX)));\n drag.moved = true;\n applyLiveWidth(drag.cells, drag.width);\n };\n\n const finishResize = (e: React.PointerEvent<HTMLElement>, commit: boolean) => {\n const drag = resizing.current;\n // Before the early return: the page gets its selection and its cursor back even\n // for a pointer we were not tracking.\n releaseResizeChrome(e.currentTarget, e.pointerId);\n if (!drag || drag.pointerId !== e.pointerId) return;\n resizing.current = null;\n if (commit && drag.moved) {\n setWidths((w) => ({ ...w, [drag.key]: drag.width }));\n return;\n }\n // Cancelled — the browser took the pointer for a system gesture — or the press\n // never became a drag. Either way the column goes back to the width React last\n // rendered it at, which nothing else can do: the inline width the drag wrote is\n // invisible to React, so a re-render with an unchanged `widths` would keep it.\n applyLiveWidth(drag.cells, drag.hadWidth);\n };\n\n // The last exit path. A table that unmounts mid-drag takes the handle, and with it\n // every handler above, out of the document — this is the only place left to hand the\n // page back its text selection and its cursor.\n useEffect(\n () => () => {\n if (!resizing.current) return;\n resizing.current = null;\n document.body.style.userSelect = \"\";\n document.body.style.cursor = \"\";\n },\n [],\n );\n\n const autoSizeAll = () => {\n // Clear widths so the table reflows to natural sizes, then snapshot each\n // column's width and write it back — except for the last visible column,\n // which is left unconstrained so it absorbs any remaining horizontal space.\n setWidths({});\n requestAnimationFrame(() => {\n const next: Record<string, number> = {};\n const cols = columns.filter((c) => !hiddenCols.has(c.key));\n cols.forEach((col, idx) => {\n if (idx === cols.length - 1) return;\n const th = headRefs.get(col.key);\n if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);\n });\n setWidths(next);\n });\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 && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const columnsCountLabel = rowActions\n ? labels.columnsCount(visibleCount - 1, totalCount - 1)\n : labels.columnsCount(visibleCount, totalCount);\n // Match Tailwind's `md` breakpoint: we render either the table or the card\n // list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate.\n const isMdUp = useMediaQuery(\"(min-width: 768px)\", true);\n // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.\n const desktopScroller = useRef<HTMLDivElement | null>(null);\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 const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);\n const mobilePrimaryCol =\n mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;\n const mobileSecondaryColumns = mobileColumns.filter((c) => c !== mobilePrimaryCol);\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 const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // The row whose selection was last toggled, so Shift+click can select the range up\n // to it (feedback #289). Held as its KEY, not as an index into `slice`: the slice is\n // re-derived from the current sort, filter and page, so an index taken before a\n // header was clicked addresses a different row afterwards — and the range then ran\n // from a row the user had never touched.\n const selectionAnchor = useRef<string | number | null>(null);\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 /** The checkbox click that drew a range, so its `onChange` can be told apart. */\n const rangeClick = useRef<Event | null>(null);\n const canSelect = (row: T) => !!selection && (selection.isSelectable?.(row) ?? true);\n const selectRange = (toIndex: number) => {\n if (!selection || selectionAnchor.current === null) return false;\n const fromIndex = slice.findIndex((r) => rowKey(r) === selectionAnchor.current);\n // The anchor has been filtered, sorted or paged off the screen: there is no range\n // on display to draw, so the click falls through to the plain single-row toggle.\n if (fromIndex < 0) return false;\n const [lo, hi] = [fromIndex, toIndex].sort((a, b) => a - b);\n const range: T[] = [];\n for (let i = lo; i <= hi; i++) {\n const r = slice[i];\n if (r && canSelect(r)) range.push(r);\n }\n // One call where the owner can take one. See `selection.onToggleMany`: the row-by-\n // row loop below is the fallback for an owner that has not adopted it, not the\n // shape this should be in.\n if (selection.onToggleMany) selection.onToggleMany(range, true);\n else for (const r of range) selection.onToggle(r, true);\n return true;\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 ? expandedRow?.(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. Extracted so the flat list and the grouped list (below)\n // share identical row markup.\n const renderMobileRow = (row: T) => {\n const expanded = isExpanded?.(row) ?? false;\n const expansion = expanded ? expandedRow?.(row) : null;\n const tint = rowClassName?.(row);\n // Use div+role=\"button\" rather than a real <button> so cells that\n // contain their own interactive controls (status toggles, action\n // icons) don't end up as illegal nested buttons.\n const interactive = !!onRowClick;\n // Say that the card OPENS something (Keksdose feedback #163). A phone card had\n // only a cursor and a tap-tint to advertise its editor — on the invoice review\n // screen, whose entire purpose is correcting mis-read lines, that read as \"these\n // values are not editable\". A chevron is the affordance every list on a phone\n // uses for exactly this, and it points the way the row actually opens: right into\n // a sheet, or down into an inline panel that flips it when expanded.\n const opensDetail = interactive && !!expandedRow;\n const swipeActions = swipeActionsFor?.(row);\n const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;\n // An expanded row never swipes: the editor below it owns the horizontal space,\n // and dragging the header away from its own form reads as a glitch.\n const swipeEnabled = !!swipe && !expanded;\n const href = mobileCardLinkable ? rowHref?.(row) : undefined;\n const cardClass = cn(\n \"w-full text-start flex items-center\",\n compact ? \"px-3 py-2 gap-2 text-sm\" : \"px-4 py-3 gap-3\",\n // Live #320's other half: the card acknowledges the touch before the sheet\n // arrives. On a cold route the data can take a beat, and an unacknowledged tap\n // reads as \"did that register?\" — which is most of what \"abrupt\" means here.\n // `origin-center` + a 0.5% squeeze is deliberately almost subliminal: this fires\n // on every row of a long list, so anything larger becomes the list's personality\n // rather than feedback. `transition-transform` alone, so the existing background\n // flip stays instant.\n interactive &&\n \"cursor-pointer transition-transform duration-100 active:scale-[0.995] active:bg-[var(--bg-active)] motion-reduce:transition-none motion-reduce:active:scale-100\",\n );\n // The card's content is written once and worn by either tag below. Note that\n // the primary cell is rendered RAW here, never through `linkColumn` — the card\n // itself is the anchor, and routing it through `linkColumn` too would nest an\n // <a> inside an <a>. Desktop and mobile are mutually exclusive (`isMdUp`), so\n // exactly one anchor exists per row.\n const cardInner = (\n <>\n {/* The card's own content keeps the column it always had; the chevron sits\n beside it rather than inside, so a caller's `mobileCard` is untouched. */}\n <div className={cn(\"flex min-w-0 flex-1 flex-col\", compact ? \"gap-1\" : \"gap-2\")}>\n {mobileCard ? (\n mobileCard(row)\n ) : (\n <>\n {/* Only the `data-*` half of `cellProps` — see its note. */}\n {mobilePrimaryCol && (\n <div {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))} className=\"font-medium\">\n {mobilePrimaryCol.cell(row)}\n </div>\n )}\n {mobileSecondaryColumns.length > 0 && (\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 {mobileSecondaryColumns.map((col) => (\n <Fragment key={col.key}>\n <dt className=\"text-xs uppercase tracking-wide text-[var(--text-muted)] self-center\">\n {col.header}\n </dt>\n <dd\n {...dataAttrsOf(col.cellProps?.(row))}\n className=\"min-w-0 text-[var(--text-secondary)] self-center\"\n >\n {col.cell(row)}\n </dd>\n </Fragment>\n ))}\n </dl>\n )}\n </>\n )}\n </div>\n {opensDetail &&\n // `aria-hidden`: the row already announces itself as a button with\n // `aria-expanded`, so the icon would only add a second, wordless stop.\n (mobileExpandAsDialog ? (\n // Points INTO the sheet, which is the reading-forward direction.\n <ChevronRight\n aria-hidden\n className=\"size-4 shrink-0 text-[var(--text-placeholder)] rtl:-scale-x-100\"\n />\n ) : (\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-[var(--text-placeholder)] transition-transform\",\n expanded && \"rotate-180\",\n )}\n />\n ))}\n </>\n );\n const body =\n interactive && href ? (\n // A card with a URL of its own is a LINK, not a div wearing role=\"button\"\n // (feedback #451): the phone's long-press \"open in new tab\" and a screen\n // reader's link semantics both come free, and the fake role goes away.\n <RowLink\n href={href}\n onActivate={() => onRowClick!(row)}\n aria-expanded={expandedRow ? expanded : undefined}\n className={cardClass}\n // Enter already activates a link natively, and that routes through\n // RowLink's own onClick — handling it here too would open the row twice.\n // Space is the only key left, and only so it opens the row instead of\n // scrolling the list.\n onKeyDown={(e) => {\n if (e.key !== \" \") return;\n e.preventDefault();\n onRowClick!(row);\n }}\n >\n {cardInner}\n </RowLink>\n ) : (\n /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are\n only attached when `interactive`, and then so are role=\"button\", tabIndex 0 and the\n Enter/Space handler below; the rule cannot follow the conditional role. */\n <div\n role={interactive ? \"button\" : undefined}\n tabIndex={interactive ? 0 : undefined}\n onClick={\n interactive\n ? (e) => {\n if (fromOutside(e.target, e.currentTarget)) return;\n onRowClick!(row);\n }\n : undefined\n }\n onKeyDown={\n interactive\n ? (e) => {\n // A key press inside a nested control is that control's.\n if (e.target !== e.currentTarget) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick!(row);\n }\n }\n : undefined\n }\n aria-expanded={expandedRow ? expanded : undefined}\n className={cardClass}\n >\n {cardInner}\n </div>\n );\n return (\n <li key={rowKey(row)} className={cn(tint)} {...cleanAttrs(rowAttributes?.(row))}>\n {swipeEnabled ? (\n <SwipeableRow enabled left={swipe!.left} right={swipe!.right}>\n {body}\n </SwipeableRow>\n ) : (\n body\n )}\n {expansion && !mobileExpandAsDialog && (\n <div\n className={cn(\n \"bg-[var(--bg-surface-2)] border-t border-[var(--border)]\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n )}\n >\n {expansion}\n </div>\n )}\n </li>\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 const mobileGroups: { key: string; rows: T[] }[] | null = mobileGroupBy\n ? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {\n const key = mobileGroupBy(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 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.\n fillHeight && \"flex flex-1 flex-col min-h-0\",\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-[var(--border)]\", 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 {(!isServer || onFiltersChange) && (\n <MobileFilters\n columns={columns}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n isServer={isServer}\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) => (!isServer || !!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 <ul\n className={cn(\n \"divide-y divide-[var(--border)]\",\n // Under `fillHeight` the list is the scroller, as the table body is on a\n // wide screen: the filter button stays above it and a server pager below.\n // The endless-scroll sentinel still fires in here — an\n // IntersectionObserver on the viewport sees through the clipping ancestor.\n fillHeight && \"min-h-0 flex-1 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 bg-[var(--bg-surface-2)] px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)] backdrop-blur\">\n {mobileGroupLabel ? mobileGroupLabel(g.key) : g.key}\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-[var(--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-[var(--text-placeholder)]\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\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-[var(--border-strong)] bg-[var(--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-[var(--text-muted)]\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-[var(--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 && (\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 && \"min-h-0 flex-1\")}>\n <div className={cn(\"relative min-w-0 flex-1\", fillHeight && \"flex flex-col min-h-0\")}>\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 className={cn(\"overflow-auto [scrollbar-gutter:stable]\", fillHeight && \"flex-1 min-h-0\")}\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 className={cn(\"w-full\", compact ? \"text-xs\" : \"text-sm\")}\n aria-label={labels.table}\n aria-busy={isLoading || undefined}\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 headClassName below). */}\n <thead className=\"bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-[var(--bg-surface-2)] align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || !!onSortsChange) && !!col.sortBy;\n const filter = !isServer || onFiltersChange ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-[var(--bg-surface-2)] backdrop-blur-sm\",\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-[var(--text-primary)]\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-[9px] font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n width={filter.type === \"date\" ? 420 : undefined}\n // The panel is the column's filter, and says so. Unnamed it\n // was announced as the kit-wide fallback, \"Popover\".\n labels={{ panel: labels.filter }}\n trigger={({ toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n aria-label={labels.filter}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-[var(--brand-bg)] text-[var(--brand-muted)]\"\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {resizable && col.key !== ROW_ACTIONS_KEY && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-[var(--brand-bg-hover)]\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody\n className={cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\")}\n >\n {leadingRow && (\n <tr className=\"border-t border-[var(--border)]\">\n <td colSpan={totalColSpan} className=\"p-0\">\n {leadingRow}\n </td>\n </tr>\n )}\n {slice.map((row, rowIndex) => {\n const expanded = isExpanded?.(row) ?? false;\n const expansion = expanded ? expandedRow?.(row) : null;\n const rowInteractive = !!onRowClick || !!selection;\n const href = rowHref?.(row);\n const key = rowKey(row);\n return (\n <Fragment key={key}>\n <tr\n {...cleanAttrs(rowAttributes?.(row))}\n data-row-nav={rowsFocusable ? \"\" : undefined}\n tabIndex={rowsFocusable ? (key === tabRowKey ? 0 : -1) : undefined}\n aria-expanded={rowsFocusable && expandedRow ? expanded : undefined}\n onFocus={\n rowsFocusable\n ? (e) => {\n if (e.target === e.currentTarget) setFocusRowKey(key);\n }\n : undefined\n }\n onKeyDown={\n rowsFocusable\n ? (e) => {\n // Only the row's own key presses: Enter on a nested button\n // or link is that control's, and Space in a nested input\n // is a space.\n if (e.target !== e.currentTarget) return;\n if (e.altKey || e.ctrlKey || e.metaKey) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n // Space would otherwise scroll the table.\n e.preventDefault();\n onRowClick!(row);\n return;\n }\n if (e.shiftKey) return;\n if (moveRowFocus(e.currentTarget, e.key)) e.preventDefault();\n }\n : undefined\n }\n className={cn(\n \"border-t border-[var(--border)]\",\n rowInteractive && \"cursor-pointer hover:bg-[var(--bg-hover)]\",\n rowsFocusable &&\n \"focus-visible:bg-[var(--bg-hover)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n rowClassName?.(row),\n )}\n onClick={\n rowInteractive\n ? (e) => {\n // A click on a control inside the row (a button, a link,\n // an input the cell renders) is that control's — it must\n // not also open the row. Cells no longer have to\n // `stopPropagation` to get that.\n if (fromOwnControl(e.target, e.currentTarget)) return;\n if (fromOutside(e.target, e.currentTarget)) return;\n // Modifier-clicks anywhere in the row drive\n // selection instead of expanding it (feedback #289):\n // Ctrl/Cmd toggles a single row, Shift selects the\n // range from the last-toggled row (inclusive of this\n // one) — the same as the checkbox, but for the whole\n // row, which is what the checkbox-only version was\n // missing.\n if (selection && canSelect(row)) {\n if (e.shiftKey) {\n // Drop the text selection a shift-click would\n // otherwise smear across the rows.\n window.getSelection?.()?.removeAllRanges();\n if (selectRange(rowIndex)) return;\n // No anchor yet: treat it as the first pick.\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, !selection.isSelected(row));\n return;\n }\n if (e.metaKey || e.ctrlKey) {\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, !selection.isSelected(row));\n return;\n }\n }\n onRowClick?.(row);\n }\n : undefined\n }\n >\n {selection && (\n <td\n className={cn(\"align-top\", compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\")}\n // Don't let selecting a row also trigger the row click\n // (which expands/edits it).\n onClick={(e) => e.stopPropagation()}\n >\n {canSelect(row) && (\n <input\n type=\"checkbox\"\n className={cn(\"accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.isSelected(row)}\n // Shift+click selects the range from the last toggled\n // row. `preventDefault` puts the box back, but it does NOT\n // stop React's `onChange`: React derives a checkbox's\n // change from the `click` itself, after the browser has\n // already flipped `checked` and before the cancellation\n // reverts it — so the same click also reported itself as\n // a single toggle, and the owner got `onToggleMany` and\n // then `onToggle` for the clicked row. The click is\n // remembered by identity and its change is dropped; a\n // boolean flag would outlive a click whose change never\n // came and swallow the user's next real one.\n onClick={(e) => {\n if (e.shiftKey && selectRange(rowIndex)) {\n e.preventDefault();\n rangeClick.current = e.nativeEvent;\n }\n }}\n onChange={(e) => {\n if (rangeClick.current && e.nativeEvent === rangeClick.current) {\n rangeClick.current = null;\n return;\n }\n selectionAnchor.current = rowKey(row);\n selection.onToggle(row, e.target.checked);\n }}\n aria-label={labels.selectRow}\n />\n )}\n </td>\n )}\n {visibleColumns.map((col) => {\n const width = widths[col.key];\n // The caller's first, the table's over it — see `cellProps`.\n const own = splitCellProps(col.cellProps?.(row));\n const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <td\n {...own.attrs}\n key={col.key}\n data-col={col.key}\n style={own.style || pinned ? { ...own.style, ...pinned } : undefined}\n className={cn(\n \"align-top\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n width && \"overflow-hidden text-ellipsis\",\n logicalAlign(own.className),\n logicalAlign(col.className),\n )}\n >\n {col === linkColumn && href ? (\n // No `onActivate`: the click is left to bubble to the\n // <tr> handler above, which already owns expand-vs-select.\n // Calling it here as well would toggle the row twice.\n <RowLink href={href} className=\"block\" data-row-link=\"\">\n {col.cell(row)}\n </RowLink>\n ) : (\n col.cell(row)\n )}\n </td>\n );\n })}\n </tr>\n {expansion && (\n <tr className=\"bg-[var(--bg-surface-2)] border-t border-[var(--border)]\">\n {/* The detail panel spans every column. In an `auto` table\n layout (the default here — columns only get fixed style\n widths once the user resizes/auto-sizes them) the cell's\n content preferred-width is redistributed across the\n spanned columns, so a long expansion visibly re-sizes the\n columns above it — and one-line content does not, since\n its preferred width is small (feedback #104). Wrapping the\n content in `w-0 min-w-full` pins the cell's preferred\n width to ~0 (the explicit width:0 child), so it can never\n perturb the columns; the div then fills to the full row\n width only at paint time, and long text wraps within it. */}\n <td colSpan={totalColSpan} className=\"p-0\">\n <div className={cn(\"w-0 min-w-full\", compact ? \"px-2 py-2\" : \"px-3 py-3\")}>\n {expansion}\n </div>\n </td>\n </tr>\n )}\n </Fragment>\n );\n })}\n {slice.length === 0 && (\n <tr>\n <td\n colSpan={totalColSpan}\n className={cn(\"text-center text-[var(--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 )}\n </tbody>\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-[var(--text-primary)]\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n <tr>\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 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 </div>\n {!unpaged && (\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 setPageSize(n);\n setPage(0);\n }\n }\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n </div>\n {columnSettings && (\n <>\n <Tooltip label={columnsCountLabel} portal>\n <button\n type=\"button\"\n onClick={() => setShowSettings(true)}\n aria-label={columnsCountLabel}\n aria-hidden={showSettings}\n tabIndex={showSettings ? -1 : 0}\n className={cn(\n \"group flex shrink-0 items-start justify-center overflow-hidden pt-3 transition-[width] duration-200 ease-out\",\n \"border-s border-[var(--border)]\",\n \"hover:bg-[var(--bg-hover)]\",\n showSettings ? \"w-0 border-s-0\" : \"w-8 cursor-pointer\",\n )}\n >\n <span\n className=\"whitespace-nowrap text-xs font-medium tracking-wide text-[var(--text-muted)] group-hover:text-[var(--text-secondary)]\"\n style={{ writingMode: \"vertical-rl\", transform: \"rotate(180deg)\" }}\n >\n {columnsCountLabel}\n </span>\n </button>\n </Tooltip>\n <div\n className={cn(\n \"shrink-0 overflow-hidden transition-[width] duration-200 ease-out\",\n showSettings ? \"w-56\" : \"w-0\",\n )}\n >\n <div className=\"flex w-56 flex-col border-s border-[var(--border)] p-2\">\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {columnsCountLabel}\n </div>\n <Tooltip label={labels.close} portal>\n <button\n type=\"button\"\n onClick={() => setShowSettings(false)}\n aria-label={labels.close}\n className=\"rounded p-0.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </Tooltip>\n </div>\n <button\n type=\"button\"\n onClick={autoSizeAll}\n className=\"mb-2 rounded border border-[var(--border)] px-2 py-1 text-xs font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\"\n >\n {labels.autoSize}\n </button>\n <div className=\"flex flex-col gap-0.5\">\n {columns.map((col) => {\n if (col.key === ROW_ACTIONS_KEY) return null;\n const checked = !hiddenCols.has(col.key);\n const headerLabel = columnLabel(col);\n return (\n <label\n key={col.key}\n className=\"inline-flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 text-xs hover:bg-[var(--bg-hover)]\"\n >\n <input\n type=\"checkbox\"\n className=\"size-3.5\"\n checked={checked}\n onChange={(e) => {\n setHiddenCols((s) => {\n const next = new Set(s);\n if (e.target.checked) next.delete(col.key);\n else next.add(col.key);\n return next;\n });\n }}\n />\n <span className=\"truncate\">{headerLabel}</span>\n </label>\n );\n })}\n </div>\n </div>\n </div>\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\n// ---------- Mobile filter sheet ----------\n\n/**\n * Filter access for the mobile card list (feedback #299). The desktop table\n * houses per-column filters in its header; the card list has no header, so the\n * same filters were unreachable on phones. This renders a compact \"Filters\" bar\n * that opens a bottom sheet listing each filterable column as a collapsible\n * section, reusing the very same {@link FilterPopover} and filter state the\n * desktop table drives — so it filters identically and stays URL-synced.\n */\nfunction MobileFilters<T>({\n columns,\n filters,\n selectOptionsByKey,\n isServer,\n hasFilterCallback,\n onSetFilter,\n onClearFilter,\n onClearAll,\n labels,\n locale,\n}: {\n columns: DataTableColumn<T>[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n isServer: boolean;\n hasFilterCallback: boolean;\n onSetFilter: (key: string, value: FilterValue) => void;\n onClearFilter: (key: string) => void;\n onClearAll: () => void;\n labels: DataTableLabels;\n locale?: string;\n}) {\n const [open, setOpen] = useState(false);\n // The sheet is portalled to <body>, out from under whatever `dir` the table sits\n // in, so it carries the trigger's direction with it — read at the moment of opening,\n // from the element the user actually pressed.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const [expanded, setExpanded] = useState<string | null>(null);\n const backdropClose = useBackdropClose(() => setOpen(false));\n useBodyScrollLock(open);\n // Back dismisses the filter sheet too (Keksdose feedback #172) — it is the same\n // full-screen surface, and the filters it sets are already in the URL, so closing\n // it never loses anything.\n useOverlayHistory(open, () => setOpen(false));\n\n const filterable = useMemo(\n () =>\n columns\n .map((col) => ({ col, filter: !isServer || hasFilterCallback ? resolveFilter(col) : null }))\n .filter((x): x is { col: DataTableColumn<T>; filter: ColumnFilter<T> } => !!x.filter),\n [columns, isServer, hasFilterCallback],\n );\n\n if (filterable.length === 0) return null;\n\n const activeCount = filterable.filter(({ col }) => {\n const s = filters[col.key];\n return s && isFilterActive(s);\n }).length;\n\n return (\n <div className=\"flex items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-2\">\n <button\n type=\"button\"\n onClick={(e) => {\n setDir(dirOf(e.currentTarget));\n setOpen(true);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\"\n >\n <Filter className=\"size-4\" />\n <span>{labels.filters}</span>\n {activeCount > 0 && (\n <span className=\"inline-flex min-w-4 items-center justify-center rounded-full bg-[var(--info-bg)] px-1 text-[11px] font-semibold text-[var(--info)]\">\n {activeCount}\n </span>\n )}\n </button>\n {activeCount > 0 && (\n <button\n type=\"button\"\n onClick={onClearAll}\n className=\"text-xs text-[var(--text-muted)] hover:text-[var(--text-secondary)]\"\n >\n {labels.clearAll}\n </button>\n )}\n {open &&\n createPortal(\n <div\n className=\"fixed inset-0 z-50 flex items-end justify-center bg-black/40\"\n role=\"dialog\"\n aria-modal=\"true\"\n dir={dir}\n {...backdropClose}\n >\n <div className=\"flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-2xl bg-[var(--bg-surface)] shadow-xl\">\n <div className=\"flex items-center justify-between gap-2 border-b border-[var(--border)] px-4 py-3\">\n <div className=\"font-medium\">{labels.filters}</div>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n aria-label={labels.close}\n className=\"-me-1 rounded p-1.5 text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-5\" />\n </button>\n </div>\n <div className=\"flex-1 divide-y divide-[var(--border)] overflow-y-auto overscroll-contain\">\n {filterable.map(({ col, filter }) => {\n const state = filters[col.key];\n const active = !!state && isFilterActive(state);\n const isItemOpen = expanded === col.key;\n return (\n <div key={col.key}>\n <button\n type=\"button\"\n onClick={() => setExpanded(isItemOpen ? null : col.key)}\n aria-expanded={isItemOpen}\n className=\"flex w-full items-center justify-between gap-2 px-4 py-3 text-start\"\n >\n <span className=\"flex items-center gap-2 text-sm font-medium text-[var(--text-secondary)]\">\n {col.header}\n {active && <span className=\"size-1.5 rounded-full bg-brand\" />}\n </span>\n <ChevronDown\n className={cn(\n \"size-4 shrink-0 text-[var(--text-placeholder)] transition-transform\",\n isItemOpen && \"rotate-180\",\n )}\n />\n </button>\n {isItemOpen && (\n <div className=\"px-4 pb-3\">\n <FilterPopover\n column={col}\n state={state ?? defaultFilterState(filter)}\n onChange={(next) => onSetFilter(col.key, next)}\n onClear={() => onClearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n <div className=\"flex items-center justify-between gap-2 border-t border-[var(--border)] px-4 py-3\">\n <button\n type=\"button\"\n onClick={onClearAll}\n disabled={activeCount === 0}\n className=\"text-sm text-[var(--text-muted)] hover:text-[var(--text-secondary)] disabled:opacity-40\"\n >\n {labels.clearAll}\n </button>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"rounded-md bg-[var(--bg-inverse)] px-4 py-2 text-sm font-medium text-[var(--text-inverse)]\"\n >\n {labels.done}\n </button>\n </div>\n </div>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Fragment"]}