@payglocal_ui/flux-ui 0.1.0
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/package.json +72 -0
- package/src/accordion.tsx +68 -0
- package/src/alert.tsx +107 -0
- package/src/avatar-group.tsx +96 -0
- package/src/avatar-tag.tsx +136 -0
- package/src/avatar.tsx +39 -0
- package/src/badge.tsx +98 -0
- package/src/blanket.tsx +61 -0
- package/src/breadcrumb.tsx +119 -0
- package/src/button-group.tsx +218 -0
- package/src/button.tsx +83 -0
- package/src/calendar.tsx +227 -0
- package/src/callout.tsx +68 -0
- package/src/card.tsx +103 -0
- package/src/chart-templates.tsx +587 -0
- package/src/chart.tsx +379 -0
- package/src/checkbox-select.tsx +239 -0
- package/src/checkbox.tsx +54 -0
- package/src/code.tsx +154 -0
- package/src/command.tsx +77 -0
- package/src/country-select.tsx +242 -0
- package/src/currency-amount-input.tsx +72 -0
- package/src/data-table.tsx +378 -0
- package/src/date-picker.tsx +317 -0
- package/src/dialog.tsx +81 -0
- package/src/drawer.tsx +91 -0
- package/src/dropdown-menu.tsx +174 -0
- package/src/empty-state.tsx +32 -0
- package/src/field.tsx +243 -0
- package/src/flag.tsx +265 -0
- package/src/form.tsx +168 -0
- package/src/grid-flex.tsx +241 -0
- package/src/heading.tsx +202 -0
- package/src/icon-button.tsx +93 -0
- package/src/index.ts +332 -0
- package/src/inline-dialog.tsx +153 -0
- package/src/inline-edit.tsx +212 -0
- package/src/input-group.tsx +151 -0
- package/src/input.tsx +28 -0
- package/src/label.tsx +21 -0
- package/src/layout.tsx +119 -0
- package/src/link.tsx +80 -0
- package/src/lozenge.tsx +61 -0
- package/src/menu.tsx +146 -0
- package/src/otp-input.tsx +117 -0
- package/src/page-header.tsx +28 -0
- package/src/pagination.tsx +185 -0
- package/src/password-input.tsx +34 -0
- package/src/popover.tsx +31 -0
- package/src/progress-indicator.tsx +94 -0
- package/src/progress.tsx +95 -0
- package/src/radio-group.tsx +46 -0
- package/src/responsive.tsx +276 -0
- package/src/scroll-area.tsx +39 -0
- package/src/section-message.tsx +119 -0
- package/src/select.tsx +144 -0
- package/src/separator.tsx +26 -0
- package/src/side-nav.tsx +264 -0
- package/src/skeleton.tsx +74 -0
- package/src/slider.tsx +25 -0
- package/src/sonner.tsx +32 -0
- package/src/spinner.tsx +54 -0
- package/src/spotlight.tsx +141 -0
- package/src/status-badge.tsx +86 -0
- package/src/switch.tsx +70 -0
- package/src/tabs.tsx +57 -0
- package/src/tag.tsx +52 -0
- package/src/textarea.tsx +25 -0
- package/src/time-picker.tsx +443 -0
- package/src/tooltip.tsx +29 -0
- package/src/utils.ts +6 -0
- package/src/visually-hidden.tsx +25 -0
package/src/flag.tsx
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Info, CheckCircle2, AlertTriangle, XCircle, X } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type FlagVariant = "info" | "success" | "warning" | "error";
|
|
10
|
+
export type FlagGroupPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
11
|
+
|
|
12
|
+
export interface FlagAction {
|
|
13
|
+
label: string;
|
|
14
|
+
onClick: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FlagProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
/** Heading text shown in bold */
|
|
19
|
+
title: string;
|
|
20
|
+
/** Optional body copy shown below the title */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Visual severity / colour */
|
|
23
|
+
variant?: FlagVariant;
|
|
24
|
+
/** Override the default lucide icon */
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
/** Called when the close button is pressed */
|
|
27
|
+
onDismiss?: () => void;
|
|
28
|
+
/** Action buttons rendered below the content. Pass `false` to disable. */
|
|
29
|
+
autoDismiss?: number | false;
|
|
30
|
+
/** Optional CTA buttons */
|
|
31
|
+
actions?: FlagAction[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FlagGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
35
|
+
/** Corner to anchor the stack */
|
|
36
|
+
position?: FlagGroupPosition;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UseFlagGroupReturn {
|
|
40
|
+
flags: Array<FlagProps & { id: string }>;
|
|
41
|
+
addFlag: (flag: Omit<FlagProps, "onDismiss"> & { id?: string }) => string;
|
|
42
|
+
removeFlag: (id: string) => void;
|
|
43
|
+
clearAll: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const DEFAULT_AUTO_DISMISS = 8000;
|
|
49
|
+
|
|
50
|
+
const variantBorder: Record<FlagVariant, string> = {
|
|
51
|
+
info: "border-l-blue-500",
|
|
52
|
+
success: "border-l-green-500",
|
|
53
|
+
warning: "border-l-amber-500",
|
|
54
|
+
error: "border-l-red-500",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const variantProgressBar: Record<FlagVariant, string> = {
|
|
58
|
+
info: "bg-blue-500",
|
|
59
|
+
success: "bg-green-500",
|
|
60
|
+
warning: "bg-amber-500",
|
|
61
|
+
error: "bg-red-500",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const variantIconColor: Record<FlagVariant, string> = {
|
|
65
|
+
info: "text-blue-500",
|
|
66
|
+
success: "text-green-500",
|
|
67
|
+
warning: "text-amber-500",
|
|
68
|
+
error: "text-red-500",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const defaultIcons: Record<FlagVariant, React.ElementType> = {
|
|
72
|
+
info: Info,
|
|
73
|
+
success: CheckCircle2,
|
|
74
|
+
warning: AlertTriangle,
|
|
75
|
+
error: XCircle,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ─── Flag ─────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const Flag = React.forwardRef<HTMLDivElement, FlagProps>(
|
|
81
|
+
(
|
|
82
|
+
{
|
|
83
|
+
className,
|
|
84
|
+
title,
|
|
85
|
+
description,
|
|
86
|
+
variant = "info",
|
|
87
|
+
icon,
|
|
88
|
+
onDismiss,
|
|
89
|
+
autoDismiss = DEFAULT_AUTO_DISMISS,
|
|
90
|
+
actions,
|
|
91
|
+
...props
|
|
92
|
+
},
|
|
93
|
+
ref
|
|
94
|
+
) => {
|
|
95
|
+
const [progress, setProgress] = React.useState(100);
|
|
96
|
+
const pausedRef = React.useRef(false);
|
|
97
|
+
const startTimeRef = React.useRef<number | null>(null);
|
|
98
|
+
const elapsedRef = React.useRef(0);
|
|
99
|
+
const rafRef = React.useRef<number | null>(null);
|
|
100
|
+
const duration = autoDismiss === false ? null : autoDismiss;
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (duration === null) return;
|
|
104
|
+
|
|
105
|
+
const tick = (now: number) => {
|
|
106
|
+
if (pausedRef.current) {
|
|
107
|
+
startTimeRef.current = now;
|
|
108
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (startTimeRef.current === null) startTimeRef.current = now;
|
|
112
|
+
elapsedRef.current += now - startTimeRef.current;
|
|
113
|
+
startTimeRef.current = now;
|
|
114
|
+
|
|
115
|
+
const remaining = Math.max(0, duration - elapsedRef.current);
|
|
116
|
+
const pct = (remaining / duration) * 100;
|
|
117
|
+
setProgress(pct);
|
|
118
|
+
|
|
119
|
+
if (remaining <= 0) {
|
|
120
|
+
onDismiss?.();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
127
|
+
return () => {
|
|
128
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
129
|
+
};
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
+
}, [duration]);
|
|
132
|
+
|
|
133
|
+
const handleMouseEnter = () => {
|
|
134
|
+
pausedRef.current = true;
|
|
135
|
+
};
|
|
136
|
+
const handleMouseLeave = () => {
|
|
137
|
+
pausedRef.current = false;
|
|
138
|
+
startTimeRef.current = null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const DefaultIcon = defaultIcons[variant];
|
|
142
|
+
const renderedIcon = icon ?? <DefaultIcon className={cn("size-4 shrink-0 mt-0.5", variantIconColor[variant])} aria-hidden />;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
ref={ref}
|
|
147
|
+
role="alert"
|
|
148
|
+
aria-live="assertive"
|
|
149
|
+
aria-atomic="true"
|
|
150
|
+
onMouseEnter={handleMouseEnter}
|
|
151
|
+
onMouseLeave={handleMouseLeave}
|
|
152
|
+
className={cn(
|
|
153
|
+
"relative w-80 overflow-hidden rounded-xl border border-border bg-card shadow-lg",
|
|
154
|
+
"border-l-4",
|
|
155
|
+
variantBorder[variant],
|
|
156
|
+
className
|
|
157
|
+
)}
|
|
158
|
+
{...props}
|
|
159
|
+
>
|
|
160
|
+
{/* Body */}
|
|
161
|
+
<div className="flex gap-3 p-4 pr-10">
|
|
162
|
+
{renderedIcon}
|
|
163
|
+
<div className="flex-1 min-w-0">
|
|
164
|
+
<p className="text-sm font-semibold text-card-foreground leading-snug">{title}</p>
|
|
165
|
+
{description && (
|
|
166
|
+
<p className="mt-1 text-sm text-muted-foreground leading-relaxed">{description}</p>
|
|
167
|
+
)}
|
|
168
|
+
{actions && actions.length > 0 && (
|
|
169
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
170
|
+
{actions.map((action) => (
|
|
171
|
+
<button
|
|
172
|
+
key={action.label}
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={action.onClick}
|
|
175
|
+
className="text-xs font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded"
|
|
176
|
+
>
|
|
177
|
+
{action.label}
|
|
178
|
+
</button>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Close button */}
|
|
186
|
+
{onDismiss && (
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
aria-label="Dismiss notification"
|
|
190
|
+
onClick={onDismiss}
|
|
191
|
+
className="absolute top-3 right-3 rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
192
|
+
>
|
|
193
|
+
<X className="size-3.5" aria-hidden />
|
|
194
|
+
</button>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Auto-dismiss progress bar */}
|
|
198
|
+
{duration !== null && (
|
|
199
|
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted">
|
|
200
|
+
<div
|
|
201
|
+
className={cn("h-full transition-none", variantProgressBar[variant])}
|
|
202
|
+
style={{ width: `${progress}%` }}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
Flag.displayName = "Flag";
|
|
211
|
+
|
|
212
|
+
// ─── FlagGroup ────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
const positionClasses: Record<FlagGroupPosition, string> = {
|
|
215
|
+
"bottom-right": "bottom-4 right-4 flex-col-reverse",
|
|
216
|
+
"bottom-left": "bottom-4 left-4 flex-col-reverse",
|
|
217
|
+
"top-right": "top-4 right-4 flex-col",
|
|
218
|
+
"top-left": "top-4 left-4 flex-col",
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const FlagGroup = React.forwardRef<HTMLDivElement, FlagGroupProps>(
|
|
222
|
+
({ className, position = "bottom-right", children, ...props }, ref) => (
|
|
223
|
+
<div
|
|
224
|
+
ref={ref}
|
|
225
|
+
className={cn(
|
|
226
|
+
"fixed z-50 flex gap-2",
|
|
227
|
+
positionClasses[position],
|
|
228
|
+
className
|
|
229
|
+
)}
|
|
230
|
+
{...props}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
FlagGroup.displayName = "FlagGroup";
|
|
237
|
+
|
|
238
|
+
// ─── useFlagGroup ─────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function useFlagGroup(): UseFlagGroupReturn {
|
|
241
|
+
const [flags, setFlags] = React.useState<Array<FlagProps & { id: string }>>([]);
|
|
242
|
+
|
|
243
|
+
const removeFlag = React.useCallback((id: string) => {
|
|
244
|
+
setFlags((prev) => prev.filter((f) => f.id !== id));
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const addFlag = React.useCallback(
|
|
248
|
+
(flag: Omit<FlagProps, "onDismiss"> & { id?: string }): string => {
|
|
249
|
+
const id = flag.id ?? `flag-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
250
|
+
setFlags((prev) => [...prev, { ...flag, id, onDismiss: () => removeFlag(id) }]);
|
|
251
|
+
return id;
|
|
252
|
+
},
|
|
253
|
+
[removeFlag]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const clearAll = React.useCallback(() => {
|
|
257
|
+
setFlags([]);
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
return { flags, addFlag, removeFlag, clearAll };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export { Flag, FlagGroup, useFlagGroup };
|
package/src/form.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { AlertCircle } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type FormValues = Record<string, string>;
|
|
8
|
+
export type FormErrors = Record<string, string>;
|
|
9
|
+
|
|
10
|
+
export interface ValidatorRule {
|
|
11
|
+
required?: boolean | string;
|
|
12
|
+
minLength?: { value: number; message: string };
|
|
13
|
+
maxLength?: { value: number; message: string };
|
|
14
|
+
pattern?: { value: RegExp; message: string };
|
|
15
|
+
validate?: (value: string) => string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FieldConfig {
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
rules?: ValidatorRule;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FieldsConfig = Record<string, FieldConfig>;
|
|
24
|
+
|
|
25
|
+
export interface RegisterResult {
|
|
26
|
+
name: string;
|
|
27
|
+
id: string;
|
|
28
|
+
value: string;
|
|
29
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
30
|
+
onBlur: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseFormReturn {
|
|
34
|
+
values: FormValues;
|
|
35
|
+
errors: FormErrors;
|
|
36
|
+
register: (name: string) => RegisterResult;
|
|
37
|
+
handleSubmit: (onValid: (values: FormValues) => void, onInvalid?: (errors: FormErrors) => void) => (e: React.FormEvent) => void;
|
|
38
|
+
setError: (name: string, message: string) => void;
|
|
39
|
+
clearError: (name: string) => void;
|
|
40
|
+
reset: () => void;
|
|
41
|
+
setValue: (name: string, value: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useForm(fields: FieldsConfig = {}): UseFormReturn {
|
|
45
|
+
const initial = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, v.defaultValue ?? ""]));
|
|
46
|
+
const [values, setValues] = React.useState<FormValues>(initial);
|
|
47
|
+
const [errors, setErrors] = React.useState<FormErrors>({});
|
|
48
|
+
|
|
49
|
+
function validate(name: string, value: string): string | undefined {
|
|
50
|
+
const rules = fields[name]?.rules;
|
|
51
|
+
if (!rules) return undefined;
|
|
52
|
+
if (rules.required && !value.trim()) return typeof rules.required === "string" ? rules.required : "This field is required";
|
|
53
|
+
if (rules.minLength && value.length < rules.minLength.value) return rules.minLength.message;
|
|
54
|
+
if (rules.maxLength && value.length > rules.maxLength.value) return rules.maxLength.message;
|
|
55
|
+
if (rules.pattern && !rules.pattern.value.test(value)) return rules.pattern.message;
|
|
56
|
+
if (rules.validate) return rules.validate(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const register = (name: string): RegisterResult => ({
|
|
60
|
+
name,
|
|
61
|
+
id: name,
|
|
62
|
+
value: values[name] ?? "",
|
|
63
|
+
onChange: (e) => setValues(v => ({ ...v, [name]: e.target.value })),
|
|
64
|
+
onBlur: () => {
|
|
65
|
+
const err = validate(name, values[name] ?? "");
|
|
66
|
+
setErrors(e => err ? { ...e, [name]: err } : (({ [name]: _, ...rest }) => rest)(e));
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const handleSubmit = (onValid: (v: FormValues) => void, onInvalid?: (e: FormErrors) => void) => (e: React.FormEvent) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
const newErrors: FormErrors = {};
|
|
73
|
+
for (const [name] of Object.entries(fields)) {
|
|
74
|
+
const err = validate(name, values[name] ?? "");
|
|
75
|
+
if (err) newErrors[name] = err;
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(newErrors).length > 0) {
|
|
78
|
+
setErrors(newErrors);
|
|
79
|
+
onInvalid?.(newErrors);
|
|
80
|
+
} else {
|
|
81
|
+
setErrors({});
|
|
82
|
+
onValid(values);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
values,
|
|
88
|
+
errors,
|
|
89
|
+
register,
|
|
90
|
+
handleSubmit,
|
|
91
|
+
setError: (name, msg) => setErrors(e => ({ ...e, [name]: msg })),
|
|
92
|
+
clearError: (name) => setErrors(e => (({ [name]: _, ...rest }) => rest)(e)),
|
|
93
|
+
reset: () => { setValues(initial); setErrors({}); },
|
|
94
|
+
setValue: (name, value) => setValues(v => ({ ...v, [name]: value })),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {}
|
|
99
|
+
|
|
100
|
+
const Form = React.forwardRef<HTMLFormElement, FormProps>(
|
|
101
|
+
({ className, ...props }, ref) => (
|
|
102
|
+
<form ref={ref} className={cn("space-y-4", className)} {...props} />
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
Form.displayName = "Form";
|
|
106
|
+
|
|
107
|
+
export interface FormItemProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
108
|
+
|
|
109
|
+
const FormItem = React.forwardRef<HTMLDivElement, FormItemProps>(
|
|
110
|
+
({ className, ...props }, ref) => (
|
|
111
|
+
<div ref={ref} className={cn("flex flex-col gap-1.5", className)} {...props} />
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
FormItem.displayName = "FormItem";
|
|
115
|
+
|
|
116
|
+
export interface FormLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
117
|
+
required?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const FormLabel = React.forwardRef<HTMLLabelElement, FormLabelProps>(
|
|
121
|
+
({ className, required, children, ...props }, ref) => (
|
|
122
|
+
<label ref={ref} className={cn("text-sm font-medium text-foreground", className)} {...props}>
|
|
123
|
+
{children}
|
|
124
|
+
{required && <span className="ml-1 text-destructive">*</span>}
|
|
125
|
+
</label>
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
FormLabel.displayName = "FormLabel";
|
|
129
|
+
|
|
130
|
+
export interface FormControlProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
131
|
+
|
|
132
|
+
const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
|
|
133
|
+
({ ...props }, ref) => <div ref={ref} {...props} />
|
|
134
|
+
);
|
|
135
|
+
FormControl.displayName = "FormControl";
|
|
136
|
+
|
|
137
|
+
export interface FormDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
138
|
+
|
|
139
|
+
const FormDescription = React.forwardRef<HTMLParagraphElement, FormDescriptionProps>(
|
|
140
|
+
({ className, ...props }, ref) => (
|
|
141
|
+
<p ref={ref} className={cn("text-xs text-muted-foreground", className)} {...props} />
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
FormDescription.displayName = "FormDescription";
|
|
145
|
+
|
|
146
|
+
export interface FormErrorProps extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
147
|
+
|
|
148
|
+
const FormError = React.forwardRef<HTMLParagraphElement, FormErrorProps>(
|
|
149
|
+
({ className, children, ...props }, ref) =>
|
|
150
|
+
children ? (
|
|
151
|
+
<p ref={ref} className={cn("flex items-center gap-1 text-xs text-destructive", className)} {...props}>
|
|
152
|
+
<AlertCircle className="size-3 shrink-0" />
|
|
153
|
+
{children}
|
|
154
|
+
</p>
|
|
155
|
+
) : null
|
|
156
|
+
);
|
|
157
|
+
FormError.displayName = "FormError";
|
|
158
|
+
|
|
159
|
+
export interface FormFieldProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
160
|
+
|
|
161
|
+
const FormField = React.forwardRef<HTMLDivElement, FormFieldProps>(
|
|
162
|
+
({ className, ...props }, ref) => (
|
|
163
|
+
<div ref={ref} className={cn("flex flex-col gap-1.5", className)} {...props} />
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
FormField.displayName = "FormField";
|
|
167
|
+
|
|
168
|
+
export { Form, FormItem, FormLabel, FormControl, FormDescription, FormError, FormField };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
import type { LayoutSpacing } from "./layout";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Shared gap map (mirrors layout.tsx)
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const gapClass: Record<LayoutSpacing, string> = {
|
|
10
|
+
none: "gap-0",
|
|
11
|
+
xs: "gap-1",
|
|
12
|
+
sm: "gap-2",
|
|
13
|
+
md: "gap-3",
|
|
14
|
+
lg: "gap-4",
|
|
15
|
+
xl: "gap-6",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Grid
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | "auto";
|
|
23
|
+
|
|
24
|
+
export type ResponsiveCols = {
|
|
25
|
+
base?: GridCols;
|
|
26
|
+
sm?: GridCols;
|
|
27
|
+
md?: GridCols;
|
|
28
|
+
lg?: GridCols;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type GridFlow = "row" | "col" | "dense";
|
|
32
|
+
|
|
33
|
+
export type GridProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
34
|
+
/** Number of columns (1–12 or "auto"), or a responsive object */
|
|
35
|
+
cols?: GridCols | ResponsiveCols;
|
|
36
|
+
/** Number of explicit rows */
|
|
37
|
+
rows?: 1 | 2 | 3 | 4 | 5 | 6 | "auto";
|
|
38
|
+
/** Gap between cells */
|
|
39
|
+
gap?: LayoutSpacing;
|
|
40
|
+
/** Grid auto-flow direction */
|
|
41
|
+
flow?: GridFlow;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const colClass: Record<string, string> = {
|
|
45
|
+
1: "grid-cols-1",
|
|
46
|
+
2: "grid-cols-2",
|
|
47
|
+
3: "grid-cols-3",
|
|
48
|
+
4: "grid-cols-4",
|
|
49
|
+
5: "grid-cols-5",
|
|
50
|
+
6: "grid-cols-6",
|
|
51
|
+
7: "grid-cols-7",
|
|
52
|
+
8: "grid-cols-8",
|
|
53
|
+
9: "grid-cols-9",
|
|
54
|
+
10: "grid-cols-10",
|
|
55
|
+
11: "grid-cols-11",
|
|
56
|
+
12: "grid-cols-12",
|
|
57
|
+
auto: "grid-cols-[repeat(auto-fit,minmax(0,1fr))]",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const smColClass: Record<string, string> = {
|
|
61
|
+
1: "sm:grid-cols-1",
|
|
62
|
+
2: "sm:grid-cols-2",
|
|
63
|
+
3: "sm:grid-cols-3",
|
|
64
|
+
4: "sm:grid-cols-4",
|
|
65
|
+
5: "sm:grid-cols-5",
|
|
66
|
+
6: "sm:grid-cols-6",
|
|
67
|
+
7: "sm:grid-cols-7",
|
|
68
|
+
8: "sm:grid-cols-8",
|
|
69
|
+
9: "sm:grid-cols-9",
|
|
70
|
+
10: "sm:grid-cols-10",
|
|
71
|
+
11: "sm:grid-cols-11",
|
|
72
|
+
12: "sm:grid-cols-12",
|
|
73
|
+
auto: "sm:grid-cols-[repeat(auto-fit,minmax(0,1fr))]",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const mdColClass: Record<string, string> = {
|
|
77
|
+
1: "md:grid-cols-1",
|
|
78
|
+
2: "md:grid-cols-2",
|
|
79
|
+
3: "md:grid-cols-3",
|
|
80
|
+
4: "md:grid-cols-4",
|
|
81
|
+
5: "md:grid-cols-5",
|
|
82
|
+
6: "md:grid-cols-6",
|
|
83
|
+
7: "md:grid-cols-7",
|
|
84
|
+
8: "md:grid-cols-8",
|
|
85
|
+
9: "md:grid-cols-9",
|
|
86
|
+
10: "md:grid-cols-10",
|
|
87
|
+
11: "md:grid-cols-11",
|
|
88
|
+
12: "md:grid-cols-12",
|
|
89
|
+
auto: "md:grid-cols-[repeat(auto-fit,minmax(0,1fr))]",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const lgColClass: Record<string, string> = {
|
|
93
|
+
1: "lg:grid-cols-1",
|
|
94
|
+
2: "lg:grid-cols-2",
|
|
95
|
+
3: "lg:grid-cols-3",
|
|
96
|
+
4: "lg:grid-cols-4",
|
|
97
|
+
5: "lg:grid-cols-5",
|
|
98
|
+
6: "lg:grid-cols-6",
|
|
99
|
+
7: "lg:grid-cols-7",
|
|
100
|
+
8: "lg:grid-cols-8",
|
|
101
|
+
9: "lg:grid-cols-9",
|
|
102
|
+
10: "lg:grid-cols-10",
|
|
103
|
+
11: "lg:grid-cols-11",
|
|
104
|
+
12: "lg:grid-cols-12",
|
|
105
|
+
auto: "lg:grid-cols-[repeat(auto-fit,minmax(0,1fr))]",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const rowClass: Record<string, string> = {
|
|
109
|
+
1: "grid-rows-1",
|
|
110
|
+
2: "grid-rows-2",
|
|
111
|
+
3: "grid-rows-3",
|
|
112
|
+
4: "grid-rows-4",
|
|
113
|
+
5: "grid-rows-5",
|
|
114
|
+
6: "grid-rows-6",
|
|
115
|
+
auto: "grid-rows-none",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const flowClass: Record<GridFlow, string> = {
|
|
119
|
+
row: "grid-flow-row",
|
|
120
|
+
col: "grid-flow-col",
|
|
121
|
+
dense: "grid-flow-dense",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function resolveColClasses(cols: GridCols | ResponsiveCols | undefined): string {
|
|
125
|
+
if (cols === undefined) return "";
|
|
126
|
+
if (typeof cols === "number" || cols === "auto") {
|
|
127
|
+
return colClass[String(cols)] ?? "";
|
|
128
|
+
}
|
|
129
|
+
// Responsive object
|
|
130
|
+
const { base, sm, md, lg } = cols as ResponsiveCols;
|
|
131
|
+
return cn(
|
|
132
|
+
base !== undefined && colClass[String(base)],
|
|
133
|
+
sm !== undefined && smColClass[String(sm)],
|
|
134
|
+
md !== undefined && mdColClass[String(md)],
|
|
135
|
+
lg !== undefined && lgColClass[String(lg)],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Grid = React.forwardRef<HTMLDivElement, GridProps>(
|
|
140
|
+
({ className, cols, rows, gap = "md", flow, ...props }, ref) => {
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
ref={ref}
|
|
144
|
+
className={cn(
|
|
145
|
+
"grid",
|
|
146
|
+
resolveColClasses(cols),
|
|
147
|
+
rows !== undefined && rowClass[String(rows)],
|
|
148
|
+
gapClass[gap],
|
|
149
|
+
flow !== undefined && flowClass[flow],
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
Grid.displayName = "Grid";
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Flex
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export type FlexDirection = "row" | "col" | "row-reverse" | "col-reverse";
|
|
164
|
+
export type FlexWrap = boolean | "reverse";
|
|
165
|
+
export type FlexAlign = "start" | "center" | "end" | "stretch" | "baseline";
|
|
166
|
+
export type FlexJustify = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
167
|
+
|
|
168
|
+
export type FlexProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
169
|
+
/** Flex direction */
|
|
170
|
+
direction?: FlexDirection;
|
|
171
|
+
/** Whether children wrap */
|
|
172
|
+
wrap?: FlexWrap;
|
|
173
|
+
/** Align items (cross axis) */
|
|
174
|
+
align?: FlexAlign;
|
|
175
|
+
/** Justify content (main axis) */
|
|
176
|
+
justify?: FlexJustify;
|
|
177
|
+
/** Gap between children */
|
|
178
|
+
gap?: LayoutSpacing;
|
|
179
|
+
/** Render as inline-flex */
|
|
180
|
+
inline?: boolean;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const directionClass: Record<FlexDirection, string> = {
|
|
184
|
+
row: "flex-row",
|
|
185
|
+
col: "flex-col",
|
|
186
|
+
"row-reverse": "flex-row-reverse",
|
|
187
|
+
"col-reverse": "flex-col-reverse",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const alignClass: Record<FlexAlign, string> = {
|
|
191
|
+
start: "items-start",
|
|
192
|
+
center: "items-center",
|
|
193
|
+
end: "items-end",
|
|
194
|
+
stretch: "items-stretch",
|
|
195
|
+
baseline: "items-baseline",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const justifyClass: Record<FlexJustify, string> = {
|
|
199
|
+
start: "justify-start",
|
|
200
|
+
center: "justify-center",
|
|
201
|
+
end: "justify-end",
|
|
202
|
+
between: "justify-between",
|
|
203
|
+
around: "justify-around",
|
|
204
|
+
evenly: "justify-evenly",
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
|
|
208
|
+
(
|
|
209
|
+
{
|
|
210
|
+
className,
|
|
211
|
+
direction = "row",
|
|
212
|
+
wrap = false,
|
|
213
|
+
align = "start",
|
|
214
|
+
justify = "start",
|
|
215
|
+
gap = "md",
|
|
216
|
+
inline = false,
|
|
217
|
+
...props
|
|
218
|
+
},
|
|
219
|
+
ref,
|
|
220
|
+
) => {
|
|
221
|
+
const wrapCls =
|
|
222
|
+
wrap === true ? "flex-wrap" : wrap === "reverse" ? "flex-wrap-reverse" : "flex-nowrap";
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
ref={ref}
|
|
227
|
+
className={cn(
|
|
228
|
+
inline ? "inline-flex" : "flex",
|
|
229
|
+
directionClass[direction],
|
|
230
|
+
wrapCls,
|
|
231
|
+
alignClass[align],
|
|
232
|
+
justifyClass[justify],
|
|
233
|
+
gapClass[gap],
|
|
234
|
+
className,
|
|
235
|
+
)}
|
|
236
|
+
{...props}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
Flex.displayName = "Flex";
|