@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,30 @@
1
+ import * as React from 'react'
2
+ import type { FieldRenderProps } from './types'
3
+ import { TimePicker } from '../../../../components/timepicker/TimePicker'
4
+
5
+ export function TimePickerField({ field, value, onChange, className }: FieldRenderProps) {
6
+ const v = React.useMemo(() => {
7
+ if (!value) return null
8
+ if (value instanceof Date) return value
9
+ const d = new Date(value as string)
10
+ return Number.isNaN(d.getTime()) ? null : d
11
+ }, [value])
12
+
13
+ return (
14
+ <TimePicker
15
+ className={className}
16
+ value={v}
17
+ onChange={(d: Date | null) => onChange(d)}
18
+ placeholder={field.placeholder}
19
+ precision={field.timePrecision ?? 'minute'}
20
+ hourCycle={field.hourCycle ?? 24}
21
+ minuteStep={field.minuteStep ?? 5}
22
+ secondStep={field.secondStep ?? 5}
23
+ showFooter={field.showFooter}
24
+ cancelLabel={field.cancelLabel}
25
+ applyLabel={field.applyLabel}
26
+ />
27
+ )
28
+ }
29
+
30
+ export default TimePickerField
@@ -0,0 +1,37 @@
1
+ import * as React from 'react'
2
+ import type { FieldRenderProps } from './types'
3
+ import { TimeRangePicker } from '../../../../components/timepicker/TimeRangePicker'
4
+
5
+ export function TimeRangePickerField({ field, value, onChange, className }: FieldRenderProps) {
6
+ const v = React.useMemo(() => {
7
+ if (!value || typeof value !== 'object') return null as { from?: Date | null; to?: Date | null } | null
8
+ const anyVal = value as { from?: unknown; to?: unknown }
9
+ const toDate = (x: unknown) => {
10
+ if (!x) return undefined
11
+ if (x instanceof Date) return x
12
+ const d = new Date(x as string)
13
+ return Number.isNaN(d.getTime()) ? undefined : d
14
+ }
15
+ const from = toDate(anyVal.from) ?? null
16
+ const to = toDate(anyVal.to) ?? null
17
+ return { from, to }
18
+ }, [value])
19
+
20
+ return (
21
+ <TimeRangePicker
22
+ className={className}
23
+ value={v}
24
+ onChange={(range) => onChange(range)}
25
+ placeholder={field.placeholder}
26
+ precision={field.timePrecision ?? 'minute'}
27
+ hourCycle={field.hourCycle ?? 24}
28
+ minuteStep={field.minuteStep ?? 5}
29
+ secondStep={field.secondStep ?? 5}
30
+ showFooter={field.showFooter}
31
+ cancelLabel={field.cancelLabel}
32
+ applyLabel={field.applyLabel}
33
+ />
34
+ )
35
+ }
36
+
37
+ export default TimeRangePickerField
@@ -12,6 +12,10 @@ export * from './DatePickerField'
12
12
  export * from './DateRangePickerField'
13
13
  export * from './MonthPickerField'
14
14
  export * from './MonthRangePickerField'
15
+ export * from './TimePickerField'
16
+ export * from './TimeRangePickerField'
17
+ export * from './DateTimePickerField'
18
+ export * from './DateTimeRangePickerField'
15
19
  export * from './FileField'
16
20
  export * from './ObjectField'
17
21
  export * from './ArrayField'
