@alexgorbatchev/shadcn 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -429,6 +429,8 @@ import {
429
429
 
430
430
  `InlineEdit` mounts an edit session with a controlled `value`, `onValueChange`, `onCommit`, and `onCancel`. Mount it in place of the displayed label. Its input focuses and selects its text by default; `shouldSelectOnMount={false}` preserves the initial caret. Enter or blur commits once, Escape cancels without committing, and asynchronous saves disable the input until completion. Set `hasChanges={false}` to cancel an unchanged edit before calling `onCommit`; the caller owns normalization and comparison. It defaults to true, so creation sessions can commit their initial value. Rejected saves display a generic error and allow retry; `renderError` can map the unknown failure to application-authored guidance.
431
431
 
432
+ The editor inherits the containing label's font size and line height and occupies that single line. Keep the same text context around display and edit states. Validation appears in an anchored tooltip with an announced alert, so errors do not move neighboring content.
433
+
432
434
  `VirtualTable` renders a bounded, dynamically measured set of native table rows using TanStack Virtual. Supply `items`, stable `getItemKey`, `header`, `label`, and `renderRow`; spread the supplied measurement props onto each native row. Optional `columns` accepts a `TableColumnGroup`; responsive columns keep their widths aligned with cells that share their `visibleFrom`. `maxHeight` defaults to `65vh`, `estimateRowHeight` to 80 pixels, and `overscan` to five rows. `retainedKeys` keeps editing rows mounted; the focused row is retained automatically. Filtering and selection belong to the consumer.
433
435
 
434
436
  `VirtualDataGrid` renders a TanStack Table instance built with `dataGridFeatures` through `VirtualTable`: every filtered and sorted row, without pagination, in a fixed-layout table. Column `meta` sets `label`, `textAlign`, `truncate`, `visibleFrom`, `fill`, and `sortColumnIds`; a `fill` column takes the width the sized columns leave, and `sortColumnIds` names other columns the header sorts with its own buttons so its `aria-sort` reports them. Other resizable columns get a window-splitter handle that resizes by dragging, by the arrow keys, Home, and End, and resets on double-click. Set `columnResizeMode: "onChange"` so columns follow the pointer while dragging; TanStack's default `"onEnd"` resizes only on release. Widths change through CSS variables, so a `useTable` selector can omit `columnSizing` and `columnResizing` to keep resizing from rendering rows. `renderRow` receives each row and its measurement props; spread the props onto a `TableRow` and render `DataGridCells` inside it to wrap rows in menus or drag sources. `retainedRowIds` keeps rows mounted while they hold editors. `DataGridColumnOptions` shows, hides, and reorders the columns that allow hiding; its reset restores the table's initial visibility, order, and sizes.
@@ -437,6 +439,10 @@ Column `meta.filter` offers a column in `DataGridFilters`, hidden columns includ
437
439
 
438
440
  Import `DataGridSelectHeader` and `DataGridSelectCell` from `@alexgorbatchev/shadcn/data-grid`. Pass the current `useTable` result as `table` to both controls, and pass `row` and an accessible `label` to each cell. The header selects selectable filtered rows across all pages and preserves selections hidden by filters. It shows a mixed state when some of those rows are selected and disables itself when none are selectable. Its default label is "Select all filtered rows"; provide `label` to name the scope in your application. Cells follow native row selection rules, including disabled rows and partially selected parents. The controls observe selection changes without requiring the parent table selector to include selection state.
439
441
 
442
+ For grids that must fit their container, set `VirtualDataGrid fitColumns`. Use exactly one `meta.fill` column with an explicit `minSize` and a preferred `size`; required columns' minima plus that minimum must fit the smallest supported container. The grid reserves the fill column's preferred size, bounded by its native sizing limits, and temporarily hides optional columns that do not fit, prioritizing their chosen order. When only required columns remain, the fill column can shrink toward its minimum. The grid lists hidden columns and explains how to reveal them. This mode replaces `meta.visibleFrom` breakpoints and offers resize handles on leaf headers only.
443
+
444
+ Keep preferred widths separate from the table's rendered sizing atom. In `onFitWidthChange`, store the reported width in a ref and write `fitDataGridColumnSizing(table, preferredWidths, width)` into the sizing atom. The helper temporarily shrinks required resizable columns toward their minima when needed and restores their preferred widths when space returns. In `onColumnSizingChange`, obtain new preferences with `constrainDataGridColumnSizing(table, updater, widthRef.current, preferredWidths)`, then derive the sizing atom with `fitDataGridColumnSizing`. Persist only the preferred widths, after the resize settles. Native pointer and keyboard resizing starts at the displayed width, stays within the available space, and leaves other columns' preferences unchanged. Widening a required column can hide optional columns again; widening an optional column preserves its fitted neighbours and the fill column's preferred space. Container changes never overwrite user visibility, order, or width preferences. The `VirtualDataGridExample` fixture demonstrates this controlled integration.
445
+
440
446
  `ActionsMenu` shares one `items` definition between a dropdown action and an optional context menu. Supply an accessible `label`; the default trigger is a ghost icon button. `size="sm"` displays the label, and `variant`, `disabled`, and `trigger` customize the action. Supplying `contextTarget` composes the native context trigger with that element and adds its popup. Compose items with `MenuItem`, `MenuLinkItem`, checkbox/radio parts, groups, labels, separators, shortcuts, and submenu parts; dropdown and context menus share these implementations. `MenuLinkItem` retains its native anchor and menu keyboard semantics.
441
447
 
442
448
  For nested layouts, compose `ActionsMenuTrigger`, `ActionsMenuContextTarget`, and `ActionsMenuContextContent` explicitly inside `ActionsMenu`. The dropdown trigger owns its local native dropdown root and popup; the context target and content share an independent context root. A tree row can use `TreeItem render={<ActionsMenuContextTarget />}` and put the trigger in its action slot. For a table row, render the context target as the actual `tr`, then place the dropdown trigger and context content inside a `td`: native popup portals insert focus guards at their declaration, which must remain valid table children.
@@ -46,10 +46,62 @@ import {
46
46
  tableFeatures
47
47
  } from "@tanstack/react-table";
48
48
  import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon } from "lucide-react";
49
- import { useId } from "react";
49
+ import { useContext as useContext2, useId } from "react";
50
+
51
+ // src/components/data-grid/DataGridColumnFitContext.ts
52
+ import { createContext } from "react";
53
+ var DataGridColumnFitContext = createContext(undefined);
50
54
 
51
55
  // src/components/data-grid/DataGridColumnResizer.tsx
