@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/code.tsx
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { forwardRef, type HTMLAttributes, useState, useCallback } from "react";
|
|
5
|
+
import { Check, Copy } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Code – inline
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export type CodeProps = HTMLAttributes<HTMLElement>;
|
|
12
|
+
|
|
13
|
+
export const Code = forwardRef<HTMLElement, CodeProps>(
|
|
14
|
+
({ className, children, ...props }, ref) => {
|
|
15
|
+
return (
|
|
16
|
+
<code
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[13px] text-foreground ring-1 ring-border",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</code>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
Code.displayName = "Code";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// CodeBlock – multi-line
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
|
|
36
|
+
/** The raw code string to display and optionally copy. */
|
|
37
|
+
code: string;
|
|
38
|
+
/** Optional filename shown in the header. */
|
|
39
|
+
filename?: string;
|
|
40
|
+
/** Optional language label shown as a badge in the header. */
|
|
41
|
+
language?: string;
|
|
42
|
+
/** Hide the copy button. Defaults to false. */
|
|
43
|
+
hideCopy?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
code,
|
|
50
|
+
filename,
|
|
51
|
+
language,
|
|
52
|
+
hideCopy = false,
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
},
|
|
56
|
+
ref
|
|
57
|
+
) => {
|
|
58
|
+
const [copied, setCopied] = useState(false);
|
|
59
|
+
|
|
60
|
+
const handleCopy = useCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(code);
|
|
63
|
+
setCopied(true);
|
|
64
|
+
const timer = setTimeout(() => setCopied(false), 2000);
|
|
65
|
+
return () => clearTimeout(timer);
|
|
66
|
+
} catch {
|
|
67
|
+
// Clipboard access denied — fail silently.
|
|
68
|
+
}
|
|
69
|
+
}, [code]);
|
|
70
|
+
|
|
71
|
+
const hasHeader = Boolean(filename || language);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn(
|
|
77
|
+
"relative overflow-hidden bg-[#0f1117] text-[#e5e7eb]",
|
|
78
|
+
hasHeader ? "rounded-xl" : "rounded-xl",
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
{hasHeader && (
|
|
85
|
+
<div className="flex items-center justify-between bg-[#1a1f2e] px-4 py-2 border-b border-[#2a3441]">
|
|
86
|
+
<div className="flex items-center gap-2.5">
|
|
87
|
+
{filename && (
|
|
88
|
+
<span className="text-[#9ca3af] text-xs font-mono">
|
|
89
|
+
{filename}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
{language && (
|
|
93
|
+
<span className="rounded-md bg-[#2a3441] px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[#6b7280]">
|
|
94
|
+
{language}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
{!hideCopy && (
|
|
99
|
+
<CopyButton copied={copied} onClick={handleCopy} inHeader />
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Code area */}
|
|
105
|
+
<div className="relative">
|
|
106
|
+
<pre className="overflow-x-auto p-4 font-mono text-[13px] leading-relaxed">
|
|
107
|
+
<code>{code}</code>
|
|
108
|
+
</pre>
|
|
109
|
+
|
|
110
|
+
{/* Copy button (no header case) */}
|
|
111
|
+
{!hasHeader && !hideCopy && (
|
|
112
|
+
<div className="absolute top-3 right-3">
|
|
113
|
+
<CopyButton copied={copied} onClick={handleCopy} />
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
CodeBlock.displayName = "CodeBlock";
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Internal: CopyButton
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
interface CopyButtonProps {
|
|
128
|
+
copied: boolean;
|
|
129
|
+
onClick: () => void;
|
|
130
|
+
inHeader?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function CopyButton({ copied, onClick, inHeader = false }: CopyButtonProps) {
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
className={cn(
|
|
140
|
+
"inline-flex items-center justify-center rounded-md transition-colors duration-pg-fast ease-pg-standard",
|
|
141
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
142
|
+
inHeader
|
|
143
|
+
? "size-6 text-[#6b7280] hover:bg-[#2a3441] hover:text-[#9ca3af]"
|
|
144
|
+
: "size-7 bg-[#1a1f2e]/80 text-[#6b7280] hover:bg-[#1a1f2e] hover:text-[#9ca3af]"
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{copied ? (
|
|
148
|
+
<Check className="size-3.5 text-green-400" />
|
|
149
|
+
) : (
|
|
150
|
+
<Copy className="size-3.5" />
|
|
151
|
+
)}
|
|
152
|
+
</button>
|
|
153
|
+
);
|
|
154
|
+
}
|
package/src/command.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Search } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export interface CommandProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
8
|
+
export interface CommandInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
9
|
+
export interface CommandListProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
10
|
+
export interface CommandEmptyProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
11
|
+
export interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> { heading?: string; }
|
|
12
|
+
export interface CommandItemProps extends React.HTMLAttributes<HTMLDivElement> { selected?: boolean; disabled?: boolean; onSelect?: () => void; }
|
|
13
|
+
export interface CommandSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
14
|
+
export interface CommandShortcutProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
15
|
+
|
|
16
|
+
const Command = React.forwardRef<HTMLDivElement, CommandProps>(({ className, ...props }, ref) => (
|
|
17
|
+
<div ref={ref} className={cn("bg-card rounded-xl border border-border shadow-lg overflow-hidden", className)} {...props} />
|
|
18
|
+
));
|
|
19
|
+
Command.displayName = "Command";
|
|
20
|
+
|
|
21
|
+
const CommandInput = React.forwardRef<HTMLInputElement, CommandInputProps>(({ className, ...props }, ref) => (
|
|
22
|
+
<div className="flex items-center gap-2 border-b border-border px-3 h-11">
|
|
23
|
+
<Search className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
24
|
+
<input ref={ref} className={cn("flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed", className)} {...props} />
|
|
25
|
+
</div>
|
|
26
|
+
));
|
|
27
|
+
CommandInput.displayName = "CommandInput";
|
|
28
|
+
|
|
29
|
+
const CommandList = React.forwardRef<HTMLDivElement, CommandListProps>(({ className, ...props }, ref) => (
|
|
30
|
+
<div ref={ref} className={cn("max-h-72 overflow-y-auto py-1", className)} role="listbox" {...props} />
|
|
31
|
+
));
|
|
32
|
+
CommandList.displayName = "CommandList";
|
|
33
|
+
|
|
34
|
+
const CommandEmpty = React.forwardRef<HTMLDivElement, CommandEmptyProps>(({ className, ...props }, ref) => (
|
|
35
|
+
<div ref={ref} className={cn("text-sm text-muted-foreground text-center py-6", className)} {...props} />
|
|
36
|
+
));
|
|
37
|
+
CommandEmpty.displayName = "CommandEmpty";
|
|
38
|
+
|
|
39
|
+
const CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(({ className, heading, children, ...props }, ref) => (
|
|
40
|
+
<div ref={ref} className={cn("px-1 py-1", className)} role="group" {...props}>
|
|
41
|
+
{heading && <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">{heading}</div>}
|
|
42
|
+
{children}
|
|
43
|
+
</div>
|
|
44
|
+
));
|
|
45
|
+
CommandGroup.displayName = "CommandGroup";
|
|
46
|
+
|
|
47
|
+
const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(({ className, selected, disabled, onSelect, children, ...props }, ref) => (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
role="option"
|
|
51
|
+
aria-selected={selected}
|
|
52
|
+
aria-disabled={disabled}
|
|
53
|
+
onClick={disabled ? undefined : onSelect}
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer transition-colors duration-pg-fast ease-pg-standard select-none",
|
|
56
|
+
selected ? "bg-muted text-foreground" : "text-foreground hover:bg-muted",
|
|
57
|
+
disabled && "pointer-events-none opacity-50",
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
));
|
|
65
|
+
CommandItem.displayName = "CommandItem";
|
|
66
|
+
|
|
67
|
+
const CommandSeparator = React.forwardRef<HTMLDivElement, CommandSeparatorProps>(({ className, ...props }, ref) => (
|
|
68
|
+
<div ref={ref} className={cn("h-px bg-border my-1 -mx-1", className)} {...props} />
|
|
69
|
+
));
|
|
70
|
+
CommandSeparator.displayName = "CommandSeparator";
|
|
71
|
+
|
|
72
|
+
const CommandShortcut = React.forwardRef<HTMLSpanElement, CommandShortcutProps>(({ className, ...props }, ref) => (
|
|
73
|
+
<span ref={ref} className={cn("ml-auto text-xs text-muted-foreground tracking-widest", className)} {...props} />
|
|
74
|
+
));
|
|
75
|
+
CommandShortcut.displayName = "CommandShortcut";
|
|
76
|
+
|
|
77
|
+
export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
|
+
import { Check, ChevronDown, Search } from "lucide-react";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
export interface Country {
|
|
9
|
+
code: string;
|
|
10
|
+
name: string;
|
|
11
|
+
flag: string;
|
|
12
|
+
dialCode: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const COUNTRIES: Country[] = [
|
|
16
|
+
{ code: "US", name: "United States", flag: "🇺🇸", dialCode: "+1" },
|
|
17
|
+
{ code: "GB", name: "United Kingdom", flag: "🇬🇧", dialCode: "+44" },
|
|
18
|
+
{ code: "IN", name: "India", flag: "🇮🇳", dialCode: "+91" },
|
|
19
|
+
{ code: "AU", name: "Australia", flag: "🇦🇺", dialCode: "+61" },
|
|
20
|
+
{ code: "CA", name: "Canada", flag: "🇨🇦", dialCode: "+1" },
|
|
21
|
+
{ code: "DE", name: "Germany", flag: "🇩🇪", dialCode: "+49" },
|
|
22
|
+
{ code: "FR", name: "France", flag: "🇫🇷", dialCode: "+33" },
|
|
23
|
+
{ code: "JP", name: "Japan", flag: "🇯🇵", dialCode: "+81" },
|
|
24
|
+
{ code: "SG", name: "Singapore", flag: "🇸🇬", dialCode: "+65" },
|
|
25
|
+
{ code: "AE", name: "United Arab Emirates", flag: "🇦🇪", dialCode: "+971" },
|
|
26
|
+
{ code: "CN", name: "China", flag: "🇨🇳", dialCode: "+86" },
|
|
27
|
+
{ code: "BR", name: "Brazil", flag: "🇧🇷", dialCode: "+55" },
|
|
28
|
+
{ code: "MX", name: "Mexico", flag: "🇲🇽", dialCode: "+52" },
|
|
29
|
+
{ code: "KR", name: "South Korea", flag: "🇰🇷", dialCode: "+82" },
|
|
30
|
+
{ code: "IT", name: "Italy", flag: "🇮🇹", dialCode: "+39" },
|
|
31
|
+
{ code: "ES", name: "Spain", flag: "🇪🇸", dialCode: "+34" },
|
|
32
|
+
{ code: "NL", name: "Netherlands", flag: "🇳🇱", dialCode: "+31" },
|
|
33
|
+
{ code: "SE", name: "Sweden", flag: "🇸🇪", dialCode: "+46" },
|
|
34
|
+
{ code: "NO", name: "Norway", flag: "🇳🇴", dialCode: "+47" },
|
|
35
|
+
{ code: "DK", name: "Denmark", flag: "🇩🇰", dialCode: "+45" },
|
|
36
|
+
{ code: "FI", name: "Finland", flag: "🇫🇮", dialCode: "+358" },
|
|
37
|
+
{ code: "CH", name: "Switzerland", flag: "🇨🇭", dialCode: "+41" },
|
|
38
|
+
{ code: "NZ", name: "New Zealand", flag: "🇳🇿", dialCode: "+64" },
|
|
39
|
+
{ code: "ZA", name: "South Africa", flag: "🇿🇦", dialCode: "+27" },
|
|
40
|
+
{ code: "NG", name: "Nigeria", flag: "🇳🇬", dialCode: "+234" },
|
|
41
|
+
{ code: "EG", name: "Egypt", flag: "🇪🇬", dialCode: "+20" },
|
|
42
|
+
{ code: "PK", name: "Pakistan", flag: "🇵🇰", dialCode: "+92" },
|
|
43
|
+
{ code: "BD", name: "Bangladesh", flag: "🇧🇩", dialCode: "+880" },
|
|
44
|
+
{ code: "ID", name: "Indonesia", flag: "🇮🇩", dialCode: "+62" },
|
|
45
|
+
{ code: "TH", name: "Thailand", flag: "🇹🇭", dialCode: "+66" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export interface CountrySelectProps {
|
|
49
|
+
value?: string;
|
|
50
|
+
onValueChange?: (code: string) => void;
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
showDialCode?: boolean;
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
className?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const CountrySelect = React.forwardRef<HTMLButtonElement, CountrySelectProps>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
value,
|
|
61
|
+
onValueChange,
|
|
62
|
+
placeholder = "Select country",
|
|
63
|
+
showDialCode = false,
|
|
64
|
+
disabled = false,
|
|
65
|
+
className,
|
|
66
|
+
},
|
|
67
|
+
ref
|
|
68
|
+
) => {
|
|
69
|
+
const [open, setOpen] = React.useState(false);
|
|
70
|
+
const [search, setSearch] = React.useState("");
|
|
71
|
+
const searchRef = React.useRef<HTMLInputElement>(null);
|
|
72
|
+
|
|
73
|
+
const selected = React.useMemo(
|
|
74
|
+
() => COUNTRIES.find((c) => c.code === value),
|
|
75
|
+
[value]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const filtered = React.useMemo(() => {
|
|
79
|
+
const q = search.trim().toLowerCase();
|
|
80
|
+
if (!q) return COUNTRIES;
|
|
81
|
+
return COUNTRIES.filter(
|
|
82
|
+
(c) =>
|
|
83
|
+
c.name.toLowerCase().includes(q) ||
|
|
84
|
+
c.code.toLowerCase().includes(q) ||
|
|
85
|
+
c.dialCode.includes(q)
|
|
86
|
+
);
|
|
87
|
+
}, [search]);
|
|
88
|
+
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (open) {
|
|
91
|
+
// Delay focus until popover has mounted
|
|
92
|
+
const id = setTimeout(() => searchRef.current?.focus(), 0);
|
|
93
|
+
return () => clearTimeout(id);
|
|
94
|
+
} else {
|
|
95
|
+
setSearch("");
|
|
96
|
+
}
|
|
97
|
+
}, [open]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
101
|
+
<PopoverPrimitive.Trigger asChild>
|
|
102
|
+
<button
|
|
103
|
+
ref={ref}
|
|
104
|
+
type="button"
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
aria-expanded={open}
|
|
107
|
+
aria-haspopup="listbox"
|
|
108
|
+
className={cn(
|
|
109
|
+
"flex h-11 min-h-11 w-full items-center justify-between gap-2.5 rounded-lg border border-border bg-card px-4 text-[15px] text-foreground shadow-sm",
|
|
110
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
111
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
112
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
113
|
+
!selected && "text-muted-foreground",
|
|
114
|
+
className
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<span className="flex min-w-0 items-center gap-2.5">
|
|
118
|
+
{selected ? (
|
|
119
|
+
<>
|
|
120
|
+
<span className="text-base leading-none" aria-hidden="true">
|
|
121
|
+
{selected.flag}
|
|
122
|
+
</span>
|
|
123
|
+
<span className="truncate">{selected.name}</span>
|
|
124
|
+
{showDialCode && (
|
|
125
|
+
<span className="shrink-0 text-muted-foreground">
|
|
126
|
+
{selected.dialCode}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</>
|
|
130
|
+
) : (
|
|
131
|
+
<span>{placeholder}</span>
|
|
132
|
+
)}
|
|
133
|
+
</span>
|
|
134
|
+
<ChevronDown
|
|
135
|
+
className={cn(
|
|
136
|
+
"h-4 w-4 shrink-0 text-muted-foreground opacity-70 transition-transform duration-pg-fast ease-pg-standard",
|
|
137
|
+
open && "rotate-180"
|
|
138
|
+
)}
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
/>
|
|
141
|
+
</button>
|
|
142
|
+
</PopoverPrimitive.Trigger>
|
|
143
|
+
|
|
144
|
+
<PopoverPrimitive.Portal>
|
|
145
|
+
<PopoverPrimitive.Content
|
|
146
|
+
align="start"
|
|
147
|
+
sideOffset={6}
|
|
148
|
+
className={cn(
|
|
149
|
+
"z-[120] w-[var(--radix-popover-trigger-width)] min-w-[260px] rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none",
|
|
150
|
+
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150"
|
|
151
|
+
)}
|
|
152
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
153
|
+
>
|
|
154
|
+
{/* Search */}
|
|
155
|
+
<div className="flex items-center gap-2 border-b border-border px-3 py-2.5">
|
|
156
|
+
<Search className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
157
|
+
<input
|
|
158
|
+
ref={searchRef}
|
|
159
|
+
type="text"
|
|
160
|
+
value={search}
|
|
161
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
162
|
+
placeholder="Search countries…"
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex-1 bg-transparent text-[14px] text-foreground placeholder:text-muted-foreground",
|
|
165
|
+
"focus-visible:outline-none"
|
|
166
|
+
)}
|
|
167
|
+
aria-label="Search countries"
|
|
168
|
+
/>
|
|
169
|
+
{search && (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={() => setSearch("")}
|
|
173
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors duration-pg-fast ease-pg-standard"
|
|
174
|
+
aria-label="Clear search"
|
|
175
|
+
>
|
|
176
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
177
|
+
<path d="M10.5 3.5L3.5 10.5M3.5 3.5L10.5 10.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
178
|
+
</svg>
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* List */}
|
|
184
|
+
<div
|
|
185
|
+
role="listbox"
|
|
186
|
+
aria-label="Countries"
|
|
187
|
+
className="max-h-64 overflow-y-auto p-1"
|
|
188
|
+
>
|
|
189
|
+
{filtered.length === 0 ? (
|
|
190
|
+
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
191
|
+
No countries found
|
|
192
|
+
</div>
|
|
193
|
+
) : (
|
|
194
|
+
filtered.map((country) => {
|
|
195
|
+
const isSelected = country.code === value;
|
|
196
|
+
return (
|
|
197
|
+
<button
|
|
198
|
+
key={country.code}
|
|
199
|
+
role="option"
|
|
200
|
+
aria-selected={isSelected}
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={() => {
|
|
203
|
+
onValueChange?.(country.code);
|
|
204
|
+
setOpen(false);
|
|
205
|
+
}}
|
|
206
|
+
className={cn(
|
|
207
|
+
"flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm",
|
|
208
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
209
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
210
|
+
"hover:bg-muted",
|
|
211
|
+
isSelected && "bg-muted/60"
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<span className="text-base leading-none" aria-hidden="true">
|
|
215
|
+
{country.flag}
|
|
216
|
+
</span>
|
|
217
|
+
<span className="flex-1 truncate text-foreground">
|
|
218
|
+
{country.name}
|
|
219
|
+
</span>
|
|
220
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
221
|
+
{country.dialCode}
|
|
222
|
+
</span>
|
|
223
|
+
{isSelected && (
|
|
224
|
+
<Check
|
|
225
|
+
className="h-3.5 w-3.5 shrink-0 text-primary"
|
|
226
|
+
aria-hidden="true"
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
</button>
|
|
230
|
+
);
|
|
231
|
+
})
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
</PopoverPrimitive.Content>
|
|
235
|
+
</PopoverPrimitive.Portal>
|
|
236
|
+
</PopoverPrimitive.Root>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
CountrySelect.displayName = "CountrySelect";
|
|
241
|
+
|
|
242
|
+
export { CountrySelect };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDown } from "lucide-react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
const CURRENCIES = ["USD", "INR", "EUR", "GBP", "SGD", "AED", "JPY"];
|
|
7
|
+
|
|
8
|
+
interface CurrencyAmountInputProps {
|
|
9
|
+
currency: string;
|
|
10
|
+
amount: string;
|
|
11
|
+
onCurrencyChange: (currency: string) => void;
|
|
12
|
+
onAmountChange: (amount: string) => void;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
currencies?: string[];
|
|
17
|
+
id?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CurrencyAmountInput({
|
|
21
|
+
currency,
|
|
22
|
+
amount,
|
|
23
|
+
onCurrencyChange,
|
|
24
|
+
onAmountChange,
|
|
25
|
+
placeholder = "0.00",
|
|
26
|
+
disabled = false,
|
|
27
|
+
required = false,
|
|
28
|
+
currencies = CURRENCIES,
|
|
29
|
+
id,
|
|
30
|
+
}: CurrencyAmountInputProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex h-12 min-h-12 items-stretch overflow-hidden rounded-xl border border-border bg-card text-[15px] transition-all focus-within:border-muted-foreground/50 focus-within:ring-2 focus-within:ring-ring/20">
|
|
33
|
+
<div className="relative flex min-w-[5.25rem] shrink-0 items-center border-r border-border bg-muted/25 px-3">
|
|
34
|
+
<select
|
|
35
|
+
value={currency}
|
|
36
|
+
onChange={(e) => onCurrencyChange(e.target.value)}
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
aria-label="Currency"
|
|
39
|
+
className={cn(
|
|
40
|
+
"h-full min-h-0 w-full min-w-0 cursor-pointer bg-transparent py-2 pl-1 pr-9 text-[15px] font-semibold text-foreground",
|
|
41
|
+
"focus:outline-none disabled:opacity-50",
|
|
42
|
+
"appearance-none [-webkit-appearance:none] [-moz-appearance:none]",
|
|
43
|
+
"[&::-ms-expand]:hidden"
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{currencies.map((c) => (
|
|
47
|
+
<option key={c} value={c}>
|
|
48
|
+
{c}
|
|
49
|
+
</option>
|
|
50
|
+
))}
|
|
51
|
+
</select>
|
|
52
|
+
<ChevronDown
|
|
53
|
+
className="pointer-events-none absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground opacity-80"
|
|
54
|
+
aria-hidden
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<input
|
|
59
|
+
id={id}
|
|
60
|
+
type="number"
|
|
61
|
+
value={amount}
|
|
62
|
+
onChange={(e) => onAmountChange(e.target.value)}
|
|
63
|
+
placeholder={placeholder}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
required={required}
|
|
66
|
+
min="0"
|
|
67
|
+
step="0.01"
|
|
68
|
+
className="min-h-0 min-w-0 flex-1 bg-transparent px-4 py-3 text-[15px] tabular-nums text-foreground placeholder:text-muted-foreground focus:outline-none disabled:opacity-50"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|