kmod-cli 1.0.10

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 (66) hide show
  1. package/README.md +53 -0
  2. package/bin/gen-components.js +68 -0
  3. package/bin/index.js +153 -0
  4. package/component-templates/components/access-denied.tsx +130 -0
  5. package/component-templates/components/breadcumb.tsx +42 -0
  6. package/component-templates/components/count-down.tsx +94 -0
  7. package/component-templates/components/count-input.tsx +221 -0
  8. package/component-templates/components/date-range-calendar/button.tsx +61 -0
  9. package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
  10. package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
  11. package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
  12. package/component-templates/components/date-range-calendar/label.tsx +31 -0
  13. package/component-templates/components/date-range-calendar/popover.tsx +32 -0
  14. package/component-templates/components/date-range-calendar/select.tsx +125 -0
  15. package/component-templates/components/date-range-calendar/switch.tsx +30 -0
  16. package/component-templates/components/datetime-picker/button.tsx +61 -0
  17. package/component-templates/components/datetime-picker/calendar.tsx +156 -0
  18. package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
  19. package/component-templates/components/datetime-picker/input.tsx +20 -0
  20. package/component-templates/components/datetime-picker/label.tsx +18 -0
  21. package/component-templates/components/datetime-picker/period-input.tsx +62 -0
  22. package/component-templates/components/datetime-picker/popover.tsx +32 -0
  23. package/component-templates/components/datetime-picker/select.tsx +125 -0
  24. package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
  25. package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
  26. package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
  27. package/component-templates/components/gradient-outline.tsx +233 -0
  28. package/component-templates/components/gradient-svg.tsx +157 -0
  29. package/component-templates/components/grid-layout.tsx +69 -0
  30. package/component-templates/components/hydrate-guard.tsx +40 -0
  31. package/component-templates/components/image.tsx +92 -0
  32. package/component-templates/components/loader-slash-gradient.tsx +85 -0
  33. package/component-templates/components/masonry-gallery.tsx +221 -0
  34. package/component-templates/components/modal.tsx +110 -0
  35. package/component-templates/components/multi-select.tsx +447 -0
  36. package/component-templates/components/non-hydration.tsx +27 -0
  37. package/component-templates/components/portal.tsx +34 -0
  38. package/component-templates/components/segments-circle.tsx +235 -0
  39. package/component-templates/components/single-select.tsx +248 -0
  40. package/component-templates/components/stroke-circle.tsx +57 -0
  41. package/component-templates/components/table/column-table.tsx +15 -0
  42. package/component-templates/components/table/data-table.tsx +339 -0
  43. package/component-templates/components/table/readme.tsx +95 -0
  44. package/component-templates/components/table/table.tsx +60 -0
  45. package/component-templates/components/text-hover-effect.tsx +120 -0
  46. package/component-templates/components/timout-loader.tsx +52 -0
  47. package/component-templates/components/toast.tsx +994 -0
  48. package/component-templates/configs/config.ts +33 -0
  49. package/component-templates/configs/feature-config.tsx +432 -0
  50. package/component-templates/configs/keys.ts +7 -0
  51. package/component-templates/core/api-service.ts +202 -0
  52. package/component-templates/core/calculate.ts +18 -0
  53. package/component-templates/core/idb.ts +166 -0
  54. package/component-templates/core/storage.ts +213 -0
  55. package/component-templates/hooks/count-down.ts +38 -0
  56. package/component-templates/hooks/fade-on-scroll.ts +52 -0
  57. package/component-templates/hooks/safe-action.ts +59 -0
  58. package/component-templates/hooks/spam-guard.ts +31 -0
  59. package/component-templates/lib/utils.ts +6 -0
  60. package/component-templates/providers/feature-guard.tsx +432 -0
  61. package/component-templates/queries/query.tsx +775 -0
  62. package/component-templates/utils/colors/color-by-text.ts +307 -0
  63. package/component-templates/utils/colors/stripe-effect.ts +100 -0
  64. package/component-templates/utils/hash/hash-aes.ts +35 -0
  65. package/components.json +348 -0
  66. package/package.json +60 -0
