cursedbelt 5.30.0 → 5.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/react/Toast.d.ts +1 -1
  2. package/dist/react/Toast.d.ts.map +1 -1
  3. package/dist/react/Toast.js +19 -4
  4. package/dist/react/Toast.js.map +1 -1
  5. package/dist/react/components/Button.d.ts +16 -1
  6. package/dist/react/components/Button.d.ts.map +1 -1
  7. package/dist/react/components/Button.js +34 -5
  8. package/dist/react/components/Button.js.map +1 -1
  9. package/dist/react/components/ChoiceGroup.d.ts +20 -1
  10. package/dist/react/components/ChoiceGroup.d.ts.map +1 -1
  11. package/dist/react/components/ChoiceGroup.js +15 -2
  12. package/dist/react/components/ChoiceGroup.js.map +1 -1
  13. package/dist/react/components/ConfirmDialog.d.ts +4 -1
  14. package/dist/react/components/ConfirmDialog.d.ts.map +1 -1
  15. package/dist/react/components/ConfirmDialog.js +2 -2
  16. package/dist/react/components/ConfirmDialog.js.map +1 -1
  17. package/dist/react/components/CopyButton.d.ts +10 -1
  18. package/dist/react/components/CopyButton.d.ts.map +1 -1
  19. package/dist/react/components/CopyButton.js +2 -2
  20. package/dist/react/components/CopyButton.js.map +1 -1
  21. package/dist/react/components/DataTable/DataTable.d.ts +27 -2
  22. package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
  23. package/dist/react/components/DataTable/DataTable.js +173 -45
  24. package/dist/react/components/DataTable/DataTable.js.map +1 -1
  25. package/dist/react/components/DataTable/RowActions.d.ts +4 -1
  26. package/dist/react/components/DataTable/RowActions.d.ts.map +1 -1
  27. package/dist/react/components/DataTable/RowActions.js +2 -2
  28. package/dist/react/components/DataTable/RowActions.js.map +1 -1
  29. package/dist/react/components/DataTable/columnPriority.d.ts +30 -0
  30. package/dist/react/components/DataTable/columnPriority.d.ts.map +1 -0
  31. package/dist/react/components/DataTable/columnPriority.js +45 -0
  32. package/dist/react/components/DataTable/columnPriority.js.map +1 -0
  33. package/dist/react/components/DataTable/types.d.ts +14 -0
  34. package/dist/react/components/DataTable/types.d.ts.map +1 -1
  35. package/dist/react/components/DataTable/types.js.map +1 -1
  36. package/dist/react/components/EmptyState.d.ts.map +1 -1
  37. package/dist/react/components/EmptyState.js +2 -2
  38. package/dist/react/components/EmptyState.js.map +1 -1
  39. package/dist/react/components/ErrorCard.d.ts +13 -1
  40. package/dist/react/components/ErrorCard.d.ts.map +1 -1
  41. package/dist/react/components/ErrorCard.js +3 -2
  42. package/dist/react/components/ErrorCard.js.map +1 -1
  43. package/dist/react/components/Field.d.ts +37 -12
  44. package/dist/react/components/Field.d.ts.map +1 -1
  45. package/dist/react/components/Field.js +19 -8
  46. package/dist/react/components/Field.js.map +1 -1
  47. package/dist/react/components/Modal.d.ts +1 -0
  48. package/dist/react/components/Modal.d.ts.map +1 -1
  49. package/dist/react/components/Modal.js +3 -2
  50. package/dist/react/components/Modal.js.map +1 -1
  51. package/dist/react/components/PromptDialog.d.ts +4 -1
  52. package/dist/react/components/PromptDialog.d.ts.map +1 -1
  53. package/dist/react/components/PromptDialog.js +2 -2
  54. package/dist/react/components/PromptDialog.js.map +1 -1
  55. package/dist/react/components/Switch.d.ts +16 -2
  56. package/dist/react/components/Switch.d.ts.map +1 -1
  57. package/dist/react/components/Switch.js +10 -3
  58. package/dist/react/components/Switch.js.map +1 -1
  59. package/dist/react/components/Tooltip.d.ts +1 -1
  60. package/dist/react/components/Tooltip.d.ts.map +1 -1
  61. package/dist/react/components/Tooltip.js +14 -1
  62. package/dist/react/components/Tooltip.js.map +1 -1
  63. package/dist/react/components/TooltipImpl.d.ts.map +1 -1
  64. package/dist/react/components/TooltipImpl.js +37 -7
  65. package/dist/react/components/TooltipImpl.js.map +1 -1
  66. package/dist/react/components/dialog.d.ts.map +1 -1
  67. package/dist/react/components/dialog.js +35 -3
  68. package/dist/react/components/dialog.js.map +1 -1
  69. package/dist/react/file-tree/FileTree.d.ts +26 -1
  70. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  71. package/dist/react/file-tree/FileTree.js +182 -40
  72. package/dist/react/file-tree/FileTree.js.map +1 -1
  73. package/dist/react/hooks/useCopyToClipboard.d.ts +10 -2
  74. package/dist/react/hooks/useCopyToClipboard.d.ts.map +1 -1
  75. package/dist/react/hooks/useCopyToClipboard.js +13 -4
  76. package/dist/react/hooks/useCopyToClipboard.js.map +1 -1
  77. package/dist/react/icons.d.ts +12 -1
  78. package/dist/react/icons.d.ts.map +1 -1
  79. package/dist/react/icons.js +12 -22
  80. package/dist/react/icons.js.map +1 -1
  81. package/dist/react/index.d.ts +4 -4
  82. package/dist/react/index.d.ts.map +1 -1
  83. package/dist/react/index.js +3 -3
  84. package/dist/react/index.js.map +1 -1
  85. package/dist/react/lib/hoverIntent.d.ts +21 -0
  86. package/dist/react/lib/hoverIntent.d.ts.map +1 -1
  87. package/dist/react/lib/hoverIntent.js +24 -0
  88. package/dist/react/lib/hoverIntent.js.map +1 -1
  89. package/dist/react/lib/openDialogs.d.ts +5 -0
  90. package/dist/react/lib/openDialogs.d.ts.map +1 -0
  91. package/dist/react/lib/openDialogs.js +45 -0
  92. package/dist/react/lib/openDialogs.js.map +1 -0
  93. package/dist/scripts/guardrailsEnforce.d.ts +11 -0
  94. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  95. package/dist/styles-areas/analytics.css +10 -9
  96. package/dist/styles-areas/calendar.css +6 -6
  97. package/dist/styles-areas/chat.css +6 -6
  98. package/dist/styles-areas/core.css +41 -36
  99. package/dist/styles-areas/data-table.css +10 -10
  100. package/dist/styles-areas/disk-usage.css +7 -7
  101. package/dist/styles-areas/fields.css +4 -4
  102. package/dist/styles-areas/filter-rail.css +7 -8
  103. package/dist/styles-areas/media-gallery.css +2 -2
  104. package/dist/styles-areas/nav.css +17 -18
  105. package/dist/styles-areas/overlays.css +2 -2
  106. package/dist/styles-areas/stats.css +4 -4
  107. package/dist/styles-areas/wizard.css +3 -3
  108. package/dist/styles-areas/workbench.css +2 -2
  109. package/dist/styles-utilities.css +46 -0
  110. package/dist/testing/index.d.ts +3 -0
  111. package/dist/testing/index.d.ts.map +1 -0
  112. package/dist/testing/index.js +4 -0
  113. package/dist/testing/index.js.map +1 -0
  114. package/dist/testing/objectLeak.d.ts +23 -0
  115. package/dist/testing/objectLeak.d.ts.map +1 -0
  116. package/dist/testing/objectLeak.js +37 -0
  117. package/dist/testing/objectLeak.js.map +1 -0
  118. package/dist/testing/tooltip.d.ts.map +1 -1
  119. package/dist/testing/tooltip.js +8 -18
  120. package/dist/testing/tooltip.js.map +1 -1
  121. package/package.json +61 -7
  122. package/scripts/guardrails-baseline.json +4 -1
  123. package/scripts/guardrailsEnforce.spec.ts +163 -6
  124. package/scripts/guardrailsEnforce.ts +365 -40
  125. package/scripts/paths.ts +1 -1
  126. package/src/react/Toast.spec.tsx +17 -1
  127. package/src/react/Toast.tsx +20 -3
  128. package/src/react/components/Button.spec.tsx +49 -1
  129. package/src/react/components/Button.tsx +63 -6
  130. package/src/react/components/ChoiceGroup.spec.tsx +32 -0
  131. package/src/react/components/ChoiceGroup.tsx +78 -23
  132. package/src/react/components/ConfirmDialog.tsx +5 -0
  133. package/src/react/components/CopyButton.spec.tsx +35 -0
  134. package/src/react/components/CopyButton.tsx +11 -2
  135. package/src/react/components/DataTable/DataTable.spec.tsx +32 -0
  136. package/src/react/components/DataTable/DataTable.tsx +271 -38
  137. package/src/react/components/DataTable/RowActions.tsx +5 -2
  138. package/src/react/components/DataTable/columnPriority.spec.ts +97 -0
  139. package/src/react/components/DataTable/columnPriority.ts +54 -0
  140. package/src/react/components/DataTable/types.ts +14 -0
  141. package/src/react/components/DataTable/virtualize.spec.tsx +47 -2
  142. package/src/react/components/EmptyState.spec.tsx +7 -0
  143. package/src/react/components/EmptyState.tsx +3 -0
  144. package/src/react/components/ErrorCard.spec.tsx +32 -0
  145. package/src/react/components/ErrorCard.tsx +17 -1
  146. package/src/react/components/Field.spec.tsx +61 -0
  147. package/src/react/components/Field.tsx +84 -27
  148. package/src/react/components/Modal.spec.tsx +84 -0
  149. package/src/react/components/Modal.tsx +4 -1
  150. package/src/react/components/PromptDialog.spec.tsx +29 -1
  151. package/src/react/components/PromptDialog.tsx +5 -0
  152. package/src/react/components/Switch.spec.tsx +25 -0
  153. package/src/react/components/Switch.tsx +49 -4
  154. package/src/react/components/Tooltip.spec.tsx +183 -2
  155. package/src/react/components/Tooltip.tsx +14 -2
  156. package/src/react/components/TooltipImpl.tsx +42 -8
  157. package/src/react/components/dialog.tsx +49 -9
  158. package/src/react/file-tree/FileTree.spec.tsx +205 -0
  159. package/src/react/file-tree/FileTree.tsx +220 -38
  160. package/src/react/hooks/useCopyToClipboard.ts +17 -4
  161. package/src/react/icons.spec.ts +32 -1
  162. package/src/react/icons.tsx +59 -0
  163. package/src/react/index.ts +7 -2
  164. package/src/react/lib/hoverIntent.ts +28 -0
  165. package/src/react/lib/openDialogs.ts +48 -0
  166. package/src/styles-areas/analytics.css +10 -9
  167. package/src/styles-areas/calendar.css +6 -6
  168. package/src/styles-areas/chat.css +6 -6
  169. package/src/styles-areas/core.css +41 -36
  170. package/src/styles-areas/data-table.css +10 -10
  171. package/src/styles-areas/disk-usage.css +7 -7
  172. package/src/styles-areas/fields.css +4 -4
  173. package/src/styles-areas/filter-rail.css +7 -8
  174. package/src/styles-areas/media-gallery.css +2 -2
  175. package/src/styles-areas/nav.css +17 -18
  176. package/src/styles-areas/overlays.css +2 -2
  177. package/src/styles-areas/stats.css +4 -4
  178. package/src/styles-areas/wizard.css +3 -3
  179. package/src/styles-areas/workbench.css +2 -2
  180. package/src/styles-utilities.css +46 -0
  181. package/src/testing/index.ts +3 -0
  182. package/src/testing/objectLeak.spec.ts +21 -0
  183. package/src/testing/objectLeak.ts +39 -0
  184. package/src/testing/tooltip.spec.ts +19 -0
  185. package/src/testing/tooltip.ts +17 -1
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table';
3
- import { useVirtualizer } from '@tanstack/react-virtual';
4
- import { Fragment, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
3
+ import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
4
+ import { Fragment, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
+ import { useResizeObserver } from '../../hooks/useResizeObserver.js';
5
6
  import { useScrollEdgeFade } from '../../hooks/useScrollEdgeFade.js';
6
7
  import { useRememberedPreference } from '../../media-gallery/rememberedPreference.js';
7
8
  import { cn } from '../../lib/cn.js';
@@ -19,6 +20,7 @@ import { preloadRowMenu, RowActions } from './RowActions.js';
19
20
  import { writeRowDrag } from './rowDrag.js';
20
21
  import { SortHeader } from './SortHeader.js';
21
22
  import { resolveColumnAlign, SELECT_COLUMN_ID } from './types.js';
23
+ import { AUTO_TRACK_FLOOR_PX, columnFloor, columnsDroppedForWidth } from './columnPriority.js';
22
24
  import { lazyPart } from './lazyPart.js';
23
25
  // 🔴 The toolbar and the pager load only when a table DRAWS them (forge task 2309).
24
26
  // Between them they reach Radix's Select, Menu and Popover — ~20 kB gzip that a
@@ -105,7 +107,7 @@ function chainUpdaterResettingPage(set, setPagination, notify) {
105
107
  * zero — this is the share below which the division stops and the table
106
108
  * overflows its wrapper instead.
107
109
  */
108
- export const AUTO_TRACK_FLOOR_PX = 120;
110
+ export { AUTO_TRACK_FLOOR_PX };
109
111
  /** The trailing row-actions column is a fixed `w-20`. */
110
112
  const ACTIONS_TRACK_PX = 80;
111
113
  /** The leading row-selection column is a fixed `w-11` (44px — a full tap target). */
@@ -150,13 +152,7 @@ const SELECT_TRACK_PX = 44;
150
152
  * spec here can see it.
151
153
  */
152
154
  export function declaredTrackFloor(columns, hasActions = false) {
153
- const columnsTotal = columns.reduce((sum, col) => {
154
- if (col.minWidth != null)
155
- return sum + col.minWidth;
156
- if (typeof col.width === 'number')
157
- return sum + col.width;
158
- return sum + AUTO_TRACK_FLOOR_PX;
159
- }, 0);
155
+ const columnsTotal = columns.reduce((sum, col) => sum + columnFloor(col), 0);
160
156
  return columnsTotal + (hasActions ? ACTIONS_TRACK_PX : 0);
161
157
  }
162
158
  /**
@@ -227,6 +223,9 @@ const HIDE_BELOW_CLASS = {
227
223
  md: '@max-4xl:w-0! @max-4xl:min-w-0! @max-4xl:p-0! @max-4xl:border-0 @max-4xl:[&>*]:hidden',
228
224
  lg: '@max-6xl:w-0! @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:border-0 @max-6xl:[&>*]:hidden',
229
225
  };
226
+ /** `ColDef.priority`'s collapse — {@link HIDE_BELOW_CLASS}'s, unconditional, for the same reason. */
227
+ const COLLAPSED_CLASS = 'w-0! min-w-0! p-0! border-0 [&>*]:hidden';
228
+ const NOTHING_DROPPED = new Set();
230
229
  /**
231
230
  * The table's own props for a row, with the caller's `getRowProps` merged on top: classes join,
232
231
  * styles spread, and a handler both set runs the table's first, then the caller's.
@@ -273,7 +272,7 @@ function isEmptyCellValue(v) {
273
272
  * full-width accordion detail row that never breaks column alignment, and an automatic
274
273
  * stacked-card layout under `sm` so phones are never handed a clipped grid.
275
274
  */
276
- export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRowDelete, rowMenu, loading, emptyState, toolbar, pageSize = 50, pageSizeOptions, hidePagination, caption, className, stickyHeader, stickyFirstColumn, renderExpanded, expandedRowId, rowClassName, hideEmptyColumns, fixedLayout, minTableWidth, scrollBody, virtualize, serverWindow, handleRef, scrollClassName, mobileCards = true, serverSide, pageCount, rowCount, onSortingChange, onColumnFiltersChange, onGlobalFilterChange, onPaginationChange, defaultSort, sorting: sortingProp, enableSortingRemoval = true, sortDisabledReason, density = 'comfortable', viewMode, onViewModeChange, cardMinWidth, searchable = true, searchPlaceholder, toolbarOptions, multiSort = true, filterQuery: filterQueryProp, onFilterQueryChange, getRowDrag, dragSourceId, selectedIds, onSelectionChange, isRowSelectable, unselectableReason, selectionLabel, selectionSummary, selectAll, getRowProps, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, columnsPersistKey, }) {
275
+ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRowDelete, rowMenu, rowMenuLabel, loading, emptyState, toolbar, pageSize = 50, pageSizeOptions, hidePagination, caption, className, stickyHeader, stickyFirstColumn, renderExpanded, expandedRowId, rowClassName, hideEmptyColumns, fixedLayout, minTableWidth, scrollBody, virtualize, serverWindow, handleRef, scrollClassName, mobileCards = true, serverSide, pageCount, rowCount, onSortingChange, onColumnFiltersChange, onGlobalFilterChange, onPaginationChange, defaultSort, sorting: sortingProp, enableSortingRemoval = true, sortDisabledReason, density = 'comfortable', viewMode, onViewModeChange, cardMinWidth, searchable = true, searchPlaceholder, toolbarOptions, multiSort = true, filterQuery: filterQueryProp, onFilterQueryChange, getRowDrag, dragSourceId, selectedIds, onSelectionChange, isRowSelectable, unselectableReason, selectionLabel, selectionSummary, selectAll, getRowProps, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, columnsPersistKey, }) {
277
276
  // Vertical cell padding for the wide grid tier — the one lever the density knob
278
277
  // pulls (headers + body cells stay aligned by sharing it).
279
278
  const cellPadY = density === 'compact' ? 'py-1.5' : 'py-2.5';
@@ -469,7 +468,8 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
469
468
  */
470
469
  const virtualOptions = virtualize === true || (serverWindow && !virtualize) ? {} : virtualize || null;
471
470
  const virtualEnabled = virtualOptions !== null;
472
- const bodyScrolls = scrollBody || virtualEnabled;
471
+ const windowScroll = virtualEnabled && virtualOptions?.scroll === 'window' && !serverWindow;
472
+ const bodyScrolls = scrollBody || (virtualEnabled && !windowScroll);
473
473
  const paginationHidden = hidePagination || virtualEnabled;
474
474
  const selectionEnabled = selectedIds !== undefined;
475
475
  /** The checkbox column is ON and not hidden through the Columns menu. Hiding it hides the
@@ -532,7 +532,11 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
532
532
  getFilteredRowModel: serverSide || serverWindow ? undefined : getFilteredRowModel(),
533
533
  // 🔴 `virtualEnabled` too, not only `serverSide`. See the note where the flag is
534
534
  // computed: a paginated virtualizer sizes its spacer from one PAGE.
535
- getPaginationRowModel: serverSide || virtualEnabled ? undefined : getPaginationRowModel(),
535
+ // 🔴 And `hidePagination` (forge task 2396): hiding the pager used to leave the paging ON,
536
+ // so a table past `pageSize` rows (50) silently showed its first page with no control to
537
+ // reach the rest — binary-server's run history and station's metric tables among them.
538
+ // A table with no pager shows every row; that is what "no pager" means.
539
+ getPaginationRowModel: serverSide || paginationHidden ? undefined : getPaginationRowModel(),
536
540
  // TanStack's built-in auto-reset fires `resetPageIndex()` on EVERY `data`
537
541
  // identity change (see `getCoreRowModel`'s memo deps), not just on filter/sort
538
542
  // changes — any live-polling table (this one included: rows are re-derived
@@ -580,6 +584,24 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
580
584
  : null;
581
585
  const estimateRowHeight = fixedRowHeight ?? virtualOptions?.estimateRowHeight ?? (density === 'compact' ? 36 : 44);
582
586
  const scrollRef = useRef(null);
587
+ const outerRef = useRef(null);
588
+ /**
589
+ * 🔴 The width tier ON SCREEN, read in JS so a virtualized table windows the ONE tier the
590
+ * reader sees and mounts no rows at all in the two that `display: none` hides. Until forge
591
+ * task 2404 `virtualize` windowed only the grid, and the phone cards and the tablet list each
592
+ * mounted EVERY row at every width — 5,000 rows put ~10,000 extra nodes on a phone, with every
593
+ * virtualization gate green because each one passed `mobileCards={false}`. A table that does
594
+ * not virtualize still renders all three tiers and lets the container query pick, as before.
595
+ */
596
+ const narrowTiers = virtualEnabled && mobileCards && viewMode !== 'card';
597
+ const [tier, setTier] = useState('grid');
598
+ const activeTier = narrowTiers ? tier : 'grid';
599
+ /** A narrow tier's row is a card or a two-line list row, nothing like the grid's height.
600
+ * Only a seed: every rendered row is measured. */
601
+ const rowEstimate = activeTier === 'cards' ? CARD_ESTIMATE_PX : activeTier === 'list' ? LIST_ESTIMATE_PX : estimateRowHeight;
602
+ /** A {@link serverWindow}'s fixed height is the GRID row's. A card cannot be held to it, so
603
+ * the narrow tiers measure — see {@link fixedRowHeight}. */
604
+ const fixedHeight = activeTier === 'grid' ? fixedRowHeight : null;
583
605
  /** Rows before the loaded window, and the size of the whole result set — both 0/`rows.length`
584
606
  * without a {@link serverWindow}, which makes every index below the same number as before. */
585
607
  const windowStart = serverWindow ? Math.max(0, serverWindow.start) : 0;
@@ -599,10 +621,11 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
599
621
  if (settleRef.current)
600
622
  clearTimeout(settleRef.current);
601
623
  }, []);
