@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
|
@@ -35,6 +35,19 @@ export interface ColumnResizeHandlers {
|
|
|
35
35
|
clampMin: number,
|
|
36
36
|
clampMax: number,
|
|
37
37
|
) => (e: RPointerEvent<HTMLElement>) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Commit a single clamped width delta — the keyboard-resize path.
|
|
40
|
+
* Wired to ArrowLeft/ArrowRight on the focused resize handle so the
|
|
41
|
+
* feature is reachable without a pointer (WCAG 2.1 — keyboard
|
|
42
|
+
* operability). Same clamping as the drag path.
|
|
43
|
+
*/
|
|
44
|
+
nudge: (
|
|
45
|
+
field: string,
|
|
46
|
+
currentWidth: number,
|
|
47
|
+
delta: number,
|
|
48
|
+
clampMin: number,
|
|
49
|
+
clampMax: number,
|
|
50
|
+
) => void;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
export function useColumnResize(args: UseColumnResizeArgs): ColumnResizeHandlers {
|
|
@@ -96,7 +109,23 @@ export function useColumnResize(args: UseColumnResizeArgs): ColumnResizeHandlers
|
|
|
96
109
|
[onChange, minWidth, maxWidth],
|
|
97
110
|
);
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
const nudge = useCallback(
|
|
113
|
+
(
|
|
114
|
+
field: string,
|
|
115
|
+
currentWidth: number,
|
|
116
|
+
delta: number,
|
|
117
|
+
clampMin: number,
|
|
118
|
+
clampMax: number,
|
|
119
|
+
) => {
|
|
120
|
+
const effectiveMin = Math.max(minWidth, clampMin);
|
|
121
|
+
const effectiveMax = Math.min(maxWidth, clampMax);
|
|
122
|
+
const next = clamp(currentWidth + delta, effectiveMin, effectiveMax);
|
|
123
|
+
onChange({ ...widthsRef.current, [field]: next });
|
|
124
|
+
},
|
|
125
|
+
[onChange, minWidth, maxWidth],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return { startResize, nudge };
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
function clamp(n: number, min: number, max: number): number {
|
|
@@ -31,15 +31,18 @@ import type { TableColumn } from '../../Table/table.types.js';
|
|
|
31
31
|
export interface ColumnVisibilityLabels {
|
|
32
32
|
columnsButton?: string;
|
|
33
33
|
columnsTitle?: string;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Accessible label for the tri-state master checkbox that
|
|
36
|
+
* shows/hides every hideable column at once. Not shown as visible
|
|
37
|
+
* text — surfaced to assistive tech via `aria-label`.
|
|
38
|
+
*/
|
|
39
|
+
columnsToggleAll?: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const DEFAULT_VISIBILITY_LABELS: Required<ColumnVisibilityLabels> = {
|
|
39
43
|
columnsButton: 'Columns',
|
|
40
44
|
columnsTitle: 'Toggle columns',
|
|
41
|
-
|
|
42
|
-
columnsHideAll: 'Hide all',
|
|
45
|
+
columnsToggleAll: 'Toggle all columns',
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
export interface ColumnVisibilityTriggerProps<T extends object> {
|
|
@@ -70,8 +73,21 @@ export function ColumnVisibilityTrigger<T extends object>(
|
|
|
70
73
|
);
|
|
71
74
|
};
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
// Tri-state master: checked = every hideable column visible,
|
|
77
|
+
// unchecked = every one hidden, indeterminate = mixed.
|
|
78
|
+
const allVisible = hideableCols.every(
|
|
79
|
+
(c) => !hiddenColumns.includes(c.field as string),
|
|
80
|
+
);
|
|
81
|
+
const allHidden =
|
|
82
|
+
hideableCols.length > 0 &&
|
|
83
|
+
hideableCols.every((c) => hiddenColumns.includes(c.field as string));
|
|
84
|
+
const someHidden = !allVisible && !allHidden;
|
|
85
|
+
|
|
86
|
+
// Standard select-all semantics: when everything is visible the
|
|
87
|
+
// master hides all; otherwise it shows all.
|
|
88
|
+
const toggleAll = () => {
|
|
89
|
+
onChange(allVisible ? hideableCols.map((c) => c.field as string) : []);
|
|
90
|
+
};
|
|
75
91
|
|
|
76
92
|
return (
|
|
77
93
|
<Popover
|
|
@@ -80,28 +96,29 @@ export function ColumnVisibilityTrigger<T extends object>(
|
|
|
80
96
|
side="bottom"
|
|
81
97
|
align="end"
|
|
82
98
|
content={
|
|
83
|
-
<div className="flex flex-col gap-
|
|
84
|
-
|
|
99
|
+
<div className="flex flex-col gap-1 min-w-[220px]">
|
|
100
|
+
{/*
|
|
101
|
+
Header row — the tri-state master checkbox + the title.
|
|
102
|
+
The master replaces the old "Show all · Hide all" text
|
|
103
|
+
links: one control, no extra copy, consistent with the
|
|
104
|
+
DataGrid row select-all checkbox. The title is a heading
|
|
105
|
+
(not part of the checkbox label) and stays consumer-
|
|
106
|
+
customizable via `labels.columnsTitle`.
|
|
107
|
+
*/}
|
|
108
|
+
<div className="flex items-center gap-2 px-1 pb-2 mb-1 border-b border-neutral-200">
|
|
109
|
+
<input
|
|
110
|
+
type="checkbox"
|
|
111
|
+
aria-label={labels.columnsToggleAll}
|
|
112
|
+
checked={allVisible}
|
|
113
|
+
ref={(el) => {
|
|
114
|
+
if (el) el.indeterminate = someHidden;
|
|
115
|
+
}}
|
|
116
|
+
onChange={toggleAll}
|
|
117
|
+
className="cursor-pointer"
|
|
118
|
+
/>
|
|
85
119
|
<span className="text-xs font-semibold text-neutral-700 uppercase tracking-wide">
|
|
86
120
|
{labels.columnsTitle}
|
|
87
121
|
</span>
|
|
88
|
-
<div className="flex items-center gap-2">
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={showAll}
|
|
92
|
-
className="text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline"
|
|
93
|
-
>
|
|
94
|
-
{labels.columnsShowAll}
|
|
95
|
-
</button>
|
|
96
|
-
<span className="text-neutral-300" aria-hidden="true">·</span>
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={hideAll}
|
|
100
|
-
className="text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline"
|
|
101
|
-
>
|
|
102
|
-
{labels.columnsHideAll}
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
122
|
</div>
|
|
106
123
|
<ul className="flex flex-col gap-0.5 max-h-72 overflow-auto">
|
|
107
124
|
{hideableCols.map((col) => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
|
|
5
|
+
import { DatePicker } from './DatePicker.js';
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
// Radix Popover (Floating UI) needs ResizeObserver — jsdom lacks it.
|
|
9
|
+
if (!('ResizeObserver' in globalThis)) {
|
|
10
|
+
class ResizeObserverStub {
|
|
11
|
+
observe(): void {
|
|
12
|
+
/* no-op */
|
|
13
|
+
}
|
|
14
|
+
unobserve(): void {
|
|
15
|
+
/* no-op */
|
|
16
|
+
}
|
|
17
|
+
disconnect(): void {
|
|
18
|
+
/* no-op */
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
(globalThis as { ResizeObserver?: unknown }).ResizeObserver =
|
|
22
|
+
ResizeObserverStub;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('DatePicker', () => {
|
|
27
|
+
it('renders a labelled trigger with the placeholder', () => {
|
|
28
|
+
render(
|
|
29
|
+
<DatePicker name="birthday" label="Birthday" placeholder="Pick one" />,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.queryByText('Birthday')).not.toBeNull();
|
|
32
|
+
expect(screen.getByRole('button').textContent).toContain('Pick one');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows a localized controlled value', () => {
|
|
36
|
+
render(<DatePicker name="d" value="2026-05-20" />);
|
|
37
|
+
expect(screen.getByRole('button').textContent).toContain('May 20, 2026');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('opens and toggles the calendar popover from the trigger', () => {
|
|
41
|
+
render(<DatePicker name="d" defaultValue="2026-05-20" />);
|
|
42
|
+
const trigger = screen.getByRole('button');
|
|
43
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
44
|
+
fireEvent.click(trigger);
|
|
45
|
+
expect(screen.queryByRole('dialog')).not.toBeNull();
|
|
46
|
+
fireEvent.click(trigger);
|
|
47
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('selects a date from the popover, updates the trigger and closes', () => {
|
|
51
|
+
const onChange = vi.fn();
|
|
52
|
+
render(
|
|
53
|
+
<DatePicker name="d" defaultValue="2026-05-20" onChange={onChange} />,
|
|
54
|
+
);
|
|
55
|
+
fireEvent.click(screen.getByRole('button'));
|
|
56
|
+
fireEvent.click(screen.getByRole('button', { name: /May 15, 2026/ }));
|
|
57
|
+
expect(onChange).toHaveBeenCalledWith('2026-05-15');
|
|
58
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
59
|
+
expect(screen.getByRole('button').textContent).toContain('May 15, 2026');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('keeps a controlled value fixed but still reports the change', () => {
|
|
63
|
+
const onChange = vi.fn();
|
|
64
|
+
render(<DatePicker name="d" value="2026-05-20" onChange={onChange} />);
|
|
65
|
+
fireEvent.click(screen.getByRole('button'));
|
|
66
|
+
fireEvent.click(screen.getByRole('button', { name: /May 15, 2026/ }));
|
|
67
|
+
expect(onChange).toHaveBeenCalledWith('2026-05-15');
|
|
68
|
+
expect(screen.getByRole('button').textContent).toContain('May 20, 2026');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does not open the popover when disabled', () => {
|
|
72
|
+
render(<DatePicker name="d" disabled />);
|
|
73
|
+
fireEvent.click(screen.getByRole('button'));
|
|
74
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
|
|
3
|
+
import type { DashFormBridge, FieldRegistration } from '@dashforge/ui-core';
|
|
4
|
+
import { useDashFieldMeta } from '@dashforge/forms';
|
|
5
|
+
import { parseISODate } from '@dashforge/calendar-core';
|
|
6
|
+
import type { ISODate } from '@dashforge/calendar-core';
|
|
7
|
+
import { cn } from '../../utils/cn.js';
|
|
8
|
+
import { useAccessState } from '../../hooks/useAccessState.js';
|
|
9
|
+
import { resolveValidationState } from '../_shared/resolveValidationState.js';
|
|
10
|
+
import { Popover } from '../Popover/Popover.js';
|
|
11
|
+
import { Calendar } from '../Calendar/Calendar.js';
|
|
12
|
+
import { datePickerVariants } from './datePicker.variants.js';
|
|
13
|
+
import type { DatePickerProps } from './datePicker.types.js';
|
|
14
|
+
|
|
15
|
+
// Inline 16×16 stroke calendar glyph — no icon dependency (tw convention).
|
|
16
|
+
function CalendarIcon() {
|
|
17
|
+
return (
|
|
18
|
+
<svg
|
|
19
|
+
width="1em"
|
|
20
|
+
height="1em"
|
|
21
|
+
viewBox="0 0 16 16"
|
|
22
|
+
fill="none"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
>
|
|
25
|
+
<rect
|
|
26
|
+
x="2.5"
|
|
27
|
+
y="3.5"
|
|
28
|
+
width="11"
|
|
29
|
+
height="10"
|
|
30
|
+
rx="1.5"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="1.5"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
d="M2.5 6.5h11M5.5 2v3M10.5 2v3"
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="1.5"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Formats a stored ISO date for the trigger display, localized to a medium
|
|
46
|
+
* date style (e.g. `"May 20, 2026"`). Formatting is done in UTC so the
|
|
47
|
+
* displayed day always matches the stored `YYYY-MM-DD`.
|
|
48
|
+
*/
|
|
49
|
+
function formatDisplayDate(
|
|
50
|
+
iso: ISODate | null | undefined,
|
|
51
|
+
locale: string,
|
|
52
|
+
): string {
|
|
53
|
+
if (iso == null || iso === '') {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
const parts = parseISODate(iso);
|
|
57
|
+
if (parts === null) {
|
|
58
|
+
return iso;
|
|
59
|
+
}
|
|
60
|
+
return new Intl.DateTimeFormat(locale, {
|
|
61
|
+
dateStyle: 'medium',
|
|
62
|
+
timeZone: 'UTC',
|
|
63
|
+
}).format(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dashforge TW `<DatePicker>` — a form-bound single-date field.
|
|
68
|
+
*
|
|
69
|
+
* A read-only trigger button paired with a `<Calendar>` popover (Radix
|
|
70
|
+
* Popover — toggle / Escape / outside-click / focus-trap are Radix-managed).
|
|
71
|
+
* Integrates with the Dashforge form bridge + RBAC, mirroring the other
|
|
72
|
+
* `@dashforge/tw` form fields.
|
|
73
|
+
*
|
|
74
|
+
* Storage contract: an ISO `YYYY-MM-DD` string, or `null` — shared with the
|
|
75
|
+
* MUI `@dashforge/ui` `DatePicker`.
|
|
76
|
+
*/
|
|
77
|
+
export function DatePicker(props: DatePickerProps) {
|
|
78
|
+
const {
|
|
79
|
+
name,
|
|
80
|
+
rules,
|
|
81
|
+
label,
|
|
82
|
+
helperText,
|
|
83
|
+
error,
|
|
84
|
+
required,
|
|
85
|
+
disabled,
|
|
86
|
+
placeholder,
|
|
87
|
+
layout = 'stacked',
|
|
88
|
+
visibleWhen,
|
|
89
|
+
access,
|
|
90
|
+
value: explicitValue,
|
|
91
|
+
defaultValue,
|
|
92
|
+
onChange,
|
|
93
|
+
minDate,
|
|
94
|
+
maxDate,
|
|
95
|
+
disabledDates,
|
|
96
|
+
isDateDisabled,
|
|
97
|
+
weekStartDay,
|
|
98
|
+
locale,
|
|
99
|
+
fullWidth,
|
|
100
|
+
sx,
|
|
101
|
+
slotProps,
|
|
102
|
+
testId,
|
|
103
|
+
} = props;
|
|
104
|
+
|
|
105
|
+
// ───── Hooks (unconditional, before any early return) ─────
|
|
106
|
+
const bridge = useContext(DashFormContext) as DashFormBridge | null;
|
|
107
|
+
const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
|
|
108
|
+
const fieldMeta = useDashFieldMeta(name);
|
|
109
|
+
const accessState = useAccessState(access);
|
|
110
|
+
|
|
111
|
+
const fieldId = useId();
|
|
112
|
+
const helperId = `${fieldId}-help`;
|
|
113
|
+
|
|
114
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
115
|
+
const [internalValue, setInternalValue] = useState<ISODate | null>(
|
|
116
|
+
defaultValue ?? null,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// StrictMode-safe unregister-on-unmount.
|
|
120
|
+
const unregisterRef = useRef({ bridge, name });
|
|
121
|
+
unregisterRef.current = { bridge, name };
|
|
122
|
+
const isMountedRef = useRef(false);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
isMountedRef.current = true;
|
|
125
|
+
return () => {
|
|
126
|
+
isMountedRef.current = false;
|
|
127
|
+
const { bridge: cap, name: capName } = unregisterRef.current;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (!isMountedRef.current) cap?.unregister?.(capName);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// ───── Derived ─────
|
|
135
|
+
const effectiveDisabled = Boolean(disabled) || accessState.disabled;
|
|
136
|
+
const effectiveReadOnly = accessState.readonly;
|
|
137
|
+
const isInteractive = !effectiveDisabled && !effectiveReadOnly;
|
|
138
|
+
const isFormMode = Boolean(bridge?.register);
|
|
139
|
+
|
|
140
|
+
let resolvedError = error;
|
|
141
|
+
let resolvedHelperText: typeof helperText = helperText;
|
|
142
|
+
let registration: FieldRegistration | null = null;
|
|
143
|
+
let resolvedValue: ISODate | null;
|
|
144
|
+
|
|
145
|
+
if (isFormMode && bridge) {
|
|
146
|
+
registration = bridge.register(name, rules);
|
|
147
|
+
const validation = resolveValidationState(name, bridge, error, helperText);
|
|
148
|
+
resolvedError = validation.error;
|
|
149
|
+
resolvedHelperText = validation.helperText;
|
|
150
|
+
if (explicitValue !== undefined) {
|
|
151
|
+
resolvedValue = explicitValue;
|
|
152
|
+
} else {
|
|
153
|
+
const bv = fieldMeta.value;
|
|
154
|
+
resolvedValue = bv == null ? null : String(bv);
|
|
155
|
+
}
|
|
156
|
+
} else if (explicitValue !== undefined) {
|
|
157
|
+
resolvedValue = explicitValue;
|
|
158
|
+
} else {
|
|
159
|
+
resolvedValue = internalValue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// RHF threads its own callback ref through here in form mode.
|
|
163
|
+
const registrationRefFn = registration?.ref;
|
|
164
|
+
const triggerRef = useCallback(
|
|
165
|
+
(instance: HTMLButtonElement | null) => {
|
|
166
|
+
if (typeof registrationRefFn === 'function') {
|
|
167
|
+
registrationRefFn(instance);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
[registrationRefFn],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ───── Render-time guards (after all hooks) ─────
|
|
174
|
+
if (!isVisible) return null;
|
|
175
|
+
if (!accessState.visible) return null;
|
|
176
|
+
|
|
177
|
+
const v = datePickerVariants({
|
|
178
|
+
layout,
|
|
179
|
+
error: resolvedError,
|
|
180
|
+
fullWidth,
|
|
181
|
+
});
|
|
182
|
+
const resolvedLocale = locale ?? 'en-US';
|
|
183
|
+
const displayValue = formatDisplayDate(resolvedValue, resolvedLocale);
|
|
184
|
+
|
|
185
|
+
const commitValue = (next: ISODate | null) => {
|
|
186
|
+
if (isFormMode && bridge) {
|
|
187
|
+
bridge.setValue?.(name, next);
|
|
188
|
+
void registration?.onChange?.({
|
|
189
|
+
target: { name, value: next ?? '' },
|
|
190
|
+
type: 'change',
|
|
191
|
+
});
|
|
192
|
+
} else if (explicitValue === undefined) {
|
|
193
|
+
setInternalValue(next);
|
|
194
|
+
}
|
|
195
|
+
onChange?.(next);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Mark the field touched on popover close. Reads the freshest committed
|
|
199
|
+
// value straight from the bridge so the touched-blur never reverts a
|
|
200
|
+
// value just chosen in the calendar.
|
|
201
|
+
const markTouched = () => {
|
|
202
|
+
if (isFormMode && bridge) {
|
|
203
|
+
const committed = bridge.getValue(name);
|
|
204
|
+
void registration?.onBlur?.({
|
|
205
|
+
target: { name, value: committed == null ? '' : String(committed) },
|
|
206
|
+
type: 'blur',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleOpenChange = (next: boolean) => {
|
|
212
|
+
if (next && !isInteractive) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
setIsOpen(next);
|
|
216
|
+
if (!next) {
|
|
217
|
+
markTouched();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const handleCalendarChange = (next: ISODate) => {
|
|
222
|
+
commitValue(next);
|
|
223
|
+
setIsOpen(false);
|
|
224
|
+
markTouched();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div
|
|
229
|
+
data-testid={testId}
|
|
230
|
+
className={cn(v.root(), sx, slotProps?.root?.className)}
|
|
231
|
+
>
|
|
232
|
+
{label && (
|
|
233
|
+
<label
|
|
234
|
+
htmlFor={fieldId}
|
|
235
|
+
className={cn(v.label(), slotProps?.label?.className)}
|
|
236
|
+
>
|
|
237
|
+
{label}
|
|
238
|
+
{required && (
|
|
239
|
+
<span
|
|
240
|
+
aria-hidden="true"
|
|
241
|
+
className={cn(
|
|
242
|
+
v.requiredMark(),
|
|
243
|
+
slotProps?.requiredMark?.className,
|
|
244
|
+
)}
|
|
245
|
+
>
|
|
246
|
+
*
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
249
|
+
</label>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<Popover
|
|
253
|
+
open={isOpen}
|
|
254
|
+
onOpenChange={handleOpenChange}
|
|
255
|
+
side="bottom"
|
|
256
|
+
align="start"
|
|
257
|
+
content={
|
|
258
|
+
<Calendar
|
|
259
|
+
value={resolvedValue}
|
|
260
|
+
onChange={handleCalendarChange}
|
|
261
|
+
minDate={minDate}
|
|
262
|
+
maxDate={maxDate}
|
|
263
|
+
disabledDates={disabledDates}
|
|
264
|
+
isDateDisabled={isDateDisabled}
|
|
265
|
+
weekStartDay={weekStartDay}
|
|
266
|
+
locale={locale}
|
|
267
|
+
aria-label="Choose date"
|
|
268
|
+
sx="border-0 bg-transparent p-0"
|
|
269
|
+
/>
|
|
270
|
+
}
|
|
271
|
+
>
|
|
272
|
+
<button
|
|
273
|
+
type="button"
|
|
274
|
+
id={fieldId}
|
|
275
|
+
ref={triggerRef}
|
|
276
|
+
disabled={effectiveDisabled}
|
|
277
|
+
aria-invalid={resolvedError ? true : undefined}
|
|
278
|
+
aria-describedby={resolvedHelperText ? helperId : undefined}
|
|
279
|
+
className={cn(v.trigger(), slotProps?.trigger?.className)}
|
|
280
|
+
>
|
|
281
|
+
<span className={displayValue ? v.value() : v.placeholder()}>
|
|
282
|
+
{displayValue || placeholder || 'Select a date'}
|
|
283
|
+
</span>
|
|
284
|
+
<span className={v.icon()}>
|
|
285
|
+
<CalendarIcon />
|
|
286
|
+
</span>
|
|
287
|
+
</button>
|
|
288
|
+
</Popover>
|
|
289
|
+
|
|
290
|
+
{resolvedHelperText && (
|
|
291
|
+
<p
|
|
292
|
+
id={helperId}
|
|
293
|
+
className={cn(
|
|
294
|
+
resolvedError ? v.errorText() : v.helperText(),
|
|
295
|
+
resolvedError
|
|
296
|
+
? slotProps?.errorText?.className
|
|
297
|
+
: slotProps?.helperText?.className,
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{resolvedHelperText}
|
|
301
|
+
</p>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Engine } from '@dashforge/ui-core';
|
|
3
|
+
import type { AccessRequirement } from '@dashforge/rbac';
|
|
4
|
+
import type { ISODate, WeekDay } from '@dashforge/calendar-core';
|
|
5
|
+
|
|
6
|
+
/** Per-slot `className` overrides for `<DatePicker>`. */
|
|
7
|
+
export interface DatePickerSlotProps {
|
|
8
|
+
root?: { className?: string };
|
|
9
|
+
label?: { className?: string };
|
|
10
|
+
requiredMark?: { className?: string };
|
|
11
|
+
trigger?: { className?: string };
|
|
12
|
+
helperText?: { className?: string };
|
|
13
|
+
errorText?: { className?: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Props for the `<DatePicker>` form field (Tailwind skin).
|
|
18
|
+
*
|
|
19
|
+
* A read-only trigger button paired with a `<Calendar>` popover, integrated
|
|
20
|
+
* with the Dashforge form bridge + RBAC. The stored value is a plain ISO
|
|
21
|
+
* calendar date — `YYYY-MM-DD` — or `null`. (A pure date carries no time or
|
|
22
|
+
* timezone, which removes the whole class of DST round-trip hazards.)
|
|
23
|
+
*
|
|
24
|
+
* The prop surface mirrors the MUI `@dashforge/ui` `DatePicker` so the API
|
|
25
|
+
* is consistent across both ecosystems; only the styling escape hatches
|
|
26
|
+
* (`sx` / `slotProps`) follow the Tailwind conventions.
|
|
27
|
+
*/
|
|
28
|
+
export interface DatePickerProps {
|
|
29
|
+
/** Field name — the bridge registration key. Required. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Validation rules forwarded to the form bridge. */
|
|
32
|
+
rules?: unknown;
|
|
33
|
+
/** Field label. */
|
|
34
|
+
label?: ReactNode;
|
|
35
|
+
/** Helper text below the control (overrides a bridge error message). */
|
|
36
|
+
helperText?: ReactNode;
|
|
37
|
+
/** Explicit error state (overrides the bridge's auto error). */
|
|
38
|
+
error?: boolean;
|
|
39
|
+
/** Marks the field required (adds the label asterisk). */
|
|
40
|
+
required?: boolean;
|
|
41
|
+
/** Disables the field. */
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
/** Placeholder shown when no date is selected. */
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
/** Label/control layout. */
|
|
46
|
+
layout?: 'stacked' | 'inline';
|
|
47
|
+
/** Reactive visibility predicate evaluated against the form engine. */
|
|
48
|
+
visibleWhen?: (engine: Engine) => boolean;
|
|
49
|
+
/** RBAC access requirement. */
|
|
50
|
+
access?: AccessRequirement;
|
|
51
|
+
/** Controlled value — ISO `YYYY-MM-DD` or `null`. */
|
|
52
|
+
value?: ISODate | null;
|
|
53
|
+
/** Uncontrolled initial value. */
|
|
54
|
+
defaultValue?: ISODate | null;
|
|
55
|
+
/** Fired with the new ISO date (or `null` when cleared). */
|
|
56
|
+
onChange?: (value: ISODate | null) => void;
|
|
57
|
+
/** Earliest selectable date (inclusive). */
|
|
58
|
+
minDate?: ISODate;
|
|
59
|
+
/** Latest selectable date (inclusive). */
|
|
60
|
+
maxDate?: ISODate;
|
|
61
|
+
/** Explicit list of disabled dates. */
|
|
62
|
+
disabledDates?: readonly ISODate[];
|
|
63
|
+
/** Predicate marking arbitrary dates disabled. */
|
|
64
|
+
isDateDisabled?: (date: ISODate) => boolean;
|
|
65
|
+
/** Weekday of the calendar's first column (`0` = Sunday). */
|
|
66
|
+
weekStartDay?: WeekDay;
|
|
67
|
+
/** BCP-47 locale for the calendar and the display format. */
|
|
68
|
+
locale?: string;
|
|
69
|
+
/** Stretches the field to its container width. */
|
|
70
|
+
fullWidth?: boolean;
|
|
71
|
+
/** Root-level Tailwind class override. */
|
|
72
|
+
sx?: string;
|
|
73
|
+
/** Per-slot `className` overrides. */
|
|
74
|
+
slotProps?: DatePickerSlotProps;
|
|
75
|
+
/** Test id applied to the field root. */
|
|
76
|
+
testId?: string;
|
|
77
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slots for `<DatePicker>` — a bridge-integrated date field whose trigger
|
|
5
|
+
* is a `<button>` styled as an input, opening a `<Calendar>` popover.
|
|
6
|
+
*
|
|
7
|
+
* Theme identity: bare neutral / brand token-backed classes only — no
|
|
8
|
+
* `dark:` variants on the neutral palette (the preset CSS-var swap
|
|
9
|
+
* auto-inverts). See `_shared/themeIdentity.test.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export const datePickerVariants = tv({
|
|
12
|
+
slots: {
|
|
13
|
+
root: 'flex flex-col',
|
|
14
|
+
label: 'block text-sm font-medium text-neutral-900 mb-1',
|
|
15
|
+
requiredMark: 'text-danger-500 ml-0.5',
|
|
16
|
+
trigger:
|
|
17
|
+
'flex items-center gap-2 h-10 min-w-[12rem] rounded-md border border-neutral-300 bg-neutral-50 px-3 text-base text-neutral-900 cursor-pointer transition-colors hover:border-neutral-400 focus-visible:outline-none focus-visible:border-primary-500 focus-visible:ring-2 focus-visible:ring-primary-500/30 disabled:cursor-not-allowed disabled:opacity-60 disabled:bg-neutral-200',
|
|
18
|
+
value: 'flex-1 truncate text-left',
|
|
19
|
+
placeholder: 'flex-1 truncate text-left text-neutral-400',
|
|
20
|
+
icon: 'shrink-0 text-[1rem] text-neutral-500',
|
|
21
|
+
helperText: 'mt-1 text-sm text-neutral-600',
|
|
22
|
+
errorText: 'mt-1 text-sm text-danger-600',
|
|
23
|
+
},
|
|
24
|
+
variants: {
|
|
25
|
+
layout: {
|
|
26
|
+
stacked: {},
|
|
27
|
+
inline: {
|
|
28
|
+
root: 'flex-row items-start gap-3',
|
|
29
|
+
label: 'mb-0 pt-2.5 whitespace-nowrap shrink-0',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
error: {
|
|
33
|
+
true: {
|
|
34
|
+
trigger:
|
|
35
|
+
'border-danger-500 hover:border-danger-500 focus-visible:border-danger-500 focus-visible:ring-danger-500/30',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
fullWidth: {
|
|
39
|
+
true: {
|
|
40
|
+
root: 'w-full',
|
|
41
|
+
trigger: 'w-full min-w-0',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
layout: 'stacked',
|
|
47
|
+
error: false,
|
|
48
|
+
fullWidth: false,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -42,6 +42,15 @@ export function Dialog(props: DialogProps) {
|
|
|
42
42
|
/>
|
|
43
43
|
<RadixDialog.Content
|
|
44
44
|
className={cn(v.content(), sx, slotProps?.content?.className)}
|
|
45
|
+
// When no `description` is supplied we render no
|
|
46
|
+
// `<RadixDialog.Description>`. Radix then logs a dev warning
|
|
47
|
+
// unless the Content explicitly opts out via
|
|
48
|
+
// `aria-describedby={undefined}` — the documented escape
|
|
49
|
+
// hatch for description-less dialogs. Spread it ONLY in that
|
|
50
|
+
// case so a present description still auto-links its id.
|
|
51
|
+
{...(description == null
|
|
52
|
+
? { 'aria-describedby': undefined }
|
|
53
|
+
: {})}
|
|
45
54
|
onPointerDownOutside={(event) => {
|
|
46
55
|
if (disableBackdropClose) event.preventDefault();
|
|
47
56
|
}}
|