@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,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="
|
|
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
|
-
{
|
|
223
|
+
{content}
|
|
215
224
|
</div>
|
|
216
225
|
);
|
|
217
226
|
}
|