cursedbelt 5.32.0 → 5.34.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 (195) 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/CopyButton.d.ts +10 -1
  14. package/dist/react/components/CopyButton.d.ts.map +1 -1
  15. package/dist/react/components/CopyButton.js +2 -2
  16. package/dist/react/components/CopyButton.js.map +1 -1
  17. package/dist/react/components/DataTable/DataTable.d.ts +27 -2
  18. package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
  19. package/dist/react/components/DataTable/DataTable.js +168 -44
  20. package/dist/react/components/DataTable/DataTable.js.map +1 -1
  21. package/dist/react/components/DataTable/RowActions.d.ts +4 -1
  22. package/dist/react/components/DataTable/RowActions.d.ts.map +1 -1
  23. package/dist/react/components/DataTable/RowActions.js +2 -2
  24. package/dist/react/components/DataTable/RowActions.js.map +1 -1
  25. package/dist/react/components/DataTable/columnPriority.d.ts +30 -0
  26. package/dist/react/components/DataTable/columnPriority.d.ts.map +1 -0
  27. package/dist/react/components/DataTable/columnPriority.js +45 -0
  28. package/dist/react/components/DataTable/columnPriority.js.map +1 -0
  29. package/dist/react/components/DataTable/types.d.ts +14 -0
  30. package/dist/react/components/DataTable/types.d.ts.map +1 -1
  31. package/dist/react/components/DataTable/types.js.map +1 -1
  32. package/dist/react/components/EmptyState.d.ts.map +1 -1
  33. package/dist/react/components/EmptyState.js +2 -2
  34. package/dist/react/components/EmptyState.js.map +1 -1
  35. package/dist/react/components/ErrorCard.d.ts +13 -1
  36. package/dist/react/components/ErrorCard.d.ts.map +1 -1
  37. package/dist/react/components/ErrorCard.js +3 -2
  38. package/dist/react/components/ErrorCard.js.map +1 -1
  39. package/dist/react/components/Field.d.ts +37 -12
  40. package/dist/react/components/Field.d.ts.map +1 -1
  41. package/dist/react/components/Field.js +19 -8
  42. package/dist/react/components/Field.js.map +1 -1
  43. package/dist/react/components/Menu.d.ts +58 -0
  44. package/dist/react/components/Menu.d.ts.map +1 -0
  45. package/dist/react/components/Menu.js +86 -0
  46. package/dist/react/components/Menu.js.map +1 -0
  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/SaveIndicator.d.ts +36 -0
  52. package/dist/react/components/SaveIndicator.d.ts.map +1 -0
  53. package/dist/react/components/SaveIndicator.js +48 -0
  54. package/dist/react/components/SaveIndicator.js.map +1 -0
  55. package/dist/react/components/SelectionBar.d.ts +26 -0
  56. package/dist/react/components/SelectionBar.d.ts.map +1 -0
  57. package/dist/react/components/SelectionBar.js +21 -0
  58. package/dist/react/components/SelectionBar.js.map +1 -0
  59. package/dist/react/components/Switch.d.ts +16 -2
  60. package/dist/react/components/Switch.d.ts.map +1 -1
  61. package/dist/react/components/Switch.js +10 -3
  62. package/dist/react/components/Switch.js.map +1 -1
  63. package/dist/react/components/Tooltip.d.ts +1 -1
  64. package/dist/react/components/Tooltip.d.ts.map +1 -1
  65. package/dist/react/components/Tooltip.js +14 -1
  66. package/dist/react/components/Tooltip.js.map +1 -1
  67. package/dist/react/components/TooltipImpl.d.ts.map +1 -1
  68. package/dist/react/components/TooltipImpl.js +37 -7
  69. package/dist/react/components/TooltipImpl.js.map +1 -1
  70. package/dist/react/components/UnstyledButton.d.ts +37 -0
  71. package/dist/react/components/UnstyledButton.d.ts.map +1 -0
  72. package/dist/react/components/UnstyledButton.js +46 -0
  73. package/dist/react/components/UnstyledButton.js.map +1 -0
  74. package/dist/react/components/auth/SignInCard.d.ts +15 -1
  75. package/dist/react/components/auth/SignInCard.d.ts.map +1 -1
  76. package/dist/react/components/auth/SignInCard.js +6 -2
  77. package/dist/react/components/auth/SignInCard.js.map +1 -1
  78. package/dist/react/components/dialog.d.ts.map +1 -1
  79. package/dist/react/components/dialog.js +35 -3
  80. package/dist/react/components/dialog.js.map +1 -1
  81. package/dist/react/hooks/useCopyToClipboard.d.ts +10 -2
  82. package/dist/react/hooks/useCopyToClipboard.d.ts.map +1 -1
  83. package/dist/react/hooks/useCopyToClipboard.js +13 -4
  84. package/dist/react/hooks/useCopyToClipboard.js.map +1 -1
  85. package/dist/react/icons.d.ts +12 -1
  86. package/dist/react/icons.d.ts.map +1 -1
  87. package/dist/react/icons.js +12 -22
  88. package/dist/react/icons.js.map +1 -1
  89. package/dist/react/index.d.ts +8 -4
  90. package/dist/react/index.d.ts.map +1 -1
  91. package/dist/react/index.js +7 -3
  92. package/dist/react/index.js.map +1 -1
  93. package/dist/react/lib/hoverIntent.d.ts +21 -0
  94. package/dist/react/lib/hoverIntent.d.ts.map +1 -1
  95. package/dist/react/lib/hoverIntent.js +24 -0
  96. package/dist/react/lib/hoverIntent.js.map +1 -1
  97. package/dist/react/lib/openDialogs.d.ts +5 -0
  98. package/dist/react/lib/openDialogs.d.ts.map +1 -0
  99. package/dist/react/lib/openDialogs.js +45 -0
  100. package/dist/react/lib/openDialogs.js.map +1 -0
  101. package/dist/styles-areas/analytics.css +16 -16
  102. package/dist/styles-areas/calendar.css +7 -7
  103. package/dist/styles-areas/chat.css +9 -10
  104. package/dist/styles-areas/core.css +69 -62
  105. package/dist/styles-areas/data-table.css +16 -16
  106. package/dist/styles-areas/disk-usage.css +7 -7
  107. package/dist/styles-areas/emoji.css +3 -3
  108. package/dist/styles-areas/fields.css +10 -10
  109. package/dist/styles-areas/filter-rail.css +7 -8
  110. package/dist/styles-areas/layout-engine.css +5 -6
  111. package/dist/styles-areas/master-detail.css +6 -7
  112. package/dist/styles-areas/media-gallery.css +2 -2
  113. package/dist/styles-areas/media.css +2 -2
  114. package/dist/styles-areas/nav.css +17 -18
  115. package/dist/styles-areas/notifications.css +5 -5
  116. package/dist/styles-areas/overlays.css +5 -5
  117. package/dist/styles-areas/sharing.css +2 -2
  118. package/dist/styles-areas/stats.css +4 -4
  119. package/dist/styles-areas/wizard.css +3 -3
  120. package/dist/styles-areas/workbench.css +18 -19
  121. package/dist/styles-utilities.css +72 -0
  122. package/dist/testing/tooltip.d.ts.map +1 -1
  123. package/dist/testing/tooltip.js +8 -18
  124. package/dist/testing/tooltip.js.map +1 -1
  125. package/package.json +81 -3
  126. package/scripts/guardrails-baseline.json +1 -1
  127. package/scripts/styleAreas.ts +4 -0
  128. package/src/react/Toast.spec.tsx +17 -1
  129. package/src/react/Toast.tsx +20 -3
  130. package/src/react/components/Button.spec.tsx +49 -1
  131. package/src/react/components/Button.tsx +63 -6
  132. package/src/react/components/ChoiceGroup.spec.tsx +32 -0
  133. package/src/react/components/ChoiceGroup.tsx +78 -23
  134. package/src/react/components/CopyButton.spec.tsx +35 -0
  135. package/src/react/components/CopyButton.tsx +11 -2
  136. package/src/react/components/DataTable/DataTable.spec.tsx +16 -0
  137. package/src/react/components/DataTable/DataTable.tsx +266 -37
  138. package/src/react/components/DataTable/RowActions.tsx +5 -2
  139. package/src/react/components/DataTable/columnPriority.spec.ts +97 -0
  140. package/src/react/components/DataTable/columnPriority.ts +54 -0
  141. package/src/react/components/DataTable/types.ts +14 -0
  142. package/src/react/components/DataTable/virtualize.spec.tsx +47 -2
  143. package/src/react/components/EmptyState.spec.tsx +7 -0
  144. package/src/react/components/EmptyState.tsx +3 -0
  145. package/src/react/components/ErrorCard.spec.tsx +32 -0
  146. package/src/react/components/ErrorCard.tsx +17 -1
  147. package/src/react/components/Field.spec.tsx +61 -0
  148. package/src/react/components/Field.tsx +84 -27
  149. package/src/react/components/Menu.spec.tsx +90 -0
  150. package/src/react/components/Menu.tsx +204 -0
  151. package/src/react/components/Modal.spec.tsx +84 -0
  152. package/src/react/components/Modal.tsx +4 -1
  153. package/src/react/components/SaveIndicator.spec.tsx +35 -0
  154. package/src/react/components/SaveIndicator.tsx +85 -0
  155. package/src/react/components/SelectionBar.spec.tsx +32 -0
  156. package/src/react/components/SelectionBar.tsx +49 -0
  157. package/src/react/components/Switch.spec.tsx +25 -0
  158. package/src/react/components/Switch.tsx +49 -4
  159. package/src/react/components/Tooltip.spec.tsx +183 -2
  160. package/src/react/components/Tooltip.tsx +14 -2
  161. package/src/react/components/TooltipImpl.tsx +42 -8
  162. package/src/react/components/UnstyledButton.spec.tsx +61 -0
  163. package/src/react/components/UnstyledButton.tsx +83 -0
  164. package/src/react/components/auth/SignInCard.spec.tsx +34 -0
  165. package/src/react/components/auth/SignInCard.tsx +36 -3
  166. package/src/react/components/dialog.tsx +49 -9
  167. package/src/react/hooks/useCopyToClipboard.ts +17 -4
  168. package/src/react/icons.spec.ts +32 -1
  169. package/src/react/icons.tsx +59 -0
  170. package/src/react/index.ts +23 -2
  171. package/src/react/lib/hoverIntent.ts +28 -0
  172. package/src/react/lib/openDialogs.ts +48 -0
  173. package/src/styles-areas/analytics.css +16 -16
  174. package/src/styles-areas/calendar.css +7 -7
  175. package/src/styles-areas/chat.css +9 -10
  176. package/src/styles-areas/core.css +69 -62
  177. package/src/styles-areas/data-table.css +16 -16
  178. package/src/styles-areas/disk-usage.css +7 -7
  179. package/src/styles-areas/emoji.css +3 -3
  180. package/src/styles-areas/fields.css +10 -10
  181. package/src/styles-areas/filter-rail.css +7 -8
  182. package/src/styles-areas/layout-engine.css +5 -6
  183. package/src/styles-areas/master-detail.css +6 -7
  184. package/src/styles-areas/media-gallery.css +2 -2
  185. package/src/styles-areas/media.css +2 -2
  186. package/src/styles-areas/nav.css +17 -18
  187. package/src/styles-areas/notifications.css +5 -5
  188. package/src/styles-areas/overlays.css +5 -5
  189. package/src/styles-areas/sharing.css +2 -2
  190. package/src/styles-areas/stats.css +4 -4
  191. package/src/styles-areas/wizard.css +3 -3
  192. package/src/styles-areas/workbench.css +18 -19
  193. package/src/styles-utilities.css +72 -0
  194. package/src/testing/tooltip.spec.ts +19 -0
  195. 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
