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.
- package/README.md +53 -0
- package/bin/gen-components.js +68 -0
- package/bin/index.js +153 -0
- package/component-templates/components/access-denied.tsx +130 -0
- package/component-templates/components/breadcumb.tsx +42 -0
- package/component-templates/components/count-down.tsx +94 -0
- package/component-templates/components/count-input.tsx +221 -0
- package/component-templates/components/date-range-calendar/button.tsx +61 -0
- package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
- package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
- package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
- package/component-templates/components/date-range-calendar/label.tsx +31 -0
- package/component-templates/components/date-range-calendar/popover.tsx +32 -0
- package/component-templates/components/date-range-calendar/select.tsx +125 -0
- package/component-templates/components/date-range-calendar/switch.tsx +30 -0
- package/component-templates/components/datetime-picker/button.tsx +61 -0
- package/component-templates/components/datetime-picker/calendar.tsx +156 -0
- package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
- package/component-templates/components/datetime-picker/input.tsx +20 -0
- package/component-templates/components/datetime-picker/label.tsx +18 -0
- package/component-templates/components/datetime-picker/period-input.tsx +62 -0
- package/component-templates/components/datetime-picker/popover.tsx +32 -0
- package/component-templates/components/datetime-picker/select.tsx +125 -0
- package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
- package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
- package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
- package/component-templates/components/gradient-outline.tsx +233 -0
- package/component-templates/components/gradient-svg.tsx +157 -0
- package/component-templates/components/grid-layout.tsx +69 -0
- package/component-templates/components/hydrate-guard.tsx +40 -0
- package/component-templates/components/image.tsx +92 -0
- package/component-templates/components/loader-slash-gradient.tsx +85 -0
- package/component-templates/components/masonry-gallery.tsx +221 -0
- package/component-templates/components/modal.tsx +110 -0
- package/component-templates/components/multi-select.tsx +447 -0
- package/component-templates/components/non-hydration.tsx +27 -0
- package/component-templates/components/portal.tsx +34 -0
- package/component-templates/components/segments-circle.tsx +235 -0
- package/component-templates/components/single-select.tsx +248 -0
- package/component-templates/components/stroke-circle.tsx +57 -0
- package/component-templates/components/table/column-table.tsx +15 -0
- package/component-templates/components/table/data-table.tsx +339 -0
- package/component-templates/components/table/readme.tsx +95 -0
- package/component-templates/components/table/table.tsx +60 -0
- package/component-templates/components/text-hover-effect.tsx +120 -0
- package/component-templates/components/timout-loader.tsx +52 -0
- package/component-templates/components/toast.tsx +994 -0
- package/component-templates/configs/config.ts +33 -0
- package/component-templates/configs/feature-config.tsx +432 -0
- package/component-templates/configs/keys.ts +7 -0
- package/component-templates/core/api-service.ts +202 -0
- package/component-templates/core/calculate.ts +18 -0
- package/component-templates/core/idb.ts +166 -0
- package/component-templates/core/storage.ts +213 -0
- package/component-templates/hooks/count-down.ts +38 -0
- package/component-templates/hooks/fade-on-scroll.ts +52 -0
- package/component-templates/hooks/safe-action.ts +59 -0
- package/component-templates/hooks/spam-guard.ts +31 -0
- package/component-templates/lib/utils.ts +6 -0
- package/component-templates/providers/feature-guard.tsx +432 -0
- package/component-templates/queries/query.tsx +775 -0
- package/component-templates/utils/colors/color-by-text.ts +307 -0
- package/component-templates/utils/colors/stripe-effect.ts +100 -0
- package/component-templates/utils/hash/hash-aes.ts +35 -0
- package/components.json +348 -0
- 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 }
|