52
56
  import { Subscribe } from "@tanstack/react-table";
57
+ import { useContext } from "react";
58
+
59
+ // src/components/data-grid/getDataGridFillPreferredSize.ts
60
+ function getDataGridFillPreferredSize(column) {
61
+ return Math.min(column.maxSize ?? Number.MAX_SAFE_INTEGER, Math.max(column.minSize ?? 20, column.size ?? 150));
62
+ }
63
+
64
+ // src/components/data-grid/getDataGridColumnFit.ts
65
+ function getDataGridColumnFit(table, width) {
66
+ const columns = table.getVisibleLeafColumns();
67
+ let remaining = width;
68
+ for (const column of columns) {
69
+ if (column.columnDef.meta?.fill)
70
+ remaining -= getDataGridFillPreferredSize(column.columnDef);
71
+ else if (!column.getCanHide())
72
+ remaining -= column.getSize();
73
+ }
74
+ const hidden = [];
75
+ for (const column of columns) {
76
+ if (column.columnDef.meta?.fill || !column.getCanHide())
77
+ continue;
78
+ if (column.getSize() <= remaining)
79
+ remaining -= column.getSize();
80
+ else
81
+ hidden.push(column.id);
82
+ }
83
+ return hidden;
84
+ }
85
+
86
+ // src/components/data-grid/getDataGridColumnFitMaximum.ts
87
+ function getDataGridColumnFitMaximum(table, columnId, width) {
88
+ const hidden = getDataGridColumnFit(table, width);
89
+ const column = table.getColumn(columnId);
90
+ const isRequired = column && !column.getCanHide();
91
+ const occupied = table.getVisibleLeafColumns().reduce((sum, other) => {
92
+ if (other.id === columnId)
93
+ return sum;
94
+ if (other.columnDef.meta?.fill) {
95
+ return sum + (isRequired ? other.columnDef.minSize ?? 20 : getDataGridFillPreferredSize(other.columnDef));
96
+ }
97
+ if (isRequired && other.getCanHide() || hidden.includes(other.id))
98
+ return sum;
99
+ return sum + other.getSize();
100
+ }, 0);
101
+ return Math.max(column?.columnDef.minSize ?? 20, Math.min(column?.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER, Math.floor(width - occupied)));
102
+ }
103
+
104
+ // src/components/data-grid/DataGridColumnResizer.tsx
53
105
  import { jsx } from "react/jsx-runtime";
54
106
  var KEYBOARD_STEP = 16;
