@gntik-ai/blocks 0.5.0 → 0.7.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/dist/index.d.ts +337 -97
- package/dist/index.js +1311 -384
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/SignUpForm/SignUpForm.configured.test.tsx +316 -0
- package/src/auth/SignUpForm/SignUpForm.tsx +194 -50
- package/src/auth/SignUpForm/block.meta.ts +15 -2
- package/src/auth/SignUpForm/examples/CustomFields.tsx +56 -0
- package/src/auth/SignUpForm/examples/Error.tsx +5 -0
- package/src/auth/SignUpForm/examples/Loading.tsx +6 -0
- package/src/auth/SignUpForm/examples/NoTerms.tsx +6 -0
- package/src/auth/SignUpForm/examples/Submitting.tsx +5 -0
- package/src/auth/SignUpForm/examples/Unavailable.tsx +15 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/shared.tsx +34 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.examples.test.tsx +25 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.extended.test.tsx +252 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.focus.test.tsx +178 -0
- package/src/feedback/ConfirmDestructive/ConfirmDestructive.tsx +219 -28
- package/src/feedback/ConfirmDestructive/block.meta.ts +2 -2
- package/src/feedback/ConfirmDestructive/examples/FailureStates.tsx +39 -0
- package/src/feedback/ConfirmDestructive/examples/PlainDeletion.tsx +6 -0
- package/src/feedback/ConfirmDestructive/examples/SecretRevocation.tsx +34 -0
- package/src/feedback/EmptyStates/EmptyStates.test.tsx +11 -1
- package/src/feedback/EmptyStates/EmptyStates.tsx +31 -1
- package/src/feedback/EmptyStates/block.meta.ts +1 -1
- package/src/feedback/EmptyStates/fixtures.ts +5 -0
- package/src/feedback/ErrorPanel/ErrorPanel.test.tsx +7 -0
- package/src/feedback/ErrorPanel/ErrorPanel.tsx +8 -3
- package/src/meta.ts +2 -0
- package/src/tables/CursorPagerFooter/CursorPagerFooter.examples.test.tsx +33 -0
- package/src/tables/CursorPagerFooter/CursorPagerFooter.test.tsx +51 -0
- package/src/tables/CursorPagerFooter/CursorPagerFooter.tsx +10 -0
- package/src/tables/CursorPagerFooter/block.meta.ts +23 -0
- package/src/tables/CursorPagerFooter/examples/Busy.tsx +7 -0
- package/src/tables/CursorPagerFooter/examples/CursorStack.tsx +40 -0
- package/src/tables/CursorPagerFooter/examples/Hidden.tsx +16 -0
- package/src/tables/CursorPagerFooter/examples/RowsPerPage.tsx +21 -0
- package/src/tables/CursorPagerFooter/fixtures.ts +13 -0
- package/src/tables/DataTable/DataTable.labels.test.tsx +291 -0
- package/src/tables/DataTable/DataTable.overflow-labels.test.tsx +59 -0
- package/src/tables/DataTable/DataTable.server-pagination.test.tsx +173 -0
- package/src/tables/DataTable/DataTable.tsx +40 -21
- package/src/tables/DataTable/block.meta.ts +104 -1
- package/src/tables/DataTable/column-menu.tsx +10 -7
- package/src/tables/DataTable/data-table-body.tsx +3 -2
- package/src/tables/DataTable/data-table-props.ts +30 -4
- package/src/tables/DataTable/data-table-row.tsx +15 -4
- package/src/tables/DataTable/density-toggle.tsx +7 -4
- package/src/tables/DataTable/editable-cell.tsx +21 -8
- package/src/tables/DataTable/examples/server-pagination.tsx +68 -0
- package/src/tables/DataTable/header-cell.tsx +4 -0
- package/src/tables/DataTable/labels.ts +54 -0
- package/src/tables/DataTable/resize-handle.tsx +6 -3
- package/src/tables/DataTable/views.tsx +21 -14
- package/src/tables/FilterBar/FilterBar.labels.test.tsx +69 -0
- package/src/tables/FilterBar/FilterBar.tsx +45 -12
- package/src/tables/FilterBar/block.meta.ts +22 -0
- package/src/tables/LoadMoreFooter/LoadMoreFooter.announcements.test.tsx +106 -0
- package/src/tables/LoadMoreFooter/LoadMoreFooter.examples.test.tsx +36 -0
- package/src/tables/LoadMoreFooter/LoadMoreFooter.labels.test.tsx +71 -0
- package/src/tables/LoadMoreFooter/LoadMoreFooter.test.tsx +184 -0
- package/src/tables/LoadMoreFooter/LoadMoreFooter.tsx +151 -0
- package/src/tables/LoadMoreFooter/block.meta.ts +28 -0
- package/src/tables/LoadMoreFooter/examples/cards.tsx +22 -0
- package/src/tables/LoadMoreFooter/examples/cursor.tsx +25 -0
- package/src/tables/LoadMoreFooter/examples/exhausted.tsx +6 -0
- package/src/tables/LoadMoreFooter/examples/failed.tsx +13 -0
- package/src/tables/LoadMoreFooter/examples/flag.tsx +33 -0
- package/src/tables/LoadMoreFooter/examples/useMockCursor.ts +30 -0
- package/src/tables/LoadMoreFooter/fixtures.ts +9 -0
- package/src/tables/LoadMoreFooter/useLoadMoreFooter.ts +56 -0
- package/src/tables/PaginationFooter/PaginationFooter.labels.test.tsx +57 -0
- package/src/tables/PaginationFooter/PaginationFooter.tsx +31 -7
- package/src/tables/PaginationFooter/block.meta.ts +23 -0
- package/src/tables/index.ts +4 -0
|
@@ -48,6 +48,7 @@ import { useControllable, useTableViews } from './use-table-views';
|
|
|
48
48
|
import { useVirtualRows } from './use-virtual-rows';
|
|
49
49
|
import { ViewSwitcher, type DataTableViewState } from './views';
|
|
50
50
|
|
|
51
|
+
export type { DataTableLabels } from './labels';
|
|
51
52
|
export type { DataTableProps } from './data-table-props';
|
|
52
53
|
|
|
53
54
|
const defaultRowId = (row: unknown): string => {
|
|
@@ -92,6 +93,12 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
92
93
|
onSortChange,
|
|
93
94
|
manualSorting = false,
|
|
94
95
|
paginated: paginatedProp,
|
|
96
|
+
paginationMode = 'client',
|
|
97
|
+
rowCount,
|
|
98
|
+
page: controlledPage,
|
|
99
|
+
pageSize: controlledPageSize,
|
|
100
|
+
onPageChange,
|
|
101
|
+
onPageSizeChange,
|
|
95
102
|
defaultPageSize = 10,
|
|
96
103
|
pageSizeOptions = [10, 25, 50],
|
|
97
104
|
density: densityProp,
|
|
@@ -135,9 +142,11 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
135
142
|
onFiltersChange,
|
|
136
143
|
toolbar,
|
|
137
144
|
className,
|
|
145
|
+
labels,
|
|
138
146
|
}: DataTableProps<T>) {
|
|
139
147
|
const { t } = useI18n();
|
|
140
148
|
const singleMode = selectionMode === 'single';
|
|
149
|
+
const serverMode = paginationMode === 'server';
|
|
141
150
|
const selectable = selectionMode === undefined ? selectableProp : selectionMode === 'multiple';
|
|
142
151
|
const selectionColumn = selectable || singleMode;
|
|
143
152
|
const paginated = !singleMode && (paginatedProp ?? true);
|
|
@@ -196,10 +205,10 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
196
205
|
const [innerCollapsed, setInnerCollapsed] = useState<ReadonlySet<string>>(() => new Set(defaultCollapsedGroups));
|
|
197
206
|
const collapsed: ReadonlySet<string> = collapsedProp ? new Set(collapsedProp) : innerCollapsed;
|
|
198
207
|
|
|
199
|
-
const sortedRows = manualSorting ? [...rows] : sortRows(rows, columns, sort);
|
|
208
|
+
const sortedRows = serverMode ? rows : manualSorting ? [...rows] : sortRows(rows, columns, sort);
|
|
200
209
|
const allGroups = effectiveGroupBy ? groupRows(sortedRows, effectiveGroupBy, rows) : null;
|
|
201
210
|
const sorted = allGroups ? allGroups.flatMap((g) => g.rows) : sortedRows;
|
|
202
|
-
const pageRows = paginated ? paginate(sorted, page, pageSize) : sorted;
|
|
211
|
+
const pageRows = serverMode ? rows : paginated ? paginate(sorted, page, pageSize) : sorted;
|
|
203
212
|
// Rows in collapsed groups are hidden, so select-all leaves them alone.
|
|
204
213
|
const pageIds = (
|
|
205
214
|
effectiveGroupBy ? pageRows.filter((row) => !collapsed.has(groupKeyOf(row, effectiveGroupBy))) : pageRows
|
|
@@ -212,6 +221,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
212
221
|
onSelectionChange?.(rows.map(getRowId).filter((id) => next.has(id)));
|
|
213
222
|
};
|
|
214
223
|
const changeSort = (columnId: string, direction: SortState['direction']) => {
|
|
224
|
+
if (serverMode) return;
|
|
215
225
|
const next: SortState = { columnId, direction: sort?.columnId === columnId ? direction : 'ascending' };
|
|
216
226
|
setInnerSort(next);
|
|
217
227
|
onSortChange?.(next);
|
|
@@ -224,7 +234,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
224
234
|
setInnerCollapsed(next);
|
|
225
235
|
onCollapsedGroupsChange?.([...next]);
|
|
226
236
|
};
|
|
227
|
-
const nameOf = (key: string) => key || 'No group';
|
|
237
|
+
const nameOf = (key: string) => key || (labels?.noGroup ?? 'No group');
|
|
228
238
|
const bodyGroups: Array<DataTableBodyGroup<T>> | undefined =
|
|
229
239
|
effectiveGroupBy && allGroups
|
|
230
240
|
? allGroups
|
|
@@ -291,7 +301,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
291
301
|
const applyState = (state: DataTableViewState) => {
|
|
292
302
|
const nextSort = state.sort ?? null;
|
|
293
303
|
setInnerSort(nextSort);
|
|
294
|
-
if (nextSort) onSortChange?.(nextSort);
|
|
304
|
+
if (nextSort && !serverMode) onSortChange?.(nextSort);
|
|
295
305
|
setHiddenList(state.hiddenColumns ?? defaultHidden);
|
|
296
306
|
setWidths(state.columnWidths ?? defaultColumnWidths);
|
|
297
307
|
setPinned(state.pinnedColumns ?? defaultPinnedColumns);
|
|
@@ -299,7 +309,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
299
309
|
setGroupOverride(nextGroup === groupName(groupBy) && typeof groupBy !== 'function' ? undefined : nextGroup);
|
|
300
310
|
if (nextGroup !== currentState.groupBy) onGroupByChange?.(nextGroup);
|
|
301
311
|
if (state.filters !== filters) onFiltersChange?.(state.filters);
|
|
302
|
-
setPage(1);
|
|
312
|
+
if (!serverMode) setPage(1);
|
|
303
313
|
};
|
|
304
314
|
const viewsApi = useTableViews({
|
|
305
315
|
views: viewsProp,
|
|
@@ -349,7 +359,11 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
349
359
|
<>
|
|
350
360
|
{selectionSummary && (
|
|
351
361
|
<span className="text-[12.5px] font-semibold text-foreground" aria-live="polite">
|
|
352
|
-
{
|
|
362
|
+
{labels?.selectionCount ? (
|
|
363
|
+
labels.selectionCount(selectedList.length)
|
|
364
|
+
) : (
|
|
365
|
+
<>{selectedList.length} selected</>
|
|
366
|
+
)}
|
|
353
367
|
</span>
|
|
354
368
|
)}
|
|
355
369
|
{selectionSummary && bulkActions && <span aria-hidden className="h-4 w-px bg-border" />}
|
|
@@ -362,6 +376,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
362
376
|
<div className="flex items-center gap-2">
|
|
363
377
|
{showViews && (
|
|
364
378
|
<ViewSwitcher
|
|
379
|
+
labels={labels}
|
|
365
380
|
views={viewsApi.views}
|
|
366
381
|
activeId={viewsApi.activeId}
|
|
367
382
|
dirty={viewsApi.dirty}
|
|
@@ -373,9 +388,10 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
373
388
|
onReset={viewsApi.reset}
|
|
374
389
|
/>
|
|
375
390
|
)}
|
|
376
|
-
{showDensityToggle && <DensityToggle value={density} onChange={changeDensity} />}
|
|
391
|
+
{showDensityToggle && <DensityToggle labels={labels} value={density} onChange={changeDensity} />}
|
|
377
392
|
{showColumnMenu && (
|
|
378
393
|
<ColumnVisibilityMenu
|
|
394
|
+
labels={labels}
|
|
379
395
|
columns={columns}
|
|
380
396
|
hidden={hidden}
|
|
381
397
|
onToggle={toggleColumn}
|
|
@@ -388,7 +404,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
388
404
|
)}
|
|
389
405
|
{anyEditable && (
|
|
390
406
|
<span id={editHintId} className="sr-only">
|
|
391
|
-
Press Enter or F2 to edit.
|
|
407
|
+
{labels?.editHint ?? 'Press Enter or F2 to edit.'}
|
|
392
408
|
</span>
|
|
393
409
|
)}
|
|
394
410
|
{/* The toggle drives a density context for the table: its rows and the controls inside them. */}
|
|
@@ -400,7 +416,7 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
400
416
|
aria-rowcount={virtual ? items + 1 : undefined}
|
|
401
417
|
stickyHeader={virtualOn}
|
|
402
418
|
containerClassName={virtualOn ? virtualContainerClassName : undefined}
|
|
403
|
-
containerLabel={virtualOn ? caption :
|
|
419
|
+
containerLabel={virtualOn ? caption : labels?.scrollRegion}
|
|
404
420
|
// Keep single-mode resize handles inside their cells. Overflow at the
|
|
405
421
|
// final column would add a scroll-container stop before the header.
|
|
406
422
|
className={cn(
|
|
@@ -423,10 +439,10 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
423
439
|
)}
|
|
424
440
|
style={layout.lead.style}
|
|
425
441
|
>
|
|
426
|
-
{singleMode && <span className="sr-only">{t('table.selection')}</span>}
|
|
442
|
+
{singleMode && <span className="sr-only">{labels?.selection ?? t('table.selection')}</span>}
|
|
427
443
|
{selectable && (
|
|
428
444
|
<Checkbox
|
|
429
|
-
aria-label={t('table.selectAllPage')}
|
|
445
|
+
aria-label={labels?.selectAll ?? t('table.selectAllPage')}
|
|
430
446
|
checked={header.checked}
|
|
431
447
|
indeterminate={header.indeterminate}
|
|
432
448
|
disabled={loading || pageIds.length === 0}
|
|
@@ -439,10 +455,11 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
439
455
|
const bounds = widthBounds(c);
|
|
440
456
|
return (
|
|
441
457
|
<DataTableHeadCell
|
|
458
|
+
labels={labels}
|
|
442
459
|
key={c.id}
|
|
443
|
-
column={c}
|
|
460
|
+
column={serverMode ? { ...c, sortable: false } : c}
|
|
444
461
|
layout={layout.columns.get(c.id)}
|
|
445
|
-
sortDirection={sort?.columnId === c.id ? sort.direction : 'none'}
|
|
462
|
+
sortDirection={!serverMode && sort?.columnId === c.id ? sort.direction : 'none'}
|
|
446
463
|
onSortChange={(dir) => changeSort(c.id, dir)}
|
|
447
464
|
resize={
|
|
448
465
|
resizable && c.resizable !== false
|
|
@@ -467,12 +484,13 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
467
484
|
)}
|
|
468
485
|
style={layout.trail.style}
|
|
469
486
|
>
|
|
470
|
-
<span className="sr-only">{t('table.actions')}</span>
|
|
487
|
+
<span className="sr-only">{labels?.actions ?? t('table.actions')}</span>
|
|
471
488
|
</TableHead>
|
|
472
489
|
)}
|
|
473
490
|
</TableRow>
|
|
474
491
|
</TableHeader>
|
|
475
492
|
<DataTableBody
|
|
493
|
+
labels={labels}
|
|
476
494
|
rows={pageRows}
|
|
477
495
|
columns={visibleColumns}
|
|
478
496
|
getRowId={getRowId}
|
|
@@ -500,17 +518,18 @@ export function DataTable<T = (typeof DEPLOYMENT_ROWS)[number]>({
|
|
|
500
518
|
</SelectionGroup>
|
|
501
519
|
{loading && (
|
|
502
520
|
<p role="status" className="sr-only">
|
|
503
|
-
Loading {caption.toLowerCase()}
|
|
521
|
+
{labels?.loading ? labels.loading(caption) : <>Loading {caption.toLowerCase()}…</>}
|
|
504
522
|
</p>
|
|
505
523
|
)}
|
|
506
|
-
{paginated && !loading && rows.length > 0 && (
|
|
524
|
+
{paginated && !loading && (serverMode || rows.length > 0) && (
|
|
507
525
|
<PaginationFooter
|
|
526
|
+
labels={labels}
|
|
508
527
|
className="border-t border-border px-4 py-3"
|
|
509
|
-
total={rows.length}
|
|
510
|
-
page={page}
|
|
511
|
-
onPageChange={setPage}
|
|
512
|
-
pageSize={pageSize}
|
|
513
|
-
onPageSizeChange={setPageSize}
|
|
528
|
+
total={serverMode ? rowCount : rows.length}
|
|
529
|
+
page={serverMode ? controlledPage : page}
|
|
530
|
+
onPageChange={serverMode ? onPageChange : setPage}
|
|
531
|
+
pageSize={serverMode ? controlledPageSize : pageSize}
|
|
532
|
+
onPageSizeChange={serverMode ? onPageSizeChange : setPageSize}
|
|
514
533
|
pageSizeOptions={pageSizeOptions}
|
|
515
534
|
noun={caption}
|
|
516
535
|
/>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockMeta } from '../../meta';
|
|
2
|
+
import type { DataTableLabels } from './labels';
|
|
2
3
|
|
|
3
4
|
export const meta: BlockMeta = {
|
|
4
5
|
name: 'Data table',
|
|
@@ -7,8 +8,110 @@ export const meta: BlockMeta = {
|
|
|
7
8
|
description:
|
|
8
9
|
'Generic typed table: sortable headers with aria-sort, row selection with select-all and indeterminate by default (selectionMode="multiple"), selectionMode="none" to hide selection, column visibility menu, density toggle, row actions, loading and empty states, client-side sort and pagination, optional row groups (groupBy) with rowgroup header rows, counts, keyboard-accessible collapse buttons and group selection; column resize (pointer drag or a focusable role="separator" handle with arrow keys, min/max widths, controlled columnWidths), column pinning to either edge with sticky cells (column menu or pinnedColumns), row virtualization for large datasets (aria-rowcount/aria-rowindex, works with selection and grouping), inline cell editing (text, number and select editors; Enter/F2 to edit, Enter/Tab to commit, Escape to cancel, onCellEdit can reject with an inline error) and saved views (filters, sort, visible columns, widths, pinning and grouping) with a view switcher to save as, rename, delete and reset. ' +
|
|
9
10
|
'Opt-in selectionMode="single" uses native radios in a labelled fieldset (never paginated or virtualized; paginated defaults to false and paginated=true is a type error). Single mode uses selectedId/onSelectedIdChange or defaultSelectedId; isRowDisabled skips unavailable rows and preserves a checked disabled choice without callbacks. Supply legend, aria-label or aria-labelledby for the group and getRowLabel for each radio (otherwise the first column labels it). selectionName is used verbatim and must be unique per page; omitted names use an instance-unique useId name, and duplicate supplied names or missing group labels warn in development. Tab enters once on the checked enabled row or first enabled row; arrows wrap and skip disabled rows, Space selects, Shift+Tab exits, and all-disabled groups have no tab stop. No select-all, group checkboxes, bulk actions or selection summary appear in single mode. ' +
|
|
10
|
-
'Examples in examples/single-selection.tsx: an uncontrolled non-paginated list with legend="Available items" and a controlled versions table with legend="Available versions", selectedId="v2" and a checked disabled version.'
|
|
11
|
+
'Examples in examples/single-selection.tsx: an uncontrolled non-paginated list with legend="Available items" and a controlled versions table with legend="Available versions", selectedId="v2" and a checked disabled version. ' +
|
|
12
|
+
'Opt-in paginationMode="server" accepts a controlled 1-based page, pageSize and required rowCount (known total), with onPageChange/onPageSizeChange requests. Supplied rows render in order without client sorting or slicing, and headers have no sort affordance. Single selection cannot use server mode. The footer stays visible for empty results, clamps stale pages for display without callbacks, and hides during loading. Page-size changes may also request a page to keep the first visible row on screen. The consumer owns fetching and reconciles stale pages. See examples/server-pagination.tsx for an items/total/page/pageSize envelope consumer that handles both callbacks.',
|
|
11
13
|
|
|
14
|
+
labels: [
|
|
15
|
+
{
|
|
16
|
+
key: 'scrollRegion',
|
|
17
|
+
type: 'string',
|
|
18
|
+
state: 'Non-virtual table overflow: focusable scroll region name; virtualized regions use the caller caption',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: 'selectionCount',
|
|
22
|
+
type: '(count: number) => string',
|
|
23
|
+
state: 'Multiple selection: toolbar count and polite live region',
|
|
24
|
+
},
|
|
25
|
+
{ key: 'columns', type: 'string', state: 'Column visibility trigger text and name' },
|
|
26
|
+
{ key: 'visibleColumns', type: 'string', state: 'Open column menu: visibility heading' },
|
|
27
|
+
{ key: 'pinColumns', type: 'string', state: 'Open column menu with pinning: pin heading' },
|
|
28
|
+
{ key: 'pinnedLeft', type: 'string', state: 'Open pin submenu trigger: left side suffix' },
|
|
29
|
+
{ key: 'pinnedRight', type: 'string', state: 'Open pin submenu trigger: right side suffix' },
|
|
30
|
+
{ key: 'pinLeft', type: 'string', state: 'Open pin submenu: left option' },
|
|
31
|
+
{ key: 'pinRight', type: 'string', state: 'Open pin submenu: right option' },
|
|
32
|
+
{ key: 'unpinned', type: 'string', state: 'Open pin submenu: unpinned option' },
|
|
33
|
+
{ key: 'density', type: 'string', state: 'Density group accessible name' },
|
|
34
|
+
{ key: 'comfortable', type: 'string', state: 'Comfortable density button name' },
|
|
35
|
+
{ key: 'compact', type: 'string', state: 'Compact density button name' },
|
|
36
|
+
{
|
|
37
|
+
key: 'resizeColumn',
|
|
38
|
+
type: '(label: string) => string',
|
|
39
|
+
state: 'Resizable header: separator name; label is caller column header',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'columnWidth',
|
|
43
|
+
type: '(width: number) => string',
|
|
44
|
+
state: 'Resizable header: separator aria-valuetext in pixels',
|
|
45
|
+
},
|
|
46
|
+
{ key: 'defaultView', type: 'string', state: 'View trigger when no active view' },
|
|
47
|
+
{ key: 'defaultViewOption', type: 'string', state: 'Open views menu: default option' },
|
|
48
|
+
{ key: 'modified', type: 'string', state: 'Dirty view trigger: screen reader suffix' },
|
|
49
|
+
{ key: 'views', type: 'string', state: 'Open views menu: heading' },
|
|
50
|
+
{ key: 'saveChanges', type: 'string', state: 'Open views menu: save current view action' },
|
|
51
|
+
{ key: 'saveAs', type: 'string', state: 'Open views menu: save as action' },
|
|
52
|
+
{ key: 'renameView', type: 'string', state: 'Open views menu: rename action' },
|
|
53
|
+
{ key: 'deleteView', type: 'string', state: 'Open views menu: immediate delete action; no confirmation dialog' },
|
|
54
|
+
{ key: 'resetChanges', type: 'string', state: 'Open views menu: reset action' },
|
|
55
|
+
{ key: 'saveViewTitle', type: 'string', state: 'Save as dialog: title and accessible name' },
|
|
56
|
+
{ key: 'renameViewTitle', type: 'string', state: 'Rename dialog: title and accessible name' },
|
|
57
|
+
{ key: 'viewName', type: 'string', state: 'Save as and Rename dialogs: input label' },
|
|
58
|
+
{ key: 'cancel', type: 'string', state: 'Save as and Rename dialogs: cancel button' },
|
|
59
|
+
{ key: 'saveView', type: 'string', state: 'Save as dialog: submit button' },
|
|
60
|
+
{ key: 'rename', type: 'string', state: 'Rename dialog: submit button' },
|
|
61
|
+
{ key: 'closeDialog', type: 'string', state: 'Save as and Rename dialogs: close button name' },
|
|
62
|
+
{ key: 'selection', type: 'string', state: 'Single selection: screen reader column heading' },
|
|
63
|
+
{ key: 'selectAll', type: 'string', state: 'Multiple selection: header checkbox name' },
|
|
64
|
+
{
|
|
65
|
+
key: 'selectRow',
|
|
66
|
+
type: '(label: string) => string',
|
|
67
|
+
state: 'Multiple selection: row checkbox name; label is caller row name',
|
|
68
|
+
},
|
|
69
|
+
{ key: 'selectGroup', type: '(group: string) => string', state: 'Grouped multiple selection: group checkbox name' },
|
|
70
|
+
{
|
|
71
|
+
key: 'groupRows',
|
|
72
|
+
type: '(count: number) => string',
|
|
73
|
+
state: 'Grouped header: screen reader row-count unit; numeric count stays unchanged',
|
|
74
|
+
},
|
|
75
|
+
{ key: 'noGroup', type: 'string', state: 'Grouped header: fallback when group key is empty' },
|
|
76
|
+
{ key: 'actions', type: 'string', state: 'Row actions present: screen reader column heading' },
|
|
77
|
+
{
|
|
78
|
+
key: 'rowActions',
|
|
79
|
+
type: '(label: string) => string',
|
|
80
|
+
state: 'Row action menu trigger name; label is caller row name',
|
|
81
|
+
},
|
|
82
|
+
{ key: 'emptyTitle', type: 'string', state: 'Empty rows: default empty-state title' },
|
|
83
|
+
{ key: 'emptyDescription', type: 'string', state: 'Empty rows: default empty-state description' },
|
|
84
|
+
{
|
|
85
|
+
key: 'loading',
|
|
86
|
+
type: '(caption: string) => string',
|
|
87
|
+
state: 'Loading: status announcement; receives original caller caption',
|
|
88
|
+
},
|
|
89
|
+
{ key: 'editHint', type: 'string', state: 'Editable table: shared keyboard instruction description' },
|
|
90
|
+
{ key: 'editLabel', type: '(column: string, row: string) => string', state: 'Editing cell: input accessible name' },
|
|
91
|
+
{ key: 'editError', type: 'string', state: 'Rejected cell edit without caller error message: alert' },
|
|
92
|
+
{ key: 'enterNumber', type: 'string', state: 'Editing number: invalid numeric value alert' },
|
|
93
|
+
{ key: 'minimumValue', type: '(min: number) => string', state: 'Editing number below minimum: alert' },
|
|
94
|
+
{ key: 'maximumValue', type: '(max: number) => string', state: 'Editing number above maximum: alert' },
|
|
95
|
+
{ key: 'rowsPerPage', type: 'string', state: 'Always: visible page-size text' },
|
|
96
|
+
{ key: 'rowsPerPageLabel', type: 'string', state: 'Always: page-size Select accessible name' },
|
|
97
|
+
{
|
|
98
|
+
key: 'rangeSummary',
|
|
99
|
+
type: '(from: number, to: number, total: number) => string',
|
|
100
|
+
state: 'Every page and page-size change: visible summary and polite live region',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: 'paginationNav',
|
|
104
|
+
type: '(noun: string) => string',
|
|
105
|
+
state: 'Always: navigation accessible name; noun is the caller caption',
|
|
106
|
+
},
|
|
107
|
+
{ key: 'previous', type: 'string', state: 'Always: previous-page button name' },
|
|
108
|
+
{ key: 'next', type: 'string', state: 'Always: next-page button name' },
|
|
109
|
+
{
|
|
110
|
+
key: 'page',
|
|
111
|
+
type: '(page: number) => string',
|
|
112
|
+
state: 'Always: numbered-page button name; number stays caller content',
|
|
113
|
+
},
|
|
114
|
+
] satisfies Array<{ key: keyof DataTableLabels; type: string; state: string }>,
|
|
12
115
|
uses: [
|
|
13
116
|
'Table',
|
|
14
117
|
'Checkbox',
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MenuTrigger,
|
|
16
16
|
useI18n,
|
|
17
17
|
} from '@gntik-ai/ui';
|
|
18
|
+
import type { DataTableLabels } from './labels';
|
|
18
19
|
import type { PinSide } from './column-sizing';
|
|
19
20
|
import type { DataTableColumn } from './data-table-utils';
|
|
20
21
|
|
|
@@ -23,6 +24,7 @@ export interface ColumnVisibilityMenuProps<T> {
|
|
|
23
24
|
hidden: ReadonlySet<string>;
|
|
24
25
|
onToggle: (columnId: string, visible: boolean) => void;
|
|
25
26
|
label?: string;
|
|
27
|
+
labels?: Partial<DataTableLabels>;
|
|
26
28
|
/** Pin side of a column; with `onPin`, the menu gets a "Pin columns" section. */
|
|
27
29
|
pinOf?: (columnId: string) => PinSide | null;
|
|
28
30
|
onPin?: (columnId: string, side: PinSide | null) => void;
|
|
@@ -39,11 +41,12 @@ export function ColumnVisibilityMenu<T>({
|
|
|
39
41
|
hidden,
|
|
40
42
|
onToggle,
|
|
41
43
|
label: labelProp,
|
|
44
|
+
labels,
|
|
42
45
|
pinOf,
|
|
43
46
|
onPin,
|
|
44
47
|
}: ColumnVisibilityMenuProps<T>) {
|
|
45
48
|
const { t } = useI18n();
|
|
46
|
-
const label = labelProp ?? t('table.columns');
|
|
49
|
+
const label = labels?.columns ?? labelProp ?? t('table.columns');
|
|
47
50
|
const visibleCount = columns.filter((c) => !hidden.has(c.id)).length;
|
|
48
51
|
const pinnable = onPin && pinOf ? columns.filter((c) => c.pinnable !== false && !hidden.has(c.id)) : [];
|
|
49
52
|
return (
|
|
@@ -53,7 +56,7 @@ export function ColumnVisibilityMenu<T>({
|
|
|
53
56
|
</MenuTrigger>
|
|
54
57
|
<MenuContent align="end" className="min-w-[200px]">
|
|
55
58
|
<MenuGroup>
|
|
56
|
-
<MenuGroupLabel>{t('table.visibleColumns')}</MenuGroupLabel>
|
|
59
|
+
<MenuGroupLabel>{labels?.visibleColumns ?? t('table.visibleColumns')}</MenuGroupLabel>
|
|
57
60
|
{columns
|
|
58
61
|
.filter((c) => c.hideable !== false)
|
|
59
62
|
.map((c) => {
|
|
@@ -74,7 +77,7 @@ export function ColumnVisibilityMenu<T>({
|
|
|
74
77
|
<>
|
|
75
78
|
<MenuSeparator />
|
|
76
79
|
<MenuGroup>
|
|
77
|
-
<MenuGroupLabel>Pin columns</MenuGroupLabel>
|
|
80
|
+
<MenuGroupLabel>{labels?.pinColumns ?? 'Pin columns'}</MenuGroupLabel>
|
|
78
81
|
{pinnable.map((c) => {
|
|
79
82
|
const side = pinOf(c.id);
|
|
80
83
|
return (
|
|
@@ -83,7 +86,7 @@ export function ColumnVisibilityMenu<T>({
|
|
|
83
86
|
{c.header}
|
|
84
87
|
{side && (
|
|
85
88
|
<span className="ms-auto ps-3 text-[11.5px] text-muted-foreground">
|
|
86
|
-
{side === 'left' ? 'Left' : 'Right'}
|
|
89
|
+
{side === 'left' ? (labels?.pinnedLeft ?? 'Left') : (labels?.pinnedRight ?? 'Right')}
|
|
87
90
|
</span>
|
|
88
91
|
)}
|
|
89
92
|
</MenuSubTrigger>
|
|
@@ -92,9 +95,9 @@ export function ColumnVisibilityMenu<T>({
|
|
|
92
95
|
value={side ?? PIN_NONE}
|
|
93
96
|
onValueChange={(v: unknown) => onPin(c.id, v === 'left' || v === 'right' ? v : null)}
|
|
94
97
|
>
|
|
95
|
-
<MenuRadioItem value="left">Pin left</MenuRadioItem>
|
|
96
|
-
<MenuRadioItem value="right">Pin right</MenuRadioItem>
|
|
97
|
-
<MenuRadioItem value={PIN_NONE}>Unpinned</MenuRadioItem>
|
|
98
|
+
<MenuRadioItem value="left">{labels?.pinLeft ?? 'Pin left'}</MenuRadioItem>
|
|
99
|
+
<MenuRadioItem value="right">{labels?.pinRight ?? 'Pin right'}</MenuRadioItem>
|
|
100
|
+
<MenuRadioItem value={PIN_NONE}>{labels?.unpinned ?? 'Unpinned'}</MenuRadioItem>
|
|
98
101
|
</MenuRadioGroup>
|
|
99
102
|
</MenuSubContent>
|
|
100
103
|
</MenuSub>
|
|
@@ -108,8 +108,8 @@ export function DataTableBody<T>(props: DataTableBodyProps<T>) {
|
|
|
108
108
|
<EmptyState
|
|
109
109
|
icon={Inbox}
|
|
110
110
|
size="sm"
|
|
111
|
-
title={t('common.noResults')}
|
|
112
|
-
description={t('table.emptyDescription')}
|
|
111
|
+
title={props.labels?.emptyTitle ?? t('common.noResults')}
|
|
112
|
+
description={props.labels?.emptyDescription ?? t('table.emptyDescription')}
|
|
113
113
|
/>
|
|
114
114
|
)}
|
|
115
115
|
</TableCell>
|
|
@@ -130,6 +130,7 @@ export function DataTableBody<T>(props: DataTableBodyProps<T>) {
|
|
|
130
130
|
<GroupHeaderRow
|
|
131
131
|
key={`group-${group.key}`}
|
|
132
132
|
group={group}
|
|
133
|
+
labels={props.labels}
|
|
133
134
|
open={open}
|
|
134
135
|
selectable={selectable}
|
|
135
136
|
selected={selected}
|
|
@@ -3,9 +3,12 @@ import type { ReactNode } from 'react';
|
|
|
3
3
|
import type { ColumnWidths, PinnedColumns } from './column-sizing';
|
|
4
4
|
import type { DataTableColumn, DataTableGroupBy, SortState } from './data-table-utils';
|
|
5
5
|
import type { DataTableCellEditHandler } from './editable-cell';
|
|
6
|
+
import type { DataTableLabels } from './labels';
|
|
6
7
|
import type { DataTableView } from './views';
|
|
7
8
|
|
|
8
9
|
interface DataTableCommonProps<T> {
|
|
10
|
+
/** Per-key overrides; omitted keys keep existing defaults and provider translations. */
|
|
11
|
+
labels?: Partial<DataTableLabels>;
|
|
9
12
|
rows?: readonly T[];
|
|
10
13
|
columns?: ReadonlyArray<DataTableColumn<T>>;
|
|
11
14
|
/** Stable row id (defaults to `row.id`). */
|
|
@@ -136,21 +139,44 @@ interface DataTableCommonProps<T> {
|
|
|
136
139
|
className?: string;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
interface ClientPaginationProps {
|
|
143
|
+
paginationMode?: 'client';
|
|
144
|
+
rowCount?: never;
|
|
145
|
+
page?: never;
|
|
146
|
+
pageSize?: never;
|
|
147
|
+
onPageChange?: never;
|
|
148
|
+
onPageSizeChange?: never;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ServerPaginationProps {
|
|
152
|
+
/** Rows arrive as one server page: no client sorting or slicing, and no sortable headers. */
|
|
153
|
+
paginationMode: 'server';
|
|
154
|
+
/** Total number of rows across all pages; required even for an empty result. */
|
|
155
|
+
rowCount: number;
|
|
156
|
+
/** Current page, 1-based. Out-of-range pages are clamped for display without callbacks. */
|
|
157
|
+
page: number;
|
|
158
|
+
pageSize: number;
|
|
159
|
+
onPageChange?: (page: number) => void;
|
|
160
|
+
/** The footer may also request a page change to keep the first visible row on screen. */
|
|
161
|
+
onPageSizeChange?: (size: number) => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
export type DataTableProps<T> = DataTableCommonProps<T> &
|
|
140
165
|
(
|
|
141
|
-
| {
|
|
166
|
+
| ({
|
|
142
167
|
selectionMode: 'single';
|
|
143
168
|
paginated?: false;
|
|
144
169
|
/** Consumer-supplied accessible group label; the kit adds no default. */
|
|
145
170
|
legend?: ReactNode;
|
|
146
171
|
'aria-label'?: string;
|
|
147
172
|
'aria-labelledby'?: string;
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
paginationMode?: never;
|
|
174
|
+
} & ClientPaginationProps)
|
|
175
|
+
| ({
|
|
150
176
|
selectionMode?: 'none' | 'multiple';
|
|
151
177
|
paginated?: boolean;
|
|
152
178
|
legend?: never;
|
|
153
179
|
'aria-label'?: never;
|
|
154
180
|
'aria-labelledby'?: never;
|
|
155
|
-
}
|
|
181
|
+
} & (ClientPaginationProps | ServerPaginationProps))
|
|
156
182
|
);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type DataTableColumn,
|
|
11
11
|
} from './data-table-utils';
|
|
12
12
|
import { EditableCell, isCellEditable, type DataTableCellEditHandler } from './editable-cell';
|
|
13
|
+
import type { DataTableLabels } from './labels';
|
|
13
14
|
import type { SingleSelection } from './single-selection';
|
|
14
15
|
|
|
15
16
|
/** A group as the body renders it: its rows on the current page plus header data. */
|
|
@@ -27,6 +28,7 @@ export interface DataTableBodyGroup<T> {
|
|
|
27
28
|
|
|
28
29
|
/** Row rendering shared by the plain, grouped and virtualized bodies. */
|
|
29
30
|
export interface RowContext<T> {
|
|
31
|
+
labels?: Partial<DataTableLabels>;
|
|
30
32
|
columns: ReadonlyArray<DataTableColumn<T>>;
|
|
31
33
|
getRowId: (row: T) => string;
|
|
32
34
|
getRowLabel: (row: T) => string;
|
|
@@ -49,6 +51,7 @@ export interface DataTableRowProps<T> extends RowContext<T> {
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export function DataTableRow<T>({
|
|
54
|
+
labels,
|
|
52
55
|
row,
|
|
53
56
|
columns,
|
|
54
57
|
getRowId,
|
|
@@ -83,7 +86,7 @@ export function DataTableRow<T>({
|
|
|
83
86
|
</span>
|
|
84
87
|
) : (
|
|
85
88
|
<Checkbox
|
|
86
|
-
aria-label={t('table.selectRow', { label })}
|
|
89
|
+
aria-label={labels?.selectRow?.(label) ?? t('table.selectRow', { label })}
|
|
87
90
|
checked={isSelected}
|
|
88
91
|
onCheckedChange={() => onToggleRow(id)}
|
|
89
92
|
/>
|
|
@@ -110,6 +113,7 @@ export function DataTableRow<T>({
|
|
|
110
113
|
>
|
|
111
114
|
{editable ? (
|
|
112
115
|
<EditableCell
|
|
116
|
+
labels={labels}
|
|
113
117
|
row={row}
|
|
114
118
|
rowId={id}
|
|
115
119
|
rowLabel={label}
|
|
@@ -131,7 +135,12 @@ export function DataTableRow<T>({
|
|
|
131
135
|
className={cn('w-12 py-0', layout?.trail.className, tint(layout?.trail))}
|
|
132
136
|
style={layout?.trail.style}
|
|
133
137
|
>
|
|
134
|
-
<MoreMenu
|
|
138
|
+
<MoreMenu
|
|
139
|
+
items={rowActions(row)}
|
|
140
|
+
label={labels?.rowActions?.(label) ?? t('table.rowActions', { label })}
|
|
141
|
+
variant="ghost"
|
|
142
|
+
size="sm"
|
|
143
|
+
/>
|
|
135
144
|
</TableCell>
|
|
136
145
|
)}
|
|
137
146
|
</TableRow>
|
|
@@ -142,6 +151,7 @@ export const groupHeadClass =
|
|
|
142
151
|
'h-9 bg-secondary/50 text-[12.5px] font-semibold tracking-normal normal-case text-foreground';
|
|
143
152
|
|
|
144
153
|
export interface GroupHeaderRowProps<T> {
|
|
154
|
+
labels?: Partial<DataTableLabels>;
|
|
145
155
|
group: DataTableBodyGroup<T>;
|
|
146
156
|
open: boolean;
|
|
147
157
|
selectable: boolean;
|
|
@@ -157,6 +167,7 @@ export interface GroupHeaderRowProps<T> {
|
|
|
157
167
|
|
|
158
168
|
/** The header row of a group: group checkbox, collapse button, label and count. */
|
|
159
169
|
export function GroupHeaderRow<T>({
|
|
170
|
+
labels,
|
|
160
171
|
group,
|
|
161
172
|
open,
|
|
162
173
|
selectable,
|
|
@@ -177,7 +188,7 @@ export function GroupHeaderRow<T>({
|
|
|
177
188
|
<span className="rounded-full bg-secondary px-1.5 font-mono text-[11px] font-medium text-muted-foreground tabular-nums">
|
|
178
189
|
{count}
|
|
179
190
|
</span>{' '}
|
|
180
|
-
<span className="sr-only">{count === 1 ? 'row' : 'rows'}</span>
|
|
191
|
+
<span className="sr-only">{labels?.groupRows?.(count) ?? (count === 1 ? 'row' : 'rows')}</span>
|
|
181
192
|
</>
|
|
182
193
|
);
|
|
183
194
|
return (
|
|
@@ -185,7 +196,7 @@ export function GroupHeaderRow<T>({
|
|
|
185
196
|
{selectable && (
|
|
186
197
|
<TableCell className={cn('w-10 pe-0', groupHeadClass)}>
|
|
187
198
|
<Checkbox
|
|
188
|
-
aria-label={t('table.selectRow', { label: `all in ${group.name}` })}
|
|
199
|
+
aria-label={labels?.selectGroup?.(group.name) ?? t('table.selectRow', { label: `all in ${group.name}` })}
|
|
189
200
|
checked={state.checked}
|
|
190
201
|
indeterminate={state.indeterminate}
|
|
191
202
|
disabled={count === 0}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { Rows2, Rows3 } from '@gntik-ai/icons';
|
|
2
2
|
import { Toggle, ToggleGroup, type TableDensity, useI18n } from '@gntik-ai/ui';
|
|
3
3
|
|
|
4
|
+
import type { DataTableLabels } from './labels';
|
|
5
|
+
|
|
4
6
|
export interface DensityToggleProps {
|
|
7
|
+
labels?: Partial<DataTableLabels>;
|
|
5
8
|
value: TableDensity;
|
|
6
9
|
onChange: (density: TableDensity) => void;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
/** Two-icon segmented control: comfortable or compact rows. */
|
|
10
|
-
export function DensityToggle({ value, onChange }: DensityToggleProps) {
|
|
13
|
+
export function DensityToggle({ value, onChange, labels }: DensityToggleProps) {
|
|
11
14
|
const { t } = useI18n();
|
|
12
15
|
return (
|
|
13
16
|
<ToggleGroup
|
|
14
|
-
aria-label={t('table.density')}
|
|
17
|
+
aria-label={labels?.density ?? t('table.density')}
|
|
15
18
|
size="sm"
|
|
16
19
|
value={[value]}
|
|
17
20
|
onValueChange={(v) => {
|
|
@@ -19,10 +22,10 @@ export function DensityToggle({ value, onChange }: DensityToggleProps) {
|
|
|
19
22
|
if (next === 'compact' || next === 'comfortable') onChange(next);
|
|
20
23
|
}}
|
|
21
24
|
>
|
|
22
|
-
<Toggle value="comfortable" iconOnly aria-label={t('table.comfortable')}>
|
|
25
|
+
<Toggle value="comfortable" iconOnly aria-label={labels?.comfortable ?? t('table.comfortable')}>
|
|
23
26
|
<Rows2 size={15} aria-hidden />
|
|
24
27
|
</Toggle>
|
|
25
|
-
<Toggle value="compact" iconOnly aria-label={t('table.compact')}>
|
|
28
|
+
<Toggle value="compact" iconOnly aria-label={labels?.compact ?? t('table.compact')}>
|
|
26
29
|
<Rows3 size={15} aria-hidden />
|
|
27
30
|
</Toggle>
|
|
28
31
|
</ToggleGroup>
|