@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.
Files changed (72) hide show
  1. package/package.json +72 -0
  2. package/src/accordion.tsx +68 -0
  3. package/src/alert.tsx +107 -0
  4. package/src/avatar-group.tsx +96 -0
  5. package/src/avatar-tag.tsx +136 -0
  6. package/src/avatar.tsx +39 -0
  7. package/src/badge.tsx +98 -0
  8. package/src/blanket.tsx +61 -0
  9. package/src/breadcrumb.tsx +119 -0
  10. package/src/button-group.tsx +218 -0
  11. package/src/button.tsx +83 -0
  12. package/src/calendar.tsx +227 -0
  13. package/src/callout.tsx +68 -0
  14. package/src/card.tsx +103 -0
  15. package/src/chart-templates.tsx +587 -0
  16. package/src/chart.tsx +379 -0
  17. package/src/checkbox-select.tsx +239 -0
  18. package/src/checkbox.tsx +54 -0
  19. package/src/code.tsx +154 -0
  20. package/src/command.tsx +77 -0
  21. package/src/country-select.tsx +242 -0
  22. package/src/currency-amount-input.tsx +72 -0
  23. package/src/data-table.tsx +378 -0
  24. package/src/date-picker.tsx +317 -0
  25. package/src/dialog.tsx +81 -0
  26. package/src/drawer.tsx +91 -0
  27. package/src/dropdown-menu.tsx +174 -0
  28. package/src/empty-state.tsx +32 -0
  29. package/src/field.tsx +243 -0
  30. package/src/flag.tsx +265 -0
  31. package/src/form.tsx +168 -0
  32. package/src/grid-flex.tsx +241 -0
  33. package/src/heading.tsx +202 -0
  34. package/src/icon-button.tsx +93 -0
  35. package/src/index.ts +332 -0
  36. package/src/inline-dialog.tsx +153 -0
  37. package/src/inline-edit.tsx +212 -0
  38. package/src/input-group.tsx +151 -0
  39. package/src/input.tsx +28 -0
  40. package/src/label.tsx +21 -0
  41. package/src/layout.tsx +119 -0
  42. package/src/link.tsx +80 -0
  43. package/src/lozenge.tsx +61 -0
  44. package/src/menu.tsx +146 -0
  45. package/src/otp-input.tsx +117 -0
  46. package/src/page-header.tsx +28 -0
  47. package/src/pagination.tsx +185 -0
  48. package/src/password-input.tsx +34 -0
  49. package/src/popover.tsx +31 -0
  50. package/src/progress-indicator.tsx +94 -0
  51. package/src/progress.tsx +95 -0
  52. package/src/radio-group.tsx +46 -0
  53. package/src/responsive.tsx +276 -0
  54. package/src/scroll-area.tsx +39 -0
  55. package/src/section-message.tsx +119 -0
  56. package/src/select.tsx +144 -0
  57. package/src/separator.tsx +26 -0
  58. package/src/side-nav.tsx +264 -0
  59. package/src/skeleton.tsx +74 -0
  60. package/src/slider.tsx +25 -0
  61. package/src/sonner.tsx +32 -0
  62. package/src/spinner.tsx +54 -0
  63. package/src/spotlight.tsx +141 -0
  64. package/src/status-badge.tsx +86 -0
  65. package/src/switch.tsx +70 -0
  66. package/src/tabs.tsx +57 -0
  67. package/src/tag.tsx +52 -0
  68. package/src/textarea.tsx +25 -0
  69. package/src/time-picker.tsx +443 -0
  70. package/src/tooltip.tsx +29 -0
  71. package/src/utils.ts +6 -0
  72. 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";