@@ -584,6 +584,24 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
584
584
  : null;
585
585
  const estimateRowHeight = fixedRowHeight ?? virtualOptions?.estimateRowHeight ?? (density === 'compact' ? 36 : 44);
586
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;
587
605
  /** Rows before the loaded window, and the size of the whole result set — both 0/`rows.length`
588
606
  * without a {@link serverWindow}, which makes every index below the same number as before. */
589
607
  const windowStart = serverWindow ? Math.max(0, serverWindow.start) : 0;
@@ -603,10 +621,11 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
603
621
  if (settleRef.current)
604
622
  clearTimeout(settleRef.current);
605
623
  }, []);
606
- const rowVirtualizer = useVirtualizer({
607
- count: virtualEnabled ? windowTotal : 0,
624
+ const selfVirtualizer = useVirtualizer({
625
+ count: virtualEnabled && !windowScroll ? windowTotal : 0,
626
+ enabled: virtualEnabled && !windowScroll,
608
627
  getScrollElement: () => scrollRef.current,
609
- estimateSize: () => estimateRowHeight,
628
+ estimateSize: () => rowEstimate,
610
629
  overscan: virtualOptions?.overscan ?? 8,
611
630
  onChange: serverWindow
612
631
  ? (instance) => {
@@ -658,8 +677,47 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
658
677
  // 🔴 MEASURED, not assumed. A wrapping cell, an expanded detail panel and the
659
678
  // `compact` density all change a row's height, and a caller cannot be asked to
660
679
  // compute any of them. `estimateSize` only seeds the spacer before first paint.
661
- 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();
662
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);
663
721
  /**
664
722
  * 🔴 Invalidate the measurement cache when the row height changes.
665
723
  *
@@ -676,11 +734,22 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
676
734
  * exists so the third virtualizer does not pay for it a third time — and it is
677
735
  * what caught this one.
678
736
  */
679
- // 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.
680
739
  useEffect(() => {
681
740
  rowVirtualizer.measure();
682
- }, [rowVirtualizer, estimateRowHeight]);
741
+ }, [rowVirtualizer, rowEstimate, activeTier]);
683
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]);
684
753
  /** The rows to render, and the two spacers that keep the scrollbar honest about the
685
754
  * ones that are not. Off, this is every row and no spacers — byte-identical output.
686
755
  * `null` is a row a {@link serverWindow} has not loaded: a placeholder of the estimated
@@ -688,25 +757,36 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
688
757
  const bodyRows = virtualEnabled
689
758
  ? virtualRows.map((v) => rows[v.index - windowStart] ?? null)
690
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
+ : [];
691
767
  useImperativeHandle(handleRef, () => ({
692
768
  get scrollElement() {
693
769
  return scrollRef.current;
694
770
  },
695
771
  scrollToIndex(index, options = {}) {
696
- const element = scrollRef.current;
772
+ const element = windowScroll ? window : scrollRef.current;
697
773
  if (!element || !virtualEnabled)
698
774
  return null;
699
775
  const clamped = Math.max(0, Math.min(index, loadedRef.current.total - 1));
700
- const [offset] = rowVirtualizer.getOffsetForIndex(clamped, options.align ?? 'center') ?? [clamped * estimateRowHeight];
776
+ const [offset] = rowVirtualizer.getOffsetForIndex(clamped, options.align ?? 'center') ?? [clamped * rowEstimate];
701
777
  const top = Math.max(0, Math.round(offset));
702
778
  element.scrollTo({ top, behavior: options.behavior ?? 'auto' });
703
779
  return top;
704
780
  },
705
- }), [estimateRowHeight, rowVirtualizer, virtualEnabled]);
706
- 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;
707
785
  const padBottom = virtualEnabled
708
- ? rowVirtualizer.getTotalSize() - (virtualRows.at(-1)?.end ?? 0)
786
+ ? Math.max(0, rowVirtualizer.getTotalSize() - ((virtualRows.at(-1)?.end ?? margin) - margin))
709
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;
710
790
  const hasActions = !!(onRowEdit || onRowDelete || rowMenu);
711
791
  // ── Row selection (see the `selectedIds` prop doc) ──────────────────────────
712
792
  const canSelect = (row) => isRowSelectable?.(row) ?? true;
@@ -817,19 +897,46 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
817
897
  // say so at the cut edge, or a narrow pane reads as a table with its right-hand
818
898
  // columns missing rather than one that scrolls (`useScrollEdgeFade`).
819
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;
820
918
  const tableMinWidth = fixedLayout
821
- ? 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))
822
920
  : minTableWidth;
823
- 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,
824
922
  // guardrails-ignore no-raw-overflow: dual-axis table scroll container — sticky header pins to it; ScrollArea cannot own horizontal table scroll.
825
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
826
924
  ? (_row, index) => {
827
925
  const tsRow = rows[index];
828
926
  return tsRow ? rowCheckbox(tsRow) : null;
829
927
  }
830
- : 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
831
- ? renderExpanded(row.original)
832
- : 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: (() => {
833
940
  const [title, ...rest] = cardColumns;
834
941
  // ONE shared grid template for every row — parent and child rows
835
942
  // lock into identical column slots, so the status/ports/actions
@@ -855,18 +962,24 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
855
962
  ...(hasActions ? ['32px'] : []),
856
963
  ].join(' ');
857
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] }));
858
- 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
+ }
859
969
  const isExpanded = renderExpanded != null && expandedRowId != null && row.id === expandedRowId;
860
- return (_jsxs(Fragment, { children: [_jsxs("div", { ...mergeRowProps({
970
+ const item = (_jsxs(_Fragment, { children: [_jsxs("div", { ...mergeRowProps({
861
971
  ...dragPropsFor(row.original),
862
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)),
863
973
  style: { gridTemplateColumns: template },
864
- }, 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));
865
978
  });
866
- 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 })] }));
867
980
  })() })), _jsx("div", { ref: bodyScrolls ? undefined : gridFade.ref, onScroll: bodyScrolls ? undefined : gridFade.onScroll, style: bodyScrolls ? undefined : gridFade.maskStyle,
868
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).
869
- 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: [(() => {
870
983
  // `ColDef.leading` columns sit BEFORE the checkbox column; see its doc.
871
984
  const renderHeader = (h, headerIndex) => {
872
985
  const sorted = h.column.getIsSorted();
@@ -897,13 +1010,13 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
897
1010
  frozen &&
898
1011
  (selectionShown
899
1012
  ? 'sticky left-11 bg-card shadow-[1px_0_0_0_var(--color-border)]'
900
- : '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
901
1014
  ? null
902
1015
  : renderTemplate(h.column.columnDef.header, h.getContext()) }, h.id));
903
1016
  };
904
1017
  const isLeading = (h) => h.column.columnDef.meta?.leading === true;
905
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)] }));
906
- })(), 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) => {
907
1020
  if (row === null) {
908
1021
  // A row the server window has not loaded yet: the estimated height, so
909
1022
  // the spacer arithmetic holds, and a skeleton line rather than a gap.
@@ -923,7 +1036,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
923
1036
  // every measurement lands on index 0 and the spacer never grows.
924
1037
  // (The ref stays OUT of the merge: `getRowProps` cannot take it.)
925
1038
  ref: virtualEnabled && fixedRowHeight === null ? rowVirtualizer.measureElement : undefined, ...mergeRowProps({
926
- ...(virtualEnabled ? { 'data-index': rowIndex } : {}),
1039
+ ...(virtualEnabled ? { 'data-index': rowIndex, 'aria-rowindex': rowIndex + 2 } : {}),
927
1040
  style: fixedRowHeight === null ? undefined : { height: fixedRowHeight },
928
1041
  'data-expanded': isExpanded || undefined,
929
1042
  ...dragPropsFor(row.original),
@@ -972,7 +1085,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
972
1085
  // `overflow: hidden`: a cell may host a menu/tooltip that
973
1086
  // has to escape its bounds.
974
1087
  fixedLayout && '[overflow-wrap:anywhere]', cell.column.columnDef.meta?.align === 'right' && 'text-right', cell.column.columnDef.meta?.hideBelow &&
975
- 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));
976
1089
  };
977
1090
  return (_jsxs(_Fragment, { children: [cells.filter(isLeading).map((cell) => renderCell(cell, -1)), selectionShown ? (_jsx("td", { ...selectCellProps(row), style: stickyFirstColumn
978
1091
  ? {
@@ -981,22 +1094,33 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
981
1094
  : 'var(--color-card)',
982
1095
  }
983
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)] }));
984
- })(), 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) => {
985
1098
  if (el && scrolledExpansionRef.current !== row.id) {
986
1099
  scrolledExpansionRef.current = row.id;
987
1100
  // A row expanded near the fold must reveal its panel.
988
1101
  el.scrollIntoView({ block: 'nearest' });
989
1102
  }
990
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));
991
- }), 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] }));
992
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;
993
1109
  /**
994
- * One row rendered as a phone card: first visible column = the title zone (unlabeled,
995
- * full width), labeled columns = compact label/value pairs, empty-labeled columns
996
- * (an actions column) + the row-actions kebab = the bordered footer row, actions
997
- * anchored bottom-right inside the card bounds. Values render through the SAME
998
- * {@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.
999
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
+ }
1000
1124
  function MobileRowCard({ row, columns, className, onRowClick, dragProps, select, actions, expanded, }) {
1001
1125
  const visible = columns.filter((c) => !c.hidden);
1002
1126
  const [title, ...rest] = visible;