@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,533 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ // Import globals to ensure CSS variables are defined
5
+ import '../../styles/globals.scss';
6
+ import styles from './DataTable.module.scss';
7
+ import { Checkbox } from '../Checkbox';
8
+
9
+ // ============================================
10
+ // Types (self-owned — no external dependency for types)
11
+ // ============================================
12
+
13
+ /** Column definition compatible with @tanstack/react-table */
14
+ export type ColumnDef<TData = unknown, TValue = unknown> = {
15
+ id?: string;
16
+ accessorKey?: string;
17
+ accessorFn?: (row: TData) => TValue;
18
+ header?: string | ((context: any) => React.ReactNode);
19
+ cell?: string | ((context: any) => React.ReactNode);
20
+ size?: number;
21
+ minSize?: number;
22
+ maxSize?: number;
23
+ enableSorting?: boolean;
24
+ [key: string]: unknown;
25
+ };
26
+
27
+ export type SortingState = Array<{ id: string; desc: boolean }>;
28
+ export type RowSelectionState = Record<string, boolean>;
29
+ export type ExpandedState = true | Record<string, boolean>;
30
+ type OnChangeFn<T> = ((updaterOrValue: T | ((prev: T) => T)) => void);
31
+
32
+ export type DataTableColumn<T> = ColumnDef<T, unknown>;
33
+
34
+ // ============================================
35
+ // Lazy-loaded dependency (@tanstack/react-table)
36
+ // ============================================
37
+
38
+ let _useReactTable: any = null;
39
+ let _getCoreRowModel: any = null;
40
+ let _getSortedRowModel: any = null;
41
+ let _getExpandedRowModel: any = null;
42
+ let _flexRender: any = null;
43
+ let _tableLoaded = false;
44
+ let _tableFailed = false;
45
+
46
+ function loadTableDeps() {
47
+ if (_tableLoaded) return;
48
+ _tableLoaded = true;
49
+ try {
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const rt = require('@tanstack/react-table');
52
+ _useReactTable = rt.useReactTable;
53
+ _getCoreRowModel = rt.getCoreRowModel;
54
+ _getSortedRowModel = rt.getSortedRowModel;
55
+ _getExpandedRowModel = rt.getExpandedRowModel;
56
+ _flexRender = rt.flexRender;
57
+ } catch {
58
+ _tableFailed = true;
59
+ }
60
+ }
61
+
62
+ export interface DataTableProps<T> extends Omit<React.HTMLAttributes<HTMLTableElement>, 'onClick'> {
63
+ /** Column definitions */
64
+ columns: DataTableColumn<T>[];
65
+ /** Data array */
66
+ data: T[];
67
+ /** Unique key extractor for each row */
68
+ getRowId?: (row: T) => string;
69
+ /** Enable sorting */
70
+ sortable?: boolean;
71
+ /** Controlled sorting state */
72
+ sorting?: SortingState;
73
+ /** Sorting change handler */
74
+ onSortingChange?: OnChangeFn<SortingState>;
75
+ /** Enable row selection */
76
+ selectable?: boolean;
77
+ /** Show checkbox column for row selection */
78
+ showCheckbox?: boolean;
79
+ /** Controlled selection state */
80
+ rowSelection?: RowSelectionState;
81
+ /** Selection change handler */
82
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>;
83
+ /** Row click handler */
84
+ onRowClick?: (row: T) => void;
85
+ /** Extract sub-rows from a row for expandable tree tables */
86
+ getSubRows?: (row: T) => T[] | undefined;
87
+ /** Controlled expanded state */
88
+ expanded?: ExpandedState;
89
+ /** Expanded state change handler */
90
+ onExpandedChange?: OnChangeFn<ExpandedState>;
91
+ /** Empty state message */
92
+ emptyMessage?: string;
93
+ /** Size variant */
94
+ size?: 'sm' | 'md';
95
+ /** Visible caption for the table (recommended for accessibility) */
96
+ caption?: string;
97
+ /** Hide the caption visually but keep it for screen readers */
98
+ captionHidden?: boolean;
99
+ /** Show alternating row backgrounds */
100
+ striped?: boolean;
101
+ /** Wrap table in a bordered container */
102
+ bordered?: boolean;
103
+ }
104
+
105
+ function DataTableRoot<T>({
106
+ columns: userColumns,
107
+ data,
108
+ getRowId,
109
+ sortable = false,
110
+ sorting: controlledSorting,
111
+ onSortingChange,
112
+ selectable = false,
113
+ showCheckbox = false,
114
+ rowSelection: controlledRowSelection,
115
+ onRowSelectionChange,
116
+ onRowClick,
117
+ getSubRows,
118
+ expanded: controlledExpanded,
119
+ onExpandedChange,
120
+ emptyMessage = 'No data available',
121
+ size = 'md',
122
+ className,
123
+ caption,
124
+ captionHidden = false,
125
+ striped = false,
126
+ bordered = false,
127
+ 'aria-label': ariaLabel,
128
+ 'aria-describedby': ariaDescribedBy,
129
+ ...htmlProps
130
+ }: DataTableProps<T>) {
131
+ loadTableDeps();
132
+
133
+ // Internal sorting state when uncontrolled
134
+ const [internalSorting, setInternalSorting] = React.useState<SortingState>([]);
135
+ const sorting = controlledSorting ?? internalSorting;
136
+ const handleSortingChange = onSortingChange ?? setInternalSorting;
137
+
138
+ // Internal selection state when uncontrolled
139
+ const [internalRowSelection, setInternalRowSelection] = React.useState<RowSelectionState>({});
140
+ const rowSelection = controlledRowSelection ?? internalRowSelection;
141
+ const handleRowSelectionChange = onRowSelectionChange ?? setInternalRowSelection;
142
+
143
+ // Internal expanded state when uncontrolled
144
+ const [internalExpanded, setInternalExpanded] = React.useState<ExpandedState>({});
145
+ const expanded = controlledExpanded ?? internalExpanded;
146
+ const handleExpandedChange = onExpandedChange ?? setInternalExpanded;
147
+
148
+ // Build columns with optional checkbox prepended
149
+ const columns = React.useMemo(() => {
150
+ if (!showCheckbox || !selectable) return userColumns;
151
+
152
+ const checkboxColumn: DataTableColumn<T> = {
153
+ id: '__checkbox',
154
+ size: 40,
155
+ minSize: 40,
156
+ maxSize: 40,
157
+ enableSorting: false,
158
+ header: ({ table }: any) => (
159
+ <Checkbox
160
+ size="sm"
161
+ checked={table.getIsAllRowsSelected()}
162
+ indeterminate={table.getIsSomeRowsSelected()}
163
+ onCheckedChange={() => table.toggleAllRowsSelected()}
164
+ aria-label="Select all rows"
165
+ />
166
+ ),
167
+ cell: ({ row }: any) => (
168
+ <Checkbox
169
+ size="sm"
170
+ checked={row.getIsSelected()}
171
+ disabled={!row.getCanSelect()}
172
+ onCheckedChange={() => row.toggleSelected()}
173
+ aria-label={`Select row ${row.id}`}
174
+ />
175
+ ),
176
+ };
177
+
178
+ return [checkboxColumn, ...userColumns];
179
+ }, [userColumns, showCheckbox, selectable]);
180
+
181
+ if (_tableFailed || !_useReactTable) {
182
+ if (_tableFailed && process.env.NODE_ENV === 'development') {
183
+ console.warn(
184
+ '[@fragments-sdk/ui] DataTable: @tanstack/react-table is not installed. ' +
185
+ 'Install it with: npm install @tanstack/react-table'
186
+ );
187
+ }
188
+ return null;
189
+ }
190
+
191
+ const hasSubRows = !!getSubRows;
192
+
193
+ const table = _useReactTable({
194
+ data,
195
+ columns,
196
+ getRowId,
197
+ getSubRows: getSubRows as any,
198
+ getCoreRowModel: _getCoreRowModel(),
199
+ getSortedRowModel: sortable ? _getSortedRowModel() : undefined,
200
+ getExpandedRowModel: hasSubRows && _getExpandedRowModel ? _getExpandedRowModel() : undefined,
201
+ state: {
202
+ sorting: sortable ? sorting : undefined,
203
+ rowSelection: selectable ? rowSelection : undefined,
204
+ expanded: hasSubRows ? expanded : undefined,
205
+ },
206
+ onSortingChange: sortable ? handleSortingChange : undefined,
207
+ onRowSelectionChange: selectable ? handleRowSelectionChange : undefined,
208
+ onExpandedChange: hasSubRows ? handleExpandedChange : undefined,
209
+ enableRowSelection: selectable,
210
+ enableSorting: sortable,
211
+ enableExpanding: hasSubRows,
212
+ });
213
+
214
+ const isEmpty = data.length === 0;
215
+
216
+ const hasExplicitColumnSizing = React.useMemo(
217
+ () =>
218
+ columns.some((column) =>
219
+ column.size !== undefined ||
220
+ column.minSize !== undefined ||
221
+ column.maxSize !== undefined
222
+ ),
223
+ [columns]
224
+ );
225
+
226
+ const rootClasses = [
227
+ styles.table,
228
+ hasExplicitColumnSizing && styles.fixedLayout,
229
+ styles[size],
230
+ striped && styles.striped,
231
+ className,
232
+ ]
233
+ .filter(Boolean)
234
+ .join(' ');
235
+
236
+ const getColumnSizeStyle = (
237
+ column: {
238
+ getSize: () => number;
239
+ columnDef: { size?: number; minSize?: number; maxSize?: number };
240
+ }
241
+ ): React.CSSProperties | undefined => {
242
+ const { size, minSize, maxSize } = column.columnDef;
243
+ const hasExplicitSize = size !== undefined || minSize !== undefined || maxSize !== undefined;
244
+
245
+ if (!hasExplicitSize) {
246
+ return undefined;
247
+ }
248
+
249
+ const resolvedSize = column.getSize();
250
+
251
+ return {
252
+ width: resolvedSize,
253
+ minWidth: minSize ?? resolvedSize,
254
+ maxWidth: maxSize ?? resolvedSize,
255
+ };
256
+ };
257
+
258
+ if (isEmpty) {
259
+ return (
260
+ <div className={styles.emptyState}>
261
+ <span className={styles.emptyMessage}>{emptyMessage}</span>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ const isInteractiveTarget = (
267
+ target: EventTarget | null,
268
+ currentTarget: HTMLTableRowElement
269
+ ) => {
270
+ if (!(target instanceof Element)) return false;
271
+
272
+ const interactiveElement = target.closest(
273
+ 'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="switch"]'
274
+ );
275
+
276
+ return Boolean(interactiveElement && currentTarget.contains(interactiveElement));
277
+ };
278
+
279
+ return (
280
+ <div className={[styles.wrapper, bordered && styles.bordered].filter(Boolean).join(' ')}>
281
+ <table
282
+ {...htmlProps}
283
+ className={rootClasses}
284
+ aria-label={ariaLabel}
285
+ aria-describedby={ariaDescribedBy}
286
+ >
287
+ {caption && (
288
+ <caption className={captionHidden ? styles.captionHidden : styles.caption}>
289
+ {caption}
290
+ </caption>
291
+ )}
292
+ <thead className={styles.thead}>
293
+ {table.getHeaderGroups().map((headerGroup: any) => (
294
+ <tr key={headerGroup.id} className={styles.headerRow}>
295
+ {headerGroup.headers.map((header: any) => {
296
+ const canSort = sortable && header.column.getCanSort();
297
+ const sortDirection = header.column.getIsSorted();
298
+ const toggleSorting = canSort ? header.column.getToggleSortingHandler() : undefined;
299
+
300
+ return (
301
+ <th
302
+ key={header.id}
303
+ className={[styles.th, canSort && styles.thSortable].filter(Boolean).join(' ')}
304
+ style={getColumnSizeStyle(header.column)}
305
+ scope="col"
306
+ aria-sort={
307
+ sortDirection
308
+ ? sortDirection === 'asc'
309
+ ? 'ascending'
310
+ : 'descending'
311
+ : canSort
312
+ ? 'none'
313
+ : undefined
314
+ }
315
+ >
316
+ {canSort ? (
317
+ <button
318
+ type="button"
319
+ className={styles.sortButton}
320
+ onClick={toggleSorting}
321
+ >
322
+ <span className={styles.headerContent}>
323
+ {header.isPlaceholder
324
+ ? null
325
+ : _flexRender(
326
+ header.column.columnDef.header,
327
+ header.getContext()
328
+ )}
329
+ </span>
330
+ <span className={styles.sortIndicator} aria-hidden="true">
331
+ {sortDirection === 'asc' ? (
332
+ <SortAscIcon />
333
+ ) : sortDirection === 'desc' ? (
334
+ <SortDescIcon />
335
+ ) : (
336
+ <SortIcon />
337
+ )}
338
+ </span>
339
+ </button>
340
+ ) : (
341
+ <div className={styles.headerContent}>
342
+ {header.isPlaceholder
343
+ ? null
344
+ : _flexRender(
345
+ header.column.columnDef.header,
346
+ header.getContext()
347
+ )}
348
+ </div>
349
+ )}
350
+ </th>
351
+ );
352
+ })}
353
+ </tr>
354
+ ))}
355
+ </thead>
356
+ <tbody className={styles.tbody}>
357
+ {table.getRowModel().rows.map((row: any) => {
358
+ const isClickable = !!onRowClick;
359
+ const isSelected = selectable ? row.getIsSelected() : false;
360
+ const depth: number = row.depth ?? 0;
361
+ const canExpand = hasSubRows && row.getCanExpand();
362
+ const handleRowClick = (event: React.MouseEvent<HTMLTableRowElement>) => {
363
+ if (!onRowClick) return;
364
+ if (isInteractiveTarget(event.target, event.currentTarget)) return;
365
+ onRowClick(row.original);
366
+ };
367
+
368
+ const handleRowKeyDown = (event: React.KeyboardEvent<HTMLTableRowElement>) => {
369
+ if (!onRowClick) return;
370
+ if (isInteractiveTarget(event.target, event.currentTarget)) return;
371
+ if (event.key === 'Enter' || event.key === ' ') {
372
+ event.preventDefault();
373
+ onRowClick(row.original);
374
+ }
375
+ };
376
+
377
+ return (
378
+ <tr
379
+ key={row.id}
380
+ className={[
381
+ styles.row,
382
+ isClickable && styles.clickable,
383
+ isSelected && styles.selected,
384
+ depth > 0 && styles.subRow,
385
+ ]
386
+ .filter(Boolean)
387
+ .join(' ')}
388
+ onClick={isClickable ? handleRowClick : undefined}
389
+ onKeyDown={isClickable ? handleRowKeyDown : undefined}
390
+ tabIndex={isClickable ? 0 : undefined}
391
+ data-selected={isSelected || undefined}
392
+ data-depth={depth > 0 ? depth : undefined}
393
+ >
394
+ {row.getVisibleCells().map((cell: any, cellIndex: number) => {
395
+ const isFirstDataCell = hasSubRows && cellIndex === (showCheckbox && selectable ? 1 : 0);
396
+ return (
397
+ <td
398
+ key={cell.id}
399
+ className={styles.td}
400
+ style={{
401
+ ...getColumnSizeStyle(cell.column),
402
+ ...(isFirstDataCell && depth > 0 ? { paddingLeft: `${depth * 24 + 12}px` } : undefined),
403
+ }}
404
+ >
405
+ {isFirstDataCell && canExpand ? (
406
+ <span className={styles.expandCell}>
407
+ <button
408
+ type="button"
409
+ className={styles.expandButton}
410
+ onClick={row.getToggleExpandedHandler()}
411
+ aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
412
+ aria-expanded={row.getIsExpanded()}
413
+ >
414
+ <ExpandIcon expanded={row.getIsExpanded()} />
415
+ </button>
416
+ {_flexRender(cell.column.columnDef.cell, cell.getContext())}
417
+ </span>
418
+ ) : (
419
+ _flexRender(cell.column.columnDef.cell, cell.getContext())
420
+ )}
421
+ </td>
422
+ );
423
+ })}
424
+ </tr>
425
+ );
426
+ })}
427
+ </tbody>
428
+ </table>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ // Expand/collapse icon for sub-rows
434
+ function ExpandIcon({ expanded }: { expanded: boolean }) {
435
+ return (
436
+ <svg
437
+ width="12"
438
+ height="12"
439
+ viewBox="0 0 12 12"
440
+ fill="none"
441
+ xmlns="http://www.w3.org/2000/svg"
442
+ aria-hidden="true"
443
+ style={{
444
+ transform: expanded ? 'rotate(90deg)' : undefined,
445
+ transition: 'transform 150ms ease',
446
+ }}
447
+ >
448
+ <path d="M4.5 2.5L8 6L4.5 9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
449
+ </svg>
450
+ );
451
+ }
452
+
453
+ // Sort icons - minimal and functional
454
+ function SortIcon() {
455
+ return (
456
+ <svg
457
+ width="12"
458
+ height="12"
459
+ viewBox="0 0 12 12"
460
+ fill="none"
461
+ xmlns="http://www.w3.org/2000/svg"
462
+ aria-hidden="true"
463
+ >
464
+ <path
465
+ d="M6 2L8.5 5H3.5L6 2Z"
466
+ fill="currentColor"
467
+ opacity="0.3"
468
+ />
469
+ <path
470
+ d="M6 10L3.5 7H8.5L6 10Z"
471
+ fill="currentColor"
472
+ opacity="0.3"
473
+ />
474
+ </svg>
475
+ );
476
+ }
477
+
478
+ function SortAscIcon() {
479
+ return (
480
+ <svg
481
+ width="12"
482
+ height="12"
483
+ viewBox="0 0 12 12"
484
+ fill="none"
485
+ xmlns="http://www.w3.org/2000/svg"
486
+ aria-hidden="true"
487
+ >
488
+ <path d="M6 2L8.5 5H3.5L6 2Z" fill="currentColor" />
489
+ </svg>
490
+ );
491
+ }
492
+
493
+ function SortDescIcon() {
494
+ return (
495
+ <svg
496
+ width="12"
497
+ height="12"
498
+ viewBox="0 0 12 12"
499
+ fill="none"
500
+ xmlns="http://www.w3.org/2000/svg"
501
+ aria-hidden="true"
502
+ >
503
+ <path d="M6 10L3.5 7H8.5L6 10Z" fill="currentColor" />
504
+ </svg>
505
+ );
506
+ }
507
+
508
+ // Helper to create simple columns without TanStack's createColumnHelper
509
+ export function createColumns<T>(
510
+ columns: Array<{
511
+ key: string;
512
+ header: string;
513
+ width?: number;
514
+ cell?: (row: T) => React.ReactNode;
515
+ }>
516
+ ): DataTableColumn<T>[] {
517
+ return columns.map((col) => ({
518
+ id: col.key,
519
+ accessorKey: col.key,
520
+ header: col.header,
521
+ size: col.width,
522
+ minSize: col.width,
523
+ maxSize: col.width,
524
+ cell: col.cell
525
+ ? ({ row }) => col.cell!(row.original)
526
+ : ({ getValue }) => getValue() ?? '--',
527
+ }));
528
+ }
529
+
530
+ export const DataTable = Object.assign(DataTableRoot, {
531
+ Root: DataTableRoot,
532
+ Columns: createColumns,
533
+ });
@@ -65,6 +65,14 @@ export default defineFragment({
65
65
  type: 'node',
66
66
  description: 'Custom avatar override (null to hide)',
67
67
  },
68
+ 'Avatar.src': {
69
+ type: 'string',
70
+ description: 'Image URL for the avatar (on Message.Avatar sub-component)',
71
+ },
72
+ 'Avatar.alt': {
73
+ type: 'string',
74
+ description: 'Alt text for image avatars (on Message.Avatar sub-component)',
75
+ },
68
76
  actions: {
69
77
  type: 'node',
70
78
  description: 'Hover actions (copy, regenerate)',
@@ -97,6 +105,8 @@ export default defineFragment({
97
105
  'timestamp: Date - when message was sent',
98
106
  'avatar: ReactNode - custom avatar (null to hide)',
99
107
  'actions: ReactNode - hover actions',
108
+ 'Avatar.src: string - image URL for avatar',
109
+ 'Avatar.alt: string - alt text for image avatar',
100
110
  ],
101
111
  scenarioTags: [
102
112
  'ui.chat',
@@ -179,6 +189,30 @@ export default defineFragment({
179
189
  </Message>
180
190
  ),
181
191
  },
192
+ {
193
+ name: 'Custom Avatars',
194
+ description: 'Messages with image-based avatars',
195
+ render: () => (
196
+ <>
197
+ <Message
198
+ role="user"
199
+ avatar={<Message.Avatar src="https://i.pravatar.cc/64?u=user" alt="Jane" />}
200
+ >
201
+ <Message.Content>
202
+ Can you help me understand this error?
203
+ </Message.Content>
204
+ </Message>
205
+ <Message
206
+ role="assistant"
207
+ avatar={<Message.Avatar src="https://i.pravatar.cc/64?u=bot" alt="AI Assistant" />}
208
+ >
209
+ <Message.Content>
210
+ Sure! Let me take a look at that for you.
211
+ </Message.Content>
212
+ </Message>
213
+ </>
214
+ ),
215
+ },
182
216
  {
183
217
  name: 'With Actions',
184
218
  description: 'Message with hover actions',
@@ -112,6 +112,17 @@
112
112
  color: var(--fui-color-info, $fui-color-info);
113
113
  }
114
114
 
115
+ .avatarImage {
116
+ padding: 0;
117
+ overflow: hidden;
118
+ }
119
+
120
+ .avatarImg {
121
+ width: 100%;
122
+ height: 100%;
123
+ object-fit: cover;
124
+ }
125
+
115
126
  // ============================================
116
127
  // Body (content container)
117
128
  // ============================================
@@ -45,6 +45,10 @@ export interface MessageTimestampProps extends React.HTMLAttributes<HTMLSpanElem
45
45
 
46
46
  export interface MessageAvatarProps extends React.HTMLAttributes<HTMLDivElement> {
47
47
  children?: React.ReactNode;
48
+ /** Image URL for the avatar */
49
+ src?: string;
50
+ /** Alt text for image avatars */
51
+ alt?: string;
48
52
  }
49
53
 
50
54
  // ============================================
@@ -118,7 +122,7 @@ function AssistantIcon() {
118
122
  fill="currentColor"
119
123
  aria-hidden="true"
120
124
  >
121
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
125
+ <path d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z" />
122
126
  </svg>
123
127
  );
124
128
  }
@@ -194,12 +198,13 @@ function MessageTimestamp({
194
198
  );
195
199
  }
196
200
 
197
- function MessageAvatar({ children, className, ...htmlProps }: MessageAvatarProps) {
201
+ function MessageAvatar({ children, src, alt, className, ...htmlProps }: MessageAvatarProps) {
198
202
  const { role } = useMessageContext();
199
203
 
200
204
  const classes = [
201
205
  styles.avatar,
202
206
  styles[`avatar${role.charAt(0).toUpperCase() + role.slice(1)}`],
207
+ src && styles.avatarImage,
203
208
  className,
204
209
  ].filter(Boolean).join(' ');
205
210
 
@@ -209,9 +214,13 @@ function MessageAvatar({ children, className, ...htmlProps }: MessageAvatarProps
209
214
  ? <AssistantIcon />
210
215
  : <SystemIcon />;
211
216
 
217
+ const content = children ?? (src
218
+ ? <img src={src} alt={alt ?? role} className={styles.avatarImg} />
219
+ : defaultIcon);
220
+
212
221
  return (
213
222
  <div {...htmlProps} className={classes}>
214
- {children ?? defaultIcon}
223
+ {content}
215
224
  </div>
216
225
  );
217
226
  }