@dashforge/tw 0.6.0-beta → 0.8.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 +451 -0
- package/THEME-AUDIT.md +175 -0
- package/dist/index.esm.js +2009 -87
- 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 +214 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +222 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.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.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +77 -5
- 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/_shared/data/useTableFilter.d.ts +36 -0
- 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 +6 -3
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +5 -5
- 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 +917 -0
- package/src/components/DataGrid/DataGrid.tsx +1199 -0
- package/src/components/DataGrid/dataGrid.types.ts +238 -0
- package/src/components/DataGrid/dataGrid.variants.ts +152 -0
- package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
- package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
- package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
- package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
- package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -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 +22 -8
- package/src/components/Table/table.types.ts +83 -5
- 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/_shared/data/useTableFilter.test.ts +166 -0
- package/src/components/_shared/data/useTableFilter.ts +118 -0
- 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 +20 -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 +0 -13
- 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/src/components/Table/_internal/useTableFilter.ts +0 -37
- /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,106 @@
|
|
|
1
|
+
import { useCallback, useRef, type PointerEvent as RPointerEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native column-resize behavior for DataGrid (and Table later if
|
|
5
|
+
* promoted to `_shared`).
|
|
6
|
+
*
|
|
7
|
+
* The returned `startResize` is wired to a `pointerdown` handler on a
|
|
8
|
+
* thin grabber positioned at the right edge of a `<th>`. We capture
|
|
9
|
+
* the pointer on the target element so `pointermove` / `pointerup`
|
|
10
|
+
* keep flowing even if the cursor leaves the grabber.
|
|
11
|
+
*
|
|
12
|
+
* Why native pointer events vs a drag library:
|
|
13
|
+
* - Zero new runtime deps (sprint constraint)
|
|
14
|
+
* - Pointer events normalize mouse/touch/pen out of the box
|
|
15
|
+
* - Pointer capture means we don't have to track the global
|
|
16
|
+
* `mousemove` / `mouseup` — the browser routes events to our
|
|
17
|
+
* listener until release.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface UseColumnResizeArgs {
|
|
21
|
+
/** Current column widths (Record<field, px>). */
|
|
22
|
+
widths: Record<string, number>;
|
|
23
|
+
/** Commit a new width map. */
|
|
24
|
+
onChange: (next: Record<string, number>) => void;
|
|
25
|
+
/** Hard floor for any column (px). @default 40 */
|
|
26
|
+
minWidth?: number;
|
|
27
|
+
/** Hard ceiling for any column (px). @default 1200 */
|
|
28
|
+
maxWidth?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ColumnResizeHandlers {
|
|
32
|
+
startResize: (
|
|
33
|
+
field: string,
|
|
34
|
+
startWidth: number,
|
|
35
|
+
clampMin: number,
|
|
36
|
+
clampMax: number,
|
|
37
|
+
) => (e: RPointerEvent<HTMLElement>) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useColumnResize(args: UseColumnResizeArgs): ColumnResizeHandlers {
|
|
41
|
+
const { widths, onChange, minWidth = 40, maxWidth = 1200 } = args;
|
|
42
|
+
|
|
43
|
+
// We need the LATEST widths inside the pointermove closure. Stick
|
|
44
|
+
// them in a ref so the closure captured at pointerdown stays valid
|
|
45
|
+
// for the duration of the drag.
|
|
46
|
+
const widthsRef = useRef(widths);
|
|
47
|
+
widthsRef.current = widths;
|
|
48
|
+
|
|
49
|
+
const startResize = useCallback(
|
|
50
|
+
(
|
|
51
|
+
field: string,
|
|
52
|
+
startWidth: number,
|
|
53
|
+
clampMin: number,
|
|
54
|
+
clampMax: number,
|
|
55
|
+
) => {
|
|
56
|
+
return (e: RPointerEvent<HTMLElement>) => {
|
|
57
|
+
// Only respond to the primary mouse button (or touch / pen).
|
|
58
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
59
|
+
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
|
|
63
|
+
const startX = e.clientX;
|
|
64
|
+
const target = e.currentTarget;
|
|
65
|
+
try {
|
|
66
|
+
target.setPointerCapture(e.pointerId);
|
|
67
|
+
} catch {
|
|
68
|
+
// jsdom or older browsers — fine to fall back to global listeners.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const effectiveMin = Math.max(minWidth, clampMin);
|
|
72
|
+
const effectiveMax = Math.min(maxWidth, clampMax);
|
|
73
|
+
|
|
74
|
+
const handleMove = (ev: PointerEvent) => {
|
|
75
|
+
const delta = ev.clientX - startX;
|
|
76
|
+
const next = clamp(startWidth + delta, effectiveMin, effectiveMax);
|
|
77
|
+
onChange({ ...widthsRef.current, [field]: next });
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleEnd = (ev: PointerEvent) => {
|
|
81
|
+
try {
|
|
82
|
+
target.releasePointerCapture(ev.pointerId);
|
|
83
|
+
} catch {
|
|
84
|
+
/* noop */
|
|
85
|
+
}
|
|
86
|
+
target.removeEventListener('pointermove', handleMove);
|
|
87
|
+
target.removeEventListener('pointerup', handleEnd);
|
|
88
|
+
target.removeEventListener('pointercancel', handleEnd);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
target.addEventListener('pointermove', handleMove);
|
|
92
|
+
target.addEventListener('pointerup', handleEnd);
|
|
93
|
+
target.addEventListener('pointercancel', handleEnd);
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
[onChange, minWidth, maxWidth],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return { startResize };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function clamp(n: number, min: number, max: number): number {
|
|
103
|
+
if (n < min) return min;
|
|
104
|
+
if (n > max) return max;
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useState, useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { Dialog } from '../../Dialog/Dialog.js';
|
|
3
|
+
import { cn } from '../../../utils/cn.js';
|
|
4
|
+
import type { TableColumn } from '../../Table/table.types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Column visibility dialog for DataGrid.
|
|
8
|
+
*
|
|
9
|
+
* Renders a toolbar button (the trigger) that opens a `<Dialog>` with
|
|
10
|
+
* one checkbox per `hideable` column. The consumer can:
|
|
11
|
+
* - Show/hide individual columns
|
|
12
|
+
* - "Show all" / "Hide all" shortcuts
|
|
13
|
+
* - Persist visibility via the controllable `hiddenColumns` model
|
|
14
|
+
*
|
|
15
|
+
* Columns with `hideable: false` are NEVER shown in the dialog —
|
|
16
|
+
* those are considered structurally required (e.g. an ID column).
|
|
17
|
+
*
|
|
18
|
+
* RBAC-hidden columns (`access.onUnauthorized === 'hide'`) are also
|
|
19
|
+
* excluded — they're already removed from `visibleCols` upstream.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ColumnVisibilityLabels {
|
|
23
|
+
columnsButton?: string;
|
|
24
|
+
columnsTitle?: string;
|
|
25
|
+
columnsDescription?: string;
|
|
26
|
+
columnsShowAll?: string;
|
|
27
|
+
columnsHideAll?: string;
|
|
28
|
+
columnsDone?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_VISIBILITY_LABELS: Required<ColumnVisibilityLabels> = {
|
|
32
|
+
columnsButton: 'Columns',
|
|
33
|
+
columnsTitle: 'Manage columns',
|
|
34
|
+
columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
|
|
35
|
+
columnsShowAll: 'Show all',
|
|
36
|
+
columnsHideAll: 'Hide all',
|
|
37
|
+
columnsDone: 'Done',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface ColumnVisibilityTriggerProps<T extends object> {
|
|
41
|
+
cols: TableColumn<T>[];
|
|
42
|
+
hiddenColumns: string[];
|
|
43
|
+
onChange: (next: string[]) => void;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
labels?: ColumnVisibilityLabels;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ColumnVisibilityTrigger<T extends object>(
|
|
49
|
+
props: ColumnVisibilityTriggerProps<T>,
|
|
50
|
+
) {
|
|
51
|
+
const { cols, hiddenColumns, onChange, disabled, labels: labelsProp } = props;
|
|
52
|
+
const labels = { ...DEFAULT_VISIBILITY_LABELS, ...labelsProp };
|
|
53
|
+
const [open, setOpen] = useState(false);
|
|
54
|
+
|
|
55
|
+
// Hideable cols are the only ones the user can toggle. `hideable`
|
|
56
|
+
// defaults to `true` — a column is hideable unless explicitly opted
|
|
57
|
+
// out with `hideable: false` (structurally required column).
|
|
58
|
+
const hideableCols = cols.filter((c) => c.hideable !== false);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setOpen(true)}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
className={cn(
|
|
67
|
+
'inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md',
|
|
68
|
+
'text-sm text-neutral-700 hover:text-neutral-900',
|
|
69
|
+
'border border-neutral-300 bg-neutral-50 hover:bg-neutral-100',
|
|
70
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
71
|
+
'transition-colors motion-reduce:transition-none',
|
|
72
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<ColumnsIcon />
|
|
76
|
+
<span>{labels.columnsButton}</span>
|
|
77
|
+
</button>
|
|
78
|
+
<Dialog
|
|
79
|
+
open={open}
|
|
80
|
+
onOpenChange={setOpen}
|
|
81
|
+
title={labels.columnsTitle}
|
|
82
|
+
description={labels.columnsDescription}
|
|
83
|
+
size="sm"
|
|
84
|
+
>
|
|
85
|
+
<ColumnVisibilityBody
|
|
86
|
+
cols={hideableCols}
|
|
87
|
+
hiddenColumns={hiddenColumns}
|
|
88
|
+
onChange={onChange}
|
|
89
|
+
onDone={() => setOpen(false)}
|
|
90
|
+
labels={labels}
|
|
91
|
+
/>
|
|
92
|
+
</Dialog>
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ───── Dialog body ─────
|
|
98
|
+
|
|
99
|
+
function ColumnVisibilityBody<T extends object>(props: {
|
|
100
|
+
cols: TableColumn<T>[];
|
|
101
|
+
hiddenColumns: string[];
|
|
102
|
+
onChange: (next: string[]) => void;
|
|
103
|
+
onDone: () => void;
|
|
104
|
+
labels: Required<ColumnVisibilityLabels>;
|
|
105
|
+
}) {
|
|
106
|
+
const { cols, hiddenColumns, onChange, onDone, labels } = props;
|
|
107
|
+
// Local draft so the user can stage changes; commit on Done.
|
|
108
|
+
const [draft, setDraft] = useState<string[]>(hiddenColumns);
|
|
109
|
+
|
|
110
|
+
// Re-sync when the dialog re-opens with a different external state.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
setDraft(hiddenColumns);
|
|
113
|
+
}, [hiddenColumns]);
|
|
114
|
+
|
|
115
|
+
const toggle = (field: string) => {
|
|
116
|
+
setDraft((prev) =>
|
|
117
|
+
prev.includes(field) ? prev.filter((f) => f !== field) : [...prev, field],
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const showAll = () => setDraft([]);
|
|
122
|
+
const hideAll = () => setDraft(cols.map((c) => c.field as string));
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex flex-col gap-3">
|
|
126
|
+
<div className="flex items-center gap-2 pb-2 border-b border-neutral-200">
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onClick={showAll}
|
|
130
|
+
className="text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline"
|
|
131
|
+
>
|
|
132
|
+
{labels.columnsShowAll}
|
|
133
|
+
</button>
|
|
134
|
+
<span className="text-neutral-300">·</span>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={hideAll}
|
|
138
|
+
className="text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline"
|
|
139
|
+
>
|
|
140
|
+
{labels.columnsHideAll}
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
<ul className="flex flex-col gap-1.5 max-h-72 overflow-auto">
|
|
144
|
+
{cols.map((col) => {
|
|
145
|
+
const field = col.field as string;
|
|
146
|
+
const hidden = draft.includes(field);
|
|
147
|
+
const header =
|
|
148
|
+
typeof col.header === 'function' ? col.header() : col.header;
|
|
149
|
+
return (
|
|
150
|
+
<li key={field}>
|
|
151
|
+
<label className="flex items-center gap-2 cursor-pointer py-1 px-1 rounded hover:bg-neutral-100">
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
checked={!hidden}
|
|
155
|
+
onChange={() => toggle(field)}
|
|
156
|
+
className="cursor-pointer"
|
|
157
|
+
/>
|
|
158
|
+
<span className="text-sm text-neutral-900">{header}</span>
|
|
159
|
+
</label>
|
|
160
|
+
</li>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</ul>
|
|
164
|
+
<div className="flex justify-end gap-2 pt-2 border-t border-neutral-200">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={() => {
|
|
168
|
+
onChange(draft);
|
|
169
|
+
onDone();
|
|
170
|
+
}}
|
|
171
|
+
className="text-sm bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 px-3 py-1.5"
|
|
172
|
+
>
|
|
173
|
+
{labels.columnsDone}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ───── Icon ─────
|
|
181
|
+
|
|
182
|
+
function ColumnsIcon(): ReactNode {
|
|
183
|
+
return (
|
|
184
|
+
<svg
|
|
185
|
+
width="0.875em"
|
|
186
|
+
height="0.875em"
|
|
187
|
+
viewBox="0 0 16 16"
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
strokeWidth="1.5"
|
|
191
|
+
strokeLinecap="round"
|
|
192
|
+
strokeLinejoin="round"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
>
|
|
195
|
+
<rect x="2" y="3" width="3.5" height="10" rx="0.5" />
|
|
196
|
+
<rect x="6.25" y="3" width="3.5" height="10" rx="0.5" />
|
|
197
|
+
<rect x="10.5" y="3" width="3.5" height="10" rx="0.5" />
|
|
198
|
+
</svg>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -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,
|
|
@@ -40,10 +40,24 @@ const DEFAULT_LABELS: Required<TableLabels> = {
|
|
|
40
40
|
filterColumn: 'Filter',
|
|
41
41
|
filterApply: 'Apply',
|
|
42
42
|
filterClear: 'Clear',
|
|
43
|
+
filterMin: 'Min',
|
|
44
|
+
filterMax: 'Max',
|
|
45
|
+
filterFrom: 'From',
|
|
46
|
+
filterTo: 'To',
|
|
47
|
+
filterAll: 'All',
|
|
48
|
+
filterTrue: 'True',
|
|
49
|
+
filterFalse: 'False',
|
|
43
50
|
density: 'Density',
|
|
44
51
|
densityCompact: 'Compact',
|
|
45
52
|
densityComfortable: 'Comfortable',
|
|
46
53
|
densitySpacious: 'Spacious',
|
|
54
|
+
columnsButton: 'Columns',
|
|
55
|
+
columnsTitle: 'Manage columns',
|
|
56
|
+
columnsDescription:
|
|
57
|
+
'Show or hide columns. Required columns cannot be toggled.',
|
|
58
|
+
columnsShowAll: 'Show all',
|
|
59
|
+
columnsHideAll: 'Hide all',
|
|
60
|
+
columnsDone: 'Done',
|
|
47
61
|
};
|
|
48
62
|
|
|
49
63
|
const DEFAULT_GET_ROW_ID = <T extends object>(_row: T, index: number) => String(index);
|
|
@@ -131,11 +131,20 @@ export interface TableColumn<T extends object> {
|
|
|
131
131
|
searchable?: boolean;
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Enable per-column quick filter
|
|
134
|
+
* Enable per-column quick filter UI in the column header.
|
|
135
135
|
* @default false
|
|
136
136
|
*/
|
|
137
137
|
filterable?: boolean;
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Override the filter UI type. By default the type is auto-detected
|
|
141
|
+
* from `useColumnAutoDetect` (number → range, date → range,
|
|
142
|
+
* boolean → select, anything else → text contains). Force a
|
|
143
|
+
* specific UI when the autodetect picks the wrong one — e.g. an
|
|
144
|
+
* ID column stored as a number that should filter by text contains.
|
|
145
|
+
*/
|
|
146
|
+
filterType?: TableFilterType;
|
|
147
|
+
|
|
139
148
|
/**
|
|
140
149
|
* Custom cell renderer. Receives the full row + the resolved
|
|
141
150
|
* (potentially nested) value, the row index, selection state,
|
|
@@ -151,9 +160,41 @@ export interface TableColumn<T extends object> {
|
|
|
151
160
|
*/
|
|
152
161
|
access?: AccessRequirement;
|
|
153
162
|
|
|
154
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Pin this column during horizontal scroll. `'left'` (DataGrid v1)
|
|
165
|
+
* uses CSS `position: sticky; left: 0`. `'right'` is reserved for
|
|
166
|
+
* v1-bis (CSS edge cases around last-column borders). Table
|
|
167
|
+
* (non-virtualized) ignores this prop today — only DataGrid honors
|
|
168
|
+
* it (Sprint 4.2).
|
|
169
|
+
*/
|
|
170
|
+
sticky?: 'left' | 'right';
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Allow user to hide this column from the column-visibility dialog.
|
|
174
|
+
* Pass `false` for structurally required columns (e.g. an ID column).
|
|
175
|
+
* @default true
|
|
176
|
+
*/
|
|
155
177
|
hideable?: boolean;
|
|
178
|
+
/** Hidden on first render (the user can re-show via the dialog). */
|
|
156
179
|
defaultHidden?: boolean;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Allow user to resize this column by dragging the right edge of
|
|
183
|
+
* its header. Pass `false` to opt out per-column. The grid-level
|
|
184
|
+
* `enableColumnResize` flag must also be on (default `true`).
|
|
185
|
+
* @default true
|
|
186
|
+
*/
|
|
187
|
+
resizable?: boolean;
|
|
188
|
+
/** Maximum width (px) honored by the resize drag. */
|
|
189
|
+
maxWidth?: number;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Allow user to reorder this column by dragging its header. Pass
|
|
193
|
+
* `false` to pin its position. The grid-level `enableColumnReorder`
|
|
194
|
+
* flag must also be on (default `true`).
|
|
195
|
+
* @default true
|
|
196
|
+
*/
|
|
197
|
+
reorderable?: boolean;
|
|
157
198
|
}
|
|
158
199
|
|
|
159
200
|
// ───── State models ─────
|
|
@@ -172,15 +213,36 @@ export interface TableSortItem {
|
|
|
172
213
|
*/
|
|
173
214
|
export type TableSortModel = TableSortItem[];
|
|
174
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Filter operator. The value shape depends on the operator:
|
|
218
|
+
* - `contains` → `value: string` (text substring match)
|
|
219
|
+
* - `equals` → `value: unknown` (exact match, used for booleans)
|
|
220
|
+
* - `between` → `value: [min, max]` where each end can be `null`
|
|
221
|
+
* for an open range. Used for number ranges (`value: [10, 50]`)
|
|
222
|
+
* and date ranges (`value: ['2024-01-01', '2024-12-31']` as ISO).
|
|
223
|
+
*/
|
|
224
|
+
export type TableFilterOperator = 'contains' | 'equals' | 'between';
|
|
225
|
+
|
|
175
226
|
export interface TableFilterItem {
|
|
176
227
|
field: string;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
value: string;
|
|
228
|
+
op: TableFilterOperator;
|
|
229
|
+
value: unknown;
|
|
180
230
|
}
|
|
181
231
|
|
|
182
232
|
export type TableFilterModel = TableFilterItem[];
|
|
183
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Column-level filter type hint. Determines which filter UI to
|
|
236
|
+
* render in the column header Popover. Auto-detected from
|
|
237
|
+
* `useColumnAutoDetect` when omitted:
|
|
238
|
+
*
|
|
239
|
+
* - `number` → number range filter
|
|
240
|
+
* - `date` → date range filter
|
|
241
|
+
* - `boolean` → true/false/all select
|
|
242
|
+
* - `string` (or anything else) → text contains
|
|
243
|
+
*/
|
|
244
|
+
export type TableFilterType = 'text' | 'number' | 'boolean' | 'date';
|
|
245
|
+
|
|
184
246
|
export type TableRowSelectionMode = 'none' | 'single' | 'multiple';
|
|
185
247
|
|
|
186
248
|
// ───── i18n labels ─────
|
|
@@ -212,11 +274,27 @@ export interface TableLabels {
|
|
|
212
274
|
filterColumn?: string;
|
|
213
275
|
filterApply?: string;
|
|
214
276
|
filterClear?: string;
|
|
277
|
+
/** Number / date range filter inputs. */
|
|
278
|
+
filterMin?: string;
|
|
279
|
+
filterMax?: string;
|
|
280
|
+
filterFrom?: string;
|
|
281
|
+
filterTo?: string;
|
|
282
|
+
/** Boolean filter radio options. */
|
|
283
|
+
filterAll?: string;
|
|
284
|
+
filterTrue?: string;
|
|
285
|
+
filterFalse?: string;
|
|
215
286
|
/** Density toggle. */
|
|
216
287
|
density?: string;
|
|
217
288
|
densityCompact?: string;
|
|
218
289
|
densityComfortable?: string;
|
|
219
290
|
densitySpacious?: string;
|
|
291
|
+
/** Column visibility dialog. */
|
|
292
|
+
columnsButton?: string;
|
|
293
|
+
columnsTitle?: string;
|
|
294
|
+
columnsDescription?: string;
|
|
295
|
+
columnsShowAll?: string;
|
|
296
|
+
columnsHideAll?: string;
|
|
297
|
+
columnsDone?: string;
|
|
220
298
|
}
|
|
221
299
|
|
|
222
300
|
// ───── Slot props ─────
|