@rovula/ui 0.1.28 → 0.1.29
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/dist/cjs/bundle.css +501 -67
- package/dist/cjs/bundle.js +589 -589
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/cjs/types/components/Table/Table.d.ts +33 -3
- package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/components/DataTable/DataTable.editing.js +385 -0
- package/dist/components/DataTable/DataTable.editing.types.js +1 -0
- package/dist/components/DataTable/DataTable.js +983 -50
- package/dist/components/DataTable/DataTable.stories.js +1077 -25
- package/dist/components/Dropdown/Dropdown.js +8 -6
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
- package/dist/components/Table/Table.js +103 -13
- package/dist/components/Table/Table.stories.js +226 -9
- package/dist/components/TextInput/TextInput.js +6 -4
- package/dist/components/TextInput/TextInput.stories.js +8 -0
- package/dist/components/TextInput/TextInput.styles.js +7 -1
- package/dist/esm/bundle.css +501 -67
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/esm/types/components/Table/Table.d.ts +33 -3
- package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/index.d.ts +493 -122
- package/dist/src/theme/global.css +747 -96
- package/package.json +14 -2
- package/src/components/DataTable/DataTable.editing.tsx +861 -0
- package/src/components/DataTable/DataTable.editing.types.ts +192 -0
- package/src/components/DataTable/DataTable.stories.tsx +2169 -31
- package/src/components/DataTable/DataTable.test.tsx +696 -0
- package/src/components/DataTable/DataTable.tsx +2260 -94
- package/src/components/Dropdown/Dropdown.tsx +22 -6
- package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
- package/src/components/ScrollArea/ScrollArea.tsx +6 -6
- package/src/components/Table/Table.stories.tsx +789 -44
- package/src/components/Table/Table.tsx +294 -28
- package/src/components/TextInput/TextInput.stories.tsx +80 -0
- package/src/components/TextInput/TextInput.styles.ts +7 -1
- package/src/components/TextInput/TextInput.tsx +21 -14
- package/src/test/setup.ts +50 -0
- package/src/theme/global.css +81 -42
- package/src/theme/presets/colors.js +12 -0
- package/src/theme/themes/variable.css +27 -28
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/scrollbar.css +9 -4
- package/src/theme/tokens/components/table.css +63 -0
|
@@ -1,46 +1,1098 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { DataTable } from "./DataTable";
|
|
4
|
+
import { EllipsisVertical, ChevronRight, FileDown, FileUp, Trash2, } from "lucide-react";
|
|
5
|
+
import ActionButton from "@/components/ActionButton/ActionButton";
|
|
6
|
+
import Button from "@/components/Button/Button";
|
|
7
|
+
import { Badge } from "@/components/Badge/Badge";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
3
9
|
const meta = {
|
|
4
10
|
title: "Components/DataTable",
|
|
5
11
|
component: DataTable,
|
|
6
12
|
tags: ["autodocs"],
|
|
7
|
-
parameters: {
|
|
8
|
-
|
|
13
|
+
parameters: { layout: "fullscreen" },
|
|
14
|
+
argTypes: {
|
|
15
|
+
bordered: { control: "boolean" },
|
|
16
|
+
surface: {
|
|
17
|
+
control: "radio",
|
|
18
|
+
options: ["default", "panel"],
|
|
19
|
+
},
|
|
20
|
+
divided: { control: "boolean" },
|
|
21
|
+
striped: { control: "boolean" },
|
|
22
|
+
fetchingMore: { control: "boolean" },
|
|
23
|
+
loading: { control: "boolean" },
|
|
9
24
|
},
|
|
10
25
|
decorators: [
|
|
11
|
-
(Story) => (_jsx("div", { className: "p-5 flex flex-1 h-full w-full ", style: { height: "100vh" }, children: _jsx(Story, {}) })),
|
|
26
|
+
(Story) => (_jsx("div", { className: "p-5 flex flex-1 h-full w-full min-h-0 bg-page-bg-main items-stretch", style: { height: "100vh" }, children: _jsx("div", { className: "w-full min-h-0 flex-1", children: _jsx(Story, {}) }) })),
|
|
12
27
|
],
|
|
13
28
|
};
|
|
14
29
|
export default meta;
|
|
15
|
-
|
|
30
|
+
/* prettier-ignore */
|
|
31
|
+
const projectData = [
|
|
32
|
+
{ id: "1", name: "Drone inspection", type: "Drone", subtype: "Visual inspection", createdDate: "15 Mar 2026", status: "To do" },
|
|
33
|
+
{ id: "2", name: "ROV Structure inspection", type: "ROV", subtype: "Text", createdDate: "01 Jan 2026", status: "In Progress" },
|
|
34
|
+
{ id: "3", name: "ROV Structure inspection", type: "ROV", subtype: "Structure Inspection", createdDate: "15 Feb 2026", status: "Completed" },
|
|
35
|
+
{ id: "4", name: "AUV Pipeline inspection", type: "AUV", subtype: "Subsea pipeline Inspection", createdDate: "30 Jan 2026", status: "In Progress" },
|
|
36
|
+
{ id: "5", name: "ROV Structure inspection", type: "ROV", subtype: "Structure Inspection", createdDate: "01 Jan 2026", status: "Completed" },
|
|
37
|
+
];
|
|
38
|
+
/** Same rows repeated for pagination demos — each row needs a unique `id` (React `key` + TanStack row id). */
|
|
39
|
+
const projectDataCopies = (copies) => Array.from({ length: copies }, (_, copy) => projectData.map((r) => (Object.assign(Object.assign({}, r), { id: `${r.id}-c${copy}` })))).flat();
|
|
40
|
+
/** Large pool for infinite-scroll load-more demos. */
|
|
41
|
+
const infiniteScrollPool = projectDataCopies(30);
|
|
42
|
+
/* prettier-ignore */
|
|
43
|
+
const statusCls = {
|
|
44
|
+
"To do": "bg-transparent-grey2-8 text-text-contrast-max",
|
|
45
|
+
"In Progress": "bg-warning-500/20 text-warning-400",
|
|
46
|
+
"Completed": "bg-success-500/20 text-success-400",
|
|
47
|
+
};
|
|
48
|
+
const StatusBadge = ({ status }) => (_jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-lg typography-body3 ${statusCls[status]}`, children: status }));
|
|
49
|
+
/* prettier-ignore */
|
|
50
|
+
const projectColumns = [
|
|
51
|
+
{ accessorKey: "name", header: "Project name" },
|
|
52
|
+
{ accessorKey: "type", header: "Type" },
|
|
53
|
+
{ accessorKey: "subtype", header: "Subtype" },
|
|
54
|
+
{ accessorKey: "createdDate", header: "Created date" },
|
|
55
|
+
{ accessorKey: "status", header: "Status", cell: ({ row }) => _jsx(StatusBadge, { status: row.original.status }) },
|
|
56
|
+
];
|
|
57
|
+
const ProjectRowActions = ({ row }) => (_jsxs(_Fragment, { children: [_jsx(ActionButton, { variant: "icon", size: "sm", onClick: () => console.log("menu", row.original), "aria-label": "More options", children: _jsx(EllipsisVertical, {}) }), _jsx(ActionButton, { variant: "icon", size: "sm", onClick: () => console.log("open", row.original), "aria-label": "Open", children: _jsx(ChevronRight, {}) })] }));
|
|
58
|
+
/* prettier-ignore */
|
|
59
|
+
const inspectionData = [
|
|
60
|
+
{ id: "1", code: "INS-001", assetName: "Pipeline A", type: "ROV", subtype: "Structural", severity: "Highest", inspector: "John D.", location: "Block A", capturedAt: "01 Jan 2026", status: "Completed", kpi: 98, notes: "Crack detected" },
|
|
61
|
+
{ id: "2", code: "INS-002", assetName: "Pipeline B", type: "AUV", subtype: "Corrosion", severity: "High", inspector: "Sarah K.", location: "Block B", capturedAt: "05 Jan 2026", status: "In Progress", kpi: 72, notes: "Corrosion spreading" },
|
|
62
|
+
{ id: "3", code: "INS-003", assetName: "Riser C", type: "Drone", subtype: "Visual", severity: "Medium", inspector: "Mike L.", location: "Block C", capturedAt: "10 Jan 2026", status: "To do", kpi: 0, notes: "-" },
|
|
63
|
+
{ id: "4", code: "INS-004", assetName: "Wellhead D", type: "ROV", subtype: "Structural", severity: "Low", inspector: "Anna T.", location: "Block A", capturedAt: "15 Jan 2026", status: "Completed", kpi: 100, notes: "No issues found" },
|
|
64
|
+
{ id: "5", code: "INS-005", assetName: "Flowline E", type: "AUV", subtype: "Leak detect", severity: "Lowest", inspector: "John D.", location: "Block D", capturedAt: "20 Jan 2026", status: "In Progress", kpi: 55, notes: "Minor seepage" },
|
|
65
|
+
{ id: "6", code: "INS-006", assetName: "Manifold F", type: "Drone", subtype: "Visual", severity: "High", inspector: "Sarah K.", location: "Block B", capturedAt: "22 Jan 2026", status: "To do", kpi: 0, notes: "-" },
|
|
66
|
+
{ id: "7", code: "INS-007", assetName: "Subsea G", type: "ROV", subtype: "Structural", severity: "Medium", inspector: "Mike L.", location: "Block E", capturedAt: "25 Jan 2026", status: "In Progress", kpi: 44, notes: "Under review" },
|
|
67
|
+
];
|
|
68
|
+
/* prettier-ignore */
|
|
69
|
+
const severityCls = {
|
|
70
|
+
Highest: "bg-error-500/20 text-error-400",
|
|
71
|
+
High: "bg-orange-500/20 text-orange-400",
|
|
72
|
+
Medium: "bg-warning-500/20 text-warning-400",
|
|
73
|
+
Low: "bg-success-500/20 text-success-400",
|
|
74
|
+
Lowest: "bg-info-500/20 text-info-400",
|
|
75
|
+
};
|
|
76
|
+
/* prettier-ignore */
|
|
77
|
+
const inspectionStatusCls = {
|
|
78
|
+
"To do": "bg-transparent text-text-contrast-max border border-white/20",
|
|
79
|
+
"In Progress": "bg-warning-500/20 text-warning-400",
|
|
80
|
+
Completed: "bg-success-500/20 text-success-400",
|
|
81
|
+
};
|
|
82
|
+
const inspectionColumns = [
|
|
83
|
+
{ accessorKey: "code", header: "Code", size: 100 },
|
|
84
|
+
{ accessorKey: "assetName", header: "Asset name" },
|
|
85
|
+
{ accessorKey: "type", header: "Type", size: 90 },
|
|
86
|
+
{ accessorKey: "subtype", header: "Subtype" },
|
|
16
87
|
{
|
|
17
|
-
accessorKey: "
|
|
88
|
+
accessorKey: "severity",
|
|
89
|
+
header: "Severity",
|
|
90
|
+
size: 120,
|
|
91
|
+
cell: ({ row }) => (_jsx("span", { className: `inline-flex items-center px-2 py-0.5 rounded-lg typography-small2 ${severityCls[row.original.severity]}`, children: row.original.severity })),
|
|
18
92
|
},
|
|
93
|
+
{ accessorKey: "inspector", header: "Inspector" },
|
|
94
|
+
{ accessorKey: "location", header: "Location", size: 100 },
|
|
95
|
+
{ accessorKey: "capturedAt", header: "Captured date", size: 140 },
|
|
19
96
|
{
|
|
20
97
|
accessorKey: "status",
|
|
98
|
+
header: "Status",
|
|
99
|
+
size: 130,
|
|
100
|
+
cell: ({ row }) => (_jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-lg typography-body3 ${inspectionStatusCls[row.original.status]}`, children: row.original.status })),
|
|
21
101
|
},
|
|
102
|
+
{ accessorKey: "kpi", header: "KPI (%)", size: 90 },
|
|
103
|
+
{ accessorKey: "notes", header: "Notes" },
|
|
104
|
+
];
|
|
105
|
+
/* prettier-ignore */
|
|
106
|
+
const eventSeverityCls = {
|
|
107
|
+
Highest: "bg-error-500 text-white",
|
|
108
|
+
High: "bg-orange-500 text-white",
|
|
109
|
+
Medium: "bg-warning-500 text-black",
|
|
110
|
+
Low: "bg-success-500 text-white",
|
|
111
|
+
Lowest: "bg-info-500 text-white",
|
|
112
|
+
};
|
|
113
|
+
const eventData = [
|
|
114
|
+
{
|
|
115
|
+
id: "1",
|
|
116
|
+
code: "EV-001",
|
|
117
|
+
name: "Structural anomaly",
|
|
118
|
+
severity: "Highest",
|
|
119
|
+
children: [
|
|
120
|
+
{
|
|
121
|
+
id: "1-1",
|
|
122
|
+
code: "EV-001-A",
|
|
123
|
+
name: "Crack detected",
|
|
124
|
+
severity: "Highest",
|
|
125
|
+
children: [
|
|
126
|
+
{
|
|
127
|
+
id: "1-1-1",
|
|
128
|
+
code: "EV-001-A-1",
|
|
129
|
+
name: "Sub-crack detected",
|
|
130
|
+
severity: "Highest",
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "1-2",
|
|
136
|
+
code: "EV-001-B",
|
|
137
|
+
name: "Corrosion detected",
|
|
138
|
+
severity: "High",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "2",
|
|
144
|
+
code: "EV-002",
|
|
145
|
+
name: "Pipeline leak",
|
|
146
|
+
severity: "Low",
|
|
147
|
+
children: [
|
|
148
|
+
{
|
|
149
|
+
id: "2-1",
|
|
150
|
+
code: "EV-002-A",
|
|
151
|
+
name: "Minor seepage",
|
|
152
|
+
severity: "Lowest",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{ id: "3", code: "EV-003", name: "Obstruction", severity: "Medium" },
|
|
157
|
+
];
|
|
158
|
+
const eventColumns = [
|
|
159
|
+
{ accessorKey: "code", header: "Code" },
|
|
160
|
+
{ accessorKey: "name", header: "Name" },
|
|
22
161
|
{
|
|
23
|
-
accessorKey: "
|
|
162
|
+
accessorKey: "severity",
|
|
163
|
+
header: "Severity level",
|
|
164
|
+
cell: ({ row }) => row.original.severity ? (_jsx("span", { className: `inline-flex items-center px-2 py-0.5 rounded typography-small3 ${eventSeverityCls[row.original.severity]}`, children: row.original.severity })) : (_jsx("span", { className: "text-text-g-contrast-medium", children: "\u2013" })),
|
|
24
165
|
},
|
|
25
166
|
];
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// 1. Basics
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** Default — striped rows + column dividers, all columns sortable. */
|
|
26
171
|
export const Default = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
172
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectData, striped: true, divided: true, onSorting: (s) => console.log("sort", s) })),
|
|
173
|
+
};
|
|
174
|
+
/** Empty state — displayed when `data` is an empty array. */
|
|
175
|
+
export const Empty = {
|
|
176
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: [], striped: true, divided: true })),
|
|
177
|
+
};
|
|
178
|
+
/** Non-striped with column dividers only. */
|
|
179
|
+
export const Divided = {
|
|
180
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectData, divided: true })),
|
|
181
|
+
};
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Panel surface (Modal / Drawer / Panel)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
//
|
|
186
|
+
// Wrapper uses data-surface="panel" so --table-* tokens from table.css apply
|
|
187
|
+
// (same pattern as Table stories). You can instead set surface="panel" on
|
|
188
|
+
// DataTable when the panel root is not a DOM ancestor of the table.
|
|
189
|
+
/** Simulates a Modal / Drawer / Panel chrome around the table. */
|
|
190
|
+
const DataTablePanelDecorator = ({ children, className, }) => (_jsxs("div", { "data-surface": "panel", className: cn("rounded-lg bg-modal-surface p-6 w-full max-w-4xl mx-auto shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)]", className), children: [_jsx("p", { className: "typography-subtitle4 text-text-g-contrast-medium mb-4", children: "Modal / Panel surface" }), children] }));
|
|
191
|
+
/**
|
|
192
|
+
* DataTable inside a panel — matches modal spec: horizontal row separators only
|
|
193
|
+
* (no vertical column rules), compact height without a stretched empty body.
|
|
194
|
+
*/
|
|
195
|
+
export const OnPanelSurface = {
|
|
196
|
+
render: () => (_jsx("div", { className: "flex w-full justify-center items-start", children: _jsx(DataTablePanelDecorator, { className: "self-start", children: _jsx(DataTable, { className: "h-auto", columns: projectColumns, data: projectData, striped: false, divided: false, onSorting: (s) => console.log("sort", s) }) }) })),
|
|
197
|
+
};
|
|
198
|
+
/** Panel + client pagination — footer sits in the same surface scope as the table body. */
|
|
199
|
+
export const OnPanelSurfaceWithPagination = {
|
|
200
|
+
render: () => (_jsx("div", { className: "flex w-full justify-center items-start", children: _jsx(DataTablePanelDecorator, { className: "self-start", children: _jsx(DataTable, { className: "h-auto max-h-[min(520px,70vh)]", columns: projectColumns, data: projectDataCopies(4), paginationMode: "client", pageSizeOptions: [5, 10, 20], striped: true, divided: true, columnManagement: true }) }) })),
|
|
201
|
+
};
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// 2. Pagination
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/** Client-side pagination — DataTable manages page index & size internally. */
|
|
206
|
+
export const WithClientPagination = {
|
|
207
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectDataCopies(4), paginationMode: "client", pageSizeOptions: [5, 10, 20], striped: true, divided: true })),
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Server-side pagination — the caller controls `pageIndex` / `pageSize` and
|
|
211
|
+
* slices data accordingly. `totalCount` drives the page-count calculation.
|
|
212
|
+
*/
|
|
213
|
+
export const WithServerPagination = {
|
|
214
|
+
render: () => {
|
|
215
|
+
const [pageIndex, setPageIndex] = React.useState(0);
|
|
216
|
+
const [pageSize, setPageSize] = React.useState(3);
|
|
217
|
+
const pagedData = projectData.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
|
218
|
+
return (_jsx(DataTable, { columns: projectColumns, data: pagedData, paginationMode: "server", totalCount: projectData.length, pageIndex: pageIndex, pageSize: pageSize, onPaginationChange: ({ pageIndex: pi, pageSize: ps }) => {
|
|
219
|
+
setPageIndex(pi);
|
|
220
|
+
setPageSize(ps);
|
|
221
|
+
}, pageSizeOptions: [3, 5], striped: true, divided: true }));
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Infinite mode (default) — no pagination bar; the table body scrolls inside a
|
|
226
|
+
* fixed height. Use when the caller already passes the full (or windowed) dataset.
|
|
227
|
+
*/
|
|
228
|
+
export const WithInfiniteScrollStatic = {
|
|
229
|
+
render: () => (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium", children: [_jsx("code", { className: "typography-mono", children: "paginationMode=\"infinite\"" }), " ", "(default) \u2014 no footer; scroll the body. Height capped via", " ", _jsx("code", { className: "typography-mono", children: "className" }), "."] }), _jsx(DataTable, { className: "h-[min(420px,55vh)] w-full", columns: projectColumns, data: projectDataCopies(8), paginationMode: "infinite", striped: true, divided: true })] })),
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Infinite scroll + load more — DataTable calls{" "}
|
|
233
|
+
* <code className="typography-mono">fetchMoreData</code> when the scroll position
|
|
234
|
+
* is within 10px of the bottom (see <code className="typography-mono">DataTable</code>{" "}
|
|
235
|
+
* <code className="typography-mono">useEffect</code> on <code className="typography-mono">scrollRef</code>).
|
|
236
|
+
* This story simulates an async page append.
|
|
237
|
+
*/
|
|
238
|
+
export const WithInfiniteScrollLoadMore = {
|
|
239
|
+
render: () => {
|
|
240
|
+
const pageSize = 10;
|
|
241
|
+
const [rows, setRows] = React.useState(() => infiniteScrollPool.slice(0, pageSize));
|
|
242
|
+
const [loading, setLoading] = React.useState(false);
|
|
243
|
+
const loadingRef = React.useRef(false);
|
|
244
|
+
const hasMore = rows.length < infiniteScrollPool.length;
|
|
245
|
+
const fetchMoreData = React.useCallback(() => {
|
|
246
|
+
if (loadingRef.current)
|
|
247
|
+
return;
|
|
248
|
+
setRows((prev) => {
|
|
249
|
+
if (prev.length >= infiniteScrollPool.length)
|
|
250
|
+
return prev;
|
|
251
|
+
const startLen = prev.length;
|
|
252
|
+
loadingRef.current = true;
|
|
253
|
+
queueMicrotask(() => setLoading(true));
|
|
254
|
+
window.setTimeout(() => {
|
|
255
|
+
setRows(infiniteScrollPool.slice(0, Math.min(startLen + pageSize, infiniteScrollPool.length)));
|
|
256
|
+
loadingRef.current = false;
|
|
257
|
+
setLoading(false);
|
|
258
|
+
}, 650);
|
|
259
|
+
return prev;
|
|
260
|
+
});
|
|
261
|
+
}, []);
|
|
262
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium", children: ["Scroll to the bottom to trigger", " ", _jsx("code", { className: "typography-mono", children: "fetchMoreData" }), ". Built-in", " ", _jsx("code", { className: "typography-mono", children: "fetchingMore" }), " shows a spinner row inside the table. Loaded ", rows.length, " /", " ", infiniteScrollPool.length, " rows.", !hasMore ? " (end of list)" : ""] }), _jsx(DataTable, { className: "h-[min(380px,50vh)] w-full", columns: projectColumns, data: rows, paginationMode: "infinite", fetchMoreData: fetchMoreData, fetchingMore: loading, striped: true, divided: true })] }));
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Initial load — pass `loading={true}` with `data={[]}` until the first page
|
|
267
|
+
* arrives; then set `loading={false}` and pass rows. Infinite `fetchMoreData`
|
|
268
|
+
* does not run while `loading` is true.
|
|
269
|
+
*/
|
|
270
|
+
export const WithInfiniteScrollInitialLoading = {
|
|
271
|
+
render: () => {
|
|
272
|
+
const pageSize = 10;
|
|
273
|
+
const [rows, setRows] = React.useState([]);
|
|
274
|
+
const [loading, setLoading] = React.useState(true);
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
const t = window.setTimeout(() => {
|
|
277
|
+
setRows(infiniteScrollPool.slice(0, pageSize));
|
|
278
|
+
setLoading(false);
|
|
279
|
+
}, 1200);
|
|
280
|
+
return () => window.clearTimeout(t);
|
|
281
|
+
}, []);
|
|
282
|
+
const [fetchingMore, setFetchingMore] = React.useState(false);
|
|
283
|
+
const loadingMoreRef = React.useRef(false);
|
|
284
|
+
const [highlightId, setHighlightId] = React.useState();
|
|
285
|
+
const fetchMoreData = React.useCallback(() => {
|
|
286
|
+
if (loadingMoreRef.current)
|
|
287
|
+
return;
|
|
288
|
+
setRows((prev) => {
|
|
289
|
+
if (prev.length >= infiniteScrollPool.length)
|
|
290
|
+
return prev;
|
|
291
|
+
loadingMoreRef.current = true;
|
|
292
|
+
queueMicrotask(() => setFetchingMore(true));
|
|
293
|
+
const startLen = prev.length;
|
|
294
|
+
window.setTimeout(() => {
|
|
295
|
+
setRows(infiniteScrollPool.slice(0, Math.min(startLen + pageSize, infiniteScrollPool.length)));
|
|
296
|
+
loadingMoreRef.current = false;
|
|
297
|
+
setFetchingMore(false);
|
|
298
|
+
}, 550);
|
|
299
|
+
return prev;
|
|
300
|
+
});
|
|
301
|
+
}, []);
|
|
302
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium", children: ["Simulated first fetch (~1.2s) with", " ", _jsx("code", { className: "typography-mono", children: "loading" }), " + empty", " ", _jsx("code", { className: "typography-mono", children: "data" }), ", then infinite scroll loads more with", " ", _jsx("code", { className: "typography-mono", children: "fetchingMore" }), ". Click", " ", _jsx("span", { className: "typography-mono", children: "Scroll to last row" }), " to jump + highlight."] }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
303
|
+
if (!rows.length)
|
|
304
|
+
return;
|
|
305
|
+
setHighlightId(rows[rows.length - 1].id);
|
|
306
|
+
}, disabled: !rows.length, children: "Scroll to last row" })] }), _jsx(DataTable, { className: "h-[min(380px,50vh)] w-full", columns: projectColumns, data: rows, paginationMode: "infinite", loading: loading, fetchMoreData: fetchMoreData, fetchingMore: fetchingMore, highlightRowId: highlightId, striped: true, divided: true })] }));
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Performance (expectations + stress demo)
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
/**
|
|
313
|
+
* **What this library does *not* do:** `DataTable` has **no built-in row
|
|
314
|
+
* virtualization** — every row in `data` is rendered in the DOM (TanStack
|
|
315
|
+
* `getRowModel`). Very large `data.length` will cost more paint/layout work.
|
|
316
|
+
*
|
|
317
|
+
* **What to use instead:**
|
|
318
|
+
* - `paginationMode="client"` or `"server"` to cap rows per page
|
|
319
|
+
* - `paginationMode="infinite"` + `fetchMoreData` and **slice/window data in the parent**
|
|
320
|
+
* - Or wrap your own virtualizer (e.g. `@tanstack/react-virtual`) if you need
|
|
321
|
+
* millions of rows in one scroll view
|
|
322
|
+
*
|
|
323
|
+
* This story mounts **800** synthetic rows so you can feel scroll/render cost
|
|
324
|
+
* in Storybook (dev builds are slower than production).
|
|
325
|
+
*/
|
|
326
|
+
export const PerformanceLargeDataset = {
|
|
327
|
+
render: () => {
|
|
328
|
+
const bigData = React.useMemo(() => {
|
|
329
|
+
const rowCount = 800;
|
|
330
|
+
const statuses = [
|
|
331
|
+
"To do",
|
|
332
|
+
"In Progress",
|
|
333
|
+
"Completed",
|
|
334
|
+
];
|
|
335
|
+
return Array.from({ length: rowCount }, (_, i) => ({
|
|
336
|
+
id: `perf-${i}`,
|
|
337
|
+
name: `Project ${i + 1}`,
|
|
338
|
+
type: (i % 3 === 0
|
|
339
|
+
? "Drone"
|
|
340
|
+
: i % 3 === 1
|
|
341
|
+
? "ROV"
|
|
342
|
+
: "AUV"),
|
|
343
|
+
subtype: "Visual inspection",
|
|
344
|
+
createdDate: "15 Mar 2026",
|
|
345
|
+
status: statuses[i % 3],
|
|
346
|
+
}));
|
|
347
|
+
}, []);
|
|
348
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium max-w-4xl", children: [_jsx("strong", { children: bigData.length }), " rows in", " ", _jsx("code", { className: "typography-mono", children: "data" }), " \u2014 no virtualization. Prefer pagination or infinite + parent-side windowing for production lists."] }), _jsx(DataTable, { className: "h-[min(520px,70vh)] w-full", columns: projectColumns, data: bigData, paginationMode: "infinite", striped: true, divided: true })] }));
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Built-in **`virtualized`** — only a vertical window of rows is mounted; the
|
|
353
|
+
* rest are represented by spacer rows. Tune **`virtualRowEstimate`** (px) to
|
|
354
|
+
* match your row height so scroll position stays aligned.
|
|
355
|
+
*
|
|
356
|
+
* Limitations: fixed-height assumption; incompatible with **`reorderable`** in
|
|
357
|
+
* the virtualized path (use non-virtualized mode for drag-reorder).
|
|
358
|
+
*/
|
|
359
|
+
export const WithVirtualizedRows = {
|
|
360
|
+
render: () => {
|
|
361
|
+
const bigData = React.useMemo(() => {
|
|
362
|
+
const rowCount = 5000;
|
|
363
|
+
const statuses = [
|
|
364
|
+
"To do",
|
|
365
|
+
"In Progress",
|
|
366
|
+
"Completed",
|
|
367
|
+
];
|
|
368
|
+
return Array.from({ length: rowCount }, (_, i) => ({
|
|
369
|
+
id: `virt-${i}`,
|
|
370
|
+
name: `Project ${i + 1}`,
|
|
371
|
+
type: (i % 3 === 0
|
|
372
|
+
? "Drone"
|
|
373
|
+
: i % 3 === 1
|
|
374
|
+
? "ROV"
|
|
375
|
+
: "AUV"),
|
|
376
|
+
subtype: "Visual inspection",
|
|
377
|
+
createdDate: "15 Mar 2026",
|
|
378
|
+
status: statuses[i % 3],
|
|
379
|
+
}));
|
|
380
|
+
}, []);
|
|
381
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium max-w-4xl", children: [_jsx("strong", { children: bigData.length }), " rows with", " ", _jsx("code", { className: "typography-mono", children: "virtualized" }), " \u2014 compare scroll smoothness vs", " ", _jsx("code", { className: "typography-mono", children: "PerformanceLargeDataset" }), " (800 rows, no virtualization)."] }), _jsx(DataTable, { className: "h-[min(520px,70vh)] w-full", columns: projectColumns, data: bigData, paginationMode: "infinite", virtualized: true, virtualRowEstimate: 52, striped: true, divided: true })] }));
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// 3. Selection
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
/** Checkbox selection — header checkbox selects all; row checkboxes select individually. */
|
|
388
|
+
export const WithSelection = {
|
|
389
|
+
render: () => {
|
|
390
|
+
const [selected, setSelected] = React.useState({});
|
|
391
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 w-full h-full", children: [_jsxs("p", { className: "typography-small2 text-text-g-contrast-high px-1", children: ["Selected IDs:", " ", Object.keys(selected)
|
|
392
|
+
.filter((k) => selected[k])
|
|
393
|
+
.join(", ") || "–"] }), _jsx(DataTable, { columns: projectColumns, data: projectData, selectable: true, onRowSelectionChange: setSelected, striped: true, divided: true })] }));
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Row highlight + scroll — use `highlightRowId` to visually mark important rows
|
|
398
|
+
* (uses the same token as selected rows) and automatically scroll them into view.
|
|
399
|
+
*/
|
|
400
|
+
export const WithRowHighlightAndScroll = {
|
|
401
|
+
render: () => {
|
|
402
|
+
const rowsForDemo = React.useMemo(() => projectDataCopies(4), []);
|
|
403
|
+
const [highlightIds, setHighlightIds] = React.useState([]);
|
|
404
|
+
const toggleId = (id) => {
|
|
405
|
+
setHighlightIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
|
|
406
|
+
};
|
|
407
|
+
const scrollToFirst = () => {
|
|
408
|
+
if (!highlightIds.length)
|
|
409
|
+
return;
|
|
410
|
+
// Re-setting the same array instance won't re-run the effect; nudge it.
|
|
411
|
+
setHighlightIds((prev) => [...prev]);
|
|
412
|
+
};
|
|
413
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium", children: ["Click IDs to highlight rows (uses", " ", _jsx("code", { className: "typography-mono", children: "highlightRowId" }), " + scroll-into-view). Highlight shares the selected token but with a softer overlay + outline."] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [rowsForDemo.map((row) => {
|
|
414
|
+
const active = highlightIds.includes(row.id);
|
|
415
|
+
return (_jsxs(Button, { size: "sm", variant: active ? "solid" : "outline", color: active ? "primary" : "secondary", onClick: () => toggleId(row.id), children: ["#", row.id] }, row.id));
|
|
416
|
+
}), _jsx(Button, { size: "sm", variant: "text", onClick: () => setHighlightIds([]), children: "Clear" }), _jsx(Button, { size: "sm", variant: "outline", onClick: scrollToFirst, disabled: !highlightIds.length, children: "Scroll to first" })] })] }), _jsx(DataTable, { className: "h-[320px] w-full", columns: [{ accessorKey: "id", header: "ID" }, ...projectColumns], data: rowsForDemo, highlightRowId: highlightIds, striped: true, divided: true, scrollToHighlightOnMouseLeave: true })] }));
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Censor-style list — large, virtualized table where `highlightRowId` moves
|
|
421
|
+
* automatically over time (e.g. matching the latest event / time).
|
|
422
|
+
*/
|
|
423
|
+
export const WithVirtualizedCensorTimeline = {
|
|
424
|
+
render: () => {
|
|
425
|
+
var _a, _b;
|
|
426
|
+
const rows = React.useMemo(() => {
|
|
427
|
+
const base = [];
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
for (let i = 0; i < 2000; i += 1) {
|
|
430
|
+
const t = new Date(now + i * 60000);
|
|
431
|
+
const hh = t.getHours().toString().padStart(2, "0");
|
|
432
|
+
const mm = t.getMinutes().toString().padStart(2, "0");
|
|
433
|
+
const status = i % 3 === 0 ? "To do" : i % 3 === 1 ? "In Progress" : "Completed";
|
|
434
|
+
base.push({
|
|
435
|
+
id: `censor-${i}`,
|
|
436
|
+
name: `Censor task #${i + 1}`,
|
|
437
|
+
type: (i % 3 === 0
|
|
438
|
+
? "Drone"
|
|
439
|
+
: i % 3 === 1
|
|
440
|
+
? "ROV"
|
|
441
|
+
: "AUV"),
|
|
442
|
+
subtype: "Automated check",
|
|
443
|
+
createdDate: `${hh}:${mm}`,
|
|
444
|
+
status,
|
|
445
|
+
time: `${hh}:${mm}`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return base;
|
|
449
|
+
}, []);
|
|
450
|
+
const [activeId, setActiveId] = React.useState((_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
if (!rows.length)
|
|
453
|
+
return undefined;
|
|
454
|
+
let index = 0;
|
|
455
|
+
const handle = setInterval(() => {
|
|
456
|
+
index = (index + 1) % rows.length;
|
|
457
|
+
setActiveId(rows[index].id);
|
|
458
|
+
}, 500);
|
|
459
|
+
return () => clearInterval(handle);
|
|
460
|
+
}, [rows]);
|
|
461
|
+
const censorColumns = React.useMemo(() => [
|
|
462
|
+
{ accessorKey: "time", header: "Time" },
|
|
463
|
+
...projectColumns,
|
|
464
|
+
], []);
|
|
465
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-3", children: [_jsxs("p", { className: "typography-body2 text-text-g-contrast-medium max-w-4xl", children: ["Virtualized censor list (~", rows.length, " rows). The active row moves every 0.5s via ", _jsx("code", { className: "typography-mono", children: "highlightRowId" }), " ", "to simulate matching incoming events over time."] }), _jsx(DataTable, { className: "h-[min(480px,70vh)] w-full", columns: censorColumns, data: rows, paginationMode: "infinite", virtualized: true, virtualRowEstimate: 52, highlightRowId: activeId !== null && activeId !== void 0 ? activeId : undefined, striped: true, divided: true, scrollToHighlightOnMouseLeave: true })] }));
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// 4. Row actions
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
/** Fixed actions column at the far right — rendered via `rowActions` prop. */
|
|
472
|
+
export const WithRowActions = {
|
|
473
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectData, striped: true, divided: true, rowActions: (row) => _jsx(ProjectRowActions, { row: row }) })),
|
|
474
|
+
};
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// 5. Expandable rows
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
/**
|
|
479
|
+
* Uncontrolled tree rows — `defaultExpanded={true}` opens all nodes on mount.
|
|
480
|
+
* The user can collapse/expand rows freely; state is managed internally.
|
|
481
|
+
*/
|
|
482
|
+
export const WithExpandableRows = {
|
|
483
|
+
render: () => (_jsx(DataTable, { columns: eventColumns, data: eventData, getSubRows: (row) => row.children, striped: true, divided: true, defaultExpanded: true, rowActions: (row) => (_jsx(ActionButton, { variant: "icon", size: "sm", onClick: () => console.log("menu", row.original), "aria-label": "More options", children: _jsx(EllipsisVertical, {}) })) })),
|
|
484
|
+
};
|
|
485
|
+
/**
|
|
486
|
+
* Controlled expand — the caller owns `expanded` state via `expanded` +
|
|
487
|
+
* `onExpandedChange`. Collapsing a row in the table calls `onExpandedChange`;
|
|
488
|
+
* if the parent doesn't update the state, the row "springs back" open.
|
|
489
|
+
*/
|
|
490
|
+
export const WithControlledExpand = {
|
|
491
|
+
render: () => {
|
|
492
|
+
const allIds = React.useMemo(() => eventData.reduce((acc, row) => {
|
|
493
|
+
var _a;
|
|
494
|
+
acc[row.id] = true;
|
|
495
|
+
(_a = row.children) === null || _a === void 0 ? void 0 : _a.forEach((c) => {
|
|
496
|
+
acc[c.id] = true;
|
|
497
|
+
});
|
|
498
|
+
return acc;
|
|
499
|
+
}, {}), []);
|
|
500
|
+
const [expanded, setExpanded] = React.useState({});
|
|
501
|
+
const allExpanded = Object.keys(allIds).every((id) => expanded[id]);
|
|
502
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 w-full h-full", children: [_jsxs("div", { className: "flex items-center gap-2 px-1", children: [_jsx("button", { type: "button", className: "px-3 py-1 rounded border border-white/20 typography-body3 text-text-contrast-max hover:bg-white/5", onClick: () => setExpanded(allExpanded ? {} : Object.assign({}, allIds)), children: allExpanded ? "Collapse all" : "Expand all" }), _jsxs("span", { className: "typography-small2 text-text-g-contrast-medium", children: ["Expanded: ", Object.keys(expanded).join(", ") || "–"] })] }), _jsx(DataTable, { columns: eventColumns, data: eventData, getSubRows: (row) => row.children, striped: true, divided: true, expanded: expanded, onExpandedChange: (next) => setExpanded(next) })] }));
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// 6. Column management
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
/**
|
|
509
|
+
* Column management panel — click ⋮ on any column header.
|
|
510
|
+
* - Toggle per-column visibility with the Switch
|
|
511
|
+
* - "Hide all" hides everything except one column
|
|
512
|
+
* - "Show all" restores all columns
|
|
513
|
+
* - Drag the ⠿ grip to reorder columns
|
|
514
|
+
*/
|
|
515
|
+
export const WithColumnManagement = {
|
|
516
|
+
render: () => (_jsx(DataTable, { columns: inspectionColumns, data: inspectionData, striped: true, divided: true, columnManagement: true, paginationMode: "client", pageSizeOptions: [5, 10] })),
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Column management with restricted options:
|
|
520
|
+
* - `reorder: false` — drag handle hidden, column order is fixed
|
|
521
|
+
* - `hideAll: false` — "Hide all" button removed
|
|
522
|
+
*
|
|
523
|
+
* Pass any subset of `ColumnManagementOptions` to tailor the panel.
|
|
524
|
+
*/
|
|
525
|
+
export const WithColumnManagementOptions = {
|
|
526
|
+
render: () => (_jsx(DataTable, { columns: inspectionColumns, data: inspectionData, striped: true, divided: true, columnManagement: { reorder: false, hideAll: false }, paginationMode: "client", pageSizeOptions: [5, 10] })),
|
|
527
|
+
};
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// 7. Column resize
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
/**
|
|
532
|
+
* Resizable columns — drag the right edge of any column header.
|
|
533
|
+
*
|
|
534
|
+
* Global bounds via `columnMinSize` / `columnMaxSize`.
|
|
535
|
+
* Per-column overrides in the column def (`minSize`, `maxSize`):
|
|
536
|
+
* - "Asset name": `minSize: 120`
|
|
537
|
+
* - "KPI (%)": `maxSize: 160`
|
|
538
|
+
*/
|
|
539
|
+
export const WithColumnResize = {
|
|
540
|
+
render: () => (_jsx(DataTable, { columns: [
|
|
541
|
+
{ accessorKey: "code", header: "Code", size: 100 },
|
|
542
|
+
{ accessorKey: "assetName", header: "Asset name", minSize: 120 },
|
|
543
|
+
{ accessorKey: "type", header: "Type", size: 90 },
|
|
544
|
+
{ accessorKey: "subtype", header: "Subtype" },
|
|
545
|
+
{
|
|
546
|
+
accessorKey: "severity",
|
|
547
|
+
header: "Severity",
|
|
548
|
+
size: 120,
|
|
549
|
+
cell: ({ row }) => (_jsx("span", { className: `inline-flex items-center px-2 py-0.5 rounded-lg typography-small2 ${severityCls[row.original.severity]}`, children: row.original.severity })),
|
|
550
|
+
},
|
|
551
|
+
{ accessorKey: "inspector", header: "Inspector" },
|
|
552
|
+
{ accessorKey: "location", header: "Location", size: 100 },
|
|
553
|
+
{ accessorKey: "capturedAt", header: "Captured date", size: 140 },
|
|
554
|
+
{
|
|
555
|
+
accessorKey: "status",
|
|
556
|
+
header: "Status",
|
|
557
|
+
size: 130,
|
|
558
|
+
cell: ({ row }) => (_jsx("span", { className: `inline-flex items-center px-3 py-1 rounded-lg typography-body3 ${inspectionStatusCls[row.original.status]}`, children: row.original.status })),
|
|
559
|
+
},
|
|
560
|
+
{ accessorKey: "kpi", header: "KPI (%)", size: 90, maxSize: 160 },
|
|
561
|
+
{ accessorKey: "notes", header: "Notes" },
|
|
562
|
+
], data: inspectionData, striped: true, divided: true, resizable: true, columnMinSize: 60, columnMaxSize: 400, paginationMode: "client", pageSizeOptions: [5, 10] })),
|
|
563
|
+
};
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// 8. Kitchen Sink
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
/**
|
|
568
|
+
* All features combined:
|
|
569
|
+
* - Checkbox selection
|
|
570
|
+
* - Sortable columns
|
|
571
|
+
* - Client-side pagination
|
|
572
|
+
* - Column management (visibility + reorder)
|
|
573
|
+
* - Column resize
|
|
574
|
+
* - Row actions
|
|
575
|
+
*/
|
|
576
|
+
export const KitchenSink = {
|
|
577
|
+
render: () => {
|
|
578
|
+
const [selected, setSelected] = React.useState({});
|
|
579
|
+
return (_jsx(DataTable, { columns: inspectionColumns, data: [...inspectionData, ...inspectionData], paginationMode: "client", pageSizeOptions: [5, 10], selectable: true, onRowSelectionChange: setSelected, striped: true, divided: true, resizable: true, columnMinSize: 60, columnManagement: true, onSorting: (s) => console.log("sort", s), rowActions: (row) => (_jsxs(_Fragment, { children: [_jsx(ActionButton, { variant: "icon", size: "sm", onClick: () => console.log("menu", row.original), "aria-label": "More options", children: _jsx(EllipsisVertical, {}) }), _jsx(ActionButton, { variant: "icon", size: "sm", onClick: () => console.log("open", row.original), "aria-label": "Open", children: _jsx(ChevronRight, {}) })] })) }));
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// 9. Row reorder
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
/**
|
|
586
|
+
* Drag-to-reorder rows — a grip handle is prepended automatically.
|
|
587
|
+
* `onRowReorder` fires with the new data array after a drop.
|
|
588
|
+
*/
|
|
589
|
+
export const WithRowReorder = {
|
|
590
|
+
render: () => {
|
|
591
|
+
const [rows, setRows] = React.useState(projectData);
|
|
592
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 w-full h-full", children: [_jsxs("p", { className: "typography-small2 text-text-g-contrast-high px-1", children: ["Order:", " ", _jsx("span", { className: "text-text-contrast-max font-medium", children: rows.map((r) => r.name.split(" ")[0]).join(", ") })] }), _jsx(DataTable, { tableLayout: "fixed", columns: projectColumns, data: rows, divided: true, reorderable: true, onRowReorder: setRows })] }));
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// 10. Row & cell click
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
/** Row click — clicking any row fires `onRowClick`. Rows show `cursor-pointer`. */
|
|
599
|
+
export const WithRowClick = {
|
|
600
|
+
render: () => {
|
|
601
|
+
const [clicked, setClicked] = React.useState(null);
|
|
602
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 w-full h-full", children: [_jsxs("p", { className: "typography-small2 text-text-g-contrast-high px-1", children: ["Last clicked row:", " ", _jsx("span", { className: "text-text-contrast-max font-medium", children: clicked !== null && clicked !== void 0 ? clicked : "–" })] }), _jsx(DataTable, { columns: projectColumns, data: projectData, striped: true, divided: true, onRowClick: (row) => setClicked(`${row.original.name} (id: ${row.original.id})`) })] }));
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* Cell click — `onCellClick` fires for individual cells.
|
|
607
|
+
* Use `event.stopPropagation()` to prevent `onRowClick` from also firing.
|
|
608
|
+
*/
|
|
609
|
+
export const WithCellClick = {
|
|
610
|
+
render: () => {
|
|
611
|
+
const [info, setInfo] = React.useState(null);
|
|
612
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 w-full h-full", children: [_jsxs("p", { className: "typography-small2 text-text-g-contrast-high px-1", children: ["Last clicked cell:", " ", _jsx("span", { className: "text-text-contrast-max font-medium", children: info ? `[${info.column}] ${info.value}` : "–" })] }), _jsx(DataTable, { columns: projectColumns, data: projectData, striped: true, divided: true, onRowClick: (row) => console.log("row", row.original.id), onCellClick: (cell, _row, e) => {
|
|
613
|
+
var _a;
|
|
614
|
+
e.stopPropagation();
|
|
615
|
+
setInfo({
|
|
616
|
+
column: cell.column.id,
|
|
617
|
+
value: String((_a = cell.getValue()) !== null && _a !== void 0 ? _a : ""),
|
|
618
|
+
});
|
|
619
|
+
} })] }));
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// 11. Sort indicator visibility
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
/**
|
|
626
|
+
* Sort indicator always visible — `sortIndicatorVisibility="always"` shows the
|
|
627
|
+
* sort icon on every sortable column, not just on hover.
|
|
628
|
+
*/
|
|
629
|
+
export const SortIndicatorAlwaysVisible = {
|
|
630
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectData, striped: true, divided: true, sortIndicatorVisibility: "always", onSorting: (s) => console.log("sort", s) })),
|
|
631
|
+
};
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
// 12. Table layout comparison
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
/* prettier-ignore */
|
|
636
|
+
const layoutCompareColumns = [
|
|
637
|
+
{ accessorKey: "name", header: "Project name", size: 280 },
|
|
638
|
+
{ accessorKey: "type", header: "Type", size: 100 },
|
|
639
|
+
{ accessorKey: "subtype", header: "Subtype" },
|
|
640
|
+
{ accessorKey: "createdDate", header: "Created date", size: 140 },
|
|
641
|
+
{ accessorKey: "status", header: "Status", size: 130, cell: ({ row }) => _jsx(StatusBadge, { status: row.original.status }) },
|
|
642
|
+
];
|
|
643
|
+
/**
|
|
644
|
+
* Side-by-side comparison of `tableLayout="auto"` vs `"fixed"` vs `"equal"`.
|
|
645
|
+
*
|
|
646
|
+
* - **auto** — browser distributes width based on cell content; `size` is ignored.
|
|
647
|
+
* - **fixed** — `size` is used as px per column; the last non-exact column grows
|
|
648
|
+
* so the table stays full-width (here: Status absorbs the remainder).
|
|
649
|
+
* - **equal** — all columns without `meta.exactWidth` share the remaining width equally.
|
|
650
|
+
* With `resizable`, drag handles are off for this row so equal widths are preserved.
|
|
651
|
+
*/
|
|
652
|
+
export const TableLayoutComparison = {
|
|
653
|
+
render: () => (_jsxs("div", { className: "flex flex-col gap-8 w-full h-full", children: [_jsxs("div", { className: "flex flex-col gap-2 flex-1 min-h-0", children: [_jsxs("p", { className: "typography-subtitle1 text-text-contrast-max px-1", children: ["tableLayout=\"auto\"", " ", _jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: "\u2014 browser decides widths from content (size ignored)" })] }), _jsx(DataTable, { columns: layoutCompareColumns, data: projectData, striped: true, divided: true, selectable: true, resizable: false, tableLayout: "auto" })] }), _jsxs("div", { className: "flex flex-col gap-2 flex-1 min-h-0", children: [_jsxs("p", { className: "typography-subtitle1 text-text-contrast-max px-1", children: ["tableLayout=\"fixed\"", " ", _jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: "\u2014 columns honour size (280 / 100 / default / 140 / 130)" })] }), _jsx(DataTable, { columns: layoutCompareColumns, data: projectData, striped: true, divided: true, selectable: true, resizable: false, tableLayout: "fixed" })] }), _jsxs("div", { className: "flex flex-col gap-2 flex-1 min-h-0", children: [_jsxs("p", { className: "typography-subtitle1 text-text-contrast-max px-1", children: ["tableLayout=\"equal\"", " ", _jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: "\u2014 non-exactWidth columns share remaining space equally" })] }), _jsx(DataTable, { columns: layoutCompareColumns, data: projectData, striped: true, divided: true, selectable: true, resizable: false, tableLayout: "equal" })] })] })),
|
|
654
|
+
};
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// 13. Custom row & cell className
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
/**
|
|
659
|
+
* `rowClassName` and `cellClassName` let you style individual rows and cells
|
|
660
|
+
* based on their data.
|
|
661
|
+
*
|
|
662
|
+
* - Rows with status **"Completed"** get a green-tinted background.
|
|
663
|
+
* - Rows with status **"To do"** get a subtle white/opacity background.
|
|
664
|
+
* - The **"Status"** column cells are bold.
|
|
665
|
+
* - The **"Type"** cells for ROV are highlighted with an info tint.
|
|
666
|
+
*/
|
|
667
|
+
export const WithCustomRowAndCellClassName = {
|
|
668
|
+
render: () => (_jsx(DataTable, { columns: projectColumns, data: projectData, divided: true, rowClassName: (row) => {
|
|
669
|
+
if (row.original.status === "Completed")
|
|
670
|
+
return "!bg-success-500/10";
|
|
671
|
+
if (row.original.status === "To do")
|
|
672
|
+
return "!bg-white/5";
|
|
673
|
+
return undefined;
|
|
674
|
+
}, cellClassName: (cell) => {
|
|
675
|
+
if (cell.column.id === "status")
|
|
676
|
+
return "font-semibold";
|
|
677
|
+
if (cell.column.id === "type" && cell.getValue() === "ROV")
|
|
678
|
+
return "!bg-info-500/10 text-info-400";
|
|
679
|
+
return undefined;
|
|
680
|
+
} })),
|
|
681
|
+
};
|
|
682
|
+
const DM_DATA_CATEGORY_LABELS = [
|
|
683
|
+
"Sequence",
|
|
684
|
+
"Date",
|
|
685
|
+
"Multiple choice",
|
|
686
|
+
"KP",
|
|
687
|
+
"Data",
|
|
688
|
+
];
|
|
689
|
+
const DM_DATA_TYPES_BY_CATEGORY = {
|
|
690
|
+
Sequence: ["Integer", "Text"],
|
|
691
|
+
Date: ["Text"],
|
|
692
|
+
"Multiple choice": ["Text"],
|
|
693
|
+
KP: ["Decimals", "Text"],
|
|
694
|
+
Data: ["Text", "Integer", "Decimals"],
|
|
695
|
+
};
|
|
696
|
+
const DM_FORMAT_BY_DATA_TYPE = {
|
|
697
|
+
Integer: ["-", "0", "Grouped"],
|
|
698
|
+
Text: ["-", "dd/mm/yyyy"],
|
|
699
|
+
Decimals: ["-", "5", "2"],
|
|
700
|
+
Date: ["-", "ISO"],
|
|
701
|
+
};
|
|
702
|
+
const DM_SOURCE_LABELS = ["-", "Q6", "Manual"];
|
|
703
|
+
function dmDepsParentsReady(o) {
|
|
704
|
+
return o.columnName.trim().length > 0 && o.dataCategory.trim().length > 0;
|
|
705
|
+
}
|
|
706
|
+
function dmDepsDataTypePicked(o) {
|
|
707
|
+
return o.dataType.trim().length > 0;
|
|
708
|
+
}
|
|
709
|
+
function dmDepsFormatPicked(o) {
|
|
710
|
+
return o.format.trim().length > 0;
|
|
711
|
+
}
|
|
712
|
+
function dmOptionsFromLabels(placeholderLabel, labels) {
|
|
713
|
+
return [
|
|
714
|
+
{ value: `__ph__:${placeholderLabel}`, label: placeholderLabel },
|
|
715
|
+
...Array.from(labels).map((l) => ({ value: l, label: l })),
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
const dmAddRow = {
|
|
719
|
+
id: "__dm_add__",
|
|
720
|
+
kind: "add",
|
|
721
|
+
columnName: "",
|
|
722
|
+
dataCategory: "",
|
|
723
|
+
dataType: "",
|
|
724
|
+
format: "",
|
|
725
|
+
source: "",
|
|
726
|
+
defaultValue: "",
|
|
727
|
+
};
|
|
728
|
+
function isDmAddRow(row) {
|
|
729
|
+
return row.original.kind === "add" || row.id === "__dm_add__";
|
|
730
|
+
}
|
|
731
|
+
/* prettier-ignore */
|
|
732
|
+
const dmData = [
|
|
733
|
+
{ id: "1", columnName: "Sequence", dataCategory: "Sequence", dataType: "Integer", format: "-", source: "-", defaultValue: "-" },
|
|
734
|
+
{ id: "2", columnName: "Date", dataCategory: "Date", dataType: "Text", format: "dd/mm/yyyy", source: "Q6", defaultValue: "-" },
|
|
735
|
+
{ id: "3", columnName: "Anomaly", dataCategory: "Multiple choice", dataType: "Text", format: "-", source: "-", defaultValue: "2 options" },
|
|
736
|
+
{ id: "4", columnName: "Data", dataCategory: "KP", dataType: "Decimals", format: "5", source: "Q6", defaultValue: "-" },
|
|
737
|
+
{ id: "5", columnName: "Data", dataCategory: "Data", dataType: "Text", format: "-", source: "-", defaultValue: "-" },
|
|
738
|
+
{ id: "6", columnName: "Data", dataCategory: "Data", dataType: "Text", format: "-", source: "-", defaultValue: "-" },
|
|
739
|
+
{ id: "7", columnName: "Data", dataCategory: "Data", dataType: "Text", format: "-", source: "-", defaultValue: "-" },
|
|
740
|
+
{ id: "8", columnName: "Data", dataCategory: "Data", dataType: "Text", format: "-", source: "-", defaultValue: "-" },
|
|
741
|
+
{ id: "9", columnName: "Data", dataCategory: "Data", dataType: "Text", format: "-", source: "-", defaultValue: "-" },
|
|
742
|
+
];
|
|
743
|
+
const DmToolbar = ({ count }) => (_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: "typography-h6 text-text-contrast-max", children: "Data management" }), _jsxs("span", { className: "typography-subtitle1 text-text-contrast-max", children: ["(", count, ")"] })] }), _jsxs("button", { type: "button", className: "flex items-center gap-1.5 rounded-md border border-state-primary-stroke px-3 py-1 typography-button-ms text-state-primary-text-outline", children: [_jsx(FileDown, { className: "size-4 shrink-0 opacity-90", "aria-hidden": true }), "Import file"] }), _jsxs("button", { type: "button", className: "flex items-center gap-1.5 rounded-md border border-state-primary-stroke px-3 py-1 typography-button-ms text-state-primary-text-outline", children: [_jsx(FileUp, { className: "size-4 shrink-0 opacity-90", "aria-hidden": true }), "Export file"] }), _jsx("button", { type: "button", className: "flex items-center gap-1.5 rounded-md bg-state-primary-default px-3 py-1 typography-button-ms text-state-primary-text-solid", children: "Save changes" })] }));
|
|
744
|
+
const dashPreview = (value) => !value || value === "-" ? (_jsx("span", { className: "text-text-g-contrast-medium", children: "-" })) : (_jsx("span", { children: value }));
|
|
745
|
+
const dmColumns = [
|
|
746
|
+
{
|
|
747
|
+
id: "#",
|
|
748
|
+
header: () => null,
|
|
749
|
+
cell: ({ row }) => (_jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: row.index + 1 })),
|
|
750
|
+
enableSorting: false,
|
|
751
|
+
enableHiding: false,
|
|
752
|
+
enableResizing: false,
|
|
753
|
+
size: 50,
|
|
754
|
+
maxSize: 50,
|
|
755
|
+
minSize: 50,
|
|
756
|
+
meta: { align: "center", exactWidth: 50 },
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
accessorKey: "columnName",
|
|
760
|
+
header: "Column name",
|
|
761
|
+
size: 200,
|
|
762
|
+
enableEditing: true,
|
|
763
|
+
editVariant: "text",
|
|
764
|
+
editTextProps: { placeholder: "Column name" },
|
|
765
|
+
onCommit: (_row, v) => ({
|
|
766
|
+
columnName: v,
|
|
767
|
+
dataType: "",
|
|
768
|
+
format: "",
|
|
769
|
+
source: "",
|
|
770
|
+
}),
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
accessorKey: "dataCategory",
|
|
774
|
+
header: "Data Category",
|
|
775
|
+
size: 200,
|
|
776
|
+
enableEditing: true,
|
|
777
|
+
editVariant: "select",
|
|
778
|
+
editSelectProps: {
|
|
779
|
+
options: () => dmOptionsFromLabels("Data Category", DM_DATA_CATEGORY_LABELS),
|
|
780
|
+
placeholder: "Data Category",
|
|
781
|
+
},
|
|
782
|
+
onCommit: (_row, v) => ({
|
|
783
|
+
dataCategory: v,
|
|
784
|
+
dataType: "",
|
|
785
|
+
format: "",
|
|
786
|
+
source: "",
|
|
787
|
+
}),
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
accessorKey: "dataType",
|
|
791
|
+
header: "Data type",
|
|
792
|
+
size: 150,
|
|
793
|
+
enableEditing: (row) => dmDepsParentsReady(row.original),
|
|
794
|
+
editVariant: "select",
|
|
795
|
+
// editShowDisabledField: true,
|
|
796
|
+
editSelectProps: {
|
|
797
|
+
options: (row) => {
|
|
798
|
+
var _a;
|
|
799
|
+
return dmOptionsFromLabels("Data type", (_a = DM_DATA_TYPES_BY_CATEGORY[row.original.dataCategory]) !== null && _a !== void 0 ? _a : ["Text"]);
|
|
800
|
+
},
|
|
801
|
+
placeholder: "Data type",
|
|
802
|
+
},
|
|
803
|
+
onCommit: (_row, v) => ({
|
|
804
|
+
dataType: v,
|
|
805
|
+
format: "",
|
|
806
|
+
source: "",
|
|
807
|
+
}),
|
|
808
|
+
displayCell: (row) => dashPreview(row.original.dataType),
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
accessorKey: "format",
|
|
812
|
+
header: "Format",
|
|
813
|
+
size: 150,
|
|
814
|
+
enableEditing: (row) => dmDepsParentsReady(row.original) && dmDepsDataTypePicked(row.original),
|
|
815
|
+
editVariant: "select",
|
|
816
|
+
// editShowDisabledField: true,
|
|
817
|
+
editSelectProps: {
|
|
818
|
+
options: (row) => {
|
|
819
|
+
var _a;
|
|
820
|
+
return dmOptionsFromLabels("Format", (_a = DM_FORMAT_BY_DATA_TYPE[row.original.dataType]) !== null && _a !== void 0 ? _a : ["-"]);
|
|
821
|
+
},
|
|
822
|
+
placeholder: "Format",
|
|
823
|
+
},
|
|
824
|
+
onCommit: (_row, v) => ({ format: v, source: "" }),
|
|
825
|
+
displayCell: (row) => dashPreview(row.original.format),
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
accessorKey: "source",
|
|
829
|
+
header: "Source",
|
|
830
|
+
size: 150,
|
|
831
|
+
enableEditing: (row) => dmDepsParentsReady(row.original) &&
|
|
832
|
+
dmDepsDataTypePicked(row.original) &&
|
|
833
|
+
dmDepsFormatPicked(row.original),
|
|
834
|
+
editVariant: "select",
|
|
835
|
+
// editShowDisabledField: true,
|
|
836
|
+
editSelectProps: {
|
|
837
|
+
options: () => dmOptionsFromLabels("Source", DM_SOURCE_LABELS),
|
|
838
|
+
placeholder: "Source",
|
|
839
|
+
},
|
|
840
|
+
displayCell: (row) => dashPreview(row.original.source),
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
accessorKey: "defaultValue",
|
|
844
|
+
header: "Default value",
|
|
845
|
+
size: 200,
|
|
846
|
+
enableSorting: false,
|
|
847
|
+
enableEditing: true,
|
|
848
|
+
editVariant: "text",
|
|
849
|
+
editTextProps: { placeholder: "Default value" },
|
|
850
|
+
onCommit: (_row, v) => ({ defaultValue: v || "-" }),
|
|
851
|
+
displayCell: (row) => {
|
|
852
|
+
const v = row.original.defaultValue;
|
|
853
|
+
return v === "-" ? (_jsx("span", { className: "text-text-g-contrast-medium", children: "-" })) : (_jsx("button", { type: "button", className: "typography-button-ms text-state-secondary-text-outline", children: v }));
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
id: "__actions__",
|
|
858
|
+
accessorFn: (row) => row.id,
|
|
859
|
+
header: () => null,
|
|
860
|
+
cell: ({ row }) => isDmAddRow(row) ? (_jsx(Button, { size: "sm", variant: "solid", color: "primary", children: "Add" })) : (_jsx(ActionButton, { variant: "icon", size: "sm", "aria-label": "Delete row", onClick: () => console.log("delete", row.original.id), children: _jsx(Trash2, { className: "size-4" }) })),
|
|
861
|
+
enableSorting: false,
|
|
862
|
+
enableHiding: false,
|
|
863
|
+
enableResizing: false,
|
|
864
|
+
size: 100,
|
|
865
|
+
maxSize: 100,
|
|
866
|
+
minSize: 100,
|
|
867
|
+
meta: { exactWidth: 100, align: "center" },
|
|
868
|
+
},
|
|
869
|
+
];
|
|
870
|
+
/**
|
|
871
|
+
* Data management table — Figma **Xspector-New**:
|
|
872
|
+
* [full frame](https://www.figma.com/design/99rq6FbfPx6hgPS0VCvHJh/Xspector-New?node-id=11965-17125),
|
|
873
|
+
* [row / field spec](https://www.figma.com/design/99rq6FbfPx6hgPS0VCvHJh/Xspector-New?node-id=11965-17883).
|
|
874
|
+
*
|
|
875
|
+
* Uses the **DataTable editing API** (`enableEditing` + `editDisplayMode` +
|
|
876
|
+
* `editVariant` per column). Edit scope and trigger are toggleable via the
|
|
877
|
+
* toolbar at the top of the story.
|
|
878
|
+
*/
|
|
879
|
+
export const DataManagement = {
|
|
880
|
+
render: () => {
|
|
881
|
+
const [rows, setRows] = React.useState(() => [
|
|
882
|
+
...dmData,
|
|
883
|
+
dmAddRow,
|
|
884
|
+
]);
|
|
885
|
+
const [editScope, setEditScope] = React.useState("row");
|
|
886
|
+
const [editTrigger, setEditTrigger] = React.useState("click");
|
|
887
|
+
const dataRowCount = rows.filter((r) => r.kind !== "add" && r.id !== "__dm_add__").length;
|
|
888
|
+
return (_jsxs("div", { "data-surface": "panel", className: "flex h-full min-h-0 w-full max-w-7xl flex-col gap-6 self-center rounded-xl bg-modal-surface p-8 shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)]", children: [_jsx(DmToolbar, { count: dataRowCount }), _jsxs("div", { className: "flex flex-wrap items-center gap-4 rounded-lg border border-table-c-col-line bg-table-c-header-bg px-4 py-3", children: [_jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: "Cell edit demo" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "dm-scope", checked: editScope === "row", onChange: () => setEditScope("row") }), "Whole row"] }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "dm-scope", checked: editScope === "cell", onChange: () => setEditScope("cell") }), "Single cell (blur \u2192 preview, Tab \u2192 next field in row)"] }), _jsx("span", { className: "mx-2 hidden h-4 w-px bg-table-c-col-line sm:inline-block" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "dm-trig", checked: editTrigger === "click", onChange: () => setEditTrigger("click") }), "Activate on click"] }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "dm-trig", checked: editTrigger === "doubleClick", onChange: () => setEditTrigger("doubleClick") }), "Activate on double-click"] })] }), _jsx(DataTable, { className: "min-h-0", columns: dmColumns, data: rows, divided: true, striped: false, surface: "panel", tableLayout: "equal", reorderable: true, getRowId: (r) => r.id, isRowReorderLocked: (row) => isDmAddRow(row), onRowReorder: (reordered) => {
|
|
889
|
+
var _a;
|
|
890
|
+
const add = (_a = reordered.find((r) => r.kind === "add" || r.id === "__dm_add__")) !== null && _a !== void 0 ? _a : dmAddRow;
|
|
891
|
+
const rest = reordered.filter((r) => r.kind !== "add" && r.id !== "__dm_add__");
|
|
892
|
+
setRows([...rest, add]);
|
|
893
|
+
}, enableEditing: true, editDisplayMode: editScope, editTrigger: editTrigger, alwaysEditing: (row) => isDmAddRow(row), onCellCommit: (rowId, _columnId, patch) => {
|
|
894
|
+
setRows((prev) => prev.map((r) => (r.id === rowId ? Object.assign(Object.assign({}, r), patch) : r)));
|
|
895
|
+
}, headerCellClassName: (header) => header.column.id === "__reorder__" ? "!border-r-0" : undefined, cellClassName: (cell, row) => cn(isDmAddRow(row) && "!py-2", cell.column.id === "__reorder__" && "!border-r-0") })] }));
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
/** Data Management — empty state when no columns have been configured yet. */
|
|
899
|
+
export const DataManagementEmpty = {
|
|
900
|
+
render: () => (_jsxs("div", { className: "flex flex-col gap-6 w-full h-full", children: [_jsx(DmToolbar, { count: 0 }), _jsx(DataTable, { columns: dmColumns, data: [], divided: true })] })),
|
|
901
|
+
};
|
|
902
|
+
const CATEGORY_OPTIONS = [
|
|
903
|
+
{ value: "electronics", label: "Electronics" },
|
|
904
|
+
{ value: "clothing", label: "Clothing" },
|
|
905
|
+
{ value: "food", label: "Food & Beverage" },
|
|
906
|
+
{ value: "furniture", label: "Furniture" },
|
|
907
|
+
{ value: "other", label: "Other" },
|
|
908
|
+
];
|
|
909
|
+
const PRIORITY_OPTIONS = [
|
|
910
|
+
{ value: "critical", label: "Critical" },
|
|
911
|
+
{ value: "high", label: "High" },
|
|
912
|
+
{ value: "medium", label: "Medium" },
|
|
913
|
+
{ value: "low", label: "Low" },
|
|
914
|
+
{ value: "none", label: "None" },
|
|
915
|
+
];
|
|
916
|
+
const PRIORITY_BADGE_COLOR = {
|
|
917
|
+
critical: "error",
|
|
918
|
+
high: "warning",
|
|
919
|
+
medium: "info",
|
|
920
|
+
low: "success",
|
|
921
|
+
none: "default",
|
|
922
|
+
};
|
|
923
|
+
/* prettier-ignore */
|
|
924
|
+
const editShowcaseData = [
|
|
925
|
+
{ id: "1", name: "Laptop Pro 16", category: "Electronics", quantity: 25, price: 1299.99, active: true, priority: "critical", notes: "Flagship model" },
|
|
926
|
+
{ id: "2", name: "Winter Jacket", category: "Clothing", quantity: 100, price: 89.50, active: true, priority: "medium", notes: "" },
|
|
927
|
+
{ id: "3", name: "Organic Coffee", category: "Food & Beverage", quantity: 500, price: 12.99, active: false, priority: "low", notes: "Fair trade certified" },
|
|
928
|
+
{ id: "4", name: "Standing Desk", category: "Furniture", quantity: 0, price: 449.00, active: true, priority: "high", notes: "Adjustable height" },
|
|
929
|
+
{ id: "5", name: "Wireless Mouse", category: "Electronics", quantity: 200, price: 29.99, active: true, priority: "none", notes: "" },
|
|
930
|
+
{ id: "6", name: "", category: "", quantity: 0, price: 0, active: false, priority: "none", notes: "Draft item" },
|
|
931
|
+
];
|
|
932
|
+
const editShowcaseColumns = [
|
|
933
|
+
{
|
|
934
|
+
id: "#",
|
|
935
|
+
header: "#",
|
|
936
|
+
cell: ({ row }) => (_jsx("span", { className: "typography-small2 text-text-g-contrast-medium", children: row.index + 1 })),
|
|
937
|
+
enableSorting: false,
|
|
938
|
+
size: 50,
|
|
939
|
+
meta: { exactWidth: 50, align: "center" },
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
accessorKey: "name",
|
|
943
|
+
header: "Product name",
|
|
944
|
+
size: 200,
|
|
945
|
+
enableEditing: true,
|
|
946
|
+
editVariant: "text",
|
|
947
|
+
editTextProps: { placeholder: "Product name" },
|
|
948
|
+
editError: (row) => {
|
|
949
|
+
if (!row.original.name.trim())
|
|
950
|
+
return "Name is required";
|
|
951
|
+
if (row.original.name.length < 3)
|
|
952
|
+
return "Min 3 characters";
|
|
953
|
+
return undefined;
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
accessorKey: "category",
|
|
958
|
+
header: "Category",
|
|
959
|
+
size: 180,
|
|
960
|
+
enableEditing: true,
|
|
961
|
+
editVariant: "select",
|
|
962
|
+
editSelectProps: {
|
|
963
|
+
options: CATEGORY_OPTIONS,
|
|
964
|
+
placeholder: "Select category",
|
|
965
|
+
},
|
|
966
|
+
editError: (row) => {
|
|
967
|
+
if (!row.original.category.trim())
|
|
968
|
+
return "Category is required";
|
|
969
|
+
return undefined;
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
accessorKey: "quantity",
|
|
974
|
+
header: "Quantity",
|
|
975
|
+
size: 130,
|
|
976
|
+
enableEditing: true,
|
|
977
|
+
editVariant: "number",
|
|
978
|
+
editNumberProps: {
|
|
979
|
+
placeholder: "Qty",
|
|
980
|
+
min: 0,
|
|
981
|
+
max: 99999,
|
|
982
|
+
step: 1,
|
|
983
|
+
allowDecimal: false,
|
|
984
|
+
allowNegative: false,
|
|
985
|
+
},
|
|
986
|
+
onCommit: (_row, v) => ({ quantity: Number(v) || 0 }),
|
|
987
|
+
editError: (row) => {
|
|
988
|
+
if (row.original.quantity < 0)
|
|
989
|
+
return "Cannot be negative";
|
|
990
|
+
return undefined;
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
accessorKey: "price",
|
|
995
|
+
header: "Price ($)",
|
|
996
|
+
size: 130,
|
|
997
|
+
enableEditing: true,
|
|
998
|
+
editVariant: "number",
|
|
999
|
+
editNumberProps: {
|
|
1000
|
+
placeholder: "0.00",
|
|
1001
|
+
min: 0,
|
|
1002
|
+
step: 0.01,
|
|
1003
|
+
precision: 2,
|
|
1004
|
+
allowDecimal: true,
|
|
1005
|
+
allowNegative: false,
|
|
1006
|
+
},
|
|
1007
|
+
onCommit: (_row, v) => ({ price: parseFloat(v) || 0 }),
|
|
1008
|
+
displayCell: (row) => (_jsxs("span", { className: "typography-small2 tabular-nums", children: ["$", Number(row.original.price).toFixed(2)] })),
|
|
1009
|
+
editError: (row) => {
|
|
1010
|
+
if (Number(row.original.price) <= 0)
|
|
1011
|
+
return "Price must be > 0";
|
|
1012
|
+
return undefined;
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
accessorKey: "active",
|
|
1017
|
+
header: "Active",
|
|
1018
|
+
size: 100,
|
|
1019
|
+
enableEditing: true,
|
|
1020
|
+
editVariant: "checkbox",
|
|
1021
|
+
editCheckboxProps: { label: "Active" },
|
|
1022
|
+
onCommit: (_row, v) => ({ active: v === "true" }),
|
|
1023
|
+
displayCell: (row) => (_jsx("span", { className: cn("inline-flex items-center px-2 py-0.5 rounded-lg typography-small2", row.original.active
|
|
1024
|
+
? "bg-success-500/20 text-success-400"
|
|
1025
|
+
: "bg-transparent-grey2-8 text-text-g-contrast-medium"), children: row.original.active ? "Yes" : "No" })),
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
accessorKey: "priority",
|
|
1029
|
+
header: "Priority",
|
|
1030
|
+
size: 200,
|
|
1031
|
+
enableEditing: true,
|
|
1032
|
+
editVariant: "custom",
|
|
1033
|
+
editCustomCell: (row, { commit, blur, onKeyDown }) => {
|
|
1034
|
+
const current = row.original.priority;
|
|
1035
|
+
const handleKey = (e) => {
|
|
1036
|
+
if (e.key === "Escape") {
|
|
1037
|
+
e.preventDefault();
|
|
1038
|
+
blur();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
|
|
1042
|
+
};
|
|
1043
|
+
return (_jsx("div", { className: "flex flex-wrap items-center gap-1", children: PRIORITY_OPTIONS.map((opt) => (_jsx("button", { type: "button", className: cn("rounded-md px-2 py-0.5 typography-small2 border transition-colors", current === opt.value
|
|
1044
|
+
? "border-primary-500 bg-primary-500/15 text-primary-400"
|
|
1045
|
+
: "border-transparent bg-transparent-grey2-8 text-text-g-contrast-medium hover:text-text-contrast-high hover:bg-transparent-grey2-12"), onClick: () => {
|
|
1046
|
+
commit(opt.value);
|
|
1047
|
+
blur();
|
|
1048
|
+
}, onKeyDown: handleKey, children: opt.label }, opt.value))) }));
|
|
1049
|
+
},
|
|
1050
|
+
onCommit: (_row, value) => ({
|
|
1051
|
+
priority: (PRIORITY_OPTIONS.find((o) => o.value === value)
|
|
1052
|
+
? value
|
|
1053
|
+
: "none"),
|
|
1054
|
+
}),
|
|
1055
|
+
displayCell: (row) => {
|
|
1056
|
+
var _a, _b, _c;
|
|
1057
|
+
const p = row.original.priority;
|
|
1058
|
+
const label = (_b = (_a = PRIORITY_OPTIONS.find((o) => o.value === p)) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : p;
|
|
1059
|
+
return (_jsx(Badge, { label: label, color: (_c = PRIORITY_BADGE_COLOR[p]) !== null && _c !== void 0 ? _c : "default" }));
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
accessorKey: "notes",
|
|
1064
|
+
header: "Notes",
|
|
1065
|
+
size: 200,
|
|
1066
|
+
enableEditing: true,
|
|
1067
|
+
editVariant: "text",
|
|
1068
|
+
editTextProps: { placeholder: "Add notes…" },
|
|
1069
|
+
displayCell: (row) => row.original.notes ? (_jsx("span", { children: row.original.notes })) : (_jsx("span", { className: "text-text-g-contrast-medium italic", children: "\u2014" })),
|
|
1070
|
+
},
|
|
1071
|
+
];
|
|
1072
|
+
/**
|
|
1073
|
+
* **Editing Field Showcase** — demonstrates every editing feature:
|
|
1074
|
+
*
|
|
1075
|
+
* | Feature | Where |
|
|
1076
|
+
* |---|---|
|
|
1077
|
+
* | `editVariant: "text"` | Product name, Notes |
|
|
1078
|
+
* | `editVariant: "select"` | Category |
|
|
1079
|
+
* | `editVariant: "number"` | Quantity, Price |
|
|
1080
|
+
* | `editVariant: "checkbox"` | Active |
|
|
1081
|
+
* | `editVariant: "custom"` + `editCustomCell` | Priority (inline button picker → Badge display) |
|
|
1082
|
+
* | `editError` | Name (required + min length), Category (required), Price (> 0) |
|
|
1083
|
+
* | `displayCell` | Price (formatted), Active (badge), Notes (italic placeholder) |
|
|
1084
|
+
* | `testId` | Root container has `data-testid="edit-showcase"` |
|
|
1085
|
+
* | Row / Cell mode toggle | Toolbar radio buttons |
|
|
1086
|
+
* | Click / Double-click trigger | Toolbar radio buttons |
|
|
1087
|
+
* | Tab traversal | Tab/Shift+Tab moves between editable fields |
|
|
1088
|
+
*/
|
|
1089
|
+
export const EditingFieldShowcase = {
|
|
1090
|
+
render: () => {
|
|
1091
|
+
const [rows, setRows] = React.useState(() => editShowcaseData);
|
|
1092
|
+
const [editScope, setEditScope] = React.useState("row");
|
|
1093
|
+
const [editTrigger, setEditTrigger] = React.useState("click");
|
|
1094
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("h2", { className: "typography-h6 text-text-contrast-max", children: "Editing Field Showcase" }), _jsxs("p", { className: "typography-body3 text-text-g-contrast-medium max-w-3xl", children: ["All built-in edit variants: ", _jsx("strong", { children: "text" }), ",", " ", _jsx("strong", { children: "select" }), ", ", _jsx("strong", { children: "number" }), ",", " ", _jsx("strong", { children: "checkbox" }), ", and a ", _jsx("strong", { children: "custom" }), " field (Priority \u2014 inline button picker). Inline validation errors appear on Name, Category, and Price. Uses", " ", _jsx("code", { className: "typography-mono", children: "testId=\"edit-showcase\"" }), " ", "for automated testing."] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-4 rounded-lg border border-table-c-col-line bg-table-c-header-bg px-4 py-3", children: [_jsx("span", { className: "typography-small2 text-text-g-contrast-medium font-medium", children: "Edit mode" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "showcase-scope", checked: editScope === "row", onChange: () => setEditScope("row") }), "Row"] }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "showcase-scope", checked: editScope === "cell", onChange: () => setEditScope("cell") }), "Cell"] }), _jsx("span", { className: "mx-2 hidden h-4 w-px bg-table-c-col-line sm:inline-block" }), _jsx("span", { className: "typography-small2 text-text-g-contrast-medium font-medium", children: "Trigger" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "showcase-trig", checked: editTrigger === "click", onChange: () => setEditTrigger("click") }), "Click"] }), _jsxs("label", { className: "flex cursor-pointer items-center gap-2 typography-small2 text-text-contrast-high", children: [_jsx("input", { type: "radio", name: "showcase-trig", checked: editTrigger === "doubleClick", onChange: () => setEditTrigger("doubleClick") }), "Double-click"] })] }), _jsx(DataTable, { testId: "edit-showcase", className: "min-h-0", columns: editShowcaseColumns, data: rows, divided: true, striped: false, bordered: true, tableLayout: "equal", enableEditing: true, editDisplayMode: editScope, editTrigger: editTrigger, onCellCommit: (rowId, _colId, patch) => {
|
|
1095
|
+
setRows((prev) => prev.map((r) => (r.id === rowId ? Object.assign(Object.assign({}, r), patch) : r)));
|
|
1096
|
+
}, rowActions: (row) => (_jsx(ActionButton, { variant: "icon", size: "sm", "aria-label": "Delete row", onClick: () => setRows((prev) => prev.filter((r) => r.id !== row.original.id)), children: _jsx(Trash2, { className: "size-4" }) })) })] }));
|
|
45
1097
|
},
|
|
46
1098
|
};
|