@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
|
@@ -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
|
+
}
|