@moontra/moonui 2.1.9 โ 2.2.1
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/dist/index.d.mts +1656 -0
- package/dist/index.d.ts +1656 -0
- package/dist/index.js +1847 -469
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1679 -471
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/alert.tsx +123 -93
- package/src/components/ui/aspect-ratio.tsx +1 -1
- package/src/components/ui/avatar.tsx +8 -6
- package/src/components/ui/badge.tsx +20 -18
- package/src/components/ui/breadcrumb.tsx +12 -10
- package/src/components/ui/button.stories.tsx +4 -2
- package/src/components/ui/button.tsx +71 -68
- package/src/components/ui/calendar.tsx +2 -2
- package/src/components/ui/card-input.tsx +231 -0
- package/src/components/ui/card.stories.tsx +2 -0
- package/src/components/ui/card.tsx +9 -7
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/collapsible.tsx +3 -3
- package/src/components/ui/color-picker.tsx +2 -2
- package/src/components/ui/command.tsx +4 -4
- package/src/components/ui/data-table.stories.tsx +4 -2
- package/src/components/ui/data-table.tsx +8 -8
- package/src/components/ui/date-picker.stories.tsx +2 -0
- package/src/components/ui/date-picker.tsx +25 -15
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/ui/draggable-list.tsx +3 -1
- package/src/components/ui/dropdown-menu.tsx +2 -2
- package/src/components/ui/file-upload.tsx +2 -2
- package/src/components/ui/github-stars.tsx +1 -1
- package/src/components/ui/index.ts +357 -45
- package/src/components/ui/input.stories.tsx +2 -0
- package/src/components/ui/input.tsx +3 -1
- package/src/components/ui/locked-component.tsx +222 -0
- package/src/components/ui/moon-logo.tsx +1 -1
- package/src/components/ui/pagination.tsx +10 -8
- package/src/components/ui/phone-input.tsx +172 -0
- package/src/components/ui/popover-pro.tsx +424 -0
- package/src/components/ui/popover.tsx +3 -3
- package/src/components/ui/progress.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/rich-text-editor/index.tsx +2 -1
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +7 -3
- package/src/components/ui/separator.tsx +2 -2
- package/src/components/ui/skeleton.tsx +19 -7
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/ui/swipeable-card.tsx +2 -0
- package/src/components/ui/switch.tsx +1 -1
- package/src/components/ui/table.tsx +7 -5
- package/src/components/ui/tabs.tsx +6 -1
- package/src/components/ui/tags-input.tsx +130 -0
- package/src/components/ui/textarea.tsx +3 -1
- package/src/components/ui/toast.tsx +5 -3
- package/src/components/ui/toggle.tsx +2 -3
- package/src/components/ui/tooltip.tsx +2 -5
- package/src/index.tsx +4 -46
- package/src/lib/micro-interactions.ts +2 -1
- package/src/lib/performance-profiler.ts +79 -0
- package/src/styles/index.css +315 -2
- package/src/use-performance-optimizer.ts +26 -26
- package/src/use-scroll-animation.ts +5 -7
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
// Locked Component UI
|
|
4
|
+
// Pro componentlerin locked state'ini gรถsteren wrapper component
|
|
5
|
+
|
|
6
|
+
import React, { useState } from 'react'
|
|
7
|
+
import { cn } from '../../lib/utils'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
import { Badge } from './badge'
|
|
10
|
+
import { Lock, Crown, Zap } from 'lucide-react'
|
|
11
|
+
|
|
12
|
+
interface LockedComponentProps {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
componentName: string
|
|
15
|
+
className?: string
|
|
16
|
+
showPreview?: boolean
|
|
17
|
+
previewDuration?: number // ms
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function LockedComponent({
|
|
21
|
+
children,
|
|
22
|
+
componentName,
|
|
23
|
+
className,
|
|
24
|
+
showPreview = true,
|
|
25
|
+
previewDuration = 2000
|
|
26
|
+
}: LockedComponentProps) {
|
|
27
|
+
const [isPreviewActive, setIsPreviewActive] = useState(false)
|
|
28
|
+
const [previewTimeout, setPreviewTimeout] = useState<NodeJS.Timeout | null>(null)
|
|
29
|
+
|
|
30
|
+
const handleMouseEnter = () => {
|
|
31
|
+
if (!showPreview) return
|
|
32
|
+
|
|
33
|
+
setIsPreviewActive(true)
|
|
34
|
+
|
|
35
|
+
// Clear existing timeout
|
|
36
|
+
if (previewTimeout) {
|
|
37
|
+
clearTimeout(previewTimeout)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set new timeout
|
|
41
|
+
const timeout = setTimeout(() => {
|
|
42
|
+
setIsPreviewActive(false)
|
|
43
|
+
}, previewDuration)
|
|
44
|
+
|
|
45
|
+
setPreviewTimeout(timeout)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleMouseLeave = () => {
|
|
49
|
+
if (previewTimeout) {
|
|
50
|
+
clearTimeout(previewTimeout)
|
|
51
|
+
setPreviewTimeout(null)
|
|
52
|
+
}
|
|
53
|
+
setIsPreviewActive(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"relative group cursor-pointer transition-all duration-300",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
onMouseEnter={handleMouseEnter}
|
|
63
|
+
onMouseLeave={handleMouseLeave}
|
|
64
|
+
>
|
|
65
|
+
{/* Component Content */}
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
"transition-all duration-300",
|
|
69
|
+
!isPreviewActive && "blur-sm grayscale-[0.3] opacity-60"
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Overlay */}
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
"absolute inset-0 bg-gradient-to-br from-amber-500/10 via-transparent to-purple-500/10",
|
|
79
|
+
"border-2 border-dashed border-amber-400/30 rounded-lg",
|
|
80
|
+
"flex items-center justify-center",
|
|
81
|
+
"transition-all duration-300",
|
|
82
|
+
isPreviewActive ? "opacity-0" : "opacity-100"
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<div className="text-center space-y-3 p-6">
|
|
86
|
+
{/* Pro Badge */}
|
|
87
|
+
<div className="flex justify-center">
|
|
88
|
+
<Badge
|
|
89
|
+
variant="secondary"
|
|
90
|
+
className="bg-gradient-to-r from-amber-500 to-orange-500 text-white border-0 px-3 py-1"
|
|
91
|
+
>
|
|
92
|
+
<Crown className="w-3 h-3 mr-1" />
|
|
93
|
+
PRO ONLY
|
|
94
|
+
</Badge>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Lock Icon */}
|
|
98
|
+
<div className="flex justify-center">
|
|
99
|
+
<div className="p-3 rounded-full bg-amber-100 dark:bg-amber-900/30">
|
|
100
|
+
<Lock className="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Component Name */}
|
|
105
|
+
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
|
|
106
|
+
{componentName}
|
|
107
|
+
</h3>
|
|
108
|
+
|
|
109
|
+
{/* Description */}
|
|
110
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs">
|
|
111
|
+
This premium component is available with Pro subscription
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
{/* Upgrade Button */}
|
|
115
|
+
<Button
|
|
116
|
+
size="sm"
|
|
117
|
+
className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 border-0"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
if (typeof window !== 'undefined') {
|
|
120
|
+
window.location.href = '/pricing'
|
|
121
|
+
}
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Zap className="w-4 h-4 mr-2" />
|
|
125
|
+
Upgrade to Pro
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Preview Hint */}
|
|
131
|
+
{showPreview && !isPreviewActive && (
|
|
132
|
+
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
133
|
+
<div className="bg-black/80 text-white text-xs px-2 py-1 rounded">
|
|
134
|
+
Hover to preview
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pro Component Wrapper - Otomatik eriลim kontrolรผ ile
|
|
143
|
+
interface ProComponentWrapperProps {
|
|
144
|
+
children: React.ReactNode
|
|
145
|
+
componentId: string
|
|
146
|
+
componentName?: string
|
|
147
|
+
className?: string
|
|
148
|
+
fallback?: React.ReactNode
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function ProComponentWrapper({
|
|
152
|
+
children,
|
|
153
|
+
componentId,
|
|
154
|
+
componentName,
|
|
155
|
+
className,
|
|
156
|
+
fallback
|
|
157
|
+
}: ProComponentWrapperProps) {
|
|
158
|
+
// For now, always show the component (no access control)
|
|
159
|
+
// This can be updated later when proper access control is implemented
|
|
160
|
+
const canUse = true
|
|
161
|
+
const isLocked = false
|
|
162
|
+
const isLoading = false
|
|
163
|
+
|
|
164
|
+
// Loading state
|
|
165
|
+
if (isLoading) {
|
|
166
|
+
return (
|
|
167
|
+
<div className={cn("animate-pulse bg-gray-200 dark:bg-gray-800 rounded-lg h-32", className)}>
|
|
168
|
+
<div className="flex items-center justify-center h-full">
|
|
169
|
+
<div className="text-sm text-gray-500">Loading...</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Unlocked - show actual component
|
|
176
|
+
if (canUse) {
|
|
177
|
+
return <div className={className}>{children}</div>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Locked - show locked state
|
|
181
|
+
if (isLocked) {
|
|
182
|
+
return (
|
|
183
|
+
<LockedComponent
|
|
184
|
+
componentName={componentName || componentId}
|
|
185
|
+
className={className}
|
|
186
|
+
>
|
|
187
|
+
{fallback || children}
|
|
188
|
+
</LockedComponent>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Default fallback
|
|
193
|
+
return <div className={className}>{children}</div>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Pro Badge Component
|
|
197
|
+
export function ProBadge({ className }: { className?: string }) {
|
|
198
|
+
return (
|
|
199
|
+
<Badge
|
|
200
|
+
variant="secondary"
|
|
201
|
+
className={cn(
|
|
202
|
+
"bg-gradient-to-r from-amber-500 to-orange-500 text-white border-0 text-xs",
|
|
203
|
+
className
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
<Crown className="w-3 h-3 mr-1" />
|
|
207
|
+
PRO
|
|
208
|
+
</Badge>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Free Badge Component
|
|
213
|
+
export function FreeBadge({ className }: { className?: string }) {
|
|
214
|
+
return (
|
|
215
|
+
<Badge
|
|
216
|
+
variant="outline"
|
|
217
|
+
className={cn("border-green-500 text-green-600 dark:text-green-400 text-xs", className)}
|
|
218
|
+
>
|
|
219
|
+
FREE
|
|
220
|
+
</Badge>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
@@ -18,7 +18,7 @@ export function MoonLogo({
|
|
|
18
18
|
const gradientId = `moon-gradient-${logoId}`
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
|
-
<div className={cn("flex items-center gap-2", className)}>
|
|
21
|
+
<div className={cn("moonui-theme", "flex items-center gap-2", className)}>
|
|
22
22
|
{/* Moon Icon */}
|
|
23
23
|
<svg
|
|
24
24
|
width="32"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import * as React from "react"
|
|
2
4
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
|
3
5
|
import { cn } from "../../lib/utils"
|
|
@@ -7,7 +9,7 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
|
|
7
9
|
<nav
|
|
8
10
|
role="navigation"
|
|
9
11
|
aria-label="pagination"
|
|
10
|
-
className={cn("mx-auto flex w-full justify-center", className)}
|
|
12
|
+
className={cn("moonui-theme", "mx-auto flex w-full justify-center", className)}
|
|
11
13
|
{...props}
|
|
12
14
|
/>
|
|
13
15
|
)
|
|
@@ -19,7 +21,7 @@ const PaginationContent = React.forwardRef<
|
|
|
19
21
|
>(({ className, ...props }, ref) => (
|
|
20
22
|
<ul
|
|
21
23
|
ref={ref}
|
|
22
|
-
className={cn("flex flex-row items-center gap-1", className)}
|
|
24
|
+
className={cn("moonui-theme", "flex flex-row items-center gap-1", className)}
|
|
23
25
|
{...props}
|
|
24
26
|
/>
|
|
25
27
|
))
|
|
@@ -29,7 +31,7 @@ const PaginationItem = React.forwardRef<
|
|
|
29
31
|
HTMLLIElement,
|
|
30
32
|
React.ComponentProps<"li">
|
|
31
33
|
>(({ className, ...props }, ref) => (
|
|
32
|
-
<li ref={ref} className={cn("", className)} {...props} />
|
|
34
|
+
<li ref={ref} className={cn("moonui-theme", "", className)} {...props} />
|
|
33
35
|
))
|
|
34
36
|
PaginationItem.displayName = "PaginationItem"
|
|
35
37
|
|
|
@@ -64,8 +66,8 @@ const PaginationPrevious = ({
|
|
|
64
66
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
65
67
|
<PaginationLink
|
|
66
68
|
aria-label="Go to previous page"
|
|
67
|
-
size="
|
|
68
|
-
className={cn("gap-1 pl-2.5", className)}
|
|
69
|
+
size="md"
|
|
70
|
+
className={cn("moonui-theme", "gap-1 pl-2.5", className)}
|
|
69
71
|
{...props}
|
|
70
72
|
>
|
|
71
73
|
<ChevronLeft className="h-4 w-4" />
|
|
@@ -80,8 +82,8 @@ const PaginationNext = ({
|
|
|
80
82
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
81
83
|
<PaginationLink
|
|
82
84
|
aria-label="Go to next page"
|
|
83
|
-
size="
|
|
84
|
-
className={cn("gap-1 pr-2.5", className)}
|
|
85
|
+
size="md"
|
|
86
|
+
className={cn("moonui-theme", "gap-1 pr-2.5", className)}
|
|
85
87
|
{...props}
|
|
86
88
|
>
|
|
87
89
|
<span>Next</span>
|
|
@@ -96,7 +98,7 @@ const PaginationEllipsis = ({
|
|
|
96
98
|
}: React.ComponentProps<"span">) => (
|
|
97
99
|
<span
|
|
98
100
|
aria-hidden
|
|
99
|
-
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
101
|
+
className={cn("moonui-theme", "flex h-9 w-9 items-center justify-center", className)}
|
|
100
102
|
{...props}
|
|
101
103
|
>
|
|
102
104
|
<MoreHorizontal className="h-4 w-4" />
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
import { Input } from "./input"
|
|
6
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
7
|
+
import { Phone } from "lucide-react"
|
|
8
|
+
|
|
9
|
+
// Country codes data
|
|
10
|
+
const countryCodes = [
|
|
11
|
+
{ code: "+1", country: "US", flag: "๐บ๐ธ", name: "United States" },
|
|
12
|
+
{ code: "+44", country: "GB", flag: "๐ฌ๐ง", name: "United Kingdom" },
|
|
13
|
+
{ code: "+49", country: "DE", flag: "๐ฉ๐ช", name: "Germany" },
|
|
14
|
+
{ code: "+33", country: "FR", flag: "๐ซ๐ท", name: "France" },
|
|
15
|
+
{ code: "+39", country: "IT", flag: "๐ฎ๐น", name: "Italy" },
|
|
16
|
+
{ code: "+34", country: "ES", flag: "๐ช๐ธ", name: "Spain" },
|
|
17
|
+
{ code: "+31", country: "NL", flag: "๐ณ๐ฑ", name: "Netherlands" },
|
|
18
|
+
{ code: "+46", country: "SE", flag: "๐ธ๐ช", name: "Sweden" },
|
|
19
|
+
{ code: "+47", country: "NO", flag: "๐ณ๐ด", name: "Norway" },
|
|
20
|
+
{ code: "+45", country: "DK", flag: "๐ฉ๐ฐ", name: "Denmark" },
|
|
21
|
+
{ code: "+358", country: "FI", flag: "๐ซ๐ฎ", name: "Finland" },
|
|
22
|
+
{ code: "+48", country: "PL", flag: "๐ต๐ฑ", name: "Poland" },
|
|
23
|
+
{ code: "+90", country: "TR", flag: "๐น๐ท", name: "Turkey" },
|
|
24
|
+
{ code: "+86", country: "CN", flag: "๐จ๐ณ", name: "China" },
|
|
25
|
+
{ code: "+81", country: "JP", flag: "๐ฏ๐ต", name: "Japan" },
|
|
26
|
+
{ code: "+82", country: "KR", flag: "๐ฐ๐ท", name: "South Korea" },
|
|
27
|
+
{ code: "+91", country: "IN", flag: "๐ฎ๐ณ", name: "India" },
|
|
28
|
+
{ code: "+61", country: "AU", flag: "๐ฆ๐บ", name: "Australia" },
|
|
29
|
+
{ code: "+64", country: "NZ", flag: "๐ณ๐ฟ", name: "New Zealand" },
|
|
30
|
+
{ code: "+27", country: "ZA", flag: "๐ฟ๐ฆ", name: "South Africa" },
|
|
31
|
+
{ code: "+55", country: "BR", flag: "๐ง๐ท", name: "Brazil" },
|
|
32
|
+
{ code: "+52", country: "MX", flag: "๐ฒ๐ฝ", name: "Mexico" },
|
|
33
|
+
{ code: "+54", country: "AR", flag: "๐ฆ๐ท", name: "Argentina" },
|
|
34
|
+
{ code: "+20", country: "EG", flag: "๐ช๐ฌ", name: "Egypt" },
|
|
35
|
+
{ code: "+966", country: "SA", flag: "๐ธ๐ฆ", name: "Saudi Arabia" },
|
|
36
|
+
{ code: "+971", country: "AE", flag: "๐ฆ๐ช", name: "UAE" },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// Format phone number based on country
|
|
40
|
+
const formatPhoneNumber = (value: string, countryCode: string): string => {
|
|
41
|
+
const cleaned = value.replace(/\D/g, "")
|
|
42
|
+
|
|
43
|
+
switch (countryCode) {
|
|
44
|
+
case "+1": // US/Canada
|
|
45
|
+
if (cleaned.length <= 3) return cleaned
|
|
46
|
+
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`
|
|
47
|
+
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`
|
|
48
|
+
|
|
49
|
+
case "+44": // UK
|
|
50
|
+
if (cleaned.length <= 4) return cleaned
|
|
51
|
+
if (cleaned.length <= 7) return `${cleaned.slice(0, 4)} ${cleaned.slice(4)}`
|
|
52
|
+
return `${cleaned.slice(0, 4)} ${cleaned.slice(4, 7)} ${cleaned.slice(7, 11)}`
|
|
53
|
+
|
|
54
|
+
case "+90": // Turkey
|
|
55
|
+
if (cleaned.length <= 3) return cleaned
|
|
56
|
+
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`
|
|
57
|
+
if (cleaned.length <= 9) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)} ${cleaned.slice(6)}`
|
|
58
|
+
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)} ${cleaned.slice(6, 8)} ${cleaned.slice(8, 10)}`
|
|
59
|
+
|
|
60
|
+
default: // Generic formatting
|
|
61
|
+
let formatted = ""
|
|
62
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
63
|
+
if (i > 0 && i % 3 === 0) formatted += " "
|
|
64
|
+
formatted += cleaned[i]
|
|
65
|
+
}
|
|
66
|
+
return formatted
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get max length based on country
|
|
71
|
+
const getMaxLength = (countryCode: string): number => {
|
|
72
|
+
switch (countryCode) {
|
|
73
|
+
case "+1": return 10 // US/Canada
|
|
74
|
+
case "+44": return 11 // UK
|
|
75
|
+
case "+90": return 10 // Turkey
|
|
76
|
+
default: return 15 // Generic max
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface PhoneInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "value"> {
|
|
81
|
+
value?: string
|
|
82
|
+
onChange?: (value: string, countryCode: string) => void
|
|
83
|
+
defaultCountry?: string
|
|
84
|
+
showIcon?: boolean
|
|
85
|
+
showCountrySelect?: boolean
|
|
86
|
+
countries?: typeof countryCodes
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
90
|
+
({
|
|
91
|
+
className,
|
|
92
|
+
value = "",
|
|
93
|
+
onChange,
|
|
94
|
+
defaultCountry = "+1",
|
|
95
|
+
showIcon = true,
|
|
96
|
+
showCountrySelect = true,
|
|
97
|
+
countries = countryCodes,
|
|
98
|
+
size,
|
|
99
|
+
...props
|
|
100
|
+
}, ref) => {
|
|
101
|
+
const [countryCode, setCountryCode] = React.useState(defaultCountry)
|
|
102
|
+
const [phoneNumber, setPhoneNumber] = React.useState(value)
|
|
103
|
+
|
|
104
|
+
const selectedCountry = countries.find(c => c.code === countryCode) || countries[0]
|
|
105
|
+
|
|
106
|
+
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
107
|
+
const rawValue = e.target.value.replace(/\D/g, "")
|
|
108
|
+
const maxLength = getMaxLength(countryCode)
|
|
109
|
+
const truncated = rawValue.slice(0, maxLength)
|
|
110
|
+
const formatted = formatPhoneNumber(truncated, countryCode)
|
|
111
|
+
|
|
112
|
+
setPhoneNumber(truncated)
|
|
113
|
+
onChange?.(truncated, countryCode)
|
|
114
|
+
|
|
115
|
+
// Update the input value with formatted version
|
|
116
|
+
e.target.value = formatted
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleCountryChange = (newCountryCode: string) => {
|
|
120
|
+
setCountryCode(newCountryCode)
|
|
121
|
+
onChange?.(phoneNumber, newCountryCode)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const formattedValue = formatPhoneNumber(phoneNumber, countryCode)
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className={cn("relative flex gap-2", className)}>
|
|
128
|
+
{showCountrySelect && (
|
|
129
|
+
<Select value={countryCode} onValueChange={handleCountryChange}>
|
|
130
|
+
<SelectTrigger className="w-[120px]">
|
|
131
|
+
<SelectValue>
|
|
132
|
+
<div className="flex items-center gap-2">
|
|
133
|
+
<span>{selectedCountry.flag}</span>
|
|
134
|
+
<span>{selectedCountry.code}</span>
|
|
135
|
+
</div>
|
|
136
|
+
</SelectValue>
|
|
137
|
+
</SelectTrigger>
|
|
138
|
+
<SelectContent>
|
|
139
|
+
{countries.map((country) => (
|
|
140
|
+
<SelectItem key={country.code} value={country.code}>
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<span>{country.flag}</span>
|
|
143
|
+
<span>{country.code}</span>
|
|
144
|
+
<span className="text-muted-foreground">{country.name}</span>
|
|
145
|
+
</div>
|
|
146
|
+
</SelectItem>
|
|
147
|
+
))}
|
|
148
|
+
</SelectContent>
|
|
149
|
+
</Select>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<div className="relative flex-1">
|
|
153
|
+
<Input
|
|
154
|
+
ref={ref}
|
|
155
|
+
type="tel"
|
|
156
|
+
inputMode="numeric"
|
|
157
|
+
autoComplete="tel"
|
|
158
|
+
placeholder={countryCode === "+1" ? "(555) 123-4567" : "123 456 789"}
|
|
159
|
+
value={formattedValue}
|
|
160
|
+
onChange={handlePhoneChange}
|
|
161
|
+
className={cn(showIcon && "pl-10", className)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
{showIcon && (
|
|
165
|
+
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
PhoneInput.displayName = "PhoneInput"
|