@@ -0,0 +1,221 @@
1
+ "use client";
2
+ import React, {
3
+ HTMLAttributes,
4
+ useRef,
5
+ useState,
6
+ } from 'react';
7
+
8
+ import {
9
+ Minus,
10
+ Plus,
11
+ } from 'lucide-react';
12
+
13
+ import { cn } from '../lib/utils';
14
+
15
+ export type CountingProps = {
16
+ initialValue?: number;
17
+ min?: number;
18
+ max?: number;
19
+ durations?: number; // delay every step when holding
20
+ steps?: number;
21
+ holdDelay?: number; // delay before starting to hold
22
+ activeHolding?: boolean;
23
+ activeIncrement?: boolean;
24
+ activeDecrement?: boolean;
25
+ onIncrement?: (value?: number, changeStatus?: "active" | "freeze") => void;
26
+ onDecrement?: (value?: number, changeStatus?: "active" | "freeze") => void;
27
+ onValueChange?: (
28
+ value: number,
29
+ changeStatus?: "inc" | "dec" | "freeze"
30
+ ) => void;
31
+ inputClass?: HTMLAttributes<HTMLInputElement>["className"];
32
+ decreaseClass?: HTMLAttributes<HTMLButtonElement>["className"];
33
+ increaseClass?: HTMLAttributes<HTMLButtonElement>["className"];
34
+ containerClass?: HTMLAttributes<HTMLDivElement>["className"];
35
+ inputWrapperClass?: HTMLAttributes<HTMLDivElement>["className"];
36
+ inputProps?: HTMLAttributes<HTMLInputElement>;
37
+ decreaseProps?: HTMLAttributes<HTMLButtonElement>;
38
+ increaseProps?: HTMLAttributes<HTMLButtonElement>;
39
+ containerProps?: HTMLAttributes<HTMLDivElement>;
40
+ inputWrapperProps?: HTMLAttributes<HTMLDivElement>;
41
+ increaseChildren?: React.ReactNode;
42
+ decreaseChildren?: React.ReactNode;
43
+ beforeInputRender?: () => React.ReactNode; // Optional prop to render before the input
44
+ afterInputRender?: () => React.ReactNode; // Optional prop to render after the input
45
+ }
46
+
47
+ export const Counting: React.FC<CountingProps> = ({
48
+ initialValue = 0,
49
+ min = 0,
50
+ max = 100,
51
+ durations = 100,
52
+ steps = 1,
53
+ holdDelay = 500,
54
+ activeHolding = true,
55
+ activeIncrement = true,
56
+ activeDecrement = true,
57
+ onIncrement,
58
+ onDecrement,
59
+ onValueChange,
60
+ inputClass,
61
+ decreaseClass,
62
+ increaseClass,
63
+ containerClass,
64
+ inputWrapperClass,
65
+ inputProps,
66
+ decreaseProps,
67
+ increaseProps,
68
+ containerProps,
69
+ inputWrapperProps,
70
+ increaseChildren = <Plus size={20} />,
71
+ decreaseChildren = <Minus size={20} />,
72
+ beforeInputRender,
73
+ afterInputRender,
74
+ }) => {
75
+ const [value, setValue] = useState(initialValue);
76
+ const [inputValue, setInputValue] = useState(initialValue.toString());
77
+ const isHolding = useRef(false);
78
+ const holdTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
79
+ const holdStarted = useRef(false);
80
+
81
+ const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms));
82
+
83
+ const stopContinuousChange = (type: "inc" | "dec") => {
84
+ isHolding.current = false;
85
+ holdStarted.current = false;
86
+ if (holdTimeout.current) clearTimeout(holdTimeout.current);
87
+
88
+ if (type === "inc") onIncrement?.(value, "freeze");
89
+ if (type === "dec") onDecrement?.(value, "freeze");
90
+ onValueChange?.(value, "freeze");
91
+ };
92
+
93
+ const startContinuousChange = async (type: "inc" | "dec") => {
94
+ if (!isHolding.current) return;
95
+ while (isHolding.current) {
96
+ setValue((prev) => {
97
+ let next = prev;
98
+ if (type === "inc" && prev < max) next = Math.min(prev + steps, max);
99
+ if (type === "dec" && prev > min) next = Math.max(prev - steps, min);
100
+
101
+ // callback
102
+ if (type === "inc" && next !== prev && activeIncrement)
103
+ onIncrement?.(next, "active");
104
+ if (type === "dec" && next !== prev && activeDecrement)
105
+ onDecrement?.(next, "active");
106
+ if (next !== prev)
107
+ onValueChange?.(next, type === "inc" ? "inc" : "dec");
108
+
109
+ setInputValue(next.toString()); // đồng bộ input
110
+ return next;
111
+ });
112
+
113
+ await sleep(durations);
114
+ }
115
+ };
116
+
117
+ const handleMouseDown = (type: "inc" | "dec") => {
118
+ if (!activeHolding) return;
119
+ isHolding.current = true;
120
+
121
+ holdTimeout.current = setTimeout(() => {
122
+ holdStarted.current = true;
123
+ startContinuousChange(type);
124
+ }, holdDelay);
125
+ };
126
+
127
+ const handleMouseUp = (type: "inc" | "dec") => {
128
+ if (!holdStarted.current) {
129
+ // nhấn nháy đơn
130
+ if (type === "inc" && value < max) {
131
+ const next = Math.min(value + steps, max);
132
+ setValue(next);
133
+ setInputValue(next.toString());
134
+ activeIncrement && onIncrement?.(next, "active");
135
+ onValueChange?.(next, "inc");
136
+ }
137
+ if (type === "dec" && value > min) {
138
+ const next = Math.max(value - steps, min);
139
+ setValue(next);
140
+ setInputValue(next.toString());
141
+ activeDecrement && onDecrement?.(next, "active");
142
+ onValueChange?.(next, "dec");
143
+ }
144
+ }
145
+ stopContinuousChange(type);
146
+ };
147
+
148
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
149
+ const val = e.target.value;
150
+ if (/^\d*$/.test(val)) {
151
+ setInputValue(val);
152
+ const num = parseInt(val, 10);
153
+ if (!isNaN(num)) {
154
+ if (num >= min && num <= max) {
155
+ setValue(num);
156
+ if (num > value) onIncrement?.(num, "active");
157
+ if (num < value) onDecrement?.(num, "active");
158
+ if (num !== value) onValueChange?.(num, num > value ? "inc" : "dec");
159
+ }
160
+ }
161
+ }
162
+ };
163
+
164
+ const handleBlur = () => {
165
+ // đồng bộ lại input với value
166
+ setInputValue(value.toString());
167
+ onValueChange?.(value, "freeze");
168
+ };
169
+
170
+ return (
171
+ <div
172
+ {...containerProps}
173
+ className={cn(
174
+ "flex items-center border border-[#DDDDE3] h-7 rounded-[4px] overflow-hidden",
175
+ containerClass
176
+ )}
177
+ >
178
+ <button
179
+ onMouseDown={() => handleMouseDown("dec")}
180
+ onMouseUp={() => handleMouseUp("dec")}
181
+ onMouseLeave={() => stopContinuousChange("dec")}
182
+ className={cn(
183
+ "h-full bg-white w-7 flex items-center justify-center",
184
+ decreaseClass
185
+ )}
186
+ disabled={value <= min}
187
+ {...decreaseProps}
188
+ >
189
+ {decreaseChildren || <Minus size={20} />}
190
+ </button>
191
+ <div className={cn(inputWrapperClass)} {...inputWrapperProps}>
192
+ {beforeInputRender && beforeInputRender()}
193
+ <input
194
+ type="text"
195
+ value={inputValue}
196
+ onChange={handleChange}
197
+ onBlur={handleBlur}
198
+ className={cn(
199
+ "w-10 h-full text-center bg-white text-sm font-medium focus:outline-none border-l border-r border-[#DDDDE3]",
200
+ inputClass
201
+ )}
202
+ {...inputProps}
203
+ />
204
+ {afterInputRender && afterInputRender()}
205
+ </div>
206
+ <button
207
+ onMouseDown={() => handleMouseDown("inc")}
208
+ onMouseUp={() => handleMouseUp("inc")}
209
+ onMouseLeave={() => stopContinuousChange("inc")}
210
+ className={cn(
211
+ "h-full bg-white w-7 flex items-center justify-center",
212
+ increaseClass
213
+ )}
214
+ disabled={value >= max}
215
+ {...increaseProps}
216
+ >
217
+ {increaseChildren || <Plus size={20} />}
218
+ </button>
219
+ </div>
220
+ );
221
+ };
@@ -0,0 +1,61 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ cva,
5
+ type VariantProps,
6
+ } from 'class-variance-authority';
7
+
8
+ import { Slot } from '@radix-ui/react-slot';
9
+
10
+ import { cn } from '../../lib/utils';
11
+
12
+ const buttonVariants = cva(
13
+ 'inline-flex items-center justify-center rounded-full text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: 'bg-black text-white',
18
+ destructive:
19
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
20
+ outline:
21
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
22
+ secondary:
23
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
24
+ ghost: 'hover:bg-hover-secondary hover:text-accent-foreground',
25
+ link: 'text-primary underline-offset-4 hover:underline'
26
+ },
27
+ size: {
28
+ default: 'h-10 px-4 py-2',
29
+ sm: 'h-9 rounded-full px-3',
30
+ lg: 'h-11 rounded-full px-8',
31
+ icon: 'h-10 w-10'
32
+ }
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ size: 'default'
37
+ }
38
+ }
39
+ )
40
+
41
+ export interface ButtonProps
42
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
43
+ VariantProps<typeof buttonVariants> {
44
+ asChild?: boolean
45
+ }
46
+
47
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
48
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
49
+ const Comp = asChild ? Slot : 'button'
50
+ return (
51
+ <Comp
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ ref={ref}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+ )
59
+ Button.displayName = 'Button'
60
+
61
+ export { Button, buttonVariants };
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import React, { JSX } from 'react';
4
+
5
+ import {
6
+ ChevronDownIcon,
7
+ ChevronLeftIcon,
8
+ ChevronRightIcon,
9
+ } from 'lucide-react';
10
+ import {
11
+ DayButton,
12
+ DayPicker,
13
+ getDefaultClassNames,
14
+ } from 'react-day-picker';
15
+
16
+ import { cn } from '../../lib/utils';
17
+ import {
18
+ Button,
19
+ buttonVariants,
20
+ } from './button';
21
+
22
+ {}
23
+
24
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>
25
+
26
+ function Calendar ({
27
+ className,
28
+ classNames,
29
+ showOutsideDays = true,
30
+ ...props
31
+ }: CalendarProps): JSX.Element {
32
+ return (
33
+ <DayPicker
34
+ showOutsideDays={showOutsideDays}
35
+ className={cn('p-3', className)}
36
+ classNames={{
37
+ months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
38
+ month: 'space-y-4',
39
+ caption: 'flex justify-center pt-1 relative items-center',
40
+ caption_label: 'text-sm font-medium',
41
+ nav: 'space-x-1 flex items-center',
42
+ nav_button: cn(
43
+ buttonVariants({ variant: 'outline' }),
44
+ 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
45
+ ),
46
+ nav_button_previous: 'absolute left-1',
47
+ nav_button_next: 'absolute right-1',
48
+ table: 'w-full border-collapse space-y-1',
49
+ head_row: 'flex',
50
+ head_cell:
51
+ 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
52
+ row: 'flex w-full mt-2 rounded-lg overflow-hidden',
53
+ cell: 'text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
54
+ day: cn(
55
+ buttonVariants({ variant: 'ghost' }),
56
+ 'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
57
+ ),
58
+ day_selected:
59
+ 'bg-indigo-500 hover:bg-indigo-500 focus:bg-indigo-500 text-white rounded-md',
60
+ day_today: 'bg-accent text-accent-foreground',
61
+ day_outside: 'text-muted-foreground opacity-40 invisible',
62
+ day_disabled: 'text-muted-foreground opacity-40',
63
+ day_range_middle:
64
+ 'aria-selected:bg-indigo-100 aria-selected:text-indigo-800 rounded-none',
65
+ day_hidden: 'invisible',
66
+ // weekday: 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
67
+ ...classNames
68
+ }}
69
+ components={{
70
+ Root: ({ className, rootRef, ...props }) => {
71
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
72
+ },
73
+ Chevron: ({ className, orientation, ...props }) => {
74
+ if (orientation === "left") {
75
+ return <Button variant="ghost" size="icon" className="hover:!bg-muted"><ChevronLeftIcon className={cn("size-4", className)} {...props} /></Button>;
76
+ }
77
+
78
+ if (orientation === "right") {
79
+ return <Button variant="ghost" size="icon" className="hover:!bg-muted"><ChevronRightIcon className={cn("size-4", className)} {...props} /></Button>;
80
+ }
81
+
82
+ return <Button variant="ghost" size="icon" className="hover:!bg-muted"><ChevronDownIcon className={cn("size-4", className)} {...props} /></Button>;
83
+ },
84
+ DayButton: CalendarDayButton,
85
+ WeekNumber: ({ children, ...props }) => {
86
+ return (
87
+ <td {...props}>
88
+ <div className="flex size-[--cell-size] items-center justify-center text-center">{children}</div>
89
+ </td>
90
+ );
91
+ },
92
+ Weekdays: ({ children, ...props }) => {
93
+ return <div className="flex px-1 justify-between">
94
+ {children}
95
+ </div>;
96
+ }
97
+ }}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+ Calendar.displayName = 'Calendar'
103
+
104
+ export { Calendar };
105
+
106
+ export function CalendarDayButton({ className, day, modifiers, ...props }: React.ComponentProps<typeof DayButton>) {
107
+ const defaultClassNames = getDefaultClassNames();
108
+
109
+ const ref = React.useRef<HTMLButtonElement>(null);
110
+ React.useEffect(() => {
111
+ if (modifiers.focused) ref.current?.focus();
112
+ }, [modifiers.focused]);
113
+
114
+ return (
115
+ <Button
116
+ ref={ref}
117
+ variant="ghost"
118
+ size="icon"
119
+ data-day={day.date.toLocaleDateString()}
120
+ data-selected-single={modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle}
121
+ data-range-start={modifiers.range_start}
122
+ data-range-end={modifiers.range_end}
123
+ data-range-middle={modifiers.range_middle}
124
+ className={cn(
125
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md [&>span]:text-xs [&>span]:opacity-70",
126
+ defaultClassNames.day,
127
+ className,
128
+ )}
129
+ {...props}
130
+ />
131
+ );
132
+ }
@@ -0,0 +1,259 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+
3
+ interface DateInputProps {
4
+ value?: Date
5
+ onChange: (date: Date) => void
6
+ }
7
+
8
+ interface DateParts {
9
+ day: number
10
+ month: number
11
+ year: number
12
+ }
13
+
14
+ const DateInput: React.FC<DateInputProps> = ({ value, onChange }) => {
15
+ const [date, setDate] = React.useState<DateParts>(() => {
16
+ const d = value ? new Date(value) : new Date()
17
+ return {
18
+ day: d.getDate(),
19
+ month: d.getMonth() + 1, // JavaScript months are 0-indexed
20
+ year: d.getFullYear()
21
+ }
22
+ })
23
+
24
+ const monthRef = useRef<HTMLInputElement | null>(null)
25
+ const dayRef = useRef<HTMLInputElement | null>(null)
26
+ const yearRef = useRef<HTMLInputElement | null>(null)
27
+
28
+ useEffect(() => {
29
+ const d = value ? new Date(value) : new Date()
30
+ setDate({
31
+ day: d.getDate(),
32
+ month: d.getMonth() + 1,
33
+ year: d.getFullYear()
34
+ })
35
+ }, [value])
36
+
37
+ const validateDate = (field: keyof DateParts, value: number): boolean => {
38
+ if (
39
+ (field === 'day' && (value < 1 || value > 31)) ||
40
+ (field === 'month' && (value < 1 || value > 12)) ||
41
+ (field === 'year' && (value < 1000 || value > 9999))
42
+ ) {
43
+ return false
44
+ }
45
+
46
+ // Validate the day of the month
47
+ const newDate = { ...date, [field]: value }
48
+ const d = new Date(newDate.year, newDate.month - 1, newDate.day)
49
+ return d.getFullYear() === newDate.year &&
50
+ d.getMonth() + 1 === newDate.month &&
51
+ d.getDate() === newDate.day
52
+ }
53
+
54
+ const handleInputChange =
55
+ (field: keyof DateParts) => (e: React.ChangeEvent<HTMLInputElement>) => {
56
+ const newValue = e.target.value ? Number(e.target.value) : ''
57
+ const isValid = typeof newValue === 'number' && validateDate(field, newValue)
58
+
59
+ // If the new value is valid, update the date
60
+ const newDate = { ...date, [field]: newValue }
61
+ setDate(newDate)
62
+
63
+ // only call onChange when the entry is valid
64
+ if (isValid) {
65
+ onChange(new Date(newDate.year, newDate.month - 1, newDate.day))
66
+ }
67
+ }
68
+
69
+ const initialDate = useRef<DateParts>(date)
70
+
71
+ const handleBlur = (field: keyof DateParts) => (
72
+ e: React.FocusEvent<HTMLInputElement>
73
+ ): void => {
74
+ if (!e.target.value) {
75
+ setDate(initialDate.current)
76
+ return
77
+ }
78
+
79
+ const newValue = Number(e.target.value)
80
+ const isValid = validateDate(field, newValue)
81
+
82
+ if (!isValid) {
83
+ setDate(initialDate.current)
84
+ } else {
85
+ // If the new value is valid, update the initial value
86
+ initialDate.current = { ...date, [field]: newValue }
87
+ }
88
+ }
89
+
90
+ const handleKeyDown =
91
+ (field: keyof DateParts) => (e: React.KeyboardEvent<HTMLInputElement>) => {
92
+ // Allow command (or control) combinations
93
+ if (e.metaKey || e.ctrlKey) {
94
+ return
95
+ }
96
+
97
+ // Prevent non-numeric characters, excluding allowed keys
98
+ if (
99
+ !/^[0-9]$/.test(e.key) &&
100
+ ![
101
+ 'ArrowUp',
102
+ 'ArrowDown',
103
+ 'ArrowLeft',
104
+ 'ArrowRight',
105
+ 'Delete',
106
+ 'Tab',
107
+ 'Backspace',
108
+ 'Enter'
109
+ ].includes(e.key)
110
+ ) {
111
+ e.preventDefault()
112
+ return
113
+ }
114
+
115
+ if (e.key === 'ArrowUp') {
116
+ e.preventDefault()
117
+ let newDate = { ...date }
118
+
119
+ if (field === 'day') {
120
+ if (date[field] === new Date(date.year, date.month, 0).getDate()) {
121
+ newDate = { ...newDate, day: 1, month: (date.month % 12) + 1 }
122
+ if (newDate.month === 1) newDate.year += 1
123
+ } else {
124
+ newDate.day += 1
125
+ }
126
+ }
127
+
128
+ if (field === 'month') {
129
+ if (date[field] === 12) {
130
+ newDate = { ...newDate, month: 1, year: date.year + 1 }
131
+ } else {
132
+ newDate.month += 1
133
+ }
134
+ }
135
+
136
+ if (field === 'year') {
137
+ newDate.year += 1
138
+ }
139
+
140
+ setDate(newDate)
141
+ onChange(new Date(newDate.year, newDate.month - 1, newDate.day))
142
+ } else if (e.key === 'ArrowDown') {
143
+ e.preventDefault()
144
+ let newDate = { ...date }
145
+
146
+ if (field === 'day') {
147
+ if (date[field] === 1) {
148
+ newDate.month -= 1
149
+ if (newDate.month === 0) {
150
+ newDate.month = 12
151
+ newDate.year -= 1
152
+ }
153
+ newDate.day = new Date(newDate.year, newDate.month, 0).getDate()
154
+ } else {
155
+ newDate.day -= 1
156
+ }
157
+ }
158
+
159
+ if (field === 'month') {
160
+ if (date[field] === 1) {
161
+ newDate = { ...newDate, month: 12, year: date.year - 1 }
162
+ } else {
163
+ newDate.month -= 1
164
+ }
165
+ }
166
+
167
+ if (field === 'year') {
168
+ newDate.year -= 1
169
+ }
170
+
171
+ setDate(newDate)
172
+ onChange(new Date(newDate.year, newDate.month - 1, newDate.day))
173
+ }
174
+
175
+ if (e.key === 'ArrowRight') {
176
+ if (
177
+ e.currentTarget.selectionStart === e.currentTarget.value.length ||
178
+ (e.currentTarget.selectionStart === 0 &&
179
+ e.currentTarget.selectionEnd === e.currentTarget.value.length)
180
+ ) {
181
+ e.preventDefault()
182
+ if (field === 'month') dayRef.current?.focus()
183
+ if (field === 'day') yearRef.current?.focus()
184
+ }
185
+ } else if (e.key === 'ArrowLeft') {
186
+ if (
187
+ e.currentTarget.selectionStart === 0 ||
188
+ (e.currentTarget.selectionStart === 0 &&
189
+ e.currentTarget.selectionEnd === e.currentTarget.value.length)
190
+ ) {
191
+ e.preventDefault()
192
+ if (field === 'day') monthRef.current?.focus()
193
+ if (field === 'year') dayRef.current?.focus()
194
+ }
195
+ }
196
+ }
197
+
198
+ return (
199
+ <div className="flex border rounded-lg items-center text-sm px-1">
200
+ <input
201
+ type="text"
202
+ ref={monthRef}
203
+ max={12}
204
+ maxLength={2}
205
+ value={date.month.toString()}
206
+ onChange={handleInputChange('month')}
207
+ onKeyDown={handleKeyDown('month')}
208
+ onFocus={(e) => {
209
+ if (window.innerWidth > 1024) {
210
+ e.target.select()
211
+ }
212
+ }}
213
+ onBlur={handleBlur('month')}
214
+ className="p-0 outline-none w-6 border-none text-center"
215
+ placeholder="M"
216
+ />
217
+ <span className="opacity-20 -mx-px">/</span>
218
+ <input
219
+ type="text"
220
+ ref={dayRef}
221
+ max={31}
222
+ maxLength={2}
223
+ value={date.day.toString()}
224
+ onChange={handleInputChange('day')}
225
+ onKeyDown={handleKeyDown('day')}
226
+ onFocus={(e) => {
227
+ if (window.innerWidth > 1024) {
228
+ e.target.select()
229
+ }
230
+ }}
231
+ onBlur={handleBlur('day')}
232
+ className="p-0 outline-none w-7 border-none text-center"
233
+ placeholder="D"
234
+ />
235
+ <span className="opacity-20 -mx-px">/</span>
236
+ <input
237
+ type="text"
238
+ ref={yearRef}
239
+ max={9999}
240
+ maxLength={4}
241
+ value={date.year.toString()}
242
+ onChange={handleInputChange('year')}
243
+ onKeyDown={handleKeyDown('year')}
244
+ onFocus={(e) => {
245
+ if (window.innerWidth > 1024) {
246
+ e.target.select()
247
+ }
248
+ }}
249
+ onBlur={handleBlur('year')}
250
+ className="p-0 outline-none w-12 border-none text-center"
251
+ placeholder="YYYY"
252
+ />
253
+ </div>
254
+ )
255
+ }
256
+
257
+ DateInput.displayName = 'DateInput'
258
+
259
+ export { DateInput }