@@ -0,0 +1,314 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Calendar as CalendarIcon } 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 { Calendar } from '../../../shadcn/ui/calendar';
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '../../../shadcn/ui/select';
16
+
17
+ export type TimePrecision = 'hour' | 'minute' | 'second';
18
+
19
+ export interface DateTimePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
20
+ value?: Date | null;
21
+ onChange?: (date: Date | null) => void;
22
+ placeholder?: string;
23
+ disabled?: boolean;
24
+ minDate?: Date;
25
+ maxDate?: Date;
26
+ disabledDates?: Array<Date | { from: Date; to: Date }>;
27
+ // time config
28
+ timePrecision?: 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 startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
42
+ const pad2 = (n: number) => String(n).padStart(2, '0');
43
+ const isBefore = (date: Date, min?: Date) => !!(min && date < startOfDay(min));
44
+ const isAfter = (date: Date, max?: Date) => !!(max && date > startOfDay(max));
45
+ function sameDay(a: Date, b: Date) {
46
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
47
+ }
48
+ function inDisabled(date: Date, items?: Array<Date | { from: Date; to: Date }>) {
49
+ if (!items || items.length === 0) return false;
50
+ const d = startOfDay(date);
51
+ for (const it of items) {
52
+ if (it instanceof Date) {
53
+ if (sameDay(d, it)) return true;
54
+ } else if (it && 'from' in it && 'to' in it) {
55
+ const from = startOfDay(it.from);
56
+ const to = startOfDay(it.to);
57
+ if (d >= from && d <= to) return true;
58
+ }
59
+ }
60
+ return false;
61
+ }
62
+
63
+ function TimeSelectors({
64
+ value,
65
+ onChange,
66
+ precision,
67
+ hourCycle,
68
+ minuteStep,
69
+ secondStep,
70
+ disabled,
71
+ }: {
72
+ value: Date | null;
73
+ onChange: (val: Date | null) => void;
74
+ precision: TimePrecision;
75
+ hourCycle: 12 | 24;
76
+ minuteStep: number;
77
+ secondStep: number;
78
+ disabled?: boolean;
79
+ }) {
80
+ const hours = React.useMemo(() => (hourCycle === 12 ? Array.from({ length: 12 }, (_, i) => i + 1) : Array.from({ length: 24 }, (_, i) => i)), [hourCycle]);
81
+ const minutes = React.useMemo(() => Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => i * minuteStep), [minuteStep]);
82
+ const seconds = React.useMemo(() => Array.from({ length: Math.ceil(60 / secondStep) }, (_, i) => i * secondStep), [secondStep]);
83
+ const selectedHour = React.useMemo(() => {
84
+ if (!value) return hourCycle === 12 ? 12 : 0;
85
+ const h = value.getHours();
86
+ return hourCycle === 12 ? (h % 12 === 0 ? 12 : h % 12) : h;
87
+ }, [value, hourCycle]);
88
+ const selectedMinute = value?.getMinutes() ?? 0;
89
+ const selectedSecond = value?.getSeconds() ?? 0;
90
+ const selectedPeriod: 'AM' | 'PM' = value && value.getHours() >= 12 ? 'PM' : 'AM';
91
+
92
+ const setPart = (part: 'hour' | 'minute' | 'second' | 'period', v: number | 'AM' | 'PM') => {
93
+ const base = value
94
+ ? new Date(value)
95
+ : (() => {
96
+ const n = new Date();
97
+ return new Date(n.getFullYear(), n.getMonth(), n.getDate(), 0, 0, 0, 0);
98
+ })();
99
+ if (part === 'hour') {
100
+ let h = Number(v);
101
+ if (hourCycle === 12) {
102
+ const isPM = base.getHours() >= 12;
103
+ h = h % 12;
104
+ base.setHours(isPM ? (h === 12 ? 12 : h + 12) : (h === 12 ? 0 : h));
105
+ } else {
106
+ base.setHours(h);
107
+ }
108
+ } else if (part === 'minute') {
109
+ base.setMinutes(Number(v));
110
+ } else if (part === 'second') {
111
+ base.setSeconds(Number(v));
112
+ } else if (part === 'period' && (v === 'AM' || v === 'PM')) {
113
+ const curH = base.getHours();
114
+ const isAMNow = curH < 12;
115
+ if (v === 'AM' && !isAMNow) base.setHours(curH - 12);
116
+ if (v === 'PM' && isAMNow) base.setHours(curH + 12);
117
+ }
118
+ base.setMilliseconds(0);
119
+ onChange(base);
120
+ };
121
+
122
+ return (
123
+ <div className="flex items-end gap-2">
124
+ <div className="w-24">
125
+ <div className="mb-1 block text-xs text-muted-foreground">Hour</div>
126
+ <Select disabled={disabled} value={String(selectedHour)} onValueChange={(v) => setPart('hour', Number(v))}>
127
+ <SelectTrigger aria-label="Hour">
128
+ <SelectValue />
129
+ </SelectTrigger>
130
+ <SelectContent>
131
+ {hours.map((h) => (
132
+ <SelectItem key={h} value={String(h)}>{hourCycle === 12 ? h : pad2(h)}</SelectItem>
133
+ ))}
134
+ </SelectContent>
135
+ </Select>
136
+ </div>
137
+
138
+ {(precision === 'minute' || precision === 'second') && (
139
+ <div className="w-24">
140
+ <div className="mb-1 block text-xs text-muted-foreground">Minute</div>
141
+ <Select disabled={disabled} value={String(selectedMinute - (selectedMinute % minuteStep))} onValueChange={(v) => setPart('minute', Number(v))}>
142
+ <SelectTrigger aria-label="Minute">
143
+ <SelectValue />
144
+ </SelectTrigger>
145
+ <SelectContent>
146
+ {minutes.map((m) => (
147
+ <SelectItem key={m} value={String(m)}>{pad2(m)}</SelectItem>
148
+ ))}
149
+ </SelectContent>
150
+ </Select>
151
+ </div>
152
+ )}
153
+
154
+ {precision === 'second' && (
155
+ <div className="w-24">
156
+ <div className="mb-1 block text-xs text-muted-foreground">Second</div>
157
+ <Select disabled={disabled} value={String(selectedSecond - (selectedSecond % secondStep))} onValueChange={(v) => setPart('second', Number(v))}>
158
+ <SelectTrigger aria-label="Second">
159
+ <SelectValue />
160
+ </SelectTrigger>
161
+ <SelectContent>
162
+ {seconds.map((s) => (
163
+ <SelectItem key={s} value={String(s)}>{pad2(s)}</SelectItem>
164
+ ))}
165
+ </SelectContent>
166
+ </Select>
167
+ </div>
168
+ )}
169
+
170
+ {hourCycle === 12 && (
171
+ <div className="w-24">
172
+ <div className="mb-1 block text-xs text-muted-foreground">Period</div>
173
+ <Select disabled={disabled} value={selectedPeriod} onValueChange={(v) => setPart('period', v as 'AM' | 'PM')}>
174
+ <SelectTrigger aria-label="Period">
175
+ <SelectValue />
176
+ </SelectTrigger>
177
+ <SelectContent>
178
+ <SelectItem value="AM">AM</SelectItem>
179
+ <SelectItem value="PM">PM</SelectItem>
180
+ </SelectContent>
181
+ </Select>
182
+ </div>
183
+ )}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ export function DateTimePicker({
189
+ value,
190
+ onChange,
191
+ placeholder = 'Pick a date & time',
192
+ disabled,
193
+ minDate,
194
+ maxDate,
195
+ disabledDates,
196
+ timePrecision = 'minute',
197
+ hourCycle = 24,
198
+ minuteStep = 5,
199
+ secondStep = 5,
200
+ className,
201
+ buttonVariant = 'outline',
202
+ ...props
203
+ }: DateTimePickerProps) {
204
+ const [internalOpen, setInternalOpen] = React.useState(false);
205
+ const isOpen = typeof props.open === 'boolean' ? props.open : internalOpen;
206
+ const setOpen = (o: boolean) => (props.onOpenChange ? props.onOpenChange(o) : setInternalOpen(o));
207
+ const [draft, setDraft] = React.useState<Date | null>(value ?? null);
208
+
209
+ React.useEffect(() => {
210
+ if (isOpen) setDraft(value ?? null);
211
+ }, [isOpen, value]);
212
+
213
+ const isDisabled = (date: Date) => {
214
+ if (isBefore(date, minDate) || isAfter(date, maxDate)) return true;
215
+ if (inDisabled(date, disabledDates)) return true;
216
+ return false;
217
+ };
218
+
219
+ const fmtLabel = (d: Date | null): string => {
220
+ if (!d) return placeholder;
221
+ const dateStr = d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: '2-digit' });
222
+ const h = d.getHours();
223
+ const m = d.getMinutes();
224
+ const s = d.getSeconds();
225
+ const timeStr = hourCycle === 12
226
+ ? `${((h % 12) || 12)}:${pad2(m)}${timePrecision === 'second' ? `:${pad2(s)}` : ''} ${h >= 12 ? 'PM' : 'AM'}`
227
+ : `${pad2(h)}:${pad2(m)}${timePrecision === 'second' ? `:${pad2(s)}` : ''}`;
228
+ return `${dateStr} ${timeStr}`;
229
+ };
230
+
231
+ const label = fmtLabel(value ?? null);
232
+
233
+ return (
234
+ <div className={cn('w-fit', className)} {...props}>
235
+ <Popover open={isOpen} onOpenChange={setOpen}>
236
+ <PopoverTrigger asChild>
237
+ <Button
238
+ type="button"
239
+ disabled={disabled}
240
+ variant={buttonVariant}
241
+ className={cn('w-[280px] justify-start text-left font-normal', !value && 'text-muted-foreground')}
242
+ >
243
+ <CalendarIcon className="mr-2 h-4 w-4" />
244
+ {label}
245
+ </Button>
246
+ </PopoverTrigger>
247
+ <PopoverContent className="p-0" align="start">
248
+ <div className="p-3 space-y-3">
249
+ <Calendar
250
+ mode="single"
251
+ selected={draft ?? undefined}
252
+ onSelect={(d) => {
253
+ if (disabled) return;
254
+ if (!d) return;
255
+ if (isDisabled(d)) return;
256
+ // preserve time parts if exist
257
+ if (draft) {
258
+ const nd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), draft.getHours(), draft.getMinutes(), draft.getSeconds());
259
+ setDraft(nd);
260
+ } else {
261
+ setDraft(new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0));
262
+ }
263
+ }}
264
+ defaultMonth={draft ?? new Date()}
265
+ disabled={isDisabled}
266
+ buttonVariant="ghost"
267
+ showOutsideDays
268
+ />
269
+ <div>
270
+ <div className="mb-1 block text-xs text-muted-foreground">Time</div>
271
+ <TimeSelectors
272
+ value={draft}
273
+ onChange={(d) => setDraft(d)}
274
+ precision={timePrecision}
275
+ hourCycle={hourCycle}
276
+ minuteStep={minuteStep}
277
+ secondStep={secondStep}
278
+ disabled={disabled}
279
+ />
280
+ </div>
281
+ </div>
282
+
283
+ {(props.showFooter ?? true) && (
284
+ <div className="flex items-center justify-between gap-2 p-2 border-t">
285
+ <Button type="button" variant="outline" size="sm" onClick={() => setOpen(false)} disabled={disabled}>
286
+ {props.cancelLabel ?? 'Cancel'}
287
+ </Button>
288
+ <div className="flex gap-2">
289
+ <Button type="button" variant="ghost" size="sm" onClick={() => onChange?.(null)} disabled={disabled}>
290
+ {props.clearLabel ?? 'Clear'}
291
+ </Button>
292
+ <Button
293
+ type="button"
294
+ variant="default"
295
+ size="sm"
296
+ onClick={() => {
297
+ onChange?.(draft ?? null);
298
+ setOpen(false);
299
+ }}
300
+ >
301
+ {props.applyLabel ?? 'Apply'}
302
+ </Button>
303
+ </div>
304
+ </div>
305
+ )}
306
+ </PopoverContent>
307
+ </Popover>
308
+ </div>
309
+ );
310
+ }
311
+
312
+ DateTimePicker.displayName = 'DateTimePicker';
313
+
314
+ export default DateTimePicker;