@eifi1/ui-kit 0.48.0 → 0.48.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +4 -3
  2. package/dist/components/data-table-fit.d.ts +8 -1
  3. package/dist/components/data-table-fit.js +7 -1
  4. package/dist/components/data-table-fit.js.map +1 -1
  5. package/dist/components/data-table-groups.d.ts +3 -1
  6. package/dist/components/data-table-groups.js +11 -5
  7. package/dist/components/data-table-groups.js.map +1 -1
  8. package/dist/components/data-table-head.d.ts +8 -1
  9. package/dist/components/data-table-head.js +11 -3
  10. package/dist/components/data-table-head.js.map +1 -1
  11. package/dist/components/data-table-leading-row.d.ts +3 -1
  12. package/dist/components/data-table-leading-row.js +2 -1
  13. package/dist/components/data-table-leading-row.js.map +1 -1
  14. package/dist/components/data-table-row-actions.js +2 -2
  15. package/dist/components/data-table-row-actions.js.map +1 -1
  16. package/dist/components/data-table-row.d.ts +84 -0
  17. package/dist/components/data-table-row.js +261 -0
  18. package/dist/components/data-table-row.js.map +1 -0
  19. package/dist/components/data-table-selection.d.ts +3 -0
  20. package/dist/components/data-table-selection.js +12 -1
  21. package/dist/components/data-table-selection.js.map +1 -1
  22. package/dist/components/data-table-virtual.d.ts +98 -0
  23. package/dist/components/data-table-virtual.js +369 -0
  24. package/dist/components/data-table-virtual.js.map +1 -0
  25. package/dist/components/data-table.d.ts +58 -2
  26. package/dist/components/data-table.js +227 -230
  27. package/dist/components/data-table.js.map +1 -1
  28. package/dist/data-table.d.ts +1 -0
  29. package/dist/data-table.js.map +1 -1
  30. package/dist/eslint/index.js +1 -1
  31. package/dist/hooks/use-anchored-panel.js +2 -1
  32. package/dist/hooks/use-anchored-panel.js.map +1 -1
  33. package/dist/shell/app-shell.js +7 -1
  34. package/dist/shell/app-shell.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/data-table-fit.ts +17 -2
  37. package/src/components/data-table-groups.tsx +15 -4
  38. package/src/components/data-table-head.tsx +25 -2
  39. package/src/components/data-table-leading-row.tsx +4 -1
  40. package/src/components/data-table-row-actions.tsx +3 -2
  41. package/src/components/data-table-row.tsx +483 -0
  42. package/src/components/data-table-selection.ts +17 -1
  43. package/src/components/data-table-virtual.ts +547 -0
  44. package/src/components/data-table.tsx +326 -327
  45. package/src/data-table.ts +2 -0
  46. package/src/hooks/use-anchored-panel.ts +9 -1
  47. package/src/shell/app-shell.tsx +7 -1
