@alexgorbatchev/shadcn 0.0.54 → 0.0.56

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 (29) hide show
  1. package/README.md +12 -1
  2. package/THIRD_PARTY_NOTICES.md +2 -0
  3. package/dist/{chunk-x7emg3jx.js → chunk-74xynyzg.js} +73 -18
  4. package/dist/{chunk-d5pabsj2.js → chunk-zwnba1pb.js} +69 -46
  5. package/dist/components/InlineEdit.d.ts +1 -1
  6. package/dist/components/ThemeProvider.d.ts +2 -1
  7. package/dist/components/data-grid/DataGridColumnFitContext.d.ts +6 -0
  8. package/dist/components/data-grid/DataGridColumnResizer.d.ts +1 -1
  9. package/dist/components/data-grid/constrainDataGridColumnSizing.d.ts +9 -0
  10. package/dist/components/data-grid/fitDataGridColumnSizing.d.ts +8 -0
  11. package/dist/components/data-grid/getDataGridColumnFit.d.ts +4 -0
  12. package/dist/components/data-grid/getDataGridColumnFitMaximum.d.ts +4 -0
  13. package/dist/components/data-grid/hooks/useDataGridColumnFit.d.ts +6 -0
  14. package/dist/components/data-grid/index.d.ts +2 -0
  15. package/dist/components/data-grid/index.js +52 -10
  16. package/dist/components/json-render/index.js +1 -1
  17. package/dist/components/virtual-data-grid/VirtualDataGrid.d.ts +11 -1
  18. package/dist/components/virtual-data-grid/index.js +81 -23
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/prototype.js +3 -1
  22. package/dist/theme/types.d.ts +2 -0
  23. package/dist/theme.css +99 -6
  24. package/llms/components/data-display/VirtualDataGrid.md +16 -0
  25. package/llms/components/forms/InlineEdit.md +23 -1
  26. package/llms/components/forms/RichTextEditor.md +6 -0
  27. package/llms/components/providers/ThemeProvider.md +8 -0
  28. package/llms/setup.md +12 -1
  29. package/package.json +3 -1
package/README.md CHANGED
@@ -198,10 +198,15 @@ An independent production Vite consumer installed from the archive verifies RTL
198
198
  | `ThemeProvider.defaultTheme` | `"light" \| "dark" \| "system"` | `"system"` | Initial preference when storage has no saved value. |
199
199
  | `ThemeProvider.storageKey` | `string` | `"shadcn-alexgorbatchev-theme"` | Local storage key for the preference. |
200
200
  | `ThemeProvider.forcedTheme` | `"light" \| "dark"` | unset | Overrides the displayed palette without changing the saved preference. |
201
+ | `ThemeProvider.preset` | `"devhost" \| "shadcn"` | `"devhost"` | Document-wide design preset, independent of color mode and density. |
201
202
  | `DensityProvider.density` | `"compact" \| "comfortable"` | `"comfortable"` | Density for descendants and their portals. |
202
203
  | Component `density` | `"compact" \| "comfortable"` | inherited | Local override on styled components that expose the prop. |
203
204
 
