@k3-universe/react-kit 0.0.10 → 0.0.11

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.
Files changed (48) hide show
  1. package/dist/index.js +1303 -75
  2. package/dist/kit/builder/form/components/FormBuilder.d.ts +5 -1
  3. package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
  4. package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
  5. package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts +4 -0
  6. package/dist/kit/builder/form/components/fields/DateTimePickerField.d.ts.map +1 -0
  7. package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts +4 -0
  8. package/dist/kit/builder/form/components/fields/DateTimeRangePickerField.d.ts.map +1 -0
  9. package/dist/kit/builder/form/components/fields/TimePickerField.d.ts +4 -0
  10. package/dist/kit/builder/form/components/fields/TimePickerField.d.ts.map +1 -0
  11. package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts +4 -0
  12. package/dist/kit/builder/form/components/fields/TimeRangePickerField.d.ts.map +1 -0
  13. package/dist/kit/builder/form/components/fields/index.d.ts +4 -0
  14. package/dist/kit/builder/form/components/fields/index.d.ts.map +1 -1
  15. package/dist/kit/components/datetimepicker/DateTimePicker.d.ts +32 -0
  16. package/dist/kit/components/datetimepicker/DateTimePicker.d.ts.map +1 -0
  17. package/dist/kit/components/datetimepicker/DateTimeRangePicker.d.ts +39 -0
  18. package/dist/kit/components/datetimepicker/DateTimeRangePicker.d.ts.map +1 -0
  19. package/dist/kit/components/datetimepicker/index.d.ts +5 -0
  20. package/dist/kit/components/datetimepicker/index.d.ts.map +1 -0
  21. package/dist/kit/components/timepicker/TimePicker.d.ts +26 -0
  22. package/dist/kit/components/timepicker/TimePicker.d.ts.map +1 -0
  23. package/dist/kit/components/timepicker/TimeRangePicker.d.ts +31 -0
  24. package/dist/kit/components/timepicker/TimeRangePicker.d.ts.map +1 -0
  25. package/dist/kit/components/timepicker/index.d.ts +5 -0
  26. package/dist/kit/components/timepicker/index.d.ts.map +1 -0
  27. package/dist/kit/themes/clean-slate.css +16 -0
  28. package/dist/kit/themes/default.css +16 -0
  29. package/dist/kit/themes/minimal-modern.css +16 -0
  30. package/dist/kit/themes/spotify.css +16 -0
  31. package/package.json +1 -1
  32. package/src/kit/builder/form/components/FormBuilder.tsx +17 -0
  33. package/src/kit/builder/form/components/FormBuilderField.tsx +48 -0
  34. package/src/kit/builder/form/components/fields/DateTimePickerField.tsx +33 -0
  35. package/src/kit/builder/form/components/fields/DateTimeRangePickerField.tsx +42 -0
  36. package/src/kit/builder/form/components/fields/TimePickerField.tsx +30 -0
  37. package/src/kit/builder/form/components/fields/TimeRangePickerField.tsx +37 -0
  38. package/src/kit/builder/form/components/fields/index.ts +4 -0
  39. package/src/kit/components/datetimepicker/DateTimePicker.tsx +314 -0
  40. package/src/kit/components/datetimepicker/DateTimeRangePicker.tsx +486 -0
  41. package/src/kit/components/datetimepicker/index.ts +3 -0
  42. package/src/kit/components/timepicker/TimePicker.tsx +311 -0
  43. package/src/kit/components/timepicker/TimeRangePicker.tsx +291 -0
  44. package/src/kit/components/timepicker/index.ts +3 -0
  45. package/src/stories/kit/builder/Form.DateTime.stories.tsx +66 -0
  46. package/src/stories/kit/builder/Form.Time.stories.tsx +64 -0
  47. package/src/stories/kit/components/TimePicker.stories.tsx +69 -0
  48. package/src/stories/kit/components/TimeRangePicker.stories.tsx +37 -0