602
- const rowVirtualizer = useVirtualizer({
603
- count: virtualEnabled ? windowTotal : 0,
624
+ const selfVirtualizer = useVirtualizer({
625
+ count: virtualEnabled && !windowScroll ? windowTotal : 0,
626
+ enabled: virtualEnabled && !windowScroll,
604
627
  getScrollElement: () => scrollRef.current,
605
- estimateSize: () => estimateRowHeight,
628
+ estimateSize: () => rowEstimate,
606
629
  overscan: virtualOptions?.overscan ?? 8,
607
630
  onChange: serverWindow
608
631
  ? (instance) => {
@@ -654,8 +677,47 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
654
677
  // 🔴 MEASURED, not assumed. A wrapping cell, an expanded detail panel and the
655
678
  // `compact` density all change a row's height, and a caller cannot be asked to
656
679
  // compute any of them. `estimateSize` only seeds the spacer before first paint.
657
- measureElement: (el) => fixedRowHeight ?? el.getBoundingClientRect().height,
680
+ measureElement: (el) => fixedHeight ?? el.getBoundingClientRect().height,
681
+ });
682
+ /**
683
+ * `virtualize.scroll: 'window'` — see the prop doc. Always called (hooks cannot be
684
+ * conditional) and `enabled: false` otherwise, so no other table on the page subscribes to
685
+ * the window's scroll. `scrollMargin` is the BODY's top in document coordinates: the
686
+ * virtualizer's offsets are measured from there, and the header, the toolbar and whatever the
687
+ * page draws above the table all sit inside that margin.
688
+ */
689
+ /** The active tier's row container — the grid's `<tbody>`, or a narrow tier's list. */
690
+ const bodyRef = useRef(null);
691
+ const [scrollMargin, setScrollMargin] = useState(0);
692
+ const readScrollMargin = useCallback(() => {
693
+ const body = bodyRef.current;
694
+ if (!body)
695
+ return;
696
+ const top = Math.round(body.getBoundingClientRect().top + window.scrollY);
697
+ setScrollMargin((prev) => (prev === top ? prev : top));
698
+ }, []);
699
+ // Every render: anything above the table (a banner, a toolbar wrapping to two lines) moves
700
+ // the body without resizing it. A no-op when the number is unchanged.
701
+ useLayoutEffect(() => {
702
+ if (windowScroll)
703
+ readScrollMargin();
658
704
  });
705
+ useEffect(() => {
706
+ if (!windowScroll)
707
+ return;
708
+ window.addEventListener('resize', readScrollMargin);
709
+ return () => window.removeEventListener('resize', readScrollMargin);
710
+ }, [readScrollMargin, windowScroll]);
711
+ const pageVirtualizer = useWindowVirtualizer({
712
+ count: windowScroll ? windowTotal : 0,
713
+ enabled: windowScroll,
714
+ estimateSize: () => rowEstimate,
715
+ overscan: virtualOptions?.overscan ?? 8,
716
+ scrollMargin,
717
+ measureElement: (el) => el.getBoundingClientRect().height,
718
+ });
719
+ // One interface from here on: the two differ only in what they measure against.
720
+ const rowVirtualizer = (windowScroll ? pageVirtualizer : selfVirtualizer);
659
721
  /**
660
722
  * 🔴 Invalidate the measurement cache when the row height changes.
661
723
  *
@@ -672,11 +734,22 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
672
734
  * exists so the third virtualizer does not pay for it a third time — and it is
673
735
  * what caught this one.
674
736
  */
675
- // biome-ignore lint/correctness/useExhaustiveDependencies: `estimateRowHeight` is the POINT of this effect, not a value it reads — re-running when it changes is what invalidates the cached offsets. Dropping it makes the measure fire once and never again, which is the bug.
737
+ // The same holds for a TIER change: a card measured at 180px is a 56px list row a resize later.
738
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `rowEstimate` and `activeTier` are the POINT of this effect, not values it reads — re-running when they change is what invalidates the cached offsets. Dropping them makes the measure fire once and never again, which is the bug.
676
739
  useEffect(() => {
677
740
  rowVirtualizer.measure();
678
- }, [rowVirtualizer, estimateRowHeight]);
741
+ }, [rowVirtualizer, rowEstimate, activeTier]);
679
742
  const virtualRows = virtualEnabled ? rowVirtualizer.getVirtualItems() : [];
743
+ // `virtualize.onNearEnd`: the last rendered row is within `nearEndCount` of the end of `data`.
744
+ const lastRendered = virtualRows.at(-1)?.index ?? -1;
745
+ const onNearEnd = virtualOptions?.onNearEnd;
746
+ const nearEndCount = virtualOptions?.nearEndCount ?? 8;
747
+ useEffect(() => {
748
+ if (!onNearEnd || lastRendered < 0)
749
+ return;
750
+ if (lastRendered >= windowStart + rows.length - nearEndCount)
751
+ onNearEnd();
752
+ }, [lastRendered, nearEndCount, onNearEnd, rows.length, windowStart]);
680
753
  /** The rows to render, and the two spacers that keep the scrollbar honest about the
681
754
  * ones that are not. Off, this is every row and no spacers — byte-identical output.
682
755
  * `null` is a row a {@link serverWindow} has not loaded: a placeholder of the estimated
@@ -684,25 +757,36 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
684
757
  const bodyRows = virtualEnabled
685
758
  ? virtualRows.map((v) => rows[v.index - windowStart] ?? null)
686
759
  : rows;
760
+ /** What a tier renders: every row when nothing is virtualized; the window in the ONE tier on
761
+ * screen, and nothing in the others. `index` is the row's position over the whole result set. */
762
+ const tierRows = (t) => !virtualEnabled
763
+ ? rows.map((row, index) => ({ row, index }))
764
+ : activeTier === t
765
+ ? bodyRows.map((row, i) => ({ row, index: virtualRows[i].index }))
766
+ : [];
687
767
  useImperativeHandle(handleRef, () => ({
688
768
  get scrollElement() {
689
769
  return scrollRef.current;
690
770
  },
691
771
  scrollToIndex(index, options = {}) {
692
- const element = scrollRef.current;
772
+ const element = windowScroll ? window : scrollRef.current;
693
773
  if (!element || !virtualEnabled)
694
774
  return null;
695
775
  const clamped = Math.max(0, Math.min(index, loadedRef.current.total - 1));
696
- const [offset] = rowVirtualizer.getOffsetForIndex(clamped, options.align ?? 'center') ?? [clamped * estimateRowHeight];
776
+ const [offset] = rowVirtualizer.getOffsetForIndex(clamped, options.align ?? 'center') ?? [clamped * rowEstimate];
697
777
  const top = Math.max(0, Math.round(offset));
698
778
  element.scrollTo({ top, behavior: options.behavior ?? 'auto' });
699
779
  return top;
700
780
  },
701
- }), [estimateRowHeight, rowVirtualizer, virtualEnabled]);
702
- const padTop = virtualEnabled ? (virtualRows[0]?.start ?? 0) : 0;
781
+ }), [rowEstimate, rowVirtualizer, virtualEnabled, windowScroll]);
782
+ // Item offsets include the `scrollMargin` (0 unless the window scrolls); the spacers do not.
783
+ const margin = windowScroll ? rowVirtualizer.options.scrollMargin : 0;
784
+ const padTop = virtualEnabled ? Math.max(0, (virtualRows[0]?.start ?? margin) - margin) : 0;
703
785
  const padBottom = virtualEnabled
