@dashforge/tw 0.6.0-beta → 0.7.0-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 +258 -0
- package/THEME-AUDIT.md +175 -0
- package/dist/index.esm.js +973 -69
- package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
- package/dist/src/components/Box/box.variants.d.ts.map +1 -1
- package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
- package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
- package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
- package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
- package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
- package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +8 -0
- package/dist/src/components/Table/table.types.d.ts.map +1 -1
- package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
- package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
- package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
- package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.d.ts +1 -1
- package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableFilter.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSearch.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSelection.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
- package/dist/src/components/{Table/_internal → _shared/data}/useTableSort.d.ts +1 -1
- package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
- package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/Accordion/accordion.variants.ts +5 -3
- package/src/components/Box/Box.test.tsx +32 -11
- package/src/components/Box/box.variants.ts +23 -4
- package/src/components/DataGrid/DataGrid.test.tsx +496 -0
- package/src/components/DataGrid/DataGrid.tsx +831 -0
- package/src/components/DataGrid/dataGrid.types.ts +189 -0
- package/src/components/DataGrid/dataGrid.variants.ts +144 -0
- package/src/components/Dialog/dialog.variants.ts +10 -6
- package/src/components/Divider/Divider.test.tsx +3 -1
- package/src/components/Divider/Divider.tsx +1 -1
- package/src/components/Divider/divider.variants.ts +2 -1
- package/src/components/Pagination/Pagination.tsx +3 -1
- package/src/components/Pagination/pagination.variants.ts +19 -13
- package/src/components/Popover/popover.variants.ts +10 -4
- package/src/components/Skeleton/skeleton.variants.ts +2 -1
- package/src/components/Table/Table.tsx +8 -8
- package/src/components/Table/table.types.ts +9 -0
- package/src/components/Tabs/tabs.variants.ts +19 -8
- package/src/components/Tooltip/tooltip.variants.ts +12 -2
- package/src/components/Typography/Typography.test.tsx +3 -1
- package/src/components/Typography/typography.variants.ts +6 -2
- package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useColumnAutoDetect.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableFilter.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSearch.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSearch.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSelection.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSort.test.ts +1 -1
- package/src/components/{Table/_internal → _shared/data}/useTableSort.ts +1 -1
- package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
- package/src/components/_shared/data/useVirtualizer.ts +199 -0
- package/src/components/_shared/themeIdentity.test.ts +113 -0
- package/src/index.ts +18 -2
- package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useControllableState.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +0 -1
- package/dist/src/components/Table/_internal/useTableSort.d.ts.map +0 -1
- package/src/components/Table/_internal/themeIdentity.test.ts +0 -84
- /package/dist/src/components/{Table/_internal → _shared/data}/getNestedValue.d.ts +0 -0
- /package/dist/src/components/{Table/_internal → _shared/data}/useControllableState.d.ts +0 -0
- /package/dist/src/components/{Table/_internal → _shared/data}/useDebouncedValue.d.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/getNestedValue.test.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/getNestedValue.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useControllableState.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useDebouncedValue.ts +0 -0
- /package/src/components/{Table/_internal → _shared/data}/useTableSelection.test.ts +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AccessRequirement } from '@dashforge/rbac';
|
|
3
|
+
import type { DataGridVariants } from './dataGrid.variants.js';
|
|
4
|
+
import type {
|
|
5
|
+
TableColumn,
|
|
6
|
+
TableSortModel,
|
|
7
|
+
TableFilterModel,
|
|
8
|
+
TableRowSelectionMode,
|
|
9
|
+
TableLabels,
|
|
10
|
+
} from '../Table/table.types.js';
|
|
11
|
+
|
|
12
|
+
// Re-export the column type unchanged — DataGrid uses the SAME shape
|
|
13
|
+
// as Table. Consumers writing `cols: TableColumn<T>[]` get autocomplete
|
|
14
|
+
// and don't need a parallel `DataGridColumn<T>` type.
|
|
15
|
+
|
|
16
|
+
// ───── Selection scope ─────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determines what the "select all" header checkbox covers when
|
|
20
|
+
* `rowSelection="multiple"` is enabled.
|
|
21
|
+
*
|
|
22
|
+
* - `'visible'` — only the rows currently in the viewport (after
|
|
23
|
+
* virtualization windowing) are toggled. Useful when the
|
|
24
|
+
* underlying data is paginated server-side and "all" semantically
|
|
25
|
+
* refers to the screen.
|
|
26
|
+
* - `'allLoaded'` (default) — all rows currently in the `rows` prop
|
|
27
|
+
* are toggled. The standard "select everything I can see in this
|
|
28
|
+
* dataset" pattern.
|
|
29
|
+
*
|
|
30
|
+
* For "select all in server-side dataset", the consumer wires it
|
|
31
|
+
* themselves (e.g. fetch all ids + setSelectedRowIds).
|
|
32
|
+
*/
|
|
33
|
+
export type DataGridSelectAllScope = 'visible' | 'allLoaded';
|
|
34
|
+
|
|
35
|
+
// ───── Server-side mode flags ─────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Independent opt-in flags that delegate the corresponding pipeline
|
|
39
|
+
* step to the consumer. With each flag enabled, DataGrid emits the
|
|
40
|
+
* `onXChange` callback but does NOT perform the operation locally
|
|
41
|
+
* — the consumer is expected to provide the resulting `rows` array.
|
|
42
|
+
*/
|
|
43
|
+
export interface DataGridServerSideFlags {
|
|
44
|
+
serverSideSort?: boolean;
|
|
45
|
+
serverSideFilter?: boolean;
|
|
46
|
+
serverSideSearch?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* When true, DataGrid does NOT slice `rows` for pagination. The
|
|
49
|
+
* virtualization math uses `totalCount` (required) to size the
|
|
50
|
+
* scroll container.
|
|
51
|
+
*/
|
|
52
|
+
serverSidePagination?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ───── Pagination (DataGrid-internal, opt-in) ─────
|
|
56
|
+
|
|
57
|
+
export interface DataGridPaginationConfig {
|
|
58
|
+
page: number;
|
|
59
|
+
pageSize: number;
|
|
60
|
+
onPageChange: (page: number) => void;
|
|
61
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
62
|
+
pageSizeOptions?: number[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ───── Slot props ─────
|
|
66
|
+
|
|
67
|
+
export interface DataGridSlotProps {
|
|
68
|
+
root?: { className?: string };
|
|
69
|
+
toolbar?: { className?: string };
|
|
70
|
+
search?: { className?: string };
|
|
71
|
+
scroll?: { className?: string };
|
|
72
|
+
table?: { className?: string };
|
|
73
|
+
thead?: { className?: string };
|
|
74
|
+
tbody?: { className?: string };
|
|
75
|
+
headerRow?: { className?: string };
|
|
76
|
+
headerCell?: { className?: string };
|
|
77
|
+
row?: { className?: string };
|
|
78
|
+
cell?: { className?: string };
|
|
79
|
+
emptyState?: { className?: string };
|
|
80
|
+
loadingOverlay?: { className?: string };
|
|
81
|
+
selectionCell?: { className?: string };
|
|
82
|
+
rowActionsCell?: { className?: string };
|
|
83
|
+
bulkActionFooter?: { className?: string };
|
|
84
|
+
/** Optional Pagination wrapper below the virtualized scroll. */
|
|
85
|
+
pagination?: { className?: string };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ───── Main props ─────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Props for `<DataGrid>`.
|
|
92
|
+
*
|
|
93
|
+
* Companion to `<Table>` for large data sets. Where Table renders
|
|
94
|
+
* every row in DOM, DataGrid uses **homemade virtualization**
|
|
95
|
+
* (`IntersectionObserver` + scroll-event + `requestAnimationFrame`
|
|
96
|
+
* — no external libs) to render only a window of visible rows.
|
|
97
|
+
*
|
|
98
|
+
* Required:
|
|
99
|
+
* - `rows`, `cols`, `getRowId` — same as Table
|
|
100
|
+
* - `rowHeight` (fixed in v1; variable-height deferred to v1-bis)
|
|
101
|
+
* - `height` — explicit container height in CSS units, OR
|
|
102
|
+
* consumer sets it via `sx` / parent layout (the scroll container
|
|
103
|
+
* needs a bounded height for virtualization to work)
|
|
104
|
+
*
|
|
105
|
+
* Optional:
|
|
106
|
+
* - `overscan` — extra rows above/below viewport (default 5)
|
|
107
|
+
* - Server-side mode flags (4 independent opt-ins)
|
|
108
|
+
* - Internal pagination (opt-in via `pagination` config)
|
|
109
|
+
* - `selectAllScope` — `'visible'` vs `'allLoaded'` for header
|
|
110
|
+
* select-all checkbox
|
|
111
|
+
* - Everything Table accepts: sort / filter / search / selection /
|
|
112
|
+
* row actions / RBAC / variants / i18n labels / sx / slotProps
|
|
113
|
+
*/
|
|
114
|
+
export interface DataGridProps<T extends object>
|
|
115
|
+
extends Pick<DataGridVariants, 'variant' | 'size' | 'density'>,
|
|
116
|
+
DataGridServerSideFlags {
|
|
117
|
+
// ───── Data ─────
|
|
118
|
+
rows: T[];
|
|
119
|
+
cols: TableColumn<T>[];
|
|
120
|
+
getRowId: (row: T, index: number) => string;
|
|
121
|
+
|
|
122
|
+
/** Required: total dataset count. In client-side mode equals
|
|
123
|
+
* `rows.length`; in `serverSidePagination` mode the consumer
|
|
124
|
+
* provides the full count so the virtual scroll height matches. */
|
|
125
|
+
totalCount?: number;
|
|
126
|
+
|
|
127
|
+
// ───── Virtualization (REQUIRED) ─────
|
|
128
|
+
/** Fixed row height in pixels. Required for windowing math. */
|
|
129
|
+
rowHeight: number;
|
|
130
|
+
/** Rows rendered above/below the viewport. @default 5 */
|
|
131
|
+
overscan?: number;
|
|
132
|
+
/** Container height in CSS units (e.g. `"600px"`, `"100vh"`).
|
|
133
|
+
* Required for the scroll container to bound the virtualization. */
|
|
134
|
+
height?: string;
|
|
135
|
+
|
|
136
|
+
// ───── Sort ─────
|
|
137
|
+
sortModel?: TableSortModel;
|
|
138
|
+
onSortChange?: (model: TableSortModel) => void;
|
|
139
|
+
defaultSortModel?: TableSortModel;
|
|
140
|
+
|
|
141
|
+
// ───── Search ─────
|
|
142
|
+
enableSearch?: boolean;
|
|
143
|
+
searchQuery?: string;
|
|
144
|
+
onSearchQueryChange?: (q: string) => void;
|
|
145
|
+
searchPlaceholder?: string;
|
|
146
|
+
searchDebounceMs?: number;
|
|
147
|
+
|
|
148
|
+
// ───── Filter (model only; per-column UI deferred to v1-bis) ─────
|
|
149
|
+
filterModel?: TableFilterModel;
|
|
150
|
+
onFilterChange?: (model: TableFilterModel) => void;
|
|
151
|
+
|
|
152
|
+
// ───── Selection ─────
|
|
153
|
+
rowSelection?: TableRowSelectionMode;
|
|
154
|
+
selectedRowIds?: string[];
|
|
155
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
156
|
+
/** Bulk action sticky footer (visible only when selection > 0). */
|
|
157
|
+
bulkActions?: (selectedRows: T[]) => ReactNode;
|
|
158
|
+
/**
|
|
159
|
+
* Scope of the "select all" header checkbox.
|
|
160
|
+
* @default 'allLoaded'
|
|
161
|
+
*/
|
|
162
|
+
selectAllScope?: DataGridSelectAllScope;
|
|
163
|
+
|
|
164
|
+
// ───── Internal pagination (opt-in) ─────
|
|
165
|
+
pagination?: DataGridPaginationConfig;
|
|
166
|
+
|
|
167
|
+
// ───── Row actions ─────
|
|
168
|
+
rowActions?: (row: T) => ReactNode;
|
|
169
|
+
|
|
170
|
+
// ───── States ─────
|
|
171
|
+
loading?: boolean;
|
|
172
|
+
loadingRowCount?: number;
|
|
173
|
+
emptyState?: ReactNode;
|
|
174
|
+
|
|
175
|
+
// ───── A11Y ─────
|
|
176
|
+
stickyHeader?: boolean;
|
|
177
|
+
caption?: ReactNode;
|
|
178
|
+
showCaption?: boolean;
|
|
179
|
+
|
|
180
|
+
// ───── RBAC ─────
|
|
181
|
+
access?: AccessRequirement;
|
|
182
|
+
|
|
183
|
+
// ───── i18n ─────
|
|
184
|
+
labels?: TableLabels;
|
|
185
|
+
|
|
186
|
+
// ───── Customization ─────
|
|
187
|
+
sx?: string;
|
|
188
|
+
slotProps?: DataGridSlotProps;
|
|
189
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<DataGrid>` variant recipe.
|
|
5
|
+
*
|
|
6
|
+
* Sibling of `tableVariants` — shares the same visual axes (variant /
|
|
7
|
+
* size / density) and the same identity-rule patterns:
|
|
8
|
+
* - No `dark:` variants on neutral palette (auto-invert via the
|
|
9
|
+
* dashforgePreset() CSS var swap).
|
|
10
|
+
* - Selected row uses LeftNav `itemActive` pattern
|
|
11
|
+
* (`bg-primary-100 text-primary-900`) — guaranteed contrast.
|
|
12
|
+
* - Hover uses one-tier elevation (`bg-neutral-100`).
|
|
13
|
+
*
|
|
14
|
+
* DataGrid-specific slot additions vs Table:
|
|
15
|
+
* - `stickyLeftCell` — column that uses `cols[i].sticky === 'left'`.
|
|
16
|
+
* Z-index ordering: `thead` (z-10) < `stickyLeftCell` (z-11) <
|
|
17
|
+
* `header × stickyLeft top-left corner cell` (z-20).
|
|
18
|
+
* - `paginationFooter` — wraps the optional `<Pagination>`.
|
|
19
|
+
*/
|
|
20
|
+
export const dataGridVariants = tv({
|
|
21
|
+
slots: {
|
|
22
|
+
root: 'w-full',
|
|
23
|
+
toolbar: 'flex flex-wrap items-center gap-3 mb-3',
|
|
24
|
+
search: [
|
|
25
|
+
'inline-flex items-center gap-2',
|
|
26
|
+
'w-full max-w-sm rounded-md',
|
|
27
|
+
'border border-neutral-300',
|
|
28
|
+
'bg-neutral-50',
|
|
29
|
+
'focus-within:ring-2 focus-within:ring-primary-500',
|
|
30
|
+
'transition-colors motion-reduce:transition-none',
|
|
31
|
+
],
|
|
32
|
+
// The scroll container needs an explicit bounded height for
|
|
33
|
+
// virtualization to compute the window. The consumer passes
|
|
34
|
+
// `height` prop (inline style) or sets it via `sx` / parent
|
|
35
|
+
// layout — DataGrid does NOT default a height (would be wrong
|
|
36
|
+
// for most layouts).
|
|
37
|
+
scroll: 'relative w-full overflow-auto rounded-md',
|
|
38
|
+
table: 'w-full caption-bottom text-left',
|
|
39
|
+
thead: 'bg-neutral-50 sticky top-0 z-10',
|
|
40
|
+
tbody: '',
|
|
41
|
+
headerRow: '',
|
|
42
|
+
headerCell: [
|
|
43
|
+
'text-neutral-700',
|
|
44
|
+
'font-semibold whitespace-nowrap select-none',
|
|
45
|
+
'border-b border-neutral-200',
|
|
46
|
+
'bg-neutral-50',
|
|
47
|
+
],
|
|
48
|
+
headerCellButton: [
|
|
49
|
+
'group inline-flex items-center gap-1 select-none',
|
|
50
|
+
'hover:text-neutral-900',
|
|
51
|
+
'focus:outline-none focus-visible:underline',
|
|
52
|
+
'transition-colors motion-reduce:transition-none',
|
|
53
|
+
],
|
|
54
|
+
row: [
|
|
55
|
+
'group/row transition-colors motion-reduce:transition-none',
|
|
56
|
+
'hover:bg-neutral-100',
|
|
57
|
+
'data-[selected=true]:bg-primary-100',
|
|
58
|
+
'data-[selected=true]:text-primary-900',
|
|
59
|
+
'data-[selected=true]:font-medium',
|
|
60
|
+
],
|
|
61
|
+
cell: [
|
|
62
|
+
'text-neutral-900',
|
|
63
|
+
'border-b border-neutral-200',
|
|
64
|
+
'align-middle',
|
|
65
|
+
],
|
|
66
|
+
selectionCell: 'w-10 text-center',
|
|
67
|
+
rowActionsCell: [
|
|
68
|
+
'w-12 text-right',
|
|
69
|
+
'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
|
|
70
|
+
'transition-opacity motion-reduce:transition-none',
|
|
71
|
+
],
|
|
72
|
+
emptyState: 'py-12 text-center text-neutral-500',
|
|
73
|
+
loadingOverlay: 'pointer-events-none',
|
|
74
|
+
bulkActionFooter: [
|
|
75
|
+
'sticky bottom-0 z-20',
|
|
76
|
+
'flex flex-wrap items-center justify-between gap-3',
|
|
77
|
+
'px-3 py-2',
|
|
78
|
+
'border-t border-neutral-200',
|
|
79
|
+
'bg-neutral-50 shadow-md',
|
|
80
|
+
],
|
|
81
|
+
// Sticky left column — uses `position: sticky; left: 0` + z-index
|
|
82
|
+
// so the column stays pinned during horizontal scroll. The
|
|
83
|
+
// `bg-neutral-50` ensures the column doesn't bleed when content
|
|
84
|
+
// scrolls underneath.
|
|
85
|
+
stickyLeftCell: 'sticky left-0 z-[1] bg-neutral-50',
|
|
86
|
+
// Sticky left × header intersection (top-left corner): higher
|
|
87
|
+
// z-index than both header (`z-10`) and sticky col (`z-[1]`).
|
|
88
|
+
stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
|
|
89
|
+
paginationFooter: 'flex justify-end mt-3',
|
|
90
|
+
},
|
|
91
|
+
variants: {
|
|
92
|
+
variant: {
|
|
93
|
+
plain: {},
|
|
94
|
+
lines: {},
|
|
95
|
+
striped: {
|
|
96
|
+
row: 'odd:bg-neutral-100/60',
|
|
97
|
+
},
|
|
98
|
+
bordered: {
|
|
99
|
+
table: 'border border-neutral-200',
|
|
100
|
+
cell: 'border-r last:border-r-0 border-neutral-200',
|
|
101
|
+
headerCell:
|
|
102
|
+
'border-r last:border-r-0 border-neutral-200',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
size: {
|
|
106
|
+
sm: {
|
|
107
|
+
headerCell: 'text-xs',
|
|
108
|
+
cell: 'text-xs',
|
|
109
|
+
search: 'text-xs',
|
|
110
|
+
},
|
|
111
|
+
md: {
|
|
112
|
+
headerCell: 'text-sm',
|
|
113
|
+
cell: 'text-sm',
|
|
114
|
+
search: 'text-sm',
|
|
115
|
+
},
|
|
116
|
+
lg: {
|
|
117
|
+
headerCell: 'text-base',
|
|
118
|
+
cell: 'text-base',
|
|
119
|
+
search: 'text-base',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
density: {
|
|
123
|
+
compact: {
|
|
124
|
+
headerCell: 'px-2 py-1',
|
|
125
|
+
cell: 'px-2 py-1',
|
|
126
|
+
},
|
|
127
|
+
comfortable: {
|
|
128
|
+
headerCell: 'px-3 py-2',
|
|
129
|
+
cell: 'px-3 py-2',
|
|
130
|
+
},
|
|
131
|
+
spacious: {
|
|
132
|
+
headerCell: 'px-4 py-3',
|
|
133
|
+
cell: 'px-4 py-3',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
defaultVariants: {
|
|
138
|
+
variant: 'lines',
|
|
139
|
+
size: 'md',
|
|
140
|
+
density: 'comfortable',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export type DataGridVariants = VariantProps<typeof dataGridVariants>;
|
|
@@ -26,21 +26,25 @@ export const dialogVariants = tv({
|
|
|
26
26
|
],
|
|
27
27
|
content: [
|
|
28
28
|
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// bg-white hardcoded (doesn't auto-invert) so `dark:` is needed.
|
|
30
|
+
// Target `dark:bg-neutral-100` = #171717 in dark mode (one
|
|
31
|
+
// elevation tier above page surface).
|
|
32
|
+
'w-full bg-white dark:bg-neutral-100 rounded-lg shadow-xl',
|
|
33
|
+
// border-neutral-200 auto-inverts — no `dark:` needed.
|
|
34
|
+
'border border-neutral-200',
|
|
31
35
|
'p-6 flex flex-col gap-4 max-h-[90vh]',
|
|
32
36
|
'focus:outline-none',
|
|
33
37
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
34
38
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
35
39
|
'motion-reduce:transition-none motion-reduce:duration-0',
|
|
36
40
|
],
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// All neutral text classes auto-invert via CSS-var swap.
|
|
42
|
+
title: 'text-lg font-semibold leading-tight text-neutral-900',
|
|
43
|
+
description: 'text-sm text-neutral-600',
|
|
39
44
|
closeButton: [
|
|
40
45
|
'absolute top-4 right-4 inline-flex items-center justify-center',
|
|
41
46
|
'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
|
|
42
|
-
'
|
|
43
|
-
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
47
|
+
'hover:bg-neutral-100',
|
|
44
48
|
'focus:outline-none focus:ring-2 focus:ring-primary-500',
|
|
45
49
|
'transition-colors',
|
|
46
50
|
],
|
|
@@ -66,8 +66,10 @@ describe('<Divider>', () => {
|
|
|
66
66
|
it('default neutral', () => {
|
|
67
67
|
const { container } = render(<Divider />);
|
|
68
68
|
const cls = container.firstElementChild?.className ?? '';
|
|
69
|
+
// Sprint 4.3 identity sweep: neutral palette auto-inverts via
|
|
70
|
+
// the dashforgePreset() CSS-var swap — no `dark:` needed.
|
|
69
71
|
expect(cls).toContain('border-neutral-200');
|
|
70
|
-
expect(cls).toContain('dark:border-neutral-
|
|
72
|
+
expect(cls).not.toContain('dark:border-neutral-');
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
it('color="primary"', () => {
|
|
@@ -128,7 +128,7 @@ export const Divider = forwardRef<HTMLElement, DividerProps>(
|
|
|
128
128
|
{...rest}
|
|
129
129
|
>
|
|
130
130
|
<span aria-hidden="true" className={cn(lineCommon, leftStubClass)} />
|
|
131
|
-
<span className={cn('shrink-0 text-sm text-neutral-500
|
|
131
|
+
<span className={cn('shrink-0 text-sm text-neutral-500', labelPadding)}>
|
|
132
132
|
{children}
|
|
133
133
|
</span>
|
|
134
134
|
<span aria-hidden="true" className={cn(lineCommon, rightStubClass)} />
|
|
@@ -85,7 +85,8 @@ export const dividerLineVariants = tv({
|
|
|
85
85
|
},
|
|
86
86
|
|
|
87
87
|
color: {
|
|
88
|
-
|
|
88
|
+
// Neutral palette auto-inverts via CSS-var swap (no `dark:`).
|
|
89
|
+
neutral: 'border-neutral-200',
|
|
89
90
|
primary: 'border-primary-300 dark:border-primary-700',
|
|
90
91
|
secondary: 'border-secondary-300 dark:border-secondary-700',
|
|
91
92
|
success: 'border-success-300 dark:border-success-700',
|
|
@@ -238,7 +238,9 @@ export function Pagination(props: PaginationProps) {
|
|
|
238
238
|
disabled={disabled}
|
|
239
239
|
aria-label={labels.perPage}
|
|
240
240
|
className={cn(
|
|
241
|
-
|
|
241
|
+
// Auto-invert via CSS-var swap — no `dark:` variants needed
|
|
242
|
+
// on neutral palette.
|
|
243
|
+
'rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1',
|
|
242
244
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
243
245
|
)}
|
|
244
246
|
>
|
|
@@ -23,28 +23,34 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
|
|
23
23
|
export const paginationVariants = tv({
|
|
24
24
|
slots: {
|
|
25
25
|
root: 'flex flex-wrap items-center gap-3',
|
|
26
|
-
|
|
26
|
+
// All neutral classes auto-invert via the dashforgePreset() CSS-var
|
|
27
|
+
// swap. `bg-neutral-50` provides the button surface (matches page
|
|
28
|
+
// surface; the border gives the separation). `hover:bg-neutral-100`
|
|
29
|
+
// is the one-tier elevation that works in both modes.
|
|
30
|
+
summary: 'text-neutral-600',
|
|
27
31
|
list: 'inline-flex items-center gap-1',
|
|
28
32
|
pageButton: [
|
|
29
33
|
'inline-flex items-center justify-center rounded-md',
|
|
30
|
-
'border border-neutral-300
|
|
31
|
-
'bg-
|
|
32
|
-
'text-neutral-700
|
|
33
|
-
'hover:bg-neutral-
|
|
34
|
+
'border border-neutral-300',
|
|
35
|
+
'bg-neutral-50',
|
|
36
|
+
'text-neutral-700',
|
|
37
|
+
'hover:bg-neutral-100',
|
|
34
38
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
35
39
|
'transition-colors motion-reduce:transition-none',
|
|
36
40
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
37
41
|
],
|
|
38
42
|
activeButton: [
|
|
43
|
+
// Primary palette does NOT auto-invert — `dark:` shift is a
|
|
44
|
+
// design choice for tone refinement (kept).
|
|
39
45
|
'bg-primary-500 text-white border-primary-500',
|
|
40
46
|
'hover:bg-primary-600 dark:hover:bg-primary-400',
|
|
41
47
|
],
|
|
42
48
|
navButton: [
|
|
43
49
|
'inline-flex items-center justify-center rounded-md',
|
|
44
|
-
'border border-neutral-300
|
|
45
|
-
'bg-
|
|
46
|
-
'text-neutral-700
|
|
47
|
-
'hover:bg-neutral-
|
|
50
|
+
'border border-neutral-300',
|
|
51
|
+
'bg-neutral-50',
|
|
52
|
+
'text-neutral-700',
|
|
53
|
+
'hover:bg-neutral-100',
|
|
48
54
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
49
55
|
'transition-colors motion-reduce:transition-none',
|
|
50
56
|
'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
|
|
@@ -52,12 +58,12 @@ export const paginationVariants = tv({
|
|
|
52
58
|
ellipsis: 'inline-flex items-center justify-center text-neutral-500 px-1',
|
|
53
59
|
pageSizeSelector: [
|
|
54
60
|
'inline-flex items-center gap-2',
|
|
55
|
-
'text-neutral-700
|
|
61
|
+
'text-neutral-700',
|
|
56
62
|
],
|
|
57
63
|
jumpInput: [
|
|
58
|
-
'rounded-md border border-neutral-300
|
|
59
|
-
'bg-
|
|
60
|
-
'text-neutral-900
|
|
64
|
+
'rounded-md border border-neutral-300',
|
|
65
|
+
'bg-neutral-50',
|
|
66
|
+
'text-neutral-900',
|
|
61
67
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
62
68
|
'transition-colors motion-reduce:transition-none',
|
|
63
69
|
],
|
|
@@ -3,15 +3,21 @@ import { tv, type VariantProps } from 'tailwind-variants';
|
|
|
3
3
|
export const popoverVariants = tv({
|
|
4
4
|
slots: {
|
|
5
5
|
content: [
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
|
|
6
|
+
// Neutral border auto-inverts via CSS-var swap.
|
|
7
|
+
'z-50 rounded-md border border-neutral-200',
|
|
8
|
+
// bg-white hardcoded (no auto-invert) so `dark:` is required.
|
|
9
|
+
// Target `dark:bg-neutral-100` resolves to #171717 in dark mode
|
|
10
|
+
// (one elevation tier above the page surface).
|
|
11
|
+
'bg-white dark:bg-neutral-100 p-4 shadow-lg',
|
|
12
|
+
// Text auto-inverts.
|
|
13
|
+
'text-sm text-neutral-900',
|
|
9
14
|
'focus:outline-none',
|
|
10
15
|
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
11
16
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
12
17
|
'motion-reduce:transition-none motion-reduce:duration-0',
|
|
13
18
|
],
|
|
14
|
-
|
|
19
|
+
// Arrow follows content bg: `fill-white` in light + matching dark target.
|
|
20
|
+
arrow: 'fill-white dark:fill-neutral-100',
|
|
15
21
|
},
|
|
16
22
|
});
|
|
17
23
|
|
|
@@ -3,19 +3,19 @@ import { cn } from '../../utils/cn.js';
|
|
|
3
3
|
import { useAccessState } from '../../hooks/useAccessState.js';
|
|
4
4
|
import { Skeleton } from '../Skeleton/Skeleton.js';
|
|
5
5
|
import { tableVariants } from './table.variants.js';
|
|
6
|
-
import { useControllableState } from '
|
|
7
|
-
import { useDebouncedValue } from '
|
|
8
|
-
import { useTableSearch } from '
|
|
9
|
-
import { useTableFilter } from '
|
|
10
|
-
import { useTableSort, cycleSortFor } from '
|
|
11
|
-
import { useTableSelection } from '
|
|
12
|
-
import { getNestedValue } from '
|
|
6
|
+
import { useControllableState } from '../_shared/data/useControllableState.js';
|
|
7
|
+
import { useDebouncedValue } from '../_shared/data/useDebouncedValue.js';
|
|
8
|
+
import { useTableSearch } from '../_shared/data/useTableSearch.js';
|
|
9
|
+
import { useTableFilter } from '../_shared/data/useTableFilter.js';
|
|
10
|
+
import { useTableSort, cycleSortFor } from '../_shared/data/useTableSort.js';
|
|
11
|
+
import { useTableSelection } from '../_shared/data/useTableSelection.js';
|
|
12
|
+
import { getNestedValue } from '../_shared/data/getNestedValue.js';
|
|
13
13
|
import {
|
|
14
14
|
useColumnAutoDetect,
|
|
15
15
|
resolveAlign,
|
|
16
16
|
resolveMonospace,
|
|
17
17
|
resolveTabularNums,
|
|
18
|
-
} from '
|
|
18
|
+
} from '../_shared/data/useColumnAutoDetect.js';
|
|
19
19
|
import type {
|
|
20
20
|
TableProps,
|
|
21
21
|
TableColumn,
|
|
@@ -151,6 +151,15 @@ export interface TableColumn<T extends object> {
|
|
|
151
151
|
*/
|
|
152
152
|
access?: AccessRequirement;
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Pin this column during horizontal scroll. `'left'` (DataGrid v1)
|
|
156
|
+
* uses CSS `position: sticky; left: 0`. `'right'` is reserved for
|
|
157
|
+
* v1-bis (CSS edge cases around last-column borders). Table
|
|
158
|
+
* (non-virtualized) ignores this prop today — only DataGrid honors
|
|
159
|
+
* it (Sprint 4.2).
|
|
160
|
+
*/
|
|
161
|
+
sticky?: 'left' | 'right';
|
|
162
|
+
|
|
154
163
|
/** Reserved for v1-bis (column-visibility dialog). */
|
|
155
164
|
hideable?: boolean;
|
|
156
165
|
defaultHidden?: boolean;
|
|
@@ -19,21 +19,32 @@ export const tabsVariants = tv({
|
|
|
19
19
|
variants: {
|
|
20
20
|
variant: {
|
|
21
21
|
underline: {
|
|
22
|
-
|
|
22
|
+
// Neutral palette auto-inverts via CSS-var swap.
|
|
23
|
+
list: 'border-b border-neutral-200',
|
|
23
24
|
trigger: [
|
|
24
|
-
'px-3 py-2 border-b-2 border-transparent text-neutral-600
|
|
25
|
-
'hover:text-neutral-900
|
|
25
|
+
'px-3 py-2 border-b-2 border-transparent text-neutral-600',
|
|
26
|
+
'hover:text-neutral-900',
|
|
27
|
+
// Primary palette does NOT auto-invert — `dark:` shift is
|
|
28
|
+
// an intentional tone refinement (kept).
|
|
26
29
|
'data-[state=active]:border-primary-500 data-[state=active]:text-primary-700',
|
|
27
30
|
'dark:data-[state=active]:text-primary-400',
|
|
28
31
|
],
|
|
29
32
|
},
|
|
30
33
|
pill: {
|
|
31
|
-
|
|
34
|
+
// Pill list bg uses the elevation tier — auto-inverts.
|
|
35
|
+
list: 'rounded-lg bg-neutral-100 p-1',
|
|
32
36
|
trigger: [
|
|
33
|
-
'px-3 py-1.5 rounded-md text-neutral-600
|
|
34
|
-
'hover:text-neutral-900
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
'px-3 py-1.5 rounded-md text-neutral-600',
|
|
38
|
+
'hover:text-neutral-900',
|
|
39
|
+
// Active pill is one tier above the list bg
|
|
40
|
+
// (`bg-neutral-100`-inverted = #171717 in dark).
|
|
41
|
+
// `bg-white` is hardcoded so a `dark:` is required —
|
|
42
|
+
// target `dark:bg-neutral-200` (= #262626 in dark) sits
|
|
43
|
+
// ONE TIER above the list bg, preserving the elevation
|
|
44
|
+
// semantic from light mode (where `bg-white` sits above
|
|
45
|
+
// `bg-neutral-100` = #f5f5f5).
|
|
46
|
+
'data-[state=active]:bg-white dark:data-[state=active]:bg-neutral-200',
|
|
47
|
+
'data-[state=active]:text-neutral-900',
|
|
37
48
|
'data-[state=active]:shadow-sm',
|
|
38
49
|
],
|
|
39
50
|
},
|
|
@@ -4,14 +4,24 @@ export const tooltipVariants = tv({
|
|
|
4
4
|
slots: {
|
|
5
5
|
content: [
|
|
6
6
|
'z-50 max-w-xs rounded-md px-3 py-1.5 text-xs font-medium',
|
|
7
|
+
// Auto-invert via the dashforgePreset() CSS-var swap:
|
|
8
|
+
// - Light mode: `bg-neutral-900` = #171717 (dark surface) +
|
|
9
|
+
// `text-neutral-50` = #fafafa (light text) — DARK tooltip
|
|
10
|
+
// on the light page (high contrast).
|
|
11
|
+
// - Dark mode: `bg-neutral-900` flips to #f5f5f5 (light) +
|
|
12
|
+
// `text-neutral-50` flips to #0a0a0a (dark text) — LIGHT
|
|
13
|
+
// tooltip on the dark page (high contrast against page).
|
|
14
|
+
// Adding `dark:bg-neutral-50 dark:text-neutral-900` (the old
|
|
15
|
+
// pattern) would force the dark-mode tooltip back to a dark
|
|
16
|
+
// surface — invisible against the dark page.
|
|
7
17
|
'bg-neutral-900 text-neutral-50',
|
|
8
|
-
'dark:bg-neutral-50 dark:text-neutral-900',
|
|
9
18
|
'shadow-md',
|
|
10
19
|
'data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95',
|
|
11
20
|
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
12
21
|
'motion-reduce:transition-none motion-reduce:duration-0',
|
|
13
22
|
],
|
|
14
|
-
|
|
23
|
+
// Arrow tracks the content bg — auto-inverts the same way.
|
|
24
|
+
arrow: 'fill-neutral-900',
|
|
15
25
|
},
|
|
16
26
|
});
|
|
17
27
|
|
|
@@ -219,8 +219,10 @@ describe('<Typography>', () => {
|
|
|
219
219
|
const { container } = render(<Typography color={color}>x</Typography>);
|
|
220
220
|
const cls = container.firstElementChild?.className ?? '';
|
|
221
221
|
if (color === 'muted') {
|
|
222
|
+
// Sprint 4.3 identity sweep: neutral palette auto-inverts via
|
|
223
|
+
// CSS-var swap — no `dark:` variant needed.
|
|
222
224
|
expect(cls).toContain('text-neutral-600');
|
|
223
|
-
expect(cls).toContain('dark:text-neutral-
|
|
225
|
+
expect(cls).not.toContain('dark:text-neutral-');
|
|
224
226
|
} else {
|
|
225
227
|
expect(cls).toContain(`text-${color}-700`);
|
|
226
228
|
expect(cls).toContain(`dark:text-${color}-400`);
|
|
@@ -62,8 +62,12 @@ export const typographyVariants = tv({
|
|
|
62
62
|
warning: 'text-warning-700 dark:text-warning-400',
|
|
63
63
|
danger: 'text-danger-700 dark:text-danger-400',
|
|
64
64
|
info: 'text-info-700 dark:text-info-400',
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Neutral palette auto-inverts via the dashforgePreset() CSS-var
|
|
66
|
+
// swap — no `dark:` variants. `text-neutral-900` resolves to
|
|
67
|
+
// dark text in light mode and light text in dark mode; same
|
|
68
|
+
// applies to `text-neutral-600` (muted).
|
|
69
|
+
neutral: 'text-neutral-900',
|
|
70
|
+
muted: 'text-neutral-600',
|
|
67
71
|
},
|
|
68
72
|
|
|
69
73
|
/*
|