@questpie/admin 0.0.1
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/.turbo/turbo-build.log +108 -0
- package/CHANGELOG.md +10 -0
- package/README.md +556 -0
- package/STATUS.md +917 -0
- package/VALIDATION.md +602 -0
- package/components.json +24 -0
- package/dist/__tests__/setup.mjs +38 -0
- package/dist/__tests__/test-utils.mjs +45 -0
- package/dist/__tests__/vitest.d.mjs +3 -0
- package/dist/components/admin-app.mjs +69 -0
- package/dist/components/fields/array-field.mjs +190 -0
- package/dist/components/fields/checkbox-field.mjs +34 -0
- package/dist/components/fields/custom-field.mjs +32 -0
- package/dist/components/fields/date-field.mjs +41 -0
- package/dist/components/fields/datetime-field.mjs +42 -0
- package/dist/components/fields/email-field.mjs +37 -0
- package/dist/components/fields/embedded-collection.mjs +253 -0
- package/dist/components/fields/field-types.mjs +1 -0
- package/dist/components/fields/field-utils.mjs +10 -0
- package/dist/components/fields/field-wrapper.mjs +34 -0
- package/dist/components/fields/index.mjs +23 -0
- package/dist/components/fields/json-field.mjs +243 -0
- package/dist/components/fields/locale-badge.mjs +16 -0
- package/dist/components/fields/number-field.mjs +39 -0
- package/dist/components/fields/password-field.mjs +37 -0
- package/dist/components/fields/relation-field.mjs +104 -0
- package/dist/components/fields/relation-picker.mjs +229 -0
- package/dist/components/fields/relation-select.mjs +188 -0
- package/dist/components/fields/rich-text-editor/index.mjs +897 -0
- package/dist/components/fields/select-field.mjs +41 -0
- package/dist/components/fields/switch-field.mjs +34 -0
- package/dist/components/fields/text-field.mjs +38 -0
- package/dist/components/fields/textarea-field.mjs +38 -0
- package/dist/components/index.mjs +59 -0
- package/dist/components/primitives/checkbox-input.mjs +127 -0
- package/dist/components/primitives/date-input.mjs +303 -0
- package/dist/components/primitives/index.mjs +12 -0
- package/dist/components/primitives/number-input.mjs +104 -0
- package/dist/components/primitives/select-input.mjs +177 -0
- package/dist/components/primitives/tag-input.mjs +135 -0
- package/dist/components/primitives/text-input.mjs +39 -0
- package/dist/components/primitives/textarea-input.mjs +37 -0
- package/dist/components/primitives/toggle-input.mjs +31 -0
- package/dist/components/primitives/types.mjs +12 -0
- package/dist/components/ui/accordion.mjs +55 -0
- package/dist/components/ui/avatar.mjs +54 -0
- package/dist/components/ui/badge.mjs +34 -0
- package/dist/components/ui/button.mjs +48 -0
- package/dist/components/ui/card.mjs +58 -0
- package/dist/components/ui/checkbox.mjs +21 -0
- package/dist/components/ui/combobox.mjs +163 -0
- package/dist/components/ui/dialog.mjs +95 -0
- package/dist/components/ui/dropdown-menu.mjs +138 -0
- package/dist/components/ui/field.mjs +113 -0
- package/dist/components/ui/input-group.mjs +82 -0
- package/dist/components/ui/input.mjs +17 -0
- package/dist/components/ui/label.mjs +15 -0
- package/dist/components/ui/popover.mjs +56 -0
- package/dist/components/ui/scroll-area.mjs +38 -0
- package/dist/components/ui/select.mjs +100 -0
- package/dist/components/ui/separator.mjs +16 -0
- package/dist/components/ui/sheet.mjs +90 -0
- package/dist/components/ui/sidebar.mjs +387 -0
- package/dist/components/ui/skeleton.mjs +14 -0
- package/dist/components/ui/spinner.mjs +16 -0
- package/dist/components/ui/switch.mjs +22 -0
- package/dist/components/ui/table.mjs +68 -0
- package/dist/components/ui/tabs.mjs +48 -0
- package/dist/components/ui/textarea.mjs +15 -0
- package/dist/components/ui/tooltip.mjs +44 -0
- package/dist/config/component-registry.mjs +38 -0
- package/dist/config/index.mjs +129 -0
- package/dist/hooks/admin-provider.mjs +70 -0
- package/dist/hooks/index.mjs +7 -0
- package/dist/hooks/store.mjs +178 -0
- package/dist/hooks/use-auth.mjs +76 -0
- package/dist/hooks/use-collection-db.mjs +146 -0
- package/dist/hooks/use-collection.mjs +112 -0
- package/dist/hooks/use-global.mjs +46 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/lib/utils.mjs +10 -0
- package/dist/styles/index.css +336 -0
- package/dist/styles/index.mjs +1 -0
- package/dist/utils/index.mjs +9 -0
- package/dist/views/auth/auth-layout.mjs +52 -0
- package/dist/views/auth/forgot-password-form.mjs +148 -0
- package/dist/views/auth/index.mjs +6 -0
- package/dist/views/auth/login-form.mjs +156 -0
- package/dist/views/auth/reset-password-form.mjs +184 -0
- package/dist/views/collection/auto-form-fields.mjs +525 -0
- package/dist/views/collection/collection-form.mjs +91 -0
- package/dist/views/collection/collection-list.mjs +76 -0
- package/dist/views/collection/form-field.mjs +42 -0
- package/dist/views/collection/index.mjs +6 -0
- package/dist/views/common/index.mjs +4 -0
- package/dist/views/common/locale-switcher.mjs +39 -0
- package/dist/views/common/version-history.mjs +272 -0
- package/dist/views/index.mjs +9 -0
- package/dist/views/layout/admin-layout.mjs +40 -0
- package/dist/views/layout/admin-router.mjs +95 -0
- package/dist/views/layout/admin-sidebar.mjs +63 -0
- package/dist/views/layout/index.mjs +5 -0
- package/package.json +276 -0
- package/src/__tests__/setup.ts +44 -0
- package/src/__tests__/test-utils.tsx +49 -0
- package/src/__tests__/vitest.d.ts +9 -0
- package/src/components/admin-app.tsx +221 -0
- package/src/components/fields/array-field.tsx +237 -0
- package/src/components/fields/checkbox-field.tsx +47 -0
- package/src/components/fields/custom-field.tsx +50 -0
- package/src/components/fields/date-field.tsx +65 -0
- package/src/components/fields/datetime-field.tsx +67 -0
- package/src/components/fields/email-field.tsx +51 -0
- package/src/components/fields/embedded-collection.tsx +315 -0
- package/src/components/fields/field-types.ts +162 -0
- package/src/components/fields/field-utils.ts +6 -0
- package/src/components/fields/field-wrapper.tsx +52 -0
- package/src/components/fields/index.ts +66 -0
- package/src/components/fields/json-field.tsx +440 -0
- package/src/components/fields/locale-badge.tsx +15 -0
- package/src/components/fields/number-field.tsx +57 -0
- package/src/components/fields/password-field.tsx +51 -0
- package/src/components/fields/relation-field.tsx +243 -0
- package/src/components/fields/relation-picker.tsx +402 -0
- package/src/components/fields/relation-select.tsx +327 -0
- package/src/components/fields/rich-text-editor/index.tsx +1337 -0
- package/src/components/fields/select-field.tsx +61 -0
- package/src/components/fields/switch-field.tsx +47 -0
- package/src/components/fields/text-field.tsx +55 -0
- package/src/components/fields/textarea-field.tsx +55 -0
- package/src/components/index.ts +40 -0
- package/src/components/primitives/checkbox-input.tsx +193 -0
- package/src/components/primitives/date-input.tsx +401 -0
- package/src/components/primitives/index.ts +24 -0
- package/src/components/primitives/number-input.tsx +132 -0
- package/src/components/primitives/select-input.tsx +296 -0
- package/src/components/primitives/tag-input.tsx +200 -0
- package/src/components/primitives/text-input.tsx +49 -0
- package/src/components/primitives/textarea-input.tsx +46 -0
- package/src/components/primitives/toggle-input.tsx +36 -0
- package/src/components/primitives/types.ts +235 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/avatar.tsx +106 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +53 -0
- package/src/components/ui/card.tsx +94 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/combobox.tsx +290 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/dropdown-menu.tsx +254 -0
- package/src/components/ui/field.tsx +227 -0
- package/src/components/ui/input-group.tsx +149 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/popover.tsx +88 -0
- package/src/components/ui/scroll-area.tsx +53 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +127 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +99 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +70 -0
- package/src/config/component-registry.ts +190 -0
- package/src/config/index.ts +1099 -0
- package/src/hooks/README.md +269 -0
- package/src/hooks/admin-provider.tsx +110 -0
- package/src/hooks/index.ts +41 -0
- package/src/hooks/store.ts +248 -0
- package/src/hooks/use-auth.ts +168 -0
- package/src/hooks/use-collection-db.ts +209 -0
- package/src/hooks/use-collection.ts +156 -0
- package/src/hooks/use-global.ts +69 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/index.css +340 -0
- package/src/utils/index.ts +6 -0
- package/src/views/auth/auth-layout.tsx +77 -0
- package/src/views/auth/forgot-password-form.tsx +192 -0
- package/src/views/auth/index.ts +21 -0
- package/src/views/auth/login-form.tsx +229 -0
- package/src/views/auth/reset-password-form.tsx +232 -0
- package/src/views/collection/auto-form-fields.tsx +982 -0
- package/src/views/collection/collection-form.tsx +186 -0
- package/src/views/collection/collection-list.tsx +223 -0
- package/src/views/collection/form-field.tsx +52 -0
- package/src/views/collection/index.ts +15 -0
- package/src/views/common/index.ts +8 -0
- package/src/views/common/locale-switcher.tsx +45 -0
- package/src/views/common/version-history.tsx +406 -0
- package/src/views/index.ts +25 -0
- package/src/views/layout/admin-layout.tsx +117 -0
- package/src/views/layout/admin-router.tsx +206 -0
- package/src/views/layout/admin-sidebar.tsx +185 -0
- package/src/views/layout/index.ts +12 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +29 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { DayPicker } from "react-day-picker";
|
|
5
|
+
import { format } from "date-fns";
|
|
6
|
+
import { CalendarBlank, X } from "@phosphor-icons/react";
|
|
7
|
+
import {
|
|
8
|
+
Popover,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from "../ui/popover";
|
|
12
|
+
import { cn } from "../../lib/utils";
|
|
13
|
+
import type {
|
|
14
|
+
DateInputProps,
|
|
15
|
+
DateTimeInputProps,
|
|
16
|
+
DateRangeInputProps,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Date Input Primitive
|
|
21
|
+
*
|
|
22
|
+
* A date picker with popover calendar.
|
|
23
|
+
* Uses react-day-picker under the hood.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <DateInput
|
|
28
|
+
* value={selectedDate}
|
|
29
|
+
* onChange={setSelectedDate}
|
|
30
|
+
* placeholder="Select date"
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function DateInput({
|
|
35
|
+
value,
|
|
36
|
+
onChange,
|
|
37
|
+
minDate,
|
|
38
|
+
maxDate,
|
|
39
|
+
format: dateFormat = "PP",
|
|
40
|
+
placeholder = "Select date",
|
|
41
|
+
disabled,
|
|
42
|
+
className,
|
|
43
|
+
id,
|
|
44
|
+
"aria-invalid": ariaInvalid,
|
|
45
|
+
}: DateInputProps) {
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
|
|
48
|
+
const handleSelect = (date: Date | undefined) => {
|
|
49
|
+
onChange(date ?? null);
|
|
50
|
+
setOpen(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
onChange(null);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
60
|
+
<PopoverTrigger
|
|
61
|
+
id={id}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
aria-invalid={ariaInvalid}
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex h-9 w-full items-center justify-start gap-2 border border-input bg-background px-3 py-2 text-sm",
|
|
66
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
67
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
68
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
69
|
+
!value && "text-muted-foreground",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<CalendarBlank className="size-4" />
|
|
74
|
+
<span className="flex-1 text-left">
|
|
75
|
+
{value ? format(value, dateFormat) : placeholder}
|
|
76
|
+
</span>
|
|
77
|
+
{value && !disabled && (
|
|
78
|
+
<X
|
|
79
|
+
className="size-4 opacity-50 hover:opacity-100"
|
|
80
|
+
onClick={handleClear}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</PopoverTrigger>
|
|
84
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
85
|
+
<DayPicker
|
|
86
|
+
mode="single"
|
|
87
|
+
selected={value ?? undefined}
|
|
88
|
+
onSelect={handleSelect}
|
|
89
|
+
disabled={(date) => {
|
|
90
|
+
if (minDate && date < minDate) return true;
|
|
91
|
+
if (maxDate && date > maxDate) return true;
|
|
92
|
+
return false;
|
|
93
|
+
}}
|
|
94
|
+
className="p-3"
|
|
95
|
+
classNames={{
|
|
96
|
+
months: "flex flex-col sm:flex-row gap-2",
|
|
97
|
+
month: "flex flex-col gap-4",
|
|
98
|
+
month_caption:
|
|
99
|
+
"flex justify-center pt-1 relative items-center w-full",
|
|
100
|
+
caption_label: "text-sm font-medium",
|
|
101
|
+
nav: "flex items-center gap-1",
|
|
102
|
+
button_previous:
|
|
103
|
+
"absolute left-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
104
|
+
button_next:
|
|
105
|
+
"absolute right-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
106
|
+
month_grid: "w-full border-collapse",
|
|
107
|
+
weekdays: "flex",
|
|
108
|
+
weekday: "text-muted-foreground w-8 font-normal text-[0.8rem]",
|
|
109
|
+
week: "flex w-full mt-2",
|
|
110
|
+
day: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50",
|
|
111
|
+
day_button: cn(
|
|
112
|
+
"size-8 p-0 font-normal",
|
|
113
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
114
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
115
|
+
),
|
|
116
|
+
selected:
|
|
117
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
118
|
+
today: "bg-accent text-accent-foreground",
|
|
119
|
+
outside: "text-muted-foreground opacity-50",
|
|
120
|
+
disabled: "text-muted-foreground opacity-50",
|
|
121
|
+
hidden: "invisible",
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</PopoverContent>
|
|
125
|
+
</Popover>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* DateTime Input Primitive
|
|
131
|
+
*
|
|
132
|
+
* A date and time picker.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* <DateTimeInput
|
|
137
|
+
* value={scheduledAt}
|
|
138
|
+
* onChange={setScheduledAt}
|
|
139
|
+
* precision="minute"
|
|
140
|
+
* />
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function DateTimeInput({
|
|
144
|
+
value,
|
|
145
|
+
onChange,
|
|
146
|
+
minDate,
|
|
147
|
+
maxDate,
|
|
148
|
+
format: dateFormat = "PPp",
|
|
149
|
+
precision = "minute",
|
|
150
|
+
placeholder = "Select date and time",
|
|
151
|
+
disabled,
|
|
152
|
+
className,
|
|
153
|
+
id,
|
|
154
|
+
"aria-invalid": ariaInvalid,
|
|
155
|
+
}: DateTimeInputProps) {
|
|
156
|
+
const [open, setOpen] = useState(false);
|
|
157
|
+
const [timeValue, setTimeValue] = useState(() => {
|
|
158
|
+
if (!value) return "";
|
|
159
|
+
return precision === "second"
|
|
160
|
+
? format(value, "HH:mm:ss")
|
|
161
|
+
: format(value, "HH:mm");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const handleDateSelect = (date: Date | undefined) => {
|
|
165
|
+
if (!date) {
|
|
166
|
+
onChange(null);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Preserve existing time if available
|
|
171
|
+
if (timeValue) {
|
|
172
|
+
const [hours, minutes, seconds] = timeValue.split(":").map(Number);
|
|
173
|
+
date.setHours(hours || 0, minutes || 0, seconds || 0);
|
|
174
|
+
}
|
|
175
|
+
onChange(date);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
179
|
+
const time = e.target.value;
|
|
180
|
+
setTimeValue(time);
|
|
181
|
+
|
|
182
|
+
if (value && time) {
|
|
183
|
+
const [hours, minutes, seconds] = time.split(":").map(Number);
|
|
184
|
+
const newDate = new Date(value);
|
|
185
|
+
newDate.setHours(hours || 0, minutes || 0, seconds || 0);
|
|
186
|
+
onChange(newDate);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
onChange(null);
|
|
193
|
+
setTimeValue("");
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
198
|
+
<PopoverTrigger
|
|
199
|
+
id={id}
|
|
200
|
+
disabled={disabled}
|
|
201
|
+
aria-invalid={ariaInvalid}
|
|
202
|
+
className={cn(
|
|
203
|
+
"flex h-9 w-full items-center justify-start gap-2 border border-input bg-background px-3 py-2 text-sm",
|
|
204
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
205
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
206
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
207
|
+
!value && "text-muted-foreground",
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<CalendarBlank className="size-4" />
|
|
212
|
+
<span className="flex-1 text-left">
|
|
213
|
+
{value ? format(value, dateFormat) : placeholder}
|
|
214
|
+
</span>
|
|
215
|
+
{value && !disabled && (
|
|
216
|
+
<X
|
|
217
|
+
className="size-4 opacity-50 hover:opacity-100"
|
|
218
|
+
onClick={handleClear}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
</PopoverTrigger>
|
|
222
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
223
|
+
<DayPicker
|
|
224
|
+
mode="single"
|
|
225
|
+
selected={value ?? undefined}
|
|
226
|
+
onSelect={handleDateSelect}
|
|
227
|
+
disabled={(date) => {
|
|
228
|
+
if (minDate && date < minDate) return true;
|
|
229
|
+
if (maxDate && date > maxDate) return true;
|
|
230
|
+
return false;
|
|
231
|
+
}}
|
|
232
|
+
className="p-3"
|
|
233
|
+
classNames={{
|
|
234
|
+
months: "flex flex-col sm:flex-row gap-2",
|
|
235
|
+
month: "flex flex-col gap-4",
|
|
236
|
+
month_caption:
|
|
237
|
+
"flex justify-center pt-1 relative items-center w-full",
|
|
238
|
+
caption_label: "text-sm font-medium",
|
|
239
|
+
nav: "flex items-center gap-1",
|
|
240
|
+
button_previous:
|
|
241
|
+
"absolute left-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
242
|
+
button_next:
|
|
243
|
+
"absolute right-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
244
|
+
month_grid: "w-full border-collapse",
|
|
245
|
+
weekdays: "flex",
|
|
246
|
+
weekday: "text-muted-foreground w-8 font-normal text-[0.8rem]",
|
|
247
|
+
week: "flex w-full mt-2",
|
|
248
|
+
day: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50",
|
|
249
|
+
day_button: cn(
|
|
250
|
+
"size-8 p-0 font-normal",
|
|
251
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
252
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
253
|
+
),
|
|
254
|
+
selected:
|
|
255
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
256
|
+
today: "bg-accent text-accent-foreground",
|
|
257
|
+
outside: "text-muted-foreground opacity-50",
|
|
258
|
+
disabled: "text-muted-foreground opacity-50",
|
|
259
|
+
hidden: "invisible",
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
<div className="border-t border-border p-3">
|
|
263
|
+
<input
|
|
264
|
+
type="time"
|
|
265
|
+
step={precision === "second" ? 1 : 60}
|
|
266
|
+
value={timeValue}
|
|
267
|
+
onChange={handleTimeChange}
|
|
268
|
+
className={cn(
|
|
269
|
+
"flex h-9 w-full border border-input bg-background px-3 py-2 text-sm",
|
|
270
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
271
|
+
)}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
</PopoverContent>
|
|
275
|
+
</Popover>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Date Range Input Primitive
|
|
281
|
+
*
|
|
282
|
+
* A date range picker for selecting start and end dates.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```tsx
|
|
286
|
+
* <DateRangeInput
|
|
287
|
+
* value={{ start: startDate, end: endDate }}
|
|
288
|
+
* onChange={setDateRange}
|
|
289
|
+
* />
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
export function DateRangeInput({
|
|
293
|
+
value,
|
|
294
|
+
onChange,
|
|
295
|
+
minDate,
|
|
296
|
+
maxDate,
|
|
297
|
+
placeholder = "Select date range",
|
|
298
|
+
disabled,
|
|
299
|
+
className,
|
|
300
|
+
id,
|
|
301
|
+
"aria-invalid": ariaInvalid,
|
|
302
|
+
}: DateRangeInputProps) {
|
|
303
|
+
const [open, setOpen] = useState(false);
|
|
304
|
+
|
|
305
|
+
const handleSelect = (range: { from?: Date; to?: Date } | undefined) => {
|
|
306
|
+
onChange({
|
|
307
|
+
start: range?.from ?? null,
|
|
308
|
+
end: range?.to ?? null,
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
onChange({ start: null, end: null });
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const displayValue = () => {
|
|
318
|
+
if (!value.start && !value.end) return placeholder;
|
|
319
|
+
if (value.start && value.end) {
|
|
320
|
+
return `${format(value.start, "PP")} - ${format(value.end, "PP")}`;
|
|
321
|
+
}
|
|
322
|
+
if (value.start) return `${format(value.start, "PP")} - ...`;
|
|
323
|
+
return placeholder;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
328
|
+
<PopoverTrigger
|
|
329
|
+
id={id}
|
|
330
|
+
disabled={disabled}
|
|
331
|
+
aria-invalid={ariaInvalid}
|
|
332
|
+
className={cn(
|
|
333
|
+
"flex h-9 w-full items-center justify-start gap-2 border border-input bg-background px-3 py-2 text-sm",
|
|
334
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
335
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
336
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
337
|
+
!value.start && !value.end && "text-muted-foreground",
|
|
338
|
+
className,
|
|
339
|
+
)}
|
|
340
|
+
>
|
|
341
|
+
<CalendarBlank className="size-4" />
|
|
342
|
+
<span className="flex-1 text-left">{displayValue()}</span>
|
|
343
|
+
{(value.start || value.end) && !disabled && (
|
|
344
|
+
<X
|
|
345
|
+
className="size-4 opacity-50 hover:opacity-100"
|
|
346
|
+
onClick={handleClear}
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
</PopoverTrigger>
|
|
350
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
351
|
+
<DayPicker
|
|
352
|
+
mode="range"
|
|
353
|
+
selected={
|
|
354
|
+
value.start || value.end
|
|
355
|
+
? { from: value.start ?? undefined, to: value.end ?? undefined }
|
|
356
|
+
: undefined
|
|
357
|
+
}
|
|
358
|
+
onSelect={handleSelect}
|
|
359
|
+
numberOfMonths={2}
|
|
360
|
+
disabled={(date) => {
|
|
361
|
+
if (minDate && date < minDate) return true;
|
|
362
|
+
if (maxDate && date > maxDate) return true;
|
|
363
|
+
return false;
|
|
364
|
+
}}
|
|
365
|
+
className="p-3"
|
|
366
|
+
classNames={{
|
|
367
|
+
months: "flex flex-col sm:flex-row gap-2",
|
|
368
|
+
month: "flex flex-col gap-4",
|
|
369
|
+
month_caption:
|
|
370
|
+
"flex justify-center pt-1 relative items-center w-full",
|
|
371
|
+
caption_label: "text-sm font-medium",
|
|
372
|
+
nav: "flex items-center gap-1",
|
|
373
|
+
button_previous:
|
|
374
|
+
"absolute left-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
375
|
+
button_next:
|
|
376
|
+
"absolute right-1 size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
377
|
+
month_grid: "w-full border-collapse",
|
|
378
|
+
weekdays: "flex",
|
|
379
|
+
weekday: "text-muted-foreground w-8 font-normal text-[0.8rem]",
|
|
380
|
+
week: "flex w-full mt-2",
|
|
381
|
+
day: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50",
|
|
382
|
+
day_button: cn(
|
|
383
|
+
"size-8 p-0 font-normal",
|
|
384
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
385
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
386
|
+
),
|
|
387
|
+
selected:
|
|
388
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
389
|
+
range_start: "rounded-l-md",
|
|
390
|
+
range_end: "rounded-r-md",
|
|
391
|
+
range_middle: "bg-accent",
|
|
392
|
+
today: "bg-accent text-accent-foreground",
|
|
393
|
+
outside: "text-muted-foreground opacity-50",
|
|
394
|
+
disabled: "text-muted-foreground opacity-50",
|
|
395
|
+
hidden: "invisible",
|
|
396
|
+
}}
|
|
397
|
+
/>
|
|
398
|
+
</PopoverContent>
|
|
399
|
+
</Popover>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Primitives - Generic UI components with value/onChange pattern
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export * from "./types";
|
|
7
|
+
|
|
8
|
+
// Text inputs
|
|
9
|
+
export { TextInput } from "./text-input";
|
|
10
|
+
export { NumberInput } from "./number-input";
|
|
11
|
+
export { TextareaInput } from "./textarea-input";
|
|
12
|
+
|
|
13
|
+
// Select inputs
|
|
14
|
+
export { SelectInput } from "./select-input";
|
|
15
|
+
|
|
16
|
+
// Boolean inputs
|
|
17
|
+
export { ToggleInput } from "./toggle-input";
|
|
18
|
+
export { CheckboxInput, CheckboxGroup, RadioGroup } from "./checkbox-input";
|
|
19
|
+
|
|
20
|
+
// Date inputs
|
|
21
|
+
export { DateInput, DateTimeInput, DateRangeInput } from "./date-input";
|
|
22
|
+
|
|
23
|
+
// Special inputs
|
|
24
|
+
export { TagInput } from "./tag-input";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Minus, Plus } from "@phosphor-icons/react";
|
|
2
|
+
import { Input } from "../ui/input";
|
|
3
|
+
import { Button } from "../ui/button";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import type { NumberInputProps } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Number Input Primitive
|
|
9
|
+
*
|
|
10
|
+
* A number input with optional increment/decrement buttons.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <NumberInput
|
|
15
|
+
* value={count}
|
|
16
|
+
* onChange={setCount}
|
|
17
|
+
* min={0}
|
|
18
|
+
* max={100}
|
|
19
|
+
* step={1}
|
|
20
|
+
* showButtons
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function NumberInput({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
min,
|
|
28
|
+
max,
|
|
29
|
+
step = 1,
|
|
30
|
+
showButtons = false,
|
|
31
|
+
placeholder,
|
|
32
|
+
disabled,
|
|
33
|
+
readOnly,
|
|
34
|
+
className,
|
|
35
|
+
id,
|
|
36
|
+
"aria-invalid": ariaInvalid,
|
|
37
|
+
}: NumberInputProps) {
|
|
38
|
+
const handleChange = (newValue: number | null) => {
|
|
39
|
+
if (newValue === null) {
|
|
40
|
+
onChange(null);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let clampedValue = newValue;
|
|
45
|
+
if (min !== undefined && clampedValue < min) clampedValue = min;
|
|
46
|
+
if (max !== undefined && clampedValue > max) clampedValue = max;
|
|
47
|
+
|
|
48
|
+
onChange(clampedValue);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const increment = () => {
|
|
52
|
+
const current = value ?? 0;
|
|
53
|
+
handleChange(current + step);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const decrement = () => {
|
|
57
|
+
const current = value ?? 0;
|
|
58
|
+
handleChange(current - step);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (showButtons) {
|
|
62
|
+
return (
|
|
63
|
+
<div className={cn("flex items-center gap-1", className)}>
|
|
64
|
+
<Button
|
|
65
|
+
type="button"
|
|
66
|
+
variant="outline"
|
|
67
|
+
size="icon-sm"
|
|
68
|
+
onClick={decrement}
|
|
69
|
+
disabled={disabled || (min !== undefined && (value ?? 0) <= min)}
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
>
|
|
72
|
+
<Minus className="size-3" />
|
|
73
|
+
</Button>
|
|
74
|
+
<Input
|
|
75
|
+
id={id}
|
|
76
|
+
type="number"
|
|
77
|
+
value={value ?? ""}
|
|
78
|
+
onChange={(e) => {
|
|
79
|
+
const val = e.target.value;
|
|
80
|
+
if (val === "") {
|
|
81
|
+
handleChange(null);
|
|
82
|
+
} else {
|
|
83
|
+
handleChange(Number(val));
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
placeholder={placeholder}
|
|
87
|
+
disabled={disabled}
|
|
88
|
+
readOnly={readOnly}
|
|
89
|
+
min={min}
|
|
90
|
+
max={max}
|
|
91
|
+
step={step}
|
|
92
|
+
aria-invalid={ariaInvalid}
|
|
93
|
+
className="text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
94
|
+
/>
|
|
95
|
+
<Button
|
|
96
|
+
type="button"
|
|
97
|
+
variant="outline"
|
|
98
|
+
size="icon-sm"
|
|
99
|
+
onClick={increment}
|
|
100
|
+
disabled={disabled || (max !== undefined && (value ?? 0) >= max)}
|
|
101
|
+
tabIndex={-1}
|
|
102
|
+
>
|
|
103
|
+
<Plus className="size-3" />
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Input
|
|
111
|
+
id={id}
|
|
112
|
+
type="number"
|
|
113
|
+
value={value ?? ""}
|
|
114
|
+
onChange={(e) => {
|
|
115
|
+
const val = e.target.value;
|
|
116
|
+
if (val === "") {
|
|
117
|
+
handleChange(null);
|
|
118
|
+
} else {
|
|
119
|
+
handleChange(Number(val));
|
|
120
|
+
}
|
|
121
|
+
}}
|
|
122
|
+
placeholder={placeholder}
|
|
123
|
+
disabled={disabled}
|
|
124
|
+
readOnly={readOnly}
|
|
125
|
+
min={min}
|
|
126
|
+
max={max}
|
|
127
|
+
step={step}
|
|
128
|
+
aria-invalid={ariaInvalid}
|
|
129
|
+
className={cn(className)}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|