55
107
  function DataGridColumnResizer({
@@ -58,14 +110,16 @@ function DataGridColumnResizer({
58
110
  }) {
59
111
  const column = header.column;
60
112
  const table = column.table;
113
+ const fit = useContext(DataGridColumnFitContext);
61
114
  const minSize = column.columnDef.minSize ?? 0;
62
- const maxSize = column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
115
+ const getMaxSize = () => fit ? getDataGridColumnFitMaximum(table, column.id, fit.width) : column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
63
116
  const resize = header.getResizeHandler();
64
- const setSize = (size) => table.setColumnSizing((sizes) => ({ ...sizes, [column.id]: Math.min(maxSize, Math.max(minSize, size)) }));
117
+ const setSize = (size) => table.setColumnSizing((sizes) => ({ ...sizes, [column.id]: Math.min(getMaxSize(), Math.max(minSize, size)) }));
65
118
  const onKeyDown = (event) => {
66
119
  const growKey = table.options.columnResizeDirection === "rtl" ? "ArrowLeft" : "ArrowRight";
67
120
  const shrinkKey = growKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
68
121
  const size = column.getSize();
122
+ const maxSize = getMaxSize();
69
123
  const next = event.key === growKey ? size + KEYBOARD_STEP : event.key === shrinkKey ? size - KEYBOARD_STEP : event.key === "Home" ? minSize : event.key === "End" && maxSize < Number.MAX_SAFE_INTEGER ? maxSize : undefined;
70
124
  if (next === undefined)
71
125
  return;
@@ -74,14 +128,14 @@ function DataGridColumnResizer({
74
128
  };
75
129
  return /* @__PURE__ */ jsx(Subscribe, {
76
130
  source: table.store,
77
- selector: (state) => `${state.columnSizing[column.id]}:${state.columnResizing.isResizingColumn === column.id}`,
131
+ selector: (state) => `${state.columnSizing[column.id]}:${getMaxSize()}:${state.columnResizing.isResizingColumn === column.id}`,
78
132
  children: () => /* @__PURE__ */ jsx("div", {
79
133
  role: "separator",
80
134
  "aria-orientation": "vertical",
81
135
  "aria-label": `Resize ${label}`,
82
136
  "aria-valuenow": column.getSize(),
83
137
  "aria-valuemin": minSize,
84
- "aria-valuemax": maxSize < Number.MAX_SAFE_INTEGER ? maxSize : undefined,
138
+ "aria-valuemax": getMaxSize() < Number.MAX_SAFE_INTEGER ? getMaxSize() : undefined,
85
139
  tabIndex: 0,
86
140
  "data-slot": "data-grid-column-resizer",
87
141
  "data-resizing": column.getIsResizing() || undefined,
@@ -139,18 +193,23 @@ function getAriaSort(column) {
139
193
  const sorted = sortable.map((candidate) => candidate?.getIsSorted()).find(Boolean);
140
194
  return sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none";
141
195
  }
142
- function DataGridHeaderCell({ header, isResizable }) {
196
+ function DataGridHeaderCell({
197
+ header,
198
+ isResizable,
199
+ colSpan
200
+ }) {
143
201
  const contentId = useId();
202
+ const fit = useContext2(DataGridColumnFitContext);
144
203
  const meta = header.column.columnDef.meta;
145
- const hasResizer = isResizable && !meta?.fill && header.column.getCanResize();
204
+ const hasResizer = isResizable && !meta?.fill && header.column.getCanResize() && (!fit || header.subHeaders.length === 0);
146
205
  return /* @__PURE__ */ jsx2(TableHead2, {
147
206
  scope: "col",
148
- colSpan: header.colSpan,
207
+ colSpan,
149
208
  "aria-sort": getAriaSort(header.column),
150
209
  "aria-labelledby": hasResizer ? contentId : undefined,
151
210
  textAlign: meta?.textAlign,
152
211
  truncate: meta?.truncate,
153
- visibleFrom: meta?.visibleFrom,
212
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
154
213
  className: hasResizer ? "relative" : undefined,
155
214
  children: hasResizer ? /* @__PURE__ */ jsxs(Fragment, {
156
215
  children: [
@@ -174,23 +233,29 @@ function DataGridHeaderRows({
174
233
  table,
175
234
  isResizable = false
176
235
  }) {
236
+ const fit = useContext2(DataGridColumnFitContext);
177
237
  return /* @__PURE__ */ jsx2(Fragment, {
178
238
  children: table.getHeaderGroups().map((group) => /* @__PURE__ */ jsx2(TableRow2, {
179
- children: group.headers.map((header) => /* @__PURE__ */ jsx2(DataGridHeaderCell, {
180
- header,
181
- isResizable
182
- }, header.id))
239
+ children: group.headers.map((header) => {
240
+ const colSpan = fit ? header.getLeafHeaders().filter((leaf) => leaf.subHeaders.length === 0 && !fit.hiddenColumnIds.includes(leaf.column.id)).length : header.colSpan;
241
+ return colSpan > 0 ? /* @__PURE__ */ jsx2(DataGridHeaderCell, {
242
+ header,
243
+ isResizable,
244
+ colSpan
245
+ }, header.id) : null;
246
+ })
183
247
  }, group.id))
184
248
  });
185
249
  }
186
250
  function DataGridCells2({ row }) {
251
+ const fit = useContext2(DataGridColumnFitContext);
187
252
  return /* @__PURE__ */ jsx2(Fragment, {
188
- children: row.getVisibleCells().map((cell) => {
253
+ children: row.getVisibleCells().filter((cell) => !fit?.hiddenColumnIds.includes(cell.column.id)).map((cell) => {
189
254
  const meta = cell.column.columnDef.meta;
190
255
  return /* @__PURE__ */ jsx2(TableCell2, {
191
256
  textAlign: meta?.textAlign,
192
257
  truncate: meta?.truncate,
193
- visibleFrom: meta?.visibleFrom,
258
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
194
259
  children: /* @__PURE__ */ jsx2(FlexRender, {
195
260
  cell
196
261
  })
@@ -199,13 +264,14 @@ function DataGridCells2({ row }) {
199
264
  });
200
265
  }
201
266
  function DataGridColumns({ table }) {
267
+ const fit = useContext2(DataGridColumnFitContext);
202
268
  const variables = getDataGridColumnSizeVariables(table);
203
269
  return /* @__PURE__ */ jsx2(TableColumnGroup2, {
204
- children: table.getVisibleLeafColumns().map((column) => {
270
+ children: table.getVisibleLeafColumns().filter((column) => !fit?.hiddenColumnIds.includes(column.id)).map((column) => {
205
271
  const meta = column.columnDef.meta;
206
272
  const variable = variables.get(column.id);
207
273
  return /* @__PURE__ */ jsx2(TableColumn2, {
208
- visibleFrom: meta?.visibleFrom,
274
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
209
275
  width: meta?.fill ? undefined : `var(${variable}, ${column.getSize()}px)`
210
276
  }, column.id);
211
277
  })
@@ -364,4 +430,4 @@ function DataGridPagination2({ table }) {
364
430
  });
365
431
  }
366
432
 
367
- export { getDataGridColumnSizeVariables, dataGridFeatures2, getDataGridColumnLabel, DataGridHeaderRows, DataGridCells2, DataGridColumns, DataGrid2, DataGridColumnHeader2, DataGridFilter2, DataGridPagination2 };
433
+ export { DataGridColumnFitContext, getDataGridColumnFit, getDataGridColumnFitMaximum, getDataGridColumnSizeVariables, dataGridFeatures2, getDataGridColumnLabel, DataGridHeaderRows, DataGridCells2, DataGridColumns, DataGrid2, DataGridColumnHeader2, DataGridFilter2, DataGridPagination2 };
@@ -2542,7 +2542,7 @@ function AttachmentGroup2({ density, className, ...props }) {
2542
2542
  });
2543
2543
  }
2544
2544
  // src/components/InlineEdit.tsx
2545
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
2545
+ import { useEffect as useEffect2, useId as useId2, useRef as useRef2, useState as useState2 } from "react";
2546
2546
  import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
2547
2547
  function InlineEdit2({
2548
2548
  analytics,
@@ -2558,6 +2558,8 @@ function InlineEdit2({
2558
2558
  onCancel
2559
2559
  }) {
2560
2560
  const track = useAnalytics2("InlineEdit", analytics);
2561
+ const triggerId = useId2();
2562
+ const errorId = `${triggerId}-error`;
2561
2563
  const input = useRef2(null);
2562
2564
  const isFinished = useRef2(false);
2563
2565
  const shouldCancel = useRef2(false);
@@ -2588,48 +2590,63 @@ function InlineEdit2({
2588
2590
  setSaving(false);
2589
2591
  }
2590
2592
  };
2591
- return /* @__PURE__ */ jsxs15("form", {
2592
- className: "flex min-w-0 flex-col gap-1",
2593
+ return /* @__PURE__ */ jsx26("form", {
2594
+ className: "min-w-0",
2593
2595
  onSubmit: (event) => {
2594
2596
  event.preventDefault();
2595
2597
  save();
2596
2598
  },
2597
- children: [
2598
- /* @__PURE__ */ jsx26(Input2, {
2599
- analytics: false,
2600
- ref: input,
2601
- density,
2602
- "aria-label": label,
2603
- "aria-invalid": Boolean(error),
2604
- "aria-description": "Enter saves. Escape cancels.",
2605
- value,
2606
- disabled: isDisabled || isSaving,
2607
- onChange: (event) => onValueChange(event.target.value),
2608
- onBlur: () => {
2609
- if (shouldCancel.current && !isFinished.current) {
2610
- isFinished.current = true;
2611
- onCancel();
2612
- } else
2613
- save();
2614
- },
2615
- onKeyDown: (event) => {
2616
- if (event.key === "Escape") {
2617
- event.preventDefault();
2618
- event.stopPropagation();
2619
- shouldCancel.current = true;
2620
- event.currentTarget.blur();
2621
- } else if (event.key === "Enter") {
2622
- event.preventDefault();
2623
- event.currentTarget.blur();
2624
- }
2625
- }
2626
- }),
2627
- error && /* @__PURE__ */ jsx26("p", {
2628
- role: "alert",
2629
- className: "text-xs text-destructive",
2630
- children: error
2631
- })
2632
- ]
2599
+ children: /* @__PURE__ */ jsxs15(Tooltip2, {
2600
+ open: Boolean(error),
2601
+ triggerId,
2602
+ children: [
2603
+ /* @__PURE__ */ jsx26(TooltipTrigger2, {
2604
+ id: triggerId,
2605
+ render: /* @__PURE__ */ jsx26(Input2, {
2606
+ analytics: false,
2607
+ ref: input,
2608
+ density,
2609
+ className: "h-[1lh] rounded-none border-0 px-0 py-0 text-[length:inherit] leading-[inherit]",
2610
+ "aria-label": label,
2611
+ "aria-invalid": Boolean(error),
2612
+ "aria-errormessage": error ? errorId : undefined,
2613
+ "aria-description": "Enter saves. Escape cancels.",
2614
+ value,
2615
+ disabled: isDisabled || isSaving,
2616
+ onChange: (event) => onValueChange(event.target.value),
2617
+ onBlur: () => {
2618
+ if (shouldCancel.current && !isFinished.current) {
2619
+ isFinished.current = true;
2620
+ onCancel();
2621
+ } else
2622
+ save();
2623
+ },
2624
+ onKeyDown: (event) => {
2625
+ if (event.key === "Escape") {
2626
+ event.preventDefault();
2627
+ event.stopPropagation();
2628
+ shouldCancel.current = true;
2629
+ event.currentTarget.blur();
2630
+ } else if (event.key === "Enter") {
2631
+ event.preventDefault();
2632
+ event.currentTarget.blur();
2633
+ }
2634
+ }
2635
+ })
2636
+ }),
2637
+ error && /* @__PURE__ */ jsx26(TooltipContent2, {
2638
+ density,
2639
+ side: "bottom",
2640
+ align: "start",
2641
+ children: /* @__PURE__ */ jsx26("p", {
2642
+ id: errorId,
2643
+ role: "alert",
2644
+ className: "text-destructive",
2645
+ children: error
2646
+ })
2647
+ })
2648
+ ]
2649
+ })
2633
2650
  });
2634
2651
  }
2635
2652
  // src/components/StarRating.tsx
@@ -3805,7 +3822,7 @@ function StatCard2({
3805
3822
  });
3806
3823
  }
3807
3824
  // src/components/AuthForm.tsx
3808
- import { useId as useId2, useState as useState5 } from "react";
3825
+ import { useId as useId3, useState as useState5 } from "react";
3809
3826
  import { jsx as jsx40, jsxs as jsxs25 } from "react/jsx-runtime";
3810
3827
  function AuthForm2({
3811
3828
  mode = "sign-in",
@@ -3815,7 +3832,7 @@ function AuthForm2({
3815
3832
  disabled = false,
3816
3833
  density
3817
3834
  }) {
3818
- const id = useId2();
3835
+ const id = useId3();
3819
3836
  const [pending, setPending] = useState5(false);
3820
3837
  const [error, setError] = useState5("");
3821
3838
  const title = mode === "sign-up" ? "Create an account" : mode === "reset-password" ? "Reset your password" : "Sign in";
@@ -12,6 +12,6 @@ interface IInlineEditProps extends IDensityProps, IAnalyticsProps {
12
12
  onCommit: (value: string) => void | Promise<void>;
13
13
  onCancel: () => void;
14
14
  }
15
- /** A mounted edit session. The consumer replaces its display with this field while editing. */
15
+ /** A mounted edit session that inherits its text line's size; validation floats without moving adjacent content. */
16
16
  export declare function InlineEdit({ analytics, label, value, density, isDisabled, shouldSelectOnMount, hasChanges, renderError, onValueChange, onCommit, onCancel, }: IInlineEditProps): ReactElement;
17
17
  export {};
@@ -0,0 +1,6 @@
1
+ export interface IDataGridColumnFit {
2
+ width: number;
3
+ hiddenColumnIds: readonly string[];
4
+ }
5
+ /** Layout-only visibility; the table's column choices remain unchanged. */
6
+ export declare const DataGridColumnFitContext: import("react").Context<IDataGridColumnFit | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { type CellData, type Header, type RowData } from "@tanstack/react-table";
2
- import type { ReactElement } from "react";
2
+ import { type ReactElement } from "react";
3
3
  import type { DataGridFeatures } from "./DataGrid.js";
4
4
  interface IDataGridColumnResizerProps<T extends RowData> {
5
5
  header: Header<DataGridFeatures, T, CellData>;
@@ -0,0 +1,10 @@
1
+ import { type ColumnSizingState, type RowData, type Table, type Updater } from "@tanstack/react-table";
2
+ import type { DataGridFeatures } from "./DataGrid.js";
3
+ /**
4
+ * Apply a user resize while preserving fill space. Required columns may displace optional columns; optional
5
+ * columns preserve fitted neighbours and the fill preference. Use in the table's
6
+ * `onColumnSizingChange`, passing the width reported by `VirtualDataGrid.onFitWidthChange`, then store the result in
7
+ * preferred sizes. Then pass those preferences through `fitDataGridColumnSizing` into the table's sizing atom.
8
+ * Container changes never call this helper or rewrite preferred sizes.
9
+ */
10
+ export declare function constrainDataGridColumnSizing<T extends RowData>(table: Table<DataGridFeatures, T>, updater: Updater<ColumnSizingState>, width: number, preferred: ColumnSizingState): ColumnSizingState;
@@ -0,0 +1,8 @@
1
+ import type { ColumnSizingState, RowData, Table } from "@tanstack/react-table";
2
+ import type { DataGridFeatures } from "./DataGrid.js";
3
+ /**
4
+ * Derive rendered sizes from preferred sizes. Required resizable columns give up space toward their minima only
5
+ * when needed to preserve the fill minimum; widening restores the preferences. Store this result in the table's
6
+ * sizing atom, separately from preferences, so native resize handlers start at the width the user sees.
7
+ */
8
+ export declare function fitDataGridColumnSizing<T extends RowData>(table: Table<DataGridFeatures, T>, preferred: ColumnSizingState, width: number): ColumnSizingState;
@@ -0,0 +1,4 @@
1
+ import type { RowData, Table } from "@tanstack/react-table";
2
+ import type { DataGridFeatures } from "./DataGrid.js";
3
+ /** Columns that do not fit after reserving each fill column's preferred size and every required fixed column. */
4
+ export declare function getDataGridColumnFit<T extends RowData>(table: Table<DataGridFeatures, T>, width: number): string[];
@@ -0,0 +1,4 @@
1
+ import type { RowData, Table } from "@tanstack/react-table";
2
+ import type { DataGridFeatures } from "./DataGrid.js";
3
+ /** Required columns reclaim optional space; optional columns keep fitted neighbours and the fill preference. */
4
+ export declare function getDataGridColumnFitMaximum<T extends RowData>(table: Table<DataGridFeatures, T>, columnId: string, width: number): number;
@@ -0,0 +1,3 @@
1
+ import type { ColumnDef_ColumnSizing } from "@tanstack/react-table";
2
+ /** The fill reservation before optional columns are admitted, bounded by the column's native sizing limits. */
3
+ export declare function getDataGridFillPreferredSize(column: ColumnDef_ColumnSizing): number;
@@ -0,0 +1,6 @@
1
+ import type { RowData, Table } from "@tanstack/react-table";
2
+ import { type RefObject } from "react";
3
+ import type { DataGridFeatures } from "../DataGrid.js";
4
+ import type { IDataGridColumnFit } from "../DataGridColumnFitContext.js";
5
+ /** Measures the rendered table and derives temporary visibility without changing the table's saved choices. */
6
+ export declare function useDataGridColumnFit<T extends RowData>(table: Table<DataGridFeatures, T>, scope: RefObject<HTMLElement | null>, isEnabled: boolean, onWidthChange?: (width: number) => void): IDataGridColumnFit | undefined;
@@ -1,6 +1,8 @@
1
1
  export { DataGrid, DataGridCells, DataGridColumnHeader, DataGridFilter, DataGridPagination, dataGridFeatures, } from "./DataGrid.js";
2
2
  export type { DataGridColumn, DataGridColumnFilter, DataGridFeatures, DataGridProps, DataGridRow, DataGridTable, IDataGridColumnMeta, IDataGridControlProps, DataGridFilterOptions, IDataGridFilterControlProps, IDataGridOptionsFilter, IDataGridRangeFilter, } from "./DataGrid.js";
3
3
  export { DataGridColumnOptions } from "./DataGridColumnOptions.js";
4
+ export { constrainDataGridColumnSizing } from "./constrainDataGridColumnSizing.js";
5
+ export { fitDataGridColumnSizing } from "./fitDataGridColumnSizing.js";
4
6
  export { DataGridFilters } from "./DataGridFilters.js";
5
7
  export { DataGridFilterSummary } from "./DataGridFilterSummary.js";
6
8
  export { DataGridSelectHeader } from "./DataGridSelectHeader.js";
@@ -1,3 +1,13 @@
1
+ import {
2
+ getDataGridColumnFitMaximum,
3
+ dataGridFeatures2,
4
+ getDataGridColumnLabel,
5
+ DataGridCells2,
6
+ DataGrid2,
7
+ DataGridColumnHeader2,
8
+ DataGridFilter2,
9
+ DataGridPagination2
10
+ } from "../../chunk-kyctwprg.js";
1
11
  import"../../chunk-7ny0vyek.js";
2
12
  import"../../chunk-8ssktt4b.js";
3
13
  import"../../chunk-en44w1pn.js";
@@ -11,15 +21,6 @@ import"../../chunk-tmr0jqv8.js";
11
21
  import"../../chunk-p6z2m3zp.js";
12
22
  import"../../chunk-7jpsybag.js";
13
23
  import"../../chunk-7vg045gd.js";
14
- import {
15
- dataGridFeatures2,
16
- getDataGridColumnLabel,
17
- DataGridCells2,
18
- DataGrid2,
19
- DataGridColumnHeader2,
20
- DataGridFilter2,
21
- DataGridPagination2
22
- } from "../../chunk-x7emg3jx.js";
23
24
  import {
24
25
  Checkbox2
25
26
  } from "../../chunk-b5s5y5wq.js";
@@ -170,6 +171,45 @@ function DataGridColumnOptions({
170
171
  ]
171
172
  });
172
173
  }
174
+ // src/components/data-grid/constrainDataGridColumnSizing.ts
175
+ import {
176
+ functionalUpdate
177
+ } from "@tanstack/react-table";
178
+ function constrainDataGridColumnSizing(table, updater, width, preferred) {
179
+ const previous = table.atoms.columnSizing.get();
180
+ const next = functionalUpdate(updater, previous);
181
+ return Object.fromEntries(Object.entries(next).map(([id, size]) => {
182
+ const column = table.getColumn(id);
183
+ if (size === previous[id])
184
+ return [id, preferred[id]];
185
+ const fitted = width <= 0 || !column?.getIsVisible() || column.columnDef.meta?.fill ? size : Math.max(column.columnDef.minSize ?? 20, Math.min(size, getDataGridColumnFitMaximum(table, id, width)));
186
+ return [id, fitted === (column?.getSize() ?? previous[id]) ? preferred[id] : fitted];
187
+ }).filter(([, size]) => size !== undefined));
188
+ }
189
+ // src/components/data-grid/fitDataGridColumnSizing.ts
190
+ function fitDataGridColumnSizing(table, preferred, width) {
191
+ const next = { ...preferred };
192
+ if (width > 0) {
193
+ const required = table.getVisibleLeafColumns().filter((column) => !column.getCanHide() || column.columnDef.meta?.fill);
194
+ const preferredSize = (column) => Math.min(column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER, Math.max(column.columnDef.minSize ?? 20, preferred[column.id] ?? column.columnDef.size ?? 150));
195
+ let excess = required.reduce((sum, column) => sum + (column.columnDef.meta?.fill ? column.columnDef.minSize ?? 20 : preferredSize(column)), 0) - width;
196
+ for (const column of required.toReversed()) {
197
+ if (excess <= 0)
198
+ break;
199
+ if (column.columnDef.meta?.fill || !column.getCanResize())
200
+ continue;
201
+ const size = preferredSize(column);
202
+ const shrink = Math.min(excess, size - (column.columnDef.minSize ?? 20));
203
+ if (shrink > 0) {
204
+ next[column.id] = size - shrink;
205
+ excess -= shrink;
206
+ }
207
+ }
208
+ }
209
+ const current = table.atoms.columnSizing.get();
210
+ const entries = Object.entries(next);
211
+ return entries.length === Object.keys(current).length && entries.every(([id, size]) => current[id] === size) ? current : next;
212
+ }
173
213
  // src/components/data-grid/DataGridFilters.tsx
174
214
  import { Subscribe as Subscribe2 } from "@tanstack/react-table";
175
215
  import { ListFilterIcon } from "lucide-react";
@@ -577,5 +617,7 @@ export {
577
617
  DataGridPagination2 as DataGridPagination,
578
618
  DataGridSelectCell,
579
619
  DataGridSelectHeader,
580
- dataGridFeatures2 as dataGridFeatures
620
+ constrainDataGridColumnSizing,
621
+ dataGridFeatures2 as dataGridFeatures,
622
+ fitDataGridColumnSizing
581
623
  };
@@ -31,7 +31,7 @@ import {
31
31
  DrawerHeader2,
32
32
  DrawerTitle2,
33
33
  DrawerDescription2
34
- } from "../../chunk-aye7r5mq.js";
34
+ } from "../../chunk-zwnba1pb.js";
35
35
  import {
36
36
  MenuPopup,
37
37
  MenuItem2,
@@ -8,6 +8,17 @@ interface IVirtualDataGridProps<T extends RowData, TSelected> {
8
8
  maxHeight?: number | string;
9
9
  estimateRowHeight?: number;
10
10
  overscan?: number;
11
+ /**
12
+ * Reserve one fill column's preferred `size` and hide optional columns that do not fit, in their chosen order.
13
+ * The fill column may shrink toward `minSize` when only required columns remain. Replaces
14
+ * `meta.visibleFrom` breakpoints; saved visibility and widths are unchanged. Use exactly one fill column, and
15
+ * ensure required columns' minima plus its minimum fit the smallest supported container. Only leaf headers resize.
16
+ * Pair with `fitDataGridColumnSizing` and `constrainDataGridColumnSizing` to preserve preferred widths separately
17
+ * from rendered widths and constrain native pointer resizing.
18
+ */
19
+ fitColumns?: boolean;
20
+ /** Reports usable width; derive the sizing atom with `fitDataGridColumnSizing`, keeping preferences unchanged. */
21
+ onFitWidthChange?: (width: number) => void;
11
22
  /** Row IDs that stay rendered while scrolled out of view, such as rows with an open editor. */
12
23
  retainedRowIds?: readonly string[];
13
24
  /**
@@ -22,5 +33,5 @@ interface IVirtualDataGridProps<T extends RowData, TSelected> {
22
33
  * Column widths change through CSS variables, so a `useTable` selector can leave out sizing state and resizing never
23
34
  * renders rows again.
24
35
  */
25
- export declare function VirtualDataGrid<T extends RowData, TSelected>({ table, label, maxHeight, estimateRowHeight, overscan, retainedRowIds, renderRow, }: IVirtualDataGridProps<T, TSelected>): ReactElement;
36
+ export declare function VirtualDataGrid<T extends RowData, TSelected>({ table, label, maxHeight, estimateRowHeight, overscan, fitColumns, onFitWidthChange, retainedRowIds, renderRow, }: IVirtualDataGridProps<T, TSelected>): ReactElement;
26
37
  export {};
@@ -2,6 +2,15 @@ import"../../chunk-en44w1pn.js";
2
2
  import {
3
3
  TableRow2
4
4
  } from "../../chunk-tmr0jqv8.js";
5
+ import {
6
+ DataGridColumnFitContext,
7
+ getDataGridColumnFit,
8
+ getDataGridColumnSizeVariables,
9
+ getDataGridColumnLabel,
10
+ DataGridHeaderRows,
11
+ DataGridCells2,
12
+ DataGridColumns
13
+ } from "../../chunk-kyctwprg.js";
5
14
  import"../../chunk-7ny0vyek.js";
6
15
  import"../../chunk-8ssktt4b.js";
7
16
  import"../../chunk-e6hk39ev.js";
@@ -11,12 +20,6 @@ import"../../chunk-bd09gygt.js";
11
20
  import"../../chunk-p6z2m3zp.js";
12
21
  import"../../chunk-7jpsybag.js";
13
22
  import"../../chunk-7vg045gd.js";
14
- import {
15
- getDataGridColumnSizeVariables,
16
- DataGridHeaderRows,
17
- DataGridCells2,
18
- DataGridColumns
19
- } from "../../chunk-x7emg3jx.js";
20
23
  import {
21
24
  VirtualTable2
22
25
  } from "../../chunk-bmyw7q4w.js";
@@ -45,8 +48,39 @@ function useDataGridColumnSizes(table, scope) {
45
48
  });
46
49
  }
47
50
 
51
+ // src/components/data-grid/hooks/useDataGridColumnFit.ts
52
+ import { useLayoutEffect as useLayoutEffect2, useState } from "react";
53
+ function useDataGridColumnFit(table, scope, isEnabled, onWidthChange) {
54
+ const [fit, setFit] = useState();
55
+ useLayoutEffect2(() => {
56
+ const element = scope.current;
57
+ if (!isEnabled || !element)
58
+ return;
59
+ const container = element.querySelector('[data-slot="table-container"]');
60
+ if (!container)
61
+ return;
62
+ const update = () => {
63
+ const width = container.clientWidth;
64
+ if (width <= 0)
65
+ return;
66
+ onWidthChange?.(width);
67
+ const hiddenColumnIds = getDataGridColumnFit(table, width);
68
+ setFit((previous) => previous?.width === width && previous.hiddenColumnIds.join("\x00") === hiddenColumnIds.join("\x00") ? previous : { width, hiddenColumnIds });
69
+ };
70
+ update();
71
+ const observer = new ResizeObserver(update);
72
+ observer.observe(container);
73
+ const subscription = table.atoms.columnSizing.subscribe(update);
74
+ return () => {
75
+ observer.disconnect();
76
+ subscription.unsubscribe();
77
+ };
78
+ });
79
+ return isEnabled ? fit : undefined;
80
+ }
81
+
48
82
  // src/components/virtual-data-grid/VirtualDataGrid.tsx
49
- import { jsx } from "react/jsx-runtime";
83
+ import { jsx, jsxs } from "react/jsx-runtime";
50
84
  function renderDefaultRow(row, props) {
51
85
  return /* @__PURE__ */ jsx(TableRow2, {
52
86
  ...props,
@@ -62,31 +96,55 @@ function VirtualDataGrid({
62
96
  maxHeight,
63
97
  estimateRowHeight,
64
98
  overscan,
99
+ fitColumns = false,
100
+ onFitWidthChange,
65
101
  retainedRowIds,
66
102
  renderRow = renderDefaultRow
67
103
  }) {
68
104
  const scope = useRef(null);
69
105
  useDataGridColumnSizes(table, scope);
106
+ const fit = useDataGridColumnFit(table, scope, fitColumns, onFitWidthChange);
107
+ const hiddenLabels = table.getVisibleLeafColumns().filter((column) => fit?.hiddenColumnIds.includes(column.id)).map(getDataGridColumnLabel);
70
108
  return /* @__PURE__ */ jsx("div", {
71
109
  ref: scope,
72
110
  "data-testid": "VirtualDataGrid",
73
111
  className: "min-w-0",
74
- children: /* @__PURE__ */ jsx(VirtualTable2, {
75
- items: table.getPrePaginatedRowModel().rows,
76
- getItemKey: (row) => row.id,
77
- label,
78
- maxHeight,
79
- estimateRowHeight,
80
- overscan,
81
- retainedKeys: retainedRowIds,
82
- columns: /* @__PURE__ */ jsx(DataGridColumns, {
83
- table
84
- }),
85
- header: /* @__PURE__ */ jsx(DataGridHeaderRows, {
86
- table,
87
- isResizable: true
88
- }),
89
- renderRow
112
+ children: /* @__PURE__ */ jsxs(DataGridColumnFitContext, {
113
+ value: fit,
114
+ children: [
115
+ hiddenLabels.length > 0 && /* @__PURE__ */ jsxs("div", {
116
+ className: "mb-2 text-xs text-muted-foreground",
117
+ children: [
118
+ /* @__PURE__ */ jsxs("p", {
119
+ children: [
120
+ "Hidden to fit: ",
121
+ hiddenLabels.join(", "),
122
+ "."
123
+ ]
124
+ }),
125
+ /* @__PURE__ */ jsx("p", {
126
+ children: "Widen the table or choose fewer columns. Move a column earlier to prioritize it."
127
+ })
128
+ ]
129
+ }),
130
+ /* @__PURE__ */ jsx(VirtualTable2, {
131
+ items: table.getPrePaginatedRowModel().rows,
132
+ getItemKey: (row) => row.id,
133
+ label,
134
+ maxHeight,
135
+ estimateRowHeight,
136
+ overscan,
137
+ retainedKeys: retainedRowIds,
138
+ columns: /* @__PURE__ */ jsx(DataGridColumns, {
139
+ table
140
+ }),
141
+ header: /* @__PURE__ */ jsx(DataGridHeaderRows, {
142
+ table,
143
+ isResizable: true
144
+ }),
145
+ renderRow
146
+ })
147
+ ]
90
148
  })
91
149
  });
92
150
  }
package/dist/index.js CHANGED
@@ -171,7 +171,7 @@ import {
171
171
  StatusTracker2,
172
172
  DashboardLayout2,
173
173
  BillingUsage2
174
- } from "./chunk-aye7r5mq.js";
174
+ } from "./chunk-zwnba1pb.js";
175
175
  import {
176
176
  Tabs2,
177
177
  tabsListVariants2,
package/dist/prototype.js CHANGED
@@ -2051,7 +2051,7 @@ var program = new Command;
2051
2051
  // package.json
2052
2052
  var package_default = {
2053
2053
  name: "@alexgorbatchev/shadcn",
2054
- version: "0.0.55",
2054
+ version: "0.0.57",
2055
2055
  description: "Sharp, monospace React components with compact and comfortable densities.",
2056
2056
  license: "MIT",
2057
2057
  repository: {
@@ -3149,8 +3149,10 @@ var package_default = {
3149
3149
  "@storybook/addon-vitest": "^10.6.1",
3150
3150
  "@storybook/react-vite": "^10.6.1",
3151
3151
  "@tailwindcss/vite": "^4.3.3",
3152
+ "@tanstack/react-store": "0.11.2",
3152
3153
  "@tanstack/react-table": "9.2.8",
3153
3154
  "@tanstack/react-virtual": "3.14.13",
3155
+ "@tanstack/table-core": "9.2.8",
3154
3156
  "@testing-library/react": "^16.3.3",
3155
3157
  "@tiptap/core": "3.32.0",
3156
3158
  "@tiptap/pm": "3.32.0",
@@ -75,3 +75,19 @@ Storybook CSF excerpts. Default args and render apply unless overridden. Local h
75
75
  render: () => <VirtualDataGridExample files={fixture_gridFiles} width={360}/>
76
76
  }
77
77
  ```
78
+
79
+ ### FittedColumns
80
+
81
+ ```tsx
82
+ {
83
+ render: () => <VirtualDataGridExample files={fixture_gridFiles} width={420} fitColumns/>
84
+ }
85
+ ```
86
+
87
+ ### FittedColumnGroups
88
+
89
+ ```tsx
90
+ {
91
+ render: () => <VirtualDataGridExample files={fixture_gridFiles} width={420} fitColumns grouped/>
92
+ }
93
+ ```
@@ -8,7 +8,7 @@ Source: src/components/stories/forms/InlineEdit.stories.tsx
8
8
  ## Public imports
9
9
 
10
10
  ```tsx
11
- import { InlineEdit } from "@alexgorbatchev/shadcn";
11
+ import { InlineEdit, Button } from "@alexgorbatchev/shadcn";
12
12
  ```
13
13
 
14
14
  ## Supporting imports
@@ -98,3 +98,25 @@ Storybook CSF excerpts. Default args and render apply unless overridden. Local h
98
98
  }
99
99
  }
100
100
  ```
101
+
102
+ ### StableRowLayout
103
+
104
+ ```tsx
105
+ {
106
+ render: function Example(args): ReactElement {
107
+ const [isEditing, setEditing] = useState(false);
108
+ const [title, setTitle] = useState("Playlist title");
109
+ return (<div className="w-64 text-sm leading-5">
110
+ <div data-testid="editable-row" className="grid">
111
+ {isEditing ? (<InlineEdit {...args} value={title} onValueChange={setTitle} onCancel={() => setEditing(false)} onCommit={async (value) => {
112
+ await args.onCommit(value);
113
+ setEditing(false);
114
+ }}/>) : (<Button size="inline" variant="link" align="start" onClick={() => setEditing(true)}>
115
+ <span className="leading-5">{title}</span>
116
+ </Button>)}
117
+ </div>
118
+ <p>Next row</p>
119
+ </div>);
120
+ }
121
+ }
122
+ ```
@@ -11,6 +11,12 @@ Source: src/components/stories/forms/RichTextEditor.stories.tsx
11
11
  import { RichTextEditor, useEditor, StarterKit } from "@alexgorbatchev/shadcn/editor";
12
12
  ```
13
13
 
14
+ ## Supporting imports
15
+
16
+ ```tsx
17
+ import { isMacOS, isiOS } from "@tiptap/core";
18
+ ```
19
+
14
20
  ## Default component
15
21
 
16
22
  ```tsx
package/llms/setup.md CHANGED
@@ -429,6 +429,8 @@ import {
429
429
 
430
430
  `InlineEdit` mounts an edit session with a controlled `value`, `onValueChange`, `onCommit`, and `onCancel`. Mount it in place of the displayed label. Its input focuses and selects its text by default; `shouldSelectOnMount={false}` preserves the initial caret. Enter or blur commits once, Escape cancels without committing, and asynchronous saves disable the input until completion. Set `hasChanges={false}` to cancel an unchanged edit before calling `onCommit`; the caller owns normalization and comparison. It defaults to true, so creation sessions can commit their initial value. Rejected saves display a generic error and allow retry; `renderError` can map the unknown failure to application-authored guidance.
431
431
 
432
+ The editor inherits the containing label's font size and line height and occupies that single line. Keep the same text context around display and edit states. Validation appears in an anchored tooltip with an announced alert, so errors do not move neighboring content.
433
+
432
434
  `VirtualTable` renders a bounded, dynamically measured set of native table rows using TanStack Virtual. Supply `items`, stable `getItemKey`, `header`, `label`, and `renderRow`; spread the supplied measurement props onto each native row. Optional `columns` accepts a `TableColumnGroup`; responsive columns keep their widths aligned with cells that share their `visibleFrom`. `maxHeight` defaults to `65vh`, `estimateRowHeight` to 80 pixels, and `overscan` to five rows. `retainedKeys` keeps editing rows mounted; the focused row is retained automatically. Filtering and selection belong to the consumer.
433
435
 
434
436
  `VirtualDataGrid` renders a TanStack Table instance built with `dataGridFeatures` through `VirtualTable`: every filtered and sorted row, without pagination, in a fixed-layout table. Column `meta` sets `label`, `textAlign`, `truncate`, `visibleFrom`, `fill`, and `sortColumnIds`; a `fill` column takes the width the sized columns leave, and `sortColumnIds` names other columns the header sorts with its own buttons so its `aria-sort` reports them. Other resizable columns get a window-splitter handle that resizes by dragging, by the arrow keys, Home, and End, and resets on double-click. Set `columnResizeMode: "onChange"` so columns follow the pointer while dragging; TanStack's default `"onEnd"` resizes only on release. Widths change through CSS variables, so a `useTable` selector can omit `columnSizing` and `columnResizing` to keep resizing from rendering rows. `renderRow` receives each row and its measurement props; spread the props onto a `TableRow` and render `DataGridCells` inside it to wrap rows in menus or drag sources. `retainedRowIds` keeps rows mounted while they hold editors. `DataGridColumnOptions` shows, hides, and reorders the columns that allow hiding; its reset restores the table's initial visibility, order, and sizes.
@@ -437,6 +439,10 @@ Column `meta.filter` offers a column in `DataGridFilters`, hidden columns includ
437
439
 
438
440
  Import `DataGridSelectHeader` and `DataGridSelectCell` from `@alexgorbatchev/shadcn/data-grid`. Pass the current `useTable` result as `table` to both controls, and pass `row` and an accessible `label` to each cell. The header selects selectable filtered rows across all pages and preserves selections hidden by filters. It shows a mixed state when some of those rows are selected and disables itself when none are selectable. Its default label is "Select all filtered rows"; provide `label` to name the scope in your application. Cells follow native row selection rules, including disabled rows and partially selected parents. The controls observe selection changes without requiring the parent table selector to include selection state.
439
441
 
442
+ For grids that must fit their container, set `VirtualDataGrid fitColumns`. Use exactly one `meta.fill` column with an explicit `minSize` and a preferred `size`; required columns' minima plus that minimum must fit the smallest supported container. The grid reserves the fill column's preferred size, bounded by its native sizing limits, and temporarily hides optional columns that do not fit, prioritizing their chosen order. When only required columns remain, the fill column can shrink toward its minimum. The grid lists hidden columns and explains how to reveal them. This mode replaces `meta.visibleFrom` breakpoints and offers resize handles on leaf headers only.
443
+
444
+ Keep preferred widths separate from the table's rendered sizing atom. In `onFitWidthChange`, store the reported width in a ref and write `fitDataGridColumnSizing(table, preferredWidths, width)` into the sizing atom. The helper temporarily shrinks required resizable columns toward their minima when needed and restores their preferred widths when space returns. In `onColumnSizingChange`, obtain new preferences with `constrainDataGridColumnSizing(table, updater, widthRef.current, preferredWidths)`, then derive the sizing atom with `fitDataGridColumnSizing`. Persist only the preferred widths, after the resize settles. Native pointer and keyboard resizing starts at the displayed width, stays within the available space, and leaves other columns' preferences unchanged. Widening a required column can hide optional columns again; widening an optional column preserves its fitted neighbours and the fill column's preferred space. Container changes never overwrite user visibility, order, or width preferences. The `VirtualDataGridExample` fixture demonstrates this controlled integration.
445
+
440
446
  `ActionsMenu` shares one `items` definition between a dropdown action and an optional context menu. Supply an accessible `label`; the default trigger is a ghost icon button. `size="sm"` displays the label, and `variant`, `disabled`, and `trigger` customize the action. Supplying `contextTarget` composes the native context trigger with that element and adds its popup. Compose items with `MenuItem`, `MenuLinkItem`, checkbox/radio parts, groups, labels, separators, shortcuts, and submenu parts; dropdown and context menus share these implementations. `MenuLinkItem` retains its native anchor and menu keyboard semantics.
441
447
 
442
448
  For nested layouts, compose `ActionsMenuTrigger`, `ActionsMenuContextTarget`, and `ActionsMenuContextContent` explicitly inside `ActionsMenu`. The dropdown trigger owns its local native dropdown root and popup; the context target and content share an independent context root. A tree row can use `TreeItem render={<ActionsMenuContextTarget />}` and put the trigger in its action slot. For a table row, render the context target as the actual `tr`, then place the dropdown trigger and context content inside a `td`: native popup portals insert focus guards at their declaration, which must remain valid table children.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/shadcn",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "description": "Sharp, monospace React components with compact and comfortable densities.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1098,8 +1098,10 @@
1098
1098
  "@storybook/addon-vitest": "^10.6.1",
1099
1099
  "@storybook/react-vite": "^10.6.1",
1100
1100
  "@tailwindcss/vite": "^4.3.3",
1101
+ "@tanstack/react-store": "0.11.2",
1101
1102
  "@tanstack/react-table": "9.2.8",
1102
1103
  "@tanstack/react-virtual": "3.14.13",
1104
+ "@tanstack/table-core": "9.2.8",
1103
1105
  "@testing-library/react": "^16.3.3",
1104
1106
  "@tiptap/core": "3.32.0",
1105
1107
  "@tiptap/pm": "3.32.0",