@@ -0,0 +1,311 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Clock } from 'lucide-react';
5
+ import { cn } from '../../../shadcn/lib/utils';
6
+ import { Button } from '../../../shadcn/ui/button';
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from '../../../shadcn/ui/popover';
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from '../../../shadcn/ui/select';
19
+
20
+ export type TimePrecision = 'hour' | 'minute' | 'second';
21
+
22
+ export interface TimePickerProps
23
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
24
+ value?: Date | null;
25
+ onChange?: (date: Date | null) => void;
26
+ placeholder?: string;
27
+ disabled?: boolean;
28
+ precision?: TimePrecision; // default 'minute'
29
+ hourCycle?: 12 | 24; // default 24
30
+ minuteStep?: number; // default 5
31
+ secondStep?: number; // default 5
32
+ buttonVariant?: React.ComponentProps<typeof Button>['variant'];
33
+ open?: boolean;
34
+ onOpenChange?: (open: boolean) => void;
35
+ showFooter?: boolean; // default true
36
+ cancelLabel?: string; // default 'Cancel'
37
+ applyLabel?: string; // default 'Apply'
38
+ clearLabel?: string; // default 'Clear'
39
+ }
40
+
41
+ const clamp = (n: number, min: number, max: number) =>
42
+ Math.max(min, Math.min(max, n));
43
+ const pad2 = (n: number) => String(n).padStart(2, '0');
44
+
45
+ export function TimePicker({
46
+ value,
47
+ onChange,
48
+ placeholder = 'Pick a time',
49
+ disabled,
50
+ precision = 'minute',
51
+ hourCycle = 24,
52
+ minuteStep = 5,
53
+ secondStep = 5,
54
+ className,
55
+ buttonVariant = 'outline',
56
+ ...props
57
+ }: TimePickerProps) {
58
+ const [internalOpen, setInternalOpen] = React.useState(false);
59
+ const isOpen = typeof props.open === 'boolean' ? props.open : internalOpen;
60
+ const setOpen = (o: boolean) =>
61
+ props.onOpenChange ? props.onOpenChange(o) : setInternalOpen(o);
62
+
63
+ const [draft, setDraft] = React.useState<Date | null>(value ?? null);
64
+
65
+ React.useEffect(() => {
66
+ if (isOpen) setDraft(value ?? null);
67
+ }, [isOpen, value]);
68
+
69
+ const fmtLabel = (d: Date | null): string => {
70
+ if (!d) return placeholder;
71
+ const h = d.getHours();
72
+ const m = d.getMinutes();
73
+ const s = d.getSeconds();
74
+ if (hourCycle === 12) {
75
+ const period = h >= 12 ? 'PM' : 'AM';
76
+ const hour12 = h % 12 === 0 ? 12 : h % 12;
77
+ if (precision === 'hour') return `${hour12} ${period}`;
78
+ if (precision === 'minute') return `${hour12}:${pad2(m)} ${period}`;
79
+ return `${hour12}:${pad2(m)}:${pad2(s)} ${period}`;
80
+ }
81
+ if (precision === 'hour') return `${pad2(h)}`;
82
+ if (precision === 'minute') return `${pad2(h)}:${pad2(m)}`;
83
+ return `${pad2(h)}:${pad2(m)}:${pad2(s)}`;
84
+ };
85
+
86
+ const setDraftPart = (
87
+ part: 'hour' | 'minute' | 'second' | 'period',
88
+ val: number | 'AM' | 'PM'
89
+ ) => {
90
+ setDraft((prev) => {
91
+ const base = prev
92
+ ? new Date(prev)
93
+ : (() => {
94
+ const n = new Date();
95
+ return new Date(
96
+ n.getFullYear(),
97
+ n.getMonth(),
98
+ n.getDate(),
99
+ 0,
100
+ 0,
101
+ 0,
102
+ 0
103
+ );
104
+ })();
105
+ if (part === 'hour') {
106
+ let h = Number(val);
107
+ if (hourCycle === 12) {
108
+ const isPM = base.getHours() >= 12;
109
+ h = h % 12;
110
+ base.setHours(isPM ? (h === 12 ? 12 : h + 12) : h === 12 ? 0 : h);
111
+ } else {
112
+ base.setHours(clamp(h, 0, 23));
113
+ }
114
+ } else if (part === 'minute') {
115
+ base.setMinutes(clamp(Number(val), 0, 59));
116
+ } else if (part === 'second') {
117
+ base.setSeconds(clamp(Number(val), 0, 59));
118
+ } else if (part === 'period' && (val === 'AM' || val === 'PM')) {
119
+ const curH = base.getHours();
120
+ const isAMNow = curH < 12;
121
+ if (val === 'AM' && !isAMNow) base.setHours(curH - 12);
122
+ if (val === 'PM' && isAMNow) base.setHours(curH + 12);
123
+ }
124
+ base.setMilliseconds(0);
125
+ return new Date(base);
126
+ });
127
+ };
128
+
129
+ const hours = React.useMemo(() => {
130
+ return hourCycle === 12
131
+ ? Array.from({ length: 12 }, (_, i) => i + 1)
132
+ : Array.from({ length: 24 }, (_, i) => i);
133
+ }, [hourCycle]);
134
+ const minutes = React.useMemo(
135
+ () =>
136
+ Array.from(
137
+ { length: Math.ceil(60 / minuteStep) },
138
+ (_, i) => i * minuteStep
139
+ ),
140
+ [minuteStep]
141
+ );
142
+ const seconds = React.useMemo(
143
+ () =>
144
+ Array.from(
145
+ { length: Math.ceil(60 / secondStep) },
146
+ (_, i) => i * secondStep
147
+ ),
148
+ [secondStep]
149
+ );
150
+
151
+ const selectedHour = React.useMemo(() => {
152
+ if (!draft) return hourCycle === 12 ? 12 : 0;
153
+ const h = draft.getHours();
154
+ return hourCycle === 12 ? (h % 12 === 0 ? 12 : h % 12) : h;
155
+ }, [draft, hourCycle]);
156
+ const selectedMinute = draft?.getMinutes() ?? 0;
157
+ const selectedSecond = draft?.getSeconds() ?? 0;
158
+ const selectedPeriod: 'AM' | 'PM' =
159
+ draft && draft.getHours() >= 12 ? 'PM' : 'AM';
160
+
161
+ const label = fmtLabel(value ?? null);
162
+
163
+ return (
164
+ <div className={cn('w-fit', className)} {...props}>
165
+ <Popover open={isOpen} onOpenChange={setOpen}>
166
+ <PopoverTrigger asChild>
167
+ <Button
168
+ type="button"
169
+ disabled={disabled}
170
+ variant={buttonVariant}
171
+ className={cn(
172
+ 'w-[240px] justify-start text-left font-normal',
173
+ !value && 'text-muted-foreground'
174
+ )}
175
+ >
176
+ <Clock className="mr-2 h-4 w-4" />
177
+ {label}
178
+ </Button>
179
+ </PopoverTrigger>
180
+ <PopoverContent className="p-3 w-auto" align="start">
181
+ <div className="flex items-end gap-2">
182
+ <div className="w-24">
183
+ <div className="mb-1 block text-xs text-muted-foreground">Hour</div>
184
+ <Select
185
+ disabled={disabled}
186
+ value={String(selectedHour)}
187
+ onValueChange={(v) => setDraftPart('hour', Number(v))}
188
+ >
189
+ <SelectTrigger aria-label="Hour">
190
+ <SelectValue />
191
+ </SelectTrigger>
192
+ <SelectContent>
193
+ {hours.map((h) => (
194
+ <SelectItem key={h} value={String(h)}>
195
+ {hourCycle === 12 ? h : pad2(h)}
196
+ </SelectItem>
197
+ ))}
198
+ </SelectContent>
199
+ </Select>
200
+ </div>
201
+
202
+ {(precision === 'minute' || precision === 'second') && (
203
+ <div className="w-24">
204
+ <div className="mb-1 block text-xs text-muted-foreground">Minute</div>
205
+ <Select
206
+ disabled={disabled}
207
+ value={String(selectedMinute - (selectedMinute % minuteStep))}
208
+ onValueChange={(v) => setDraftPart('minute', Number(v))}
209
+ >
210
+ <SelectTrigger aria-label="Minute">
211
+ <SelectValue />
212
+ </SelectTrigger>
213
+ <SelectContent>
214
+ {minutes.map((m) => (
215
+ <SelectItem key={m} value={String(m)}>
216
+ {pad2(m)}
217
+ </SelectItem>
218
+ ))}
219
+ </SelectContent>
220
+ </Select>
221
+ </div>
222
+ )}
223
+
224
+ {precision === 'second' && (
225
+ <div className="w-24">
226
+ <div className="mb-1 block text-xs text-muted-foreground">Second</div>
227
+ <Select
228
+ disabled={disabled}
229
+ value={String(selectedSecond - (selectedSecond % secondStep))}
230
+ onValueChange={(v) => setDraftPart('second', Number(v))}
231
+ >
232
+ <SelectTrigger aria-label="Second">
233
+ <SelectValue />
234
+ </SelectTrigger>
235
+ <SelectContent>
236
+ {seconds.map((s) => (
237
+ <SelectItem key={s} value={String(s)}>
238
+ {pad2(s)}
239
+ </SelectItem>
240
+ ))}
241
+ </SelectContent>
242
+ </Select>
243
+ </div>
244
+ )}
245
+
246
+ {hourCycle === 12 && (
247
+ <div className="w-24">
248
+ <div className="mb-1 block text-xs text-muted-foreground">Period</div>
249
+ <Select
250
+ disabled={disabled}
251
+ value={selectedPeriod}
252
+ onValueChange={(v) =>
253
+ setDraftPart('period', v as 'AM' | 'PM')
254
+ }
255
+ >
256
+ <SelectTrigger aria-label="Period">
257
+ <SelectValue />
258
+ </SelectTrigger>
259
+ <SelectContent>
260
+ <SelectItem value="AM">AM</SelectItem>
261
+ <SelectItem value="PM">PM</SelectItem>
262
+ </SelectContent>
263
+ </Select>
264
+ </div>
265
+ )}
266
+ </div>
267
+
268
+ {(props.showFooter ?? true) && (
269
+ <div className="flex items-center justify-between gap-2 pt-3 mt-3 border-t">
270
+ <Button
271
+ type="button"
272
+ variant="outline"
273
+ size="sm"
274
+ onClick={() => setOpen(false)}
275
+ disabled={disabled}
276
+ >
277
+ {props.cancelLabel ?? 'Cancel'}
278
+ </Button>
279
+ <div className="flex gap-2">
280
+ <Button
281
+ type="button"
282
+ variant="ghost"
283
+ size="sm"
284
+ onClick={() => onChange?.(null)}
285
+ disabled={disabled}
286
+ >
287
+ {props.clearLabel ?? 'Clear'}
288
+ </Button>
289
+ <Button
290
+ type="button"
291
+ variant="default"
292
+ size="sm"
293
+ onClick={() => {
294
+ onChange?.(draft ?? null);
295
+ setOpen(false);
296
+ }}
297
+ >
298
+ {props.applyLabel ?? 'Apply'}
299
+ </Button>
300
+ </div>
301
+ </div>
302
+ )}
303
+ </PopoverContent>
304
+ </Popover>
305
+ </div>
306
+ );
307
+ }
308
+
309
+ TimePicker.displayName = 'TimePicker';
310
+
311
+ export default TimePicker;
@@ -0,0 +1,291 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Clock } from 'lucide-react';
5
+ import { cn } from '../../../shadcn/lib/utils';
6
+ import { Button } from '../../../shadcn/ui/button';
7
+ import { Popover, PopoverContent, PopoverTrigger } from '../../../shadcn/ui/popover';
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '../../../shadcn/ui/select';
15
+
16
+ export type TimePrecision = 'hour' | 'minute' | 'second';
17
+
18
+ export interface TimeRangePickerValue {
19
+ from?: Date | null;
20
+ to?: Date | null;
21
+ }
22
+
23
+ export interface TimeRangePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
24
+ value?: TimeRangePickerValue | null;
25
+ onChange?: (range: TimeRangePickerValue | null) => void;
26
+ placeholder?: string;
27
+ disabled?: boolean;
28
+ precision?: TimePrecision; // default 'minute'
29
+ hourCycle?: 12 | 24; // default 24
30
+ minuteStep?: number; // default 5
31
+ secondStep?: number; // default 5
32
+ buttonVariant?: React.ComponentProps<typeof Button>['variant'];
33
+ open?: boolean;
34
+ onOpenChange?: (open: boolean) => void;
35
+ showFooter?: boolean; // default true
36
+ cancelLabel?: string; // default 'Cancel'
37
+ applyLabel?: string; // default 'Apply'
38
+ clearLabel?: string; // default 'Clear'
39
+ format?: (from?: Date | null, to?: Date | null) => string;
40
+ }
41
+
42
+ const pad2 = (n: number) => String(n).padStart(2, '0');
43
+
44
+ function TimeUnitSelector({
45
+ label,
46
+ hourCycle,
47
+ precision,
48
+ minuteStep,
49
+ secondStep,
50
+ disabled,
51
+ value,
52
+ onChange,
53
+ }: {
54
+ label: string;
55
+ hourCycle: 12 | 24;
56
+ precision: TimePrecision;
57
+ minuteStep: number;
58
+ secondStep: number;
59
+ disabled?: boolean;
60
+ value: Date | null | undefined;
61
+ onChange: (next: Date | null) => void;
62
+ }) {
63
+ const hours = React.useMemo(() => (hourCycle === 12 ? Array.from({ length: 12 }, (_, i) => i + 1) : Array.from({ length: 24 }, (_, i) => i)), [hourCycle]);
64
+ const minutes = React.useMemo(() => Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep), [minuteStep]);
65
+ const seconds = React.useMemo(() => Array.from({ length: Math.ceil(60 / secondStep) }, (_, i) => i * secondStep), [secondStep]);
66
+ const selectedHour = React.useMemo(() => {
67
+ if (!value) return hourCycle === 12 ? 12 : 0;
68
+ const h = value.getHours();
69
+ return hourCycle === 12 ? (h % 12 === 0 ? 12 : h % 12) : h;
70
+ }, [value, hourCycle]);
71
+ const selectedMinute = value?.getMinutes() ?? 0;
72
+ const selectedSecond = value?.getSeconds() ?? 0;
73
+ const selectedPeriod: 'AM' | 'PM' = value && value.getHours() >= 12 ? 'PM' : 'AM';
74
+
75
+ const setPart = (part: 'hour' | 'minute' | 'second' | 'period', v: number | 'AM' | 'PM') => {
76
+ const base = value
77
+ ? new Date(value)
78
+ : (() => {
79
+ const n = new Date();
80
+ return new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0, 0);
81
+ })();
82
+ if (part === 'hour') {
83
+ let h = Number(v);
84
+ if (hourCycle === 12) {
85
+ const isPM = base.getHours() >= 12;
86
+ h = h % 12;
87
+ base.setHours(isPM ? (h === 12 ? 12 : h + 12) : (h === 12 ? 0 : h));
88
+ } else {
89
+ base.setHours(h);
90
+ }
91
+ } else if (part === 'minute') {
92
+ base.setMinutes(Number(v));
93
+ } else if (part === 'second') {
94
+ base.setSeconds(Number(v));
95
+ } else if (part === 'period' && (v === 'AM' || v === 'PM')) {
96
+ const curH = base.getHours();
97
+ const isAMNow = curH < 12;
98
+ if (v === 'AM' && !isAMNow) base.setHours(curH - 12);
99
+ if (v === 'PM' && isAMNow) base.setHours(curH + 12);
100
+ }
101
+ base.setMilliseconds(0);
102
+ onChange(base);
103
+ };
104
+
105
+ return (
106
+ <div className="space-y-1">
107
+ <div className="text-xs text-muted-foreground">{label}</div>
108
+ <div className="flex items-end gap-2">
109
+ <div className="w-24">
110
+ <div className="mb-1 block text-xs text-muted-foreground">Hour</div>
111
+ <Select disabled={disabled} value={String(selectedHour)} onValueChange={(v) => setPart('hour', Number(v))}>
112
+ <SelectTrigger aria-label={`${label} hour`}>
113
+ <SelectValue />
114
+ </SelectTrigger>
115
+ <SelectContent>
116
+ {hours.map((h) => (
117
+ <SelectItem key={h} value={String(h)}>{hourCycle === 12 ? h : pad2(h)}</SelectItem>
118
+ ))}
119
+ </SelectContent>
120
+ </Select>
121
+ </div>
122
+
123
+ {(precision === 'minute' || precision === 'second') && (
124
+ <div className="w-24">
125
+ <div className="mb-1 block text-xs text-muted-foreground">Minute</div>
126
+ <Select disabled={disabled} value={String(selectedMinute - (selectedMinute % minuteStep))} onValueChange={(v) => setPart('minute', Number(v))}>
127
+ <SelectTrigger aria-label={`${label} minute`}>
128
+ <SelectValue />
129
+ </SelectTrigger>
130
+ <SelectContent>
131
+ {minutes.map((m) => (
132
+ <SelectItem key={m} value={String(m)}>{pad2(m)}</SelectItem>
133
+ ))}
134
+ </SelectContent>
135
+ </Select>
136
+ </div>
137
+ )}
138
+
139
+ {precision === 'second' && (
140
+ <div className="w-24">
141
+ <div className="mb-1 block text-xs text-muted-foreground">Second</div>
142
+ <Select disabled={disabled} value={String(selectedSecond - (selectedSecond % secondStep))} onValueChange={(v) => setPart('second', Number(v))}>
143
+ <SelectTrigger aria-label={`${label} second`}>
144
+ <SelectValue />
145
+ </SelectTrigger>
146
+ <SelectContent>
147
+ {seconds.map((s) => (
148
+ <SelectItem key={s} value={String(s)}>{pad2(s)}</SelectItem>
149
+ ))}
150
+ </SelectContent>
151
+ </Select>
152
+ </div>
153
+ )}
154
+
155
+ {hourCycle === 12 && (
156
+ <div className="w-24">
157
+ <div className="mb-1 block text-xs text-muted-foreground">Period</div>
158
+ <Select disabled={disabled} value={selectedPeriod} onValueChange={(v) => setPart('period', v as 'AM' | 'PM')}>
159
+ <SelectTrigger aria-label={`${label} period`}>
160
+ <SelectValue />
161
+ </SelectTrigger>
162
+ <SelectContent>
163
+ <SelectItem value="AM">AM</SelectItem>
164
+ <SelectItem value="PM">PM</SelectItem>
165
+ </SelectContent>
166
+ </Select>
167
+ </div>
168
+ )}
169
+ </div>
170
+ </div>
171
+ );
172
+ }
173
+
174
+ export function TimeRangePicker({
175
+ value,
176
+ onChange,
177
+ placeholder = 'Pick a time range',
178
+ disabled,
179
+ precision = 'minute',
180
+ hourCycle = 24,
181
+ minuteStep = 5,
182
+ secondStep = 5,
183
+ className,
184
+ buttonVariant = 'outline',
185
+ format,
186
+ ...props
187
+ }: TimeRangePickerProps) {
188
+ const [internalOpen, setInternalOpen] = React.useState(false);
189
+ const isOpen = typeof props.open === 'boolean' ? props.open : internalOpen;
190
+ const setOpen = (o: boolean) => (props.onOpenChange ? props.onOpenChange(o) : setInternalOpen(o));
191
+ const [draft, setDraft] = React.useState<TimeRangePickerValue | null>(value ?? null);
192
+
193
+ React.useEffect(() => {
194
+ if (isOpen) setDraft(value ?? null);
195
+ }, [isOpen, value]);
196
+
197
+ const label = React.useMemo(() => {
198
+ const f = draft?.from ?? value?.from ?? null;
199
+ const t = draft?.to ?? value?.to ?? null;
200
+ if (format) return format(f ?? null, t ?? null);
201
+ const fs = f ? (hourCycle === 12
202
+ ? `${((f.getHours() % 12) || 12)}:${pad2(f.getMinutes())}${precision === 'second' ? `:${pad2(f.getSeconds())}` : ''} ${f.getHours() >= 12 ? 'PM' : 'AM'}`
203
+ : `${pad2(f.getHours())}:${pad2(f.getMinutes())}${precision === 'second' ? `:${pad2(f.getSeconds())}` : ''}`) : null;
204
+ const ts = t ? (hourCycle === 12
205
+ ? `${((t.getHours() % 12) || 12)}:${pad2(t.getMinutes())}${precision === 'second' ? `:${pad2(t.getSeconds())}` : ''} ${t.getHours() >= 12 ? 'PM' : 'AM'}`
206
+ : `${pad2(t.getHours())}:${pad2(t.getMinutes())}${precision === 'second' ? `:${pad2(t.getSeconds())}` : ''}`) : null;
207
+ return fs && ts ? `${fs} – ${ts}` : placeholder;
208
+ }, [draft, value, format, hourCycle, precision, placeholder]);
209
+
210
+ return (
211
+ <div className={cn('w-fit', className)} {...props}>
212
+ <Popover open={isOpen} onOpenChange={setOpen}>
213
+ <PopoverTrigger asChild>
214
+ <Button
215
+ type="button"
216
+ disabled={disabled}
217
+ variant={buttonVariant}
218
+ className={cn('w-[280px] justify-start text-left font-normal', !value && 'text-muted-foreground')}
219
+ >
220
+ <Clock className="mr-2 h-4 w-4" />
221
+ {label}
222
+ </Button>
223
+ </PopoverTrigger>
224
+ <PopoverContent className="p-3 w-auto" align="start">
225
+ <div className="space-y-4">
226
+ <TimeUnitSelector
227
+ label="From"
228
+ value={draft?.from ?? null}
229
+ onChange={(d) => setDraft((prev) => ({ ...(prev ?? {}), from: d }))}
230
+ hourCycle={hourCycle}
231
+ precision={precision}
232
+ minuteStep={minuteStep}
233
+ secondStep={secondStep}
234
+ disabled={disabled}
235
+ />
236
+ <TimeUnitSelector
237
+ label="To"
238
+ value={draft?.to ?? null}
239
+ onChange={(d) => setDraft((prev) => ({ ...(prev ?? {}), to: d }))}
240
+ hourCycle={hourCycle}
241
+ precision={precision}
242
+ minuteStep={minuteStep}
243
+ secondStep={secondStep}
244
+ disabled={disabled}
245
+ />
246
+ </div>
247
+
248
+ {(props.showFooter ?? true) && (
249
+ <div className="flex items-center justify-between gap-2 pt-3 mt-3 border-t">
250
+ <Button
251
+ type="button"
252
+ variant="outline"
253
+ size="sm"
254
+ onClick={() => setOpen(false)}
255
+ disabled={disabled}
256
+ >
257
+ {props.cancelLabel ?? 'Cancel'}
258
+ </Button>
259
+ <div className="flex gap-2">
260
+ <Button
261
+ type="button"
262
+ variant="ghost"
263
+ size="sm"
264
+ onClick={() => onChange?.(null)}
265
+ disabled={disabled}
266
+ >
267
+ {props.clearLabel ?? 'Clear'}
268
+ </Button>
269
+ <Button
270
+ type="button"
271
+ variant="default"
272
+ size="sm"
273
+ onClick={() => {
274
+ onChange?.(draft ?? null);
275
+ setOpen(false);
276
+ }}
277
+ >
278
+ {props.applyLabel ?? 'Apply'}
279
+ </Button>
280
+ </div>
281
+ </div>
282
+ )}
283
+ </PopoverContent>
284
+ </Popover>
285
+ </div>
286
+ );
287
+ }
288
+
289
+ TimeRangePicker.displayName = 'TimeRangePicker';
290
+
291
+ export default TimeRangePicker;
@@ -0,0 +1,3 @@
1
+ export { TimePicker } from './TimePicker'
2
+ export { TimeRangePicker } from './TimeRangePicker'
3
+ export default void 0
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { FormBuilder, type FormBuilderProps } from '../../../kit/builder/form/components/FormBuilder'
3
+
4
+ const meta: Meta<typeof FormBuilder> = {
5
+ title: 'Kit/Builder/Form (DateTime)',
6
+ component: FormBuilder,
7
+ parameters: {
8
+ controls: { expanded: true },
9
+ backgrounds: { disable: true },
10
+ },
11
+ }
12
+
13
+ export default meta
14
+
15
+ type Story = StoryObj<typeof FormBuilder>
16
+
17
+ export const DateTimeFields: Story = {
18
+ name: 'Date/Time fields',
19
+ args: {
20
+ sections: [
21
+ {
22
+ title: 'Date & Time',
23
+ description: 'Examples of DateTimePicker, DateTimeRangePicker and TimeRangePicker fields',
24
+ variant: 'card',
25
+ layout: 'grid',
26
+ grid: { cols: 1, mdCols: 2, gap: 'gap-4' },
27
+ fields: [
28
+ {
29
+ name: 'dt',
30
+ label: 'Date & Time',
31
+ type: 'date_time',
32
+ timePrecision: 'minute',
33
+ hourCycle: 24,
34
+ },
35
+ {
36
+ name: 'dtRange',
37
+ label: 'Date & Time Range',
38
+ type: 'date_time_range',
39
+ numberOfMonths: 2,
40
+ timePrecision: 'minute',
41
+ hourCycle: 24,
42
+ },
43
+ {
44
+ name: 'timeRange',
45
+ label: 'Time Range',
46
+ type: 'time_range',
47
+ timePrecision: 'minute',
48
+ hourCycle: 24,
49
+ minuteStep: 5,
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ onSubmit: (data: unknown) => {
55
+ // Showing output in console for demo
56
+ // eslint-disable-next-line no-console
57
+ console.log('Submit (date/time):', data)
58
+ },
59
+ showActions: true,
60
+ } satisfies Partial<FormBuilderProps>,
61
+ render: (args) => (
62
+ <div className="max-w-5xl mx-auto p-6">
63
+ <FormBuilder {...(args as FormBuilderProps)} />
64
+ </div>
65
+ ),
66
+ }