@eifi1/ui-kit 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -38
- package/dist/billing/billing-banner.d.ts +5 -2
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/plan-limit-notice.d.ts +5 -2
- package/dist/billing/plan-limit-notice.js.map +1 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/amount-input.d.ts +0 -3
- package/dist/components/amount-input.js +2 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/calculator.d.ts +1 -4
- package/dist/components/calculator.js +1 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.d.ts +0 -37
- package/dist/components/chart-zoom.js +1 -28
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip-input.d.ts +0 -4
- package/dist/components/chip-input.js +1 -5
- package/dist/components/chip-input.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/currency-option-list.d.ts +3 -8
- package/dist/components/currency-option-list.js +1 -29
- package/dist/components/currency-option-list.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +3 -3
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +1 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +1 -42
- package/dist/components/data-table-labels.js +2 -31
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +17 -1
- package/dist/components/data-table-shared.js +22 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table.d.ts +12 -2
- package/dist/components/data-table.js +18 -14
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +3 -1
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/dropdown.d.ts +18 -15
- package/dist/components/dropdown.js +11 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-base.d.ts +0 -5
- package/dist/components/field-base.js +0 -5
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-parts.d.ts +3 -11
- package/dist/components/field-parts.js +1 -9
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +8 -23
- package/dist/components/field-strip.js +24 -22
- package/dist/components/field-strip.js.map +1 -1
- package/dist/components/field-sync.d.ts +0 -4
- package/dist/components/field-sync.js +0 -4
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.d.ts +4 -4
- package/dist/components/field.js.map +1 -1
- package/dist/components/full-bleed-dialog.js +2 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.d.ts +6 -13
- package/dist/components/hover-menu.js +3 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/inline-entity-combobox.d.ts +1 -1
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/modal.d.ts +9 -1
- package/dist/components/modal.js +7 -4
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-select.d.ts +5 -16
- package/dist/components/multi-select.js +5 -10
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.d.ts +0 -3
- package/dist/components/number-input.js +2 -4
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/popover.js +2 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/server-wake.d.ts +0 -6
- package/dist/components/server-wake.js.map +1 -1
- package/dist/components/settings-fields.d.ts +4 -2
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/stat-tile.d.ts +8 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +2 -17
- package/dist/components/swipeable-row.js +11 -16
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tooltip.d.ts +9 -1
- package/dist/components/tooltip.js +20 -5
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review.d.ts +10 -2
- package/dist/components/translation-review.js +11 -3
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/use-table-state.d.ts +31 -3
- package/dist/components/use-table-state.js +53 -8
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +2 -2
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/components/wizard-stepper.d.ts +1 -4
- package/dist/components/wizard-stepper.js +2 -3
- package/dist/components/wizard-stepper.js.map +1 -1
- package/dist/currencies.d.ts +23 -0
- package/dist/currencies.js +38 -0
- package/dist/currencies.js.map +1 -0
- package/dist/demo/demo-banner.d.ts +7 -2
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/eslint/index.d.ts +83 -0
- package/dist/eslint/index.js +69 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/role-utilities.d.ts +48 -0
- package/dist/eslint/role-utilities.generated.d.ts +2 -0
- package/dist/eslint/role-utilities.generated.js +143 -0
- package/dist/eslint/role-utilities.generated.js.map +1 -0
- package/dist/eslint/role-utilities.js +43 -0
- package/dist/eslint/role-utilities.js.map +1 -0
- package/dist/feedback/feedback-menu.d.ts +2 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +3 -8
- package/dist/feedback/feedback-thread.js +0 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +16 -3
- package/dist/hooks/use-dismiss.js +14 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +14 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/fr.js +3 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/it.js +3 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/refs.d.ts +13 -0
- package/dist/lib/refs.js +15 -0
- package/dist/lib/refs.js.map +1 -0
- package/dist/rhf/fields.d.ts +10 -10
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form-core.d.ts +5 -4
- package/dist/rhf/form-core.js.map +1 -1
- package/dist/rhf/form.d.ts +7 -36
- package/dist/rhf/form.js +1 -16
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf/line-items.d.ts +1 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
- package/dist/rhf.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +5 -17
- package/dist/shell/option-switcher-menu.js +1 -3
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +1 -4
- package/dist/shell/role-switcher.js +2 -3
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/topbar-action-menu.d.ts +7 -19
- package/dist/shell/topbar-action-menu.js +1 -3
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +3 -12
- package/dist/shell/topbar-controls.js +6 -9
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/viewport.d.ts +25 -2
- package/dist/testing/viewport.js +61 -10
- package/dist/testing/viewport.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +0 -7
- package/dist/theme/palette-presets.js +0 -279
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/wizard/types.d.ts +0 -5
- package/dist/wizard/types.js +1 -5
- package/dist/wizard/types.js.map +1 -1
- package/package.json +9 -1
- package/src/billing/billing-banner.tsx +5 -2
- package/src/billing/plan-limit-notice.tsx +5 -2
- package/src/chart.ts +5 -3
- package/src/components/amount-input.tsx +2 -6
- package/src/components/calculator.tsx +2 -7
- package/src/components/chart-zoom.tsx +4 -47
- package/src/components/chip-input.tsx +0 -7
- package/src/components/combobox.tsx +1 -1
- package/src/components/copy-button.tsx +5 -1
- package/src/components/currency-option-list.tsx +8 -42
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-filter-popover.tsx +2 -1
- package/src/components/data-table-labels.ts +2 -84
- package/src/components/data-table-shared.tsx +55 -2
- package/src/components/data-table.tsx +29 -17
- package/src/components/dialog-frame.tsx +3 -1
- package/src/components/dropdown.tsx +42 -18
- package/src/components/entity-combobox.tsx +1 -1
- package/src/components/field-base.tsx +0 -11
- package/src/components/field-parts.tsx +5 -19
- package/src/components/field-strip.tsx +9 -25
- package/src/components/field-sync.tsx +0 -7
- package/src/components/field.tsx +4 -4
- package/src/components/full-bleed-dialog.tsx +4 -3
- package/src/components/hover-menu.tsx +12 -16
- package/src/components/inline-entity-combobox.tsx +1 -1
- package/src/components/modal.tsx +20 -4
- package/src/components/multi-select.tsx +13 -30
- package/src/components/number-input.tsx +3 -8
- package/src/components/popover.tsx +5 -3
- package/src/components/series-chart.tsx +1 -1
- package/src/components/server-wake.tsx +0 -6
- package/src/components/settings-fields.tsx +4 -2
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/swipeable-row.tsx +15 -37
- package/src/components/tooltip.tsx +37 -6
- package/src/components/translation-review.tsx +25 -7
- package/src/components/use-table-state.ts +91 -10
- package/src/components/user-avatar.tsx +2 -2
- package/src/components/wizard-stepper.tsx +2 -6
- package/src/currencies.ts +70 -0
- package/src/demo/demo-banner.tsx +7 -2
- package/src/eslint/index.ts +161 -0
- package/src/eslint/role-utilities.generated.ts +146 -0
- package/src/eslint/role-utilities.ts +99 -0
- package/src/feedback/feedback-menu.tsx +2 -2
- package/src/feedback/feedback-thread.tsx +3 -8
- package/src/hooks/use-dismiss.ts +39 -4
- package/src/i18n/kit-labels.tsx +15 -5
- package/src/i18n/locales/fr.ts +3 -2
- package/src/i18n/locales/it.ts +3 -2
- package/src/index.ts +3 -1
- package/src/lib/refs.ts +22 -0
- package/src/rhf/fields.tsx +10 -10
- package/src/rhf/form-core.tsx +5 -4
- package/src/rhf/form.tsx +7 -43
- package/src/rhf/line-items.tsx +1 -1
- package/src/rhf/use-rhf-wizard-step.ts +4 -4
- package/src/rhf.ts +3 -2
- package/src/shell/option-switcher-menu.tsx +5 -22
- package/src/shell/role-switcher.tsx +2 -6
- package/src/shell/topbar-action-menu.tsx +7 -24
- package/src/shell/topbar-controls.tsx +6 -18
- package/src/testing/index.ts +3 -1
- package/src/testing/viewport.ts +125 -19
- package/src/theme/palette-presets.ts +7 -293
- package/src/wizard/types.ts +0 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { Card } from \"./card\";\nimport { Spinner } from \"./spinner\";\nimport { ROW_ACTIONS_KEY, useRowActionColumn } from \"./data-table-row-actions\";\nimport { useColumnResize } from \"./data-table-resize\";\nimport { useRangeSelection } from \"./data-table-selection\";\nimport { RowLink, cleanAttrs, columnLabel, fromOutside, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport { MobileFilters, MobileRow, MobileSort, StackedFields } from \"./data-table-mobile\";\nimport { DataTableHead } from \"./data-table-head\";\nimport { ColumnSettingsPanel } from \"./data-table-column-settings\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { cn } from \"../lib/cn\";\nimport { isFilterActive, resolveFilter, rowMatches } from \"./data-table-filters\";\nimport type { ColumnFilter, FilterValue } from \"./data-table-filters\";\nimport { nextSorts } from \"./data-table-sort\";\nimport type { SortCycle, SortDir, SortState } from \"./data-table-sort\";\nimport { Pagination } from \"./data-table-pagination\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale, useKitTableStorageKeyPrefix, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { dirOf, type Direction } from \"../lib/direction\";\n\n// ---------- Types ----------\n\nexport interface DataTableColumn<T> {\n key: string;\n header: ReactNode;\n cell: (row: T) => ReactNode;\n sortBy?: (row: T) => string | number | null | undefined;\n filter?: ColumnFilter<T>;\n filterBy?: (row: T) => string;\n className?: string;\n headClassName?: string;\n // Mobile card layout: when no `mobilePrimary` column is set the first\n // visible non-hidden column acts as the primary. The primary cell renders\n // bold at the top of the card without a label; the rest stack below as\n // labelled key/value pairs. `mobileHidden` skips a column entirely on\n // narrow viewports — useful for noisy details (IDs, raw URLs, internal\n // bookkeeping) that would crowd a card without aiding scanning.\n mobilePrimary?: boolean;\n mobileHidden?: boolean;\n /**\n * Set on a column whose cell renders its own `<a>` (or anything else that must\n * keep its own click): the row anchor of `rowHref` then skips it and lands on\n * the next column that allows wrapping (Keksdose feedback #451). Without it the\n * fallback can wrap such a cell the moment the headline column is hidden from\n * the settings panel — `<a>` inside `<a>`, which React warns about and which\n * kills the inner link outright, since RowLink cancels the click on the capture\n * phase and the cell's own `stopPropagation` then blocks the row handler too.\n * The rejected alternative was sniffing the rendered output for an anchor: a\n * cell that renders a link is a fact its author knows and the renderer cannot\n * see without reaching into React elements it deliberately treats as opaque.\n */\n noRowLink?: boolean;\n /**\n * The direction this column sorts in on its FIRST click. `\"desc\"` for money and\n * count columns, where the rows worth reading are the large ones and an ascending\n * first click opens on the zero rows (keksdose's aggregated report tables). The\n * rest of the cycle follows from it — see {@link DataTableProps.sortCycle}.\n * Default `\"asc\"`.\n */\n firstSort?: SortDir;\n /**\n * The column's name as plain text, for the places a ReactNode `header` cannot go:\n * the phone sort control's options ({@link DataTableProps.mobileSort}), the spoken\n * \"Sorted by …\" and the column-settings checklist. Only needed when `header` is not\n * a string (an icon, a unit suffix); without it those places fall back to the\n * header string, then to `key`.\n */\n headerText?: string;\n /**\n * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a\n * `title`, an `aria-describedby`, a class. keksdose's accounts page\n * (accounts-page.tsx, the name and IBAN columns) is why it exists: its demo mode\n * blurs everything marked `data-private`, and with nowhere to put the marker on the\n * cell it wrapped every value in an inner `<span data-private>` — which blurs the\n * text but not the cell, and has to be remembered in every `cell` renderer.\n *\n * MERGED under the kit's own, which win: `className` is joined before the column's\n * `className` (so the column's alignment is still the alignment), `style` is laid\n * under a resized column's pinned width, and `data-col` and a `role` stay the\n * table's — `data-col` is how a resize finds the column's cells, and a `<td>` that\n * claims another role leaves its row's cell count wrong for a screen reader.\n *\n * On a phone the card field (`<dd>`, or the primary line) gets the `data-*`\n * attributes only: the rest — padding classes, `colSpan`, a width — describe a\n * table cell and would mean something else, or nothing, on a card.\n */\n cellProps?: (row: T) => DataTableCellProps | undefined;\n /** The same for the column's `<th>`, once. `scope`, `aria-sort`, `data-col` and the\n * width a resize pins stay the table's, and the class list goes before\n * `headClassName` for the same reason as in {@link cellProps}. */\n headProps?: DataTableHeadProps;\n /**\n * This column's cell in the table's totals row — a `<tfoot>` on a wide screen, a\n * labelled summary card under the list on a phone. kastlan's trial balance is why\n * it exists: it hand-built a debit/credit/balance strip in a `div` UNDER the table,\n * which lined up with no column, dropped out of the table's semantics, and stayed\n * put when the user hid or resized the columns it was summing.\n *\n * `rows` is every row that passes the current filters (in the current sort order),\n * NOT just the page on screen: a total that changes when the user pages is not a\n * total, and the trial balance has to balance on page 1 as much as on page 7. The\n * page is the second argument for the rare \"subtotal of this page\" footer.\n *\n * Under `serverPagination` the table only ever holds one page, so both arguments are\n * that page — a server-side table computes its totals on the server and returns\n * them here (`footer: () => fmt(response.totalDebit)`), ignoring the arguments.\n *\n * The cell takes the column's `className` (so a `text-end` amount column totals\n * end-aligned), its resized width, and drops out with the column when it is hidden.\n */\n footer?: (rows: T[], page: T[]) => ReactNode;\n}\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** What {@link DataTableColumn.cellProps} may return. */\nexport type DataTableCellProps = TdHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\n/** What {@link DataTableColumn.headProps} takes. */\nexport type DataTableHeadProps = ThHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\nexport interface ServerPagination {\n page: number; // 0-based\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n /** Without it the page size is the server's to choose, and the pager shows no\n * page-size select at all — one that changed nothing was a control that lied. */\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * A page is being fetched. The table marks itself `aria-busy`, dims the rows it is\n * still showing (they are the PREVIOUS page's, and about to go), and replaces the\n * empty state with a loading row when there is nothing to show yet — \"no results\"\n * while the first page is still in flight is a claim the table cannot make.\n *\n * The pager stays usable. Disabling it would drop focus from the very button the\n * user just pressed (a disabled button cannot hold focus), and a second click\n * during a fetch is a request the owner already has to handle for a slow network.\n */\n isLoading?: boolean;\n}\n\nexport interface DataTableProps<T> {\n rows: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string | number;\n defaultPageSize?: number;\n onRowClick?: (row: T) => void;\n /**\n * The URL that shows this row, when one exists — the table then renders a REAL\n * anchor for it (Keksdose feedback #451, \"to be able to middle click with the\n * mouse and open in a new tab\"). A row that opens through `onRowClick` alone is\n * invisible to the browser: no middle click, no ⌘/Ctrl-click, no \"copy link\n * address\", no status-bar preview, no long-press menu on a phone. The\n * alternative was an `onAuxClick` that calls `window.open` — it buys back only\n * the middle click and loses the other four, none of which JavaScript can fake.\n *\n * Return `undefined` for a row that has no address of its own. Only wire this up\n * where the OPEN row is genuinely in the URL (a `?row=` param, a detail route);\n * a row whose expansion lives in local component state has no link to hand out,\n * and a link the target page cannot honour is worse than no link.\n *\n * The anchor covers the row's primary cell, not the whole `<tr>`: an `<a>` may\n * not wrap table cells, and the absolutely-positioned row overlay that is the\n * usual workaround would sit on top of the per-cell controls these rows carry\n * (status toggles, the selection checkbox, nested links) and swallow them.\n */\n rowHref?: (row: T) => string | undefined;\n expandedRow?: (row: T) => ReactNode | null;\n isExpanded?: (row: T) => boolean;\n /**\n * Extra classes for a row — the `<tr>` on desktop, the card on mobile. Merged last,\n * so a `hover:` fill here replaces the row's own `hover:bg-hover`.\n *\n * **A row you paint has to pick its own hover** (0.33, docs/colour-roles-harmonization.md\n * §12.2): the kit cannot see your rest colour, and `--bg-hover` is mixed against the\n * card. The rule: a hover differs from its rest by 1.08:1 or more, and every text role\n * on it keeps 4.5:1.\n * - A row that rests on the well (`bg-surface-2`) hovers to the CARD,\n * `hover:bg-surface`: in light the well is the page, and `--bg-hover` sits 1.04:1\n * from it.\n * - A row whose well MARKS A STATE (selected) keeps its fill and draws a 1 px inside\n * line instead, since the card would read as \"not selected\":\n * `bg-surface-2 hover:bg-surface-2 hover:outline hover:-outline-offset-1 hover:outline-strong`.\n * An outline, not a shadow: a shadow on a `<tr>` is not painted by every engine.\n * - Not `--bg-active` for a selected row: its muted and money text fall to 3.8–4.7:1.\n */\n rowClassName?: (row: T) => string | undefined;\n /**\n * Extra DOM attributes for a row's container — the `<tr>` on desktop, the\n * `<li>` on mobile. For hooks that have to sit on the row itself rather than\n * inside a cell: guided-tour anchors, analytics ids, e2e selectors. Undefined\n * values are dropped, so a per-row conditional needs no filtering at the call\n * site. Not a styling escape hatch — that is `rowClassName`.\n */\n rowAttributes?: (row: T) => Record<string, string | undefined> | undefined;\n /**\n * Set false for a table that is short by construction — an invoice's line\n * items, a wizard's picks — where the pager is chrome around a list that can\n * never need one. Renders every row and drops the footer entirely. Ignored in\n * `serverPagination` mode, where the pager is the only way to reach the rest\n * of the data.\n */\n paginated?: boolean;\n empty?: ReactNode;\n /**\n * If set, the table persists filter / sort / page-size state to localStorage under this key.\n * Use a stable, unique key per table (e.g. \"transactions-table\").\n */\n storageKey?: string;\n /**\n * When true, mirror filter/sort/page state into URL search params so views are\n * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.\n * URL state takes precedence over localStorage on initial load. Requires the\n * consuming app to render inside a react-router Router.\n *\n * `{ prefix }` namespaces them, for two URL-synced tables on one page:\n * `urlSync={{ prefix: \"inv.\" }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.\n */\n urlSync?: TableUrlSync;\n /**\n * When true, the table fills its parent's height and scrolls INTERNALLY, with the\n * header pinned — so the page itself doesn't add a second, outer scrollbar. 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 * The cure for TWO SCROLLBARS (round 0.34 C14, kastlan feedback 55): the default\n * `maxBodyHeight` scrolls the body inside a page that scrolls too. How a page fills\n * the height is in {@link DataTableProps.maxBodyHeight}'s note.\n */\n fillHeight?: boolean;\n /** How tall the scrolling body may get before it scrolls, as a CSS length.\n *\n * Default `calc(100dvh - 12rem)`, which is what every caller had before this\n * existed: a bound is what makes the sticky header work, because a header\n * can only stick to a wrapper that actually scrolls. The cap assumes the\n * table has page chrome above and below it, and a table that is the last\n * thing on its page can afford more — ASPICE Atlas report #12, \"for the 25\n * elements there should in best case be no scrolling necessary\".\n *\n * Ignored under `fillHeight`, which bounds the body by its flex parent\n * instead and is the stronger statement of the same intent.\n *\n * **Two scrollbars** (round 0.34 C14, kastlan feedback 55). The cap scrolls the\n * body INSIDE a page that, in most shells, scrolls itself: a table taller than\n * the cap on a page with anything else on it shows two scrollbars side by side,\n * and the wheel moves whichever is under the pointer. For a list page whose table\n * is the point, fill the height instead, from `md` up (a phone scrolls the page\n * and its card list as one):\n *\n * 1. the shell's content column becomes a flex column of the main area's height\n * when the page asks — kastlan's\n * `md:has-[>[data-fill-height]]:flex md:has-[>[data-fill-height]]:h-full md:has-[>[data-fill-height]]:flex-col`;\n * 2. the page's root asks, and is a flex column that may shrink:\n * `data-fill-height` and `md:flex md:min-h-0 md:flex-1 md:flex-col`;\n * 3. the table takes the rest: `<DataTable fillHeight>`.\n *\n * Only the body then scrolls, under its pinned header, and every other page grows\n * with its content as before (kastlan e3985c9, its leases list). A short table that\n * is the last thing on its page can instead drop the cap: `maxBodyHeight=\"none\"`. */\n maxBodyHeight?: string;\n /**\n * When provided, the table renders the supplied rows as-is (no client-side\n * filtering/sorting/slicing) and the pagination footer is driven by these\n * server-controlled values. Column-level filters and sorting are hidden\n * UNLESS the caller takes them over via `onFiltersChange`/`onSortsChange` —\n * the caller is then expected to translate the state into the server query\n * that produces `rows`.\n */\n serverPagination?: ServerPagination;\n /**\n * 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?: LabelOverride<DataTableLabels>;\n /** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls\n * back to `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** localStorage namespace prefix for `storageKey` persistence: the stored key is this\n * plus `storageKey`. Falls back to `<UiKitProvider tableStorageKeyPrefix>` (0.34, set\n * your app's prefix there once), then to \"hbui-table:\". This prop wins over both. */\n storageKeyPrefix?: string;\n /**\n * On phones, surface the expanded row's detail in a full-screen dialog instead\n * of unfolding it inline (feedback #204). Desktop always uses inline expansion.\n */\n mobileExpandAsDialog?: boolean;\n /**\n * Show the `mobileHidden` columns in the OPENED row on the phone layout — inline\n * under the card, or in the `mobileExpandAsDialog` dialog — so a column dropped from\n * the card to keep it short is not lost (0.32, docs/text-size-harmonization.md §4:\n * \"a new `mobileDetailsInRow` puts the `mobileHidden` columns into the opened row, so\n * nothing is lost\"). At Large a card holds fewer facts per screen, and dropping one\n * from the card must not drop it from the phone.\n *\n * Each column is a label above its value, BEFORE what `expandedRow` renders: the\n * row's own facts first, then the app's detail or editor. A row opens as it always\n * does (`onRowClick` toggling `isExpanded`); with this set it may open without an\n * `expandedRow` at all. `rowActions`' column is one of them — the actions, which the\n * card otherwise offers only as swipes, reachable by a tap. Desktop is unaffected.\n */\n mobileDetailsInRow?: boolean;\n /**\n * The row's name, as plain text — for the \"⋯\" menu its `rowActions` collapse into at\n * Large (\"Actions for Ada Example\", §10.8). Without it every row's menu button is\n * named \"Actions\", which a screen reader cannot tell apart down the list.\n */\n rowName?: (row: T) => string | undefined;\n /**\n * The phone row dialog's `backCloses` ({@link FullBleedDialog}): whether it pushes\n * its own history entry so Back dismisses it. On by default — right for a row whose\n * expansion is component state, where nothing else would answer Back.\n *\n * **Set it `false` when the open row is in the URL** and opening it PUSHES (a\n * `?row=` written with `useSearchParamState(…, { history: \"replace-on-clear\" })` or\n * `\"push\"`): Back already closes the row through the router, and a second entry on\n * top of the page's is two owners of one Back press (keksdose dev #584). Keep it on\n * when opening the row REPLACES the URL — then nothing was pushed, and the dialog's\n * own entry is the only thing Back can pop.\n *\n * The table cannot decide this for you: `isExpanded` is a predicate, and neither it\n * nor `rowHref` says whether opening pushed or replaced (keksdose's feedback page\n * has `rowHref` with a REPLACING `?row=`, and needs the entry). Ignored unless\n * `mobileExpandAsDialog`.\n */\n mobileDialogBackCloses?: boolean;\n /**\n * Group the mobile card list into sections, each with a sticky header — the\n * \"assistance\"-style list (like the transactions list grouped by date, but by\n * whatever key you return, e.g. a name's first letter). The returned string is\n * the section a row belongs to; rows must already be ordered so equal keys are\n * contiguous (pair with a matching default sort). `mobileGroupLabel` formats\n * the header. Desktop is unaffected. Feedback #317.\n */\n mobileGroupBy?: (row: T) => string;\n mobileGroupLabel?: (key: string) => ReactNode;\n /**\n * Fully replace the default mobile card body (bold primary + labelled\n * key/value rows) with a compact custom layout, while keeping the shared\n * clickable/expandable row wrapper. Use when the stacked label/value grid\n * wastes space (feedback #317). Desktop is unaffected.\n *\n * It replaces the WHOLE body, `mobilePrimary` included: a column's\n * `mobilePrimary` cell is not drawn when `mobileCard` is set, so anything that\n * cell carried (keksdose's environment and rework chips, 0.27) must be drawn by\n * the card itself — as `FeedbackMobileCard` does.\n */\n mobileCard?: (row: T) => ReactNode;\n /**\n * A \"Sort by\" select and a direction button above the phone card list (keksdose\n * K10). The card list has no header row, so without it a phone user is stuck with\n * whatever order the table opened in. It drives the same sort state as the column\n * headers — `sorts`/`onSortsChange` when controlled, the URL and `storageKey` when\n * not — and offers the columns a header click could sort (those with a `sortBy`).\n * Each option is named by the column's `headerText`, else its string `header`.\n *\n * Off by default, so no existing phone layout gains a row. Strings:\n * {@link DataTableSortLabels}. Phones only; the desktop table is unchanged.\n */\n mobileSort?: boolean;\n /**\n * Swipe actions for a mobile row, revealed by dragging it horizontally. Return\n * `null` (or empty sides) for rows that should not move — a locked row, one whose\n * mutation is in flight, one the user has expanded.\n *\n * Prefer `start`/`end`: actions committed by dragging the row toward the reading\n * START or END of the line. In a left-to-right layout `end` is a drag to the\n * right, and in a right-to-left one it is a drag to the left — so a \"drag forward\n * to archive\" row stays forward in Arabic or Hebrew without a second definition.\n * `left`/`right` are the physical directions and keep meaning exactly that; each\n * is used only where the logical side for that direction is not given.\n *\n * Mobile only, and deliberately so: the desktop table already has room for an\n * actions column, and a drag gesture on a pointer device is a worse version of a\n * button. Wraps only the row body, so an expansion panel below stays put while the\n * row above it slides.\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; with `mobileDetailsInRow` also as buttons in the opened row). At Large and\n * Extra large two or more collapse into one \"⋯\" menu with each action's words\n * (0.32, §10.8, `RowActions`) — name the rows with `rowName` so each menu button is\n * \"Actions for …\". kastlan's `ResourceListPage` appended this column by hand to every CRUD\n * list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a\n * `stopPropagation` on each so a press did not also open the row.\n *\n * `kind: \"edit\"` / `\"delete\"` bring the icon, the label (`labels.edit` /\n * `labels.delete`), the tone and the swipe side; any other action states its own\n * `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's\n * `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.\n *\n * const confirm = useConfirm();\n * rowActions={[\n * { kind: \"edit\", onAction: openEditor },\n * { kind: \"delete\", onAction: (r) => remove(r.id),\n * confirm: (r) => confirm({ title: t(\"delete\", { name: r.name }), tone: \"danger\" }) },\n * ]}\n *\n * A function receives the row, for actions that differ per row. An explicit\n * `mobileSwipeActions` wins over the swipes derived from here.\n */\n rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);\n /**\n * The 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/** 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/**\n * A totals-row cell: pinned to the scroller's bottom edge, on the header's surface.\n * The rule above it is an inset shadow rather than a `border-t`: the table collapses\n * its borders, and a collapsed border belongs to the TABLE, not the cell, so it stays\n * behind at the bottom of the rows while the sticky cell rides over them.\n */\nconst FOOTER_CELL =\n \"sticky bottom-0 z-10 bg-surface-2 shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n// ---------- Phone sort control (keksdose K10) ----------\n\n/**\n * The phone sort control's strings (keksdose K10). They are `dataTable` strings —\n * read from the table's resolved labels, so the `labels` prop and the provider's\n * `dataTable` namespace reach them — and are declared here, with the table's other\n * public types (the control itself, `MobileSort`, is in data-table-mobile.tsx), with\n * English defaults until `DataTableLabels` declares them itself.\n */\nexport interface DataTableSortLabels {\n /** The control's visible label: \"Sort by\". */\n sortBy: string;\n /** The \"no sort\" option — the rows in the order they were given: \"Default order\". */\n sortDefault: string;\n /** The direction button's name while the sort ascends: \"Ascending\". */\n sortAscending: string;\n /** …and while it descends: \"Descending\". */\n sortDescending: string;\n}\n\n/** The phone sort control's words, as they sit in {@link DEFAULT_DATA_TABLE_LABELS}\n * (DataTableLabels extends DataTableSortLabels; one source for the English). */\nexport const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {\n sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,\n sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,\n sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,\n sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending,\n};\n\nconst flipDir = (d: SortDir): SortDir => (d === \"asc\" ? \"desc\" : \"asc\");\n\n/**\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/** 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// ---------- 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: storageKeyPrefixProp,\n mobileExpandAsDialog = false,\n mobileDetailsInRow = false,\n rowName,\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 // prop > provider > the kit's default, the order every kit setting resolves in (round\n // 0.34 C5, KS U4): one prefix on the provider reaches the tables an app cannot pass a\n // prop to, `TranslationReviewPanel`'s among them.\n const providerStorageKeyPrefix = useKitTableStorageKeyPrefix();\n const storageKeyPrefix = storageKeyPrefixProp ?? providerStorageKeyPrefix ?? DEFAULT_PERSIST_PREFIX;\n const labels = useMemo(\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 ---- (data-table-row-actions.tsx)\n // The actions column joins the caller's columns; on a phone the same actions are the\n // card's swipes, unless the caller's own `mobileSwipeActions` says otherwise.\n const { columns, swipesFromActions } = useRowActionColumn({\n columns: columnsProp,\n rowActions,\n rowName,\n labels,\n density,\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 ---- (data-table-resize.ts: pointer capture, one `setWidths` per\n // drag, and the page's cursor and selection handed back on an unmount mid-drag)\n const { startResize, moveResize, finishResize, autoSizeAll } = useColumnResize({\n headRefs,\n widths,\n setWidths,\n columns,\n hiddenCols,\n });\n\n const visibleCount = visibleColumns.length;\n const totalCount = columns.length;\n // Spans the full row width including the optional leading selection column.\n const totalColSpan = visibleCount + (selection ? 1 : 0);\n\n // ---- Totals row ----\n // `sorted`, not `slice`: the totals are of everything the filters let through, so\n // paging does not move them — see `DataTableColumn.footer`. In server mode `sorted`\n // IS the page (the table holds nothing else), which the prop's note spells out.\n const hasFooterColumns = columns.some((c) => c.footer);\n const showFooter = footerProp !== false && hasFooterColumns && sorted.length > 0;\n // The row header sits in the first visible column that has nothing of its own to\n // show — so hiding the \"Account\" column moves \"Total\" into \"Name\" rather than\n // dropping it — and on the first visible column when every one carries a value.\n const footerLabelCol = showFooter\n ? (visibleColumns.find((c) => !c.footer && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // Each open row's detail row, for its disclosure button's `aria-controls`.\n const detailIdBase = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const columnsCountLabel = rowActions\n ? labels.columnsCount(visibleCount - 1, totalCount - 1)\n : labels.columnsCount(visibleCount, totalCount);\n // Match the `md:` breakpoint at the text size in force: we render either the table or\n // the card list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin\n // `useBreakpoint(\"md\")`: the same query an app's own phone checks ask (keksdose's\n // sticky editor footer), so the card list and the full-screen row dialog switch with\n // them and never a frame apart (§10.4 \"usePhoneLayout() is the single answer\"; 0.32.1,\n // keksdose's 0.32 report, where `md` and `max-md` both missed at 767.5 px). Without\n // `matchMedia` (SSR, jsdom) it is the table, as before.\n const isMdUp = !usePhoneLayout();\n // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.\n const desktopScroller = useRef<HTMLDivElement | null>(null);\n 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 columns the card leaves out, for the opened row (`mobileDetailsInRow`).\n const mobileDetailColumns = mobileDetailsInRow ? visibleColumns.filter((c) => c.mobileHidden) : [];\n // Large and Extra large (§4 \"DataTable phone card\"): the card's labels go ABOVE their\n // values, nothing in it truncates, and the rows get more room. Most of it is a\n // `large:` class; the label/value grid needs a different STRUCTURE (each pair in a\n // box of its own), which a class cannot write.\n const large = useLargeText();\n // What an opened phone row shows: the card's left-out columns first (when asked\n // for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.\n const opensRow = !!expandedRow || mobileDetailColumns.length > 0;\n const openedRowContent = (row: T): ReactNode => {\n const detail = expandedRow?.(row) ?? null;\n if (mobileDetailColumns.length === 0) return detail;\n return (\n <div data-row-details=\"\" className={cn(\"flex flex-col\", compact ? \"gap-2\" : \"gap-3\")}>\n <StackedFields columns={mobileDetailColumns} row={row} compact={compact} />\n {detail}\n </div>\n );\n };\n // The phone card IS the anchor, so unlike the desktop table there is no other\n // cell to move the row link into: if any cell the card renders owns a link, the\n // card itself cannot be one (feedback #451 — same nesting rule as `linkColumn`\n // above). It then falls back to the pre-#451 role=\"button\" card, which still\n // opens the row and leaves the cell's own link working. A caller's `mobileCard`\n // renderer is opaque to us; a link inside one is the caller's to declare by\n // marking the column it comes from.\n const mobileCardLinkable = mobileColumns.every((c) => !c.noRowLink);\n // The card list's reading direction, for the one thing on it CSS cannot flip: which\n // physical drag a logical `start`/`end` swipe action means. Read off the DOM through\n // a ref callback (it runs at commit, like a layout effect) because `dir` is an\n // attribute of some ancestor the table does not own; LTR until the first commit,\n // which is also before any finger can have touched a row.\n const [mobileDir, setMobileDir] = useState<Direction>(\"ltr\");\n const mobileRootRef = (el: HTMLElement | null) => {\n if (!el) return;\n const d = dirOf(el);\n if (d !== mobileDir) setMobileDir(d);\n };\n\n const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,\n // anchored on the row's KEY so a sort, filter or page change cannot move it)\n const { selectionAnchor, rangeClick, canSelect, selectRange } = useRangeSelection({\n selection,\n slice,\n rowKey,\n });\n // ---- Keyboard-activatable rows ----\n //\n // A desktop row with `onRowClick` opened on a mouse click and on nothing else: no\n // tab stop, no Enter, no focus ring (keksdose's report tables hand-rolled all three,\n // which is half of why they could not move onto this table). The row now takes\n // focus itself, as a ROVING tab stop: one row of the table is in the tab order\n // (the last one focused, else the first), ArrowUp/ArrowDown/Home/End move between\n // rows, Enter and Space activate.\n //\n // Why the `<tr>` rather than the two alternatives:\n // * `role=\"button\"` on the row would replace its `row` role, and the cells with\n // it — a screen reader would stop reading the table as a table.\n // * A button or link in one cell is what `rowHref` already is where the row HAS a\n // URL. Without one, a synthetic button would have to wrap caller-rendered cell\n // content, which nests the controls those cells carry (status toggles, links)\n // inside it — illegal markup, and the reason the mobile card is a div.\n // A focused `<tr>` keeps its role and reads out its cells. Roving rather than a tab\n // stop per row, so a 100-row page does not put 100 stops between the header and\n // the pager.\n const rowsFocusable = !!onRowClick;\n const [focusRowKey, setFocusRowKey] = useState<string | number | null>(null);\n const tabRowKey =\n focusRowKey !== null && slice.some((r) => rowKey(r) === focusRowKey)\n ? focusRowKey\n : slice.length\n ? rowKey(slice[0])\n : null;\n\n // When dialog-mode is on, the expanded row's detail goes into a bottom-sheet\n // modal instead of unfolding inline. Only one row is ever expanded at a time.\n const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;\n const mobileDialogContent = mobileDialogRow ? openedRowContent(mobileDialogRow) : null;\n // The full-screen row dialog. Its panel, header, scroll lock and Back handling all\n // live in {@link FullBleedDialog} now — this was the only copy of that shell until\n // the transactions create card needed the same one (Keksdose live #307's follow-up),\n // and two copies of a dialog is how the row editor and the create form come to look\n // like two different products.\n const dialogOpen = !!(mobileDialogRow && mobileDialogContent);\n const closeDialog = () => {\n if (mobileDialogRow) onRowClick?.(mobileDialogRow);\n };\n\n // One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list\n // and the grouped list (below) share identical row markup.\n const renderMobileRow = (row: T) => (\n <MobileRow\n key={rowKey(row)}\n row={row}\n isExpanded={isExpanded}\n openedRowContent={openedRowContent}\n opensRow={opensRow}\n rowClassName={rowClassName}\n rowAttributes={rowAttributes}\n onRowClick={onRowClick}\n rowHref={rowHref}\n swipeActionsFor={swipeActionsFor}\n mobileDir={mobileDir}\n mobileCardLinkable={mobileCardLinkable}\n mobileCard={mobileCard}\n mobilePrimaryCol={mobilePrimaryCol}\n mobileSecondaryColumns={mobileSecondaryColumns}\n mobileExpandAsDialog={mobileExpandAsDialog}\n compact={compact}\n large={large}\n />\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-subtle\", compact ? \"px-2 py-1.5\" : \"px-3 py-2\")\n : \"pb-3\",\n )}\n >\n {toolbar}\n </div>\n )}\n {!isMdUp && (\n <div ref={mobileRootRef} className={cn(fillHeight && \"flex min-h-0 flex-1 flex-col\")}>\n {/* Mobile filter access (feedback #299): the per-column filter popovers\n live in the desktop header, which the card list doesn't render — so\n expose the same filters through a bottom sheet here. */}\n {(!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-subtle\",\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-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide 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-muted\",\n compact ? \"px-3 py-3 text-xs\" : \"px-4 py-6 text-sm\",\n )}\n >\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </li>\n )}\n {/* Endless-scroll sentinel: observed by IntersectionObserver to pull in\n the next chunk as it nears the viewport (feedback #232). */}\n {canRevealMoreMobile && (\n <li\n ref={loadMoreRef}\n className=\"px-4 py-4 text-center text-xs text-placeholder\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\n )}\n </ul>\n {/* The phone's totals: a summary card after the list rather than one more\n `<li>` in it. The list reveals rows in chunks as it is scrolled, so a last\n item would sit mid-list until the end was reached; outside it, and under\n `fillHeight` outside the scroller too, the totals stay in view the way the\n sticky `<tfoot>` keeps them in view on a wide screen. Named by\n `footerLabel` so it is announced as what it is, not as a stray list of\n numbers; `mobileHidden` columns stay hidden here as on the cards. */}\n {showFooter && mobileColumns.some((c) => c.footer) && (\n <section\n data-table-footer=\"\"\n aria-labelledby={footerLabel != null ? footerHeadingId : undefined}\n className={cn(\n \"border-t-2 border-strong bg-surface-2\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n {footerLabel != null && (\n <div id={footerHeadingId} className={cn(\"font-semibold\", compact ? \"mb-1 text-sm\" : \"mb-2\")}>\n {footerLabel}\n </div>\n )}\n <dl\n className={cn(\n \"grid grid-cols-[auto_1fr] gap-x-3\",\n compact ? \"gap-y-0.5 text-xs\" : \"gap-y-1 text-sm\",\n )}\n >\n {mobileColumns\n .filter((c) => c.footer)\n .map((col) => (\n <Fragment key={col.key}>\n <dt className=\"self-center text-xs uppercase tracking-wide text-muted\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-primary\">\n {footerValue(col)}\n </dd>\n </Fragment>\n ))}\n </dl>\n </section>\n )}\n {/* Client-side mobile lists scroll endlessly (sentinel above); only\n server-paginated tables keep the pager here. */}\n {isServer && (\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 DataTableHead, data-table-head.tsx). */}\n <DataTableHead\n visibleColumns={visibleColumns}\n selection={selection}\n compact={compact}\n labels={labels}\n locale={locale}\n isServer={isServer}\n hasSortCallback={!!onSortsChange}\n hasFilterCallback={!!onFiltersChange}\n sorts={sorts}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n multiSort={multiSort}\n resizable={resizable}\n widths={widths}\n setWidths={setWidths}\n headRefs={headRefs}\n toggleSort={toggleSort}\n setFilterValue={setFilterValue}\n clearFilter={clearFilter}\n startResize={startResize}\n moveResize={moveResize}\n finishResize={finishResize}\n />\n <tbody\n className={cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\")}\n >\n {leadingRow && (\n <tr className=\"border-t border-subtle\">\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 const detailId = `${detailIdBase}-${rowIndex}`;\n // Whether the row is open, said where ARIA allows it (round 0.34, the\n // showcase's axe check): `aria-expanded` belongs to a treegrid's row, not a\n // table's, and the <tr> wore it. A disclosure button in the first cell carries\n // it now, with `aria-controls` on the detail row while that is there.\n // Visually hidden and not a tab stop: the row stays the one roving stop, with\n // Enter/Space and the whole-row click — this is the screen reader cursor's\n // way in, and where it hears the state. Named by `aria-label`, so the cell's\n // text stays the cell's own.\n const disclosure =\n rowsFocusable && expandedRow ? (\n <button\n type=\"button\"\n tabIndex={-1}\n className=\"sr-only-fixed\"\n aria-label={labels.rowDetails}\n aria-expanded={expanded}\n aria-controls={expansion ? detailId : undefined}\n // The row's own handler leaves a click on a control inside it alone.\n onClick={() => onRowClick!(row)}\n />\n ) : null;\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 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-subtle\",\n rowInteractive && \"cursor-pointer hover:bg-hover\",\n // The kit's focus frame width (§5, `--focus-ring-width`), drawn as an\n // OUTLINE rather than FOCUS_RING's box-shadow: a shadow on a `<tr>`\n // is not painted by every engine, an outline is.\n rowsFocusable &&\n \"focus-visible:bg-hover focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-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, colIndex) => {\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 {colIndex === 0 && disclosure}\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 id={disclosure ? detailId : undefined} className=\"bg-surface-2 border-t border-subtle\">\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-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-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 <ColumnSettingsPanel\n columns={columns}\n hiddenCols={hiddenCols}\n setHiddenCols={setHiddenCols}\n showSettings={showSettings}\n setShowSettings={setShowSettings}\n columnsCountLabel={columnsCountLabel}\n labels={labels}\n autoSizeAll={autoSizeAll}\n />\n )}\n </div>\n )}\n </Root>\n );\n}\n\n/** The framed root: the kit's flush Card, as every table had before `frame`. */\nfunction FramedRoot({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <Card flush {...rest}>\n {children}\n </Card>\n );\n}\n\n/** The unframed root (`frame={false}`): no border, surface, radius or shadow — the\n * host's own card supplies them. */\nfunction PlainRoot(props: React.HTMLAttributes<HTMLDivElement>) {\n return <div {...props} />;\n}\n\n/** The loading row's content: a spinner beside the word, the word for everyone. */\nfunction LoadingText({ label }: { label: string }) {\n // `<bdi>`: the label takes its direction from its own letters, not the table's. The\n // English default inside a `dir=\"rtl\"` table otherwise had its trailing \"…\" — a\n // neutral with no letter after it — resolved right-to-left and drawn in front of\n // the word: \"…Loading\". An Arabic or Hebrew label still reads right-to-left.\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{label}</bdi>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table.tsx"],"sourcesContent":["import { Fragment, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { Card } from \"./card\";\nimport { Spinner } from \"./spinner\";\nimport { ROW_ACTIONS_KEY, useRowActionColumn } from \"./data-table-row-actions\";\nimport { useColumnResize } from \"./data-table-resize\";\nimport { useRangeSelection } from \"./data-table-selection\";\nimport {\n RowLink,\n cleanAttrs,\n columnLabel,\n fromOutside,\n logicalAlign,\n resolveDataTableLabels,\n splitCellProps,\n} from \"./data-table-shared\";\nimport { MobileFilters, MobileRow, MobileSort, StackedFields } from \"./data-table-mobile\";\nimport { DataTableHead } from \"./data-table-head\";\nimport { ColumnSettingsPanel } from \"./data-table-column-settings\";\nimport { useLargeText } from \"../hooks/use-large-text\";\nimport { cn } from \"../lib/cn\";\nimport { isFilterActive, resolveFilter, rowMatches } from \"./data-table-filters\";\nimport type { ColumnFilter, FilterValue } from \"./data-table-filters\";\nimport { nextSorts } from \"./data-table-sort\";\nimport type { SortCycle, SortDir, SortState } from \"./data-table-sort\";\nimport { Pagination } from \"./data-table-pagination\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from \"./data-table-labels\";\nimport { useKitLabelOverrides, useKitLocale, useKitTableStorageKeyPrefix, type LabelOverride } from \"../i18n/kit-labels\";\nimport { useEdgeFade } from \"../lib/strip-fade\";\nimport { dirOf, type Direction } from \"../lib/direction\";\n\n// ---------- Types ----------\n\nexport interface DataTableColumn<T> {\n key: string;\n header: ReactNode;\n cell: (row: T) => ReactNode;\n sortBy?: (row: T) => string | number | null | undefined;\n filter?: ColumnFilter<T>;\n filterBy?: (row: T) => string;\n className?: string;\n headClassName?: string;\n // Mobile card layout: when no `mobilePrimary` column is set the first\n // visible non-hidden column acts as the primary. The primary cell renders\n // bold at the top of the card without a label; the rest stack below as\n // labelled key/value pairs. `mobileHidden` skips a column entirely on\n // narrow viewports — useful for noisy details (IDs, raw URLs, internal\n // bookkeeping) that would crowd a card without aiding scanning.\n mobilePrimary?: boolean;\n mobileHidden?: boolean;\n /**\n * Set on a column whose cell renders its own `<a>` (or anything else that must\n * keep its own click): the row anchor of `rowHref` then skips it and lands on\n * the next column that allows wrapping (Keksdose feedback #451). Without it the\n * fallback can wrap such a cell the moment the headline column is hidden from\n * the settings panel — `<a>` inside `<a>`, which React warns about and which\n * kills the inner link outright, since RowLink cancels the click on the capture\n * phase and the cell's own `stopPropagation` then blocks the row handler too.\n * The rejected alternative was sniffing the rendered output for an anchor: a\n * cell that renders a link is a fact its author knows and the renderer cannot\n * see without reaching into React elements it deliberately treats as opaque.\n */\n noRowLink?: boolean;\n /**\n * The direction this column sorts in on its FIRST click. `\"desc\"` for money and\n * count columns, where the rows worth reading are the large ones and an ascending\n * first click opens on the zero rows (keksdose's aggregated report tables). The\n * rest of the cycle follows from it — see {@link DataTableProps.sortCycle}.\n * Default `\"asc\"`.\n */\n firstSort?: SortDir;\n /**\n * The column's name as plain text, for the places a ReactNode `header` cannot go:\n * the phone sort control's options ({@link DataTableProps.mobileSort}), the spoken\n * \"Sorted by …\" and the column-settings checklist. Only needed when `header` is not\n * a string (an icon, a unit suffix); without it those places fall back to the\n * header string, then to `key`.\n */\n headerText?: string;\n /**\n * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a\n * `title`, an `aria-describedby`, a class. keksdose's accounts page\n * (accounts-page.tsx, the name and IBAN columns) is why it exists: its demo mode\n * blurs everything marked `data-private`, and with nowhere to put the marker on the\n * cell it wrapped every value in an inner `<span data-private>` — which blurs the\n * text but not the cell, and has to be remembered in every `cell` renderer.\n *\n * MERGED under the kit's own, which win: `className` is joined before the column's\n * `className` (so the column's alignment is still the alignment), `style` is laid\n * under a resized column's pinned width, and `data-col` and a `role` stay the\n * table's — `data-col` is how a resize finds the column's cells, and a `<td>` that\n * claims another role leaves its row's cell count wrong for a screen reader.\n *\n * On a phone the card field (`<dd>`, or the primary line) gets the `data-*`\n * attributes only: the rest — padding classes, `colSpan`, a width — describe a\n * table cell and would mean something else, or nothing, on a card.\n */\n cellProps?: (row: T) => DataTableCellProps | undefined;\n /** The same for the column's `<th>`, once. `scope`, `aria-sort`, `data-col` and the\n * width a resize pins stay the table's, and the class list goes before\n * `headClassName` for the same reason as in {@link cellProps}. */\n headProps?: DataTableHeadProps;\n /**\n * This column's cell in the table's totals row — a `<tfoot>` on a wide screen, a\n * labelled summary card under the list on a phone. kastlan's trial balance is why\n * it exists: it hand-built a debit/credit/balance strip in a `div` UNDER the table,\n * which lined up with no column, dropped out of the table's semantics, and stayed\n * put when the user hid or resized the columns it was summing.\n *\n * `rows` is every row that passes the current filters (in the current sort order),\n * NOT just the page on screen: a total that changes when the user pages is not a\n * total, and the trial balance has to balance on page 1 as much as on page 7. The\n * page is the second argument for the rare \"subtotal of this page\" footer.\n *\n * Under `serverPagination` the table only ever holds one page, so both arguments are\n * that page — a server-side table computes its totals on the server and returns\n * them here (`footer: () => fmt(response.totalDebit)`), ignoring the arguments.\n *\n * The cell takes the column's `className` (so a `text-end` amount column totals\n * end-aligned), its resized width, and drops out with the column when it is hidden.\n */\n footer?: (rows: T[], page: T[]) => ReactNode;\n}\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** What {@link DataTableColumn.cellProps} may return. */\nexport type DataTableCellProps = TdHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\n/** What {@link DataTableColumn.headProps} takes. */\nexport type DataTableHeadProps = ThHTMLAttributes<HTMLTableCellElement> & DataAttributes;\n\nexport interface ServerPagination {\n page: number; // 0-based\n pageSize: number;\n total: number;\n onPageChange: (page: number) => void;\n /** Without it the page size is the server's to choose, and the pager shows no\n * page-size select at all — one that changed nothing was a control that lied. */\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * A page is being fetched. The table marks itself `aria-busy`, dims the rows it is\n * still showing (they are the PREVIOUS page's, and about to go), and replaces the\n * empty state with a loading row when there is nothing to show yet — \"no results\"\n * while the first page is still in flight is a claim the table cannot make.\n *\n * The pager stays usable. Disabling it would drop focus from the very button the\n * user just pressed (a disabled button cannot hold focus), and a second click\n * during a fetch is a request the owner already has to handle for a slow network.\n */\n isLoading?: boolean;\n}\n\nexport interface DataTableProps<T> {\n rows: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string | number;\n defaultPageSize?: number;\n onRowClick?: (row: T) => void;\n /**\n * The URL that shows this row, when one exists — the table then renders a REAL\n * anchor for it (Keksdose feedback #451, \"to be able to middle click with the\n * mouse and open in a new tab\"). A row that opens through `onRowClick` alone is\n * invisible to the browser: no middle click, no ⌘/Ctrl-click, no \"copy link\n * address\", no status-bar preview, no long-press menu on a phone. The\n * alternative was an `onAuxClick` that calls `window.open` — it buys back only\n * the middle click and loses the other four, none of which JavaScript can fake.\n *\n * Return `undefined` for a row that has no address of its own. Only wire this up\n * where the OPEN row is genuinely in the URL (a `?row=` param, a detail route);\n * a row whose expansion lives in local component state has no link to hand out,\n * and a link the target page cannot honour is worse than no link.\n *\n * The anchor covers the row's primary cell, not the whole `<tr>`: an `<a>` may\n * not wrap table cells, and the absolutely-positioned row overlay that is the\n * usual workaround would sit on top of the per-cell controls these rows carry\n * (status toggles, the selection checkbox, nested links) and swallow them.\n */\n rowHref?: (row: T) => string | undefined;\n expandedRow?: (row: T) => ReactNode | null;\n isExpanded?: (row: T) => boolean;\n /**\n * Extra classes for a row — the `<tr>` on desktop, the card on mobile. Merged last,\n * so a `hover:` fill here replaces the row's own `hover:bg-hover`.\n *\n * **A row you paint has to pick its own hover** (0.33, docs/colour-roles-harmonization.md\n * §12.2): the kit cannot see your rest colour, and `--bg-hover` is mixed against the\n * card. The rule: a hover differs from its rest by 1.08:1 or more, and every text role\n * on it keeps 4.5:1.\n * - A row that rests on the well (`bg-surface-2`) hovers to the CARD,\n * `hover:bg-surface`: in light the well is the page, and `--bg-hover` sits 1.04:1\n * from it.\n * - A row whose well MARKS A STATE (selected) keeps its fill and draws a 1 px inside\n * line instead, since the card would read as \"not selected\":\n * `bg-surface-2 hover:bg-surface-2 hover:outline hover:-outline-offset-1 hover:outline-strong`.\n * An outline, not a shadow: a shadow on a `<tr>` is not painted by every engine.\n * - Not `--bg-active` for a selected row: its muted and money text fall to 3.8–4.7:1.\n */\n rowClassName?: (row: T) => string | undefined;\n /**\n * Extra DOM attributes for a row's container — the `<tr>` on desktop, the\n * `<li>` on mobile. For hooks that have to sit on the row itself rather than\n * inside a cell: guided-tour anchors, analytics ids, e2e selectors. Undefined\n * values are dropped, so a per-row conditional needs no filtering at the call\n * site. Not a styling escape hatch — that is `rowClassName`.\n */\n rowAttributes?: (row: T) => Record<string, string | undefined> | undefined;\n /**\n * Set false for a table that is short by construction — an invoice's line\n * items, a wizard's picks — where the pager is chrome around a list that can\n * never need one. Renders every row and drops the footer entirely. Ignored in\n * `serverPagination` mode, where the pager is the only way to reach the rest\n * of the data.\n */\n paginated?: boolean;\n empty?: ReactNode;\n /**\n * If set, the table persists filter / sort / page-size state to localStorage under this key.\n * Use a stable, unique key per table (e.g. \"transactions-table\").\n */\n storageKey?: string;\n /**\n * When true, mirror filter/sort/page state into URL search params so views are\n * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.\n * URL state takes precedence over localStorage on initial load. Requires the\n * consuming app to render inside a react-router Router.\n *\n * `{ prefix }` namespaces them, for two URL-synced tables on one page:\n * `urlSync={{ prefix: \"inv.\" }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.\n */\n urlSync?: TableUrlSync;\n /**\n * When true, the table fills its parent's height and scrolls INTERNALLY, with the\n * header pinned — so the page itself doesn't add a second, outer scrollbar. 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 * The cure for TWO SCROLLBARS (round 0.34 C14, kastlan feedback 55): the default\n * `maxBodyHeight` scrolls the body inside a page that scrolls too. How a page fills\n * the height is in {@link DataTableProps.maxBodyHeight}'s note.\n */\n fillHeight?: boolean;\n /** How tall the scrolling body may get before it scrolls, as a CSS length.\n *\n * Default `calc(100dvh - 12rem)`, which is what every caller had before this\n * existed: a bound is what makes the sticky header work, because a header\n * can only stick to a wrapper that actually scrolls. The cap assumes the\n * table has page chrome above and below it, and a table that is the last\n * thing on its page can afford more — ASPICE Atlas report #12, \"for the 25\n * elements there should in best case be no scrolling necessary\".\n *\n * Ignored under `fillHeight`, which bounds the body by its flex parent\n * instead and is the stronger statement of the same intent.\n *\n * **Two scrollbars** (round 0.34 C14, kastlan feedback 55). The cap scrolls the\n * body INSIDE a page that, in most shells, scrolls itself: a table taller than\n * the cap on a page with anything else on it shows two scrollbars side by side,\n * and the wheel moves whichever is under the pointer. For a list page whose table\n * is the point, fill the height instead, from `md` up (a phone scrolls the page\n * and its card list as one):\n *\n * 1. the shell's content column becomes a flex column of the main area's height\n * when the page asks — kastlan's\n * `md:has-[>[data-fill-height]]:flex md:has-[>[data-fill-height]]:h-full md:has-[>[data-fill-height]]:flex-col`;\n * 2. the page's root asks, and is a flex column that may shrink:\n * `data-fill-height` and `md:flex md:min-h-0 md:flex-1 md:flex-col`;\n * 3. the table takes the rest: `<DataTable fillHeight>`.\n *\n * Only the body then scrolls, under its pinned header, and every other page grows\n * with its content as before (kastlan e3985c9, its leases list). A short table that\n * is the last thing on its page can instead drop the cap: `maxBodyHeight=\"none\"`. */\n maxBodyHeight?: string;\n /**\n * When provided, the table renders the supplied rows as-is (no client-side\n * filtering/sorting/slicing) and the pagination footer is driven by these\n * server-controlled values. Column-level filters and sorting are hidden\n * UNLESS the caller takes them over via `onFiltersChange`/`onSortsChange` —\n * the caller is then expected to translate the state into the server query\n * that produces `rows`.\n */\n serverPagination?: ServerPagination;\n /**\n * Controlled filter/sort state. When the `on*Change` callback is provided\n * the table stops owning that piece of state: it renders `filters`/`sorts`\n * as given and reports user interactions through the callback (including\n * in serverPagination mode, where the UI is otherwise disabled).\n *\n * Page reset: on a client-side table, a change in the VALUE of `filters` or\n * `sorts` sends the table back to page 1 — whoever made the change, the user\n * through the header or the owner from outside (a saved view, a chip). Compared\n * by value, so passing a fresh but equal object on every render does not reset\n * anything. In `serverPagination` mode the owner's `page` is the page, and\n * resetting it on a filter change stays the owner's job.\n */\n filters?: FilterState;\n onFiltersChange?: (next: FilterState) => void;\n sorts?: SortState[];\n onSortsChange?: (next: SortState[]) => void;\n /**\n * Controlled page size, as `sorts` above: the table pages by it and hands its pager's\n * choice to `onPageSizeChange` (`Infinity` is \"All\"). For tables that page ALIKE —\n * `TranslationReviewPanel`'s groups share one page size as they share one sort\n * (0.35). Left out, the table keeps its own, from `defaultPageSize`, the URL and\n * `storageKey`. A change of value sends a client-side table back to page 1. In\n * `serverPagination` mode the page size is `serverPagination.pageSize`.\n */\n pageSize?: number;\n onPageSizeChange?: (pageSize: number) => void;\n /**\n * Opt-in multi-row selection (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?: LabelOverride<DataTableLabels>;\n /** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls\n * back to `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** localStorage namespace prefix for `storageKey` persistence: the stored key is this\n * plus `storageKey`. Falls back to `<UiKitProvider tableStorageKeyPrefix>` (0.34, set\n * your app's prefix there once), then to \"hbui-table:\". This prop wins over both. */\n storageKeyPrefix?: string;\n /**\n * On phones, surface the expanded row's detail in a full-screen dialog instead\n * of unfolding it inline (feedback #204). Desktop always uses inline expansion.\n */\n mobileExpandAsDialog?: boolean;\n /**\n * Show the `mobileHidden` columns in the OPENED row on the phone layout — inline\n * under the card, or in the `mobileExpandAsDialog` dialog — so a column dropped from\n * the card to keep it short is not lost (0.32, docs/text-size-harmonization.md §4:\n * \"a new `mobileDetailsInRow` puts the `mobileHidden` columns into the opened row, so\n * nothing is lost\"). At Large a card holds fewer facts per screen, and dropping one\n * from the card must not drop it from the phone.\n *\n * Each column is a label above its value, BEFORE what `expandedRow` renders: the\n * row's own facts first, then the app's detail or editor. A row opens as it always\n * does (`onRowClick` toggling `isExpanded`); with this set it may open without an\n * `expandedRow` at all. `rowActions`' column is one of them — the actions, which the\n * card otherwise offers only as swipes, reachable by a tap. Desktop is unaffected.\n */\n mobileDetailsInRow?: boolean;\n /**\n * The row's name, as plain text — for the \"⋯\" menu its `rowActions` collapse into at\n * Large (\"Actions for Ada Example\", §10.8). Without it every row's menu button is\n * named \"Actions\", which a screen reader cannot tell apart down the list.\n */\n rowName?: (row: T) => string | undefined;\n /**\n * The phone row dialog's `backCloses` ({@link FullBleedDialog}): whether it pushes\n * its own history entry so Back dismisses it. On by default — right for a row whose\n * expansion is component state, where nothing else would answer Back.\n *\n * **Set it `false` when the open row is in the URL** and opening it PUSHES (a\n * `?row=` written with `useSearchParamState(…, { history: \"replace-on-clear\" })` or\n * `\"push\"`): Back already closes the row through the router, and a second entry on\n * top of the page's is two owners of one Back press (keksdose dev #584). Keep it on\n * when opening the row REPLACES the URL — then nothing was pushed, and the dialog's\n * own entry is the only thing Back can pop.\n *\n * The table cannot decide this for you: `isExpanded` is a predicate, and neither it\n * nor `rowHref` says whether opening pushed or replaced (keksdose's feedback page\n * has `rowHref` with a REPLACING `?row=`, and needs the entry). Ignored unless\n * `mobileExpandAsDialog`.\n */\n mobileDialogBackCloses?: boolean;\n /**\n * Group the mobile card list into sections, each with a sticky header — the\n * \"assistance\"-style list (like the transactions list grouped by date, but by\n * whatever key you return, e.g. a name's first letter). The returned string is\n * the section a row belongs to; rows must already be ordered so equal keys are\n * contiguous (pair with a matching default sort). `mobileGroupLabel` formats\n * the header. Desktop is unaffected. Feedback #317.\n */\n mobileGroupBy?: (row: T) => string;\n mobileGroupLabel?: (key: string) => ReactNode;\n /**\n * Fully replace the default mobile card body (bold primary + labelled\n * key/value rows) with a compact custom layout, while keeping the shared\n * clickable/expandable row wrapper. Use when the stacked label/value grid\n * wastes space (feedback #317). Desktop is unaffected.\n *\n * It replaces the WHOLE body, `mobilePrimary` included: a column's\n * `mobilePrimary` cell is not drawn when `mobileCard` is set, so anything that\n * cell carried (keksdose's environment and rework chips, 0.27) must be drawn by\n * the card itself — as `FeedbackMobileCard` does.\n */\n mobileCard?: (row: T) => ReactNode;\n /**\n * A \"Sort by\" select and a direction button above the phone card list (keksdose\n * K10). The card list has no header row, so without it a phone user is stuck with\n * whatever order the table opened in. It drives the same sort state as the column\n * headers — `sorts`/`onSortsChange` when controlled, the URL and `storageKey` when\n * not — and offers the columns a header click could sort (those with a `sortBy`).\n * Each option is named by the column's `headerText`, else its string `header`.\n *\n * Off by default, so no existing phone layout gains a row. Strings:\n * {@link DataTableSortLabels}. Phones only; the desktop table is unchanged.\n */\n mobileSort?: boolean;\n /**\n * Swipe actions for a mobile row, revealed by dragging it horizontally. Return\n * `null` (or empty sides) for rows that should not move — a locked row, one whose\n * mutation is in flight, one the user has expanded.\n *\n * Prefer `start`/`end`: actions committed by dragging the row toward the reading\n * START or END of the line. In a left-to-right layout `end` is a drag to the\n * right, and in a right-to-left one it is a drag to the left — so a \"drag forward\n * to archive\" row stays forward in Arabic or Hebrew without a second definition.\n * `left`/`right` are the physical directions and keep meaning exactly that; each\n * is used only where the logical side for that direction is not given.\n *\n * Mobile only, and deliberately so: the desktop table already has room for an\n * actions column, and a drag gesture on a pointer device is a worse version of a\n * button. Wraps only the row body, so an expansion panel below stays put while the\n * row above it slides.\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; with `mobileDetailsInRow` also as buttons in the opened row). At Large and\n * Extra large two or more collapse into one \"⋯\" menu with each action's words\n * (0.32, §10.8, `RowActions`) — name the rows with `rowName` so each menu button is\n * \"Actions for …\". kastlan's `ResourceListPage` appended this column by hand to every CRUD\n * list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a\n * `stopPropagation` on each so a press did not also open the row.\n *\n * `kind: \"edit\"` / `\"delete\"` bring the icon, the label (`labels.edit` /\n * `labels.delete`), the tone and the swipe side; any other action states its own\n * `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's\n * `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.\n *\n * const confirm = useConfirm();\n * rowActions={[\n * { kind: \"edit\", onAction: openEditor },\n * { kind: \"delete\", onAction: (r) => remove(r.id),\n * confirm: (r) => confirm({ title: t(\"delete\", { name: r.name }), tone: \"danger\" }) },\n * ]}\n *\n * A function receives the row, for actions that differ per row. An explicit\n * `mobileSwipeActions` wins over the swipes derived from here.\n */\n rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);\n /**\n * The bar above the table — a search field, filter chips, a create button. Inside\n * the table's frame, above the header, on desktop and phone alike (resource-list\n * pages set a search box in a `div` above the card, outside the frame it filters).\n */\n toolbar?: ReactNode;\n /**\n * The totals row's name — \"Total\", \"Summe\", \"Totals as of 30 June\". It is the row's\n * header: rendered as a `<th scope=\"row\">` in the first visible column that has no\n * `footer` of its own (falling back to the first visible column if every one has a\n * value), so a screen reader reads \"Total, Debit, 12 400.00\" for the cell beside it.\n * On a phone it heads the summary card, which is also named by it.\n *\n * There is no built-in default word: a new kit label key is a silent English string\n * in every translated app (see `missingKitLabels`), and the caller has the\n * word in its own catalogue already. Without it the row is unlabelled on screen —\n * pass one.\n */\n footerLabel?: ReactNode;\n /**\n * Show the totals row. Default: shown when any column declares a `footer`. Set\n * false to drop it without touching the column definitions (a \"show totals\"\n * switch). The row is also left out while there are no rows to total — a row of\n * zeroes under \"No results\" says nothing the empty state has not.\n */\n footer?: boolean;\n}\n\n/** One entry of {@link DataTableProps.rowActions}. */\nexport interface DataTableRowAction<T> {\n /** A preset: `\"edit\"` (pencil, `labels.edit`, swipes toward the END) or `\"delete\"`\n * (bin, `labels.delete`, danger tone, swipes toward the START). */\n kind?: \"edit\" | \"delete\";\n /** The action's name — its tooltip, accessible name and swipe label. Required\n * without a `kind`. */\n label?: string;\n /** The glyph. Required without a `kind`. */\n icon?: ReactNode;\n /** Runs after `confirm` (when given) has said yes. */\n onAction: (row: T) => void;\n /** Asked before `onAction`; a `false` (or a promise of one) cancels. */\n confirm?: (row: T) => boolean | Promise<boolean>;\n /** Default: `\"danger\"` for `kind: \"delete\"`, else `\"default\"`. */\n tone?: \"default\" | \"danger\";\n /** Leave the action out for this row. */\n hidden?: (row: T) => boolean;\n /** Why the action is locked for this row — the button stays focusable and says so,\n * and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */\n disabledReason?: (row: T) => ReactNode;\n /** Which way the phone card is dragged to commit it, or `false` for no swipe.\n * Default: `\"start\"` for a danger action, `\"end\"` otherwise. */\n swipe?: \"start\" | \"end\" | false;\n}\n\n/** See {@link DataTableProps.chrome}. */\nexport type DataTableChrome = \"full\" | \"minimal\";\n\n/** See {@link DataTableProps.density}. */\nexport type DataTableDensity = \"comfortable\" | \"compact\";\n\nexport type FilterState = Record<string, FilterValue>;\nexport type { SortState };\n\n/** What {@link DataTableProps.mobileSwipeActions} returns for one row. */\nexport interface MobileSwipeActions {\n /** Committed by dragging toward the reading start (left in LTR, right in RTL). */\n start?: SwipeAction[];\n /** Committed by dragging toward the reading end (right in LTR, left in RTL). */\n end?: SwipeAction[];\n /** Committed by dragging physically LEFT, whatever the direction. */\n left?: SwipeAction[];\n /** Committed by dragging physically RIGHT, whatever the direction. */\n right?: SwipeAction[];\n}\n\n/**\n * A totals-row cell: pinned to the scroller's bottom edge, on the header's surface.\n * The rule above it is an inset shadow rather than a `border-t`: the table collapses\n * its borders, and a collapsed border belongs to the TABLE, not the cell, so it stays\n * behind at the bottom of the rows while the sticky cell rides over them.\n */\nconst FOOTER_CELL =\n \"sticky bottom-0 z-10 bg-surface-2 shadow-[inset_0_2px_0_var(--border-strong)]\";\n\n// ---------- Phone sort control (keksdose K10) ----------\n\n/**\n * The phone sort control's strings (keksdose K10). They are `dataTable` strings —\n * read from the table's resolved labels, so the `labels` prop and the provider's\n * `dataTable` namespace reach them — and are declared here, with the table's other\n * public types (the control itself, `MobileSort`, is in data-table-mobile.tsx), with\n * English defaults until `DataTableLabels` declares them itself.\n */\nexport interface DataTableSortLabels {\n /** The control's visible label: \"Sort by\". */\n sortBy: string;\n /** The \"no sort\" option — the rows in the order they were given: \"Default order\". */\n sortDefault: string;\n /** The direction button's name while the sort ascends: \"Ascending\". */\n sortAscending: string;\n /** …and while it descends: \"Descending\". */\n sortDescending: string;\n}\n\n/** The phone sort control's words, as they sit in {@link DEFAULT_DATA_TABLE_LABELS}\n * (DataTableLabels extends DataTableSortLabels; one source for the English). */\nexport const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {\n sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,\n sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,\n sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,\n sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending,\n};\n\nconst flipDir = (d: SortDir): SortDir => (d === \"asc\" ? \"desc\" : \"asc\");\n\n/**\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/** 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// ---------- 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 pageSize: pageSizeProp,\n onPageSizeChange,\n selection,\n labels: labelsProp,\n locale: localeProp,\n storageKeyPrefix: storageKeyPrefixProp,\n mobileExpandAsDialog = false,\n mobileDetailsInRow = false,\n rowName,\n mobileDialogBackCloses = true,\n mobileGroupBy,\n mobileGroupLabel,\n mobileCard,\n mobileSort = false,\n mobileSwipeActions,\n 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 // prop > provider > the kit's default, the order every kit setting resolves in (round\n // 0.34 C5, KS U4): one prefix on the provider reaches the tables an app cannot pass a\n // prop to, `TranslationReviewPanel`'s among them.\n const providerStorageKeyPrefix = useKitTableStorageKeyPrefix();\n const storageKeyPrefix = storageKeyPrefixProp ?? providerStorageKeyPrefix ?? DEFAULT_PERSIST_PREFIX;\n const labels = useMemo(() => resolveDataTableLabels(labelOverrides, labelsProp), [labelOverrides, labelsProp]);\n const locale = useKitLocale(localeProp);\n\n // ---- Row actions ---- (data-table-row-actions.tsx)\n // The actions column joins the caller's columns; on a phone the same actions are the\n // card's swipes, unless the caller's own `mobileSwipeActions` says otherwise.\n const { columns, swipesFromActions } = useRowActionColumn({\n columns: columnsProp,\n rowActions,\n rowName,\n labels,\n density,\n });\n const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);\n\n // Sorting, filtering and paging all change WHICH rows are on screen without\n // moving focus — the header button the user pressed is still the header button\n // they are on — so there is no other channel to say it on. See use-announce.ts.\n const { announce, regionProps } = useAnnounce();\n const {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths: storedWidths,\n setWidths,\n hiddenCols: storedHiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n } = useTableState({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync,\n defaultPageSize,\n sortsProp,\n filtersProp,\n // Ignored in `serverPagination` mode, where the owner's is the page size.\n pageSizeProp: isServer ? undefined : pageSizeProp,\n });\n\n // With the controls that change them switched off, persisted widths and hidden\n // columns are ignored rather than applied: a table cannot be left in a state the\n // user has no control to get out of. They stay in storage untouched, so turning\n // the control back on brings them back.\n 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 ---- (data-table-resize.ts: pointer capture, one `setWidths` per\n // drag, and the page's cursor and selection handed back on an unmount mid-drag)\n const { startResize, moveResize, finishResize, autoSizeAll } = useColumnResize({\n headRefs,\n widths,\n setWidths,\n columns,\n hiddenCols,\n });\n\n const visibleCount = visibleColumns.length;\n const totalCount = columns.length;\n // Spans the full row width including the optional leading selection column.\n const totalColSpan = visibleCount + (selection ? 1 : 0);\n\n // ---- Totals row ----\n // `sorted`, not `slice`: the totals are of everything the filters let through, so\n // paging does not move them — see `DataTableColumn.footer`. In server mode `sorted`\n // IS the page (the table holds nothing else), which the prop's note spells out.\n const hasFooterColumns = columns.some((c) => c.footer);\n const showFooter = footerProp !== false && hasFooterColumns && sorted.length > 0;\n // The row header sits in the first visible column that has nothing of its own to\n // show — so hiding the \"Account\" column moves \"Total\" into \"Name\" rather than\n // dropping it — and on the first visible column when every one carries a value.\n const footerLabelCol = showFooter\n ? (visibleColumns.find((c) => !c.footer && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0])\n : undefined;\n const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);\n const footerHeadingId = useId();\n // Each open row's detail row, for its disclosure button's `aria-controls`.\n const detailIdBase = useId();\n // The actions column is the table's own, not one the user can hide: out of the count.\n const columnsCountLabel = rowActions\n ? labels.columnsCount(visibleCount - 1, totalCount - 1)\n : labels.columnsCount(visibleCount, totalCount);\n // Match the `md:` breakpoint at the text size in force: we render either the table or\n // the card list — never both — so we don't double up DOM nodes that screen readers and\n // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin\n // `useBreakpoint(\"md\")`: the same query an app's own phone checks ask (keksdose's\n // sticky editor footer), so the card list and the full-screen row dialog switch with\n // them and never a frame apart (§10.4 \"usePhoneLayout() is the single answer\"; 0.32.1,\n // keksdose's 0.32 report, where `md` and `max-md` both missed at 767.5 px). Without\n // `matchMedia` (SSR, jsdom) it is the table, as before.\n const isMdUp = !usePhoneLayout();\n // `edgeFade` (0.25) on the desktop scroller; the phone list never scrolls sideways.\n const desktopScroller = useRef<HTMLDivElement | null>(null);\n 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 columns the card leaves out, for the opened row (`mobileDetailsInRow`).\n const mobileDetailColumns = mobileDetailsInRow ? visibleColumns.filter((c) => c.mobileHidden) : [];\n // Large and Extra large (§4 \"DataTable phone card\"): the card's labels go ABOVE their\n // values, nothing in it truncates, and the rows get more room. Most of it is a\n // `large:` class; the label/value grid needs a different STRUCTURE (each pair in a\n // box of its own), which a class cannot write.\n const large = useLargeText();\n // What an opened phone row shows: the card's left-out columns first (when asked\n // for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.\n const opensRow = !!expandedRow || mobileDetailColumns.length > 0;\n const openedRowContent = (row: T): ReactNode => {\n const detail = expandedRow?.(row) ?? null;\n if (mobileDetailColumns.length === 0) return detail;\n return (\n <div data-row-details=\"\" className={cn(\"flex flex-col\", compact ? \"gap-2\" : \"gap-3\")}>\n <StackedFields columns={mobileDetailColumns} row={row} compact={compact} />\n {detail}\n </div>\n );\n };\n // The phone card IS the anchor, so unlike the desktop table there is no other\n // cell to move the row link into: if any cell the card renders owns a link, the\n // card itself cannot be one (feedback #451 — same nesting rule as `linkColumn`\n // above). It then falls back to the pre-#451 role=\"button\" card, which still\n // opens the row and leaves the cell's own link working. A caller's `mobileCard`\n // renderer is opaque to us; a link inside one is the caller's to declare by\n // marking the column it comes from.\n const mobileCardLinkable = mobileColumns.every((c) => !c.noRowLink);\n // The card list's reading direction, for the one thing on it CSS cannot flip: which\n // physical drag a logical `start`/`end` swipe action means. Read off the DOM through\n // a ref callback (it runs at commit, like a layout effect) because `dir` is an\n // attribute of some ancestor the table does not own; LTR until the first commit,\n // which is also before any finger can have touched a row.\n const [mobileDir, setMobileDir] = useState<Direction>(\"ltr\");\n const mobileRootRef = (el: HTMLElement | null) => {\n if (!el) return;\n const d = dirOf(el);\n if (d !== mobileDir) setMobileDir(d);\n };\n\n const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({\n rows: sorted,\n slice,\n isServer,\n isMdUp,\n unpaged,\n defaultPageSize,\n });\n // ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,\n // anchored on the row's KEY so a sort, filter or page change cannot move it)\n const { selectionAnchor, rangeClick, canSelect, selectRange } = useRangeSelection({\n selection,\n slice,\n rowKey,\n });\n // ---- Keyboard-activatable rows ----\n //\n // A desktop row with `onRowClick` opened on a mouse click and on nothing else: no\n // tab stop, no Enter, no focus ring (keksdose's report tables hand-rolled all three,\n // which is half of why they could not move onto this table). The row now takes\n // focus itself, as a ROVING tab stop: one row of the table is in the tab order\n // (the last one focused, else the first), ArrowUp/ArrowDown/Home/End move between\n // rows, Enter and Space activate.\n //\n // Why the `<tr>` rather than the two alternatives:\n // * `role=\"button\"` on the row would replace its `row` role, and the cells with\n // it — a screen reader would stop reading the table as a table.\n // * A button or link in one cell is what `rowHref` already is where the row HAS a\n // URL. Without one, a synthetic button would have to wrap caller-rendered cell\n // content, which nests the controls those cells carry (status toggles, links)\n // inside it — illegal markup, and the reason the mobile card is a div.\n // A focused `<tr>` keeps its role and reads out its cells. Roving rather than a tab\n // stop per row, so a 100-row page does not put 100 stops between the header and\n // the pager.\n const rowsFocusable = !!onRowClick;\n const [focusRowKey, setFocusRowKey] = useState<string | number | null>(null);\n const tabRowKey =\n focusRowKey !== null && slice.some((r) => rowKey(r) === focusRowKey)\n ? focusRowKey\n : slice.length\n ? rowKey(slice[0])\n : null;\n\n // When dialog-mode is on, the expanded row's detail goes into a bottom-sheet\n // modal instead of unfolding inline. Only one row is ever expanded at a time.\n const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;\n const mobileDialogContent = mobileDialogRow ? openedRowContent(mobileDialogRow) : null;\n // The full-screen row dialog. Its panel, header, scroll lock and Back handling all\n // live in {@link FullBleedDialog} now — this was the only copy of that shell until\n // the transactions create card needed the same one (Keksdose live #307's follow-up),\n // and two copies of a dialog is how the row editor and the create form come to look\n // like two different products.\n const dialogOpen = !!(mobileDialogRow && mobileDialogContent);\n const closeDialog = () => {\n if (mobileDialogRow) onRowClick?.(mobileDialogRow);\n };\n\n // One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list\n // and the grouped list (below) share identical row markup.\n const renderMobileRow = (row: T) => (\n <MobileRow\n key={rowKey(row)}\n row={row}\n isExpanded={isExpanded}\n openedRowContent={openedRowContent}\n opensRow={opensRow}\n rowClassName={rowClassName}\n rowAttributes={rowAttributes}\n onRowClick={onRowClick}\n rowHref={rowHref}\n swipeActionsFor={swipeActionsFor}\n mobileDir={mobileDir}\n mobileCardLinkable={mobileCardLinkable}\n mobileCard={mobileCard}\n mobilePrimaryCol={mobilePrimaryCol}\n mobileSecondaryColumns={mobileSecondaryColumns}\n mobileExpandAsDialog={mobileExpandAsDialog}\n compact={compact}\n large={large}\n />\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-subtle\", compact ? \"px-2 py-1.5\" : \"px-3 py-2\")\n : \"pb-3\",\n )}\n >\n {toolbar}\n </div>\n )}\n {!isMdUp && (\n <div ref={mobileRootRef} className={cn(fillHeight && \"flex min-h-0 flex-1 flex-col\")}>\n {/* Mobile filter access (feedback #299): the per-column filter popovers\n live in the desktop header, which the card list doesn't render — so\n expose the same filters through a bottom sheet here. */}\n {(!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-subtle\",\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-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide 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-muted\",\n compact ? \"px-3 py-3 text-xs\" : \"px-4 py-6 text-sm\",\n )}\n >\n {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}\n </li>\n )}\n {/* Endless-scroll sentinel: observed by IntersectionObserver to pull in\n the next chunk as it nears the viewport (feedback #232). */}\n {canRevealMoreMobile && (\n <li\n ref={loadMoreRef}\n className=\"px-4 py-4 text-center text-xs text-placeholder\"\n >\n {/* Isolated for the reason spelled out on LoadingText. */}\n <bdi>{labels.loading}</bdi>\n </li>\n )}\n </ul>\n {/* The phone's totals: a summary card after the list rather than one more\n `<li>` in it. The list reveals rows in chunks as it is scrolled, so a last\n item would sit mid-list until the end was reached; outside it, and under\n `fillHeight` outside the scroller too, the totals stay in view the way the\n sticky `<tfoot>` keeps them in view on a wide screen. Named by\n `footerLabel` so it is announced as what it is, not as a stray list of\n numbers; `mobileHidden` columns stay hidden here as on the cards. */}\n {showFooter && mobileColumns.some((c) => c.footer) && (\n <section\n data-table-footer=\"\"\n aria-labelledby={footerLabel != null ? footerHeadingId : undefined}\n className={cn(\n \"border-t-2 border-strong bg-surface-2\",\n compact ? \"px-3 py-2\" : \"px-4 py-3\",\n isLoading && \"opacity-60 transition-opacity\",\n )}\n >\n {footerLabel != null && (\n <div id={footerHeadingId} className={cn(\"font-semibold\", compact ? \"mb-1 text-sm\" : \"mb-2\")}>\n {footerLabel}\n </div>\n )}\n <dl\n className={cn(\n \"grid grid-cols-[auto_1fr] gap-x-3\",\n compact ? \"gap-y-0.5 text-xs\" : \"gap-y-1 text-sm\",\n )}\n >\n {mobileColumns\n .filter((c) => c.footer)\n .map((col) => (\n <Fragment key={col.key}>\n <dt className=\"self-center text-xs uppercase tracking-wide text-muted\">\n {col.header}\n </dt>\n <dd className=\"min-w-0 self-center font-semibold text-primary\">\n {footerValue(col)}\n </dd>\n </Fragment>\n ))}\n </dl>\n </section>\n )}\n {/* Client-side mobile lists scroll endlessly (sentinel above); only\n server-paginated tables keep the pager here. */}\n {isServer && (\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 DataTableHead, data-table-head.tsx). */}\n <DataTableHead\n visibleColumns={visibleColumns}\n selection={selection}\n compact={compact}\n labels={labels}\n locale={locale}\n isServer={isServer}\n hasSortCallback={!!onSortsChange}\n hasFilterCallback={!!onFiltersChange}\n sorts={sorts}\n filters={filters}\n selectOptionsByKey={selectOptionsByKey}\n multiSort={multiSort}\n resizable={resizable}\n widths={widths}\n setWidths={setWidths}\n headRefs={headRefs}\n toggleSort={toggleSort}\n setFilterValue={setFilterValue}\n clearFilter={clearFilter}\n startResize={startResize}\n moveResize={moveResize}\n finishResize={finishResize}\n />\n <tbody\n className={cn(isLoading && slice.length > 0 && \"opacity-60 transition-opacity\")}\n >\n {leadingRow && (\n <tr className=\"border-t border-subtle\">\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 const detailId = `${detailIdBase}-${rowIndex}`;\n // Whether the row is open, said where ARIA allows it (round 0.34, the\n // showcase's axe check): `aria-expanded` belongs to a treegrid's row, not a\n // table's, and the <tr> wore it. A disclosure button in the first cell carries\n // it now, with `aria-controls` on the detail row while that is there.\n // Visually hidden and not a tab stop: the row stays the one roving stop, with\n // Enter/Space and the whole-row click — this is the screen reader cursor's\n // way in, and where it hears the state. Named by `aria-label`, so the cell's\n // text stays the cell's own.\n const disclosure =\n rowsFocusable && expandedRow ? (\n <button\n type=\"button\"\n tabIndex={-1}\n className=\"sr-only-fixed\"\n aria-label={labels.rowDetails}\n aria-expanded={expanded}\n aria-controls={expansion ? detailId : undefined}\n // The row's own handler leaves a click on a control inside it alone.\n onClick={() => onRowClick!(row)}\n />\n ) : null;\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 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-subtle\",\n rowInteractive && \"cursor-pointer hover:bg-hover\",\n // The kit's focus frame width (§5, `--focus-ring-width`), drawn as an\n // OUTLINE rather than FOCUS_RING's box-shadow: a shadow on a `<tr>`\n // is not painted by every engine, an outline is.\n rowsFocusable &&\n \"focus-visible:bg-hover focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-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, colIndex) => {\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 {colIndex === 0 && disclosure}\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 id={disclosure ? detailId : undefined} className=\"bg-surface-2 border-t border-subtle\">\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-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-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 // To the owner when it holds the page size, as `commitSorts` does.\n if (onPageSizeChange) onPageSizeChange(n);\n else setPageSize(n);\n setPage(0);\n }\n }\n labels={labels}\n locale={locale}\n density={density}\n />\n )}\n </div>\n {columnSettings && (\n <ColumnSettingsPanel\n columns={columns}\n hiddenCols={hiddenCols}\n setHiddenCols={setHiddenCols}\n showSettings={showSettings}\n setShowSettings={setShowSettings}\n columnsCountLabel={columnsCountLabel}\n labels={labels}\n autoSizeAll={autoSizeAll}\n />\n )}\n </div>\n )}\n </Root>\n );\n}\n\n/** The framed root: the kit's flush Card, as every table had before `frame`. */\nfunction FramedRoot({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) {\n return (\n <Card flush {...rest}>\n {children}\n </Card>\n );\n}\n\n/** The unframed root (`frame={false}`): no border, surface, radius or shadow — the\n * host's own card supplies them. */\nfunction PlainRoot(props: React.HTMLAttributes<HTMLDivElement>) {\n return <div {...props} />;\n}\n\n/** The loading row's content: a spinner beside the word, the word for everyone. */\nfunction LoadingText({ label }: { label: string }) {\n // `<bdi>`: the label takes its direction from its own letters, not the table's. The\n // English default inside a `dir=\"rtl\"` table otherwise had its trailing \"…\" — a\n // neutral with no letter after it — resolved right-to-left and drawn in front of\n // the word: \"…Loading\". An Arabic or Hebrew label still reads right-to-left.\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{label}</bdi>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,7 +12,9 @@ export { DEFAULT_DIALOG_FRAME_LABELS };
|
|
|
12
12
|
* (`children` is the body here, not the whole panel) and `title`, which is the dialog's
|
|
13
13
|
* HEADING and a ReactNode rather than the browser's tooltip string — the collision
|
|
14
14
|
* `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,
|
|
15
|
-
* `onKeyDown`, a `data-tour` anchor, still reaches the `Modal
|
|
15
|
+
* `onKeyDown`, a `data-tour` anchor, still reaches the `Modal` — and since 0.35 `ref`
|
|
16
|
+
* (round 0.35 R4): the panel, the `role="dialog"` element the frame's header, body and
|
|
17
|
+
* actions sit in.
|
|
16
18
|
*/
|
|
17
19
|
export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" | "title"> {
|
|
18
20
|
/** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */
|