@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
@@ -0,0 +1,153 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+ import { X } from "lucide-react";
6
+ import { cn } from "./utils";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface InlineDialogProps
13
+ extends React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root> {}
14
+
15
+ export interface InlineDialogContentProps
16
+ extends Omit<
17
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>,
18
+ "side" | "sideOffset"
19
+ > {
20
+ side?: "top" | "right" | "bottom" | "left";
21
+ sideOffset?: number;
22
+ /** Hide the built-in close button */
23
+ hideClose?: boolean;
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // InlineDialog (Popover root)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const InlineDialog = PopoverPrimitive.Root;
31
+ InlineDialog.displayName = "InlineDialog";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // InlineDialogTrigger
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const InlineDialogTrigger = PopoverPrimitive.Trigger;
38
+ InlineDialogTrigger.displayName = "InlineDialogTrigger";
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Arrow positioning helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ type ArrowSide = "top" | "right" | "bottom" | "left";
45
+
46
+ /**
47
+ * Returns Tailwind classes that position the 8×8 arrow square on the correct
48
+ * edge of the floating card and rotate it so the corner points at the trigger.
49
+ */
50
+ function getArrowClasses(side: ArrowSide): string {
51
+ const base =
52
+ "absolute size-2 bg-card border border-border pointer-events-none";
53
+
54
+ switch (side) {
55
+ case "top":
56
+ // Arrow appears on the bottom edge of the card (card is above trigger)
57
+ return cn(
58
+ base,
59
+ "bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 rotate-45",
60
+ "border-t-0 border-l-0"
61
+ );
62
+ case "bottom":
63
+ // Arrow appears on the top edge of the card (card is below trigger)
64
+ return cn(
65
+ base,
66
+ "top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-45",
67
+ "border-b-0 border-r-0"
68
+ );
69
+ case "left":
70
+ // Arrow appears on the right edge of the card (card is left of trigger)
71
+ return cn(
72
+ base,
73
+ "right-0 top-1/2 -translate-y-1/2 translate-x-1/2 rotate-45",
74
+ "border-t-0 border-l-0"
75
+ );
76
+ case "right":
77
+ // Arrow appears on the left edge of the card (card is right of trigger)
78
+ return cn(
79
+ base,
80
+ "left-0 top-1/2 -translate-y-1/2 -translate-x-1/2 rotate-45",
81
+ "border-b-0 border-r-0"
82
+ );
83
+ }
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // InlineDialogContent
88
+ // ---------------------------------------------------------------------------
89
+
90
+ const InlineDialogContent = React.forwardRef<
91
+ React.ElementRef<typeof PopoverPrimitive.Content>,
92
+ InlineDialogContentProps
93
+ >(
94
+ (
95
+ {
96
+ className,
97
+ side = "bottom",
98
+ sideOffset = 8,
99
+ hideClose = false,
100
+ children,
101
+ ...props
102
+ },
103
+ ref
104
+ ) => (
105
+ <PopoverPrimitive.Portal>
106
+ <PopoverPrimitive.Content
107
+ ref={ref}
108
+ side={side}
109
+ sideOffset={sideOffset}
110
+ className={cn(
111
+ // Base card
112
+ "relative z-[120] max-w-xs overflow-visible",
113
+ "rounded-xl border border-border bg-card p-4 text-foreground shadow-lg",
114
+ // Animation
115
+ "data-[state=open]:opacity-100 data-[state=closed]:opacity-0",
116
+ "transition-opacity duration-pg-fast ease-pg-standard",
117
+ "outline-none",
118
+ className
119
+ )}
120
+ {...props}
121
+ >
122
+ {/* Arrow pointer */}
123
+ <span className={getArrowClasses(side)} aria-hidden="true" />
124
+
125
+ {/* Close button */}
126
+ {!hideClose && (
127
+ <PopoverPrimitive.Close
128
+ className={cn(
129
+ "absolute right-2 top-2 z-10",
130
+ "inline-flex size-6 items-center justify-center rounded-md",
131
+ "text-muted-foreground hover:text-foreground",
132
+ "transition-colors duration-pg-fast ease-pg-standard",
133
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
134
+ "disabled:cursor-not-allowed disabled:opacity-50"
135
+ )}
136
+ aria-label="Close"
137
+ >
138
+ <X className="size-3.5" />
139
+ </PopoverPrimitive.Close>
140
+ )}
141
+
142
+ {children}
143
+ </PopoverPrimitive.Content>
144
+ </PopoverPrimitive.Portal>
145
+ )
146
+ );
147
+ InlineDialogContent.displayName = "InlineDialogContent";
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Exports
151
+ // ---------------------------------------------------------------------------
152
+
153
+ export { InlineDialog, InlineDialogTrigger, InlineDialogContent };
@@ -0,0 +1,212 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, Pencil, X } from "lucide-react";
5
+ import { cn } from "./utils";
6
+
7
+ export interface InlineEditProps {
8
+ value: string;
9
+ onConfirm: (val: string) => void;
10
+ placeholder?: string;
11
+ multiline?: boolean;
12
+ disabled?: boolean;
13
+ showButtons?: boolean;
14
+ readClassName?: string;
15
+ inputClassName?: string;
16
+ }
17
+
18
+ const InlineEdit = React.forwardRef<HTMLDivElement, InlineEditProps>(
19
+ (
20
+ {
21
+ value,
22
+ onConfirm,
23
+ placeholder = "Click to edit…",
24
+ multiline = false,
25
+ disabled = false,
26
+ showButtons = false,
27
+ readClassName,
28
+ inputClassName,
29
+ },
30
+ ref
31
+ ) => {
32
+ const [editing, setEditing] = React.useState(false);
33
+ const [draft, setDraft] = React.useState(value);
34
+ const [hovered, setHovered] = React.useState(false);
35
+ const inputRef = React.useRef<HTMLInputElement & HTMLTextAreaElement>(null);
36
+
37
+ React.useEffect(() => {
38
+ if (!editing) setDraft(value);
39
+ }, [value, editing]);
40
+
41
+ React.useEffect(() => {
42
+ if (editing) {
43
+ inputRef.current?.focus();
44
+ if (inputRef.current) {
45
+ const el = inputRef.current;
46
+ const len = el.value.length;
47
+ el.setSelectionRange(len, len);
48
+ }
49
+ }
50
+ }, [editing]);
51
+
52
+ const confirm = React.useCallback(() => {
53
+ setEditing(false);
54
+ if (draft !== value) onConfirm(draft);
55
+ }, [draft, value, onConfirm]);
56
+
57
+ const cancel = React.useCallback(() => {
58
+ setEditing(false);
59
+ setDraft(value);
60
+ }, [value]);
61
+
62
+ const handleKeyDown = React.useCallback(
63
+ (e: React.KeyboardEvent) => {
64
+ if (e.key === "Enter" && !multiline) {
65
+ e.preventDefault();
66
+ confirm();
67
+ } else if (e.key === "Escape") {
68
+ e.preventDefault();
69
+ cancel();
70
+ }
71
+ },
72
+ [confirm, cancel, multiline]
73
+ );
74
+
75
+ const handleBlur = React.useCallback(
76
+ (e: React.FocusEvent) => {
77
+ // If showButtons, don't auto-confirm on blur unless focus leaves the whole group
78
+ if (showButtons) {
79
+ const related = e.relatedTarget as Node | null;
80
+ const container = (e.currentTarget as HTMLElement).closest(
81
+ "[data-inline-edit]"
82
+ );
83
+ if (container && related && container.contains(related)) return;
84
+ }
85
+ confirm();
86
+ },
87
+ [confirm, showButtons]
88
+ );
89
+
90
+ const startEditing = () => {
91
+ if (disabled) return;
92
+ setEditing(true);
93
+ };
94
+
95
+ const sharedInputClass = cn(
96
+ "w-full rounded-md border border-border bg-card px-2 py-1 text-[15px] leading-tight text-foreground shadow-sm",
97
+ "placeholder:text-muted-foreground",
98
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
99
+ "transition-colors duration-pg-fast ease-pg-standard",
100
+ "disabled:cursor-not-allowed disabled:opacity-50",
101
+ inputClassName
102
+ );
103
+
104
+ return (
105
+ <div ref={ref} data-inline-edit="" className="relative inline-flex flex-col gap-1 max-w-full">
106
+ {editing ? (
107
+ <>
108
+ {multiline ? (
109
+ <textarea
110
+ ref={inputRef as React.Ref<HTMLTextAreaElement>}
111
+ value={draft}
112
+ onChange={(e) => setDraft(e.target.value)}
113
+ onKeyDown={handleKeyDown}
114
+ onBlur={handleBlur}
115
+ placeholder={placeholder}
116
+ rows={3}
117
+ className={cn(sharedInputClass, "resize-none leading-relaxed py-1.5")}
118
+ />
119
+ ) : (
120
+ <input
121
+ ref={inputRef as React.Ref<HTMLInputElement>}
122
+ type="text"
123
+ value={draft}
124
+ onChange={(e) => setDraft(e.target.value)}
125
+ onKeyDown={handleKeyDown}
126
+ onBlur={handleBlur}
127
+ placeholder={placeholder}
128
+ className={sharedInputClass}
129
+ />
130
+ )}
131
+
132
+ {showButtons && (
133
+ <div className="flex items-center gap-1">
134
+ <button
135
+ type="button"
136
+ onMouseDown={(e) => {
137
+ e.preventDefault();
138
+ confirm();
139
+ }}
140
+ className={cn(
141
+ "inline-flex items-center justify-center rounded-md p-1",
142
+ "text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-950/40",
143
+ "transition-colors duration-pg-fast ease-pg-standard",
144
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
145
+ )}
146
+ aria-label="Confirm"
147
+ >
148
+ <Check className="size-3.5" />
149
+ </button>
150
+ <button
151
+ type="button"
152
+ onMouseDown={(e) => {
153
+ e.preventDefault();
154
+ cancel();
155
+ }}
156
+ className={cn(
157
+ "inline-flex items-center justify-center rounded-md p-1",
158
+ "text-destructive hover:bg-destructive/10",
159
+ "transition-colors duration-pg-fast ease-pg-standard",
160
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
161
+ )}
162
+ aria-label="Cancel"
163
+ >
164
+ <X className="size-3.5" />
165
+ </button>
166
+ </div>
167
+ )}
168
+ </>
169
+ ) : (
170
+ <span
171
+ role="button"
172
+ tabIndex={disabled ? -1 : 0}
173
+ aria-label={value || placeholder}
174
+ onMouseEnter={() => setHovered(true)}
175
+ onMouseLeave={() => setHovered(false)}
176
+ onClick={startEditing}
177
+ onKeyDown={(e) => {
178
+ if (e.key === "Enter" || e.key === " ") {
179
+ e.preventDefault();
180
+ startEditing();
181
+ }
182
+ }}
183
+ className={cn(
184
+ "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[15px] leading-tight",
185
+ "transition-colors duration-pg-fast ease-pg-standard",
186
+ !disabled && "hover:bg-muted cursor-text",
187
+ disabled && "cursor-not-allowed opacity-50",
188
+ readClassName
189
+ )}
190
+ >
191
+ <span className={cn(!value && "text-muted-foreground")}>
192
+ {value || placeholder}
193
+ </span>
194
+ {!disabled && (
195
+ <Pencil
196
+ className={cn(
197
+ "size-3.5 shrink-0 text-muted-foreground transition-opacity duration-pg-fast ease-pg-standard",
198
+ hovered ? "opacity-100" : "opacity-0"
199
+ )}
200
+ aria-hidden
201
+ />
202
+ )}
203
+ </span>
204
+ )}
205
+ </div>
206
+ );
207
+ }
208
+ );
209
+
210
+ InlineEdit.displayName = "InlineEdit";
211
+
212
+ export { InlineEdit };
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "./utils";
7
+ import { Input } from "./input";
8
+ import { Textarea } from "./textarea";
9
+
10
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
11
+ return (
12
+ <div
13
+ data-slot="input-group"
14
+ role="group"
15
+ className={cn(
16
+ "group/input-group relative flex w-full items-stretch rounded-xl border border-border bg-card shadow-sm outline-none transition-[color,box-shadow] dark:bg-card/50",
17
+ "min-h-11 h-11 has-[>textarea]:h-auto",
18
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
19
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
20
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
21
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
22
+ "has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring",
23
+ "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ const inputGroupAddonVariants = cva(
32
+ "text-muted-foreground flex h-auto min-h-11 cursor-text select-none items-center justify-center gap-2 px-0 text-[15px] font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
33
+ {
34
+ variants: {
35
+ align: {
36
+ "inline-start":
37
+ "order-first pl-3.5 has-[>button]:-ml-[0.45rem] has-[>kbd]:-ml-[0.35rem]",
38
+ "inline-end":
39
+ "order-last pr-3.5 has-[>button]:-mr-[0.4rem] has-[>kbd]:-mr-[0.35rem]",
40
+ "block-start":
41
+ "order-first w-full justify-start border-b px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
42
+ "block-end":
43
+ "order-last w-full justify-start border-t px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ align: "inline-start",
48
+ },
49
+ }
50
+ );
51
+
52
+ function InputGroupAddon({
53
+ className,
54
+ align = "inline-start",
55
+ ...props
56
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
57
+ return (
58
+ <div
59
+ role="group"
60
+ data-slot="input-group-addon"
61
+ data-align={align}
62
+ className={cn(inputGroupAddonVariants({ align }), className)}
63
+ onClick={(e) => {
64
+ if ((e.target as HTMLElement).closest("button")) {
65
+ return;
66
+ }
67
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
68
+ }}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ const inputGroupButtonVariants = cva(
75
+ "inline-flex items-center justify-center gap-2 border border-transparent bg-transparent text-sm font-medium text-foreground shadow-none transition-colors hover:bg-muted",
76
+ {
77
+ variants: {
78
+ size: {
79
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
80
+ sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
81
+ "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
82
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
83
+ },
84
+ },
85
+ defaultVariants: {
86
+ size: "xs",
87
+ },
88
+ }
89
+ );
90
+
91
+ function InputGroupButton({
92
+ className,
93
+ type = "button",
94
+ size = "xs",
95
+ ...props
96
+ }: React.ComponentProps<"button"> & VariantProps<typeof inputGroupButtonVariants>) {
97
+ return (
98
+ <button
99
+ type={type}
100
+ className={cn(inputGroupButtonVariants({ size }), className)}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
107
+ return (
108
+ <span
109
+ className={cn(
110
+ "text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
111
+ className
112
+ )}
113
+ {...props}
114
+ />
115
+ );
116
+ }
117
+
118
+ function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
119
+ return (
120
+ <Input
121
+ data-slot="input-group-control"
122
+ className={cn(
123
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
124
+ className
125
+ )}
126
+ {...props}
127
+ />
128
+ );
129
+ }
130
+
131
+ function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
132
+ return (
133
+ <Textarea
134
+ data-slot="input-group-control"
135
+ className={cn(
136
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
137
+ className
138
+ )}
139
+ {...props}
140
+ />
141
+ );
142
+ }
143
+
144
+ export {
145
+ InputGroup,
146
+ InputGroupAddon,
147
+ InputGroupButton,
148
+ InputGroupText,
149
+ InputGroupInput,
150
+ InputGroupTextarea,
151
+ };
package/src/input.tsx ADDED
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-11 min-h-11 w-full rounded-lg border border-border bg-card px-4 py-2 text-[15px] leading-tight shadow-sm transition-colors",
12
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
13
+ "placeholder:text-muted-foreground",
14
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
15
+ "disabled:cursor-not-allowed disabled:bg-muted/40 disabled:opacity-60",
16
+ "read-only:bg-muted/25 read-only:text-muted-foreground",
17
+ "aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/25 dark:aria-invalid:ring-destructive/40",
18
+ className
19
+ )}
20
+ ref={ref}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+ );
26
+ Input.displayName = "Input";
27
+
28
+ export { Input };
package/src/label.tsx ADDED
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ const labelVariants = cva(
10
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11
+ );
12
+
13
+ const Label = React.forwardRef<
14
+ React.ElementRef<typeof LabelPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
16
+ >(({ className, ...props }, ref) => (
17
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
18
+ ));
19
+ Label.displayName = LabelPrimitive.Root.displayName;
20
+
21
+ export { Label };
package/src/layout.tsx ADDED
@@ -0,0 +1,119 @@
1
+ import * as React from "react";
2
+ import { cn } from "./utils";
3
+
4
+ /** Maps to Tailwind spacing steps used across PayGlocal dashboard surfaces */
5
+ export type LayoutSpacing = "none" | "xs" | "sm" | "md" | "lg" | "xl";
6
+
7
+ const gapClass: Record<LayoutSpacing, string> = {
8
+ none: "gap-0",
9
+ xs: "gap-1",
10
+ sm: "gap-2",
11
+ md: "gap-3",
12
+ lg: "gap-4",
13
+ xl: "gap-6",
14
+ };
15
+
16
+ const padClass: Record<LayoutSpacing, string> = {
17
+ none: "p-0",
18
+ xs: "p-1",
19
+ sm: "p-2",
20
+ md: "p-4",
21
+ lg: "p-6",
22
+ xl: "p-8",
23
+ };
24
+
25
+ const padXClass: Record<LayoutSpacing, string> = {
26
+ none: "px-0",
27
+ xs: "px-1",
28
+ sm: "px-2",
29
+ md: "px-4",
30
+ lg: "px-6",
31
+ xl: "px-8",
32
+ };
33
+
34
+ const padYClass: Record<LayoutSpacing, string> = {
35
+ none: "py-0",
36
+ xs: "py-1",
37
+ sm: "py-2",
38
+ md: "py-4",
39
+ lg: "py-6",
40
+ xl: "py-8",
41
+ };
42
+
43
+ export type BoxProps = React.HTMLAttributes<HTMLDivElement> & {
44
+ as?: React.ElementType;
45
+ p?: LayoutSpacing;
46
+ px?: LayoutSpacing;
47
+ py?: LayoutSpacing;
48
+ };
49
+
50
+ /** Neutral block with token-aligned padding; extend with `className` for radius, borders, flex, etc. */
51
+ export function Box({ as: Comp = "div", className, p = "none", px, py, ...props }: BoxProps) {
52
+ const axis = px !== undefined || py !== undefined;
53
+ return (
54
+ <Comp
55
+ className={cn(
56
+ !axis && padClass[p],
57
+ px !== undefined && padXClass[px],
58
+ py !== undefined && padYClass[py],
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ export type StackProps = React.HTMLAttributes<HTMLDivElement> & {
67
+ as?: React.ElementType;
68
+ gap?: LayoutSpacing;
69
+ align?: "start" | "center" | "stretch" | "end";
70
+ };
71
+
72
+ /** Vertical flex stack; default gap matches common section rhythm (`gap-4`). */
73
+ export function Stack({
74
+ as: Comp = "div",
75
+ className,
76
+ gap = "lg",
77
+ align = "stretch",
78
+ ...props
79
+ }: StackProps) {
80
+ const alignCls =
81
+ align === "start"
82
+ ? "items-start"
83
+ : align === "center"
84
+ ? "items-center"
85
+ : align === "end"
86
+ ? "items-end"
87
+ : "items-stretch";
88
+ return <Comp className={cn("flex flex-col", gapClass[gap], alignCls, className)} {...props} />;
89
+ }
90
+
91
+ export type InlineProps = React.HTMLAttributes<HTMLDivElement> & {
92
+ gap?: LayoutSpacing;
93
+ wrap?: boolean;
94
+ justify?: "start" | "center" | "end" | "between";
95
+ };
96
+
97
+ /** Horizontal flex row for toolbars and inline field groups */
98
+ export function Inline({
99
+ className,
100
+ gap = "md",
101
+ wrap = false,
102
+ justify = "start",
103
+ ...props
104
+ }: InlineProps) {
105
+ const justifyCls =
106
+ justify === "center"
107
+ ? "justify-center"
108
+ : justify === "end"
109
+ ? "justify-end"
110
+ : justify === "between"
111
+ ? "justify-between"
112
+ : "justify-start";
113
+ return (
114
+ <div
115
+ className={cn("flex flex-row items-center", gapClass[gap], justifyCls, wrap && "flex-wrap", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }