@dynostack/react-grid 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -1
- package/dist/index.cjs +45 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -10
- package/dist/index.d.ts +23 -10
- package/dist/index.js +45 -17
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -0
- package/dist/tailwind-content.js +3 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -13,12 +13,45 @@ Built on [TanStack Table v8](https://tanstack.com/table) · [Radix UI](https://w
|
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
+
**[Live showcase](https://dynostack-react-grid.vercel.app)** · **[Interactive playground](https://dynostack-react-grid.vercel.app/playground)** · **[Setup and configuration docs](https://dynostack-react-grid.vercel.app/docs)**
|
|
17
|
+
|
|
18
|
+
Try every feature on real sample data, customize themes and density, share a configuration link, and copy the matching React props. The demo includes spring-based interactions and works on mobile.
|
|
19
|
+
|
|
16
20
|
---
|
|
17
21
|
|
|
18
22
|
A single `<DataTable />` component that gives you ag-grid–level functionality with a fraction of the API surface and a shadcn/ui aesthetic. Every behavior is opt-in via props — drop it in and it works; configure it and it scales.
|
|
19
23
|
|
|
24
|
+
## Showcase
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="public/table-images/Screenshot%202026-05-25%20233937.png" alt="React data grid with toolbar, sorting, filters, pagination, export, and add-row controls" width="100%" />
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
| Selection + bulk actions | Column menu |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| <img src="public/table-images/Screenshot%202026-05-25%20234013.png" alt="Rows selected with bulk delete and clear actions visible in the toolbar" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234035.png" alt="Column menu with ascending sort, descending sort, pin, unpin, and hide column actions" width="100%" /> |
|
|
33
|
+
|
|
34
|
+
| Filter builder | Column visibility |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| <img src="public/table-images/Screenshot%202026-05-25%20234221.png" alt="Column filter builder with operator select, value input, add condition, clear, close, and apply controls" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234246.png" alt="Column visibility popover with checked columns and reset control" width="100%" /> |
|
|
37
|
+
|
|
38
|
+
| Export selected rows | Inline add and edit |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| <img src="public/table-images/Screenshot%202026-05-25%20234301.png" alt="Export menu for selected rows with CSV and Excel options" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234313.png" alt="Inline add-row editing with text fields, select input, date input, save, and cancel controls" width="100%" /> |
|
|
41
|
+
|
|
42
|
+
| Row actions | Details panel |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| <img src="public/table-images/Screenshot%202026-05-25%20234410.png" alt="Row actions menu with view, edit, duplicate, and delete commands" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234428.png" alt="Right-side details panel with compact, relaxed, and comfy density controls" width="100%" /> |
|
|
45
|
+
|
|
46
|
+
| Search |
|
|
47
|
+
| --- |
|
|
48
|
+
| <img src="public/table-images/Screenshot%202026-05-25%20234453.png" alt="Search input expanded in the table toolbar" width="100%" /> |
|
|
49
|
+
|
|
20
50
|
## Highlights
|
|
21
51
|
|
|
52
|
+
- **Layout and state controls** — `initialSorting`, `onSelectionChange`, `ariaLabel`, `striped`, `stickyHeader`, and `maxHeight`. Disabling pagination renders all matching loaded rows.
|
|
53
|
+
- **Safer exports and editing** — spreadsheet formulas in untrusted text are neutralized; row editing respects `meta.isEditable`; theme values cannot escape scoped CSS declarations.
|
|
54
|
+
|
|
22
55
|
- **Filters that actually filter** — text, number, date with operators (`contains`, `not contains`, `equals`, `before`, `after`, `in range`, `blank`, `not blank`, …), AND/OR combine of two conditions, and a set filter with search + select-all
|
|
23
56
|
- **Inline editing** — double-click cell to edit, or enter row-edit mode with `Save` / `Cancel`
|
|
24
57
|
- **Add row** — local optimistic insert, edit, then commit on save
|
|
@@ -39,6 +72,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
|
|
|
39
72
|
## Table of contents
|
|
40
73
|
|
|
41
74
|
- [Install](#install)
|
|
75
|
+
- [Showcase](#showcase)
|
|
42
76
|
- [Tailwind setup](#tailwind-setup)
|
|
43
77
|
- [Theme tokens](#theme-tokens)
|
|
44
78
|
- [Quick start](#quick-start)
|
|
@@ -67,6 +101,34 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
|
|
|
67
101
|
|
|
68
102
|
## Install
|
|
69
103
|
|
|
104
|
+
### Run the showcase locally
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
git clone https://github.com/wanted-coder-vijay/wcv-data-grid.git
|
|
108
|
+
cd wcv-data-grid
|
|
109
|
+
npm install
|
|
110
|
+
npm run build
|
|
111
|
+
npm install --prefix showcase
|
|
112
|
+
npm run showcase:dev
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### New layout controls
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<DataTable
|
|
119
|
+
data={rows}
|
|
120
|
+
columns={columns}
|
|
121
|
+
initialSorting={[{ id: "name", desc: false }]}
|
|
122
|
+
onSelectionChange={(selectedRows) => setSelectedRows(selectedRows)}
|
|
123
|
+
ariaLabel="Project workspace"
|
|
124
|
+
striped
|
|
125
|
+
stickyHeader
|
|
126
|
+
maxHeight="480px"
|
|
127
|
+
/>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`onSelectionChange` returns selected **loaded** row objects; server-side exports and selection do not fetch unseen pages. Editing flags are UI controls: your server must also validate fields, values, and permissions. CSV/Excel exports neutralize formula prefixes in strings while preserving actual numeric values. Excel output remains an HTML-based `.xls`, not native XLSX. Theme tokens reject declaration delimiters, CSS comments, backslash escapes, and URL expressions.
|
|
131
|
+
|
|
70
132
|
```sh
|
|
71
133
|
npm i @dynostack/react-grid
|
|
72
134
|
# or
|
|
@@ -75,7 +137,7 @@ pnpm add @dynostack/react-grid
|
|
|
75
137
|
yarn add @dynostack/react-grid
|
|
76
138
|
```
|
|
77
139
|
|
|
78
|
-
**Peer deps:** `react >= 18`, `react-dom >= 18`. All other
|
|
140
|
+
**Peer deps:** `react >= 18.2`, `react-dom >= 18.2`. All other runtime dependencies (`@tanstack/react-table`, `radix-ui`, `lucide-react`, `class-variance-authority`, `clsx`, `tailwind-merge`) are installed automatically and remain external to the package bundle.
|
|
79
141
|
|
|
80
142
|
## Tailwind setup
|
|
81
143
|
|
|
@@ -850,6 +912,12 @@ operations and only renders the returned page.
|
|
|
850
912
|
| `pageSizeOptions` | `number[]` | shadcn defaults | Page-size dropdown options. |
|
|
851
913
|
| `initialColumnPinning` | `ColumnPinningState` | `{ left: [], right: [] }` | Initial pinned columns. |
|
|
852
914
|
| `initialColumnVisibility` | `VisibilityState` | `{}` | Initial hidden columns. |
|
|
915
|
+
| `initialSorting` | `SortingState` | `[]` | Initial column sort order. |
|
|
916
|
+
| `onSelectionChange` | `(rows: TData[]) => void` | — | Observe selected loaded rows. |
|
|
917
|
+
| `ariaLabel` | `string` | `"Data table"` | Accessible name for the table. |
|
|
918
|
+
| `striped` | `boolean` | `false` | Alternate row backgrounds. |
|
|
919
|
+
| `stickyHeader` | `boolean` | `false` | Keep headers visible in the scroll viewport. |
|
|
920
|
+
| `maxHeight` | `CSSProperties["maxHeight"]` | — | Limit the vertical scroll viewport. |
|
|
853
921
|
| `globalFilter` | `string` | uncontrolled | Controlled global filter value. |
|
|
854
922
|
| `onGlobalFilterChange` | `(value: string) => void` | — | Controlled global filter setter. |
|
|
855
923
|
| `className` | `string` | — | Extra classes on the table root. |
|
package/dist/index.cjs
CHANGED
|
@@ -283,15 +283,21 @@ function PopoverContent({
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// src/export.ts
|
|
286
|
+
function sanitizeSpreadsheetValue(value) {
|
|
287
|
+
if (value == null) return "";
|
|
288
|
+
const text = String(value);
|
|
289
|
+
if (typeof value === "string" && (/^[\s\u0000-\u001f]*[=+\-@=+-@]/u.test(text) || /^[\t\r\n]/.test(text))) return `'${text}`;
|
|
290
|
+
return text;
|
|
291
|
+
}
|
|
286
292
|
function escapeCsv(value) {
|
|
287
293
|
if (value === null || value === void 0) return "";
|
|
288
|
-
const s =
|
|
294
|
+
const s = sanitizeSpreadsheetValue(value);
|
|
289
295
|
if (/[",\n\r]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
290
296
|
return s;
|
|
291
297
|
}
|
|
292
298
|
function escapeHtml(value) {
|
|
293
299
|
if (value === null || value === void 0) return "";
|
|
294
|
-
return
|
|
300
|
+
return sanitizeSpreadsheetValue(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
295
301
|
}
|
|
296
302
|
function getExportRows(table) {
|
|
297
303
|
const visible = table.getVisibleLeafColumns().filter((c) => {
|
|
@@ -1901,7 +1907,7 @@ function EditableCell({
|
|
|
1901
1907
|
if (typeof flag === "function") return flag(row.original);
|
|
1902
1908
|
return flag;
|
|
1903
1909
|
})();
|
|
1904
|
-
if (!isEditing) {
|
|
1910
|
+
if (!isEditing || !isEditable) {
|
|
1905
1911
|
const display = renderDisplay(initial, editor, meta);
|
|
1906
1912
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1907
1913
|
"div",
|
|
@@ -1918,7 +1924,9 @@ function EditableCell({
|
|
|
1918
1924
|
}
|
|
1919
1925
|
);
|
|
1920
1926
|
}
|
|
1921
|
-
const commit = (value) =>
|
|
1927
|
+
const commit = (value) => {
|
|
1928
|
+
if (isEditable) onCommit(value);
|
|
1929
|
+
};
|
|
1922
1930
|
const handleKey = (e) => {
|
|
1923
1931
|
if (e.key === "Enter") commit(local);
|
|
1924
1932
|
else if (e.key === "Escape") onCancel();
|
|
@@ -2048,6 +2056,9 @@ var TOKEN_TO_VAR = {
|
|
|
2048
2056
|
radius: "--radius",
|
|
2049
2057
|
fontFamily: "--font-family"
|
|
2050
2058
|
};
|
|
2059
|
+
function isSafeToken(value) {
|
|
2060
|
+
return typeof value === "string" && !/[;{}<>@\\\u0000-\u001f]/.test(value) && !/\/\*|\*\/|url\s*\(|expression\s*\(/i.test(value);
|
|
2061
|
+
}
|
|
2051
2062
|
function isModed(theme) {
|
|
2052
2063
|
if (!theme) return false;
|
|
2053
2064
|
return "light" in theme || "dark" in theme;
|
|
@@ -2056,8 +2067,8 @@ function tokensToStyle(tokens) {
|
|
|
2056
2067
|
if (!tokens) return {};
|
|
2057
2068
|
const out = {};
|
|
2058
2069
|
for (const [k, v] of Object.entries(tokens)) {
|
|
2059
|
-
if (v
|
|
2060
|
-
const cssVar = TOKEN_TO_VAR[k];
|
|
2070
|
+
if (!isSafeToken(v)) continue;
|
|
2071
|
+
const cssVar = Object.prototype.hasOwnProperty.call(TOKEN_TO_VAR, k) ? TOKEN_TO_VAR[k] : void 0;
|
|
2061
2072
|
if (cssVar) out[cssVar] = v;
|
|
2062
2073
|
}
|
|
2063
2074
|
return out;
|
|
@@ -2131,8 +2142,8 @@ function tokensToCssBlock(tokens) {
|
|
|
2131
2142
|
if (!tokens) return "";
|
|
2132
2143
|
const parts = [];
|
|
2133
2144
|
for (const [k, v] of Object.entries(tokens)) {
|
|
2134
|
-
if (v
|
|
2135
|
-
const cssVar = TOKEN_TO_VAR[k];
|
|
2145
|
+
if (!isSafeToken(v)) continue;
|
|
2146
|
+
const cssVar = Object.prototype.hasOwnProperty.call(TOKEN_TO_VAR, k) ? TOKEN_TO_VAR[k] : void 0;
|
|
2136
2147
|
if (cssVar) parts.push(`${cssVar}:${v}`);
|
|
2137
2148
|
}
|
|
2138
2149
|
return parts.join(";");
|
|
@@ -2556,6 +2567,10 @@ function useEditingContext() {
|
|
|
2556
2567
|
if (!ctx) throw new Error("DataTable editing context missing");
|
|
2557
2568
|
return ctx;
|
|
2558
2569
|
}
|
|
2570
|
+
function DefaultCellHost(ctx) {
|
|
2571
|
+
const value = ctx.getValue();
|
|
2572
|
+
return value == null ? null : String(value);
|
|
2573
|
+
}
|
|
2559
2574
|
function EditableCellHost(ctx) {
|
|
2560
2575
|
const {
|
|
2561
2576
|
editingCell,
|
|
@@ -2677,6 +2692,7 @@ var DENSITY_CLASS = {
|
|
|
2677
2692
|
default: "",
|
|
2678
2693
|
comfortable: "[&_tbody_td]:py-2"
|
|
2679
2694
|
};
|
|
2695
|
+
var EMPTY_DATA = [];
|
|
2680
2696
|
function DataTable({
|
|
2681
2697
|
data,
|
|
2682
2698
|
columns,
|
|
@@ -2700,6 +2716,12 @@ function DataTable({
|
|
|
2700
2716
|
pageSizeOptions,
|
|
2701
2717
|
initialColumnPinning,
|
|
2702
2718
|
initialColumnVisibility,
|
|
2719
|
+
initialSorting,
|
|
2720
|
+
onSelectionChange,
|
|
2721
|
+
ariaLabel = "Data table",
|
|
2722
|
+
striped = false,
|
|
2723
|
+
stickyHeader = false,
|
|
2724
|
+
maxHeight,
|
|
2703
2725
|
globalFilter: globalFilterProp,
|
|
2704
2726
|
onGlobalFilterChange,
|
|
2705
2727
|
className,
|
|
@@ -2776,7 +2798,7 @@ function DataTable({
|
|
|
2776
2798
|
}
|
|
2777
2799
|
setConfirmCtx(null);
|
|
2778
2800
|
};
|
|
2779
|
-
const [sorting, setSorting] = react.useState([]);
|
|
2801
|
+
const [sorting, setSorting] = react.useState(initialSorting ?? []);
|
|
2780
2802
|
const [columnFilters, setColumnFilters] = react.useState([]);
|
|
2781
2803
|
const [internalGlobalFilter, setInternalGlobalFilter] = react.useState("");
|
|
2782
2804
|
const isControlledFilter = globalFilterProp !== void 0;
|
|
@@ -2872,7 +2894,7 @@ function DataTable({
|
|
|
2872
2894
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2873
2895
|
...dataSourceDeps
|
|
2874
2896
|
]);
|
|
2875
|
-
const baseData = dataSourceEnabled ? sourceRows : data ??
|
|
2897
|
+
const baseData = dataSourceEnabled ? sourceRows : data ?? EMPTY_DATA;
|
|
2876
2898
|
const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
|
|
2877
2899
|
const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
|
|
2878
2900
|
const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
|
|
@@ -2916,7 +2938,7 @@ function DataTable({
|
|
|
2916
2938
|
title: headerLabel
|
|
2917
2939
|
}
|
|
2918
2940
|
),
|
|
2919
|
-
cell: hasEditor ? EditableCellHost : original
|
|
2941
|
+
cell: hasEditor ? EditableCellHost : original ?? DefaultCellHost
|
|
2920
2942
|
};
|
|
2921
2943
|
};
|
|
2922
2944
|
const built = columns.map(wrap);
|
|
@@ -3028,10 +3050,10 @@ function DataTable({
|
|
|
3028
3050
|
enableColumnFilters: f.filtering,
|
|
3029
3051
|
enableColumnResizing: f.resizing,
|
|
3030
3052
|
columnResizeMode: "onChange",
|
|
3031
|
-
enableRowSelection:
|
|
3053
|
+
enableRowSelection: enableSelection,
|
|
3032
3054
|
enableExpanding: !!(renderSubRow || getSubRows),
|
|
3033
3055
|
enablePinning: f.pinning,
|
|
3034
|
-
manualPagination: isServerSideDataSource,
|
|
3056
|
+
manualPagination: isServerSideDataSource || !f.pagination,
|
|
3035
3057
|
manualSorting: isServerSideDataSource,
|
|
3036
3058
|
manualFiltering: isServerSideDataSource,
|
|
3037
3059
|
pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
|
|
@@ -3121,6 +3143,11 @@ function DataTable({
|
|
|
3121
3143
|
setEditingRowId(tempId);
|
|
3122
3144
|
} : void 0;
|
|
3123
3145
|
const rows = table.getRowModel().rows;
|
|
3146
|
+
const selectionCallback = react.useRef(onSelectionChange);
|
|
3147
|
+
selectionCallback.current = onSelectionChange;
|
|
3148
|
+
react.useEffect(() => {
|
|
3149
|
+
selectionCallback.current?.(table.getSelectedRowModel().flatRows.map((row) => row.original));
|
|
3150
|
+
}, [rowSelection, mergedData, table]);
|
|
3124
3151
|
const visibleLeafCount = table.getVisibleLeafColumns().length;
|
|
3125
3152
|
const editingContextValue = react.useMemo(
|
|
3126
3153
|
() => ({
|
|
@@ -3198,17 +3225,18 @@ function DataTable({
|
|
|
3198
3225
|
labels
|
|
3199
3226
|
}
|
|
3200
3227
|
),
|
|
3201
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-auto", children: [
|
|
3228
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-auto", style: { maxHeight }, children: [
|
|
3202
3229
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3203
3230
|
"table",
|
|
3204
3231
|
{
|
|
3205
|
-
|
|
3232
|
+
"aria-label": ariaLabel,
|
|
3233
|
+
className: cn("w-full caption-bottom text-sm", striped && "[&_tbody>tr:nth-child(even)]:bg-muted/25"),
|
|
3206
3234
|
style: {
|
|
3207
3235
|
width: table.getCenterTotalSize() ? void 0 : "100%",
|
|
3208
3236
|
minWidth: "100%"
|
|
3209
3237
|
},
|
|
3210
3238
|
children: [
|
|
3211
|
-
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 z-10
|
|
3239
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: cn("bg-card", stickyHeader && "sticky top-0 z-10"), children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b", children: hg.headers.map((header) => {
|
|
3212
3240
|
const meta = header.column.columnDef.meta;
|
|
3213
3241
|
const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
|
|
3214
3242
|
const isPinned = !!header.column.getIsPinned();
|
|
@@ -3401,6 +3429,7 @@ exports.exportToCsv = exportToCsv;
|
|
|
3401
3429
|
exports.exportToExcel = exportToExcel;
|
|
3402
3430
|
exports.isFilterActive = isFilterActive;
|
|
3403
3431
|
exports.numberFilterFn = numberFilterFn;
|
|
3432
|
+
exports.sanitizeSpreadsheetValue = sanitizeSpreadsheetValue;
|
|
3404
3433
|
exports.setFilterFn = setFilterFn;
|
|
3405
3434
|
exports.splitTheme = splitTheme;
|
|
3406
3435
|
exports.textFilterFn = textFilterFn;
|