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