@fragments-sdk/ui 0.9.4 → 0.9.6
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/assets/ui.css +443 -247
- package/dist/blocks/components/index.d.ts +0 -2
- package/dist/blocks/components/index.d.ts.map +1 -1
- package/dist/codeblock.cjs +187 -184
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +183 -180
- package/dist/codeblock.js.map +1 -1
- package/dist/components/Box/Box.module.scss.cjs +73 -0
- package/dist/components/Box/Box.module.scss.cjs.map +1 -1
- package/dist/components/Box/Box.module.scss.js +73 -0
- package/dist/components/Box/Box.module.scss.js.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/index.d.ts +11 -7
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +15 -15
- package/dist/components/Combobox/Combobox.module.scss.js +15 -15
- package/dist/components/DataTable/DataTable.module.scss.cjs +84 -0
- package/dist/components/DataTable/DataTable.module.scss.cjs.map +1 -0
- package/dist/components/DataTable/DataTable.module.scss.js +84 -0
- package/dist/components/DataTable/DataTable.module.scss.js.map +1 -0
- package/dist/components/DataTable/index.cjs +383 -0
- package/dist/components/DataTable/index.cjs.map +1 -0
- package/dist/components/DataTable/index.d.ts +78 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +366 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs +9 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.module.scss.js +9 -0
- package/dist/components/Drawer/Drawer.module.scss.js.map +1 -1
- package/dist/components/Image/Image.module.scss.cjs +12 -0
- package/dist/components/Image/Image.module.scss.cjs.map +1 -1
- package/dist/components/Image/Image.module.scss.js +12 -0
- package/dist/components/Image/Image.module.scss.js.map +1 -1
- package/dist/components/Link/Link.module.scss.cjs +3 -0
- package/dist/components/Link/Link.module.scss.cjs.map +1 -1
- package/dist/components/Link/Link.module.scss.js +3 -0
- package/dist/components/Link/Link.module.scss.js.map +1 -1
- package/dist/components/List/List.module.scss.cjs +5 -0
- package/dist/components/List/List.module.scss.cjs.map +1 -1
- package/dist/components/List/List.module.scss.js +5 -0
- package/dist/components/List/List.module.scss.js.map +1 -1
- package/dist/components/Loading/Loading.module.scss.cjs +5 -0
- package/dist/components/Loading/Loading.module.scss.cjs.map +1 -1
- package/dist/components/Loading/Loading.module.scss.js +5 -0
- package/dist/components/Loading/Loading.module.scss.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Message/Message.module.scss.cjs +22 -16
- package/dist/components/Message/Message.module.scss.cjs.map +1 -1
- package/dist/components/Message/Message.module.scss.js +22 -16
- package/dist/components/Message/Message.module.scss.js.map +1 -1
- package/dist/components/Message/index.cjs +5 -3
- package/dist/components/Message/index.cjs.map +1 -1
- package/dist/components/Message/index.d.ts +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -3
- package/dist/components/Message/index.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.cjs +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.cjs.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.js +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.js.map +1 -1
- package/dist/components/Stack/Stack.module.scss.cjs +14 -0
- package/dist/components/Stack/Stack.module.scss.cjs.map +1 -1
- package/dist/components/Stack/Stack.module.scss.js +14 -0
- package/dist/components/Stack/Stack.module.scss.js.map +1 -1
- package/dist/components/Table/Table.module.scss.cjs +21 -36
- package/dist/components/Table/Table.module.scss.cjs.map +1 -1
- package/dist/components/Table/Table.module.scss.js +21 -36
- package/dist/components/Table/Table.module.scss.js.map +1 -1
- package/dist/components/Table/index.d.ts +35 -55
- package/dist/components/Table/index.d.ts.map +1 -1
- package/dist/components/Text/Text.module.scss.cjs +14 -0
- package/dist/components/Text/Text.module.scss.cjs.map +1 -1
- package/dist/components/Text/Text.module.scss.js +14 -0
- package/dist/components/Text/Text.module.scss.js.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.cjs +4 -0
- package/dist/components/Textarea/Textarea.module.scss.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.js +4 -0
- package/dist/components/Textarea/Textarea.module.scss.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js.map +1 -1
- package/dist/index.cjs +119 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/table.cjs +44 -262
- package/dist/table.cjs.map +1 -1
- package/dist/table.js +47 -248
- package/dist/table.js.map +1 -1
- package/fragments.json +1 -1
- package/package.json +110 -118
- package/src/blocks/components/index.ts +0 -3
- package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
- package/src/components/CodeBlock/index.tsx +351 -345
- package/src/components/Combobox/Combobox.module.scss +13 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
- package/src/components/DataTable/DataTable.fragment.tsx +754 -0
- package/src/components/DataTable/DataTable.module.scss +300 -0
- package/src/components/DataTable/DataTable.test.tsx +224 -0
- package/src/components/DataTable/index.tsx +533 -0
- package/src/components/Message/Message.fragment.tsx +34 -0
- package/src/components/Message/Message.module.scss +11 -0
- package/src/components/Message/index.tsx +12 -3
- package/src/components/Table/Table.fragment.tsx +190 -175
- package/src/components/Table/Table.module.scss +15 -88
- package/src/components/Table/Table.test.tsx +184 -94
- package/src/components/Table/index.tsx +105 -374
- package/src/index.ts +15 -4
- package/src/tokens/_computed.scss +7 -6
- package/src/tokens/_density.scss +87 -47
- package/src/tokens/_variables.scss +46 -31
- package/dist/blocks/components/DataTable.d.ts +0 -19
- package/dist/blocks/components/DataTable.d.ts.map +0 -1
- package/src/blocks/components/DataTable.tsx +0 -124
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { defineFragment } from '@fragments-sdk/cli/core';
|
|
3
|
+
import { DataTable, createColumns } from '.';
|
|
4
|
+
import { Badge } from '../Badge';
|
|
5
|
+
import { Avatar } from '../Avatar';
|
|
6
|
+
import { Text } from '../Text';
|
|
7
|
+
import { Stack } from '../Stack';
|
|
8
|
+
import { Button } from '../Button';
|
|
9
|
+
import { Menu } from '../Menu';
|
|
10
|
+
import { Input } from '../Input';
|
|
11
|
+
|
|
12
|
+
// ─── Sample Data ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
interface User {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
email: string;
|
|
18
|
+
status: 'active' | 'inactive' | 'pending';
|
|
19
|
+
role: string;
|
|
20
|
+
initials: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sampleUsers: User[] = [
|
|
24
|
+
{ id: '1', name: 'Alice Johnson', email: 'alice@example.com', status: 'active', role: 'Admin', initials: 'AJ' },
|
|
25
|
+
{ id: '2', name: 'Bob Smith', email: 'bob@example.com', status: 'active', role: 'Editor', initials: 'BS' },
|
|
26
|
+
{ id: '3', name: 'Carol Williams', email: 'carol@example.com', status: 'pending', role: 'Viewer', initials: 'CW' },
|
|
27
|
+
{ id: '4', name: 'David Brown', email: 'david@example.com', status: 'inactive', role: 'Editor', initials: 'DB' },
|
|
28
|
+
{ id: '5', name: 'Eva Martinez', email: 'eva@example.com', status: 'active', role: 'Admin', initials: 'EM' },
|
|
29
|
+
{ id: '6', name: 'Frank Lee', email: 'frank@example.com', status: 'active', role: 'Viewer', initials: 'FL' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
interface Transaction {
|
|
33
|
+
id: string;
|
|
34
|
+
description: string;
|
|
35
|
+
category: string;
|
|
36
|
+
amount: number;
|
|
37
|
+
date: string;
|
|
38
|
+
status: 'completed' | 'pending' | 'failed';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sampleTransactions: Transaction[] = [
|
|
42
|
+
{ id: '1', description: 'Subscription renewal', category: 'Software', amount: 29.99, date: 'Feb 15, 2026', status: 'completed' },
|
|
43
|
+
{ id: '2', description: 'Cloud hosting', category: 'Infrastructure', amount: 149.00, date: 'Feb 14, 2026', status: 'completed' },
|
|
44
|
+
{ id: '3', description: 'Domain transfer', category: 'Infrastructure', amount: 12.50, date: 'Feb 13, 2026', status: 'pending' },
|
|
45
|
+
{ id: '4', description: 'API credits', category: 'Software', amount: 75.00, date: 'Feb 12, 2026', status: 'failed' },
|
|
46
|
+
{ id: '5', description: 'SSL certificate', category: 'Security', amount: 49.99, date: 'Feb 11, 2026', status: 'completed' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
interface ApiEndpoint {
|
|
50
|
+
id: string;
|
|
51
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
52
|
+
path: string;
|
|
53
|
+
description: string;
|
|
54
|
+
latency: string;
|
|
55
|
+
calls: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sampleEndpoints: ApiEndpoint[] = [
|
|
59
|
+
{ id: '1', method: 'GET', path: '/api/users', description: 'List all users', latency: '45ms', calls: 12450 },
|
|
60
|
+
{ id: '2', method: 'POST', path: '/api/users', description: 'Create user', latency: '120ms', calls: 3200 },
|
|
61
|
+
{ id: '3', method: 'GET', path: '/api/users/:id', description: 'Get user by ID', latency: '32ms', calls: 8900 },
|
|
62
|
+
{ id: '4', method: 'PUT', path: '/api/users/:id', description: 'Update user', latency: '95ms', calls: 1560 },
|
|
63
|
+
{ id: '5', method: 'DELETE', path: '/api/users/:id', description: 'Delete user', latency: '58ms', calls: 420 },
|
|
64
|
+
{ id: '6', method: 'GET', path: '/api/analytics', description: 'Analytics data', latency: '210ms', calls: 6700 },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
interface FileNode {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
type: 'folder' | 'file';
|
|
71
|
+
size?: string;
|
|
72
|
+
modified: string;
|
|
73
|
+
subRows?: FileNode[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fileTreeData: FileNode[] = [
|
|
77
|
+
{
|
|
78
|
+
id: '1', name: 'src', type: 'folder', modified: 'Feb 18, 2026',
|
|
79
|
+
subRows: [
|
|
80
|
+
{
|
|
81
|
+
id: '1.1', name: 'components', type: 'folder', modified: 'Feb 18, 2026',
|
|
82
|
+
subRows: [
|
|
83
|
+
{ id: '1.1.1', name: 'Button.tsx', type: 'file', size: '4.2 KB', modified: 'Feb 17, 2026' },
|
|
84
|
+
{ id: '1.1.2', name: 'Card.tsx', type: 'file', size: '3.8 KB', modified: 'Feb 16, 2026' },
|
|
85
|
+
{ id: '1.1.3', name: 'DataTable.tsx', type: 'file', size: '8.1 KB', modified: 'Feb 18, 2026' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: '1.2', name: 'utils', type: 'folder', modified: 'Feb 15, 2026',
|
|
90
|
+
subRows: [
|
|
91
|
+
{ id: '1.2.1', name: 'helpers.ts', type: 'file', size: '1.5 KB', modified: 'Feb 15, 2026' },
|
|
92
|
+
{ id: '1.2.2', name: 'constants.ts', type: 'file', size: '0.8 KB', modified: 'Feb 14, 2026' },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{ id: '1.3', name: 'index.ts', type: 'file', size: '0.5 KB', modified: 'Feb 18, 2026' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: '2', name: 'tests', type: 'folder', modified: 'Feb 17, 2026',
|
|
100
|
+
subRows: [
|
|
101
|
+
{ id: '2.1', name: 'Button.test.tsx', type: 'file', size: '2.1 KB', modified: 'Feb 17, 2026' },
|
|
102
|
+
{ id: '2.2', name: 'Card.test.tsx', type: 'file', size: '1.9 KB', modified: 'Feb 16, 2026' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{ id: '3', name: 'package.json', type: 'file', size: '1.2 KB', modified: 'Feb 18, 2026' },
|
|
106
|
+
{ id: '4', name: 'tsconfig.json', type: 'file', size: '0.4 KB', modified: 'Feb 10, 2026' },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// ─── Column Definitions ─────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const basicColumns = createColumns<User>([
|
|
112
|
+
{ key: 'name', header: 'Name' },
|
|
113
|
+
{ key: 'email', header: 'Email' },
|
|
114
|
+
{
|
|
115
|
+
key: 'status',
|
|
116
|
+
header: 'Status',
|
|
117
|
+
cell: (row) => (
|
|
118
|
+
<Badge
|
|
119
|
+
variant={row.status === 'active' ? 'success' : row.status === 'pending' ? 'warning' : 'default'}
|
|
120
|
+
dot
|
|
121
|
+
>
|
|
122
|
+
{row.status}
|
|
123
|
+
</Badge>
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
{ key: 'role', header: 'Role' },
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const richColumns = createColumns<User>([
|
|
130
|
+
{
|
|
131
|
+
key: 'name',
|
|
132
|
+
header: 'Member',
|
|
133
|
+
width: 240,
|
|
134
|
+
cell: (row) => (
|
|
135
|
+
<Stack direction="row" gap="sm" align="center">
|
|
136
|
+
<Avatar size="sm" initials={row.initials} />
|
|
137
|
+
<Stack gap="xs">
|
|
138
|
+
<Text size="sm" weight="medium">{row.name}</Text>
|
|
139
|
+
<Text size="xs" color="tertiary">{row.email}</Text>
|
|
140
|
+
</Stack>
|
|
141
|
+
</Stack>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'status',
|
|
146
|
+
header: 'Status',
|
|
147
|
+
width: 120,
|
|
148
|
+
cell: (row) => (
|
|
149
|
+
<Badge
|
|
150
|
+
variant={row.status === 'active' ? 'success' : row.status === 'pending' ? 'warning' : 'default'}
|
|
151
|
+
dot
|
|
152
|
+
>
|
|
153
|
+
{row.status}
|
|
154
|
+
</Badge>
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'role',
|
|
159
|
+
header: 'Role',
|
|
160
|
+
width: 100,
|
|
161
|
+
cell: (row) => <Badge variant="outline" size="sm">{row.role}</Badge>,
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const transactionColumns = createColumns<Transaction>([
|
|
166
|
+
{
|
|
167
|
+
key: 'description',
|
|
168
|
+
header: 'Description',
|
|
169
|
+
cell: (row) => (
|
|
170
|
+
<Stack gap="xs">
|
|
171
|
+
<Text size="sm" weight="medium">{row.description}</Text>
|
|
172
|
+
<Text size="xs" color="tertiary">{row.category}</Text>
|
|
173
|
+
</Stack>
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'amount',
|
|
178
|
+
header: 'Amount',
|
|
179
|
+
width: 100,
|
|
180
|
+
cell: (row) => (
|
|
181
|
+
<Text size="sm" weight="medium" font="mono">
|
|
182
|
+
${row.amount.toFixed(2)}
|
|
183
|
+
</Text>
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: 'date',
|
|
188
|
+
header: 'Date',
|
|
189
|
+
width: 130,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'status',
|
|
193
|
+
header: 'Status',
|
|
194
|
+
width: 120,
|
|
195
|
+
cell: (row) => (
|
|
196
|
+
<Badge
|
|
197
|
+
variant={row.status === 'completed' ? 'success' : row.status === 'pending' ? 'warning' : 'error'}
|
|
198
|
+
size="sm"
|
|
199
|
+
>
|
|
200
|
+
{row.status}
|
|
201
|
+
</Badge>
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
const methodColors: Record<string, 'success' | 'info' | 'warning' | 'error'> = {
|
|
207
|
+
GET: 'success',
|
|
208
|
+
POST: 'info',
|
|
209
|
+
PUT: 'warning',
|
|
210
|
+
DELETE: 'error',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const endpointColumns = createColumns<ApiEndpoint>([
|
|
214
|
+
{
|
|
215
|
+
key: 'method',
|
|
216
|
+
header: 'Method',
|
|
217
|
+
width: 90,
|
|
218
|
+
cell: (row) => (
|
|
219
|
+
<Badge variant={methodColors[row.method]} size="sm">
|
|
220
|
+
{row.method}
|
|
221
|
+
</Badge>
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: 'path',
|
|
226
|
+
header: 'Endpoint',
|
|
227
|
+
cell: (row) => (
|
|
228
|
+
<Stack gap="xs">
|
|
229
|
+
<Text size="sm" weight="medium" font="mono">{row.path}</Text>
|
|
230
|
+
<Text size="xs" color="tertiary">{row.description}</Text>
|
|
231
|
+
</Stack>
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
key: 'latency',
|
|
236
|
+
header: 'Latency',
|
|
237
|
+
width: 80,
|
|
238
|
+
cell: (row) => <Text size="sm" font="mono" color="secondary">{row.latency}</Text>,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: 'calls',
|
|
242
|
+
header: 'Calls (24h)',
|
|
243
|
+
width: 100,
|
|
244
|
+
cell: (row) => <Text size="sm" font="mono">{row.calls.toLocaleString()}</Text>,
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const fileColumns = createColumns<FileNode>([
|
|
249
|
+
{
|
|
250
|
+
key: 'name',
|
|
251
|
+
header: 'Name',
|
|
252
|
+
cell: (row) => (
|
|
253
|
+
<Stack direction="row" gap="xs" align="center">
|
|
254
|
+
<Text size="sm" color="tertiary">{row.type === 'folder' ? '\uD83D\uDCC1' : '\uD83D\uDCC4'}</Text>
|
|
255
|
+
<Text size="sm" weight={row.type === 'folder' ? 'medium' : 'normal'}>{row.name}</Text>
|
|
256
|
+
</Stack>
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
key: 'size',
|
|
261
|
+
header: 'Size',
|
|
262
|
+
width: 80,
|
|
263
|
+
cell: (row) => <Text size="sm" color="secondary" font="mono">{row.size ?? '\u2014'}</Text>,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
key: 'modified',
|
|
267
|
+
header: 'Modified',
|
|
268
|
+
width: 130,
|
|
269
|
+
},
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
// ─── Interactive Variants ───────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
function CheckboxSelectionExample() {
|
|
275
|
+
const [selection, setSelection] = useState({});
|
|
276
|
+
const selectedCount = Object.keys(selection).filter((k) => selection[k as keyof typeof selection]).length;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<Stack gap="sm">
|
|
280
|
+
<Stack direction="row" justify="between" align="center">
|
|
281
|
+
<Text size="sm" color="secondary">
|
|
282
|
+
{selectedCount > 0 ? `${selectedCount} selected` : 'Select rows with checkboxes'}
|
|
283
|
+
</Text>
|
|
284
|
+
{selectedCount > 0 && (
|
|
285
|
+
<Button size="sm" variant="ghost" onClick={() => setSelection({})}>
|
|
286
|
+
Clear
|
|
287
|
+
</Button>
|
|
288
|
+
)}
|
|
289
|
+
</Stack>
|
|
290
|
+
<DataTable
|
|
291
|
+
columns={richColumns}
|
|
292
|
+
data={sampleUsers}
|
|
293
|
+
selectable
|
|
294
|
+
showCheckbox
|
|
295
|
+
rowSelection={selection}
|
|
296
|
+
onRowSelectionChange={setSelection as any}
|
|
297
|
+
getRowId={(row) => row.id}
|
|
298
|
+
bordered
|
|
299
|
+
aria-label="Team members with checkbox selection"
|
|
300
|
+
/>
|
|
301
|
+
</Stack>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ExpandableRowsExample() {
|
|
306
|
+
return (
|
|
307
|
+
<DataTable
|
|
308
|
+
columns={fileColumns}
|
|
309
|
+
data={fileTreeData}
|
|
310
|
+
getSubRows={(row) => row.subRows}
|
|
311
|
+
getRowId={(row) => row.id}
|
|
312
|
+
bordered
|
|
313
|
+
size="sm"
|
|
314
|
+
aria-label="File tree"
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function FilteredTableExample() {
|
|
320
|
+
const [search, setSearch] = useState('');
|
|
321
|
+
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
|
322
|
+
const [roleFilter, setRoleFilter] = useState<string | null>(null);
|
|
323
|
+
|
|
324
|
+
const filteredData = useMemo(() => {
|
|
325
|
+
return sampleUsers.filter((user) => {
|
|
326
|
+
if (search && !user.name.toLowerCase().includes(search.toLowerCase()) && !user.email.toLowerCase().includes(search.toLowerCase())) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (statusFilter && user.status !== statusFilter) return false;
|
|
330
|
+
if (roleFilter && user.role !== roleFilter) return false;
|
|
331
|
+
return true;
|
|
332
|
+
});
|
|
333
|
+
}, [search, statusFilter, roleFilter]);
|
|
334
|
+
|
|
335
|
+
const activeFilters = [statusFilter, roleFilter].filter(Boolean).length;
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<Stack gap="sm">
|
|
339
|
+
<Stack direction="row" gap="sm" align="center">
|
|
340
|
+
<div style={{ flex: 1 }}>
|
|
341
|
+
<Input
|
|
342
|
+
placeholder="Search by name or email..."
|
|
343
|
+
size="sm"
|
|
344
|
+
value={search}
|
|
345
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
<Menu>
|
|
349
|
+
<Menu.Trigger asChild>
|
|
350
|
+
<Button variant="secondary" size="sm">
|
|
351
|
+
Status {statusFilter && <Badge size="sm" variant="info">{statusFilter}</Badge>}
|
|
352
|
+
</Button>
|
|
353
|
+
</Menu.Trigger>
|
|
354
|
+
<Menu.Content>
|
|
355
|
+
<Menu.Item onClick={() => setStatusFilter(null)}>All statuses</Menu.Item>
|
|
356
|
+
<Menu.Item onClick={() => setStatusFilter('active')}>Active</Menu.Item>
|
|
357
|
+
<Menu.Item onClick={() => setStatusFilter('pending')}>Pending</Menu.Item>
|
|
358
|
+
<Menu.Item onClick={() => setStatusFilter('inactive')}>Inactive</Menu.Item>
|
|
359
|
+
</Menu.Content>
|
|
360
|
+
</Menu>
|
|
361
|
+
<Menu>
|
|
362
|
+
<Menu.Trigger asChild>
|
|
363
|
+
<Button variant="secondary" size="sm">
|
|
364
|
+
Role {roleFilter && <Badge size="sm" variant="info">{roleFilter}</Badge>}
|
|
365
|
+
</Button>
|
|
366
|
+
</Menu.Trigger>
|
|
367
|
+
<Menu.Content>
|
|
368
|
+
<Menu.Item onClick={() => setRoleFilter(null)}>All roles</Menu.Item>
|
|
369
|
+
<Menu.Item onClick={() => setRoleFilter('Admin')}>Admin</Menu.Item>
|
|
370
|
+
<Menu.Item onClick={() => setRoleFilter('Editor')}>Editor</Menu.Item>
|
|
371
|
+
<Menu.Item onClick={() => setRoleFilter('Viewer')}>Viewer</Menu.Item>
|
|
372
|
+
</Menu.Content>
|
|
373
|
+
</Menu>
|
|
374
|
+
{activeFilters > 0 && (
|
|
375
|
+
<Button variant="ghost" size="sm" onClick={() => { setStatusFilter(null); setRoleFilter(null); setSearch(''); }}>
|
|
376
|
+
Clear all
|
|
377
|
+
</Button>
|
|
378
|
+
)}
|
|
379
|
+
</Stack>
|
|
380
|
+
<DataTable
|
|
381
|
+
columns={richColumns}
|
|
382
|
+
data={filteredData}
|
|
383
|
+
sortable
|
|
384
|
+
bordered
|
|
385
|
+
getRowId={(row) => row.id}
|
|
386
|
+
emptyMessage="No users match the current filters"
|
|
387
|
+
aria-label="Filtered team members"
|
|
388
|
+
/>
|
|
389
|
+
</Stack>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Fragment Definition ────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
export default defineFragment({
|
|
396
|
+
component: DataTable,
|
|
397
|
+
|
|
398
|
+
meta: {
|
|
399
|
+
name: 'DataTable',
|
|
400
|
+
description: 'Data table with sorting, selection, and column management. Powered by TanStack Table.',
|
|
401
|
+
category: 'display',
|
|
402
|
+
status: 'stable',
|
|
403
|
+
tags: ['table', 'data', 'grid', 'list', 'sorting', 'tanstack'],
|
|
404
|
+
since: '0.1.0',
|
|
405
|
+
dependencies: [
|
|
406
|
+
{ name: '@tanstack/react-table', version: '>=8.0.0', reason: 'Table state management and rendering' },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
usage: {
|
|
411
|
+
when: [
|
|
412
|
+
'Displaying structured, tabular data with sorting',
|
|
413
|
+
'Data that users need to scan, compare, and act upon',
|
|
414
|
+
'Lists with multiple attributes per item that need sorting or selection',
|
|
415
|
+
'Data-rich tables requiring column sizing and row clicks',
|
|
416
|
+
'Hierarchical data with expandable sub-rows',
|
|
417
|
+
],
|
|
418
|
+
whenNot: [
|
|
419
|
+
'Simple static tables (use Table component)',
|
|
420
|
+
'Simple lists (use List component)',
|
|
421
|
+
'Card-based layouts (use Grid with Cards)',
|
|
422
|
+
'Small screens (consider card or list view)',
|
|
423
|
+
],
|
|
424
|
+
guidelines: [
|
|
425
|
+
'Keep columns to a reasonable number (5-7 max)',
|
|
426
|
+
'Use consistent alignment (numbers right, text left)',
|
|
427
|
+
'Provide meaningful empty states',
|
|
428
|
+
'Consider mobile responsiveness',
|
|
429
|
+
'Use showCheckbox for bulk selection workflows',
|
|
430
|
+
],
|
|
431
|
+
accessibility: [
|
|
432
|
+
'Proper table semantics with headers',
|
|
433
|
+
'Sortable columns are keyboard accessible',
|
|
434
|
+
'Row selection checkboxes include aria-labels',
|
|
435
|
+
'Expand/collapse buttons have aria-expanded state',
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
props: {
|
|
440
|
+
columns: {
|
|
441
|
+
type: 'array',
|
|
442
|
+
description: 'Column definitions',
|
|
443
|
+
required: true,
|
|
444
|
+
},
|
|
445
|
+
data: {
|
|
446
|
+
type: 'array',
|
|
447
|
+
description: 'Data rows to display',
|
|
448
|
+
required: true,
|
|
449
|
+
},
|
|
450
|
+
getRowId: {
|
|
451
|
+
type: 'function',
|
|
452
|
+
description: 'Unique key extractor for each row',
|
|
453
|
+
},
|
|
454
|
+
sortable: {
|
|
455
|
+
type: 'boolean',
|
|
456
|
+
description: 'Enable column sorting',
|
|
457
|
+
default: 'false',
|
|
458
|
+
},
|
|
459
|
+
sorting: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
description: 'Controlled sorting state',
|
|
462
|
+
},
|
|
463
|
+
onSortingChange: {
|
|
464
|
+
type: 'function',
|
|
465
|
+
description: 'Sorting change handler',
|
|
466
|
+
},
|
|
467
|
+
selectable: {
|
|
468
|
+
type: 'boolean',
|
|
469
|
+
description: 'Enable row selection',
|
|
470
|
+
default: 'false',
|
|
471
|
+
},
|
|
472
|
+
showCheckbox: {
|
|
473
|
+
type: 'boolean',
|
|
474
|
+
description: 'Show checkbox column for row selection (requires selectable)',
|
|
475
|
+
default: 'false',
|
|
476
|
+
},
|
|
477
|
+
rowSelection: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
description: 'Controlled row selection state',
|
|
480
|
+
},
|
|
481
|
+
onRowSelectionChange: {
|
|
482
|
+
type: 'function',
|
|
483
|
+
description: 'Row selection change handler',
|
|
484
|
+
},
|
|
485
|
+
onRowClick: {
|
|
486
|
+
type: 'function',
|
|
487
|
+
description: 'Handler for row clicks',
|
|
488
|
+
},
|
|
489
|
+
getSubRows: {
|
|
490
|
+
type: 'function',
|
|
491
|
+
description: 'Extract sub-rows for expandable tree tables',
|
|
492
|
+
},
|
|
493
|
+
expanded: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
description: 'Controlled expanded state',
|
|
496
|
+
},
|
|
497
|
+
onExpandedChange: {
|
|
498
|
+
type: 'function',
|
|
499
|
+
description: 'Expanded state change handler',
|
|
500
|
+
},
|
|
501
|
+
emptyMessage: {
|
|
502
|
+
type: 'string',
|
|
503
|
+
description: 'Message when no data',
|
|
504
|
+
default: 'No data available',
|
|
505
|
+
},
|
|
506
|
+
size: {
|
|
507
|
+
type: 'enum',
|
|
508
|
+
description: 'Table density',
|
|
509
|
+
values: ['sm', 'md'],
|
|
510
|
+
default: 'md',
|
|
511
|
+
},
|
|
512
|
+
caption: {
|
|
513
|
+
type: 'string',
|
|
514
|
+
description: 'Visible caption for the table',
|
|
515
|
+
},
|
|
516
|
+
captionHidden: {
|
|
517
|
+
type: 'boolean',
|
|
518
|
+
default: 'false',
|
|
519
|
+
description: 'Hide caption visually but keep it for screen readers',
|
|
520
|
+
},
|
|
521
|
+
striped: {
|
|
522
|
+
type: 'boolean',
|
|
523
|
+
description: 'Show alternating row backgrounds',
|
|
524
|
+
default: 'false',
|
|
525
|
+
},
|
|
526
|
+
bordered: {
|
|
527
|
+
type: 'boolean',
|
|
528
|
+
description: 'Wrap table in a bordered container',
|
|
529
|
+
default: 'false',
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
relations: [
|
|
534
|
+
{ component: 'Table', relationship: 'alternative', note: 'Use Table for simple semantic HTML tables' },
|
|
535
|
+
{ component: 'EmptyState', relationship: 'sibling', note: 'Use EmptyState for empty table states' },
|
|
536
|
+
{ component: 'Badge', relationship: 'sibling', note: 'Use Badge for status columns' },
|
|
537
|
+
{ component: 'Menu', relationship: 'sibling', note: 'Use Menu for filter dropdowns' },
|
|
538
|
+
{ component: 'Checkbox', relationship: 'sibling', note: 'Built-in checkbox selection via showCheckbox' },
|
|
539
|
+
],
|
|
540
|
+
|
|
541
|
+
contract: {
|
|
542
|
+
propsSummary: [
|
|
543
|
+
'columns: ColumnDef[] - column definitions',
|
|
544
|
+
'data: T[] - row data array',
|
|
545
|
+
'sortable: boolean - enable sorting',
|
|
546
|
+
'selectable: boolean - enable row selection',
|
|
547
|
+
'showCheckbox: boolean - add checkbox column',
|
|
548
|
+
'getSubRows: (row) => T[] - enable expandable rows',
|
|
549
|
+
'size: sm|md - table density',
|
|
550
|
+
'striped: boolean - alternating row backgrounds',
|
|
551
|
+
'bordered: boolean - bordered container',
|
|
552
|
+
],
|
|
553
|
+
scenarioTags: [
|
|
554
|
+
'data.table',
|
|
555
|
+
'display.list',
|
|
556
|
+
'data.grid',
|
|
557
|
+
],
|
|
558
|
+
a11yRules: ['A11Y_TABLE_HEADERS', 'A11Y_TABLE_SORT'],
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
ai: {
|
|
562
|
+
compositionPattern: 'simple',
|
|
563
|
+
commonPatterns: [
|
|
564
|
+
'<DataTable columns={createColumns([{key:"name",header:"Name"},{key:"status",header:"Status"}])} data={[{name:"Item 1",status:"Active"}]} />',
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
variants: [
|
|
569
|
+
{
|
|
570
|
+
name: 'Default',
|
|
571
|
+
description: 'Basic data table with status badges and role columns',
|
|
572
|
+
render: () => (
|
|
573
|
+
<DataTable
|
|
574
|
+
columns={basicColumns}
|
|
575
|
+
data={sampleUsers}
|
|
576
|
+
aria-label="Team members"
|
|
577
|
+
/>
|
|
578
|
+
),
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: 'Rich Cells',
|
|
582
|
+
description: 'Custom cells with avatars, stacked text, and column sizing',
|
|
583
|
+
render: () => (
|
|
584
|
+
<DataTable
|
|
585
|
+
columns={richColumns}
|
|
586
|
+
data={sampleUsers}
|
|
587
|
+
bordered
|
|
588
|
+
aria-label="Team members"
|
|
589
|
+
/>
|
|
590
|
+
),
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: 'Sortable',
|
|
594
|
+
description: 'Click column headers to sort ascending or descending',
|
|
595
|
+
render: () => (
|
|
596
|
+
<DataTable
|
|
597
|
+
columns={transactionColumns}
|
|
598
|
+
data={sampleTransactions}
|
|
599
|
+
sortable
|
|
600
|
+
bordered
|
|
601
|
+
caption="Recent transactions"
|
|
602
|
+
captionHidden
|
|
603
|
+
aria-label="Transactions"
|
|
604
|
+
/>
|
|
605
|
+
),
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'Checkbox Selection',
|
|
609
|
+
description: 'Select rows with header checkbox for select-all and individual row checkboxes',
|
|
610
|
+
code: `function TeamTable() {
|
|
611
|
+
const [selection, setSelection] = useState({});
|
|
612
|
+
const selectedCount = Object.values(selection).filter(Boolean).length;
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
<Stack gap="sm">
|
|
616
|
+
<Stack direction="row" justify="between" align="center">
|
|
617
|
+
<Text size="sm" color="secondary">
|
|
618
|
+
{selectedCount > 0 ? \`\${selectedCount} selected\` : 'Select rows with checkboxes'}
|
|
619
|
+
</Text>
|
|
620
|
+
{selectedCount > 0 && (
|
|
621
|
+
<Button size="sm" variant="ghost" onClick={() => setSelection({})}>
|
|
622
|
+
Clear
|
|
623
|
+
</Button>
|
|
624
|
+
)}
|
|
625
|
+
</Stack>
|
|
626
|
+
<DataTable
|
|
627
|
+
columns={columns}
|
|
628
|
+
data={users}
|
|
629
|
+
selectable
|
|
630
|
+
showCheckbox
|
|
631
|
+
rowSelection={selection}
|
|
632
|
+
onRowSelectionChange={setSelection}
|
|
633
|
+
getRowId={(row) => row.id}
|
|
634
|
+
bordered
|
|
635
|
+
aria-label="Team members"
|
|
636
|
+
/>
|
|
637
|
+
</Stack>
|
|
638
|
+
);
|
|
639
|
+
}`,
|
|
640
|
+
render: () => <CheckboxSelectionExample />,
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: 'Expandable Rows',
|
|
644
|
+
description: 'Hierarchical data with collapsible sub-rows, like a file tree',
|
|
645
|
+
code: `const fileTreeData = [
|
|
646
|
+
{
|
|
647
|
+
id: '1', name: 'src', type: 'folder', modified: 'Feb 18, 2026',
|
|
648
|
+
subRows: [
|
|
649
|
+
{ id: '1.1', name: 'components', type: 'folder', modified: 'Feb 18, 2026',
|
|
650
|
+
subRows: [
|
|
651
|
+
{ id: '1.1.1', name: 'Button.tsx', type: 'file', size: '4.2 KB', modified: 'Feb 17, 2026' },
|
|
652
|
+
{ id: '1.1.2', name: 'Card.tsx', type: 'file', size: '3.8 KB', modified: 'Feb 16, 2026' },
|
|
653
|
+
],
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
},
|
|
657
|
+
{ id: '2', name: 'package.json', type: 'file', size: '1.2 KB', modified: 'Feb 18, 2026' },
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
<DataTable
|
|
661
|
+
columns={fileColumns}
|
|
662
|
+
data={fileTreeData}
|
|
663
|
+
getSubRows={(row) => row.subRows}
|
|
664
|
+
getRowId={(row) => row.id}
|
|
665
|
+
bordered
|
|
666
|
+
size="sm"
|
|
667
|
+
aria-label="File tree"
|
|
668
|
+
/>`,
|
|
669
|
+
render: () => <ExpandableRowsExample />,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: 'With Filters',
|
|
673
|
+
description: 'Combine with search input and menu dropdowns for filtered views',
|
|
674
|
+
code: `function FilteredTable() {
|
|
675
|
+
const [search, setSearch] = useState('');
|
|
676
|
+
const [statusFilter, setStatusFilter] = useState(null);
|
|
677
|
+
|
|
678
|
+
const filteredData = useMemo(() => {
|
|
679
|
+
return users.filter((user) => {
|
|
680
|
+
if (search && !user.name.toLowerCase().includes(search.toLowerCase())) return false;
|
|
681
|
+
if (statusFilter && user.status !== statusFilter) return false;
|
|
682
|
+
return true;
|
|
683
|
+
});
|
|
684
|
+
}, [search, statusFilter]);
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<Stack gap="sm">
|
|
688
|
+
<Stack direction="row" gap="sm" align="center">
|
|
689
|
+
<Input placeholder="Search..." size="sm" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
690
|
+
<Menu>
|
|
691
|
+
<Menu.Trigger asChild>
|
|
692
|
+
<Button variant="secondary" size="sm">Status</Button>
|
|
693
|
+
</Menu.Trigger>
|
|
694
|
+
<Menu.Content>
|
|
695
|
+
<Menu.Item onClick={() => setStatusFilter(null)}>All</Menu.Item>
|
|
696
|
+
<Menu.Item onClick={() => setStatusFilter('active')}>Active</Menu.Item>
|
|
697
|
+
<Menu.Item onClick={() => setStatusFilter('pending')}>Pending</Menu.Item>
|
|
698
|
+
</Menu.Content>
|
|
699
|
+
</Menu>
|
|
700
|
+
</Stack>
|
|
701
|
+
<DataTable
|
|
702
|
+
columns={columns}
|
|
703
|
+
data={filteredData}
|
|
704
|
+
sortable
|
|
705
|
+
bordered
|
|
706
|
+
emptyMessage="No users match the current filters"
|
|
707
|
+
aria-label="Filtered team members"
|
|
708
|
+
/>
|
|
709
|
+
</Stack>
|
|
710
|
+
);
|
|
711
|
+
}`,
|
|
712
|
+
render: () => <FilteredTableExample />,
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: 'Clickable Rows',
|
|
716
|
+
description: 'Rows respond to click and keyboard activation',
|
|
717
|
+
render: () => (
|
|
718
|
+
<DataTable
|
|
719
|
+
columns={endpointColumns}
|
|
720
|
+
data={sampleEndpoints}
|
|
721
|
+
onRowClick={(row) => alert(`${row.method} ${row.path}`)}
|
|
722
|
+
size="sm"
|
|
723
|
+
aria-label="API endpoints"
|
|
724
|
+
/>
|
|
725
|
+
),
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: 'Striped',
|
|
729
|
+
description: 'Alternating row backgrounds for dense data',
|
|
730
|
+
render: () => (
|
|
731
|
+
<DataTable
|
|
732
|
+
columns={endpointColumns}
|
|
733
|
+
data={sampleEndpoints}
|
|
734
|
+
striped
|
|
735
|
+
size="sm"
|
|
736
|
+
sortable
|
|
737
|
+
aria-label="API endpoints"
|
|
738
|
+
/>
|
|
739
|
+
),
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
name: 'Empty State',
|
|
743
|
+
description: 'Display when no data matches the current filters',
|
|
744
|
+
render: () => (
|
|
745
|
+
<DataTable
|
|
746
|
+
columns={basicColumns}
|
|
747
|
+
data={[]}
|
|
748
|
+
emptyMessage="No users match your search criteria"
|
|
749
|
+
aria-label="Search results"
|
|
750
|
+
/>
|
|
751
|
+
),
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
});
|