package/README.md CHANGED
@@ -559,17 +559,17 @@ columns, strings and permissions (see the note at the top of `src/feedback/feedb
559
559
 
560
560
  <!-- BEGIN GENERATED: exports — node scripts/gen-export-inventory.mjs -->
561
561
 
562
- **2147 names from 352 modules** — 991 values and 1156 types. _Italic_ is a type-only export.
562
+ **2148 names from 353 modules** — 991 values and 1157 types. _Italic_ is a type-only export.
563
563
 
564
564
  Generated from `dist/index.d.ts` by `node scripts/gen-export-inventory.mjs`; the count
565
565
  is pinned by `src/__tests__/public-surface.test.ts`. Do not edit between the markers.
566
566
 
567
567
  | Entry point | Names |
568
568
  |---|---|
569
- | `@eifi1/ui-kit` | 2147 |
569
+ | `@eifi1/ui-kit` | 2148 |
570
570
  | `@eifi1/ui-kit/chart` | 92 |
571
571
  | `@eifi1/ui-kit/shell` | 114 |
572
- | `@eifi1/ui-kit/data-table` | 55 |
572
+ | `@eifi1/ui-kit/data-table` | 56 |
573
573
  | `@eifi1/ui-kit/wizard` | 24 |
574
574
  | `@eifi1/ui-kit/tour` | 37 |
575
575
  | `@eifi1/ui-kit/feedback` | 212 |
@@ -805,6 +805,7 @@ re-slicing of it, never a second API.
805
805
  | `components/data-table` | `DataTable`, `DEFAULT_DATA_TABLE_SORT_LABELS`, _`DataTableCellProps`_, _`DataTableChrome`_, _`DataTableColumn`_, _`DataTableDensity`_, _`DataTableDesktopSwipeActions`_, _`DataTableHeadProps`_, _`DataTableProps`_, _`DataTableRowAction`_, _`DataTableSortLabels`_, _`FilterState`_, _`MobileSwipeActions`_, _`ServerPagination`_ |
806
806
  | `components/data-table-row-actions` | `swipePlanRowActions` |
807
807
  | `components/data-table-groups` | _`DataTableGroupBy`_, _`DataTableLoadMore`_ |
808
+ | `components/data-table-virtual` | _`DataTableVirtualize`_ |
808
809
  | `components/data-table-cells` | `booleanColumn`, `BooleanMark`, _`BooleanColumnOptions`_, _`BooleanMarkProps`_ |
809
810
  | `components/use-table-state` | `readTableUrlState`, `useTableUrlState`, _`TableUrlState`_, _`TableUrlStateProps`_, _`TableUrlSync`_, _`UseTableUrlStateOptions`_, _`UseTableUrlStateReturn`_ |
810
811
  | `components/translation-review-editor` | `TranslationReviewEditor`, _`TranslationReviewEditorProps`_ |
@@ -27,7 +27,7 @@ export declare function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTabl
27
27
  * candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and
28
28
  * drops the keep. Keeps are not persisted: on the next visit the table fits again.
29
29
  */
30
- export declare function useColumnFit<T>({ columns, hiddenCols, headRefs, scroller, enabled, }: {
30
+ export declare function useColumnFit<T>({ columns, hiddenCols, headRefs, scroller, enabled, content, }: {
31
31
  columns: DataTableColumn<T>[];
32
32
  /** The user's hidden columns: never candidates (they are out already). */
33
33
  hiddenCols: Set<string>;
@@ -36,6 +36,13 @@ export declare function useColumnFit<T>({ columns, hiddenCols, headRefs, scrolle
36
36
  scroller: () => HTMLElement | null;
37
37
  /** The wide layout. On a phone (the card list) nothing is left out for width. */
38
38
  enabled: boolean;
39
+ /**
40
+ * What can change the columns' widths besides the scroller (0.48.2): the rows on
41
+ * screen, the columns, the stored widths, the text size. Compared item by item after
42
+ * each render, and the table is measured only when one changed — a selection tick no
43
+ * longer forces a layout of the whole table. Without it, every render measures.
44
+ */
45
+ content?: readonly unknown[];
39
46
  }): {
40
47
  /** The columns left out for width now — empty while off. */
41
48
  autoHidden: Set<string>;
@@ -12,7 +12,8 @@ function useColumnFit({
12
12
  hiddenCols,
13
13
  headRefs,
14
14
  scroller,
15
- enabled
15
+ enabled,
16
+ content
16
17
  }) {
17
18
  const [kept, setKept] = useState(() => /* @__PURE__ */ new Set());
18
19
  const [hidden, setHidden] = useState([]);
@@ -70,7 +71,12 @@ function useColumnFit({
70
71
  }
71
72
  if (n !== current2.length) setHidden(current2.slice(0, n));
72
73
  }, [headRefs]);
74
+ const lastContent = useRef(null);
73
75
  useLayoutEffect(() => {
76
+ const now = content ? [...content, order, current, growKey, active] : null;
77
+ const prev = lastContent.current;
78
+ lastContent.current = now;
79
+ if (now && prev && prev.length === now.length && prev.every((v, i) => Object.is(v, now[i]))) return;
74
80
  measure();
75
81
  });
76
82
  useEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-fit.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's fit to its own width (0.48, keksdose live #451: \"On expanded sidebar for\n// desktop the table scrolls horizontally. On collapsed sidebar it fits […] Compute the\n// width selectively\"). Two column options, both opt-in:\n//\n// - `grow`: the column takes what the others leave and ellipsises (`w-full max-w-0` on\n// its cells: in an auto table layout that is \"the rest of the row\", never its content's\n// width), so one long title can no longer widen the table.\n// - `fitPriority`: while the table is wider than its scroller — or, with a `grow` column,\n// while that column would get less than {@link GROW_MIN_REM} — the table leaves out\n// its `fitPriority` columns, the LOWEST number first (ties: the later column first),\n// and brings them back in the reverse order as soon as the table, as it is NOW, has\n// room for them (its need re-measured, plus the column's own header width from when\n// it was left out), so narrower content (a filter, another page) counts as much as a\n// wider scroller. The scroller's own width, through a ResizeObserver: a sidebar, a\n// narrower window, the Columns drawer and Large text all count the same.\n\n/** The least a `grow` column keeps before the table starts leaving columns out: about\n * a sentence of a title. In rem, so Large text asks for more. */\nexport const GROW_MIN_REM = 10;\n\n/** The `<th>`/`<td>` classes of a `grow` column. `truncate-until-large`: one line and an\n * ellipsis at Normal, a wrap from Large up (text size §10.17), as every kit title. The\n * header's `min-w-[10rem]` ({@link GROW_MIN_REM}) is the floor once no `fitPriority`\n * column is left to leave out: the table then scrolls sideways rather than crushing the\n * column it exists for (Kurvenschmiede S1, 0.48: Subject was 24px at 1100px). */\nexport const GROW_HEAD_CLASS = \"w-full max-w-0 min-w-[10rem]\";\nexport const GROW_CELL_CLASS = \"w-full max-w-0 truncate-until-large\";\n\n/** Every OTHER cell of a table with a `grow` column (kastlan S2, 0.48). A `w-full` column\n * squeezes the rest to their min-content width, so any text with a space broke in two\n * even at 1920px (\"CHF 2'568.38\", \"Peter Mueller\", a date with its time). One line at\n * Normal, so a column is never narrower than its text and `fitPriority` measures what\n * the columns really need; from Large up the cell wraps as before (§10.17). The headers\n * are `whitespace-nowrap` already. A column whose prose should wrap anyway passes\n * `not-large:whitespace-normal` in its `className`. */\nexport const GROW_PEER_CELL_CLASS = \"not-large:whitespace-nowrap\";\n\n// A cell's own flex-wrap row (chips) is left to wrap beside a `grow` column: a row of\n// badges reads as well stacked, and held to one line it widened the user roster's Status\n// until kastlan's admin pane scrolled 21px at 1280px (0.48 build review 2, kastlan S1).\n\n/** The auto-hide order: lowest `fitPriority` first, the later column first on a tie. */\nexport function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTableColumn<T>[] {\n return columns\n .map((col, index) => ({ col, index }))\n .filter(({ col }) => col.fitPriority != null)\n .sort((a, b) => a.col.fitPriority! - b.col.fitPriority! || b.index - a.index)\n .map(({ col }) => col);\n}\n\n/**\n * Which columns the table leaves out for width, and the Columns panel's \"keep it\".\n *\n * A column the user TICKS in the Columns panel is theirs: it is never left out for width\n * again while the table is mounted (`keep`): the table makes room by leaving out the next\n * candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and\n * drops the keep. Keeps are not persisted: on the next visit the table fits again.\n */\nexport function useColumnFit<T>({\n columns,\n hiddenCols,\n headRefs,\n scroller,\n enabled,\n}: {\n columns: DataTableColumn<T>[];\n /** The user's hidden columns: never candidates (they are out already). */\n hiddenCols: Set<string>;\n headRefs: Map<string, HTMLTableCellElement>;\n /** The desktop scroller, read after render (it is declared below the call). */\n scroller: () => HTMLElement | null;\n /** The wide layout. On a phone (the card list) nothing is left out for width. */\n enabled: boolean;\n}): {\n /** The columns left out for width now — empty while off. */\n autoHidden: Set<string>;\n /** The user's choice in the Columns panel: `true` keeps the column in whatever the width. */\n keep: (key: string, on: boolean) => void;\n} {\n const [kept, setKept] = useState<ReadonlySet<string>>(() => new Set());\n const [hidden, setHidden] = useState<string[]>([]);\n // A column's own header width, written when it is left out: it comes back once the\n // table's need as it is now, plus this, fits the scroller (keksdose S3, 0.48: not the\n // whole table's need at hide time, which went stale when the content narrowed).\n const ownWidth = useRef(new Map<string, number>());\n\n const hasFit = columns.some((c) => c.fitPriority != null);\n const active = enabled && hasFit;\n const order = useMemo(\n () => fitHideOrder(columns).filter((c) => !hiddenCols.has(c.key) && !kept.has(c.key)),\n [columns, hiddenCols, kept],\n );\n // Only the candidates still in play, in hide order — a column the user hid or kept\n // since, or one the app dropped, is no longer \"hidden for width\".\n const current = useMemo(\n () => (active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : []),\n [active, order, hidden],\n );\n const growKey = columns.find((c) => c.grow && !hiddenCols.has(c.key))?.key;\n\n // What `measure` reads, refreshed after each render (before the measure below runs):\n // the ResizeObserver's callback is set up once and must see the current columns.\n const latest = useRef({ order, current, growKey, active, scroller });\n useLayoutEffect(() => {\n latest.current = { order, current, growKey, active, scroller };\n });\n\n const measure = useCallback(() => {\n const { order, current, growKey, active, scroller } = latest.current;\n const el = scroller();\n if (!active || !el) return;\n const table = el.querySelector(\"table\");\n const avail = el.clientWidth;\n // Not laid out (display:none, jsdom): nothing to fit to.\n if (!table || avail <= 0) return;\n const growTh = growKey ? headRefs.get(growKey) : undefined;\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n let need = growTh\n ? table.offsetWidth - growTh.offsetWidth + GROW_MIN_REM * rem\n : table.offsetWidth;\n if (!growTh && need <= avail + 0.5 && current.length > 0) {\n // A `w-full` table is at least the scroller's width, which says nothing about how\n // much room its content leaves: probe its content's width (what it takes once it\n // overflows) for the bring-back below. Read and put back in one layout pass.\n const before = table.style.width;\n table.style.width = \"min-content\";\n need = table.offsetWidth;\n table.style.width = before;\n }\n if (need > avail + 0.5) {\n // Leave out, in order, until the rest fits: each column saves its header's width\n // (the others are at their content's width while a `grow` column takes the rest).\n const next = [...current];\n for (const col of order) {\n if (need <= avail + 0.5) break;\n if (next.includes(col.key)) continue;\n const w = headRefs.get(col.key)?.offsetWidth ?? 0;\n ownWidth.current.set(col.key, w);\n need -= w;\n next.push(col.key);\n }\n if (next.length !== current.length) setHidden(next);\n return;\n }\n // Bring back, the last left out first, while the table as it is now has room for\n // them. Should the column have grown since (wider content), the next measure leaves\n // it out again before paint and records its new width, so it cannot flicker.\n let n = current.length;\n while (n > 0) {\n const w = ownWidth.current.get(current[n - 1]);\n if (w == null || need + w > avail + 0.5) break;\n need += w;\n n--;\n }\n if (n !== current.length) setHidden(current.slice(0, n));\n }, [headRefs]);\n\n // After every render: rows, a filter or a page can widen a column as much as a resize.\n // Before paint, so a column the table cannot hold is never drawn first.\n useLayoutEffect(() => {\n measure();\n });\n\n useEffect(() => {\n const el = latest.current.scroller();\n if (!active || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => measure());\n ro.observe(el);\n return () => ro.disconnect();\n }, [active, measure]);\n\n const keep = useCallback((key: string, on: boolean) => {\n setKept((s) => {\n if (s.has(key) === on) return s;\n const next = new Set(s);\n if (on) next.add(key);\n else next.delete(key);\n return next;\n });\n }, []);\n\n const autoHidden = useMemo(() => new Set(current), [current]);\n return { autoHidden, keep };\n}\n"],"mappings":"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","names":["order","current","growKey","active","scroller"]}
1
+ {"version":3,"sources":["../../src/components/data-table-fit.ts"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { DataTableColumn } from \"./data-table\";\n\n// DataTable's fit to its own width (0.48, keksdose live #451: \"On expanded sidebar for\n// desktop the table scrolls horizontally. On collapsed sidebar it fits […] Compute the\n// width selectively\"). Two column options, both opt-in:\n//\n// - `grow`: the column takes what the others leave and ellipsises (`w-full max-w-0` on\n// its cells: in an auto table layout that is \"the rest of the row\", never its content's\n// width), so one long title can no longer widen the table.\n// - `fitPriority`: while the table is wider than its scroller — or, with a `grow` column,\n// while that column would get less than {@link GROW_MIN_REM} — the table leaves out\n// its `fitPriority` columns, the LOWEST number first (ties: the later column first),\n// and brings them back in the reverse order as soon as the table, as it is NOW, has\n// room for them (its need re-measured, plus the column's own header width from when\n// it was left out), so narrower content (a filter, another page) counts as much as a\n// wider scroller. The scroller's own width, through a ResizeObserver: a sidebar, a\n// narrower window, the Columns drawer and Large text all count the same.\n\n/** The least a `grow` column keeps before the table starts leaving columns out: about\n * a sentence of a title. In rem, so Large text asks for more. */\nexport const GROW_MIN_REM = 10;\n\n/** The `<th>`/`<td>` classes of a `grow` column. `truncate-until-large`: one line and an\n * ellipsis at Normal, a wrap from Large up (text size §10.17), as every kit title. The\n * header's `min-w-[10rem]` ({@link GROW_MIN_REM}) is the floor once no `fitPriority`\n * column is left to leave out: the table then scrolls sideways rather than crushing the\n * column it exists for (Kurvenschmiede S1, 0.48: Subject was 24px at 1100px). */\nexport const GROW_HEAD_CLASS = \"w-full max-w-0 min-w-[10rem]\";\nexport const GROW_CELL_CLASS = \"w-full max-w-0 truncate-until-large\";\n\n/** Every OTHER cell of a table with a `grow` column (kastlan S2, 0.48). A `w-full` column\n * squeezes the rest to their min-content width, so any text with a space broke in two\n * even at 1920px (\"CHF 2'568.38\", \"Peter Mueller\", a date with its time). One line at\n * Normal, so a column is never narrower than its text and `fitPriority` measures what\n * the columns really need; from Large up the cell wraps as before (§10.17). The headers\n * are `whitespace-nowrap` already. A column whose prose should wrap anyway passes\n * `not-large:whitespace-normal` in its `className`. */\nexport const GROW_PEER_CELL_CLASS = \"not-large:whitespace-nowrap\";\n\n// A cell's own flex-wrap row (chips) is left to wrap beside a `grow` column: a row of\n// badges reads as well stacked, and held to one line it widened the user roster's Status\n// until kastlan's admin pane scrolled 21px at 1280px (0.48 build review 2, kastlan S1).\n\n/** The auto-hide order: lowest `fitPriority` first, the later column first on a tie. */\nexport function fitHideOrder<T>(columns: DataTableColumn<T>[]): DataTableColumn<T>[] {\n return columns\n .map((col, index) => ({ col, index }))\n .filter(({ col }) => col.fitPriority != null)\n .sort((a, b) => a.col.fitPriority! - b.col.fitPriority! || b.index - a.index)\n .map(({ col }) => col);\n}\n\n/**\n * Which columns the table leaves out for width, and the Columns panel's \"keep it\".\n *\n * A column the user TICKS in the Columns panel is theirs: it is never left out for width\n * again while the table is mounted (`keep`): the table makes room by leaving out the next\n * candidate, and scrolls sideways once none is left — what they asked for. Unticking it hides it as before (the persisted `hiddenCols`) and\n * drops the keep. Keeps are not persisted: on the next visit the table fits again.\n */\nexport function useColumnFit<T>({\n columns,\n hiddenCols,\n headRefs,\n scroller,\n enabled,\n content,\n}: {\n columns: DataTableColumn<T>[];\n /** The user's hidden columns: never candidates (they are out already). */\n hiddenCols: Set<string>;\n headRefs: Map<string, HTMLTableCellElement>;\n /** The desktop scroller, read after render (it is declared below the call). */\n scroller: () => HTMLElement | null;\n /** The wide layout. On a phone (the card list) nothing is left out for width. */\n enabled: boolean;\n /**\n * What can change the columns' widths besides the scroller (0.48.2): the rows on\n * screen, the columns, the stored widths, the text size. Compared item by item after\n * each render, and the table is measured only when one changed — a selection tick no\n * longer forces a layout of the whole table. Without it, every render measures.\n */\n content?: readonly unknown[];\n}): {\n /** The columns left out for width now — empty while off. */\n autoHidden: Set<string>;\n /** The user's choice in the Columns panel: `true` keeps the column in whatever the width. */\n keep: (key: string, on: boolean) => void;\n} {\n const [kept, setKept] = useState<ReadonlySet<string>>(() => new Set());\n const [hidden, setHidden] = useState<string[]>([]);\n // A column's own header width, written when it is left out: it comes back once the\n // table's need as it is now, plus this, fits the scroller (keksdose S3, 0.48: not the\n // whole table's need at hide time, which went stale when the content narrowed).\n const ownWidth = useRef(new Map<string, number>());\n\n const hasFit = columns.some((c) => c.fitPriority != null);\n const active = enabled && hasFit;\n const order = useMemo(\n () => fitHideOrder(columns).filter((c) => !hiddenCols.has(c.key) && !kept.has(c.key)),\n [columns, hiddenCols, kept],\n );\n // Only the candidates still in play, in hide order — a column the user hid or kept\n // since, or one the app dropped, is no longer \"hidden for width\".\n const current = useMemo(\n () => (active ? order.filter((c) => hidden.includes(c.key)).map((c) => c.key) : []),\n [active, order, hidden],\n );\n const growKey = columns.find((c) => c.grow && !hiddenCols.has(c.key))?.key;\n\n // What `measure` reads, refreshed after each render (before the measure below runs):\n // the ResizeObserver's callback is set up once and must see the current columns.\n const latest = useRef({ order, current, growKey, active, scroller });\n useLayoutEffect(() => {\n latest.current = { order, current, growKey, active, scroller };\n });\n\n const measure = useCallback(() => {\n const { order, current, growKey, active, scroller } = latest.current;\n const el = scroller();\n if (!active || !el) return;\n const table = el.querySelector(\"table\");\n const avail = el.clientWidth;\n // Not laid out (display:none, jsdom): nothing to fit to.\n if (!table || avail <= 0) return;\n const growTh = growKey ? headRefs.get(growKey) : undefined;\n const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;\n let need = growTh\n ? table.offsetWidth - growTh.offsetWidth + GROW_MIN_REM * rem\n : table.offsetWidth;\n if (!growTh && need <= avail + 0.5 && current.length > 0) {\n // A `w-full` table is at least the scroller's width, which says nothing about how\n // much room its content leaves: probe its content's width (what it takes once it\n // overflows) for the bring-back below. Read and put back in one layout pass.\n const before = table.style.width;\n table.style.width = \"min-content\";\n need = table.offsetWidth;\n table.style.width = before;\n }\n if (need > avail + 0.5) {\n // Leave out, in order, until the rest fits: each column saves its header's width\n // (the others are at their content's width while a `grow` column takes the rest).\n const next = [...current];\n for (const col of order) {\n if (need <= avail + 0.5) break;\n if (next.includes(col.key)) continue;\n const w = headRefs.get(col.key)?.offsetWidth ?? 0;\n ownWidth.current.set(col.key, w);\n need -= w;\n next.push(col.key);\n }\n if (next.length !== current.length) setHidden(next);\n return;\n }\n // Bring back, the last left out first, while the table as it is now has room for\n // them. Should the column have grown since (wider content), the next measure leaves\n // it out again before paint and records its new width, so it cannot flicker.\n let n = current.length;\n while (n > 0) {\n const w = ownWidth.current.get(current[n - 1]);\n if (w == null || need + w > avail + 0.5) break;\n need += w;\n n--;\n }\n if (n !== current.length) setHidden(current.slice(0, n));\n }, [headRefs]);\n\n // After a render that changed what is drawn: rows, a filter or a page can widen a\n // column as much as a resize. Before paint, so a column the table cannot hold is never\n // drawn first. A render that changed none of it (a ticked box, a focused row) skips the\n // measure, which forces a layout of the whole table (0.48.2, keksdose dev #632).\n const lastContent = useRef<readonly unknown[] | null>(null);\n useLayoutEffect(() => {\n const now = content ? [...content, order, current, growKey, active] : null;\n const prev = lastContent.current;\n lastContent.current = now;\n if (now && prev && prev.length === now.length && prev.every((v, i) => Object.is(v, now[i]))) return;\n measure();\n });\n\n useEffect(() => {\n const el = latest.current.scroller();\n if (!active || !el || typeof ResizeObserver === \"undefined\") return;\n const ro = new ResizeObserver(() => measure());\n ro.observe(el);\n return () => ro.disconnect();\n }, [active, measure]);\n\n const keep = useCallback((key: string, on: boolean) => {\n setKept((s) => {\n if (s.has(key) === on) return s;\n const next = new Set(s);\n if (on) next.add(key);\n else next.delete(key);\n return next;\n });\n }, []);\n\n const autoHidden = useMemo(() => new Set(current), [current]);\n return { autoHidden, keep };\n}\n"],"mappings":"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","names":["order","current","growKey","active","scroller"]}
@@ -66,13 +66,15 @@ export declare function useGroupBandTops(tableRef: RefObject<HTMLTableElement |
66
66
  * period arriving from below covers the day still pinned under the one before. The line
67
67
  * over it is the cell's own `::before`, which moves with the stuck cell.
68
68
  */
69
- export declare function DataTableGroupBand({ level, colSpan, top, compact, label, end, }: {
69
+ export declare function DataTableGroupBand({ level, colSpan, top, compact, label, end, ariaRowIndex, }: {
70
70
  level: 1 | 2;
71
71
  colSpan: number;
72
72
  top: number;
73
73
  compact: boolean;
74
74
  label: ReactNode;
75
75
  end: ReactNode;
76
+ /** Under `virtualize` (0.48.2). */
77
+ ariaRowIndex?: number;
76
78
  }): import("react").JSX.Element;
77
79
  /**
78
80
  * The load-more footer. Observed against the viewport (an implicit root sees through the
@@ -27,19 +27,24 @@ function useGroupBandTops(tableRef, enabled, signature) {
27
27
  useLayoutEffect(() => {
28
28
  const table = tableRef.current;
29
29
  if (!enabled || !table) return;
30
- const parts = [
30
+ const parts = () => [
31
31
  table.tHead,
32
32
  table.querySelector("[data-leading-row='sticky']"),
33
33
  table.querySelector("th[data-group-band='1']")
34
34
  ];
35
+ let bandHeight = 0;
35
36
  const measure = () => {
36
- const [head, lead, band] = parts.map((el) => el ? el.getBoundingClientRect().height : 0);
37
+ const [headEl, leadEl, bandEl] = parts();
38
+ const head = headEl ? headEl.getBoundingClientRect().height : 0;
39
+ const lead = leadEl ? leadEl.getBoundingClientRect().height : 0;
40
+ if (bandEl) bandHeight = bandEl.getBoundingClientRect().height;
41
+ const band = bandHeight;
37
42
  setTops((prev) => prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]);
38
43
  };
39
44
  measure();
40
45
  if (typeof ResizeObserver === "undefined") return;
41
46
  const observer = new ResizeObserver(measure);
42
- for (const el of parts) if (el) observer.observe(el);
47
+ for (const el of parts()) if (el) observer.observe(el);
43
48
  return () => observer.disconnect();
44
49
  }, [tableRef, enabled, signature]);
45
50
  return tops;
@@ -50,9 +55,10 @@ function DataTableGroupBand({
50
55
  top,
51
56
  compact,
52
57
  label,
53
- end
58
+ end,
59
+ ariaRowIndex
54
60
  }) {
55
- return /* @__PURE__ */ jsx("tr", { "data-group-row": level, children: /* @__PURE__ */ jsx(
61
+ return /* @__PURE__ */ jsx("tr", { "data-group-row": level, "aria-rowindex": ariaRowIndex, children: /* @__PURE__ */ jsx(
56
62
  "th",
57
63
  {
58
64
  scope: "rowgroup",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-groups.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"./button\";\nimport { Spinner } from \"./spinner\";\n\n/**\n * One level of {@link DataTableProps.groupBy} (0.48, keksdose dev #605): the rows are cut\n * into consecutive runs of equal `key` — the rows must already be ordered so equal keys\n * are contiguous, as for `mobileGroupBy` — and each run is headed by a full-width band.\n *\n * The same object drives the phone: `mobileGroupBy` takes a level too, so\n * `groupBy={[period, day]} mobileGroupBy={period}` is one definition for both layouts.\n */\nexport interface DataTableGroupBy<T> {\n /** The group a row belongs to. */\n key: (row: T) => string;\n /** The band's label. Default: the key itself. */\n label?: (key: string, rows: T[]) => ReactNode;\n /**\n * The band's end slot — a count, a total (\"176 · +8'782.30 · −3'200.10\"). `rows` are\n * the group's rows the table holds; an app whose total covers rows not yet loaded\n * looks it up by `key` instead.\n */\n end?: (key: string, rows: T[]) => ReactNode;\n}\n\n/**\n * {@link DataTableProps.loadMore} (0.48, keksdose dev #605): a footer in place of the\n * pager that loads the next slice of rows — on its own as it scrolls into view (an\n * IntersectionObserver), and through a button that is always there for the keyboard and\n * the screen reader. The owner appends the rows; the table renders every row it is given.\n */\nexport interface DataTableLoadMore {\n /** Whether there is anything left to load. False draws the end state. */\n hasMore: boolean;\n /** Load the next slice. Called at most once per row count while `loading` is false. */\n onLoadMore: () => void;\n /** A load is in flight: the button says \"Loading…\" and ignores presses. */\n loading?: boolean;\n /** The button's words — what it loads (\"Load August 2026\"). Default: the kit's \"Load more\". */\n label?: ReactNode;\n /** What the footer says once `hasMore` is false. Default: the kit's \"All rows loaded\";\n * `null` draws nothing. */\n endLabel?: ReactNode;\n /** Load as the footer scrolls into view. Default true; false leaves only the button. */\n auto?: boolean;\n}\n\n/** A run of rows under one band; `start` is its first row's index in the slice. */\nexport interface RowGroup<T> {\n key: string;\n rows: T[];\n start: number;\n /** The second level's runs inside this one, or null with one level. */\n children: RowGroup<T>[] | null;\n}\n\n/** Consecutive runs by `level.key`, nested once for a second level. */\nexport function groupRows<T>(rows: T[], levels: readonly DataTableGroupBy<T>[], offset = 0): RowGroup<T>[] {\n const [level, ...rest] = levels;\n if (!level) return [];\n const out: RowGroup<T>[] = [];\n rows.forEach((row, i) => {\n const key = level.key(row);\n const last = out[out.length - 1];\n if (last && last.key === key) last.rows.push(row);\n else out.push({ key, rows: [row], start: offset + i, children: null });\n });\n if (rest.length > 0) for (const g of out) g.children = groupRows(g.rows, rest.slice(0, 1), g.start);\n return out;\n}\n\n/** `groupBy` as a list of at most two levels. */\nexport function groupLevels<T>(\n groupBy: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?] | undefined,\n): DataTableGroupBy<T>[] {\n if (!groupBy) return [];\n const list = \"key\" in groupBy ? [groupBy] : (groupBy as readonly (DataTableGroupBy<T> | undefined)[]);\n return list.filter((l): l is DataTableGroupBy<T> => !!l).slice(0, 2);\n}\n\n/**\n * The bands' sticky offsets: the first level under the header (and under a sticky\n * `leadingRow`), the second under the first. Measured — the header grows at Large text,\n * a band's end slot can wrap — and followed with a ResizeObserver.\n */\nexport function useGroupBandTops(\n tableRef: RefObject<HTMLTableElement | null>,\n enabled: boolean,\n signature: string,\n): [number, number] {\n const [tops, setTops] = useState<[number, number]>([0, 0]);\n useLayoutEffect(() => {\n const table = tableRef.current;\n if (!enabled || !table) return;\n const parts = [\n table.tHead,\n table.querySelector<HTMLElement>(\"[data-leading-row='sticky']\"),\n table.querySelector<HTMLElement>(\"th[data-group-band='1']\"),\n ];\n const measure = () => {\n const [head, lead, band] = parts.map((el) => (el ? el.getBoundingClientRect().height : 0));\n setTops((prev) => (prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]));\n };\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n for (const el of parts) if (el) observer.observe(el);\n return () => observer.disconnect();\n }, [tableRef, enabled, signature]);\n return tops;\n}\n\n/**\n * A band row. A `<th scope=\"rowgroup\">` spanning the row — the header of the `<tbody>` it\n * opens, which a screen reader announces as the reader enters the group — and sticky on\n * the cell, not the `<tr>`, as the header's and the leading row's are. Opaque, under the\n * sticky leading row's `z-[5]`: the first level `z-[4]` over the second's `z-[3]`, so a\n * period arriving from below covers the day still pinned under the one before. The line\n * over it is the cell's own `::before`, which moves with the stuck cell.\n */\nexport function DataTableGroupBand({\n level,\n colSpan,\n top,\n compact,\n label,\n end,\n}: {\n level: 1 | 2;\n colSpan: number;\n top: number;\n compact: boolean;\n label: ReactNode;\n end: ReactNode;\n}) {\n return (\n <tr data-group-row={level}>\n <th\n scope=\"rowgroup\"\n colSpan={colSpan}\n data-group-band={level}\n style={{ top }}\n className={cn(\n \"sticky p-0 text-start align-middle\",\n \"before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n level === 1 ? \"z-[4] bg-surface-2 font-semibold text-primary\" : \"z-[3] bg-surface font-medium text-secondary\",\n level === 1 ? (compact ? \"text-xs\" : \"text-sm\") : \"text-xs\",\n )}\n >\n {/* Pinned to what the scroller shows, as the expansion row's panel is: the end\n slot stays at the visible end of a table wider than its scroller. */}\n <div className=\"w-0 min-w-full\">\n <div\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-wrap items-baseline justify-between gap-x-3\",\n compact ? \"px-2 py-1\" : level === 1 ? \"px-3 py-2\" : \"px-3 py-1.5\",\n )}\n >\n <span className=\"min-w-0\">{label}</span>\n {end != null && end !== false && <span className=\"ms-auto tabular-nums\">{end}</span>}\n </div>\n </div>\n </th>\n </tr>\n );\n}\n\n/**\n * The load-more footer. Observed against the viewport (an implicit root sees through the\n * clipping scroller, so it works in a bounded body, under `fillHeight`, and in a page that\n * scrolls), and guarded: one call per row count, none while `loading`, so an observer\n * that reports twice — or a footer still in view while the owner sets `loading` — loads\n * once. When the rows arrive and the footer is still in view (a short slice), it loads\n * again, until the scroller is full.\n */\nexport function DataTableLoadMoreFooter({\n loadMore,\n rowCount,\n labels,\n compact,\n as: Tag = \"div\",\n}: {\n loadMore: DataTableLoadMore;\n rowCount: number;\n labels: { loadMore: string; allLoaded: string; loading: string };\n compact: boolean;\n as?: \"div\" | \"li\";\n}) {\n const { hasMore, onLoadMore, loading = false, auto = true, label, endLabel } = loadMore;\n const rootRef = useRef<HTMLElement | null>(null);\n const endRef = useRef<HTMLParagraphElement | null>(null);\n const firedAt = useRef<number | null>(null);\n const buttonFocused = useRef(false);\n // What the observer's callback acts on: the latest props, not the ones it was made with.\n const latest = useRef({ onLoadMore, loading, rowCount });\n useEffect(() => {\n latest.current = { onLoadMore, loading, rowCount };\n });\n\n // A fresh observer whenever the rows or the in-flight flag change: its first report is\n // the footer's position AFTER the new rows are laid out. Acting on a remembered \"in\n // view\" instead fired a second load the moment the rows arrived, while the footer was\n // still where the old last row had been (headless Chromium caught it).\n useEffect(() => {\n const el = rootRef.current;\n if (!auto || !hasMore || loading || !el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(\n (entries) => {\n const now = latest.current;\n if (!entries.some((e) => e.isIntersecting) || now.loading || firedAt.current === now.rowCount) return;\n firedAt.current = now.rowCount;\n now.onLoadMore();\n },\n { rootMargin: \"200px\" },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [auto, hasMore, loading, rowCount]);\n\n // A focused button that goes away (nothing left) hands its focus to the end line.\n useEffect(() => {\n if (!hasMore && buttonFocused.current) {\n buttonFocused.current = false;\n endRef.current?.focus();\n }\n }, [hasMore]);\n\n const end = endLabel === undefined ? labels.allLoaded : endLabel;\n return (\n <Tag\n ref={(el: HTMLElement | null) => {\n rootRef.current = el;\n }}\n data-table-load-more=\"\"\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-col items-center border-t border-subtle text-center\",\n compact ? \"px-2 py-2\" : \"px-3 py-3\",\n )}\n >\n {hasMore ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n aria-busy={loading || undefined}\n onFocus={() => (buttonFocused.current = true)}\n onBlur={() => (buttonFocused.current = false)}\n onClick={() => {\n if (loading) return;\n firedAt.current = rowCount;\n onLoadMore();\n }}\n >\n {loading ? (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{labels.loading}</bdi>\n </span>\n ) : (\n (label ?? labels.loadMore)\n )}\n </Button>\n ) : end != null && end !== false ? (\n <p ref={endRef} tabIndex={-1} className=\"text-xs text-muted outline-none\">\n {end}\n </p>\n ) : null}\n {/* Mounted throughout, so a screen reader hears the load start. */}\n <span role=\"status\" className=\"sr-only\">\n {loading ? labels.loading : \"\"}\n </span>\n </Tag>\n );\n}\n"],"mappings":";AA0JU,SAME,KANF;AA1JV,SAAS,WAAW,iBAAiB,QAAQ,gBAAgB;AAE7D,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,eAAe;AAuDjB,SAAS,UAAa,MAAW,QAAwC,SAAS,GAAkB;AACzG,QAAM,CAAC,OAAO,GAAG,IAAI,IAAI;AACzB,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAqB,CAAC;AAC5B,OAAK,QAAQ,CAAC,KAAK,MAAM;AACvB,UAAM,MAAM,MAAM,IAAI,GAAG;AACzB,UAAM,OAAO,IAAI,IAAI,SAAS,CAAC;AAC/B,QAAI,QAAQ,KAAK,QAAQ,IAAK,MAAK,KAAK,KAAK,GAAG;AAAA,QAC3C,KAAI,KAAK,EAAE,KAAK,MAAM,CAAC,GAAG,GAAG,OAAO,SAAS,GAAG,UAAU,KAAK,CAAC;AAAA,EACvE,CAAC;AACD,MAAI,KAAK,SAAS,EAAG,YAAW,KAAK,IAAK,GAAE,WAAW,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,KAAK;AAClG,SAAO;AACT;AAGO,SAAS,YACd,SACuB;AACvB,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,QAAM,OAAO,SAAS,UAAU,CAAC,OAAO,IAAK;AAC7C,SAAO,KAAK,OAAO,CAAC,MAAgC,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC;AACrE;AAOO,SAAS,iBACd,UACA,SACA,WACkB;AAClB,QAAM,CAAC,MAAM,OAAO,IAAI,SAA2B,CAAC,GAAG,CAAC,CAAC;AACzD,kBAAgB,MAAM;AACpB,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,WAAW,CAAC,MAAO;AACxB,UAAM,QAAQ;AAAA,MACZ,MAAM;AAAA,MACN,MAAM,cAA2B,6BAA6B;AAAA,MAC9D,MAAM,cAA2B,yBAAyB;AAAA,IAC5D;AACA,UAAM,UAAU,MAAM;AACpB,YAAM,CAAC,MAAM,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,OAAQ,KAAK,GAAG,sBAAsB,EAAE,SAAS,CAAE;AACzF,cAAQ,CAAC,SAAU,KAAK,CAAC,MAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,OAAO,OAAO,OAAO,OAAO,CAAC,OAAO,MAAM,OAAO,OAAO,IAAI,CAAE;AAAA,IAC1H;AACA,YAAQ;AACR,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,eAAW,MAAM,MAAO,KAAI,GAAI,UAAS,QAAQ,EAAE;AACnD,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,UAAU,SAAS,SAAS,CAAC;AACjC,SAAO;AACT;AAUO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE,oBAAC,QAAG,kBAAgB,OAClB;AAAA,IAAC;AAAA;AAAA,MACC,OAAM;AAAA,MACN;AAAA,MACA,mBAAiB;AAAA,MACjB,OAAO,EAAE,IAAI;AAAA,MACb,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,UAAU,IAAI,kDAAkD;AAAA,QAChE,UAAU,IAAK,UAAU,YAAY,YAAa;AAAA,MACpD;AAAA,MAIA,8BAAC,SAAI,WAAU,kBACb;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,UAAU,cAAc,UAAU,IAAI,cAAc;AAAA,UACtD;AAAA,UAEA;AAAA,gCAAC,UAAK,WAAU,WAAW,iBAAM;AAAA,YAChC,OAAO,QAAQ,QAAQ,SAAS,oBAAC,UAAK,WAAU,wBAAwB,eAAI;AAAA;AAAA;AAAA,MAC/E,GACF;AAAA;AAAA,EACF,GACF;AAEJ;AAUO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,IAAI,MAAM;AACZ,GAMG;AACD,QAAM,EAAE,SAAS,YAAY,UAAU,OAAO,OAAO,MAAM,OAAO,SAAS,IAAI;AAC/E,QAAM,UAAU,OAA2B,IAAI;AAC/C,QAAM,SAAS,OAAoC,IAAI;AACvD,QAAM,UAAU,OAAsB,IAAI;AAC1C,QAAM,gBAAgB,OAAO,KAAK;AAElC,QAAM,SAAS,OAAO,EAAE,YAAY,SAAS,SAAS,CAAC;AACvD,YAAU,MAAM;AACd,WAAO,UAAU,EAAE,YAAY,SAAS,SAAS;AAAA,EACnD,CAAC;AAMD,YAAU,MAAM;AACd,UAAM,KAAK,QAAQ;AACnB,QAAI,CAAC,QAAQ,CAAC,WAAW,WAAW,CAAC,MAAM,OAAO,yBAAyB,YAAa;AACxF,UAAM,KAAK,IAAI;AAAA,MACb,CAAC,YAAY;AACX,cAAM,MAAM,OAAO;AACnB,YAAI,CAAC,QAAQ,KAAK,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,WAAW,QAAQ,YAAY,IAAI,SAAU;AAC/F,gBAAQ,UAAU,IAAI;AACtB,YAAI,WAAW;AAAA,MACjB;AAAA,MACA,EAAE,YAAY,QAAQ;AAAA,IACxB;AACA,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAW;AAAA,EAC7B,GAAG,CAAC,MAAM,SAAS,SAAS,QAAQ,CAAC;AAGrC,YAAU,MAAM;AACd,QAAI,CAAC,WAAW,cAAc,SAAS;AACrC,oBAAc,UAAU;AACxB,aAAO,SAAS,MAAM;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,MAAM,aAAa,SAAY,OAAO,YAAY;AACxD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,CAAC,OAA2B;AAC/B,gBAAQ,UAAU;AAAA,MACpB;AAAA,MACA,wBAAqB;AAAA,MACrB,WAAW;AAAA,QACT;AAAA,QACA,UAAU,cAAc;AAAA,MAC1B;AAAA,MAEC;AAAA,kBACC;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,aAAW,WAAW;AAAA,YACtB,SAAS,MAAO,cAAc,UAAU;AAAA,YACxC,QAAQ,MAAO,cAAc,UAAU;AAAA,YACvC,SAAS,MAAM;AACb,kBAAI,QAAS;AACb,sBAAQ,UAAU;AAClB,yBAAW;AAAA,YACb;AAAA,YAEC,oBACC,qBAAC,UAAK,WAAU,kCACd;AAAA,kCAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,cACzC,oBAAC,SAAK,iBAAO,SAAQ;AAAA,eACvB,IAEC,SAAS,OAAO;AAAA;AAAA,QAErB,IACE,OAAO,QAAQ,QAAQ,QACzB,oBAAC,OAAE,KAAK,QAAQ,UAAU,IAAI,WAAU,mCACrC,eACH,IACE;AAAA,QAEJ,oBAAC,UAAK,MAAK,UAAS,WAAU,WAC3B,oBAAU,OAAO,UAAU,IAC9B;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-groups.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode, RefObject } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"./button\";\nimport { Spinner } from \"./spinner\";\n\n/**\n * One level of {@link DataTableProps.groupBy} (0.48, keksdose dev #605): the rows are cut\n * into consecutive runs of equal `key` — the rows must already be ordered so equal keys\n * are contiguous, as for `mobileGroupBy` — and each run is headed by a full-width band.\n *\n * The same object drives the phone: `mobileGroupBy` takes a level too, so\n * `groupBy={[period, day]} mobileGroupBy={period}` is one definition for both layouts.\n */\nexport interface DataTableGroupBy<T> {\n /** The group a row belongs to. */\n key: (row: T) => string;\n /** The band's label. Default: the key itself. */\n label?: (key: string, rows: T[]) => ReactNode;\n /**\n * The band's end slot — a count, a total (\"176 · +8'782.30 · −3'200.10\"). `rows` are\n * the group's rows the table holds; an app whose total covers rows not yet loaded\n * looks it up by `key` instead.\n */\n end?: (key: string, rows: T[]) => ReactNode;\n}\n\n/**\n * {@link DataTableProps.loadMore} (0.48, keksdose dev #605): a footer in place of the\n * pager that loads the next slice of rows — on its own as it scrolls into view (an\n * IntersectionObserver), and through a button that is always there for the keyboard and\n * the screen reader. The owner appends the rows; the table renders every row it is given.\n */\nexport interface DataTableLoadMore {\n /** Whether there is anything left to load. False draws the end state. */\n hasMore: boolean;\n /** Load the next slice. Called at most once per row count while `loading` is false. */\n onLoadMore: () => void;\n /** A load is in flight: the button says \"Loading…\" and ignores presses. */\n loading?: boolean;\n /** The button's words — what it loads (\"Load August 2026\"). Default: the kit's \"Load more\". */\n label?: ReactNode;\n /** What the footer says once `hasMore` is false. Default: the kit's \"All rows loaded\";\n * `null` draws nothing. */\n endLabel?: ReactNode;\n /** Load as the footer scrolls into view. Default true; false leaves only the button. */\n auto?: boolean;\n}\n\n/** A run of rows under one band; `start` is its first row's index in the slice. */\nexport interface RowGroup<T> {\n key: string;\n rows: T[];\n start: number;\n /** The second level's runs inside this one, or null with one level. */\n children: RowGroup<T>[] | null;\n}\n\n/** Consecutive runs by `level.key`, nested once for a second level. */\nexport function groupRows<T>(rows: T[], levels: readonly DataTableGroupBy<T>[], offset = 0): RowGroup<T>[] {\n const [level, ...rest] = levels;\n if (!level) return [];\n const out: RowGroup<T>[] = [];\n rows.forEach((row, i) => {\n const key = level.key(row);\n const last = out[out.length - 1];\n if (last && last.key === key) last.rows.push(row);\n else out.push({ key, rows: [row], start: offset + i, children: null });\n });\n if (rest.length > 0) for (const g of out) g.children = groupRows(g.rows, rest.slice(0, 1), g.start);\n return out;\n}\n\n/** `groupBy` as a list of at most two levels. */\nexport function groupLevels<T>(\n groupBy: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?] | undefined,\n): DataTableGroupBy<T>[] {\n if (!groupBy) return [];\n const list = \"key\" in groupBy ? [groupBy] : (groupBy as readonly (DataTableGroupBy<T> | undefined)[]);\n return list.filter((l): l is DataTableGroupBy<T> => !!l).slice(0, 2);\n}\n\n/**\n * The bands' sticky offsets: the first level under the header (and under a sticky\n * `leadingRow`), the second under the first. Measured — the header grows at Large text,\n * a band's end slot can wrap — and followed with a ResizeObserver.\n */\nexport function useGroupBandTops(\n tableRef: RefObject<HTMLTableElement | null>,\n enabled: boolean,\n signature: string,\n): [number, number] {\n const [tops, setTops] = useState<[number, number]>([0, 0]);\n useLayoutEffect(() => {\n const table = tableRef.current;\n if (!enabled || !table) return;\n // Looked up afresh on each measure: under `virtualize` (0.48.2) the first band can\n // leave the DOM as the rows scroll, and a detached cell measures 0 — the bands would\n // stack on one another. A band missing for now keeps the height it had.\n const parts = () => [\n table.tHead,\n table.querySelector<HTMLElement>(\"[data-leading-row='sticky']\"),\n table.querySelector<HTMLElement>(\"th[data-group-band='1']\"),\n ];\n let bandHeight = 0;\n const measure = () => {\n const [headEl, leadEl, bandEl] = parts();\n const head = headEl ? headEl.getBoundingClientRect().height : 0;\n const lead = leadEl ? leadEl.getBoundingClientRect().height : 0;\n if (bandEl) bandHeight = bandEl.getBoundingClientRect().height;\n const band = bandHeight;\n setTops((prev) => (prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]));\n };\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n for (const el of parts()) if (el) observer.observe(el);\n return () => observer.disconnect();\n }, [tableRef, enabled, signature]);\n return tops;\n}\n\n/**\n * A band row. A `<th scope=\"rowgroup\">` spanning the row — the header of the `<tbody>` it\n * opens, which a screen reader announces as the reader enters the group — and sticky on\n * the cell, not the `<tr>`, as the header's and the leading row's are. Opaque, under the\n * sticky leading row's `z-[5]`: the first level `z-[4]` over the second's `z-[3]`, so a\n * period arriving from below covers the day still pinned under the one before. The line\n * over it is the cell's own `::before`, which moves with the stuck cell.\n */\nexport function DataTableGroupBand({\n level,\n colSpan,\n top,\n compact,\n label,\n end,\n ariaRowIndex,\n}: {\n level: 1 | 2;\n colSpan: number;\n top: number;\n compact: boolean;\n label: ReactNode;\n end: ReactNode;\n /** Under `virtualize` (0.48.2). */\n ariaRowIndex?: number;\n}) {\n return (\n <tr data-group-row={level} aria-rowindex={ariaRowIndex}>\n <th\n scope=\"rowgroup\"\n colSpan={colSpan}\n data-group-band={level}\n style={{ top }}\n className={cn(\n \"sticky p-0 text-start align-middle\",\n \"before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n level === 1 ? \"z-[4] bg-surface-2 font-semibold text-primary\" : \"z-[3] bg-surface font-medium text-secondary\",\n level === 1 ? (compact ? \"text-xs\" : \"text-sm\") : \"text-xs\",\n )}\n >\n {/* Pinned to what the scroller shows, as the expansion row's panel is: the end\n slot stays at the visible end of a table wider than its scroller. */}\n <div className=\"w-0 min-w-full\">\n <div\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-wrap items-baseline justify-between gap-x-3\",\n compact ? \"px-2 py-1\" : level === 1 ? \"px-3 py-2\" : \"px-3 py-1.5\",\n )}\n >\n <span className=\"min-w-0\">{label}</span>\n {end != null && end !== false && <span className=\"ms-auto tabular-nums\">{end}</span>}\n </div>\n </div>\n </th>\n </tr>\n );\n}\n\n/**\n * The load-more footer. Observed against the viewport (an implicit root sees through the\n * clipping scroller, so it works in a bounded body, under `fillHeight`, and in a page that\n * scrolls), and guarded: one call per row count, none while `loading`, so an observer\n * that reports twice — or a footer still in view while the owner sets `loading` — loads\n * once. When the rows arrive and the footer is still in view (a short slice), it loads\n * again, until the scroller is full.\n */\nexport function DataTableLoadMoreFooter({\n loadMore,\n rowCount,\n labels,\n compact,\n as: Tag = \"div\",\n}: {\n loadMore: DataTableLoadMore;\n rowCount: number;\n labels: { loadMore: string; allLoaded: string; loading: string };\n compact: boolean;\n as?: \"div\" | \"li\";\n}) {\n const { hasMore, onLoadMore, loading = false, auto = true, label, endLabel } = loadMore;\n const rootRef = useRef<HTMLElement | null>(null);\n const endRef = useRef<HTMLParagraphElement | null>(null);\n const firedAt = useRef<number | null>(null);\n const buttonFocused = useRef(false);\n // What the observer's callback acts on: the latest props, not the ones it was made with.\n const latest = useRef({ onLoadMore, loading, rowCount });\n useEffect(() => {\n latest.current = { onLoadMore, loading, rowCount };\n });\n\n // A fresh observer whenever the rows or the in-flight flag change: its first report is\n // the footer's position AFTER the new rows are laid out. Acting on a remembered \"in\n // view\" instead fired a second load the moment the rows arrived, while the footer was\n // still where the old last row had been (headless Chromium caught it).\n useEffect(() => {\n const el = rootRef.current;\n if (!auto || !hasMore || loading || !el || typeof IntersectionObserver === \"undefined\") return;\n const io = new IntersectionObserver(\n (entries) => {\n const now = latest.current;\n if (!entries.some((e) => e.isIntersecting) || now.loading || firedAt.current === now.rowCount) return;\n firedAt.current = now.rowCount;\n now.onLoadMore();\n },\n { rootMargin: \"200px\" },\n );\n io.observe(el);\n return () => io.disconnect();\n }, [auto, hasMore, loading, rowCount]);\n\n // A focused button that goes away (nothing left) hands its focus to the end line.\n useEffect(() => {\n if (!hasMore && buttonFocused.current) {\n buttonFocused.current = false;\n endRef.current?.focus();\n }\n }, [hasMore]);\n\n const end = endLabel === undefined ? labels.allLoaded : endLabel;\n return (\n <Tag\n ref={(el: HTMLElement | null) => {\n rootRef.current = el;\n }}\n data-table-load-more=\"\"\n className={cn(\n \"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-col items-center border-t border-subtle text-center\",\n compact ? \"px-2 py-2\" : \"px-3 py-3\",\n )}\n >\n {hasMore ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n aria-busy={loading || undefined}\n onFocus={() => (buttonFocused.current = true)}\n onBlur={() => (buttonFocused.current = false)}\n onClick={() => {\n if (loading) return;\n firedAt.current = rowCount;\n onLoadMore();\n }}\n >\n {loading ? (\n <span className=\"inline-flex items-center gap-2\">\n <Spinner label={null} className=\"size-4\" />\n <bdi>{labels.loading}</bdi>\n </span>\n ) : (\n (label ?? labels.loadMore)\n )}\n </Button>\n ) : end != null && end !== false ? (\n <p ref={endRef} tabIndex={-1} className=\"text-xs text-muted outline-none\">\n {end}\n </p>\n ) : null}\n {/* Mounted throughout, so a screen reader hears the load start. */}\n <span role=\"status\" className=\"sr-only\">\n {loading ? labels.loading : \"\"}\n </span>\n </Tag>\n );\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import type { SortState } from "./data-table-sort.js";
