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,368 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
interface BaseLoaderProps {
|
|
5
|
+
color?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Helper to resolve sizes
|
|
11
|
+
const getPixelSize = (size: 'sm' | 'md' | 'lg', sm: number, md: number, lg: number) => {
|
|
12
|
+
if (size === 'sm') return sm;
|
|
13
|
+
if (size === 'lg') return lg;
|
|
14
|
+
return md;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 1. PulseBars: Soundwave-like pulsing vertical bars
|
|
18
|
+
export const PulseBars: React.FC<BaseLoaderProps> = ({
|
|
19
|
+
color = 'var(--color-accent)',
|
|
20
|
+
size = 'md',
|
|
21
|
+
className = ''
|
|
22
|
+
}) => {
|
|
23
|
+
const height = getPixelSize(size, 20, 36, 52);
|
|
24
|
+
const width = getPixelSize(size, 3, 5, 7);
|
|
25
|
+
const gap = getPixelSize(size, 3, 4, 6);
|
|
26
|
+
|
|
27
|
+
const containerVariants = {
|
|
28
|
+
animate: {
|
|
29
|
+
transition: {
|
|
30
|
+
staggerChildren: 0.1,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const barVariants = {
|
|
36
|
+
initial: { scaleY: 0.3 },
|
|
37
|
+
animate: {
|
|
38
|
+
scaleY: [0.3, 1, 0.3],
|
|
39
|
+
transition: {
|
|
40
|
+
duration: 0.8,
|
|
41
|
+
repeat: Infinity,
|
|
42
|
+
ease: 'easeInOut' as const
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<motion.div
|
|
49
|
+
variants={containerVariants}
|
|
50
|
+
initial="initial"
|
|
51
|
+
animate="animate"
|
|
52
|
+
className={`flex items-center justify-center ${className}`}
|
|
53
|
+
style={{ height, gap }}
|
|
54
|
+
>
|
|
55
|
+
{[...Array(5)].map((_, i) => (
|
|
56
|
+
<motion.div
|
|
57
|
+
key={i}
|
|
58
|
+
variants={barVariants}
|
|
59
|
+
className="rounded-full origin-center"
|
|
60
|
+
style={{
|
|
61
|
+
backgroundColor: color,
|
|
62
|
+
width,
|
|
63
|
+
height: '100%',
|
|
64
|
+
boxShadow: `0 0 10px ${color}50`
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</motion.div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 2. BouncingDots: Classic sequential bouncing spheres
|
|
73
|
+
export const BouncingDots: React.FC<BaseLoaderProps> = ({
|
|
74
|
+
color = 'var(--color-accent)',
|
|
75
|
+
size = 'md',
|
|
76
|
+
className = ''
|
|
77
|
+
}) => {
|
|
78
|
+
const dotSize = getPixelSize(size, 8, 12, 16);
|
|
79
|
+
const gap = getPixelSize(size, 4, 6, 8);
|
|
80
|
+
|
|
81
|
+
const containerVariants = {
|
|
82
|
+
animate: {
|
|
83
|
+
transition: {
|
|
84
|
+
staggerChildren: 0.15,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const dotVariants = {
|
|
90
|
+
initial: { y: 0 },
|
|
91
|
+
animate: {
|
|
92
|
+
y: [0, -12, 0],
|
|
93
|
+
transition: {
|
|
94
|
+
duration: 0.6,
|
|
95
|
+
repeat: Infinity,
|
|
96
|
+
ease: 'easeInOut' as const
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<motion.div
|
|
103
|
+
variants={containerVariants}
|
|
104
|
+
initial="initial"
|
|
105
|
+
animate="animate"
|
|
106
|
+
className={`flex items-center justify-center ${className}`}
|
|
107
|
+
style={{ gap, height: dotSize + 16 }}
|
|
108
|
+
>
|
|
109
|
+
{[...Array(3)].map((_, i) => (
|
|
110
|
+
<motion.div
|
|
111
|
+
key={i}
|
|
112
|
+
variants={dotVariants}
|
|
113
|
+
className="rounded-full"
|
|
114
|
+
style={{
|
|
115
|
+
backgroundColor: color,
|
|
116
|
+
width: dotSize,
|
|
117
|
+
height: dotSize,
|
|
118
|
+
boxShadow: `0 0 10px ${color}50`
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</motion.div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// 3. ElasticGrid: 3x3 staggered grid scaling
|
|
127
|
+
export const ElasticGrid: React.FC<BaseLoaderProps> = ({
|
|
128
|
+
color = 'var(--color-accent)',
|
|
129
|
+
size = 'md',
|
|
130
|
+
className = ''
|
|
131
|
+
}) => {
|
|
132
|
+
const dotSize = getPixelSize(size, 6, 10, 14);
|
|
133
|
+
const gap = getPixelSize(size, 4, 6, 8);
|
|
134
|
+
const gridWidth = dotSize * 3 + gap * 2;
|
|
135
|
+
|
|
136
|
+
const dotVariants = (index: number) => ({
|
|
137
|
+
animate: {
|
|
138
|
+
scale: [0.4, 1, 0.4],
|
|
139
|
+
opacity: [0.3, 1, 0.3],
|
|
140
|
+
transition: {
|
|
141
|
+
duration: 1.2,
|
|
142
|
+
repeat: Infinity,
|
|
143
|
+
ease: 'easeInOut' as const,
|
|
144
|
+
delay: (index % 3 + Math.floor(index / 3)) * 0.15
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={`grid grid-cols-3 justify-center items-center ${className}`}
|
|
152
|
+
style={{ gap, width: gridWidth, height: gridWidth }}
|
|
153
|
+
>
|
|
154
|
+
{[...Array(9)].map((_, i) => (
|
|
155
|
+
<motion.div
|
|
156
|
+
key={i}
|
|
157
|
+
animate="animate"
|
|
158
|
+
variants={dotVariants(i)}
|
|
159
|
+
className="rounded-full"
|
|
160
|
+
style={{
|
|
161
|
+
backgroundColor: color,
|
|
162
|
+
width: dotSize,
|
|
163
|
+
height: dotSize,
|
|
164
|
+
boxShadow: `0 0 8px ${color}40`
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// 4. ConcentricCircles: Rings rotating in opposite directions
|
|
173
|
+
export const ConcentricCircles: React.FC<BaseLoaderProps> = ({
|
|
174
|
+
color = 'var(--color-accent)',
|
|
175
|
+
size = 'md',
|
|
176
|
+
className = ''
|
|
177
|
+
}) => {
|
|
178
|
+
const outerSize = getPixelSize(size, 28, 48, 68);
|
|
179
|
+
const midSize = outerSize * 0.65;
|
|
180
|
+
const innerSize = outerSize * 0.35;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
className={`relative flex items-center justify-center ${className}`}
|
|
185
|
+
style={{ width: outerSize, height: outerSize }}
|
|
186
|
+
>
|
|
187
|
+
{/* Outer Ring */}
|
|
188
|
+
<motion.div
|
|
189
|
+
animate={{ rotate: 360 }}
|
|
190
|
+
transition={{ duration: 1.8, repeat: Infinity, ease: 'linear' }}
|
|
191
|
+
className="absolute rounded-full border-2 border-transparent border-t-accent"
|
|
192
|
+
style={{
|
|
193
|
+
width: '100%',
|
|
194
|
+
height: '100%',
|
|
195
|
+
borderColor: `${color} 2px`,
|
|
196
|
+
borderStyle: 'solid',
|
|
197
|
+
borderLeftColor: 'transparent',
|
|
198
|
+
borderRightColor: 'transparent',
|
|
199
|
+
borderBottomColor: 'transparent',
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
{/* Middle Ring */}
|
|
203
|
+
<motion.div
|
|
204
|
+
animate={{ rotate: -360 }}
|
|
205
|
+
transition={{ duration: 1.2, repeat: Infinity, ease: 'linear' }}
|
|
206
|
+
className="absolute rounded-full border-2 border-transparent"
|
|
207
|
+
style={{
|
|
208
|
+
width: midSize,
|
|
209
|
+
height: midSize,
|
|
210
|
+
borderColor: `${color}40 2px`,
|
|
211
|
+
borderStyle: 'solid',
|
|
212
|
+
borderTopColor: color,
|
|
213
|
+
borderBottomColor: color,
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
{/* Inner Dot */}
|
|
217
|
+
<motion.div
|
|
218
|
+
animate={{ scale: [0.8, 1.2, 0.8] }}
|
|
219
|
+
transition={{ duration: 1, repeat: Infinity, ease: 'easeInOut' }}
|
|
220
|
+
className="absolute rounded-full"
|
|
221
|
+
style={{
|
|
222
|
+
width: innerSize,
|
|
223
|
+
height: innerSize,
|
|
224
|
+
backgroundColor: color,
|
|
225
|
+
boxShadow: `0 0 12px ${color}`
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// 5. MeshSpinner: orbital nodes rotating in a ring
|
|
233
|
+
export const MeshSpinner: React.FC<BaseLoaderProps> = ({
|
|
234
|
+
color = 'var(--color-accent)',
|
|
235
|
+
size = 'md',
|
|
236
|
+
className = ''
|
|
237
|
+
}) => {
|
|
238
|
+
const containerSize = getPixelSize(size, 32, 54, 76);
|
|
239
|
+
const nodeSize = getPixelSize(size, 5, 8, 11);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
className={`relative flex items-center justify-center ${className}`}
|
|
244
|
+
style={{ width: containerSize, height: containerSize }}
|
|
245
|
+
>
|
|
246
|
+
<motion.div
|
|
247
|
+
animate={{ rotate: 360 }}
|
|
248
|
+
transition={{ duration: 1.5, repeat: Infinity, ease: 'linear' }}
|
|
249
|
+
className="relative w-full h-full"
|
|
250
|
+
>
|
|
251
|
+
{[...Array(6)].map((_, i) => {
|
|
252
|
+
const angle = (i * 360) / 6;
|
|
253
|
+
const radius = (containerSize - nodeSize) / 2;
|
|
254
|
+
const x = radius * Math.cos((angle * Math.PI) / 180);
|
|
255
|
+
const y = radius * Math.sin((angle * Math.PI) / 180);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<motion.div
|
|
259
|
+
key={i}
|
|
260
|
+
animate={{
|
|
261
|
+
scale: [0.5, 1.1, 0.5],
|
|
262
|
+
opacity: [0.4, 1, 0.4]
|
|
263
|
+
}}
|
|
264
|
+
transition={{
|
|
265
|
+
duration: 1.2,
|
|
266
|
+
repeat: Infinity,
|
|
267
|
+
delay: i * 0.12,
|
|
268
|
+
ease: 'easeInOut' as const
|
|
269
|
+
}}
|
|
270
|
+
className="absolute rounded-full"
|
|
271
|
+
style={{
|
|
272
|
+
backgroundColor: color,
|
|
273
|
+
width: nodeSize,
|
|
274
|
+
height: nodeSize,
|
|
275
|
+
left: `calc(50% - ${nodeSize / 2}px + ${x}px)`,
|
|
276
|
+
top: `calc(50% - ${nodeSize / 2}px + ${y}px)`,
|
|
277
|
+
boxShadow: `0 0 10px ${color}`
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</motion.div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// 6. InfinityLoop: Animated infinity SVG loop
|
|
288
|
+
export const InfinityLoop: React.FC<BaseLoaderProps> = ({
|
|
289
|
+
color = 'var(--color-accent)',
|
|
290
|
+
size = 'md',
|
|
291
|
+
className = ''
|
|
292
|
+
}) => {
|
|
293
|
+
const width = getPixelSize(size, 48, 80, 112);
|
|
294
|
+
const height = width / 2;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div
|
|
298
|
+
className={`flex items-center justify-center ${className}`}
|
|
299
|
+
style={{ width, height }}
|
|
300
|
+
>
|
|
301
|
+
<svg
|
|
302
|
+
viewBox="0 0 100 50"
|
|
303
|
+
className="w-full h-full overflow-visible"
|
|
304
|
+
>
|
|
305
|
+
{/* Draw Infinity Path Background */}
|
|
306
|
+
<path
|
|
307
|
+
d="M 30,25 C 15,5 5,17 5,25 C 5,33 15,45 30,25 C 45,5 55,5 70,25 C 85,45 95,33 95,25 C 95,17 85,5 70,25 C 55,45 45,45 30,25 Z"
|
|
308
|
+
fill="none"
|
|
309
|
+
stroke={`${color}20`}
|
|
310
|
+
strokeWidth="3.5"
|
|
311
|
+
strokeLinecap="round"
|
|
312
|
+
/>
|
|
313
|
+
|
|
314
|
+
{/* Animated Moving Path Overlay */}
|
|
315
|
+
<motion.path
|
|
316
|
+
d="M 30,25 C 15,5 5,17 5,25 C 5,33 15,45 30,25 C 45,5 55,5 70,25 C 85,45 95,33 95,25 C 95,17 85,5 70,25 C 55,45 45,45 30,25 Z"
|
|
317
|
+
fill="none"
|
|
318
|
+
stroke={color}
|
|
319
|
+
strokeWidth="3.5"
|
|
320
|
+
strokeLinecap="round"
|
|
321
|
+
strokeDasharray="20, 100"
|
|
322
|
+
animate={{
|
|
323
|
+
strokeDashoffset: [0, -120]
|
|
324
|
+
}}
|
|
325
|
+
transition={{
|
|
326
|
+
duration: 1.6,
|
|
327
|
+
repeat: Infinity,
|
|
328
|
+
ease: 'linear'
|
|
329
|
+
}}
|
|
330
|
+
style={{
|
|
331
|
+
filter: `drop-shadow(0 0 6px ${color})`
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
</svg>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Collection Wrapper component to showcase or render dynamic types
|
|
340
|
+
export interface LoaderCollectionProps {
|
|
341
|
+
type?: 'pulse' | 'bounce' | 'grid' | 'concentric' | 'mesh' | 'infinity';
|
|
342
|
+
color?: string;
|
|
343
|
+
size?: 'sm' | 'md' | 'lg';
|
|
344
|
+
className?: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export const LoaderCollection: React.FC<LoaderCollectionProps> = ({
|
|
348
|
+
type = 'pulse',
|
|
349
|
+
color,
|
|
350
|
+
size,
|
|
351
|
+
className
|
|
352
|
+
}) => {
|
|
353
|
+
switch (type) {
|
|
354
|
+
case 'bounce':
|
|
355
|
+
return <BouncingDots color={color} size={size} className={className} />;
|
|
356
|
+
case 'grid':
|
|
357
|
+
return <ElasticGrid color={color} size={size} className={className} />;
|
|
358
|
+
case 'concentric':
|
|
359
|
+
return <ConcentricCircles color={color} size={size} className={className} />;
|
|
360
|
+
case 'mesh':
|
|
361
|
+
return <MeshSpinner color={color} size={size} className={className} />;
|
|
362
|
+
case 'infinity':
|
|
363
|
+
return <InfinityLoop color={color} size={size} className={className} />;
|
|
364
|
+
case 'pulse':
|
|
365
|
+
default:
|
|
366
|
+
return <PulseBars color={color} size={size} className={className} />;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { GlowInput } from './GlowInput';
|
|
4
|
+
import { MorphingSwitch } from './MorphingSwitch';
|
|
5
|
+
import { MagneticButton } from './MagneticButton';
|
|
6
|
+
import { Sparkles } from 'lucide-react';
|
|
7
|
+
import { Skeleton } from './Skeleton';
|
|
8
|
+
|
|
9
|
+
export interface LoginFormProps {
|
|
10
|
+
onSubmit?: (data: { email: string; pass: string; remember: boolean }) => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LoginForm: React.FC<LoginFormProps> = ({
|
|
17
|
+
onSubmit,
|
|
18
|
+
className = '',
|
|
19
|
+
isLoading = false,
|
|
20
|
+
disabled = false
|
|
21
|
+
}) => {
|
|
22
|
+
const [email, setEmail] = useState('');
|
|
23
|
+
const [password, setPassword] = useState('');
|
|
24
|
+
const [remember, setRemember] = useState(false);
|
|
25
|
+
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
|
26
|
+
|
|
27
|
+
const validate = () => {
|
|
28
|
+
const nextErrors: { email?: string; password?: string } = {};
|
|
29
|
+
if (!email) {
|
|
30
|
+
nextErrors.email = 'El correo es obligatorio';
|
|
31
|
+
} else if (!email.includes('@')) {
|
|
32
|
+
nextErrors.email = 'Correo electrónico no válido';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!password) {
|
|
36
|
+
nextErrors.password = 'La contraseña es obligatoria';
|
|
37
|
+
} else if (password.length < 6) {
|
|
38
|
+
nextErrors.password = 'Debe contener al menos 6 caracteres';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setErrors(nextErrors);
|
|
42
|
+
return Object.keys(nextErrors).length === 0;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
if (disabled) return;
|
|
48
|
+
if (validate() && onSubmit) {
|
|
49
|
+
onSubmit({ email, pass: password, remember });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Staggered layout animations
|
|
54
|
+
const formContainerVariants = {
|
|
55
|
+
hidden: { opacity: 0, scale: 0.95, y: 15 },
|
|
56
|
+
show: {
|
|
57
|
+
opacity: 1,
|
|
58
|
+
scale: 1,
|
|
59
|
+
y: 0,
|
|
60
|
+
transition: {
|
|
61
|
+
type: 'spring' as const,
|
|
62
|
+
stiffness: 300,
|
|
63
|
+
damping: 25,
|
|
64
|
+
staggerChildren: 0.08,
|
|
65
|
+
delayChildren: 0.1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const itemVariants = {
|
|
71
|
+
hidden: { opacity: 0, y: 10 },
|
|
72
|
+
show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 300, damping: 25 } }
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<motion.div
|
|
77
|
+
variants={formContainerVariants}
|
|
78
|
+
initial="hidden"
|
|
79
|
+
animate="show"
|
|
80
|
+
className={`w-full max-w-md glass rounded-3xl p-8 bg-bg-card/70 border border-border-app shadow-2xl ${className}`}
|
|
81
|
+
>
|
|
82
|
+
{isLoading ? (
|
|
83
|
+
<div className="flex flex-col gap-6">
|
|
84
|
+
<div className="text-center flex flex-col items-center gap-1">
|
|
85
|
+
<Skeleton variant="circle" className="w-10 h-10 mb-2" />
|
|
86
|
+
<Skeleton variant="text" className="w-32 h-5 mb-1 animate-pulse" />
|
|
87
|
+
<Skeleton variant="text" className="w-48 h-3 animate-pulse" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex flex-col gap-6">
|
|
90
|
+
<div className="flex flex-col gap-2">
|
|
91
|
+
<Skeleton variant="text" className="w-24 h-3 animate-pulse" />
|
|
92
|
+
<Skeleton variant="rect" className="w-full h-11" />
|
|
93
|
+
</div>
|
|
94
|
+
<div className="flex flex-col gap-2">
|
|
95
|
+
<Skeleton variant="text" className="w-20 h-3 animate-pulse" />
|
|
96
|
+
<Skeleton variant="rect" className="w-full h-11" />
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="flex items-center justify-between text-xs">
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
<Skeleton variant="rect" className="w-10 h-6 rounded-full" />
|
|
102
|
+
<Skeleton variant="text" className="w-16 h-3 animate-pulse" />
|
|
103
|
+
</div>
|
|
104
|
+
<Skeleton variant="text" className="w-28 h-3 animate-pulse" />
|
|
105
|
+
</div>
|
|
106
|
+
<Skeleton variant="rect" className="w-full h-12 mt-2" />
|
|
107
|
+
</div>
|
|
108
|
+
) : (
|
|
109
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
110
|
+
|
|
111
|
+
{/* Title Group */}
|
|
112
|
+
<motion.div variants={itemVariants} className="text-center flex flex-col items-center gap-1">
|
|
113
|
+
<div className="w-10 h-10 rounded-2xl bg-accent/15 border border-accent/20 flex items-center justify-center text-accent mb-2">
|
|
114
|
+
<Sparkles className="w-5 h-5 animate-pulse" />
|
|
115
|
+
</div>
|
|
116
|
+
<h2 className="text-2xl font-extrabold tracking-tight text-text-main font-display">
|
|
117
|
+
¡Te damos la bienvenida!
|
|
118
|
+
</h2>
|
|
119
|
+
<p className="text-xs text-text-muted">
|
|
120
|
+
Ingresá tus credenciales para acceder a tu panel
|
|
121
|
+
</p>
|
|
122
|
+
</motion.div>
|
|
123
|
+
|
|
124
|
+
{/* Inputs */}
|
|
125
|
+
<div className="flex flex-col gap-4">
|
|
126
|
+
<motion.div variants={itemVariants}>
|
|
127
|
+
<GlowInput
|
|
128
|
+
label="Correo Electrónico"
|
|
129
|
+
type="email"
|
|
130
|
+
value={email}
|
|
131
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
132
|
+
error={errors.email}
|
|
133
|
+
placeholder="ejemplo@correo.com"
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
/>
|
|
136
|
+
</motion.div>
|
|
137
|
+
|
|
138
|
+
<motion.div variants={itemVariants}>
|
|
139
|
+
<GlowInput
|
|
140
|
+
label="Contraseña"
|
|
141
|
+
type="password"
|
|
142
|
+
value={password}
|
|
143
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
144
|
+
error={errors.password}
|
|
145
|
+
placeholder="••••••••"
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
/>
|
|
148
|
+
</motion.div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Remember / Forget Password settings */}
|
|
152
|
+
<motion.div
|
|
153
|
+
variants={itemVariants}
|
|
154
|
+
className="flex items-center justify-between text-xs"
|
|
155
|
+
>
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<MorphingSwitch
|
|
158
|
+
checked={remember}
|
|
159
|
+
onChange={setRemember}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
/>
|
|
162
|
+
<span className="font-semibold text-text-muted select-none">Recordarme</span>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<a
|
|
166
|
+
href="#"
|
|
167
|
+
onClick={(e) => disabled && e.preventDefault()}
|
|
168
|
+
className={`font-bold text-accent transition-colors ${
|
|
169
|
+
disabled ? 'opacity-40 cursor-not-allowed text-text-muted' : 'hover:text-accent-hover'
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
¿Olvidaste tu contraseña?
|
|
173
|
+
</a>
|
|
174
|
+
</motion.div>
|
|
175
|
+
|
|
176
|
+
{/* Submit button */}
|
|
177
|
+
<motion.div variants={itemVariants} className="w-full flex justify-center mt-2">
|
|
178
|
+
<MagneticButton
|
|
179
|
+
type="submit"
|
|
180
|
+
className="w-full py-3.5"
|
|
181
|
+
range={60}
|
|
182
|
+
disabled={disabled || isLoading}
|
|
183
|
+
>
|
|
184
|
+
Iniciar Sesión
|
|
185
|
+
</MagneticButton>
|
|
186
|
+
</motion.div>
|
|
187
|
+
|
|
188
|
+
</form>
|
|
189
|
+
)}
|
|
190
|
+
</motion.div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { motion, useMotionValue, useSpring } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
type MotionSafeButtonProps = Omit<
|
|
5
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
6
|
+
'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'onDrag' | 'onDragEnd' | 'onDragStart'
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export interface MagneticButtonProps extends MotionSafeButtonProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
range?: number; // Mouse distance range where magnetic effect activates
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MagneticButton: React.FC<MagneticButtonProps> = ({
|
|
16
|
+
children,
|
|
17
|
+
className = '',
|
|
18
|
+
range = 60,
|
|
19
|
+
...props
|
|
20
|
+
}) => {
|
|
21
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
22
|
+
|
|
23
|
+
// Motion values for button container movement
|
|
24
|
+
const x = useMotionValue(0);
|
|
25
|
+
const y = useMotionValue(0);
|
|
26
|
+
|
|
27
|
+
// Motion values for internal text movement (parallax)
|
|
28
|
+
const textX = useMotionValue(0);
|
|
29
|
+
const textY = useMotionValue(0);
|
|
30
|
+
|
|
31
|
+
// Smooth springs for button
|
|
32
|
+
const springX = useSpring(x, { stiffness: 80, damping: 15, mass: 0.8 });
|
|
33
|
+
const springY = useSpring(y, { stiffness: 80, damping: 15, mass: 0.8 });
|
|
34
|
+
|
|
35
|
+
// Smooth springs for text
|
|
36
|
+
const textSpringX = useSpring(textX, { stiffness: 120, damping: 15, mass: 0.6 });
|
|
37
|
+
const textSpringY = useSpring(textY, { stiffness: 120, damping: 15, mass: 0.6 });
|
|
38
|
+
|
|
39
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
40
|
+
if (!buttonRef.current || props.disabled) return;
|
|
41
|
+
|
|
42
|
+
const { clientX, clientY } = e;
|
|
43
|
+
const { left, top, width, height } = buttonRef.current.getBoundingClientRect();
|
|
44
|
+
|
|
45
|
+
const centerX = left + width / 2;
|
|
46
|
+
const centerY = top + height / 2;
|
|
47
|
+
|
|
48
|
+
const distanceX = clientX - centerX;
|
|
49
|
+
const distanceY = clientY - centerY;
|
|
50
|
+
const distance = Math.hypot(distanceX, distanceY);
|
|
51
|
+
|
|
52
|
+
if (distance < range) {
|
|
53
|
+
// Calculate pull strength based on distance (closer = stronger pull)
|
|
54
|
+
const pull = 0.4;
|
|
55
|
+
x.set(distanceX * pull);
|
|
56
|
+
y.set(distanceY * pull);
|
|
57
|
+
|
|
58
|
+
// Text moves a bit less for parallax depth
|
|
59
|
+
textX.set(distanceX * 0.2);
|
|
60
|
+
textY.set(distanceY * 0.2);
|
|
61
|
+
} else {
|
|
62
|
+
handleMouseLeave();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleMouseLeave = () => {
|
|
67
|
+
x.set(0);
|
|
68
|
+
y.set(0);
|
|
69
|
+
textX.set(0);
|
|
70
|
+
textY.set(0);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<motion.button
|
|
75
|
+
ref={buttonRef}
|
|
76
|
+
onMouseMove={handleMouseMove}
|
|
77
|
+
onMouseLeave={handleMouseLeave}
|
|
78
|
+
style={{
|
|
79
|
+
x: springX,
|
|
80
|
+
y: springY,
|
|
81
|
+
}}
|
|
82
|
+
className={`relative inline-flex items-center justify-center rounded-xl bg-accent px-6 py-3 font-semibold text-white shadow-md transition-colors select-none ${
|
|
83
|
+
props.disabled
|
|
84
|
+
? 'opacity-40 cursor-not-allowed bg-accent/60'
|
|
85
|
+
: 'hover:bg-accent-hover active:scale-95 cursor-pointer'
|
|
86
|
+
} ${className}`}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
<motion.span
|
|
90
|
+
style={{
|
|
91
|
+
x: textSpringX,
|
|
92
|
+
y: textSpringY,
|
|
93
|
+
display: 'block',
|
|
94
|
+
}}
|
|
95
|
+
className="pointer-events-none"
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</motion.span>
|
|
99
|
+
</motion.button>
|
|
100
|
+
);
|
|
101
|
+
};
|