704
- ? rowVirtualizer.getTotalSize() - (virtualRows.at(-1)?.end ?? 0)
786
+ ? Math.max(0, rowVirtualizer.getTotalSize() - ((virtualRows.at(-1)?.end ?? margin) - margin))
705
787
  : 0;
788
+ /** The spacers belong to the tier on screen; the hidden ones hold no rows and no padding. */
789
+ const tierPad = (t) => virtualEnabled && activeTier === t ? { paddingTop: padTop, paddingBottom: padBottom } : undefined;
706
790
  const hasActions = !!(onRowEdit || onRowDelete || rowMenu);
707
791
  // ── Row selection (see the `selectedIds` prop doc) ──────────────────────────
708
792
  const canSelect = (row) => isRowSelectable?.(row) ?? true;
@@ -813,19 +897,46 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
813
897
  // say so at the cut edge, or a narrow pane reads as a table with its right-hand
814
898
  // columns missing rather than one that scrolls (`useScrollEdgeFade`).
815
899
  const gridFade = useScrollEdgeFade();
900
+ // `ColDef.priority`: collapse the columns that do not fit the table's MEASURED width. Only a
901
+ // table that declares a priority re-renders on resize for it.
902
+ const hasPriority = liveColumns.some((c) => c.priority != null);
903
+ const [measuredWidth, setMeasuredWidth] = useState(0);
904
+ useResizeObserver(outerRef, (el) => {
905
+ if (hasPriority)
906
+ setMeasuredWidth(el.clientWidth);
907
+ if (narrowTiers)
908
+ setTier(tierForWidth(el));
909
+ }, { layout: true });
910
+ // The observer only fires on a RESIZE: a table that starts virtualizing reads its tier now.
911
+ useLayoutEffect(() => {
912
+ if (narrowTiers && outerRef.current)
913
+ setTier(tierForWidth(outerRef.current));
914
+ }, [narrowTiers]);
915
+ const droppedByWidth = hasPriority
916
+ ? columnsDroppedForWidth(cardColumns, measuredWidth, (hasActions ? ACTIONS_TRACK_PX : 0) + (selectionShown ? SELECT_TRACK_PX : 0))
917
+ : NOTHING_DROPPED;
816
918
  const tableMinWidth = fixedLayout
