@dashforge/tw 0.9.1-beta → 0.11.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 +53 -0
- package/dist/index.esm.js +1330 -405
- package/dist/src/components/DateRangePicker/DateRangePicker.d.ts +13 -0
- package/dist/src/components/DateRangePicker/DateRangePicker.d.ts.map +1 -0
- package/dist/src/components/DateRangePicker/dateRangePicker.types.d.ts +85 -0
- package/dist/src/components/DateRangePicker/dateRangePicker.types.d.ts.map +1 -0
- package/dist/src/components/DateRangePicker/dateRangePicker.variants.d.ts +183 -0
- package/dist/src/components/DateRangePicker/dateRangePicker.variants.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts +7 -43
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts.map +1 -1
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts +50 -55
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -1
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts +36 -94
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -1
- package/dist/src/components/Tabs/Tabs.d.ts +8 -4
- package/dist/src/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/src/components/Tabs/tabs.types.d.ts +8 -2
- package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -1
- package/dist/src/components/Tabs/useTabs.d.ts +55 -0
- package/dist/src/components/Tabs/useTabs.d.ts.map +1 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts +12 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts.map +1 -0
- package/dist/src/components/TimePicker/timePicker.types.d.ts +80 -0
- package/dist/src/components/TimePicker/timePicker.types.d.ts.map +1 -0
- package/dist/src/components/TimePicker/timePicker.variants.d.ts +103 -0
- package/dist/src/components/TimePicker/timePicker.variants.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -4
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/components/DateRangePicker/DateRangePicker.test.tsx +92 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +510 -0
- package/src/components/DateRangePicker/dateRangePicker.types.ts +74 -0
- package/src/components/DateRangePicker/dateRangePicker.variants.ts +101 -0
- package/src/components/DateTimePicker/DateTimePicker.test.tsx +60 -186
- package/src/components/DateTimePicker/DateTimePicker.tsx +209 -181
- package/src/components/DateTimePicker/dateTimePicker.types.ts +50 -54
- package/src/components/DateTimePicker/dateTimePicker.variants.ts +22 -51
- package/src/components/Tabs/Tabs.test.tsx +85 -14
- package/src/components/Tabs/Tabs.tsx +35 -24
- package/src/components/Tabs/tabs.types.ts +8 -2
- package/src/components/Tabs/useTabs.ts +167 -0
- package/src/components/TimePicker/TimePicker.test.tsx +65 -0
- package/src/components/TimePicker/TimePicker.tsx +274 -0
- package/src/components/TimePicker/timePicker.types.ts +69 -0
- package/src/components/TimePicker/timePicker.variants.ts +53 -0
- package/src/index.ts +28 -16
- package/src/components/DateTimePicker/DateTimePicker.perf.test.tsx +0 -153
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slots for `<DateRangePicker>` — a bridge-integrated start/end date field
|
|
5
|
+
* whose trigger is a `<button>` styled as an input, opening a dual-month
|
|
6
|
+
* range calendar popover.
|
|
7
|
+
*
|
|
8
|
+
* Theme identity: bare neutral / brand token-backed classes only — no
|
|
9
|
+
* `dark:` variants on the neutral palette (the preset CSS-var swap
|
|
10
|
+
* auto-inverts). See `_shared/themeIdentity.test.ts`.
|
|
11
|
+
*/
|
|
12
|
+
export const dateRangePickerVariants = tv({
|
|
13
|
+
slots: {
|
|
14
|
+
root: 'flex flex-col',
|
|
15
|
+
label: 'block text-sm font-medium text-neutral-900 mb-1',
|
|
16
|
+
requiredMark: 'text-danger-500 ml-0.5',
|
|
17
|
+
trigger:
|
|
18
|
+
'flex items-center gap-2 h-10 min-w-[15rem] 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',
|
|
19
|
+
value: 'flex-1 truncate text-left',
|
|
20
|
+
placeholder: 'flex-1 truncate text-left text-neutral-400',
|
|
21
|
+
icon: 'shrink-0 text-[1rem] text-neutral-500',
|
|
22
|
+
panel: 'flex gap-3 p-3 select-none',
|
|
23
|
+
month: 'flex flex-col gap-2',
|
|
24
|
+
header: 'flex items-center justify-between gap-1',
|
|
25
|
+
navButton:
|
|
26
|
+
'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-[1rem] text-neutral-600 transition-colors hover:bg-neutral-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
27
|
+
navSpacer: 'h-7 w-7 shrink-0',
|
|
28
|
+
monthLabel: 'flex-1 text-center text-sm font-semibold text-neutral-900',
|
|
29
|
+
grid: 'flex flex-col gap-1',
|
|
30
|
+
weekdayRow: 'grid grid-cols-7 gap-1',
|
|
31
|
+
weekday:
|
|
32
|
+
'flex h-8 items-center justify-center text-xs font-semibold text-neutral-500',
|
|
33
|
+
weekRow: 'grid grid-cols-7 gap-1',
|
|
34
|
+
helperText: 'mt-1 text-sm text-neutral-600',
|
|
35
|
+
errorText: 'mt-1 text-sm text-danger-600',
|
|
36
|
+
},
|
|
37
|
+
variants: {
|
|
38
|
+
layout: {
|
|
39
|
+
stacked: {},
|
|
40
|
+
inline: {
|
|
41
|
+
root: 'flex-row items-start gap-3',
|
|
42
|
+
label: 'mb-0 pt-2.5 whitespace-nowrap shrink-0',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
error: {
|
|
46
|
+
true: {
|
|
47
|
+
trigger:
|
|
48
|
+
'border-danger-500 hover:border-danger-500 focus-visible:border-danger-500 focus-visible:ring-danger-500/30',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
fullWidth: {
|
|
52
|
+
true: {
|
|
53
|
+
root: 'w-full',
|
|
54
|
+
trigger: 'w-full min-w-0',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: {
|
|
59
|
+
layout: 'stacked',
|
|
60
|
+
error: false,
|
|
61
|
+
fullWidth: false,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Per-day-cell recipe for the range grid. `endpoint` (a committed start/end)
|
|
67
|
+
* is the solid fill; `inBand` (inside the committed or previewed range) is
|
|
68
|
+
* the light tint. Declared `inBand` before `endpoint` so an endpoint cell —
|
|
69
|
+
* which is also in-band — resolves to the solid fill.
|
|
70
|
+
*/
|
|
71
|
+
export const dateRangeDayVariants = tv({
|
|
72
|
+
base: 'flex h-9 w-9 items-center justify-center rounded-md text-sm text-neutral-900 transition-colors hover:bg-neutral-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500',
|
|
73
|
+
variants: {
|
|
74
|
+
inBand: {
|
|
75
|
+
true: 'bg-primary-100 hover:bg-primary-100',
|
|
76
|
+
},
|
|
77
|
+
endpoint: {
|
|
78
|
+
true: 'bg-primary-600 text-white hover:bg-primary-600',
|
|
79
|
+
},
|
|
80
|
+
siblingMonth: {
|
|
81
|
+
true: 'text-neutral-400',
|
|
82
|
+
},
|
|
83
|
+
today: {
|
|
84
|
+
true: 'font-bold ring-1 ring-inset ring-primary-400',
|
|
85
|
+
},
|
|
86
|
+
disabled: {
|
|
87
|
+
true: 'cursor-default text-neutral-300 opacity-60 hover:bg-transparent',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
compoundVariants: [
|
|
91
|
+
{ endpoint: true, today: true, class: 'ring-0' },
|
|
92
|
+
{ endpoint: true, siblingMonth: true, class: 'text-white' },
|
|
93
|
+
],
|
|
94
|
+
defaultVariants: {
|
|
95
|
+
inBand: false,
|
|
96
|
+
endpoint: false,
|
|
97
|
+
siblingMonth: false,
|
|
98
|
+
today: false,
|
|
99
|
+
disabled: false,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -1,204 +1,78 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
3
3
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
-
import { DashFormProvider } from '@dashforge/forms';
|
|
5
|
-
import { DateTimePicker, isoToInputValue } from './DateTimePicker.js';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
import { DateTimePicker } from './DateTimePicker.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
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
describe('DateTimePicker', () => {
|
|
27
|
+
it('renders a labelled trigger with the placeholder', () => {
|
|
28
|
+
render(
|
|
29
|
+
<DateTimePicker name="appt" label="Appointment" placeholder="Pick one" />,
|
|
29
30
|
);
|
|
31
|
+
expect(screen.queryByText('Appointment')).not.toBeNull();
|
|
32
|
+
expect(screen.getByRole('button').textContent).toContain('Pick one');
|
|
30
33
|
});
|
|
31
34
|
|
|
32
|
-
it('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
it('shows a formatted controlled value', () => {
|
|
36
|
+
render(<DateTimePicker name="d" value="2026-05-20T14:30" />);
|
|
37
|
+
expect(screen.getByRole('button').textContent).toContain(
|
|
38
|
+
'May 20, 2026, 14:30',
|
|
35
39
|
);
|
|
36
40
|
});
|
|
37
41
|
|
|
38
|
-
it('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
it('renders a native date input by default', () => {
|
|
46
|
-
render(<DateTimePicker name="dob" label="DOB" />);
|
|
47
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
48
|
-
expect(input.type).toBe('date');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('renders a time input when mode="time"', () => {
|
|
52
|
-
render(<DateTimePicker name="t" label="Time" mode="time" />);
|
|
53
|
-
const input = screen.getByLabelText('Time') as HTMLInputElement;
|
|
54
|
-
expect(input.type).toBe('time');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('renders a datetime-local input when mode="datetime"', () => {
|
|
58
|
-
render(<DateTimePicker name="dt" label="When" mode="datetime" />);
|
|
59
|
-
const input = screen.getByLabelText('When') as HTMLInputElement;
|
|
60
|
-
expect(input.type).toBe('datetime-local');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('seeds `defaultValue` (uncontrolled)', () => {
|
|
64
|
-
render(
|
|
65
|
-
<DateTimePicker name="dob" label="DOB" defaultValue="2026-05-16" />
|
|
66
|
-
);
|
|
67
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
68
|
-
expect(input.defaultValue).toBe('2026-05-16');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('reflects controlled `value`', () => {
|
|
72
|
-
render(
|
|
73
|
-
<DateTimePicker name="dob" label="DOB" value="2026-05-16" />
|
|
74
|
-
);
|
|
75
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
76
|
-
expect(input.value).toBe('2026-05-16');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('truncates a controlled rich ISO to the date mode prefix', () => {
|
|
80
|
-
render(
|
|
81
|
-
<DateTimePicker
|
|
82
|
-
name="dob"
|
|
83
|
-
label="DOB"
|
|
84
|
-
value="2026-05-16T09:42:15.000Z"
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
88
|
-
expect(input.value).toBe('2026-05-16');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('forwards min/max/step to the native input', () => {
|
|
92
|
-
render(
|
|
93
|
-
<DateTimePicker
|
|
94
|
-
name="t"
|
|
95
|
-
label="Time"
|
|
96
|
-
mode="time"
|
|
97
|
-
min="08:00"
|
|
98
|
-
max="18:00"
|
|
99
|
-
step={300}
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
102
|
-
const input = screen.getByLabelText('Time') as HTMLInputElement;
|
|
103
|
-
expect(input.min).toBe('08:00');
|
|
104
|
-
expect(input.max).toBe('18:00');
|
|
105
|
-
expect(input.step).toBe('300');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('renders a `*` for required fields and sets the native attr', () => {
|
|
109
|
-
render(<DateTimePicker name="dob" label="DOB" required />);
|
|
110
|
-
const input = screen.getByLabelText(/DOB/) as HTMLInputElement;
|
|
111
|
-
expect(input.required).toBe(true);
|
|
112
|
-
expect(screen.getByText('*')).toBeTruthy();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('renders helperText when not in error', () => {
|
|
116
|
-
render(
|
|
117
|
-
<DateTimePicker
|
|
118
|
-
name="dob"
|
|
119
|
-
label="DOB"
|
|
120
|
-
helperText="Pick a date in the past."
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
expect(screen.getByText('Pick a date in the past.')).toBeTruthy();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('shows the helperText in the error slot when `error` is true', () => {
|
|
127
|
-
render(
|
|
128
|
-
<DateTimePicker
|
|
129
|
-
name="dob"
|
|
130
|
-
label="DOB"
|
|
131
|
-
error
|
|
132
|
-
helperText="Required"
|
|
133
|
-
/>
|
|
134
|
-
);
|
|
135
|
-
const p = screen.getByText('Required');
|
|
136
|
-
// The error slot has the `text-danger-600` class per the variants.
|
|
137
|
-
expect(p.className).toMatch(/danger/);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('disables the input', () => {
|
|
141
|
-
render(<DateTimePicker name="dob" label="DOB" disabled />);
|
|
142
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
143
|
-
expect(input.disabled).toBe(true);
|
|
144
|
-
});
|
|
42
|
+
it('opens a popover with a calendar and a time list', () => {
|
|
43
|
+
render(<DateTimePicker name="d" defaultValue="2026-05-20T10:00" />);
|
|
44
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
45
|
+
fireEvent.click(screen.getByRole('button'));
|
|
46
|
+
expect(screen.queryByRole('dialog')).not.toBeNull();
|
|
47
|
+
expect(screen.getByRole('grid')).not.toBeNull();
|
|
48
|
+
expect(screen.getByRole('listbox', { name: 'Time options' })).not.toBeNull();
|
|
145
49
|
});
|
|
146
50
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
158
|
-
fireEvent.change(input, { target: { value: '2026-12-25' } });
|
|
159
|
-
expect(onValueChange).toHaveBeenCalledWith('2026-12-25');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('fires onValueChange with `null` when the input is cleared', () => {
|
|
163
|
-
const onValueChange = vi.fn();
|
|
164
|
-
render(
|
|
165
|
-
<DateTimePicker
|
|
166
|
-
name="dob"
|
|
167
|
-
label="DOB"
|
|
168
|
-
value="2026-05-16"
|
|
169
|
-
onValueChange={onValueChange}
|
|
170
|
-
/>
|
|
171
|
-
);
|
|
172
|
-
const input = screen.getByLabelText('DOB') as HTMLInputElement;
|
|
173
|
-
fireEvent.change(input, { target: { value: '' } });
|
|
174
|
-
expect(onValueChange).toHaveBeenCalledWith(null);
|
|
175
|
-
});
|
|
51
|
+
it('picking a date updates the value and keeps the popover open', () => {
|
|
52
|
+
const onChange = vi.fn();
|
|
53
|
+
render(
|
|
54
|
+
<DateTimePicker name="d" defaultValue="2026-05-20T10:00" onChange={onChange} />,
|
|
55
|
+
);
|
|
56
|
+
fireEvent.click(screen.getByRole('button'));
|
|
57
|
+
fireEvent.click(screen.getByRole('button', { name: /May 15, 2026/ }));
|
|
58
|
+
expect(onChange).toHaveBeenCalledWith('2026-05-15T10:00');
|
|
59
|
+
expect(screen.queryByRole('dialog')).not.toBeNull();
|
|
176
60
|
});
|
|
177
61
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// are tested), so we don't assert input.value here.
|
|
188
|
-
expect(screen.getByLabelText('DOB')).toBeTruthy();
|
|
189
|
-
});
|
|
62
|
+
it('picking a time completes the value and closes the popover', () => {
|
|
63
|
+
const onChange = vi.fn();
|
|
64
|
+
render(
|
|
65
|
+
<DateTimePicker name="d" defaultValue="2026-05-20T10:00" onChange={onChange} />,
|
|
66
|
+
);
|
|
67
|
+
fireEvent.click(screen.getByRole('button'));
|
|
68
|
+
fireEvent.click(screen.getByRole('option', { name: '14:30' }));
|
|
69
|
+
expect(onChange).toHaveBeenCalledWith('2026-05-20T14:30');
|
|
70
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
190
71
|
});
|
|
191
72
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
name="dob"
|
|
197
|
-
label="DOB"
|
|
198
|
-
visibleWhen={() => false}
|
|
199
|
-
/>
|
|
200
|
-
);
|
|
201
|
-
expect(container.querySelector('input')).toBeNull();
|
|
202
|
-
});
|
|
73
|
+
it('does not open the popover when disabled', () => {
|
|
74
|
+
render(<DateTimePicker name="d" disabled />);
|
|
75
|
+
fireEvent.click(screen.getByRole('button'));
|
|
76
|
+
expect(screen.queryByRole('dialog')).toBeNull();
|
|
203
77
|
});
|
|
204
78
|
});
|