@dashforge/tw 0.8.1-beta → 0.9.1-beta
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/CHANGELOG.md +155 -0
- package/COVERAGE.md +63 -0
- package/LICENSE +21 -0
- package/PERFORMANCE.md +59 -0
- package/REFINEMENT-NOTES.md +50 -0
- package/dist/index.esm.js +806 -134
- package/dist/src/components/Calendar/Calendar.d.ts +14 -0
- package/dist/src/components/Calendar/Calendar.d.ts.map +1 -0
- package/dist/src/components/Calendar/calendar.types.d.ts +72 -0
- package/dist/src/components/Calendar/calendar.types.d.ts.map +1 -0
- package/dist/src/components/Calendar/calendar.variants.d.ts +99 -0
- package/dist/src/components/Calendar/calendar.variants.d.ts.map +1 -0
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -1
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -1
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +7 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -1
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts +6 -2
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityMenu.d.ts.map +1 -1
- package/dist/src/components/DatePicker/DatePicker.d.ts +14 -0
- package/dist/src/components/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/src/components/DatePicker/datePicker.types.d.ts +88 -0
- package/dist/src/components/DatePicker/datePicker.types.d.ts.map +1 -0
- package/dist/src/components/DatePicker/datePicker.variants.d.ts +97 -0
- package/dist/src/components/DatePicker/datePicker.variants.d.ts.map +1 -0
- package/dist/src/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/src/components/Table/Table.d.ts.map +1 -1
- package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +28 -3
- package/dist/src/components/Table/table.types.d.ts.map +1 -1
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/eslint.config.mjs +16 -1
- package/package.json +10 -6
- package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +5 -2
- package/src/components/Calendar/Calendar.test.tsx +75 -0
- package/src/components/Calendar/Calendar.tsx +264 -0
- package/src/components/Calendar/calendar.types.ts +65 -0
- package/src/components/Calendar/calendar.variants.ts +64 -0
- package/src/components/DataGrid/DataGrid.perf.test.tsx +114 -0
- package/src/components/DataGrid/DataGrid.test.tsx +240 -0
- package/src/components/DataGrid/DataGrid.tsx +104 -16
- package/src/components/DataGrid/filters/ColumnFilters.test.tsx +330 -0
- package/src/components/DataGrid/filters/ColumnFilters.tsx +26 -2
- package/src/components/DataGrid/reorder/useColumnReorder.test.ts +95 -3
- package/src/components/DataGrid/resize/useColumnResize.test.ts +144 -0
- package/src/components/DataGrid/resize/useColumnResize.ts +30 -1
- package/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx +42 -25
- package/src/components/DatePicker/DatePicker.test.tsx +76 -0
- package/src/components/DatePicker/DatePicker.tsx +305 -0
- package/src/components/DatePicker/datePicker.types.ts +77 -0
- package/src/components/DatePicker/datePicker.variants.ts +50 -0
- package/src/components/Dialog/Dialog.tsx +9 -0
- package/src/components/Table/Table.perf.test.tsx +81 -0
- package/src/components/Table/Table.test.tsx +72 -1
- package/src/components/Table/Table.tsx +40 -4
- package/src/components/Table/cells/RenderChip.tsx +6 -9
- package/src/components/Table/cells/cells.test.tsx +247 -0
- package/src/components/Table/table.types.ts +28 -3
- package/src/components/Typography/Typography.test.tsx +15 -0
- package/src/components/Typography/typography.variants.ts +14 -1
- package/src/index.ts +21 -1
- package/test-output/vitest/coverage/base.css +224 -0
- package/test-output/vitest/coverage/block-navigation.js +87 -0
- package/test-output/vitest/coverage/clover.xml +2418 -0
- package/test-output/vitest/coverage/coverage-final.json +89 -0
- package/test-output/vitest/coverage/favicon.png +0 -0
- package/test-output/vitest/coverage/index.html +746 -0
- package/test-output/vitest/coverage/prettify.css +1 -0
- package/test-output/vitest/coverage/prettify.js +2 -0
- package/test-output/vitest/coverage/sort-arrow-sprite.png +0 -0
- package/test-output/vitest/coverage/sorter.js +210 -0
- package/test-output/vitest/coverage/src/components/Accordion/Accordion.tsx.html +376 -0
- package/test-output/vitest/coverage/src/components/Accordion/accordion.variants.ts.html +181 -0
- package/test-output/vitest/coverage/src/components/Accordion/index.html +131 -0
- package/test-output/vitest/coverage/src/components/AppShell/AppShell.tsx.html +802 -0
- package/test-output/vitest/coverage/src/components/AppShell/appShell.variants.ts.html +247 -0
- package/test-output/vitest/coverage/src/components/AppShell/index.html +131 -0
- package/test-output/vitest/coverage/src/components/AspectRatio/AspectRatio.tsx.html +304 -0
- package/test-output/vitest/coverage/src/components/AspectRatio/index.html +116 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/Autocomplete.tsx.html +3292 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/autocomplete.variants.ts.html +508 -0
- package/test-output/vitest/coverage/src/components/Autocomplete/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Box/Box.tsx.html +334 -0
- package/test-output/vitest/coverage/src/components/Box/box.variants.ts.html +775 -0
- package/test-output/vitest/coverage/src/components/Box/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/Breadcrumbs.tsx.html +694 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/breadcrumbs.variants.ts.html +247 -0
- package/test-output/vitest/coverage/src/components/Breadcrumbs/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Button/Button.tsx.html +535 -0
- package/test-output/vitest/coverage/src/components/Button/button.variants.ts.html +562 -0
- package/test-output/vitest/coverage/src/components/Button/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Checkbox/Checkbox.tsx.html +943 -0
- package/test-output/vitest/coverage/src/components/Checkbox/checkbox.variants.ts.html +289 -0
- package/test-output/vitest/coverage/src/components/Checkbox/index.html +131 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/ConfirmDialog.tsx.html +841 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/confirmDialog.variants.ts.html +313 -0
- package/test-output/vitest/coverage/src/components/ConfirmDialog/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Container/Container.tsx.html +307 -0
- package/test-output/vitest/coverage/src/components/Container/container.variants.ts.html +328 -0
- package/test-output/vitest/coverage/src/components/Container/index.html +131 -0
- package/test-output/vitest/coverage/src/components/DataGrid/DataGrid.tsx.html +3937 -0
- package/test-output/vitest/coverage/src/components/DataGrid/dataGrid.variants.ts.html +541 -0
- package/test-output/vitest/coverage/src/components/DataGrid/filters/ColumnFilters.tsx.html +1453 -0
- package/test-output/vitest/coverage/src/components/DataGrid/filters/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/index.html +131 -0
- package/test-output/vitest/coverage/src/components/DataGrid/reorder/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/reorder/useColumnReorder.ts.html +517 -0
- package/test-output/vitest/coverage/src/components/DataGrid/resize/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DataGrid/resize/useColumnResize.ts.html +490 -0
- package/test-output/vitest/coverage/src/components/DataGrid/visibility/ColumnVisibilityMenu.tsx.html +592 -0
- package/test-output/vitest/coverage/src/components/DataGrid/visibility/index.html +116 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/DateTimePicker.tsx.html +1042 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/dateTimePicker.variants.ts.html +337 -0
- package/test-output/vitest/coverage/src/components/DateTimePicker/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Dialog/Dialog.tsx.html +391 -0
- package/test-output/vitest/coverage/src/components/Dialog/dialog.variants.ts.html +283 -0
- package/test-output/vitest/coverage/src/components/Dialog/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Divider/Divider.tsx.html +505 -0
- package/test-output/vitest/coverage/src/components/Divider/divider.variants.ts.html +433 -0
- package/test-output/vitest/coverage/src/components/Divider/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Grid/Grid.tsx.html +529 -0
- package/test-output/vitest/coverage/src/components/Grid/grid.variants.ts.html +484 -0
- package/test-output/vitest/coverage/src/components/Grid/index.html +131 -0
- package/test-output/vitest/coverage/src/components/LeftNav/LeftNav.tsx.html +1075 -0
- package/test-output/vitest/coverage/src/components/LeftNav/index.html +131 -0
- package/test-output/vitest/coverage/src/components/LeftNav/leftNav.variants.ts.html +406 -0
- package/test-output/vitest/coverage/src/components/NumberField/NumberField.tsx.html +1048 -0
- package/test-output/vitest/coverage/src/components/NumberField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/NumberField/numberField.variants.ts.html +367 -0
- package/test-output/vitest/coverage/src/components/OTPField/OTPField.tsx.html +835 -0
- package/test-output/vitest/coverage/src/components/OTPField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/OTPField/otpField.variants.ts.html +301 -0
- package/test-output/vitest/coverage/src/components/Pagination/Pagination.tsx.html +916 -0
- package/test-output/vitest/coverage/src/components/Pagination/index.html +146 -0
- package/test-output/vitest/coverage/src/components/Pagination/pagination.helpers.ts.html +391 -0
- package/test-output/vitest/coverage/src/components/Pagination/pagination.variants.ts.html +436 -0
- package/test-output/vitest/coverage/src/components/Popover/Popover.tsx.html +250 -0
- package/test-output/vitest/coverage/src/components/Popover/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Popover/popover.variants.ts.html +157 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/RadioGroup.tsx.html +934 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/index.html +131 -0
- package/test-output/vitest/coverage/src/components/RadioGroup/radioGroup.variants.ts.html +346 -0
- package/test-output/vitest/coverage/src/components/Skeleton/Skeleton.tsx.html +232 -0
- package/test-output/vitest/coverage/src/components/Skeleton/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Skeleton/skeleton.variants.ts.html +346 -0
- package/test-output/vitest/coverage/src/components/Snackbar/Snackbar.tsx.html +979 -0
- package/test-output/vitest/coverage/src/components/Snackbar/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Snackbar/snackbar.variants.ts.html +307 -0
- package/test-output/vitest/coverage/src/components/Stack/Stack.tsx.html +430 -0
- package/test-output/vitest/coverage/src/components/Stack/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Stack/stack.variants.ts.html +325 -0
- package/test-output/vitest/coverage/src/components/Switch/Switch.tsx.html +559 -0
- package/test-output/vitest/coverage/src/components/Switch/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Switch/switch.variants.ts.html +295 -0
- package/test-output/vitest/coverage/src/components/Table/Table.tsx.html +2677 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderButton.tsx.html +154 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderChip.tsx.html +385 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderText.tsx.html +196 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RenderTwoLine.tsx.html +187 -0
- package/test-output/vitest/coverage/src/components/Table/cells/RowActionsMenu.tsx.html +526 -0
- package/test-output/vitest/coverage/src/components/Table/cells/index.html +191 -0
- package/test-output/vitest/coverage/src/components/Table/cells/index.ts.html +127 -0
- package/test-output/vitest/coverage/src/components/Table/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Table/table.variants.ts.html +706 -0
- package/test-output/vitest/coverage/src/components/Tabs/Tabs.tsx.html +283 -0
- package/test-output/vitest/coverage/src/components/Tabs/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Tabs/tabs.variants.ts.html +292 -0
- package/test-output/vitest/coverage/src/components/TextField/TextField.tsx.html +820 -0
- package/test-output/vitest/coverage/src/components/TextField/index.html +131 -0
- package/test-output/vitest/coverage/src/components/TextField/textField.variants.ts.html +400 -0
- package/test-output/vitest/coverage/src/components/Textarea/Textarea.tsx.html +655 -0
- package/test-output/vitest/coverage/src/components/Textarea/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Textarea/textarea.variants.ts.html +340 -0
- package/test-output/vitest/coverage/src/components/Tooltip/Tooltip.tsx.html +295 -0
- package/test-output/vitest/coverage/src/components/Tooltip/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Tooltip/tooltip.variants.ts.html +169 -0
- package/test-output/vitest/coverage/src/components/TopBar/TopBar.tsx.html +283 -0
- package/test-output/vitest/coverage/src/components/TopBar/index.html +131 -0
- package/test-output/vitest/coverage/src/components/TopBar/topBar.variants.ts.html +199 -0
- package/test-output/vitest/coverage/src/components/Typography/Typography.tsx.html +421 -0
- package/test-output/vitest/coverage/src/components/Typography/index.html +131 -0
- package/test-output/vitest/coverage/src/components/Typography/typography.variants.ts.html +436 -0
- package/test-output/vitest/coverage/src/components/VisuallyHidden/VisuallyHidden.tsx.html +262 -0
- package/test-output/vitest/coverage/src/components/VisuallyHidden/index.html +116 -0
- package/test-output/vitest/coverage/src/components/_shared/data/getNestedValue.ts.html +181 -0
- package/test-output/vitest/coverage/src/components/_shared/data/index.html +236 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useColumnAutoDetect.ts.html +385 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useControllableState.ts.html +229 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useDebouncedValue.ts.html +157 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableFilter.ts.html +439 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSearch.ts.html +283 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSelection.ts.html +385 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useTableSort.ts.html +508 -0
- package/test-output/vitest/coverage/src/components/_shared/data/useVirtualizer.ts.html +682 -0
- package/test-output/vitest/coverage/src/components/_shared/index.html +116 -0
- package/test-output/vitest/coverage/src/components/_shared/resolveValidationState.ts.html +265 -0
- package/test-output/vitest/coverage/src/hooks/index.html +116 -0
- package/test-output/vitest/coverage/src/hooks/useAccessState.ts.html +328 -0
- package/test-output/vitest/coverage/src/index.html +116 -0
- package/test-output/vitest/coverage/src/index.ts.html +1153 -0
- package/test-output/vitest/coverage/src/utils/cn.ts.html +172 -0
- package/test-output/vitest/coverage/src/utils/index.html +116 -0
- package/tsconfig.lib.json +7 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Performance guardrails for `<Table>` (non-virtualized).
|
|
4
|
+
*
|
|
5
|
+
* Table renders EVERY row in the DOM — its design ceiling is ~500
|
|
6
|
+
* rows (past that, the consumer should switch to `<DataGrid>`). These
|
|
7
|
+
* tests pin the mount cost at that ceiling so a regression in the
|
|
8
|
+
* render path (e.g. an accidental O(n²) in the sort / search pipeline)
|
|
9
|
+
* surfaces in CI.
|
|
10
|
+
*
|
|
11
|
+
* Budgets are deliberately generous — they catch order-of-magnitude
|
|
12
|
+
* regressions under a cold workspace-wide test run, not micro-jitter.
|
|
13
|
+
*/
|
|
14
|
+
import * as React from 'react';
|
|
15
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
16
|
+
import { render, cleanup } from '@testing-library/react';
|
|
17
|
+
import { Table } from './Table.js';
|
|
18
|
+
import type { TableColumn } from './table.types.js';
|
|
19
|
+
|
|
20
|
+
void React;
|
|
21
|
+
afterEach(() => cleanup());
|
|
22
|
+
|
|
23
|
+
interface Row {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
email: string;
|
|
27
|
+
age: number;
|
|
28
|
+
city: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const COLS: TableColumn<Row>[] = [
|
|
32
|
+
{ field: 'name', header: 'Name', sortable: true, searchable: true },
|
|
33
|
+
{ field: 'email', header: 'Email', searchable: true },
|
|
34
|
+
{ field: 'age', header: 'Age', sortable: true },
|
|
35
|
+
{ field: 'city', header: 'City' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function makeRows(n: number): Row[] {
|
|
39
|
+
const cities = ['Roma', 'Milano', 'Berlin'];
|
|
40
|
+
return Array.from({ length: n }).map((_, i) => ({
|
|
41
|
+
id: `r${i}`,
|
|
42
|
+
name: `User ${i}`,
|
|
43
|
+
email: `user${i}@example.com`,
|
|
44
|
+
age: 20 + (i % 50),
|
|
45
|
+
city: cities[i % 3] as string,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('<Table> performance', () => {
|
|
50
|
+
it("mounts 500 rows (Table's row-count ceiling) under 800ms", () => {
|
|
51
|
+
const rows = makeRows(500);
|
|
52
|
+
const t0 = performance.now();
|
|
53
|
+
render(<Table rows={rows} cols={COLS} getRowId={(r) => r.id} />);
|
|
54
|
+
const t1 = performance.now();
|
|
55
|
+
expect(t1 - t0).toBeLessThan(800);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders every row in the DOM (non-virtualized contract)', () => {
|
|
59
|
+
const rows = makeRows(500);
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<Table rows={rows} cols={COLS} getRowId={(r) => r.id} />,
|
|
62
|
+
);
|
|
63
|
+
// Non-virtualized: all 500 data rows are mounted.
|
|
64
|
+
expect(container.querySelectorAll('tbody tr').length).toBe(500);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('applies a default sort over 500 rows within the mount budget', () => {
|
|
68
|
+
const rows = makeRows(500);
|
|
69
|
+
const t0 = performance.now();
|
|
70
|
+
render(
|
|
71
|
+
<Table
|
|
72
|
+
rows={rows}
|
|
73
|
+
cols={COLS}
|
|
74
|
+
getRowId={(r) => r.id}
|
|
75
|
+
defaultSortModel={[{ field: 'name', direction: 'asc' }]}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
const t1 = performance.now();
|
|
79
|
+
expect(t1 - t0).toBeLessThan(800);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { describe, it, expect, afterEach, beforeAll } from 'vitest';
|
|
3
|
+
import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
|
|
4
4
|
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
5
5
|
import { Table } from './Table.js';
|
|
6
6
|
import type { TableColumn } from './table.types.js';
|
|
@@ -601,3 +601,74 @@ describe('Table — i18n', () => {
|
|
|
601
601
|
expect(screen.queryByLabelText('Seleziona tutte')).not.toBeNull();
|
|
602
602
|
});
|
|
603
603
|
});
|
|
604
|
+
|
|
605
|
+
describe('Table — Sprint 6 P2 refinements', () => {
|
|
606
|
+
// ───── D7: no-results vs no-data empty state ─────
|
|
607
|
+
|
|
608
|
+
it('shows the noData message when the dataset is genuinely empty', () => {
|
|
609
|
+
render(<Table rows={[]} cols={baseCols} getRowId={getRowId} />);
|
|
610
|
+
expect(screen.queryByText('No data')).not.toBeNull();
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('shows the noResults message when a search excludes every row', () => {
|
|
614
|
+
render(
|
|
615
|
+
<Table
|
|
616
|
+
rows={baseRows}
|
|
617
|
+
cols={baseCols}
|
|
618
|
+
getRowId={getRowId}
|
|
619
|
+
enableSearch
|
|
620
|
+
searchQuery="zzz-no-match"
|
|
621
|
+
searchDebounceMs={0}
|
|
622
|
+
/>,
|
|
623
|
+
);
|
|
624
|
+
expect(screen.queryByText('No matching results')).not.toBeNull();
|
|
625
|
+
expect(screen.queryByText('No data')).toBeNull();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('shows the noResults message when a filter excludes every row', () => {
|
|
629
|
+
render(
|
|
630
|
+
<Table
|
|
631
|
+
rows={baseRows}
|
|
632
|
+
cols={baseCols}
|
|
633
|
+
getRowId={getRowId}
|
|
634
|
+
filterModel={[{ field: 'name', op: 'contains', value: 'zzz-no-match' }]}
|
|
635
|
+
/>,
|
|
636
|
+
);
|
|
637
|
+
expect(screen.queryByText('No matching results')).not.toBeNull();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// ───── T1: dev-warn when getRowId omitted + identity-sensitive feature ─────
|
|
641
|
+
|
|
642
|
+
it('warns in dev when getRowId is omitted and a sortable column exists', () => {
|
|
643
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
644
|
+
// baseCols has sortable columns; no getRowId passed.
|
|
645
|
+
render(<Table rows={baseRows} cols={baseCols} />);
|
|
646
|
+
expect(warn).toHaveBeenCalledWith(
|
|
647
|
+
expect.stringContaining('`getRowId` is not set'),
|
|
648
|
+
);
|
|
649
|
+
warn.mockRestore();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('does NOT warn when getRowId is provided', () => {
|
|
653
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
654
|
+
render(<Table rows={baseRows} cols={baseCols} getRowId={getRowId} />);
|
|
655
|
+
expect(warn).not.toHaveBeenCalledWith(
|
|
656
|
+
expect.stringContaining('`getRowId` is not set'),
|
|
657
|
+
);
|
|
658
|
+
warn.mockRestore();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('does NOT warn when getRowId is omitted but no identity-sensitive feature is active', () => {
|
|
662
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
663
|
+
// Plain columns: not sortable, not searchable, no selection/expansion.
|
|
664
|
+
const plainCols: TableColumn<User>[] = [
|
|
665
|
+
{ field: 'name', header: 'Name' },
|
|
666
|
+
{ field: 'email', header: 'Email' },
|
|
667
|
+
];
|
|
668
|
+
render(<Table rows={baseRows} cols={plainCols} />);
|
|
669
|
+
expect(warn).not.toHaveBeenCalledWith(
|
|
670
|
+
expect.stringContaining('`getRowId` is not set'),
|
|
671
|
+
);
|
|
672
|
+
warn.mockRestore();
|
|
673
|
+
});
|
|
674
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, type ReactNode, type MouseEvent } from 'react';
|
|
1
|
+
import { useMemo, useEffect, type ReactNode, type MouseEvent } from 'react';
|
|
2
2
|
import { cn } from '../../utils/cn.js';
|
|
3
3
|
import { useAccessState } from '../../hooks/useAccessState.js';
|
|
4
4
|
import { Skeleton } from '../Skeleton/Skeleton.js';
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
const DEFAULT_LABELS: Required<TableLabels> = {
|
|
28
28
|
searchPlaceholder: 'Search...',
|
|
29
29
|
noData: 'No data',
|
|
30
|
+
noResults: 'No matching results',
|
|
30
31
|
loading: 'Loading…',
|
|
31
32
|
ariaSortNone: 'Not sorted. Click to sort ascending.',
|
|
32
33
|
ariaSortAscending: 'Sorted ascending. Click to sort descending.',
|
|
@@ -53,8 +54,7 @@ const DEFAULT_LABELS: Required<TableLabels> = {
|
|
|
53
54
|
densitySpacious: 'Spacious',
|
|
54
55
|
columnsButton: 'Columns',
|
|
55
56
|
columnsTitle: 'Toggle columns',
|
|
56
|
-
|
|
57
|
-
columnsHideAll: 'Hide all',
|
|
57
|
+
columnsToggleAll: 'Toggle all columns',
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const DEFAULT_GET_ROW_ID = <T extends object>(_row: T, index: number) => String(index);
|
|
@@ -132,6 +132,31 @@ export function Table<T extends object>(props: TableProps<T>) {
|
|
|
132
132
|
const labels = { ...DEFAULT_LABELS, ...labelsProp };
|
|
133
133
|
const tableAccessState = useAccessState(tableAccess);
|
|
134
134
|
|
|
135
|
+
// Dev-only guard: `getRowId` is optional (positional-index fallback)
|
|
136
|
+
// but that fallback breaks row identity once rows reorder (sort) or
|
|
137
|
+
// change presence (search / filter) — selection + expansion state
|
|
138
|
+
// then jump to the wrong rows. Warn when it's omitted AND a feature
|
|
139
|
+
// that relies on stable identity is active. Stripped in production.
|
|
140
|
+
const getRowIdProvided = props.getRowId !== undefined;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
143
|
+
if (getRowIdProvided) return;
|
|
144
|
+
const identityMatters =
|
|
145
|
+
rowSelection !== 'none' ||
|
|
146
|
+
expandable != null ||
|
|
147
|
+
enableSearch ||
|
|
148
|
+
cols.some((c) => Boolean(c.sortable));
|
|
149
|
+
if (identityMatters) {
|
|
150
|
+
console.warn(
|
|
151
|
+
'[Dashforge Table] `getRowId` is not set — falling back to the ' +
|
|
152
|
+
'positional row index. With sort / search / selection / ' +
|
|
153
|
+
'expandable rows active, the index-based key breaks row ' +
|
|
154
|
+
'identity when rows reorder. Pass a stable resolver, e.g. ' +
|
|
155
|
+
'`getRowId={(row) => row.id}`.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}, [getRowIdProvided, rowSelection, expandable, enableSearch, cols]);
|
|
159
|
+
|
|
135
160
|
// ───── Controllable state machinery ─────
|
|
136
161
|
|
|
137
162
|
const [sortModel, setSortModel] = useControllableState<TableSortModel>({
|
|
@@ -341,7 +366,14 @@ export function Table<T extends object>(props: TableProps<T>) {
|
|
|
341
366
|
? renderEmptyState({
|
|
342
367
|
totalColumnCount,
|
|
343
368
|
emptyState,
|
|
344
|
-
|
|
369
|
+
// Distinguish a genuinely empty dataset from one
|
|
370
|
+
// emptied by an active search / filter.
|
|
371
|
+
fallback:
|
|
372
|
+
rows.length > 0 &&
|
|
373
|
+
(debouncedSearchQuery.trim().length > 0 ||
|
|
374
|
+
filterModel.length > 0)
|
|
375
|
+
? labels.noResults
|
|
376
|
+
: labels.noData,
|
|
345
377
|
cellClass: cn(v.cell(), v.emptyState(), slotProps?.emptyState?.className),
|
|
346
378
|
})
|
|
347
379
|
: sortedRows.map((row, idx) => {
|
|
@@ -733,6 +765,10 @@ function renderLoadingRows<T extends object>(args: {
|
|
|
733
765
|
cellClass: string;
|
|
734
766
|
}): ReactNode {
|
|
735
767
|
const { count, visibleCols, showSelectionColumn, showExpandColumn, showRowActionsColumn, rowClass, cellClass } = args;
|
|
768
|
+
// No explicit row height here (unlike DataGrid's loading rows, which
|
|
769
|
+
// pin `rowHeight` for the virtualization math): Table is auto-height
|
|
770
|
+
// by design. Skeleton rows inherit the same density padding via
|
|
771
|
+
// `cellClass`, so they line up with real rows without a fixed height.
|
|
736
772
|
return Array.from({ length: count }).map((_, i) => (
|
|
737
773
|
<tr key={`loading-${i}`} className={rowClass} aria-busy="true">
|
|
738
774
|
{showSelectionColumn && <td className={cellClass}><Skeleton variant="rectangle" width="16px" height="16px" /></td>}
|
|
@@ -53,15 +53,12 @@ const chipVariants = tv({
|
|
|
53
53
|
{ variant: 'soft', color: 'danger', class: 'bg-danger-100 dark:bg-danger-950 text-danger-700 dark:text-danger-300' },
|
|
54
54
|
{ variant: 'soft', color: 'info', class: 'bg-info-100 dark:bg-info-950 text-info-700 dark:text-info-300' },
|
|
55
55
|
{ variant: 'soft', color: 'secondary', class: 'bg-secondary-100 dark:bg-secondary-950 text-secondary-700 dark:text-secondary-300' },
|
|
56
|
-
// solid = full bg +
|
|
57
|
-
// solid neutral: bg
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// auto-inverts: light text in light mode (against dark bg) is
|
|
63
|
-
// what we want; in dark mode `bg-neutral-700` becomes light AND
|
|
64
|
-
// `text-neutral-50` becomes dark — contrast preserved.
|
|
56
|
+
// solid = full bg + contrasting text.
|
|
57
|
+
// solid neutral: both `bg-neutral-700` and `text-neutral-50` sit
|
|
58
|
+
// on the neutral palette, so they auto-invert together via the
|
|
59
|
+
// CSS-var swap — bg/text contrast is preserved in BOTH modes.
|
|
60
|
+
// (`text-white` is deliberately NOT used here: white does not
|
|
61
|
+
// auto-invert, which would flip the contrast in dark mode.)
|
|
65
62
|
{ variant: 'solid', color: 'neutral', class: 'bg-neutral-700 text-neutral-50' },
|
|
66
63
|
{ variant: 'solid', color: 'primary', class: 'bg-primary-500 text-white' },
|
|
67
64
|
{ variant: 'solid', color: 'success', class: 'bg-success-500 text-white' },
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the Table cell renderer library — `RenderText`,
|
|
4
|
+
* `RenderTwoLine`, `RenderChip`, `RenderButton`, `RowActionsMenu`.
|
|
5
|
+
*
|
|
6
|
+
* Sprint 6 P4 — these shipped in Sprint 4.1 with only integration
|
|
7
|
+
* coverage (via Table/DataGrid tests). This file exercises them
|
|
8
|
+
* directly, including the RBAC + edge-case branches.
|
|
9
|
+
*/
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import { describe, it, expect, afterEach, beforeAll, vi } from 'vitest';
|
|
12
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
13
|
+
import { RenderText } from './RenderText.js';
|
|
14
|
+
import { RenderTwoLine } from './RenderTwoLine.js';
|
|
15
|
+
import { RenderChip } from './RenderChip.js';
|
|
16
|
+
import { RenderButton } from './RenderButton.js';
|
|
17
|
+
import { RowActionsMenu } from './RowActionsMenu.js';
|
|
18
|
+
|
|
19
|
+
void React;
|
|
20
|
+
afterEach(() => cleanup());
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
24
|
+
(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver =
|
|
25
|
+
class {
|
|
26
|
+
observe() {}
|
|
27
|
+
unobserve() {}
|
|
28
|
+
disconnect() {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('RenderText', () => {
|
|
34
|
+
it('renders the value in a span', () => {
|
|
35
|
+
render(<RenderText value="hello" />);
|
|
36
|
+
expect(screen.getByText('hello').tagName).toBe('SPAN');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies the truncate class when truncate is set', () => {
|
|
40
|
+
render(<RenderText value="long text" truncate />);
|
|
41
|
+
expect(screen.getByText('long text').className).toContain('truncate');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies the muted neutral color when muted is set', () => {
|
|
45
|
+
render(<RenderText value="dim" muted />);
|
|
46
|
+
expect(screen.getByText('dim').className).toContain('text-neutral-500');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('merges a custom className', () => {
|
|
50
|
+
render(<RenderText value="x" className="custom-cls" />);
|
|
51
|
+
expect(screen.getByText('x').className).toContain('custom-cls');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders nothing extra when no flags are set', () => {
|
|
55
|
+
render(<RenderText value="plain" />);
|
|
56
|
+
const el = screen.getByText('plain');
|
|
57
|
+
expect(el.className).not.toContain('truncate');
|
|
58
|
+
expect(el.className).not.toContain('text-neutral-500');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('RenderTwoLine', () => {
|
|
63
|
+
it('renders both primary and secondary text', () => {
|
|
64
|
+
render(<RenderTwoLine primary="Jane Doe" secondary="jane@example.com" />);
|
|
65
|
+
expect(screen.getByText('Jane Doe')).not.toBeNull();
|
|
66
|
+
expect(screen.getByText('jane@example.com')).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('primary is font-medium, secondary is muted + smaller', () => {
|
|
70
|
+
render(<RenderTwoLine primary="P" secondary="S" />);
|
|
71
|
+
expect(screen.getByText('P').className).toContain('font-medium');
|
|
72
|
+
expect(screen.getByText('S').className).toContain('text-neutral-500');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('merges a custom className on the wrapper', () => {
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<RenderTwoLine primary="P" secondary="S" className="twoline-cls" />,
|
|
78
|
+
);
|
|
79
|
+
expect(container.firstElementChild?.className).toContain('twoline-cls');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('RenderChip', () => {
|
|
84
|
+
it('renders children', () => {
|
|
85
|
+
render(<RenderChip>active</RenderChip>);
|
|
86
|
+
expect(screen.getByText('active')).not.toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('defaults to soft + neutral + sm', () => {
|
|
90
|
+
render(<RenderChip>x</RenderChip>);
|
|
91
|
+
// soft neutral → bg-neutral-100
|
|
92
|
+
expect(screen.getByText('x').className).toContain('bg-neutral-100');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it.each(['primary', 'success', 'warning', 'danger', 'info', 'secondary'] as const)(
|
|
96
|
+
'renders soft variant for the %s intent',
|
|
97
|
+
(color) => {
|
|
98
|
+
render(<RenderChip color={color}>{color}</RenderChip>);
|
|
99
|
+
expect(screen.getByText(color).className).toContain(`bg-${color}-100`);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it('renders the solid variant', () => {
|
|
104
|
+
render(
|
|
105
|
+
<RenderChip variant="solid" color="primary">
|
|
106
|
+
solid
|
|
107
|
+
</RenderChip>,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.getByText('solid').className).toContain('bg-primary-500');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('renders the outline variant', () => {
|
|
113
|
+
render(
|
|
114
|
+
<RenderChip variant="outline" color="danger">
|
|
115
|
+
outline
|
|
116
|
+
</RenderChip>,
|
|
117
|
+
);
|
|
118
|
+
expect(screen.getByText('outline').className).toContain('border-danger-500');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('renders the md size', () => {
|
|
122
|
+
render(
|
|
123
|
+
<RenderChip size="md">md</RenderChip>,
|
|
124
|
+
);
|
|
125
|
+
expect(screen.getByText('md').className).toContain('h-6');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('merges a custom className', () => {
|
|
129
|
+
render(<RenderChip className="chip-cls">x</RenderChip>);
|
|
130
|
+
expect(screen.getByText('x').className).toContain('chip-cls');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('RenderButton', () => {
|
|
135
|
+
it('renders the label inside a button', () => {
|
|
136
|
+
render(<RenderButton label="Edit" />);
|
|
137
|
+
expect(screen.getByRole('button', { name: 'Edit' })).not.toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('fires onClick when clicked', () => {
|
|
141
|
+
const onClick = vi.fn();
|
|
142
|
+
render(<RenderButton label="Go" onClick={onClick} />);
|
|
143
|
+
fireEvent.click(screen.getByRole('button', { name: 'Go' }));
|
|
144
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('honors the disabled prop', () => {
|
|
148
|
+
const onClick = vi.fn();
|
|
149
|
+
render(<RenderButton label="Nope" onClick={onClick} disabled />);
|
|
150
|
+
const btn = screen.getByRole('button', { name: 'Nope' }) as HTMLButtonElement;
|
|
151
|
+
expect(btn.disabled).toBe(true);
|
|
152
|
+
fireEvent.click(btn);
|
|
153
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('RowActionsMenu', () => {
|
|
158
|
+
interface Row {
|
|
159
|
+
id: string;
|
|
160
|
+
}
|
|
161
|
+
const row: Row = { id: 'r1' };
|
|
162
|
+
|
|
163
|
+
it('renders the default 3-dot trigger', () => {
|
|
164
|
+
render(
|
|
165
|
+
<RowActionsMenu
|
|
166
|
+
row={row}
|
|
167
|
+
actions={[{ label: 'Edit', onClick: () => {} }]}
|
|
168
|
+
/>,
|
|
169
|
+
);
|
|
170
|
+
expect(screen.getByRole('button', { name: 'Row actions' })).not.toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('returns null when there are no visible actions', () => {
|
|
174
|
+
const { container } = render(
|
|
175
|
+
<RowActionsMenu row={row} actions={[]} />,
|
|
176
|
+
);
|
|
177
|
+
expect(container.innerHTML).toBe('');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('opens the menu and renders the action items', () => {
|
|
181
|
+
render(
|
|
182
|
+
<RowActionsMenu
|
|
183
|
+
row={row}
|
|
184
|
+
actions={[
|
|
185
|
+
{ label: 'Edit', onClick: () => {} },
|
|
186
|
+
{ label: 'Delete', onClick: () => {}, color: 'danger' },
|
|
187
|
+
]}
|
|
188
|
+
/>,
|
|
189
|
+
);
|
|
190
|
+
fireEvent.click(screen.getByRole('button', { name: 'Row actions' }));
|
|
191
|
+
expect(screen.getByRole('menuitem', { name: 'Edit' })).not.toBeNull();
|
|
192
|
+
expect(screen.getByRole('menuitem', { name: 'Delete' })).not.toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('invokes the action onClick with the row', () => {
|
|
196
|
+
const onClick = vi.fn();
|
|
197
|
+
render(
|
|
198
|
+
<RowActionsMenu row={row} actions={[{ label: 'Edit', onClick }]} />,
|
|
199
|
+
);
|
|
200
|
+
fireEvent.click(screen.getByRole('button', { name: 'Row actions' }));
|
|
201
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Edit' }));
|
|
202
|
+
expect(onClick).toHaveBeenCalledWith(row);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('filters out actions whose RBAC says hide', () => {
|
|
206
|
+
render(
|
|
207
|
+
<RowActionsMenu
|
|
208
|
+
row={row}
|
|
209
|
+
actions={[
|
|
210
|
+
{ label: 'Edit', onClick: () => {} },
|
|
211
|
+
{
|
|
212
|
+
label: 'SecretDelete',
|
|
213
|
+
onClick: () => {},
|
|
214
|
+
access: { onUnauthorized: 'hide' },
|
|
215
|
+
},
|
|
216
|
+
]}
|
|
217
|
+
/>,
|
|
218
|
+
);
|
|
219
|
+
fireEvent.click(screen.getByRole('button', { name: 'Row actions' }));
|
|
220
|
+
expect(screen.queryByRole('menuitem', { name: 'Edit' })).not.toBeNull();
|
|
221
|
+
expect(screen.queryByRole('menuitem', { name: 'SecretDelete' })).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('renders a disabled action item when action.disabled is set', () => {
|
|
225
|
+
render(
|
|
226
|
+
<RowActionsMenu
|
|
227
|
+
row={row}
|
|
228
|
+
actions={[{ label: 'Edit', onClick: () => {}, disabled: true }]}
|
|
229
|
+
/>,
|
|
230
|
+
);
|
|
231
|
+
fireEvent.click(screen.getByRole('button', { name: 'Row actions' }));
|
|
232
|
+
const item = screen.getByRole('menuitem', { name: 'Edit' }) as HTMLButtonElement;
|
|
233
|
+
expect(item.disabled).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('uses a custom trigger when provided', () => {
|
|
237
|
+
render(
|
|
238
|
+
<RowActionsMenu
|
|
239
|
+
row={row}
|
|
240
|
+
actions={[{ label: 'Edit', onClick: () => {} }]}
|
|
241
|
+
trigger={<button type="button">custom-trigger</button>}
|
|
242
|
+
/>,
|
|
243
|
+
);
|
|
244
|
+
expect(screen.getByRole('button', { name: 'custom-trigger' })).not.toBeNull();
|
|
245
|
+
expect(screen.queryByRole('button', { name: 'Row actions' })).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -258,7 +258,14 @@ export type TableRowSelectionMode = 'none' | 'single' | 'multiple';
|
|
|
258
258
|
*/
|
|
259
259
|
export interface TableLabels {
|
|
260
260
|
searchPlaceholder?: string;
|
|
261
|
+
/** Shown when the dataset itself is empty (0 input rows). */
|
|
261
262
|
noData?: string;
|
|
263
|
+
/**
|
|
264
|
+
* Shown when the dataset has rows but an active search / filter
|
|
265
|
+
* excluded them all. Distinct from `noData` so the user can tell
|
|
266
|
+
* "nothing here" from "nothing matches your query".
|
|
267
|
+
*/
|
|
268
|
+
noResults?: string;
|
|
262
269
|
loading?: string;
|
|
263
270
|
ariaSortNone?: string;
|
|
264
271
|
ariaSortAscending?: string;
|
|
@@ -291,8 +298,8 @@ export interface TableLabels {
|
|
|
291
298
|
/** Column visibility menu (Popover-based, auto-commit). */
|
|
292
299
|
columnsButton?: string;
|
|
293
300
|
columnsTitle?: string;
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
/** Aria-label for the tri-state show/hide-all master checkbox. */
|
|
302
|
+
columnsToggleAll?: string;
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
// ───── Slot props ─────
|
|
@@ -353,7 +360,25 @@ export interface TableProps<T extends object>
|
|
|
353
360
|
// ───── Data ─────
|
|
354
361
|
rows: T[];
|
|
355
362
|
cols: TableColumn<T>[];
|
|
356
|
-
/**
|
|
363
|
+
/**
|
|
364
|
+
* Stable row-id resolver — the React key + the identity used by
|
|
365
|
+
* selection and row-expansion state.
|
|
366
|
+
*
|
|
367
|
+
* **Strongly recommended whenever sort / search / selection /
|
|
368
|
+
* expandable rows are used.** The default falls back to the
|
|
369
|
+
* positional index (`String(index)`); once rows reorder (sort) or
|
|
370
|
+
* change presence (search / filter), index `2` points at a
|
|
371
|
+
* *different* row, so selection and expansion state silently jump
|
|
372
|
+
* to the wrong rows. Pass a value derived from the data itself:
|
|
373
|
+
*
|
|
374
|
+
* ```tsx
|
|
375
|
+
* <Table getRowId={(row) => row.id} … />
|
|
376
|
+
* ```
|
|
377
|
+
*
|
|
378
|
+
* Dashforge emits a `console.warn` in development when `getRowId`
|
|
379
|
+
* is omitted and one of those features is active. (`<DataGrid>`
|
|
380
|
+
* makes this prop required for the same reason.)
|
|
381
|
+
*/
|
|
357
382
|
getRowId?: (row: T, index: number) => string;
|
|
358
383
|
|
|
359
384
|
// ───── Sort ─────
|
|
@@ -84,6 +84,21 @@ describe('<Typography>', () => {
|
|
|
84
84
|
const { container } = render(<Typography color="inherit">x</Typography>);
|
|
85
85
|
expect(container.firstElementChild?.className).toContain('text-inherit');
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
it('defaults to color="neutral" (text-neutral-900, NOT text-inherit)', () => {
|
|
89
|
+
// Sprint 6 P5 fix — a bare <Typography> must emit the
|
|
90
|
+
// auto-inverting neutral token, not inherit the consumer's
|
|
91
|
+
// arbitrary root colour (which broke dark-mode consistency).
|
|
92
|
+
const { container } = render(<Typography>x</Typography>);
|
|
93
|
+
const cls = container.firstElementChild?.className ?? '';
|
|
94
|
+
expect(cls).toContain('text-neutral-900');
|
|
95
|
+
expect(cls).not.toContain('text-inherit');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('color="muted" emits text-neutral-600', () => {
|
|
99
|
+
const { container } = render(<Typography color="muted">x</Typography>);
|
|
100
|
+
expect(container.firstElementChild?.className).toContain('text-neutral-600');
|
|
101
|
+
});
|
|
87
102
|
});
|
|
88
103
|
|
|
89
104
|
// ─── Override semantics ─────────────────────────────────────────────
|
|
@@ -55,6 +55,15 @@ export const typographyVariants = tv({
|
|
|
55
55
|
* paints white text).
|
|
56
56
|
*/
|
|
57
57
|
color: {
|
|
58
|
+
// `inherit` is the explicit escape hatch — use it on a
|
|
59
|
+
// `<Typography>` inside a coloured surface (e.g.
|
|
60
|
+
// `<Box variant="solid" color="primary">` paints white text)
|
|
61
|
+
// so the text picks up the surface colour. It is NO LONGER the
|
|
62
|
+
// default: a bare `<Typography>` would otherwise inherit the
|
|
63
|
+
// consuming app's arbitrary root colour (often OS-`prefers-
|
|
64
|
+
// color-scheme` driven), which diverges from the Dashforge
|
|
65
|
+
// `data-dash-tw-theme` mode and breaks colour consistency in
|
|
66
|
+
// dark mode. The default is now `neutral` — see `defaultVariants`.
|
|
58
67
|
inherit: 'text-inherit',
|
|
59
68
|
primary: 'text-primary-700 dark:text-primary-400',
|
|
60
69
|
secondary: 'text-secondary-700 dark:text-secondary-400',
|
|
@@ -109,7 +118,11 @@ export const typographyVariants = tv({
|
|
|
109
118
|
|
|
110
119
|
defaultVariants: {
|
|
111
120
|
variant: 'body1',
|
|
112
|
-
|
|
121
|
+
// `neutral` (not `inherit`): a bare `<Typography>` emits
|
|
122
|
+
// `text-neutral-900`, which auto-inverts via the dashforgePreset()
|
|
123
|
+
// CSS-var swap and follows the Dashforge mode. Pass
|
|
124
|
+
// `color="inherit"` explicitly inside a coloured surface.
|
|
125
|
+
color: 'neutral',
|
|
113
126
|
align: 'left',
|
|
114
127
|
},
|
|
115
128
|
});
|
package/src/index.ts
CHANGED
|
@@ -108,6 +108,26 @@ export type {
|
|
|
108
108
|
} from './components/DateTimePicker/dateTimePicker.types.js';
|
|
109
109
|
export { dateTimePickerVariants } from './components/DateTimePicker/dateTimePicker.variants.js';
|
|
110
110
|
|
|
111
|
+
// Custom calendar suite — Calendar primitive + bridge-integrated DatePicker,
|
|
112
|
+
// both built on the shared headless `@dashforge/calendar-core` engine.
|
|
113
|
+
|
|
114
|
+
export { Calendar } from './components/Calendar/Calendar.js';
|
|
115
|
+
export type {
|
|
116
|
+
CalendarProps,
|
|
117
|
+
CalendarSlotProps,
|
|
118
|
+
} from './components/Calendar/calendar.types.js';
|
|
119
|
+
export {
|
|
120
|
+
calendarVariants,
|
|
121
|
+
calendarDayVariants,
|
|
122
|
+
} from './components/Calendar/calendar.variants.js';
|
|
123
|
+
|
|
124
|
+
export { DatePicker } from './components/DatePicker/DatePicker.js';
|
|
125
|
+
export type {
|
|
126
|
+
DatePickerProps,
|
|
127
|
+
DatePickerSlotProps,
|
|
128
|
+
} from './components/DatePicker/datePicker.types.js';
|
|
129
|
+
export { datePickerVariants } from './components/DatePicker/datePicker.variants.js';
|
|
130
|
+
|
|
111
131
|
// F6 layout: router-agnostic navigation primitives.
|
|
112
132
|
|
|
113
133
|
export { Breadcrumbs } from './components/Breadcrumbs/Breadcrumbs.js';
|
|
@@ -353,4 +373,4 @@ export type { VariantProps } from 'tailwind-variants';
|
|
|
353
373
|
/**
|
|
354
374
|
* Package version (synced with `package.json` at publish time).
|
|
355
375
|
*/
|
|
356
|
-
export const VERSION = '0.
|
|
376
|
+
export const VERSION = '0.9.1-beta';
|