817
- ? Math.max(minTableWidth ?? 0, declaredTrackFloor(cardColumns, hasActions) + (selectionShown ? SELECT_TRACK_PX : 0))
919
+ ? Math.max(minTableWidth ?? 0, declaredTrackFloor(cardColumns.filter((c) => !droppedByWidth.has(c.key)), hasActions) + (selectionShown ? SELECT_TRACK_PX : 0))
818
920
  : minTableWidth;
819
- return (_jsxs("div", { className: cn('@container flex min-h-0 flex-col', className), children: [toolbarNode, summaryNode != null ? (_jsx("div", { "data-cb-table-selection-summary": '', "aria-live": 'polite', className: 'flex min-h-11 flex-wrap items-center gap-3 border-b border-border px-4 py-2 text-sm', children: summaryNode })) : null, loading ? (_jsx("div", { className: 'flex flex-col gap-2 p-4', children: Array.from({ length: 5 }).map((_, i) => (_jsx(Skeleton, { className: 'h-10 w-full' }, `dt-skeleton-${i}`))) })) : rows.length === 0 && windowTotal === 0 ? (_jsx("div", { className: 'p-2', children: emptyState ?? _jsx(EmptyState, { title: 'No results' }) })) : (_jsxs("div", { ref: scrollRef, "data-cb-table-scroll": bodyScrolls ? '' : undefined,
921
+ return (_jsxs("div", { ref: outerRef, className: cn('@container flex min-h-0 flex-col', className), children: [toolbarNode, summaryNode != null ? (_jsx("div", { "data-cb-table-selection-summary": '', "aria-live": 'polite', className: 'flex min-h-11 flex-wrap items-center gap-3 border-b border-border px-4 py-2 text-sm', children: summaryNode })) : null, loading ? (_jsx("div", { className: 'flex flex-col gap-2 p-4', children: Array.from({ length: 5 }).map((_, i) => (_jsx(Skeleton, { className: 'h-10 w-full' }, `dt-skeleton-${i}`))) })) : rows.length === 0 && windowTotal === 0 ? (_jsx("div", { className: 'p-2', children: emptyState ?? _jsx(EmptyState, { title: 'No results' }) })) : (_jsxs("div", { ref: scrollRef, "data-cb-table-scroll": bodyScrolls ? '' : undefined,
820
922
  // guardrails-ignore no-raw-overflow: dual-axis table scroll container — sticky header pins to it; ScrollArea cannot own horizontal table scroll.
821
923
  className: cn(bodyScrolls && 'min-h-0 flex-1 overflow-auto', scrollClassName), children: [selectionShown && (viewMode === 'card' || mobileCards) ? (_jsx("div", { "data-cb-table-select-all": '', className: cn('flex items-center px-2 py-1', viewMode !== 'card' && '@xl:hidden'), children: headerCheckbox('Select all') })) : null, viewMode === 'card' ? (_jsx(CardView, { columns: cardColumns, data: rows.map((row) => row.original), getRowId: getRowId, onRowClick: onRowClick, dragPropsFor: getRowDrag ? dragPropsFor : undefined, selectFor: selectionShown
822
924
  ? (_row, index) => {
823
925
  const tsRow = rows[index];
824
926
  return tsRow ? rowCheckbox(tsRow) : null;
825
927
  }
826
- : undefined, cardMinWidth: cardMinWidth, className: 'p-2' })) : (_jsxs(_Fragment, { children: [mobileCards && (_jsx("div", { "data-cb-table-cards": '', className: 'flex flex-col gap-3 @xl:hidden', children: rows.map((row) => (_jsx(MobileRowCard, { row: row, columns: cardColumns, className: rowClassName?.(row.original), onRowClick: onRowClick, dragProps: mergeRowProps(dragPropsFor(row.original), getRowProps?.(row.original, windowStart + row.index)), select: selectionShown ? rowCheckbox(row) : undefined, actions: hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) })) : null, expanded: renderExpanded && expandedRowId != null && row.id === expandedRowId
827
- ? renderExpanded(row.original)
828
- : null }, row.id))) })), mobileCards && (_jsx("div", { "data-cb-table-list": '', className: 'hidden @xl:block @3xl:hidden', children: (() => {
928
+ : undefined, cardMinWidth: cardMinWidth, className: 'p-2' })) : (_jsxs(_Fragment, { children: [mobileCards && (_jsx("div", { ref: activeTier === 'cards' && narrowTiers ? bodyRef : undefined, "data-cb-table-cards": '',
929
+ // Virtualized, each card sits in a measured wrapper that carries the gap as padding —
930
+ // a flex `gap` is outside every row, so the virtualizer would never count it.
931
+ className: cn('flex flex-col @xl:hidden', !virtualEnabled && 'gap-3'), style: tierPad('cards'), children: tierRows('cards').map(({ row, index }) => {
932
+ if (row === null) {
933
+ return (_jsx("div", { ref: rowVirtualizer.measureElement, "data-index": index, "data-cb-placeholder": '', "aria-hidden": true, className: 'pb-3', children: _jsx(Skeleton, { className: 'w-full rounded-lg', style: { height: rowEstimate - 12 } }) }, `cb-placeholder-${index}`));
934
+ }
935
+ const card = (_jsx(MobileRowCard, { row: row, columns: cardColumns, className: rowClassName?.(row.original), onRowClick: onRowClick, dragProps: mergeRowProps(dragPropsFor(row.original), getRowProps?.(row.original, windowStart + row.index)), select: selectionShown ? rowCheckbox(row) : undefined, actions: hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original), menuLabel: rowMenuLabel?.(row.original) })) : null, expanded: renderExpanded && expandedRowId != null && row.id === expandedRowId
936
+ ? renderExpanded(row.original)
937
+ : null }, row.id));
938
+ return virtualEnabled ? (_jsx("div", { ref: rowVirtualizer.measureElement, "data-index": index, className: 'pb-3', children: card }, row.id)) : (card);
939
+ }) })), mobileCards && (_jsx("div", { "data-cb-table-list": '', className: 'hidden @xl:block @3xl:hidden', children: (() => {
829
940
  const [title, ...rest] = cardColumns;
830
941
  // ONE shared grid template for every row — parent and child rows
831
942
  // lock into identical column slots, so the status/ports/actions
@@ -851,18 +962,24 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
851
962
  ...(hasActions ? ['32px'] : []),
852
963
  ].join(' ');
853
964
  const header = (_jsxs("div", { "data-cb-table-list-header": '', className: 'grid items-center gap-3 border-b border-border px-4 py-2 text-caption font-bold uppercase tracking-wider text-muted-foreground', style: { gridTemplateColumns: template }, children: [selectionShown ? _jsx("span", { className: 'flex justify-center', children: headerCheckbox() }) : null, _jsx("span", { children: title?.label ?? '' }), rest.map((col) => (_jsx("span", { className: cn(resolveColumnAlign(col) === 'right' && 'text-right'), children: col.label }, col.key))), hasActions ? _jsx("span", { "aria-hidden": true }) : null] }));
854
- const body = rows.map((row) => {
965
+ const body = tierRows('list').map(({ row, index }) => {
966
+ if (row === null) {
967
+ return (_jsx("div", { ref: rowVirtualizer.measureElement, "data-index": index, "data-cb-placeholder": '', "aria-hidden": true, className: 'flex items-center px-4', style: { height: rowEstimate }, children: _jsx(Skeleton, { className: 'h-3 w-40' }) }, `cb-placeholder-${index}`));
968
+ }
855
969
  const isExpanded = renderExpanded != null && expandedRowId != null && row.id === expandedRowId;
856
- return (_jsxs(Fragment, { children: [_jsxs("div", { ...mergeRowProps({
970
+ const item = (_jsxs(_Fragment, { children: [_jsxs("div", { ...mergeRowProps({
857
971
  ...dragPropsFor(row.original),
858
972
  className: cn('grid items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/40', getRowDrag && 'cursor-grab active:cursor-grabbing', rowClassName?.(row.original)),
859
973
  style: { gridTemplateColumns: template },
860
- }, getRowProps?.(row.original, windowStart + row.index)), children: [selectionShown ? (_jsx("div", { ...selectCellProps(row), className: '-my-3 flex self-stretch items-center justify-center', children: rowCheckbox(row) })) : null, _jsx("div", { className: 'min-w-0', children: title ? (_jsx(CellRenderer, { col: title, value: row.original[title.key], row: row.original })) : null }), rest.map((col) => (_jsx("div", { className: cn(resolveColumnAlign(col) === 'right' && 'justify-self-end text-right'), children: _jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }) }, col.key))), hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) })) : null] }), isExpanded ? (_jsx("div", { className: 'bg-muted/20 px-4 pt-1 pb-3', children: _jsx(Surface, { level: "raised", pad: "sm", radius: "lg", children: renderExpanded(row.original) }) })) : null] }, row.id));
974
+ }, getRowProps?.(row.original, windowStart + row.index)), children: [selectionShown ? (_jsx("div", { ...selectCellProps(row), className: '-my-3 flex self-stretch items-center justify-center', children: rowCheckbox(row) })) : null, _jsx("div", { className: 'min-w-0', children: title ? (_jsx(CellRenderer, { col: title, value: row.original[title.key], row: row.original })) : null }), rest.map((col) => (_jsx("div", { className: cn(resolveColumnAlign(col) === 'right' && 'justify-self-end text-right'), children: _jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }) }, col.key))), hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original), menuLabel: rowMenuLabel?.(row.original) })) : null] }), isExpanded ? (_jsx("div", { className: 'bg-muted/20 px-4 pt-1 pb-3', children: _jsx(Surface, { level: "raised", pad: "sm", radius: "lg", children: renderExpanded(row.original) }) })) : null] }));
975
+ // Virtualized, the row AND its expansion are one measured node — the
976
+ // virtualizer's `data-index` names a row, not the first half of one.
977
+ return virtualEnabled ? (_jsx("div", { ref: rowVirtualizer.measureElement, "data-index": index, children: item }, row.id)) : (_jsx(Fragment, { children: item }, row.id));
861
978
  });
862
- return (_jsxs(_Fragment, { children: [header, _jsx("div", { className: 'divide-y divide-border', children: body })] }));
979
+ return (_jsxs(_Fragment, { children: [header, _jsx("div", { ref: activeTier === 'list' && narrowTiers ? bodyRef : undefined, className: 'divide-y divide-border', style: tierPad('list'), children: body })] }));
863
980
  })() })), _jsx("div", { ref: bodyScrolls ? undefined : gridFade.ref, onScroll: bodyScrolls ? undefined : gridFade.onScroll, style: bodyScrolls ? undefined : gridFade.maskStyle,
864
981
  // guardrails-ignore no-raw-overflow: content-sized horizontal table scroll, not a page-level vertical scroll region — see reasoning above. Under scrollBody the OUTER wrapper owns both axes (a nested overflow ancestor would un-stick the sticky header).
865
- className: cn(!bodyScrolls && 'overflow-x-auto', mobileCards && 'hidden @3xl:block'), children: _jsxs("table", { className: cn('w-full border-collapse text-sm', fixedLayout && 'table-fixed'), style: tableMinWidth != null ? { minWidth: tableMinWidth } : undefined, children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { children: table.getHeaderGroups().map((hg) => (_jsxs("tr", { children: [(() => {
982
+ className: cn(!bodyScrolls && 'overflow-x-auto', mobileCards && 'hidden @3xl:block'), children: _jsxs("table", { "aria-rowcount": virtualEnabled ? (serverWindow ? windowTotal + 1 : onNearEnd ? -1 : rows.length + 1) : undefined, className: cn('w-full border-collapse text-sm', fixedLayout && 'table-fixed'), style: tableMinWidth != null ? { minWidth: tableMinWidth } : undefined, children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { children: table.getHeaderGroups().map((hg) => (_jsxs("tr", { "aria-rowindex": virtualEnabled ? 1 : undefined, children: [(() => {
866
983
  // `ColDef.leading` columns sit BEFORE the checkbox column; see its doc.
867
984
  const renderHeader = (h, headerIndex) => {
868
985
  const sorted = h.column.getIsSorted();
@@ -893,13 +1010,13 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
893
1010
  frozen &&
894
1011
  (selectionShown
895
1012
  ? 'sticky left-11 bg-card shadow-[1px_0_0_0_var(--color-border)]'
896
- : 'sticky left-0 bg-card shadow-[1px_0_0_0_var(--color-border)]'), frozen && (stickyHeader ? 'z-30' : 'z-20'), meta?.hideBelow && HIDE_BELOW_CLASS[meta.hideBelow]), children: h.isPlaceholder
1013
+ : 'sticky left-0 bg-card shadow-[1px_0_0_0_var(--color-border)]'), frozen && (stickyHeader ? 'z-30' : 'z-20'), meta?.hideBelow && HIDE_BELOW_CLASS[meta.hideBelow], droppedByWidth.has(h.column.id) && COLLAPSED_CLASS), children: h.isPlaceholder
897
1014
  ? null
898
1015
  : renderTemplate(h.column.columnDef.header, h.getContext()) }, h.id));
899
1016
  };
900
1017
  const isLeading = (h) => h.column.columnDef.meta?.leading === true;
901
1018
  return (_jsxs(_Fragment, { children: [hg.headers.filter(isLeading).map((h) => renderHeader(h, -1)), selectionShown ? (_jsx("th", { scope: 'col', className: cn('w-11 border-b border-border px-0 text-center', stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm', stickyFirstColumn && 'sticky left-0 bg-card', stickyFirstColumn && (stickyHeader ? 'z-30' : 'z-20')), children: headerCheckbox() })) : null, hg.headers.filter((h) => !isLeading(h)).map(renderHeader)] }));
902
- })(), hasActions ? (_jsx("th", { className: cn('w-20 border-b border-border px-2', stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm'), "aria-label": 'Row actions' })) : null] }, hg.id))) }), _jsxs("tbody", { children: [padTop > 0 ? (_jsx("tr", { "aria-hidden": true, style: { height: padTop }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null, bodyRows.map((row, sliceIndex) => {
1019
+ })(), hasActions ? (_jsx("th", { className: cn('w-20 border-b border-border px-2', stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm'), "aria-label": 'Row actions' })) : null] }, hg.id))) }), _jsxs("tbody", { ref: activeTier === 'grid' ? bodyRef : undefined, children: [padTop > 0 && activeTier === 'grid' ? (_jsx("tr", { "aria-hidden": true, style: { height: padTop }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null, (activeTier === 'grid' ? bodyRows : []).map((row, sliceIndex) => {
903
1020
  if (row === null) {
904
1021
  // A row the server window has not loaded yet: the estimated height, so
905
1022
  // the spacer arithmetic holds, and a skeleton line rather than a gap.
@@ -919,7 +1036,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
919
1036
  // every measurement lands on index 0 and the spacer never grows.
920
1037
  // (The ref stays OUT of the merge: `getRowProps` cannot take it.)
921
1038
  ref: virtualEnabled && fixedRowHeight === null ? rowVirtualizer.measureElement : undefined, ...mergeRowProps({
922
- ...(virtualEnabled ? { 'data-index': rowIndex } : {}),
1039
+ ...(virtualEnabled ? { 'data-index': rowIndex, 'aria-rowindex': rowIndex + 2 } : {}),
923
1040
  style: fixedRowHeight === null ? undefined : { height: fixedRowHeight },
924
1041
  'data-expanded': isExpanded || undefined,
925
1042
  ...dragPropsFor(row.original),
@@ -968,7 +1085,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
968
1085
  // `overflow: hidden`: a cell may host a menu/tooltip that
969
1086
  // has to escape its bounds.
970
1087
  fixedLayout && '[overflow-wrap:anywhere]', cell.column.columnDef.meta?.align === 'right' && 'text-right', cell.column.columnDef.meta?.hideBelow &&
971
- HIDE_BELOW_CLASS[cell.column.columnDef.meta.hideBelow]), children: renderTemplate(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
1088
+ HIDE_BELOW_CLASS[cell.column.columnDef.meta.hideBelow], droppedByWidth.has(cell.column.id) && COLLAPSED_CLASS), children: renderTemplate(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
972
1089
  };
973
1090
  return (_jsxs(_Fragment, { children: [cells.filter(isLeading).map((cell) => renderCell(cell, -1)), selectionShown ? (_jsx("td", { ...selectCellProps(row), style: stickyFirstColumn
974
1091
  ? {
@@ -977,22 +1094,33 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
977
1094
  : 'var(--color-card)',
978
1095
  }
979
1096
  : undefined, className: cn('w-11 cursor-pointer px-0 text-center align-middle', stickyFirstColumn && 'sticky left-0 z-10 bg-[var(--cb-frozen-bg)]'), children: rowCheckbox(row) })) : null, cells.filter((cell) => !isLeading(cell)).map(renderCell)] }));
980
- })(), hasActions ? (_jsx("td", { className: 'px-2 py-2', children: _jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) }) })) : null] }), isExpanded ? (_jsx("tr", { className: 'border-b border-border bg-muted/20', ref: (el) => {
1097
+ })(), hasActions ? (_jsx("td", { className: 'px-2 py-2', children: _jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original), menuLabel: rowMenuLabel?.(row.original) }) })) : null] }), isExpanded ? (_jsx("tr", { className: 'border-b border-border bg-muted/20', ref: (el) => {
981
1098
  if (el && scrolledExpansionRef.current !== row.id) {
982
1099
  scrolledExpansionRef.current = row.id;
983
1100
  // A row expanded near the fold must reveal its panel.
984
1101
  el.scrollIntoView({ block: 'nearest' });
985
1102
  }
986
1103
  }, children: _jsx("td", { colSpan: visibleColumnCount, className: 'p-0', children: _jsx("div", { className: 'animate-in fade-in slide-in-from-top-1 px-4 pt-1 pb-3 duration-200', children: _jsx(Surface, { level: "raised", pad: "sm", radius: "lg", children: renderExpanded(row.original) }) }) }) })) : null] }, row.id));
987
- }), padBottom > 0 ? (_jsx("tr", { "aria-hidden": true, style: { height: padBottom }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null] })] }) })] })), bodyScrolls && (_jsx("div", { "aria-hidden": true, className: 'pointer-events-none sticky bottom-0 -mt-6 h-6 w-full bg-gradient-to-t from-card to-transparent' }))] })), !loading && rows.length > 0 && !paginationHidden ? (_jsx(PaginationPart, { table: table, pageSizeOptions: pageSizeOptions, fallback: _jsx(PaginationPlaceholder, {}) })) : null] }));
1104
+ }), padBottom > 0 && activeTier === 'grid' ? (_jsx("tr", { "aria-hidden": true, style: { height: padBottom }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null] })] }) })] })), bodyScrolls && (_jsx("div", { "aria-hidden": true, className: 'pointer-events-none sticky bottom-0 -mt-6 h-6 w-full bg-gradient-to-t from-card to-transparent' }))] })), !loading && rows.length > 0 && !paginationHidden ? (_jsx(PaginationPart, { table: table, pageSizeOptions: pageSizeOptions, fallback: _jsx(PaginationPlaceholder, {}) })) : null] }));
988
1105
  }
1106
+ /** Seeds for the narrow tiers' row heights; every rendered row is measured over them. */
1107
+ const CARD_ESTIMATE_PX = 160;
1108
+ const LIST_ESTIMATE_PX = 56;
989
1109
  /**
990
- * One row rendered as a phone card: first visible column = the title zone (unlabeled,
991
- * full width), labeled columns = compact label/value pairs, empty-labeled columns
992
- * (an actions column) + the row-actions kebab = the bordered footer row, actions
993
- * anchored bottom-right inside the card bounds. Values render through the SAME
994
- * {@link CellRenderer} registry as the grid, so a custom cell shows identically here.
1110
+ * The tier `mobileCards`' container query shows at `el`'s width: its two steps are 36rem and
1111
+ * 48rem, both `min-width` against the container's CONTENT box — so padding comes off, and the
1112
+ * rem is the root's, read live, because an app that scales its root size scales the ladder too.
995
1113
  */
1114
+ function tierForWidth(el) {
1115
+ // Nothing laid out (a hidden ancestor, happy-dom): no tier is on screen, and the grid is the
1116
+ // one whose output a caller without layout can still inspect.
1117
+ if (el.clientWidth === 0)
1118
+ return 'grid';
1119
+ const style = getComputedStyle(el);
1120
+ const width = el.clientWidth - (Number.parseFloat(style.paddingLeft) || 0) - (Number.parseFloat(style.paddingRight) || 0);
1121
+ const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
1122
+ return width < 36 * rem ? 'cards' : width < 48 * rem ? 'list' : 'grid';
1123
+ }
996
1124
  function MobileRowCard({ row, columns, className, onRowClick, dragProps, select, actions, expanded, }) {
997
1125
  const visible = columns.filter((c) => !c.hidden);
998
1126
  const [title, ...rest] = visible;