alexui 1.0.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/README.md +57 -0
- package/components/ActionTable.tsx +307 -0
- package/components/AlertBanner.tsx +124 -0
- package/components/AnimatedAccordion.tsx +95 -0
- package/components/Autocomplete.tsx +144 -0
- package/components/Avatar.tsx +123 -0
- package/components/Badge.tsx +80 -0
- package/components/Breadcrumb.tsx +74 -0
- package/components/Calendar.tsx +340 -0
- package/components/Card3D.tsx +117 -0
- package/components/Carousel3D.tsx +193 -0
- package/components/CascadeSelect.tsx +232 -0
- package/components/ChartShowcase.tsx +700 -0
- package/components/Checkbox.tsx +212 -0
- package/components/ChipsInput.tsx +152 -0
- package/components/CircularKnob.tsx +240 -0
- package/components/CodeVisualizer.tsx +67 -0
- package/components/Collapsible.tsx +72 -0
- package/components/ColorThemeManager.tsx +458 -0
- package/components/CommandMenu.tsx +191 -0
- package/components/ConfirmDialog.tsx +152 -0
- package/components/ContextMenu.tsx +192 -0
- package/components/DashboardLayout.tsx +115 -0
- package/components/DatePicker.tsx +108 -0
- package/components/Divider.tsx +67 -0
- package/components/Dock.tsx +93 -0
- package/components/DragDropLists.tsx +160 -0
- package/components/Drawer.tsx +161 -0
- package/components/DropdownPlus.tsx +304 -0
- package/components/EmptyState.tsx +49 -0
- package/components/ErrorPage.tsx +62 -0
- package/components/FileDropzone.tsx +206 -0
- package/components/ForgotPassword.tsx +137 -0
- package/components/FormField.tsx +81 -0
- package/components/GlassButton.tsx +56 -0
- package/components/GlassCard.tsx +82 -0
- package/components/GlassInput.tsx +96 -0
- package/components/GlassmorphicModal.tsx +108 -0
- package/components/GlowInput.tsx +111 -0
- package/components/GlowSelect.tsx +203 -0
- package/components/GlowTextArea.tsx +105 -0
- package/components/HorizontalTimeline.tsx +121 -0
- package/components/HoverCard.tsx +105 -0
- package/components/ImageLightbox.tsx +259 -0
- package/components/InputGroup.tsx +118 -0
- package/components/InputOTP.tsx +147 -0
- package/components/InteractiveNavbar.tsx +266 -0
- package/components/InteractiveSidebar.tsx +211 -0
- package/components/Kbd.tsx +51 -0
- package/components/LiteYouTube.tsx +118 -0
- package/components/LoaderCollection.tsx +368 -0
- package/components/LoginForm.tsx +192 -0
- package/components/MagneticButton.tsx +101 -0
- package/components/MaskedInput.tsx +79 -0
- package/components/MentionInput.tsx +413 -0
- package/components/MorphingSwitch.tsx +86 -0
- package/components/MultiSelect.tsx +158 -0
- package/components/NumberInput.tsx +203 -0
- package/components/Panel.tsx +104 -0
- package/components/PasswordInput.tsx +203 -0
- package/components/Popover.tsx +91 -0
- package/components/PricingTable.tsx +113 -0
- package/components/ProgressBar.tsx +152 -0
- package/components/RadioButton.tsx +211 -0
- package/components/Rating.tsx +82 -0
- package/components/ResizablePanel.tsx +114 -0
- package/components/ScrollPanel.tsx +103 -0
- package/components/SettingsPage.tsx +154 -0
- package/components/SignupForm.tsx +182 -0
- package/components/Skeleton.tsx +41 -0
- package/components/Slider.tsx +95 -0
- package/components/SlidingTabs.tsx +54 -0
- package/components/SortableList.tsx +91 -0
- package/components/SpeedDial.tsx +134 -0
- package/components/Spinner.tsx +40 -0
- package/components/Stepper.tsx +124 -0
- package/components/TabMenu.tsx +72 -0
- package/components/TableControls.tsx +77 -0
- package/components/TablePagination.tsx +88 -0
- package/components/TextEditor.tsx +329 -0
- package/components/TextReveal.tsx +99 -0
- package/components/ThemeSwitcher.tsx +133 -0
- package/components/TimelineGSAP.tsx +164 -0
- package/components/ToastSystem.tsx +110 -0
- package/components/ToggleButton.tsx +79 -0
- package/components/Tooltip.tsx +121 -0
- package/components/Tree.tsx +138 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +110 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +76 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +60 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +38 -0
- package/dist/tui/browse.d.ts +3 -0
- package/dist/tui/browse.js +139 -0
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/format.js +52 -0
- package/dist/tui/main.d.ts +1 -0
- package/dist/tui/main.js +86 -0
- package/dist/tui/panels.d.ts +9 -0
- package/dist/tui/panels.js +50 -0
- package/dist/tui/theme.d.ts +28 -0
- package/dist/tui/theme.js +76 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +24 -0
- package/dist/utils/copy.d.ts +9 -0
- package/dist/utils/copy.js +43 -0
- package/dist/utils/cwd.d.ts +6 -0
- package/dist/utils/cwd.js +30 -0
- package/dist/utils/deps.d.ts +1 -0
- package/dist/utils/deps.js +19 -0
- package/dist/utils/project.d.ts +5 -0
- package/dist/utils/project.js +30 -0
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +24 -0
- package/package.json +52 -0
- package/registry.json +1133 -0
- package/templates/theme.css +81 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { motion, AnimatePresence, useMotionValue, useTransform, animate } from 'framer-motion';
|
|
4
|
+
import { X, ChevronLeft, ChevronRight, Maximize2 } from 'lucide-react';
|
|
5
|
+
import { Skeleton } from './Skeleton';
|
|
6
|
+
|
|
7
|
+
export interface GalleryImage {
|
|
8
|
+
src: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
caption?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ImageLightboxProps {
|
|
14
|
+
images: GalleryImage[];
|
|
15
|
+
className?: string;
|
|
16
|
+
isLoading?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ImageLightbox: React.FC<ImageLightboxProps> = ({ images, className = '', isLoading = false }) => {
|
|
20
|
+
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const [containerWidth, setContainerWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 800);
|
|
23
|
+
const dragX = useMotionValue(0);
|
|
24
|
+
|
|
25
|
+
// Measure container width for slide translations
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (selectedIndex === null || !containerRef.current) return;
|
|
28
|
+
const measure = () => {
|
|
29
|
+
if (containerRef.current) {
|
|
30
|
+
setContainerWidth(containerRef.current.offsetWidth);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
measure();
|
|
34
|
+
window.addEventListener('resize', measure);
|
|
35
|
+
return () => window.removeEventListener('resize', measure);
|
|
36
|
+
}, [selectedIndex]);
|
|
37
|
+
|
|
38
|
+
const handleOpen = (idx: number) => {
|
|
39
|
+
setSelectedIndex(idx);
|
|
40
|
+
dragX.set(0);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleClose = () => {
|
|
44
|
+
setSelectedIndex(null);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const prevIndex = selectedIndex !== null ? (selectedIndex > 0 ? selectedIndex - 1 : images.length - 1) : 0;
|
|
48
|
+
const nextIndex = selectedIndex !== null ? (selectedIndex < images.length - 1 ? selectedIndex + 1 : 0) : 0;
|
|
49
|
+
|
|
50
|
+
const currentX = dragX;
|
|
51
|
+
const prevX = useTransform(dragX, (val) => val - containerWidth);
|
|
52
|
+
const nextX = useTransform(dragX, (val) => val + containerWidth);
|
|
53
|
+
|
|
54
|
+
const handlePrev = useCallback((e?: React.MouseEvent) => {
|
|
55
|
+
if (e) e.stopPropagation();
|
|
56
|
+
if (selectedIndex === null) return;
|
|
57
|
+
|
|
58
|
+
// Slide right to show previous image
|
|
59
|
+
animate(dragX, containerWidth, { type: 'spring', stiffness: 300, damping: 30 }).then(() => {
|
|
60
|
+
setSelectedIndex((prev) => (prev !== null && prev > 0 ? prev - 1 : images.length - 1));
|
|
61
|
+
dragX.set(0);
|
|
62
|
+
});
|
|
63
|
+
}, [selectedIndex, containerWidth, images.length, dragX]);
|
|
64
|
+
|
|
65
|
+
const handleNext = useCallback((e?: React.MouseEvent) => {
|
|
66
|
+
if (e) e.stopPropagation();
|
|
67
|
+
if (selectedIndex === null) return;
|
|
68
|
+
|
|
69
|
+
// Slide left to show next image
|
|
70
|
+
animate(dragX, -containerWidth, { type: 'spring', stiffness: 300, damping: 30 }).then(() => {
|
|
71
|
+
setSelectedIndex((prev) => (prev !== null && prev < images.length - 1 ? prev + 1 : 0));
|
|
72
|
+
dragX.set(0);
|
|
73
|
+
});
|
|
74
|
+
}, [selectedIndex, containerWidth, images.length, dragX]);
|
|
75
|
+
|
|
76
|
+
// Keyboard navigation support
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
79
|
+
if (selectedIndex === null) return;
|
|
80
|
+
if (e.key === 'ArrowRight') handleNext();
|
|
81
|
+
if (e.key === 'ArrowLeft') handlePrev();
|
|
82
|
+
if (e.key === 'Escape') handleClose();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
86
|
+
if (selectedIndex !== null) {
|
|
87
|
+
document.body.style.overflow = 'hidden';
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
91
|
+
document.body.style.overflow = '';
|
|
92
|
+
};
|
|
93
|
+
}, [selectedIndex, handleNext, handlePrev]);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={`w-full ${className}`}>
|
|
97
|
+
|
|
98
|
+
{/* Photo grid */}
|
|
99
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
|
100
|
+
{isLoading ? (
|
|
101
|
+
Array(4).fill(0).map((_, idx) => (
|
|
102
|
+
<Skeleton
|
|
103
|
+
key={idx}
|
|
104
|
+
variant="rect"
|
|
105
|
+
className="w-full aspect-square rounded-2xl border border-border-app"
|
|
106
|
+
/>
|
|
107
|
+
))
|
|
108
|
+
) : (
|
|
109
|
+
images.map((img, idx) => (
|
|
110
|
+
<div
|
|
111
|
+
key={idx}
|
|
112
|
+
onClick={() => handleOpen(idx)}
|
|
113
|
+
className="group relative aspect-square rounded-2xl overflow-hidden bg-bg-card border border-border-app cursor-pointer shadow-sm hover:shadow-md transition-all duration-300"
|
|
114
|
+
>
|
|
115
|
+
<img
|
|
116
|
+
src={img.src}
|
|
117
|
+
alt={img.alt}
|
|
118
|
+
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-108"
|
|
119
|
+
loading="lazy"
|
|
120
|
+
/>
|
|
121
|
+
{/* Hover overlay */}
|
|
122
|
+
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
|
123
|
+
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center text-white border border-white/20">
|
|
124
|
+
<Maximize2 className="w-5 h-5" />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
))
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Lightbox full-screen Overlay */}
|
|
133
|
+
{typeof document !== 'undefined' && createPortal(
|
|
134
|
+
<AnimatePresence>
|
|
135
|
+
{selectedIndex !== null && (
|
|
136
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 select-none">
|
|
137
|
+
|
|
138
|
+
{/* Backdrop Blur overlay */}
|
|
139
|
+
<motion.div
|
|
140
|
+
initial={{ opacity: 0 }}
|
|
141
|
+
animate={{ opacity: 1 }}
|
|
142
|
+
exit={{ opacity: 0 }}
|
|
143
|
+
onClick={handleClose}
|
|
144
|
+
className="fixed inset-0 bg-black/90 backdrop-blur-md cursor-pointer"
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
{/* Upper Action Controls */}
|
|
148
|
+
<div className="absolute top-6 left-6 right-6 z-10 flex items-center justify-between text-white pointer-events-none">
|
|
149
|
+
<span className="text-sm font-semibold tracking-wider font-mono">
|
|
150
|
+
{selectedIndex + 1} / {images.length}
|
|
151
|
+
</span>
|
|
152
|
+
<button
|
|
153
|
+
onClick={handleClose}
|
|
154
|
+
className="pointer-events-auto p-2 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10 text-white transition-colors cursor-pointer"
|
|
155
|
+
aria-label="Cerrar galería"
|
|
156
|
+
>
|
|
157
|
+
<X className="w-6 h-6" />
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Previous Arrow Button */}
|
|
162
|
+
<button
|
|
163
|
+
onClick={handlePrev}
|
|
164
|
+
className="absolute left-6 z-10 p-3 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10 text-white transition-colors cursor-pointer"
|
|
165
|
+
aria-label="Imagen anterior"
|
|
166
|
+
>
|
|
167
|
+
<ChevronLeft className="w-6 h-6" />
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{/* Slider container zoom wrapper */}
|
|
171
|
+
<motion.div
|
|
172
|
+
initial={{ scale: 0.92, opacity: 0 }}
|
|
173
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
174
|
+
exit={{ scale: 0.92, opacity: 0 }}
|
|
175
|
+
transition={{ type: 'spring', stiffness: 350, damping: 26 }}
|
|
176
|
+
ref={containerRef}
|
|
177
|
+
className="relative max-w-5xl max-h-[80vh] aspect-auto w-full h-full flex items-center justify-center z-10 overflow-hidden pointer-events-auto"
|
|
178
|
+
>
|
|
179
|
+
{/* Previous Slide */}
|
|
180
|
+
<motion.div
|
|
181
|
+
style={{ x: prevX }}
|
|
182
|
+
className="absolute inset-0 w-full h-full flex flex-col items-center justify-center p-4 select-none pointer-events-none"
|
|
183
|
+
>
|
|
184
|
+
<img
|
|
185
|
+
src={images[prevIndex].src}
|
|
186
|
+
alt={images[prevIndex].alt}
|
|
187
|
+
className="max-w-full max-h-[70vh] object-contain rounded-xl shadow-2xl"
|
|
188
|
+
/>
|
|
189
|
+
{images[prevIndex].caption && (
|
|
190
|
+
<span className="mt-4 text-sm font-semibold text-white/80 drop-shadow-md text-center max-w-lg">
|
|
191
|
+
{images[prevIndex].caption}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</motion.div>
|
|
195
|
+
|
|
196
|
+
{/* Current Slide */}
|
|
197
|
+
<motion.div
|
|
198
|
+
style={{ x: currentX }}
|
|
199
|
+
drag="x"
|
|
200
|
+
dragConstraints={{ left: 0, right: 0 }}
|
|
201
|
+
dragElastic={0.6}
|
|
202
|
+
onDragEnd={(_, info) => {
|
|
203
|
+
const threshold = containerWidth * 0.2; // 20% of width is enough to swipe
|
|
204
|
+
if (info.offset.x < -threshold) {
|
|
205
|
+
handleNext();
|
|
206
|
+
} else if (info.offset.x > threshold) {
|
|
207
|
+
handlePrev();
|
|
208
|
+
} else {
|
|
209
|
+
animate(dragX, 0, { type: 'spring', stiffness: 300, damping: 30 });
|
|
210
|
+
}
|
|
211
|
+
}}
|
|
212
|
+
className="absolute inset-0 w-full h-full flex flex-col items-center justify-center p-4 select-none cursor-grab active:cursor-grabbing z-20"
|
|
213
|
+
>
|
|
214
|
+
<img
|
|
215
|
+
src={images[selectedIndex].src}
|
|
216
|
+
alt={images[selectedIndex].alt}
|
|
217
|
+
className="max-w-full max-h-[70vh] object-contain rounded-xl shadow-2xl pointer-events-none"
|
|
218
|
+
/>
|
|
219
|
+
{images[selectedIndex].caption && (
|
|
220
|
+
<span className="mt-4 text-sm font-semibold text-white/80 drop-shadow-md text-center max-w-lg">
|
|
221
|
+
{images[selectedIndex].caption}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
</motion.div>
|
|
225
|
+
|
|
226
|
+
{/* Next Slide */}
|
|
227
|
+
<motion.div
|
|
228
|
+
style={{ x: nextX }}
|
|
229
|
+
className="absolute inset-0 w-full h-full flex flex-col items-center justify-center p-4 select-none pointer-events-none"
|
|
230
|
+
>
|
|
231
|
+
<img
|
|
232
|
+
src={images[nextIndex].src}
|
|
233
|
+
alt={images[nextIndex].alt}
|
|
234
|
+
className="max-w-full max-h-[70vh] object-contain rounded-xl shadow-2xl"
|
|
235
|
+
/>
|
|
236
|
+
{images[nextIndex].caption && (
|
|
237
|
+
<span className="mt-4 text-sm font-semibold text-white/80 drop-shadow-md text-center max-w-lg">
|
|
238
|
+
{images[nextIndex].caption}
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
</motion.div>
|
|
242
|
+
</motion.div>
|
|
243
|
+
|
|
244
|
+
{/* Next Arrow Button */}
|
|
245
|
+
<button
|
|
246
|
+
onClick={handleNext}
|
|
247
|
+
className="absolute right-6 z-10 p-3 rounded-xl bg-white/10 hover:bg-white/20 border border-white/10 text-white transition-colors cursor-pointer"
|
|
248
|
+
aria-label="Siguiente imagen"
|
|
249
|
+
>
|
|
250
|
+
<ChevronRight className="w-6 h-6" />
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</AnimatePresence>,
|
|
255
|
+
document.body
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// 1. INPUT GROUP ADDON / TEXT COMPONENT
|
|
4
|
+
export interface InputGroupTextProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const InputGroupText: React.FC<InputGroupTextProps> = ({ children, className = '' }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={`input-group-text px-4 py-3 flex items-center justify-center bg-bg-app/50 border border-border-app text-xs font-bold text-text-muted select-none whitespace-nowrap ${className}`}>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 2. MAIN INPUT GROUP CONTAINER
|
|
18
|
+
export interface InputGroupProps {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const InputGroup: React.FC<InputGroupProps> = ({ children, className = '', disabled = false }) => {
|
|
25
|
+
return (
|
|
26
|
+
<div className={`input-group-container flex items-stretch w-full relative ${
|
|
27
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none' : ''
|
|
28
|
+
} ${className}`}>
|
|
29
|
+
|
|
30
|
+
{/* Dynamic inline styles to target complex nested components cleanly */}
|
|
31
|
+
<style>{`
|
|
32
|
+
.input-group-container > * {
|
|
33
|
+
flex: 1;
|
|
34
|
+
}
|
|
35
|
+
.input-group-container > .input-group-text,
|
|
36
|
+
.input-group-container > button,
|
|
37
|
+
.input-group-container > div > button {
|
|
38
|
+
flex: 0 0 auto;
|
|
39
|
+
}
|
|
40
|
+
/* First element rounding adjustments */
|
|
41
|
+
.input-group-container > *:first-child,
|
|
42
|
+
.input-group-container > *:first-child * {
|
|
43
|
+
border-top-right-radius: 0px !important;
|
|
44
|
+
border-bottom-right-radius: 0px !important;
|
|
45
|
+
}
|
|
46
|
+
/* Last element rounding adjustments */
|
|
47
|
+
.input-group-container > *:last-child,
|
|
48
|
+
.input-group-container > *:last-child * {
|
|
49
|
+
border-top-left-radius: 0px !important;
|
|
50
|
+
border-bottom-left-radius: 0px !important;
|
|
51
|
+
}
|
|
52
|
+
/* Middle elements rounding adjustments */
|
|
53
|
+
.input-group-container > *:not(:first-child):not(:last-child),
|
|
54
|
+
.input-group-container > *:not(:first-child):not(:last-child) * {
|
|
55
|
+
border-radius: 0px !important;
|
|
56
|
+
}
|
|
57
|
+
/* Overlap duplicate borders smoothly */
|
|
58
|
+
.input-group-container > *:not(:first-child) {
|
|
59
|
+
margin-left: -1px;
|
|
60
|
+
}
|
|
61
|
+
/* Ensure inputs take full height within wrappers */
|
|
62
|
+
.input-group-container > * > div {
|
|
63
|
+
height: 100%;
|
|
64
|
+
}
|
|
65
|
+
`}</style>
|
|
66
|
+
|
|
67
|
+
{React.Children.map(children, (child) => {
|
|
68
|
+
if (!React.isValidElement(child)) return child;
|
|
69
|
+
|
|
70
|
+
// Propagate disabled property to children
|
|
71
|
+
return React.cloneElement(child as React.ReactElement<{ disabled?: boolean }>, {
|
|
72
|
+
disabled: (child as React.ReactElement<{ disabled?: boolean }>).props.disabled !== undefined
|
|
73
|
+
? (child as React.ReactElement<{ disabled?: boolean }>).props.disabled
|
|
74
|
+
: disabled
|
|
75
|
+
});
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 3. ICON FIELD CONTAINER (To place icons absolute-positioned inside inputs)
|
|
82
|
+
export type IconFieldPosition = 'left' | 'right';
|
|
83
|
+
|
|
84
|
+
export interface IconFieldProps {
|
|
85
|
+
icon: React.ReactNode;
|
|
86
|
+
iconPosition?: IconFieldPosition;
|
|
87
|
+
children: React.ReactElement; // Must be a single input element
|
|
88
|
+
className?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const IconField: React.FC<IconFieldProps> = ({
|
|
92
|
+
icon,
|
|
93
|
+
iconPosition = 'left',
|
|
94
|
+
children,
|
|
95
|
+
className = ''
|
|
96
|
+
}) => {
|
|
97
|
+
const isLeft = iconPosition === 'left';
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className={`relative w-full ${className}`}>
|
|
101
|
+
{/* Icon Wrapper */}
|
|
102
|
+
<div
|
|
103
|
+
className={`absolute top-1/2 -translate-y-1/2 flex items-center justify-center text-text-muted/70 z-20 pointer-events-none ${
|
|
104
|
+
isLeft ? 'left-4' : 'right-4'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{icon}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Clone Input to inject correct padding */}
|
|
111
|
+
{React.cloneElement(children as React.ReactElement<{ className?: string }>, {
|
|
112
|
+
className: `${(children as React.ReactElement<{ className?: string }>).props.className || ''} ${
|
|
113
|
+
isLeft ? '!pl-10.5' : '!pr-10.5'
|
|
114
|
+
}`.trim()
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface InputOTPProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
length?: 4 | 6;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
isInvalid?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const InputOTP: React.FC<InputOTPProps> = ({
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
length = 6,
|
|
17
|
+
disabled = false,
|
|
18
|
+
isInvalid = false,
|
|
19
|
+
className = ''
|
|
20
|
+
}) => {
|
|
21
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
22
|
+
const otpArray = value.split('').concat(Array(length).fill('')).slice(0, length);
|
|
23
|
+
|
|
24
|
+
// Focus the first empty input or the first one if empty
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (inputRefs.current[0] && value.length === 0) {
|
|
27
|
+
// Don't auto-focus unless requested, just set up array sizes
|
|
28
|
+
inputRefs.current = inputRefs.current.slice(0, length);
|
|
29
|
+
}
|
|
30
|
+
}, [length, value]);
|
|
31
|
+
|
|
32
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, idx: number) => {
|
|
33
|
+
const val = e.target.value.replace(/\D/g, ''); // numbers only
|
|
34
|
+
if (!val) return;
|
|
35
|
+
|
|
36
|
+
const valChars = [...otpArray];
|
|
37
|
+
valChars[idx] = val.slice(-1); // keep only last character
|
|
38
|
+
const nextVal = valChars.slice(0, length).join('');
|
|
39
|
+
onChange(nextVal);
|
|
40
|
+
|
|
41
|
+
// Auto-focus next box
|
|
42
|
+
if (idx < length - 1) {
|
|
43
|
+
inputRefs.current[idx + 1]?.focus();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, idx: number) => {
|
|
48
|
+
if (e.key === 'Backspace') {
|
|
49
|
+
const valChars = [...otpArray];
|
|
50
|
+
|
|
51
|
+
if (otpArray[idx] === '') {
|
|
52
|
+
// If current is empty, delete previous and focus it
|
|
53
|
+
if (idx > 0) {
|
|
54
|
+
valChars[idx - 1] = '';
|
|
55
|
+
onChange(valChars.slice(0, length).join(''));
|
|
56
|
+
inputRefs.current[idx - 1]?.focus();
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Delete current char
|
|
60
|
+
valChars[idx] = '';
|
|
61
|
+
onChange(valChars.slice(0, length).join(''));
|
|
62
|
+
}
|
|
63
|
+
} else if (e.key === 'ArrowLeft' && idx > 0) {
|
|
64
|
+
inputRefs.current[idx - 1]?.focus();
|
|
65
|
+
} else if (e.key === 'ArrowRight' && idx < length - 1) {
|
|
66
|
+
inputRefs.current[idx + 1]?.focus();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
const pastedData = e.clipboardData.getData('text')
|
|
73
|
+
.replace(/\D/g, '')
|
|
74
|
+
.slice(0, length);
|
|
75
|
+
|
|
76
|
+
if (pastedData) {
|
|
77
|
+
onChange(pastedData);
|
|
78
|
+
const focusIdx = Math.min(pastedData.length, length - 1);
|
|
79
|
+
inputRefs.current[focusIdx]?.focus();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={`flex flex-col gap-2.5 items-center ${className}`}>
|
|
85
|
+
<div className="flex gap-2.5 items-center justify-center">
|
|
86
|
+
{otpArray.map((char, idx) => {
|
|
87
|
+
const isFocused = document.activeElement === inputRefs.current[idx];
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div key={idx} className="relative w-12 h-14 overflow-visible">
|
|
91
|
+
|
|
92
|
+
{/* Focus Glow border ring */}
|
|
93
|
+
<motion.div
|
|
94
|
+
animate={{
|
|
95
|
+
opacity: isFocused && !disabled ? 1 : 0,
|
|
96
|
+
scale: isFocused && !disabled ? 1 : 0.95,
|
|
97
|
+
}}
|
|
98
|
+
className="absolute -inset-[1px] bg-gradient-to-r from-accent to-pink-500 rounded-xl pointer-events-none z-0 blur-[1px]"
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<input
|
|
102
|
+
ref={(el) => { inputRefs.current[idx] = el; }}
|
|
103
|
+
type="text"
|
|
104
|
+
inputMode="numeric"
|
|
105
|
+
pattern="[0-9]*"
|
|
106
|
+
maxLength={1}
|
|
107
|
+
value={char}
|
|
108
|
+
onChange={(e) => handleInputChange(e, idx)}
|
|
109
|
+
onKeyDown={(e) => handleKeyDown(e, idx)}
|
|
110
|
+
onPaste={handlePaste}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
className={`w-full h-full text-center text-lg font-extrabold text-text-main bg-bg-card border rounded-xl z-10 relative focus:outline-hidden transition-all ${
|
|
113
|
+
isInvalid
|
|
114
|
+
? 'border-error/60'
|
|
115
|
+
: isFocused
|
|
116
|
+
? 'border-transparent'
|
|
117
|
+
: 'border-border-app/80'
|
|
118
|
+
} ${
|
|
119
|
+
disabled
|
|
120
|
+
? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10'
|
|
121
|
+
: 'cursor-text'
|
|
122
|
+
}`}
|
|
123
|
+
style={{
|
|
124
|
+
boxShadow: isFocused && !disabled && !isInvalid
|
|
125
|
+
? '0 0 10px var(--color-accent-glow)'
|
|
126
|
+
: isInvalid
|
|
127
|
+
? '0 0 10px var(--color-error-glow)'
|
|
128
|
+
: 'none'
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{isInvalid && (
|
|
137
|
+
<motion.span
|
|
138
|
+
initial={{ opacity: 0, y: -4 }}
|
|
139
|
+
animate={{ opacity: 1, y: 0 }}
|
|
140
|
+
className="text-[11px] text-red-500 font-semibold mt-1"
|
|
141
|
+
>
|
|
142
|
+
Código de verificación incorrecto
|
|
143
|
+
</motion.span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|