noecosystem-design 0.2.3 → 0.3.0
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/README.md +2 -2
- package/apps/storybook/stories/catalog/checkbox.stories.tsx +7 -2
- package/apps/storybook/stories/catalog/data-table.stories.tsx +226 -3
- package/docs/design-system/components.md +32 -0
- package/docs/design-system/inventory.md +1 -0
- package/docs/design-system/visual-qa.md +6 -0
- package/package.json +1 -1
- package/packages/data-ui/package.json +6 -1
- package/packages/data-ui/src/data-table.tsx +2 -99
- package/packages/data-ui/src/pagination.tsx +2 -2
- package/packages/data-ui/src/table/adapters/tanstack.test.ts +116 -0
- package/packages/data-ui/src/table/adapters/tanstack.ts +179 -0
- package/packages/data-ui/src/table/contracts.ts +133 -0
- package/packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx +157 -0
- package/packages/data-ui/src/table/data-table.browser.test.tsx +180 -0
- package/packages/data-ui/src/table/data-table.tsx +524 -0
- package/packages/design-tokens/src/tokens.css +10 -4
- package/packages/design-tokens/src/tokens.json +8 -4
- package/packages/design-tokens/src/tokens.mjs +8 -4
- package/packages/registry/catalog-index.json +2 -2
- package/packages/registry/manifest.json +60 -18
- package/packages/registry/r/activity-feed.json +1 -1
- package/packages/registry/r/agent-composer.json +7 -1
- package/packages/registry/r/agent-message.json +1 -1
- package/packages/registry/r/agent-plan.json +1 -1
- package/packages/registry/r/agent-workspace.json +1 -1
- package/packages/registry/r/approval-request.json +7 -1
- package/packages/registry/r/calendar.json +6 -5
- package/packages/registry/r/chart-frame.json +1 -1
- package/packages/registry/r/chart.json +1 -1
- package/packages/registry/r/dashboard-01.json +22 -2
- package/packages/registry/r/dashboard-02.json +22 -2
- package/packages/registry/r/dashboard-shell.json +1 -1
- package/packages/registry/r/data-table.json +22 -2
- package/packages/registry/r/date-picker.json +6 -5
- package/packages/registry/r/filter-bar.json +1 -1
- package/packages/registry/r/inline-cta.json +6 -6
- package/packages/registry/r/metric-card.json +1 -1
- package/packages/registry/r/pagination.json +7 -5
- package/packages/registry/r/run-inspector.json +1 -1
- package/packages/registry/r/tool-call.json +1 -1
- package/packages/registry/r/workflow-canvas.json +1 -1
- package/packages/registry/r/workflow-node.json +1 -1
- package/packages/registry/registry.json +12 -3
- package/packages/registry/search-index.json +23 -8
- package/packages/ui/package.json +5 -1
- package/packages/ui/src/checkbox.tsx +1 -1
- package/scripts/generate-manifest.mjs +71 -0
- package/vitest.config.ts +5 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { useId, useMemo, useState } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { ArrowDown2, ArrowUp2 } from '@noe/icons';
|
|
4
|
+
import { TechnicalValue } from '@noe/ui/bidi';
|
|
5
|
+
import { Button } from '@noe/ui/button';
|
|
6
|
+
import { Checkbox } from '@noe/ui/checkbox';
|
|
7
|
+
import { cn } from '@noe/ui/lib/cn';
|
|
8
|
+
import { Skeleton } from '@noe/ui/skeleton';
|
|
9
|
+
import { functionalUpdate, useTable } from '@tanstack/react-table';
|
|
10
|
+
import type {
|
|
11
|
+
ColumnFiltersState,
|
|
12
|
+
OnChangeFn,
|
|
13
|
+
RowSelectionState,
|
|
14
|
+
SortingState,
|
|
15
|
+
} from '@tanstack/react-table';
|
|
16
|
+
|
|
17
|
+
import { Pagination } from '../pagination';
|
|
18
|
+
import {
|
|
19
|
+
fromTanStackFilters,
|
|
20
|
+
fromTanStackSelection,
|
|
21
|
+
fromTanStackSorting,
|
|
22
|
+
noeTableFeatures,
|
|
23
|
+
toTanStackColumns,
|
|
24
|
+
toTanStackFilters,
|
|
25
|
+
toTanStackSelection,
|
|
26
|
+
toTanStackSorting,
|
|
27
|
+
} from './adapters/tanstack';
|
|
28
|
+
import type { DataColumn, DataSort, FilterSpec, PageSpec, RowId, SelectionMode } from './contracts';
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
DataColumn,
|
|
32
|
+
DataColumnSortFn,
|
|
33
|
+
DataQuery,
|
|
34
|
+
DataQueryInput,
|
|
35
|
+
DataSort,
|
|
36
|
+
DataSortDirection,
|
|
37
|
+
FilterOperator,
|
|
38
|
+
FilterSpec,
|
|
39
|
+
PageSpec,
|
|
40
|
+
RowId,
|
|
41
|
+
SelectionMode,
|
|
42
|
+
SelectionState,
|
|
43
|
+
TableStateAdapter,
|
|
44
|
+
} from './contracts';
|
|
45
|
+
export { createMemoryTableStateAdapter, toDataQuery } from './contracts';
|
|
46
|
+
|
|
47
|
+
export interface DataTablePaginationStatusInfo {
|
|
48
|
+
/** One-based index of the first row on the page. */
|
|
49
|
+
startIndex: number;
|
|
50
|
+
/** One-based index of the last row on the page. */
|
|
51
|
+
endIndex: number;
|
|
52
|
+
/** Total row count across pages. */
|
|
53
|
+
total: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DataTableSelectionInfo {
|
|
57
|
+
selectedIds: RowId[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type DataTableFeatureProps<Row extends { id: string }> = {
|
|
61
|
+
/** Controlled NOE sorting state. */
|
|
62
|
+
sorting?: readonly DataSort[];
|
|
63
|
+
/** Initial sorting state for uncontrolled usage. */
|
|
64
|
+
defaultSorting?: readonly DataSort[];
|
|
65
|
+
onSortingChange?: (sorting: DataSort[]) => void;
|
|
66
|
+
/** Server owns row order; the table only reflects sorting state. */
|
|
67
|
+
manualSorting?: boolean;
|
|
68
|
+
} & (
|
|
69
|
+
| {
|
|
70
|
+
filters?: undefined;
|
|
71
|
+
defaultFilters?: undefined;
|
|
72
|
+
onFiltersChange?: undefined;
|
|
73
|
+
manualFiltering?: undefined;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
/** Controlled NOE filter state. */
|
|
77
|
+
filters: readonly FilterSpec[];
|
|
78
|
+
defaultFilters?: undefined;
|
|
79
|
+
onFiltersChange: (filters: FilterSpec[]) => void;
|
|
80
|
+
manualFiltering?: boolean;
|
|
81
|
+
}
|
|
82
|
+
| {
|
|
83
|
+
filters?: undefined;
|
|
84
|
+
/** Initial filter state for uncontrolled usage. */
|
|
85
|
+
defaultFilters?: readonly FilterSpec[];
|
|
86
|
+
onFiltersChange?: (filters: FilterSpec[]) => void;
|
|
87
|
+
manualFiltering?: boolean;
|
|
88
|
+
}
|
|
89
|
+
) &
|
|
90
|
+
(
|
|
91
|
+
| {
|
|
92
|
+
page?: undefined;
|
|
93
|
+
onPageChange?: undefined;
|
|
94
|
+
previousPageLabel?: undefined;
|
|
95
|
+
nextPageLabel?: undefined;
|
|
96
|
+
paginationLabel?: undefined;
|
|
97
|
+
paginationStatus?: undefined;
|
|
98
|
+
totalRowCount?: undefined;
|
|
99
|
+
manualPagination?: undefined;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
/** Controlled zero-based page state. */
|
|
103
|
+
page: PageSpec;
|
|
104
|
+
onPageChange: (page: PageSpec) => void;
|
|
105
|
+
/** Accessible names for the icon-only pagination controls. */
|
|
106
|
+
previousPageLabel: string;
|
|
107
|
+
nextPageLabel: string;
|
|
108
|
+
paginationLabel?: string;
|
|
109
|
+
paginationStatus?: (info: DataTablePaginationStatusInfo) => React.ReactNode;
|
|
110
|
+
/** Server-side total; required for accurate status in manual mode. */
|
|
111
|
+
totalRowCount?: number;
|
|
112
|
+
/** Server owns row slicing; `rows` holds exactly the current page. */
|
|
113
|
+
manualPagination?: boolean;
|
|
114
|
+
}
|
|
115
|
+
) &
|
|
116
|
+
(
|
|
117
|
+
| {
|
|
118
|
+
selection?: undefined;
|
|
119
|
+
defaultSelection?: undefined;
|
|
120
|
+
onSelectionChange?: undefined;
|
|
121
|
+
selectionMode?: undefined;
|
|
122
|
+
selectAllLabel?: undefined;
|
|
123
|
+
rowSelectionLabel?: undefined;
|
|
124
|
+
selectedCountLabel?: undefined;
|
|
125
|
+
renderSelectionActions?: undefined;
|
|
126
|
+
}
|
|
127
|
+
| {
|
|
128
|
+
/** Controlled selected row ids. */
|
|
129
|
+
selection?: readonly RowId[];
|
|
130
|
+
/** Initial selection for uncontrolled usage. */
|
|
131
|
+
defaultSelection?: readonly RowId[];
|
|
132
|
+
onSelectionChange?: (ids: RowId[]) => void;
|
|
133
|
+
selectionMode?: SelectionMode;
|
|
134
|
+
/** Accessible name for the select-all header control. */
|
|
135
|
+
selectAllLabel: string;
|
|
136
|
+
/** Accessible name factory for row selection controls. */
|
|
137
|
+
rowSelectionLabel?: (row: Row) => string;
|
|
138
|
+
selectedCountLabel?: (count: number) => React.ReactNode;
|
|
139
|
+
renderSelectionActions?: (info: DataTableSelectionInfo) => React.ReactNode;
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
export type DataTableProps<Row extends { id: string }> = {
|
|
144
|
+
columns: DataColumn<Row>[];
|
|
145
|
+
rows: Row[];
|
|
146
|
+
caption: string;
|
|
147
|
+
empty: React.ReactNode;
|
|
148
|
+
className?: string;
|
|
149
|
+
loading?: boolean;
|
|
150
|
+
/** Skeleton row count while `loading` is true. Default: 5. */
|
|
151
|
+
loadingRows?: number;
|
|
152
|
+
/** Visual rhythm: `regular` (default) or `compact` for dense workspaces. */
|
|
153
|
+
density?: 'compact' | 'regular';
|
|
154
|
+
} & DataTableFeatureProps<Row>;
|
|
155
|
+
|
|
156
|
+
/** Pagination state that keeps every row visible when paging is disabled. */
|
|
157
|
+
const ALL_ROWS_PAGINATION = { pageIndex: 0, pageSize: Number.MAX_SAFE_INTEGER };
|
|
158
|
+
|
|
159
|
+
export function DataTable<Row extends { id: string }>({
|
|
160
|
+
columns,
|
|
161
|
+
rows,
|
|
162
|
+
caption,
|
|
163
|
+
empty,
|
|
164
|
+
className,
|
|
165
|
+
loading = false,
|
|
166
|
+
loadingRows = 5,
|
|
167
|
+
density = 'regular',
|
|
168
|
+
sorting,
|
|
169
|
+
defaultSorting,
|
|
170
|
+
onSortingChange,
|
|
171
|
+
manualSorting,
|
|
172
|
+
filters,
|
|
173
|
+
defaultFilters,
|
|
174
|
+
onFiltersChange,
|
|
175
|
+
manualFiltering,
|
|
176
|
+
page,
|
|
177
|
+
onPageChange,
|
|
178
|
+
previousPageLabel,
|
|
179
|
+
nextPageLabel,
|
|
180
|
+
paginationLabel,
|
|
181
|
+
paginationStatus,
|
|
182
|
+
totalRowCount,
|
|
183
|
+
manualPagination,
|
|
184
|
+
selection,
|
|
185
|
+
defaultSelection,
|
|
186
|
+
onSelectionChange,
|
|
187
|
+
selectionMode = 'multiple',
|
|
188
|
+
selectAllLabel,
|
|
189
|
+
rowSelectionLabel,
|
|
190
|
+
selectedCountLabel,
|
|
191
|
+
renderSelectionActions,
|
|
192
|
+
}: DataTableProps<Row>) {
|
|
193
|
+
const captionId = useId();
|
|
194
|
+
const selectionEnabled = selectAllLabel !== undefined;
|
|
195
|
+
const paginationEnabled = page !== undefined;
|
|
196
|
+
|
|
197
|
+
const [internalSorting, setInternalSorting] = useState<SortingState>(() =>
|
|
198
|
+
toTanStackSorting(defaultSorting ?? []),
|
|
199
|
+
);
|
|
200
|
+
const [internalFilters, setInternalFilters] = useState<ColumnFiltersState>(() =>
|
|
201
|
+
toTanStackFilters(defaultFilters ?? []),
|
|
202
|
+
);
|
|
203
|
+
const [internalSelection, setInternalSelection] = useState<RowSelectionState>(() =>
|
|
204
|
+
toTanStackSelection(defaultSelection ?? []),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const controlledSorting = useMemo(
|
|
208
|
+
() => (sorting !== undefined ? toTanStackSorting(sorting) : undefined),
|
|
209
|
+
[sorting],
|
|
210
|
+
);
|
|
211
|
+
const controlledFilters = useMemo(
|
|
212
|
+
() => (filters !== undefined ? toTanStackFilters(filters) : undefined),
|
|
213
|
+
[filters],
|
|
214
|
+
);
|
|
215
|
+
const controlledSelection = useMemo(
|
|
216
|
+
() => (selection !== undefined ? toTanStackSelection(selection) : undefined),
|
|
217
|
+
[selection],
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const sortingState = controlledSorting ?? internalSorting;
|
|
221
|
+
const filtersState = controlledFilters ?? internalFilters;
|
|
222
|
+
const selectionState = controlledSelection ?? internalSelection;
|
|
223
|
+
const paginationState = useMemo(
|
|
224
|
+
() =>
|
|
225
|
+
page !== undefined ? { pageIndex: page.index, pageSize: page.size } : ALL_ROWS_PAGINATION,
|
|
226
|
+
[page],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const tanstackColumns = useMemo(() => toTanStackColumns(columns), [columns]);
|
|
230
|
+
|
|
231
|
+
const handleSortingChange: OnChangeFn<SortingState> = (updater) => {
|
|
232
|
+
const next = functionalUpdate(updater, sortingState);
|
|
233
|
+
setInternalSorting(next);
|
|
234
|
+
onSortingChange?.(fromTanStackSorting(next));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const handleFiltersChange: OnChangeFn<ColumnFiltersState> = (updater) => {
|
|
238
|
+
const next = functionalUpdate(updater, filtersState);
|
|
239
|
+
setInternalFilters(next);
|
|
240
|
+
onFiltersChange?.(fromTanStackFilters(next));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const handleSelectionChange: OnChangeFn<RowSelectionState> = (updater) => {
|
|
244
|
+
const next = functionalUpdate(updater, selectionState);
|
|
245
|
+
setInternalSelection(next);
|
|
246
|
+
onSelectionChange?.(fromTanStackSelection(next));
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const table = useTable({
|
|
250
|
+
features: noeTableFeatures,
|
|
251
|
+
columns: tanstackColumns,
|
|
252
|
+
data: rows,
|
|
253
|
+
getRowId: (row: Row) => row.id,
|
|
254
|
+
state: {
|
|
255
|
+
sorting: sortingState,
|
|
256
|
+
columnFilters: filtersState,
|
|
257
|
+
rowSelection: selectionState,
|
|
258
|
+
pagination: paginationState,
|
|
259
|
+
},
|
|
260
|
+
onSortingChange: handleSortingChange,
|
|
261
|
+
onColumnFiltersChange: handleFiltersChange,
|
|
262
|
+
onRowSelectionChange: handleSelectionChange,
|
|
263
|
+
enableMultiSort: false,
|
|
264
|
+
autoResetPageIndex: false,
|
|
265
|
+
...(manualSorting !== undefined ? { manualSorting } : {}),
|
|
266
|
+
...(manualFiltering !== undefined ? { manualFiltering } : {}),
|
|
267
|
+
...(manualPagination !== undefined ? { manualPagination } : {}),
|
|
268
|
+
...(totalRowCount !== undefined ? { rowCount: totalRowCount } : {}),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const modelRows = table.getRowModel().rows;
|
|
272
|
+
const selectedIds =
|
|
273
|
+
selection !== undefined ? [...selection] : fromTanStackSelection(selectionState);
|
|
274
|
+
|
|
275
|
+
const toggleRowSelection = (rowId: RowId) => {
|
|
276
|
+
const isSelected = selectedIds.includes(rowId);
|
|
277
|
+
if (selectionMode === 'single') {
|
|
278
|
+
handleSelectionChange(isSelected ? {} : toTanStackSelection([rowId]));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
handleSelectionChange(
|
|
282
|
+
toTanStackSelection(
|
|
283
|
+
isSelected ? selectedIds.filter((id) => id !== rowId) : [...selectedIds, rowId],
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const allRowsSelected = table.getIsAllRowsSelected();
|
|
289
|
+
const someRowsSelected = table.getIsSomeRowsSelected();
|
|
290
|
+
|
|
291
|
+
const pageSize = page?.size ?? ALL_ROWS_PAGINATION.pageSize;
|
|
292
|
+
const pageIndex = page?.index ?? 0;
|
|
293
|
+
const totalRows =
|
|
294
|
+
manualPagination === true
|
|
295
|
+
? (totalRowCount ?? pageIndex * pageSize + modelRows.length)
|
|
296
|
+
: table.getSortedRowModel().rows.length;
|
|
297
|
+
const canPreviousPage = paginationEnabled && pageIndex > 0;
|
|
298
|
+
const canNextPage = paginationEnabled
|
|
299
|
+
? manualPagination === true
|
|
300
|
+
? totalRowCount !== undefined
|
|
301
|
+
? (pageIndex + 1) * pageSize < totalRowCount
|
|
302
|
+
: modelRows.length === pageSize
|
|
303
|
+
: pageIndex + 1 < table.getPageCount()
|
|
304
|
+
: false;
|
|
305
|
+
const paginationInfo: DataTablePaginationStatusInfo | undefined = paginationEnabled
|
|
306
|
+
? {
|
|
307
|
+
startIndex: totalRows === 0 ? 0 : pageIndex * pageSize + 1,
|
|
308
|
+
endIndex: Math.min(totalRows, (pageIndex + 1) * pageSize),
|
|
309
|
+
total: totalRows,
|
|
310
|
+
}
|
|
311
|
+
: undefined;
|
|
312
|
+
|
|
313
|
+
const cellPadding = density === 'compact' ? 'px-3 py-1.5' : 'px-4 py-2.5';
|
|
314
|
+
|
|
315
|
+
if (!loading && modelRows.length === 0) {
|
|
316
|
+
return (
|
|
317
|
+
<div
|
|
318
|
+
className={cn(
|
|
319
|
+
'flex min-h-32 items-center justify-center rounded-lg border border-dashed border-border bg-surface-sunken/30 p-6 text-center text-sm text-muted-foreground',
|
|
320
|
+
className,
|
|
321
|
+
)}
|
|
322
|
+
data-slot="data-table-empty"
|
|
323
|
+
>
|
|
324
|
+
{empty}
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const showSelectionBar =
|
|
330
|
+
selectionEnabled &&
|
|
331
|
+
selectedIds.length > 0 &&
|
|
332
|
+
(selectedCountLabel !== undefined || renderSelectionActions !== undefined);
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div className={cn('flex flex-col gap-3', className)} data-slot="data-table-root">
|
|
336
|
+
{showSelectionBar ? (
|
|
337
|
+
<div
|
|
338
|
+
className="flex items-center gap-3 rounded-md border border-border bg-surface-sunken/40 px-4 py-2.5 text-sm"
|
|
339
|
+
data-slot="data-table-selection-bar"
|
|
340
|
+
>
|
|
341
|
+
{selectedCountLabel ? (
|
|
342
|
+
<span className="tabular-nums text-muted-foreground">
|
|
343
|
+
{selectedCountLabel(selectedIds.length)}
|
|
344
|
+
</span>
|
|
345
|
+
) : null}
|
|
346
|
+
{renderSelectionActions ? (
|
|
347
|
+
<div className="ms-auto flex items-center gap-2">
|
|
348
|
+
{renderSelectionActions({ selectedIds })}
|
|
349
|
+
</div>
|
|
350
|
+
) : null}
|
|
351
|
+
</div>
|
|
352
|
+
) : null}
|
|
353
|
+
{/* WCAG 2.1: keyboard-accessible scrollable container for overflow tables. */}
|
|
354
|
+
<section
|
|
355
|
+
aria-labelledby={captionId}
|
|
356
|
+
className="overflow-x-auto rounded-lg border border-border"
|
|
357
|
+
data-slot="data-table-scroll"
|
|
358
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable region focus pattern
|
|
359
|
+
tabIndex={0}
|
|
360
|
+
>
|
|
361
|
+
<table
|
|
362
|
+
className="w-full min-w-full border-collapse text-sm"
|
|
363
|
+
data-density={density}
|
|
364
|
+
data-slot="data-table"
|
|
365
|
+
>
|
|
366
|
+
<caption className="sr-only" id={captionId}>
|
|
367
|
+
{caption}
|
|
368
|
+
</caption>
|
|
369
|
+
<thead>
|
|
370
|
+
<tr className="border-b border-border bg-surface-sunken/60">
|
|
371
|
+
{selectionEnabled && selectionMode === 'multiple' ? (
|
|
372
|
+
<th className={cn('w-10', cellPadding)} data-slot="data-table-select-header">
|
|
373
|
+
<Checkbox
|
|
374
|
+
aria-label={selectAllLabel}
|
|
375
|
+
checked={allRowsSelected}
|
|
376
|
+
onChange={() => table.toggleAllRowsSelected(!allRowsSelected)}
|
|
377
|
+
ref={(element) => {
|
|
378
|
+
if (element) element.indeterminate = someRowsSelected && !allRowsSelected;
|
|
379
|
+
}}
|
|
380
|
+
/>
|
|
381
|
+
</th>
|
|
382
|
+
) : null}
|
|
383
|
+
{columns.map((column) => {
|
|
384
|
+
const tanstackColumn = table.getColumn(column.key);
|
|
385
|
+
const canSort = tanstackColumn?.getCanSort() ?? false;
|
|
386
|
+
const sorted = tanstackColumn?.getIsSorted() ?? false;
|
|
387
|
+
return (
|
|
388
|
+
<th
|
|
389
|
+
aria-sort={
|
|
390
|
+
sorted === 'asc'
|
|
391
|
+
? 'ascending'
|
|
392
|
+
: sorted === 'desc'
|
|
393
|
+
? 'descending'
|
|
394
|
+
: canSort
|
|
395
|
+
? 'none'
|
|
396
|
+
: undefined
|
|
397
|
+
}
|
|
398
|
+
className={cn(
|
|
399
|
+
'whitespace-nowrap text-xs font-medium text-muted-foreground',
|
|
400
|
+
cellPadding,
|
|
401
|
+
column.align === 'end' ? 'text-end' : 'text-start',
|
|
402
|
+
)}
|
|
403
|
+
data-align={column.align ?? 'start'}
|
|
404
|
+
key={column.key}
|
|
405
|
+
scope="col"
|
|
406
|
+
>
|
|
407
|
+
{canSort && tanstackColumn ? (
|
|
408
|
+
<Button
|
|
409
|
+
className="-ms-2 text-muted-foreground hover:text-foreground"
|
|
410
|
+
data-slot="data-table-sort-button"
|
|
411
|
+
onClick={(event) => tanstackColumn.getToggleSortingHandler()?.(event)}
|
|
412
|
+
size="sm"
|
|
413
|
+
variant="ghost"
|
|
414
|
+
>
|
|
415
|
+
{column.header}
|
|
416
|
+
{sorted === 'desc' ? (
|
|
417
|
+
<ArrowDown2 className="size-3.5" />
|
|
418
|
+
) : (
|
|
419
|
+
<ArrowUp2 className={cn('size-3.5', sorted === false && 'opacity-40')} />
|
|
420
|
+
)}
|
|
421
|
+
</Button>
|
|
422
|
+
) : (
|
|
423
|
+
column.header
|
|
424
|
+
)}
|
|
425
|
+
</th>
|
|
426
|
+
);
|
|
427
|
+
})}
|
|
428
|
+
</tr>
|
|
429
|
+
</thead>
|
|
430
|
+
<tbody>
|
|
431
|
+
{loading
|
|
432
|
+
? Array.from(
|
|
433
|
+
{ length: loadingRows },
|
|
434
|
+
(_, index) => `data-table-loading-${index}`,
|
|
435
|
+
).map((rowKey) => (
|
|
436
|
+
<tr
|
|
437
|
+
className="border-b border-border last:border-b-0"
|
|
438
|
+
data-slot="data-table-loading-row"
|
|
439
|
+
key={rowKey}
|
|
440
|
+
>
|
|
441
|
+
{selectionEnabled ? (
|
|
442
|
+
<td className={cellPadding}>
|
|
443
|
+
<Skeleton className="size-4" />
|
|
444
|
+
</td>
|
|
445
|
+
) : null}
|
|
446
|
+
{columns.map((column) => (
|
|
447
|
+
<td className={cellPadding} key={column.key}>
|
|
448
|
+
<Skeleton className="h-4 w-full max-w-32" />
|
|
449
|
+
</td>
|
|
450
|
+
))}
|
|
451
|
+
</tr>
|
|
452
|
+
))
|
|
453
|
+
: modelRows.map((tanstackRow) => {
|
|
454
|
+
const row = tanstackRow.original;
|
|
455
|
+
const isSelected = tanstackRow.getIsSelected();
|
|
456
|
+
return (
|
|
457
|
+
<tr
|
|
458
|
+
className="border-b border-border last:border-b-0 hover:bg-muted/50"
|
|
459
|
+
data-selected={isSelected || undefined}
|
|
460
|
+
key={tanstackRow.id}
|
|
461
|
+
>
|
|
462
|
+
{selectionEnabled ? (
|
|
463
|
+
<td className={cellPadding} data-slot="data-table-select-cell">
|
|
464
|
+
<Checkbox
|
|
465
|
+
aria-label={rowSelectionLabel?.(row)}
|
|
466
|
+
checked={isSelected}
|
|
467
|
+
onChange={() => toggleRowSelection(tanstackRow.id)}
|
|
468
|
+
/>
|
|
469
|
+
</td>
|
|
470
|
+
) : null}
|
|
471
|
+
{columns.map((column) => {
|
|
472
|
+
const content = column.render
|
|
473
|
+
? column.render(row)
|
|
474
|
+
: String(row[column.key] ?? '');
|
|
475
|
+
const value = column.technical ? (
|
|
476
|
+
<TechnicalValue>{content}</TechnicalValue>
|
|
477
|
+
) : (
|
|
478
|
+
content
|
|
479
|
+
);
|
|
480
|
+
return (
|
|
481
|
+
<td
|
|
482
|
+
className={cn(
|
|
483
|
+
'whitespace-nowrap text-foreground',
|
|
484
|
+
cellPadding,
|
|
485
|
+
column.align === 'end' ? 'text-end' : 'text-start',
|
|
486
|
+
)}
|
|
487
|
+
data-align={column.align ?? 'start'}
|
|
488
|
+
key={column.key}
|
|
489
|
+
>
|
|
490
|
+
{value}
|
|
491
|
+
</td>
|
|
492
|
+
);
|
|
493
|
+
})}
|
|
494
|
+
</tr>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</tbody>
|
|
498
|
+
</table>
|
|
499
|
+
</section>
|
|
500
|
+
{paginationEnabled && paginationInfo ? (
|
|
501
|
+
<Pagination
|
|
502
|
+
canNext={canNextPage}
|
|
503
|
+
canPrevious={canPreviousPage}
|
|
504
|
+
label={paginationLabel ?? caption}
|
|
505
|
+
nextLabel={nextPageLabel}
|
|
506
|
+
onNext={
|
|
507
|
+
canNextPage ? () => onPageChange({ index: pageIndex + 1, size: pageSize }) : undefined
|
|
508
|
+
}
|
|
509
|
+
onPrevious={
|
|
510
|
+
canPreviousPage
|
|
511
|
+
? () => onPageChange({ index: pageIndex - 1, size: pageSize })
|
|
512
|
+
: undefined
|
|
513
|
+
}
|
|
514
|
+
previousLabel={previousPageLabel}
|
|
515
|
+
status={
|
|
516
|
+
paginationStatus
|
|
517
|
+
? paginationStatus(paginationInfo)
|
|
518
|
+
: `${paginationInfo.startIndex}–${paginationInfo.endIndex} / ${paginationInfo.total}`
|
|
519
|
+
}
|
|
520
|
+
/>
|
|
521
|
+
) : null}
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
--noe-primitive-color-ink-500: #565c58;
|
|
14
14
|
--noe-primitive-color-ink-600: #302f2e;
|
|
15
15
|
--noe-primitive-color-ink-700: #252423;
|
|
16
|
-
--noe-primitive-color-ink-800: #
|
|
17
|
-
--noe-primitive-color-ink-850: #
|
|
16
|
+
--noe-primitive-color-ink-800: #2a2a2a;
|
|
17
|
+
--noe-primitive-color-ink-850: #202020;
|
|
18
18
|
--noe-primitive-color-ink-900: #171918;
|
|
19
|
-
--noe-primitive-color-ink-950: #
|
|
20
|
-
--noe-primitive-color-ink-1000: #
|
|
19
|
+
--noe-primitive-color-ink-950: #161616;
|
|
20
|
+
--noe-primitive-color-ink-1000: #111111;
|
|
21
21
|
--noe-primitive-color-mint-100: #98e8cf;
|
|
22
22
|
--noe-primitive-color-mint-200: #abefd9;
|
|
23
23
|
--noe-primitive-color-mint-300: #1dbf98;
|
|
@@ -140,6 +140,8 @@
|
|
|
140
140
|
--noe-color-surface: var(--noe-primitive-color-paper-50);
|
|
141
141
|
--noe-color-surface-raised: var(--noe-primitive-color-paper-0);
|
|
142
142
|
--noe-color-surface-sunken: var(--noe-primitive-color-paper-200);
|
|
143
|
+
--noe-color-sidebar: var(--noe-primitive-color-paper-200);
|
|
144
|
+
--noe-color-sidebar-active: var(--noe-primitive-color-paper-0);
|
|
143
145
|
--noe-color-foreground: var(--noe-primitive-color-ink-900);
|
|
144
146
|
--noe-color-muted: var(--noe-primitive-color-paper-200);
|
|
145
147
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-500);
|
|
@@ -170,6 +172,8 @@
|
|
|
170
172
|
--noe-color-surface: var(--noe-primitive-color-ink-850);
|
|
171
173
|
--noe-color-surface-raised: var(--noe-primitive-color-ink-800);
|
|
172
174
|
--noe-color-surface-sunken: var(--noe-primitive-color-ink-1000);
|
|
175
|
+
--noe-color-sidebar: var(--noe-primitive-color-ink-850);
|
|
176
|
+
--noe-color-sidebar-active: var(--noe-primitive-color-ink-800);
|
|
173
177
|
--noe-color-foreground: var(--noe-primitive-color-paper-250);
|
|
174
178
|
--noe-color-muted: var(--noe-primitive-color-ink-800);
|
|
175
179
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-400);
|
|
@@ -201,6 +205,8 @@
|
|
|
201
205
|
--noe-color-surface: var(--noe-primitive-color-ink-850);
|
|
202
206
|
--noe-color-surface-raised: var(--noe-primitive-color-ink-800);
|
|
203
207
|
--noe-color-surface-sunken: var(--noe-primitive-color-ink-1000);
|
|
208
|
+
--noe-color-sidebar: var(--noe-primitive-color-ink-850);
|
|
209
|
+
--noe-color-sidebar-active: var(--noe-primitive-color-ink-800);
|
|
204
210
|
--noe-color-foreground: var(--noe-primitive-color-paper-250);
|
|
205
211
|
--noe-color-muted: var(--noe-primitive-color-ink-800);
|
|
206
212
|
--noe-color-muted-foreground: var(--noe-primitive-color-ink-400);
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"500": "#565c58",
|
|
16
16
|
"600": "#302f2e",
|
|
17
17
|
"700": "#252423",
|
|
18
|
-
"800": "#
|
|
19
|
-
"850": "#
|
|
18
|
+
"800": "#2a2a2a",
|
|
19
|
+
"850": "#202020",
|
|
20
20
|
"900": "#171918",
|
|
21
|
-
"950": "#
|
|
22
|
-
"1000": "#
|
|
21
|
+
"950": "#161616",
|
|
22
|
+
"1000": "#111111"
|
|
23
23
|
},
|
|
24
24
|
"mint": {
|
|
25
25
|
"100": "#98e8cf",
|
|
@@ -106,6 +106,8 @@
|
|
|
106
106
|
"surface": "{primitive.color.paper.50}",
|
|
107
107
|
"surfaceRaised": "{primitive.color.paper.0}",
|
|
108
108
|
"surfaceSunken": "{primitive.color.paper.200}",
|
|
109
|
+
"sidebar": "{primitive.color.paper.200}",
|
|
110
|
+
"sidebarActive": "{primitive.color.paper.0}",
|
|
109
111
|
"foreground": "{primitive.color.ink.900}",
|
|
110
112
|
"muted": "{primitive.color.paper.200}",
|
|
111
113
|
"mutedForeground": "{primitive.color.ink.500}",
|
|
@@ -131,6 +133,8 @@
|
|
|
131
133
|
"surface": "{primitive.color.ink.850}",
|
|
132
134
|
"surfaceRaised": "{primitive.color.ink.800}",
|
|
133
135
|
"surfaceSunken": "{primitive.color.ink.1000}",
|
|
136
|
+
"sidebar": "{primitive.color.ink.850}",
|
|
137
|
+
"sidebarActive": "{primitive.color.ink.800}",
|
|
134
138
|
"foreground": "{primitive.color.paper.250}",
|
|
135
139
|
"muted": "{primitive.color.ink.800}",
|
|
136
140
|
"mutedForeground": "{primitive.color.ink.400}",
|
|
@@ -16,11 +16,11 @@ export const tokens = {
|
|
|
16
16
|
"500": "#565c58",
|
|
17
17
|
"600": "#302f2e",
|
|
18
18
|
"700": "#252423",
|
|
19
|
-
"800": "#
|
|
20
|
-
"850": "#
|
|
19
|
+
"800": "#2a2a2a",
|
|
20
|
+
"850": "#202020",
|
|
21
21
|
"900": "#171918",
|
|
22
|
-
"950": "#
|
|
23
|
-
"1000": "#
|
|
22
|
+
"950": "#161616",
|
|
23
|
+
"1000": "#111111"
|
|
24
24
|
},
|
|
25
25
|
"mint": {
|
|
26
26
|
"100": "#98e8cf",
|
|
@@ -107,6 +107,8 @@ export const tokens = {
|
|
|
107
107
|
"surface": "{primitive.color.paper.50}",
|
|
108
108
|
"surfaceRaised": "{primitive.color.paper.0}",
|
|
109
109
|
"surfaceSunken": "{primitive.color.paper.200}",
|
|
110
|
+
"sidebar": "{primitive.color.paper.200}",
|
|
111
|
+
"sidebarActive": "{primitive.color.paper.0}",
|
|
110
112
|
"foreground": "{primitive.color.ink.900}",
|
|
111
113
|
"muted": "{primitive.color.paper.200}",
|
|
112
114
|
"mutedForeground": "{primitive.color.ink.500}",
|
|
@@ -132,6 +134,8 @@ export const tokens = {
|
|
|
132
134
|
"surface": "{primitive.color.ink.850}",
|
|
133
135
|
"surfaceRaised": "{primitive.color.ink.800}",
|
|
134
136
|
"surfaceSunken": "{primitive.color.ink.1000}",
|
|
137
|
+
"sidebar": "{primitive.color.ink.850}",
|
|
138
|
+
"sidebarActive": "{primitive.color.ink.800}",
|
|
135
139
|
"foreground": "{primitive.color.paper.250}",
|
|
136
140
|
"muted": "{primitive.color.ink.800}",
|
|
137
141
|
"mutedForeground": "{primitive.color.ink.400}",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"generatedAt": "2026-08-30T12:09:40.539Z",
|
|
3
3
|
"items": [
|
|
4
4
|
{
|
|
5
5
|
"id": "bidi-text",
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
"package": "@noe/data-ui",
|
|
209
209
|
"status": "available",
|
|
210
210
|
"maturity": "beta",
|
|
211
|
-
"description": "
|
|
211
|
+
"description": "Semantic data table with TanStack Table v9 as a headless engine behind the internal NOE adapter: sorting, NOE filter operators, controlled pagination, stable-id selection, loading skeletons and server mode.",
|
|
212
212
|
"registryItem": "data-table"
|
|
213
213
|
},
|
|
214
214
|
{
|