4
4
  import type { DataTableLabels } from "./data-table-labels.js";
5
5
  import type { DataTableColumn, DataTableProps, FilterState } from "./data-table.js";
6
6
  /** The wide-screen table's header row. */
7
- export declare function DataTableHead<T>({ visibleColumns, selection, compact, labels, locale, isServer, hasSortCallback, hasFilterCallback, sorts, filters, selectOptionsByKey, multiSort, resizable, widths, setWidths, headRefs, toggleSort, setFilterValue, clearFilter, startResize, moveResize, finishResize, }: {
7
+ export declare function DataTableHead<T>({ visibleColumns, selection, compact, labels, locale, isServer, hasSortCallback, hasFilterCallback, sorts, filters, selectOptionsByKey, multiSort, resizable, widths, setWidths, headRefs, toggleSort, setFilterValue, clearFilter, startResize, moveResize, finishResize, ariaRowIndex, lockedWidths, selectionLockKey, }: {
8
8
  visibleColumns: DataTableColumn<T>[];
9
9
  selection: DataTableProps<T>["selection"];
10
10
  compact: boolean;
@@ -33,4 +33,11 @@ export declare function DataTableHead<T>({ visibleColumns, selection, compact, l
33
33
  startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;
34
34
  moveResize: (e: React.PointerEvent<HTMLElement>) => void;
35
35
  finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;
36
+ /** Under `virtualize` (0.48.2): 1. */
37
+ ariaRowIndex?: number;
38
+ /** Under `virtualize` (0.48.2): each column's locked width, as the header cell's
39
+ * `min-width` — a `<col>` width alone gave way to a `grow` column's 100%. */
40
+ lockedWidths?: Record<string, number> | null;
41
+ /** The selection column's key in `lockedWidths`. */
42
+ selectionLockKey?: string;
36
43
  }): import("react").JSX.Element;
@@ -33,13 +33,17 @@ function DataTableHead({
33
33
  clearFilter,
34
34
  startResize,
35
35
  moveResize,
36
- finishResize
36
+ finishResize,
37
+ ariaRowIndex,
38
+ lockedWidths,
39
+ selectionLockKey
37
40
  }) {
38
- return /* @__PURE__ */ jsx("thead", { className: "bg-surface-2 text-secondary", children: /* @__PURE__ */ jsxs("tr", { className: "text-start", children: [
41
+ return /* @__PURE__ */ jsx("thead", { className: "bg-surface-2 text-secondary", children: /* @__PURE__ */ jsxs("tr", { className: "text-start", "aria-rowindex": ariaRowIndex, children: [
39
42
  selection && /* @__PURE__ */ jsx(
40
43
  "th",
41
44
  {
42
45
  scope: "col",
46
+ style: selectionLockKey && lockedWidths?.[selectionLockKey] !== void 0 ? { minWidth: lockedWidths[selectionLockKey] } : void 0,
43
47
  className: cn(
44
48
  "sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm",
45
49
  compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2"
@@ -83,7 +87,11 @@ function DataTableHead({
83
87
  },
84
88
  scope: "col",
85
89
  "aria-sort": sortable ? sortIdx === 0 ? sorts[0].dir === "asc" ? "ascending" : "descending" : "none" : void 0,
86
- style: ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : void 0,
90
+ style: ownHead.style || pinnedHead || lockedWidths?.[col.key] !== void 0 ? {
91
+ ...ownHead.style,
92
+ ...lockedWidths?.[col.key] !== void 0 ? { minWidth: lockedWidths[col.key] } : null,
93
+ ...pinnedHead
94
+ } : void 0,
87
95
  className: cn(
88
96
  "relative font-medium align-middle whitespace-nowrap",
89
97
  compact ? "px-2 py-1" : "px-3 py-2",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport { GROW_HEAD_CLASS } from \"./data-table-fit\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\">\n {selection && (\n <th\n scope=\"col\"\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n col.grow && GROW_HEAD_CLASS,\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {/* A `grow` column takes the rest: no width of its own to drag (0.48, KS N2). */}\n {resizable && !isTableOwnColumn(col.key) && !col.grow && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-head.tsx"],"sourcesContent":["import type { Dispatch, SetStateAction } from \"react\";\nimport { ArrowDown, ArrowUp, ArrowUpDown, Filter } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { defaultFilterState, isFilterActive, resolveFilter } from \"./data-table-filters\";\nimport type { FilterValue } from \"./data-table-filters\";\nimport type { SortState } from \"./data-table-sort\";\nimport { FilterPopover } from \"./data-table-filter-popover\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport { isTableOwnColumn } from \"./data-table-row-actions\";\nimport { columnLabel, logicalAlign, splitCellProps } from \"./data-table-shared\";\nimport { GROW_HEAD_CLASS } from \"./data-table-fit\";\nimport type { DataTableColumn, DataTableProps, FilterState } from \"./data-table\";\n\n// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort\n// button, the filter popover and the resize handle. Moved out of data-table.tsx whole\n// (refactor audit 2026-10, MOD-3 step 5); the state it shows and changes stays the\n// table's.\n\n/** Whether a column is end-aligned — the header then puts its filter button first. */\nconst END_ALIGN = /(^|\\s)text-end(?=\\s|$)/;\n\n/** The wide-screen table's header row. */\nexport function DataTableHead<T>({\n visibleColumns,\n selection,\n compact,\n labels,\n locale,\n isServer,\n hasSortCallback,\n hasFilterCallback,\n sorts,\n filters,\n selectOptionsByKey,\n multiSort,\n resizable,\n widths,\n setWidths,\n headRefs,\n toggleSort,\n setFilterValue,\n clearFilter,\n startResize,\n moveResize,\n finishResize,\n ariaRowIndex,\n lockedWidths,\n selectionLockKey,\n}: {\n visibleColumns: DataTableColumn<T>[];\n selection: DataTableProps<T>[\"selection\"];\n compact: boolean;\n labels: DataTableLabels;\n locale?: string;\n isServer: boolean;\n /** The caller owns the sort (`onSortsChange`), so a server table can offer it. */\n hasSortCallback: boolean;\n /** The caller owns the filters (`onFiltersChange`), likewise. */\n hasFilterCallback: boolean;\n sorts: SortState[];\n filters: FilterState;\n selectOptionsByKey: Record<string, { value: string; label: string }[]>;\n multiSort: boolean;\n resizable: boolean;\n widths: Record<string, number>;\n setWidths: Dispatch<SetStateAction<Record<string, number>>>;\n /** Filled here, one `<th>` per column — what a resize and \"Auto-size\" measure. */\n headRefs: Map<string, HTMLTableCellElement>;\n toggleSort: (key: string, shiftKey: boolean) => void;\n setFilterValue: (key: string, value: FilterValue) => void;\n clearFilter: (key: string) => void;\n startResize: (e: React.PointerEvent<HTMLElement>, key: string) => void;\n moveResize: (e: React.PointerEvent<HTMLElement>) => void;\n finishResize: (e: React.PointerEvent<HTMLElement>, commit: boolean) => void;\n /** Under `virtualize` (0.48.2): 1. */\n ariaRowIndex?: number;\n /** Under `virtualize` (0.48.2): each column's locked width, as the header cell's\n * `min-width` — a `<col>` width alone gave way to a `grow` column's 100%. */\n lockedWidths?: Record<string, number> | null;\n /** The selection column's key in `lockedWidths`. */\n selectionLockKey?: string;\n}) {\n return (\n <thead className=\"bg-surface-2 text-secondary\">\n <tr className=\"text-start\" aria-rowindex={ariaRowIndex}>\n {selection && (\n <th\n scope=\"col\"\n style={\n selectionLockKey && lockedWidths?.[selectionLockKey] !== undefined\n ? { minWidth: lockedWidths[selectionLockKey] }\n : undefined\n }\n className={cn(\n \"sticky top-0 z-10 bg-surface-2 align-middle backdrop-blur-sm\",\n compact ? \"w-8 px-2 py-1\" : \"w-10 px-3 py-2\",\n )}\n >\n <input\n type=\"checkbox\"\n className={cn(\"align-middle accent-indigo-600\", compact ? \"size-3.5\" : \"size-4\")}\n checked={selection.allSelected}\n ref={(el) => {\n if (el) el.indeterminate = selection.someSelected && !selection.allSelected;\n }}\n onChange={(e) => selection.onToggleAll(e.target.checked)}\n aria-label={labels.selectAllRows}\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n // In server mode the utilities stay hidden unless the caller\n // takes them over and feeds them back into the server query.\n const sortable = (!isServer || hasSortCallback) && !!col.sortBy;\n const filter = !isServer || hasFilterCallback ? resolveFilter(col) : null;\n const filterState = filters[col.key] ?? (filter ? defaultFilterState(filter) : undefined);\n const sortIdx = sorts.findIndex((s) => s.key === col.key);\n const active = sortIdx !== -1;\n const Icon = active ? (sorts[sortIdx].dir === \"asc\" ? ArrowUp : ArrowDown) : ArrowUpDown;\n const filterActive = filterState && isFilterActive(filterState);\n // Logical, and a legacy `text-right` still counts — see `logicalAlign`.\n const headClass = logicalAlign(col.headClassName);\n const isEndAligned = !!headClass && END_ALIGN.test(headClass);\n const width = widths[col.key];\n const ownHead = splitCellProps(col.headProps);\n const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;\n return (\n <th\n {...ownHead.attrs}\n key={col.key}\n ref={(el) => {\n if (el) headRefs.set(col.key, el);\n else headRefs.delete(col.key);\n }}\n scope=\"col\"\n // Only a header the user can actually sort says anything: an\n // `aria-sort` on a plain column is a control that is not there,\n // and a persisted or URL sort can name a column with no `sortBy`\n // (the sort chain drops it silently) — which would otherwise\n // claim a sort the table is not applying.\n //\n // \"none\" on the rest is the half that was missing. Without it a\n // sortable header is indistinguishable from a plain one until the\n // user presses it. Secondary criteria read \"none\" too: ARIA allows\n // exactly one sorted column, and the priority is carried by the\n // badge and by what the header button announces on press.\n aria-sort={\n sortable\n ? sortIdx === 0\n ? sorts[0].dir === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : \"none\"\n : undefined\n }\n style={\n ownHead.style || pinnedHead || lockedWidths?.[col.key] !== undefined\n ? {\n ...ownHead.style,\n ...(lockedWidths?.[col.key] !== undefined ? { minWidth: lockedWidths[col.key] } : null),\n ...pinnedHead,\n }\n : undefined\n }\n className={cn(\n \"relative font-medium align-middle whitespace-nowrap\",\n compact ? \"px-2 py-1\" : \"px-3 py-2\",\n // Keep the header visible while the user scrolls the page.\n // Each <th> carries its own bg so the row doesn't render\n // transparent over the data rows underneath.\n \"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm\",\n col.grow && GROW_HEAD_CLASS,\n logicalAlign(ownHead.className),\n headClass,\n )}\n >\n <div\n className={cn(\n \"flex items-center gap-1\",\n isEndAligned && \"flex-row-reverse\",\n )}\n >\n <Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>\n <button\n type=\"button\"\n onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}\n disabled={!sortable}\n className={cn(\n \"flex items-center gap-1 text-start\",\n sortable && \"hover:text-primary\",\n )}\n >\n <span>{col.header}</span>\n {sortable && (\n <Icon className={cn(\"size-3\", active ? \"opacity-100\" : \"opacity-40\")} />\n )}\n {/* Priority badge, only meaningful with 2+ sort keys */}\n {active && sorts.length > 1 && (\n <span className=\"text-micro font-semibold leading-none text-brand\">\n {sortIdx + 1}\n </span>\n )}\n </button>\n </Tooltip>\n {filter && (\n <Popover\n // 420 px at Normal, in rem so the calendar grows with its\n // text (§3.2); Popover caps it at the viewport.\n width={filter.type === \"date\" ? \"26.25rem\" : undefined}\n // The panel is THIS column's filter, and says so. Unnamed it was\n // announced as the kit-wide fallback, \"Popover\"; then as \"Filter\"\n // in every column alike (round 0.34 Wave 2, BUG-7c).\n labels={{ panel: labels.filterColumn(columnLabel(col, labels), false) }}\n trigger={({ open, toggle, ref }) => (\n <button\n type=\"button\"\n ref={ref}\n onClick={toggle}\n // Named for its column, and for being filtered: the tint\n // alone said that to nobody who cannot see it (BUG-7c).\n aria-label={labels.filterColumn(columnLabel(col, labels), Boolean(filterActive))}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n className={cn(\n \"rounded p-1 transition-colors\",\n filterActive\n ? \"bg-brand-soft text-brand-muted\"\n : \"text-placeholder hover:text-secondary hover:bg-hover\",\n )}\n >\n <Filter className=\"size-3\" />\n </button>\n )}\n >\n {() => (\n <FilterPopover\n column={col}\n state={filterState ?? defaultFilterState(filter)}\n onChange={(next) => setFilterValue(col.key, next)}\n onClear={() => clearFilter(col.key)}\n selectOptions={selectOptionsByKey[col.key] ?? []}\n labels={labels}\n locale={locale}\n />\n )}\n </Popover>\n )}\n </div>\n {/* A `grow` column takes the rest: no width of its own to drag (0.48, KS N2). */}\n {resizable && !isTableOwnColumn(col.key) && !col.grow && (\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a\n pointer-only drag handle for a visual preference (column width); it is\n deliberately not a tab stop, and no content or action depends on it. */\n <span\n role=\"separator\"\n aria-orientation=\"vertical\"\n onPointerDown={(e) => startResize(e, col.key)}\n onPointerMove={moveResize}\n onPointerUp={(e) => finishResize(e, true)}\n onPointerCancel={(e) => finishResize(e, false)}\n onDoubleClick={(e) => {\n e.preventDefault();\n setWidths((w) => {\n const next = { ...w };\n delete next[col.key];\n return next;\n });\n }}\n // `touch-none`: without it the browser claims a finger drag for\n // scrolling before the first `pointermove` ever arrives.\n // `end-0` and a translate that flips with it: the handle sits on the column's\n // trailing edge, which is its left one in a right-to-left table.\n className=\"absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-brand-soft-hover\"\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n );\n}\n"],"mappings":"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","names":[]}
@@ -10,8 +10,10 @@ import type { ReactNode } from "react";
10
10
  * the content (a full-width disclosure header paints its own fill) and moving with the
11
11
  * cell; a collapsed table's row border would stay behind at the row's place.
12
12
  */
13
- export declare function DataTableLeadingRow({ colSpan, sticky, children, }: {
13
+ export declare function DataTableLeadingRow({ colSpan, sticky, ariaRowIndex, children, }: {
14
14
  colSpan: number;
15
15
  sticky?: boolean;
16
+ /** Under `virtualize` (0.48.2). */
17
+ ariaRowIndex?: number;
16
18
  children: ReactNode;
17
19
  }): import("react").JSX.Element;
@@ -5,6 +5,7 @@ import { cn } from "../lib/cn.js";
5
5
  function DataTableLeadingRow({
6
6
  colSpan,
7
7
  sticky = false,
8
+ ariaRowIndex,
8
9
  children
9
10
  }) {
10
11
  const cellRef = useRef(null);
@@ -20,7 +21,7 @@ function DataTableLeadingRow({
20
21
  observer.observe(head);
21
22
  return () => observer.disconnect();
22
23
  }, [sticky]);
23
- return /* @__PURE__ */ jsx("tr", { className: cn(!sticky && "border-t border-subtle"), children: /* @__PURE__ */ jsx(
24
+ return /* @__PURE__ */ jsx("tr", { "aria-rowindex": ariaRowIndex, className: cn(!sticky && "border-t border-subtle"), children: /* @__PURE__ */ jsx(
24
25
  "td",
25
26
  {
26
27
  ref: cellRef,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-leading-row.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * The desktop table's `leadingRow`: one full-width cell above the data rows. With\n * `sticky` (0.48, keksdose dev #603) the cell is pinned under the sticky header while the\n * rows scroll — `top` is the header's height, measured and followed (a header that wraps\n * at Large text grows). On the cell, not the `<tr>`: position:sticky on a table row is not\n * reliable across browsers, the reason the header's sticky sits on each `<th>`. Opaque\n * (`bg-surface`), so the rows pass beneath it; `z-[5]`, under the header's `z-10` and over\n * the rows' own sticky cells. The line over it is the cell's own `::before`, drawn over\n * the content (a full-width disclosure header paints its own fill) and moving with the\n * cell; a collapsed table's row border would stay behind at the row's place.\n */\nexport function DataTableLeadingRow({\n colSpan,\n sticky = false,\n children,\n}: {\n colSpan: number;\n sticky?: boolean;\n children: ReactNode;\n}) {\n const cellRef = useRef<HTMLTableCellElement>(null);\n const [top, setTop] = useState(0);\n useLayoutEffect(() => {\n if (!sticky) return;\n const head = cellRef.current?.closest(\"table\")?.tHead;\n if (!head) return;\n const measure = () => setTop(head.getBoundingClientRect().height);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(head);\n return () => observer.disconnect();\n }, [sticky]);\n return (\n <tr className={cn(!sticky && \"border-t border-subtle\")}>\n <td\n ref={cellRef}\n colSpan={colSpan}\n data-leading-row={sticky ? \"sticky\" : \"\"}\n className={cn(\n \"p-0\",\n sticky &&\n \"sticky z-[5] bg-surface before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n )}\n style={sticky ? { top } : undefined}\n >\n {children}\n </td>\n </tr>\n );\n}\n"],"mappings":";AAuCM;AAvCN,SAAS,iBAAiB,QAAQ,gBAAgB;AAElD,SAAS,UAAU;AAaZ,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA,SAAS;AAAA,EACT;AACF,GAIG;AACD,QAAM,UAAU,OAA6B,IAAI;AACjD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAChC,kBAAgB,MAAM;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,QAAQ,SAAS,QAAQ,OAAO,GAAG;AAChD,QAAI,CAAC,KAAM;AACX,UAAM,UAAU,MAAM,OAAO,KAAK,sBAAsB,EAAE,MAAM;AAChE,YAAQ;AACR,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,aAAS,QAAQ,IAAI;AACrB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,MAAM,CAAC;AACX,SACE,oBAAC,QAAG,WAAW,GAAG,CAAC,UAAU,wBAAwB,GACnD;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,oBAAkB,SAAS,WAAW;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,UACE;AAAA,MACJ;AAAA,MACA,OAAO,SAAS,EAAE,IAAI,IAAI;AAAA,MAEzB;AAAA;AAAA,EACH,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-leading-row.tsx"],"sourcesContent":["import { useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * The desktop table's `leadingRow`: one full-width cell above the data rows. With\n * `sticky` (0.48, keksdose dev #603) the cell is pinned under the sticky header while the\n * rows scroll — `top` is the header's height, measured and followed (a header that wraps\n * at Large text grows). On the cell, not the `<tr>`: position:sticky on a table row is not\n * reliable across browsers, the reason the header's sticky sits on each `<th>`. Opaque\n * (`bg-surface`), so the rows pass beneath it; `z-[5]`, under the header's `z-10` and over\n * the rows' own sticky cells. The line over it is the cell's own `::before`, drawn over\n * the content (a full-width disclosure header paints its own fill) and moving with the\n * cell; a collapsed table's row border would stay behind at the row's place.\n */\nexport function DataTableLeadingRow({\n colSpan,\n sticky = false,\n ariaRowIndex,\n children,\n}: {\n colSpan: number;\n sticky?: boolean;\n /** Under `virtualize` (0.48.2). */\n ariaRowIndex?: number;\n children: ReactNode;\n}) {\n const cellRef = useRef<HTMLTableCellElement>(null);\n const [top, setTop] = useState(0);\n useLayoutEffect(() => {\n if (!sticky) return;\n const head = cellRef.current?.closest(\"table\")?.tHead;\n if (!head) return;\n const measure = () => setTop(head.getBoundingClientRect().height);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(head);\n return () => observer.disconnect();\n }, [sticky]);\n return (\n <tr aria-rowindex={ariaRowIndex} className={cn(!sticky && \"border-t border-subtle\")}>\n <td\n ref={cellRef}\n colSpan={colSpan}\n data-leading-row={sticky ? \"sticky\" : \"\"}\n className={cn(\n \"p-0\",\n sticky &&\n \"sticky z-[5] bg-surface before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']\",\n )}\n style={sticky ? { top } : undefined}\n >\n {children}\n </td>\n </tr>\n );\n}\n"],"mappings":";AA0CM;AA1CN,SAAS,iBAAiB,QAAQ,gBAAgB;AAElD,SAAS,UAAU;AAaZ,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAMG;AACD,QAAM,UAAU,OAA6B,IAAI;AACjD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAChC,kBAAgB,MAAM;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,QAAQ,SAAS,QAAQ,OAAO,GAAG;AAChD,QAAI,CAAC,KAAM;AACX,UAAM,UAAU,MAAM,OAAO,KAAK,sBAAsB,EAAE,MAAM;AAChE,YAAQ;AACR,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,aAAS,QAAQ,IAAI;AACrB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,MAAM,CAAC;AACX,SACE,oBAAC,QAAG,iBAAe,cAAc,WAAW,GAAG,CAAC,UAAU,wBAAwB,GAChF;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA,oBAAkB,SAAS,WAAW;AAAA,MACtC,WAAW;AAAA,QACT;AAAA,QACA,UACE;AAAA,MACJ;AAAA,MACA,OAAO,SAAS,EAAE,IAAI,IAAI;AAAA,MAEzB;AAAA;AAAA,EACH,GACF;AAEJ;","names":[]}
@@ -159,7 +159,7 @@ function useRowActionColumn({
159
159
  () => swipeColumn ? [...actionColumns, swipeColumn] : actionColumns,
160
160
  [actionColumns, swipeColumn]
161
161
  );
162
- const swipesFromActions = (row) => {
162
+ const swipesFromActions = useCallback((row) => {
163
163
  const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
164
164
  if (!swipes.length) return null;
165
165
  const toSwipe = (a) => ({
@@ -174,7 +174,7 @@ function useRowActionColumn({
174
174
  start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
175
175
  end: swipes.filter((a) => a.swipe === "end").map(toSwipe)
176
176
  };
177
- };
177
+ }, [rowActionsFor]);
178
178
  return { columns, swipesFromActions };
179
179
  }
180
180
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/data-table-row-actions.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Pencil, Trash2 } from \"lucide-react\";\nimport { RowActions, type RowAction, type RowActionTone } from \"./row-actions\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport type {\n DataTableColumn,\n DataTableDensity,\n DataTableDesktopSwipeActions,\n DataTableProps,\n DataTableRowAction,\n MobileSwipeActions,\n} from \"./data-table\";\n\n// DataTable's per-row actions (`rowActions`): the actions column on a wide screen and\n// the swipes on a phone card. Moved out of data-table.tsx whole (refactor audit\n// 2026-10, MOD-3 step 1); internal — the public types stay in data-table.tsx.\n\n/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nexport const ROW_ACTIONS_KEY = \"__rowActions\";\n\n/** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the\n * table's own too. */\nexport const SWIPE_ACTIONS_KEY = \"__swipeActions\";\n\n/** A column the TABLE adds, not the caller: out of the column settings and the count,\n * never resized, never the totals row's header. */\nexport function isTableOwnColumn(key: string): boolean {\n return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;\n}\n\n/**\n * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE\n * \"Actions\" heading on every table with an actions column): the caller's node, else the\n * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.\n */\nexport function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode {\n if (header === false) return <span className=\"sr-only\">{word}</span>;\n return header ?? word;\n}\n\n/** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN\n * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays\n * on the curtain and reads muted. */\nfunction swipeTone(action: SwipeAction): RowActionTone {\n switch (action.tone) {\n case \"danger\":\n case \"warning\":\n case \"info\":\n return action.tone;\n default:\n return \"muted\";\n }\n}\n\n/**\n * A row's swipe plan as desktop row actions (dev#598) — what DataTable's\n * `desktopSwipeActions` column draws, for an app that builds its own actions column (or\n * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two\n * cannot drift (public since 0.46.1, keksdose N3).\n *\n * `end` then `start` — the physical `right` / `left` only where the logical side is\n * missing, as the card reads them — each action once, told apart by its `id`, else by its\n * label (two stages with one label would be one button twice). Each button runs the\n * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any\n * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a\n * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.\n *\n * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />\n */\nexport function swipePlanRowActions<T>(\n plan: MobileSwipeActions | null | undefined,\n row: T,\n options: Pick<DataTableDesktopSwipeActions<T>, \"omit\" | \"commit\"> = {},\n): RowAction[] {\n if (!plan) return [];\n const out: RowAction[] = [];\n const seen = new Set<string>();\n for (const action of [...(plan.end ?? plan.right ?? []), ...(plan.start ?? plan.left ?? [])]) {\n const key = action.id ?? action.label;\n if (seen.has(key)) continue;\n seen.add(key);\n if (options.omit?.(action, row)) continue;\n const commit = typeof options.commit === \"function\" ? options.commit(action, row) : options.commit;\n out.push({\n key,\n label: action.label,\n icon: isValidElement(action.icon)\n ? action.icon\n : action.icon != null && action.icon !== false\n ? <>{action.icon}</>\n : undefined,\n tone: swipeTone(action),\n commit,\n onSelect: action.onCommit,\n });\n }\n return out;\n}\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\n}\n\n/** A resolved row action as {@link RowActions} takes it. Its `icon` is any node; a\n * non-element one (a string glyph) is wrapped, since RowAction wants an element. */\nfunction toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {\n return {\n key: a.key,\n label: a.label,\n icon: isValidElement(a.icon) ? a.icon : a.icon != null && a.icon !== false ? <>{a.icon}</> : undefined,\n tone: a.tone,\n disabledReason: a.action.disabledReason?.(row),\n onSelect: () => void runRowAction(a.action, row),\n };\n}\n\n/**\n * The caller's columns with the actions column appended, and the same actions as a\n * phone card's swipes (`swipesFromActions`; DataTable uses it unless the caller passes\n * `mobileSwipeActions`). Without `rowActions` the columns come back as given.\n *\n * The actions column is appended to the caller's columns rather than drawn as a\n * second special case beside the selection column: it then gets the cell padding,\n * the widths and the phone rules (`mobileHidden`) every other column gets.\n */\nexport function useRowActionColumn<T>({\n columns: columnsProp,\n rowActions,\n rowActionsHeader,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n}: {\n columns: DataTableColumn<T>[];\n rowActions: DataTableProps<T>[\"rowActions\"];\n rowActionsHeader?: DataTableProps<T>[\"rowActionsHeader\"];\n rowName: DataTableProps<T>[\"rowName\"];\n labels: DataTableLabels;\n density: DataTableDensity;\n mobileSwipeActions?: DataTableProps<T>[\"mobileSwipeActions\"];\n desktopSwipeActions?: DataTableProps<T>[\"desktopSwipeActions\"];\n}): {\n columns: DataTableColumn<T>[];\n swipesFromActions: (row: T) => MobileSwipeActions | null;\n} {\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n // dev#598: the desktop half of the caller's swipes. On by default, except beside\n // `rowActions`, whose column already is the desktop's (and whose swipes these usually\n // are). The options object is often a fresh literal per render; its parts are the deps.\n const swipeOptions = typeof desktopSwipeActions === \"object\" ? desktopSwipeActions : undefined;\n const swipeColumnOn =\n !!mobileSwipeActions && (desktopSwipeActions === undefined ? !rowActions : desktopSwipeActions !== false);\n const swipeOmit = swipeOptions?.omit;\n const swipeName = swipeOptions?.name;\n const swipeCollapse = swipeOptions?.collapse;\n const swipeCommit = swipeOptions?.commit;\n const swipeHeader = swipeOptions?.header;\n const swipeColumn = useMemo<DataTableColumn<T> | null>(\n () =>\n swipeColumnOn\n ? {\n key: SWIPE_ACTIONS_KEY,\n header: actionsHeading(swipeHeader, labels.actions),\n cell: (row) => (\n <RowActions\n actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}\n name={(swipeName ?? rowName)?.(row)}\n collapse={swipeCollapse}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n // The last column's tooltips open toward the row's start.\n tooltipSide=\"start\"\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n // The heading over the buttons, at the row's end (0.46.1).\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n }\n : null,\n [\n swipeColumnOn,\n mobileSwipeActions,\n swipeOmit,\n swipeCommit,\n swipeName,\n rowName,\n swipeCollapse,\n swipeHeader,\n density,\n labels.actions,\n ],\n );\n const actionColumns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: actionsHeading(rowActionsHeader, labels.actions),\n // Through RowActions (0.32, §10.8): the icons inline at Normal, one \"⋯\"\n // menu at Large once there are two — every label at once does not fit a\n // row. Each control stops its own click: the row's handler already skips\n // its controls, and this is for the caller who wraps the table in a\n // clickable element of their own.\n cell: (row) => (\n <RowActions\n actions={rowActionsFor(row).map((a) => toRowAction(a, row))}\n name={rowName?.(row)}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density],\n );\n const columns = useMemo(\n () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),\n [actionColumns, swipeColumn],\n );\n const swipesFromActions = (row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n // The row action's tone, painted by SwipeableRow: its soft wash idle, its fill\n // and that fill's foreground armed (0.33).\n tone: a.tone === \"danger\" ? \"danger\" : \"brand\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\n };\n return { columns, swipesFromActions };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/data-table-row-actions.tsx"],"sourcesContent":["import { isValidElement, useCallback, useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Pencil, Trash2 } from \"lucide-react\";\nimport { RowActions, type RowAction, type RowActionTone } from \"./row-actions\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport type { DataTableLabels } from \"./data-table-labels\";\nimport type {\n DataTableColumn,\n DataTableDensity,\n DataTableDesktopSwipeActions,\n DataTableProps,\n DataTableRowAction,\n MobileSwipeActions,\n} from \"./data-table\";\n\n// DataTable's per-row actions (`rowActions`): the actions column on a wide screen and\n// the swipes on a phone card. Moved out of data-table.tsx whole (refactor audit\n// 2026-10, MOD-3 step 1); internal — the public types stay in data-table.tsx.\n\n/** The actions column's key. Not a column a caller declares, so it is kept out of the\n * column-settings panel. */\nexport const ROW_ACTIONS_KEY = \"__rowActions\";\n\n/** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the\n * table's own too. */\nexport const SWIPE_ACTIONS_KEY = \"__swipeActions\";\n\n/** A column the TABLE adds, not the caller: out of the column settings and the count,\n * never resized, never the totals row's header. */\nexport function isTableOwnColumn(key: string): boolean {\n return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;\n}\n\n/**\n * An actions column's heading (0.46.1, keksdose dev#598 rework — Marcel: a VISIBLE\n * \"Actions\" heading on every table with an actions column): the caller's node, else the\n * word on screen; `false` is the pre-0.46.1 heading, a screen reader's name only.\n */\nexport function actionsHeading(header: ReactNode | false | undefined, word: string): ReactNode {\n if (header === false) return <span className=\"sr-only\">{word}</span>;\n return header ?? word;\n}\n\n/** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN\n * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays\n * on the curtain and reads muted. */\nfunction swipeTone(action: SwipeAction): RowActionTone {\n switch (action.tone) {\n case \"danger\":\n case \"warning\":\n case \"info\":\n return action.tone;\n default:\n return \"muted\";\n }\n}\n\n/**\n * A row's swipe plan as desktop row actions (dev#598) — what DataTable's\n * `desktopSwipeActions` column draws, for an app that builds its own actions column (or\n * a `RowActions` outside a table) from the same `mobileSwipeActions` plan, so the two\n * cannot drift (public since 0.46.1, keksdose N3).\n *\n * `end` then `start` — the physical `right` / `left` only where the logical side is\n * missing, as the card reads them — each action once, told apart by its `id`, else by its\n * label (two stages with one label would be one button twice). Each button runs the\n * swipe's own `onCommit`; a swipe's `danger` / `warning` / `info` tone carries over, any\n * other reads `muted`. `options.omit` leaves an action out, `options.commit` gives it a\n * `CommitScope` (see {@link DataTableDesktopSwipeActions}). A `null` plan is `[]`.\n *\n * <RowActions actions={swipePlanRowActions(plan(row), row, { omit })} name={row.title} />\n */\nexport function swipePlanRowActions<T>(\n plan: MobileSwipeActions | null | undefined,\n row: T,\n options: Pick<DataTableDesktopSwipeActions<T>, \"omit\" | \"commit\"> = {},\n): RowAction[] {\n if (!plan) return [];\n const out: RowAction[] = [];\n const seen = new Set<string>();\n for (const action of [...(plan.end ?? plan.right ?? []), ...(plan.start ?? plan.left ?? [])]) {\n const key = action.id ?? action.label;\n if (seen.has(key)) continue;\n seen.add(key);\n if (options.omit?.(action, row)) continue;\n const commit = typeof options.commit === \"function\" ? options.commit(action, row) : options.commit;\n out.push({\n key,\n label: action.label,\n icon: isValidElement(action.icon)\n ? action.icon\n : action.icon != null && action.icon !== false\n ? <>{action.icon}</>\n : undefined,\n tone: swipeTone(action),\n commit,\n onSelect: action.onCommit,\n });\n }\n return out;\n}\n\ninterface ResolvedRowAction<T> {\n key: string;\n label: string;\n icon: ReactNode;\n tone: \"default\" | \"danger\";\n swipe: \"start\" | \"end\" | false;\n action: DataTableRowAction<T>;\n}\n\nfunction resolveRowAction<T>(\n action: DataTableRowAction<T>,\n index: number,\n labels: DataTableLabels,\n): ResolvedRowAction<T> {\n const tone = action.tone ?? (action.kind === \"delete\" ? \"danger\" : \"default\");\n return {\n key: action.kind ?? `${index}:${action.label ?? \"\"}`,\n label: action.label ?? (action.kind === \"edit\" ? labels.edit : action.kind === \"delete\" ? labels.delete : \"\"),\n icon:\n action.icon ??\n (action.kind === \"edit\" ? <Pencil /> : action.kind === \"delete\" ? <Trash2 /> : null),\n tone,\n swipe: action.swipe ?? (tone === \"danger\" ? \"start\" : \"end\"),\n action,\n };\n}\n\n/** Ask, then act. */\nasync function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {\n if (action.confirm && !(await action.confirm(row))) return;\n action.onAction(row);\n}\n\n/** A resolved row action as {@link RowActions} takes it. Its `icon` is any node; a\n * non-element one (a string glyph) is wrapped, since RowAction wants an element. */\nfunction toRowAction<T>(a: ResolvedRowAction<T>, row: T): RowAction {\n return {\n key: a.key,\n label: a.label,\n icon: isValidElement(a.icon) ? a.icon : a.icon != null && a.icon !== false ? <>{a.icon}</> : undefined,\n tone: a.tone,\n disabledReason: a.action.disabledReason?.(row),\n onSelect: () => void runRowAction(a.action, row),\n };\n}\n\n/**\n * The caller's columns with the actions column appended, and the same actions as a\n * phone card's swipes (`swipesFromActions`; DataTable uses it unless the caller passes\n * `mobileSwipeActions`). Without `rowActions` the columns come back as given.\n *\n * The actions column is appended to the caller's columns rather than drawn as a\n * second special case beside the selection column: it then gets the cell padding,\n * the widths and the phone rules (`mobileHidden`) every other column gets.\n */\nexport function useRowActionColumn<T>({\n columns: columnsProp,\n rowActions,\n rowActionsHeader,\n rowName,\n labels,\n density,\n mobileSwipeActions,\n desktopSwipeActions,\n}: {\n columns: DataTableColumn<T>[];\n rowActions: DataTableProps<T>[\"rowActions\"];\n rowActionsHeader?: DataTableProps<T>[\"rowActionsHeader\"];\n rowName: DataTableProps<T>[\"rowName\"];\n labels: DataTableLabels;\n density: DataTableDensity;\n mobileSwipeActions?: DataTableProps<T>[\"mobileSwipeActions\"];\n desktopSwipeActions?: DataTableProps<T>[\"desktopSwipeActions\"];\n}): {\n columns: DataTableColumn<T>[];\n swipesFromActions: (row: T) => MobileSwipeActions | null;\n} {\n const rowActionsFor = useCallback(\n (row: T): ResolvedRowAction<T>[] =>\n rowActions\n ? (typeof rowActions === \"function\" ? rowActions(row) : rowActions)\n .filter((a) => !a.hidden?.(row))\n .map((a, i) => resolveRowAction(a, i, labels))\n : [],\n [rowActions, labels],\n );\n // dev#598: the desktop half of the caller's swipes. On by default, except beside\n // `rowActions`, whose column already is the desktop's (and whose swipes these usually\n // are). The options object is often a fresh literal per render; its parts are the deps.\n const swipeOptions = typeof desktopSwipeActions === \"object\" ? desktopSwipeActions : undefined;\n const swipeColumnOn =\n !!mobileSwipeActions && (desktopSwipeActions === undefined ? !rowActions : desktopSwipeActions !== false);\n const swipeOmit = swipeOptions?.omit;\n const swipeName = swipeOptions?.name;\n const swipeCollapse = swipeOptions?.collapse;\n const swipeCommit = swipeOptions?.commit;\n const swipeHeader = swipeOptions?.header;\n const swipeColumn = useMemo<DataTableColumn<T> | null>(\n () =>\n swipeColumnOn\n ? {\n key: SWIPE_ACTIONS_KEY,\n header: actionsHeading(swipeHeader, labels.actions),\n cell: (row) => (\n <RowActions\n actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}\n name={(swipeName ?? rowName)?.(row)}\n collapse={swipeCollapse}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n // The last column's tooltips open toward the row's start.\n tooltipSide=\"start\"\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n // The heading over the buttons, at the row's end (0.46.1).\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n }\n : null,\n [\n swipeColumnOn,\n mobileSwipeActions,\n swipeOmit,\n swipeCommit,\n swipeName,\n rowName,\n swipeCollapse,\n swipeHeader,\n density,\n labels.actions,\n ],\n );\n const actionColumns = useMemo<DataTableColumn<T>[]>(\n () =>\n rowActions\n ? [\n ...columnsProp,\n {\n key: ROW_ACTIONS_KEY,\n header: actionsHeading(rowActionsHeader, labels.actions),\n // Through RowActions (0.32, §10.8): the icons inline at Normal, one \"⋯\"\n // menu at Large once there are two — every label at once does not fit a\n // row. Each control stops its own click: the row's handler already skips\n // its controls, and this is for the caller who wraps the table in a\n // clickable element of their own.\n cell: (row) => (\n <RowActions\n actions={rowActionsFor(row).map((a) => toRowAction(a, row))}\n name={rowName?.(row)}\n size={density === \"compact\" ? \"2xs\" : \"xs\"}\n labels={{ actions: labels.actions }}\n className=\"justify-end\"\n />\n ),\n className: \"w-px whitespace-nowrap text-end\",\n headClassName: \"w-px text-end\",\n // On a phone the same actions are the card's swipes.\n mobileHidden: true,\n noRowLink: true,\n },\n ]\n : columnsProp,\n [columnsProp, rowActions, rowActionsHeader, rowActionsFor, rowName, labels.actions, density],\n );\n const columns = useMemo(\n () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),\n [actionColumns, swipeColumn],\n );\n // A stable function (0.48.2): the phone cards compare it by identity.\n const swipesFromActions = useCallback((row: T): MobileSwipeActions | null => {\n const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));\n if (!swipes.length) return null;\n const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({\n label: a.label,\n icon: a.icon,\n onCommit: () => void runRowAction(a.action, row),\n // The row action's tone, painted by SwipeableRow: its soft wash idle, its fill\n // and that fill's foreground armed (0.33).\n tone: a.tone === \"danger\" ? \"danger\" : \"brand\",\n });\n return {\n start: swipes.filter((a) => a.swipe === \"start\").map(toSwipe),\n end: swipes.filter((a) => a.swipe === \"end\").map(toSwipe),\n };\n }, [rowActionsFor]);\n return { columns, swipesFromActions };\n}\n"],"mappings":"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","names":[]}