@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.
Files changed (126) hide show
  1. package/dist/assets/ui.css +443 -247
  2. package/dist/blocks/components/index.d.ts +0 -2
  3. package/dist/blocks/components/index.d.ts.map +1 -1
  4. package/dist/codeblock.cjs +187 -184
  5. package/dist/codeblock.cjs.map +1 -1
  6. package/dist/codeblock.js +183 -180
  7. package/dist/codeblock.js.map +1 -1
  8. package/dist/components/Box/Box.module.scss.cjs +73 -0
  9. package/dist/components/Box/Box.module.scss.cjs.map +1 -1
  10. package/dist/components/Box/Box.module.scss.js +73 -0
  11. package/dist/components/Box/Box.module.scss.js.map +1 -1
  12. package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs +6 -0
  13. package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs.map +1 -1
  14. package/dist/components/ButtonGroup/ButtonGroup.module.scss.js +6 -0
  15. package/dist/components/ButtonGroup/ButtonGroup.module.scss.js.map +1 -1
  16. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +20 -23
  17. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs.map +1 -1
  18. package/dist/components/CodeBlock/CodeBlock.module.scss.js +20 -23
  19. package/dist/components/CodeBlock/CodeBlock.module.scss.js.map +1 -1
  20. package/dist/components/CodeBlock/index.d.ts +11 -7
  21. package/dist/components/CodeBlock/index.d.ts.map +1 -1
  22. package/dist/components/Combobox/Combobox.module.scss.cjs +15 -15
  23. package/dist/components/Combobox/Combobox.module.scss.js +15 -15
  24. package/dist/components/DataTable/DataTable.module.scss.cjs +84 -0
  25. package/dist/components/DataTable/DataTable.module.scss.cjs.map +1 -0
  26. package/dist/components/DataTable/DataTable.module.scss.js +84 -0
  27. package/dist/components/DataTable/DataTable.module.scss.js.map +1 -0
  28. package/dist/components/DataTable/index.cjs +383 -0
  29. package/dist/components/DataTable/index.cjs.map +1 -0
  30. package/dist/components/DataTable/index.d.ts +78 -0
  31. package/dist/components/DataTable/index.d.ts.map +1 -0
  32. package/dist/components/DataTable/index.js +366 -0
  33. package/dist/components/DataTable/index.js.map +1 -0
  34. package/dist/components/Drawer/Drawer.module.scss.cjs +9 -0
  35. package/dist/components/Drawer/Drawer.module.scss.cjs.map +1 -1
  36. package/dist/components/Drawer/Drawer.module.scss.js +9 -0
  37. package/dist/components/Drawer/Drawer.module.scss.js.map +1 -1
  38. package/dist/components/Image/Image.module.scss.cjs +12 -0
  39. package/dist/components/Image/Image.module.scss.cjs.map +1 -1
  40. package/dist/components/Image/Image.module.scss.js +12 -0
  41. package/dist/components/Image/Image.module.scss.js.map +1 -1
  42. package/dist/components/Link/Link.module.scss.cjs +3 -0
  43. package/dist/components/Link/Link.module.scss.cjs.map +1 -1
  44. package/dist/components/Link/Link.module.scss.js +3 -0
  45. package/dist/components/Link/Link.module.scss.js.map +1 -1
  46. package/dist/components/List/List.module.scss.cjs +5 -0
  47. package/dist/components/List/List.module.scss.cjs.map +1 -1
  48. package/dist/components/List/List.module.scss.js +5 -0
  49. package/dist/components/List/List.module.scss.js.map +1 -1
  50. package/dist/components/Loading/Loading.module.scss.cjs +5 -0
  51. package/dist/components/Loading/Loading.module.scss.cjs.map +1 -1
  52. package/dist/components/Loading/Loading.module.scss.js +5 -0
  53. package/dist/components/Loading/Loading.module.scss.js.map +1 -1
  54. package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
  55. package/dist/components/Markdown/Markdown.module.scss.js +1 -1
  56. package/dist/components/Message/Message.module.scss.cjs +22 -16
  57. package/dist/components/Message/Message.module.scss.cjs.map +1 -1
  58. package/dist/components/Message/Message.module.scss.js +22 -16
  59. package/dist/components/Message/Message.module.scss.js.map +1 -1
  60. package/dist/components/Message/index.cjs +5 -3
  61. package/dist/components/Message/index.cjs.map +1 -1
  62. package/dist/components/Message/index.d.ts +5 -1
  63. package/dist/components/Message/index.d.ts.map +1 -1
  64. package/dist/components/Message/index.js +5 -3
  65. package/dist/components/Message/index.js.map +1 -1
  66. package/dist/components/Skeleton/Skeleton.module.scss.cjs +14 -0
  67. package/dist/components/Skeleton/Skeleton.module.scss.cjs.map +1 -1
  68. package/dist/components/Skeleton/Skeleton.module.scss.js +14 -0
  69. package/dist/components/Skeleton/Skeleton.module.scss.js.map +1 -1
  70. package/dist/components/Stack/Stack.module.scss.cjs +14 -0
  71. package/dist/components/Stack/Stack.module.scss.cjs.map +1 -1
  72. package/dist/components/Stack/Stack.module.scss.js +14 -0
  73. package/dist/components/Stack/Stack.module.scss.js.map +1 -1
  74. package/dist/components/Table/Table.module.scss.cjs +21 -36
  75. package/dist/components/Table/Table.module.scss.cjs.map +1 -1
  76. package/dist/components/Table/Table.module.scss.js +21 -36
  77. package/dist/components/Table/Table.module.scss.js.map +1 -1
  78. package/dist/components/Table/index.d.ts +35 -55
  79. package/dist/components/Table/index.d.ts.map +1 -1
  80. package/dist/components/Text/Text.module.scss.cjs +14 -0
  81. package/dist/components/Text/Text.module.scss.cjs.map +1 -1
  82. package/dist/components/Text/Text.module.scss.js +14 -0
  83. package/dist/components/Text/Text.module.scss.js.map +1 -1
  84. package/dist/components/Textarea/Textarea.module.scss.cjs +4 -0
  85. package/dist/components/Textarea/Textarea.module.scss.cjs.map +1 -1
  86. package/dist/components/Textarea/Textarea.module.scss.js +4 -0
  87. package/dist/components/Textarea/Textarea.module.scss.js.map +1 -1
  88. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +5 -0
  89. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs.map +1 -1
  90. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +5 -0
  91. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js.map +1 -1
  92. package/dist/index.cjs +119 -117
  93. package/dist/index.cjs.map +1 -1
  94. package/dist/index.d.ts +2 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +3 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/table.cjs +44 -262
  99. package/dist/table.cjs.map +1 -1
  100. package/dist/table.js +47 -248
  101. package/dist/table.js.map +1 -1
  102. package/fragments.json +1 -1
  103. package/package.json +110 -118
  104. package/src/blocks/components/index.ts +0 -3
  105. package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
  106. package/src/components/CodeBlock/index.tsx +351 -345
  107. package/src/components/Combobox/Combobox.module.scss +13 -9
  108. package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
  109. package/src/components/DataTable/DataTable.fragment.tsx +754 -0
  110. package/src/components/DataTable/DataTable.module.scss +300 -0
  111. package/src/components/DataTable/DataTable.test.tsx +224 -0
  112. package/src/components/DataTable/index.tsx +533 -0
  113. package/src/components/Message/Message.fragment.tsx +34 -0
  114. package/src/components/Message/Message.module.scss +11 -0
  115. package/src/components/Message/index.tsx +12 -3
  116. package/src/components/Table/Table.fragment.tsx +190 -175
  117. package/src/components/Table/Table.module.scss +15 -88
  118. package/src/components/Table/Table.test.tsx +184 -94
  119. package/src/components/Table/index.tsx +105 -374
  120. package/src/index.ts +15 -4
  121. package/src/tokens/_computed.scss +7 -6
  122. package/src/tokens/_density.scss +87 -47
  123. package/src/tokens/_variables.scss +46 -31
  124. package/dist/blocks/components/DataTable.d.ts +0 -19
  125. package/dist/blocks/components/DataTable.d.ts.map +0 -1
  126. 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
+ });