204
- `useTheme()` returns `theme`, `resolvedTheme`, and `setTheme`. Call `setTheme("light")`, `setTheme("dark")`, or `setTheme("system")` from a component inside the provider.
205
+ `useTheme()` returns `theme`, `resolvedTheme`, `setTheme`, and `preset`. Call `setTheme("light")`, `setTheme("dark")`, or `setTheme("system")` from a component inside the provider.
206
+
207
+ Use `<ThemeProvider preset="shadcn" defaultTheme="system">` for the [default shadcn Neutral palette](https://ui.shadcn.com/docs/theming), system sans-serif text, and its rounded corner scale. The default `devhost` preset uses the package's monospace design and sharp corners. Both presets support light, dark, and system modes, including portaled content. Code retains the monospace font. Density, component composition, and package-specific status colors remain independent of the preset.
208
+
209
+ The preset is controlled by the prop; changing it updates the document without changing the saved color-mode preference. Persist that prop in your application if needed. Storybook exposes the same choice in its **Design** toolbar. For a page styled before React mounts, set `data-theme-preset="shadcn"` and `data-theme="light"` or `"dark"` on `<html>` to match the provider. The existing `theme.css` import and `@source` setup cover both presets.
205
210
 
206
211
  Override tokens after the theme imports. This example uses the existing success palette as the primary accent:
207
212
 
@@ -424,6 +429,8 @@ import {
424
429
 
425
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.
426
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
+
427
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.
428
435
 
429
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.
@@ -432,6 +439,10 @@ Column `meta.filter` offers a column in `DataGridFilters`, hidden columns includ
432
439
 
433
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.
434
441
 
442
+ For grids that must fit their container, set `VirtualDataGrid fitColumns`. Use exactly one `meta.fill` column with an explicit `minSize`; required columns' minima plus that minimum must fit the smallest supported container. The grid reserves that space and temporarily hides optional columns that do not fit, prioritizing their chosen order. It lists those 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. Container changes never overwrite user visibility, order, or width preferences. The `VirtualDataGridExample` fixture demonstrates this controlled integration.
445
+
435
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.
436
447
 
437
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.
@@ -14,6 +14,8 @@ Questionnaire adapts the official registry's fifteen styled parts around `@shadc
14
14
 
15
15
  ## shadcn/ui
16
16
 
17
+ The `shadcn` design preset in `src/theme.css` adapts the default Neutral light/dark palette and radius scale from https://ui.shadcn.com/docs/theming, retrieved on 2026-10-31. It preserves the package's independent density and status tokens and uses document data attributes for selection. The upstream MIT notice below covers these tokens.
18
+
17
19
  Questionnaire's NativeChoiceKeyboard and ItemArrowNavigation Storybook interactions adapt the native choice-navigation and contextual horizontal-arrow browser tests from `packages/react/src/questionnaire/questionnaire.browser.test.tsx` at upstream commit `3d98900e9b73c45f035f8109e9d41ba6a51685e7` (https://github.com/shadcn-ui/ui). Local changes use this package's styled compositions, Storybook's play context and accessible queries, and assert unanswered-item guards, focus, selection and submitted answers. The upstream MIT notice below covers these adaptations.
18
20
 
19
21
  Sidebar adapts the official provider and styled parts, preserving custom render/ref contracts, keyboard toggling within each provider, consumer-owned persistence, and native mobile Sheet behavior. Local changes support density, cancelled toggle events, observation without controlled state, inert offcanvas panels, configured mobile widths, and centered icons with accessible collapsed labels. Separator uses Base UI's current orientation attributes, and Tooltip preserves native state callbacks.
@@ -46,10 +46,51 @@ 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/getDataGridColumnFit.ts
60
+ function getDataGridColumnFit(table, width) {
61
+ const columns = table.getVisibleLeafColumns();
62
+ let remaining = width;
63
+ for (const column of columns) {
64
+ if (column.columnDef.meta?.fill)
65
+ remaining -= column.columnDef.minSize ?? 20;
66
+ else if (!column.getCanHide())
67
+ remaining -= column.getSize();
68
+ }
69
+ const hidden = [];
70
+ for (const column of columns) {
71
+ if (column.columnDef.meta?.fill || !column.getCanHide())
72
+ continue;
73
+ if (column.getSize() <= remaining)
74
+ remaining -= column.getSize();
75
+ else
76
+ hidden.push(column.id);
77
+ }
78
+ return hidden;
79
+ }
80
+
81
+ // src/components/data-grid/getDataGridColumnFitMaximum.ts
82
+ function getDataGridColumnFitMaximum(table, columnId, width) {
83
+ const hidden = getDataGridColumnFit(table, width);
84
+ const column = table.getColumn(columnId);
85
+ const occupied = table.getVisibleLeafColumns().reduce((sum, other) => {
86
+ if (other.id === columnId || hidden.includes(other.id))
87
+ return sum;
88
+ return sum + (other.columnDef.meta?.fill ? other.columnDef.minSize ?? 20 : other.getSize());
89
+ }, 0);
90
+ return Math.max(column?.columnDef.minSize ?? 20, Math.min(column?.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER, Math.floor(width - occupied)));
91
+ }
92
+
93
+ // src/components/data-grid/DataGridColumnResizer.tsx
53
94
  import { jsx } from "react/jsx-runtime";
54
95
  var KEYBOARD_STEP = 16;
55
96
  function DataGridColumnResizer({
@@ -58,14 +99,16 @@ function DataGridColumnResizer({
58
99
  }) {
59
100
  const column = header.column;
60
101
  const table = column.table;
102
+ const fit = useContext(DataGridColumnFitContext);
61
103
  const minSize = column.columnDef.minSize ?? 0;
62
- const maxSize = column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
104
+ const getMaxSize = () => fit ? getDataGridColumnFitMaximum(table, column.id, fit.width) : column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
63
105
  const resize = header.getResizeHandler();
64
- const setSize = (size) => table.setColumnSizing((sizes) => ({ ...sizes, [column.id]: Math.min(maxSize, Math.max(minSize, size)) }));
106
+ const setSize = (size) => table.setColumnSizing((sizes) => ({ ...sizes, [column.id]: Math.min(getMaxSize(), Math.max(minSize, size)) }));
65
107
  const onKeyDown = (event) => {
66
108
  const growKey = table.options.columnResizeDirection === "rtl" ? "ArrowLeft" : "ArrowRight";
67
109
  const shrinkKey = growKey === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
68
110
  const size = column.getSize();
111
+ const maxSize = getMaxSize();
69
112
  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
113
  if (next === undefined)
71
114
  return;
@@ -74,14 +117,14 @@ function DataGridColumnResizer({
74
117
  };
75
118
  return /* @__PURE__ */ jsx(Subscribe, {
76
119
  source: table.store,
77
- selector: (state) => `${state.columnSizing[column.id]}:${state.columnResizing.isResizingColumn === column.id}`,
120
+ selector: (state) => `${state.columnSizing[column.id]}:${getMaxSize()}:${state.columnResizing.isResizingColumn === column.id}`,
78
121
  children: () => /* @__PURE__ */ jsx("div", {
79
122
  role: "separator",
80
123
  "aria-orientation": "vertical",
81
124
  "aria-label": `Resize ${label}`,
82
125
  "aria-valuenow": column.getSize(),
83
126
  "aria-valuemin": minSize,
84
- "aria-valuemax": maxSize < Number.MAX_SAFE_INTEGER ? maxSize : undefined,
127
+ "aria-valuemax": getMaxSize() < Number.MAX_SAFE_INTEGER ? getMaxSize() : undefined,
85
128
  tabIndex: 0,
86
129
  "data-slot": "data-grid-column-resizer",
87
130
  "data-resizing": column.getIsResizing() || undefined,
@@ -139,18 +182,23 @@ function getAriaSort(column) {
139
182
  const sorted = sortable.map((candidate) => candidate?.getIsSorted()).find(Boolean);
140
183
  return sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none";
141
184
  }
142
- function DataGridHeaderCell({ header, isResizable }) {
185
+ function DataGridHeaderCell({
186
+ header,
187
+ isResizable,
188
+ colSpan
189
+ }) {
143
190
  const contentId = useId();
191
+ const fit = useContext2(DataGridColumnFitContext);
144
192
  const meta = header.column.columnDef.meta;
145
- const hasResizer = isResizable && !meta?.fill && header.column.getCanResize();
193
+ const hasResizer = isResizable && !meta?.fill && header.column.getCanResize() && (!fit || header.subHeaders.length === 0);
146
194
  return /* @__PURE__ */ jsx2(TableHead2, {
147
195
  scope: "col",
148
- colSpan: header.colSpan,
196
+ colSpan,
149
197
  "aria-sort": getAriaSort(header.column),
150
198
  "aria-labelledby": hasResizer ? contentId : undefined,
151
199
  textAlign: meta?.textAlign,
152
200
  truncate: meta?.truncate,
153
- visibleFrom: meta?.visibleFrom,
201
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
154
202
  className: hasResizer ? "relative" : undefined,
155
203
  children: hasResizer ? /* @__PURE__ */ jsxs(Fragment, {
156
204
  children: [
@@ -174,23 +222,29 @@ function DataGridHeaderRows({
174
222
  table,
175
223
  isResizable = false
176
224
  }) {
225
+ const fit = useContext2(DataGridColumnFitContext);
177
226
  return /* @__PURE__ */ jsx2(Fragment, {
178
227
  children: table.getHeaderGroups().map((group) => /* @__PURE__ */ jsx2(TableRow2, {
179
- children: group.headers.map((header) => /* @__PURE__ */ jsx2(DataGridHeaderCell, {
180
- header,
181
- isResizable
182
- }, header.id))
228
+ children: group.headers.map((header) => {
229
+ const colSpan = fit ? header.getLeafHeaders().filter((leaf) => leaf.subHeaders.length === 0 && !fit.hiddenColumnIds.includes(leaf.column.id)).length : header.colSpan;
230
+ return colSpan > 0 ? /* @__PURE__ */ jsx2(DataGridHeaderCell, {
231
+ header,
232
+ isResizable,
233
+ colSpan
234
+ }, header.id) : null;
235
+ })
183
236
  }, group.id))
184
237
  });
185
238
  }
186
239
  function DataGridCells2({ row }) {
240
+ const fit = useContext2(DataGridColumnFitContext);
187
241
  return /* @__PURE__ */ jsx2(Fragment, {
188
- children: row.getVisibleCells().map((cell) => {
242
+ children: row.getVisibleCells().filter((cell) => !fit?.hiddenColumnIds.includes(cell.column.id)).map((cell) => {
189
243
  const meta = cell.column.columnDef.meta;
190
244
  return /* @__PURE__ */ jsx2(TableCell2, {
191
245
  textAlign: meta?.textAlign,
192
246
  truncate: meta?.truncate,
193
- visibleFrom: meta?.visibleFrom,
247
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
194
248
  children: /* @__PURE__ */ jsx2(FlexRender, {
195
249
  cell
196
250
  })
@@ -199,13 +253,14 @@ function DataGridCells2({ row }) {
199
253
  });
200
254
  }
201
255
  function DataGridColumns({ table }) {
256
+ const fit = useContext2(DataGridColumnFitContext);
202
257
  const variables = getDataGridColumnSizeVariables(table);
203
258
  return /* @__PURE__ */ jsx2(TableColumnGroup2, {
204
- children: table.getVisibleLeafColumns().map((column) => {
259
+ children: table.getVisibleLeafColumns().filter((column) => !fit?.hiddenColumnIds.includes(column.id)).map((column) => {
205
260
  const meta = column.columnDef.meta;
206
261
  const variable = variables.get(column.id);
207
262
  return /* @__PURE__ */ jsx2(TableColumn2, {
208
- visibleFrom: meta?.visibleFrom,
263
+ visibleFrom: fit ? undefined : meta?.visibleFrom,
209
264
  width: meta?.fill ? undefined : `var(${variable}, ${column.getSize()}px)`
210
265
  }, column.id);
211
266
  })
@@ -364,4 +419,4 @@ function DataGridPagination2({ table }) {
364
419
  });
365
420
  }
366
421
 
367
- export { getDataGridColumnSizeVariables, dataGridFeatures2, getDataGridColumnLabel, DataGridHeaderRows, DataGridCells2, DataGridColumns, DataGrid2, DataGridColumnHeader2, DataGridFilter2, DataGridPagination2 };
422
+ export { DataGridColumnFitContext, getDataGridColumnFit, getDataGridColumnFitMaximum, getDataGridColumnSizeVariables, dataGridFeatures2, getDataGridColumnLabel, DataGridHeaderRows, DataGridCells2, DataGridColumns, DataGrid2, DataGridColumnHeader2, DataGridFilter2, DataGridPagination2 };
@@ -1111,13 +1111,15 @@ function StoredThemeProvider({
1111
1111
  children,
1112
1112
  defaultTheme = "system",
1113
1113
  storageKey = "shadcn-alexgorbatchev-theme",
1114
- forcedTheme
1114
+ forcedTheme,
1115
+ preset = "devhost"
1115
1116
  }) {
1116
1117
  const { isDarkMode, ternaryDarkMode, setTernaryDarkMode } = useTernaryDarkMode({
1117
1118
  defaultValue: defaultTheme,
1118
1119
  localStorageKey: storageKey
1119
1120
  });
1120
1121
  return /* @__PURE__ */ jsx12(ThemePalette, {
1122
+ preset,
1121
1123
  theme: forcedTheme ?? ternaryDarkMode,
1122
1124
  resolvedTheme: forcedTheme ?? (isDarkMode ? "dark" : "light"),
1123
1125
  setTheme: setTernaryDarkMode,
@@ -1128,13 +1130,15 @@ function ThemePalette({
1128
1130
  children,
1129
1131
  theme,
1130
1132
  resolvedTheme,
1131
- setTheme
1133
+ setTheme,
1134
+ preset
1132
1135
  }) {
1133
1136
  useIsomorphicLayoutEffect(() => {
1134
1137
  document.documentElement.dataset.theme = resolvedTheme;
1138
+ document.documentElement.dataset.themePreset = preset;
1135
1139
  document.documentElement.style.colorScheme = resolvedTheme;
1136
- }, [resolvedTheme]);
1137
- const value = useMemo(() => ({ theme, resolvedTheme, setTheme }), [theme, resolvedTheme, setTheme]);
1140
+ }, [resolvedTheme, preset]);
1141
+ const value = useMemo(() => ({ theme, resolvedTheme, setTheme, preset }), [theme, resolvedTheme, setTheme, preset]);
1138
1142
  return /* @__PURE__ */ jsxs8(ThemeContext.Provider, {
1139
1143
  value,
1140
1144
  children: [
@@ -1147,7 +1151,8 @@ function ThemePalette({
1147
1151
  function ControlledThemeProvider({
1148
1152
  children,
1149
1153
  controlledTheme,
1150
- forcedTheme
1154
+ forcedTheme,
1155
+ preset = "devhost"
1151
1156
  }) {
1152
1157
  const isDark = useMediaQuery("(prefers-color-scheme: dark)");
1153
1158
  const { theme, setTheme } = controlledTheme;
@@ -1156,6 +1161,7 @@ function ControlledThemeProvider({
1156
1161
  theme: forcedTheme ?? theme,
1157
1162
  resolvedTheme,
1158
1163
  setTheme,
1164
+ preset,
1159
1165
  children
1160
1166
  });
1161
1167
  }
@@ -2536,7 +2542,7 @@ function AttachmentGroup2({ density, className, ...props }) {
2536
2542
  });
2537
2543
  }
2538
2544
  // src/components/InlineEdit.tsx
2539
- 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";
2540
2546
  import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
2541
2547
  function InlineEdit2({
2542
2548
  analytics,
@@ -2552,6 +2558,8 @@ function InlineEdit2({
2552
2558
  onCancel
2553
2559
  }) {
2554
2560
  const track = useAnalytics2("InlineEdit", analytics);
2561
+ const triggerId = useId2();
2562
+ const errorId = `${triggerId}-error`;
2555
2563
  const input = useRef2(null);
2556
2564
  const isFinished = useRef2(false);
2557
2565
  const shouldCancel = useRef2(false);
@@ -2582,48 +2590,63 @@ function InlineEdit2({
2582
2590
  setSaving(false);
2583
2591
  }
2584
2592
  };
2585
- return /* @__PURE__ */ jsxs15("form", {
2586
- className: "flex min-w-0 flex-col gap-1",
2593
+ return /* @__PURE__ */ jsx26("form", {
2594
+ className: "min-w-0",
2587
2595
  onSubmit: (event) => {
2588
2596
  event.preventDefault();
2589
2597
  save();
2590
2598
  },
2591
- children: [
2592
- /* @__PURE__ */ jsx26(Input2, {
2593
- analytics: false,
2594
- ref: input,
2595
- density,
2596
- "aria-label": label,
2597
- "aria-invalid": Boolean(error),
2598
- "aria-description": "Enter saves. Escape cancels.",
2599
- value,
2600
- disabled: isDisabled || isSaving,
2601
- onChange: (event) => onValueChange(event.target.value),
2602
- onBlur: () => {
2603
- if (shouldCancel.current && !isFinished.current) {
2604
- isFinished.current = true;
2605
- onCancel();
2606
- } else
2607
- save();
2608
- },
2609
- onKeyDown: (event) => {
2610
- if (event.key === "Escape") {
2611
- event.preventDefault();
2612
- event.stopPropagation();
2613
- shouldCancel.current = true;
2614
- event.currentTarget.blur();
2615
- } else if (event.key === "Enter") {
2616
- event.preventDefault();
2617
- event.currentTarget.blur();
2618
- }
2619
- }
2620
- }),
2621
- error && /* @__PURE__ */ jsx26("p", {
2622
- role: "alert",
2623
- className: "text-xs text-destructive",
2624
- children: error
2625
- })
2626
- ]
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
+ })
2627
2650
  });
2628
2651
  }
2629
2652
  // src/components/StarRating.tsx
@@ -3799,7 +3822,7 @@ function StatCard2({
3799
3822
  });
3800
3823
  }
3801
3824
  // src/components/AuthForm.tsx
3802
- import { useId as useId2, useState as useState5 } from "react";
3825
+ import { useId as useId3, useState as useState5 } from "react";
3803
3826
  import { jsx as jsx40, jsxs as jsxs25 } from "react/jsx-runtime";
3804
3827
  function AuthForm2({
3805
3828
  mode = "sign-in",
@@ -3809,7 +3832,7 @@ function AuthForm2({
3809
3832
  disabled = false,
3810
3833
  density
3811
3834
  }) {
3812
- const id = useId2();
3835
+ const id = useId3();
3813
3836
  const [pending, setPending] = useState5(false);
3814
3837
  const [error, setError] = useState5("");
3815
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 {};
@@ -1,7 +1,8 @@
1
1
  import { type ReactElement, type ReactNode } from "react";
2
- import type { IThemeContextValue, ResolvedTheme, ThemeMode } from "../theme/types.js";
2
+ import type { IThemeContextValue, ResolvedTheme, ThemeMode, ThemePreset } from "../theme/types.js";
3
3
  export interface IThemeProviderProps {
4
4
  children: ReactNode;
5
+ preset?: ThemePreset;
5
6
  defaultTheme?: ThemeMode;
6
7
  storageKey?: string;
7
8
  forcedTheme?: ResolvedTheme;
@@ -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,9 @@
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 without displacing another fitted column or collapsing a fill column. Use in the table's
5
+ * `onColumnSizingChange`, passing the width reported by `VirtualDataGrid.onFitWidthChange`, then store the result in
6
+ * preferred sizes. Then pass those preferences through `fitDataGridColumnSizing` into the table's sizing atom.
7
+ * Container changes never call this helper or rewrite preferred sizes.
8
+ */
9
+ 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 minimum 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
+ /** The resize ceiling that keeps the current fitted columns and the fill columns' minimum space. */
4
+ export declare function getDataGridColumnFitMaximum<T extends RowData>(table: Table<DataGridFeatures, T>, columnId: string, width: number): 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-74xynyzg.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-d5pabsj2.js";
34
+ } from "../../chunk-zwnba1pb.js";
35
35
  import {
36
36
  MenuPopup,
37
37
  MenuItem2,
@@ -8,6 +8,16 @@ 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 `minSize` and hide optional columns that do not fit, in their chosen order. Replaces
13
+ * `meta.visibleFrom` breakpoints; saved visibility and widths are unchanged. Use exactly one fill column, and
14
+ * ensure required columns' minima plus its minimum fit the smallest supported container. Only leaf headers resize.
15
+ * Pair with `fitDataGridColumnSizing` and `constrainDataGridColumnSizing` to preserve preferred widths separately
16
+ * from rendered widths and constrain native pointer resizing.
17
+ */
18
+ fitColumns?: boolean;
19
+ /** Reports usable width; derive the sizing atom with `fitDataGridColumnSizing`, keeping preferences unchanged. */
20
+ onFitWidthChange?: (width: number) => void;
11
21
  /** Row IDs that stay rendered while scrolled out of view, such as rows with an open editor. */
12
22
  retainedRowIds?: readonly string[];
13
23
  /**
@@ -22,5 +32,5 @@ interface IVirtualDataGridProps<T extends RowData, TSelected> {
22
32
  * Column widths change through CSS variables, so a `useTable` selector can leave out sizing state and resizing never
23
33
  * renders rows again.
24
34
  */
25
- export declare function VirtualDataGrid<T extends RowData, TSelected>({ table, label, maxHeight, estimateRowHeight, overscan, retainedRowIds, renderRow, }: IVirtualDataGridProps<T, TSelected>): ReactElement;
35
+ export declare function VirtualDataGrid<T extends RowData, TSelected>({ table, label, maxHeight, estimateRowHeight, overscan, fitColumns, onFitWidthChange, retainedRowIds, renderRow, }: IVirtualDataGridProps<T, TSelected>): ReactElement;
26
36
  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-74xynyzg.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.d.ts CHANGED
@@ -47,7 +47,7 @@ export { ScrollGuard } from "./components/ScrollGuard.js";
47
47
  export type { IScrollGuardProps } from "./components/ScrollGuard.js";
48
48
  export type { IThemeProviderProps } from "./components/ThemeProvider.js";
49
49
  export { useTheme } from "./hooks/useTheme.js";
50
- export type { ThemeMode, ResolvedTheme, IThemeContextValue } from "./theme/types.js";
50
+ export type { ThemeMode, ResolvedTheme, ThemePreset, IThemeContextValue } from "./theme/types.js";
51
51
  export { useComboboxAnchor } from "./hooks/useComboboxAnchor.js";
52
52
  export { cn } from "./utils/cn.js";
53
53
  export { Toggle, toggleVariants } from "./components/Toggle.js";
package/dist/index.js CHANGED
@@ -171,7 +171,7 @@ import {
171
171
  StatusTracker2,
172
172
  DashboardLayout2,
173
173
  BillingUsage2
174
- } from "./chunk-d5pabsj2.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.54",
2054
+ version: "0.0.56",
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",
@@ -1,7 +1,9 @@
1
1
  import type { TernaryDarkMode } from "usehooks-ts";
2
2
  export type ThemeMode = TernaryDarkMode;
3
3
  export type ResolvedTheme = "light" | "dark";
4
+ export type ThemePreset = "devhost" | "shadcn";
4
5
  export interface IThemeContextValue {
6
+ preset: ThemePreset;
5
7
  theme: ThemeMode;
6
8
  resolvedTheme: ResolvedTheme;
7
9
  setTheme: (theme: ThemeMode) => void;
package/dist/theme.css CHANGED
@@ -46,16 +46,16 @@
46
46
  --color-sidebar-border: var(--sidebar-border, var(--border));
47
47
  --color-sidebar-ring: var(--sidebar-ring, var(--ring));
48
48
  --font-sans: var(--ag-font);
49
- --font-mono: var(--ag-font);
49
+ --font-mono: var(--ag-font-mono);
50
50
  --text-xs: var(--ag-text-xs);
51
51
  --text-sm: var(--ag-control-text);
52
52
  --text-base: var(--ag-text);
53
53
  --text-lg: var(--ag-heading);
54
- --radius-xs: 2px;
55
- --radius-sm: 2px;
56
- --radius-md: 4px;
57
- --radius-lg: 4px;
58
- --radius-xl: 4px;
54
+ --radius-xs: var(--ag-radius-xs);
55
+ --radius-sm: var(--ag-radius-sm);
56
+ --radius-md: var(--ag-radius-md);
57
+ --radius-lg: var(--radius);
58
+ --radius-xl: var(--ag-radius-xl);
59
59
  --spacing-control: var(--ag-control-h);
60
60
  --spacing-control-xs: calc(var(--ag-control-h) - 4px);
61
61
  --spacing-control-sm: calc(var(--ag-control-h) - 2px);
@@ -74,7 +74,13 @@
74
74
  @layer base {
75
75
  :root {
76
76
  --ag-font: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
77
+ --ag-font-mono:
78
+ "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
77
79
  --radius: 4px;
80
+ --ag-radius-xs: 2px;
81
+ --ag-radius-sm: 2px;
82
+ --ag-radius-md: 4px;
83
+ --ag-radius-xl: 4px;
78
84
  --ag-text-xs: 11px;
79
85
  --ag-control-text: 12px;
80
86
  --ag-text: 14px;
@@ -205,6 +211,93 @@
205
211
  --ag-shadow: 0 6px 22px rgb(0 0 0 / 0.3);
206
212
  }
207
213
 
214
+ [data-theme-preset="shadcn"] {
215
+ --ag-font:
216
+ ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
217
+ --radius: 0.625rem;
218
+ --ag-radius-xs: calc(var(--radius) * 0.4);
219
+ --ag-radius-sm: calc(var(--radius) * 0.6);
220
+ --ag-radius-md: calc(var(--radius) * 0.8);
221
+ --ag-radius-xl: calc(var(--radius) * 1.4);
222
+ --faint-foreground: var(--muted-foreground);
223
+ --destructive-soft: color-mix(in oklab, var(--destructive) 15%, transparent);
224
+ --frame-edge: var(--border);
225
+ --frame-halo: transparent;
226
+ --terminal-background: var(--background);
227
+ --ag-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
228
+ }
229
+
230
+ [data-theme-preset="shadcn"],
231
+ [data-theme-preset="shadcn"][data-theme="dark"] {
232
+ --background: oklch(0.145 0 0);
233
+ --foreground: oklch(0.985 0 0);
234
+ --card: oklch(0.205 0 0);
235
+ --card-foreground: oklch(0.985 0 0);
236
+ --popover: oklch(0.205 0 0);
237
+ --popover-foreground: oklch(0.985 0 0);
238
+ --primary: oklch(0.922 0 0);
239
+ --primary-foreground: oklch(0.205 0 0);
240
+ --secondary: oklch(0.269 0 0);
241
+ --secondary-foreground: oklch(0.985 0 0);
242
+ --muted: oklch(0.269 0 0);
243
+ --muted-foreground: oklch(0.708 0 0);
244
+ --accent: oklch(0.269 0 0);
245
+ --accent-foreground: oklch(0.985 0 0);
246
+ --destructive: oklch(0.704 0.191 22.216);
247
+ --destructive-foreground: oklch(0.145 0 0);
248
+ --border: oklch(1 0 0 / 10%);
249
+ --input: oklch(1 0 0 / 15%);
250
+ --ring: oklch(0.556 0 0);
251
+ --chart-1: oklch(0.488 0.243 264.376);
252
+ --chart-2: oklch(0.696 0.17 162.48);
253
+ --chart-3: oklch(0.769 0.188 70.08);
254
+ --chart-4: oklch(0.627 0.265 303.9);
255
+ --chart-5: oklch(0.645 0.246 16.439);
256
+ --sidebar: oklch(0.205 0 0);
257
+ --sidebar-foreground: oklch(0.985 0 0);
258
+ --sidebar-primary: oklch(0.488 0.243 264.376);
259
+ --sidebar-primary-foreground: oklch(0.985 0 0);
260
+ --sidebar-accent: oklch(0.269 0 0);
261
+ --sidebar-accent-foreground: oklch(0.985 0 0);
262
+ --sidebar-border: oklch(1 0 0 / 10%);
263
+ --sidebar-ring: oklch(0.556 0 0);
264
+ }
265
+
266
+ [data-theme-preset="shadcn"][data-theme="light"] {
267
+ --background: oklch(1 0 0);
268
+ --foreground: oklch(0.145 0 0);
269
+ --card: oklch(1 0 0);
270
+ --card-foreground: oklch(0.145 0 0);
271
+ --popover: oklch(1 0 0);
272
+ --popover-foreground: oklch(0.145 0 0);
273
+ --primary: oklch(0.205 0 0);
274
+ --primary-foreground: oklch(0.985 0 0);
275
+ --secondary: oklch(0.97 0 0);
276
+ --secondary-foreground: oklch(0.205 0 0);
277
+ --muted: oklch(0.97 0 0);
278
+ --muted-foreground: oklch(0.556 0 0);
279
+ --accent: oklch(0.97 0 0);
280
+ --accent-foreground: oklch(0.205 0 0);
281
+ --destructive: oklch(0.577 0.245 27.325);
282
+ --destructive-foreground: oklch(1 0 0);
283
+ --border: oklch(0.922 0 0);
284
+ --input: oklch(0.922 0 0);
285
+ --ring: oklch(0.708 0 0);
286
+ --chart-1: oklch(0.646 0.222 41.116);
287
+ --chart-2: oklch(0.6 0.118 184.704);
288
+ --chart-3: oklch(0.398 0.07 227.392);
289
+ --chart-4: oklch(0.828 0.189 84.429);
290
+ --chart-5: oklch(0.769 0.188 70.08);
291
+ --sidebar: oklch(0.985 0 0);
292
+ --sidebar-foreground: oklch(0.145 0 0);
293
+ --sidebar-primary: oklch(0.205 0 0);
294
+ --sidebar-primary-foreground: oklch(0.985 0 0);
295
+ --sidebar-accent: oklch(0.97 0 0);
296
+ --sidebar-accent-foreground: oklch(0.205 0 0);
297
+ --sidebar-border: oklch(0.922 0 0);
298
+ --sidebar-ring: oklch(0.708 0 0);
299
+ }
300
+
208
301
  [data-density="comfortable"] {
209
302
  --ag-text-xs: 11px;
210
303
  --ag-control-text: 12px;
@@ -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
@@ -49,6 +49,14 @@ Storybook CSF excerpts. Default args and render apply unless overridden. Local h
49
49
  }
50
50
  ```
51
51
 
52
+ ### Shadcn
53
+
54
+ ```tsx
55
+ {
56
+ globals: { preset: "shadcn", theme: "system" }
57
+ }
58
+ ```
59
+
52
60
  ### ForcedLight
53
61
 
54
62
  ```tsx
package/llms/setup.md CHANGED
@@ -198,10 +198,15 @@ An independent production Vite consumer installed from the archive verifies RTL
198
198
  | `ThemeProvider.defaultTheme` | `"light" \| "dark" \| "system"` | `"system"` | Initial preference when storage has no saved value. |
199
199
  | `ThemeProvider.storageKey` | `string` | `"shadcn-alexgorbatchev-theme"` | Local storage key for the preference. |
200
200
  | `ThemeProvider.forcedTheme` | `"light" \| "dark"` | unset | Overrides the displayed palette without changing the saved preference. |
201
+ | `ThemeProvider.preset` | `"devhost" \| "shadcn"` | `"devhost"` | Document-wide design preset, independent of color mode and density. |
201
202
  | `DensityProvider.density` | `"compact" \| "comfortable"` | `"comfortable"` | Density for descendants and their portals. |
202
203
  | Component `density` | `"compact" \| "comfortable"` | inherited | Local override on styled components that expose the prop. |
203
204
 
204
- `useTheme()` returns `theme`, `resolvedTheme`, and `setTheme`. Call `setTheme("light")`, `setTheme("dark")`, or `setTheme("system")` from a component inside the provider.
205
+ `useTheme()` returns `theme`, `resolvedTheme`, `setTheme`, and `preset`. Call `setTheme("light")`, `setTheme("dark")`, or `setTheme("system")` from a component inside the provider.
206
+
207
+ Use `<ThemeProvider preset="shadcn" defaultTheme="system">` for the [default shadcn Neutral palette](https://ui.shadcn.com/docs/theming), system sans-serif text, and its rounded corner scale. The default `devhost` preset uses the package's monospace design and sharp corners. Both presets support light, dark, and system modes, including portaled content. Code retains the monospace font. Density, component composition, and package-specific status colors remain independent of the preset.
208
+
209
+ The preset is controlled by the prop; changing it updates the document without changing the saved color-mode preference. Persist that prop in your application if needed. Storybook exposes the same choice in its **Design** toolbar. For a page styled before React mounts, set `data-theme-preset="shadcn"` and `data-theme="light"` or `"dark"` on `<html>` to match the provider. The existing `theme.css` import and `@source` setup cover both presets.
205
210
 
206
211
  Override tokens after the theme imports. This example uses the existing success palette as the primary accent:
207
212
 
@@ -424,6 +429,8 @@ import {
424
429
 
425
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.
426
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
+
427
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.
428
435
 
429
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.
@@ -432,6 +439,10 @@ Column `meta.filter` offers a column in `DataGridFilters`, hidden columns includ
432
439
 
433
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.
434
441
 
442
+ For grids that must fit their container, set `VirtualDataGrid fitColumns`. Use exactly one `meta.fill` column with an explicit `minSize`; required columns' minima plus that minimum must fit the smallest supported container. The grid reserves that space and temporarily hides optional columns that do not fit, prioritizing their chosen order. It lists those 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. Container changes never overwrite user visibility, order, or width preferences. The `VirtualDataGridExample` fixture demonstrates this controlled integration.
445
+
435
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.
436
447
 
437
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.54",
3
+ "version": "0.0.56",
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",