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.
Files changed (124) hide show
  1. package/README.md +57 -0
  2. package/components/ActionTable.tsx +307 -0
  3. package/components/AlertBanner.tsx +124 -0
  4. package/components/AnimatedAccordion.tsx +95 -0
  5. package/components/Autocomplete.tsx +144 -0
  6. package/components/Avatar.tsx +123 -0
  7. package/components/Badge.tsx +80 -0
  8. package/components/Breadcrumb.tsx +74 -0
  9. package/components/Calendar.tsx +340 -0
  10. package/components/Card3D.tsx +117 -0
  11. package/components/Carousel3D.tsx +193 -0
  12. package/components/CascadeSelect.tsx +232 -0
  13. package/components/ChartShowcase.tsx +700 -0
  14. package/components/Checkbox.tsx +212 -0
  15. package/components/ChipsInput.tsx +152 -0
  16. package/components/CircularKnob.tsx +240 -0
  17. package/components/CodeVisualizer.tsx +67 -0
  18. package/components/Collapsible.tsx +72 -0
  19. package/components/ColorThemeManager.tsx +458 -0
  20. package/components/CommandMenu.tsx +191 -0
  21. package/components/ConfirmDialog.tsx +152 -0
  22. package/components/ContextMenu.tsx +192 -0
  23. package/components/DashboardLayout.tsx +115 -0
  24. package/components/DatePicker.tsx +108 -0
  25. package/components/Divider.tsx +67 -0
  26. package/components/Dock.tsx +93 -0
  27. package/components/DragDropLists.tsx +160 -0
  28. package/components/Drawer.tsx +161 -0
  29. package/components/DropdownPlus.tsx +304 -0
  30. package/components/EmptyState.tsx +49 -0
  31. package/components/ErrorPage.tsx +62 -0
  32. package/components/FileDropzone.tsx +206 -0
  33. package/components/ForgotPassword.tsx +137 -0
  34. package/components/FormField.tsx +81 -0
  35. package/components/GlassButton.tsx +56 -0
  36. package/components/GlassCard.tsx +82 -0
  37. package/components/GlassInput.tsx +96 -0
  38. package/components/GlassmorphicModal.tsx +108 -0
  39. package/components/GlowInput.tsx +111 -0
  40. package/components/GlowSelect.tsx +203 -0
  41. package/components/GlowTextArea.tsx +105 -0
  42. package/components/HorizontalTimeline.tsx +121 -0
  43. package/components/HoverCard.tsx +105 -0
  44. package/components/ImageLightbox.tsx +259 -0
  45. package/components/InputGroup.tsx +118 -0
  46. package/components/InputOTP.tsx +147 -0
  47. package/components/InteractiveNavbar.tsx +266 -0
  48. package/components/InteractiveSidebar.tsx +211 -0
  49. package/components/Kbd.tsx +51 -0
  50. package/components/LiteYouTube.tsx +118 -0
  51. package/components/LoaderCollection.tsx +368 -0
  52. package/components/LoginForm.tsx +192 -0
  53. package/components/MagneticButton.tsx +101 -0
  54. package/components/MaskedInput.tsx +79 -0
  55. package/components/MentionInput.tsx +413 -0
  56. package/components/MorphingSwitch.tsx +86 -0
  57. package/components/MultiSelect.tsx +158 -0
  58. package/components/NumberInput.tsx +203 -0
  59. package/components/Panel.tsx +104 -0
  60. package/components/PasswordInput.tsx +203 -0
  61. package/components/Popover.tsx +91 -0
  62. package/components/PricingTable.tsx +113 -0
  63. package/components/ProgressBar.tsx +152 -0
  64. package/components/RadioButton.tsx +211 -0
  65. package/components/Rating.tsx +82 -0
  66. package/components/ResizablePanel.tsx +114 -0
  67. package/components/ScrollPanel.tsx +103 -0
  68. package/components/SettingsPage.tsx +154 -0
  69. package/components/SignupForm.tsx +182 -0
  70. package/components/Skeleton.tsx +41 -0
  71. package/components/Slider.tsx +95 -0
  72. package/components/SlidingTabs.tsx +54 -0
  73. package/components/SortableList.tsx +91 -0
  74. package/components/SpeedDial.tsx +134 -0
  75. package/components/Spinner.tsx +40 -0
  76. package/components/Stepper.tsx +124 -0
  77. package/components/TabMenu.tsx +72 -0
  78. package/components/TableControls.tsx +77 -0
  79. package/components/TablePagination.tsx +88 -0
  80. package/components/TextEditor.tsx +329 -0
  81. package/components/TextReveal.tsx +99 -0
  82. package/components/ThemeSwitcher.tsx +133 -0
  83. package/components/TimelineGSAP.tsx +164 -0
  84. package/components/ToastSystem.tsx +110 -0
  85. package/components/ToggleButton.tsx +79 -0
  86. package/components/Tooltip.tsx +121 -0
  87. package/components/Tree.tsx +138 -0
  88. package/dist/commands/add.d.ts +7 -0
  89. package/dist/commands/add.js +110 -0
  90. package/dist/commands/init.d.ts +5 -0
  91. package/dist/commands/init.js +76 -0
  92. package/dist/commands/list.d.ts +1 -0
  93. package/dist/commands/list.js +32 -0
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.js +60 -0
  96. package/dist/registry.d.ts +6 -0
  97. package/dist/registry.js +38 -0
  98. package/dist/tui/browse.d.ts +3 -0
  99. package/dist/tui/browse.js +139 -0
  100. package/dist/tui/format.d.ts +11 -0
  101. package/dist/tui/format.js +52 -0
  102. package/dist/tui/main.d.ts +1 -0
  103. package/dist/tui/main.js +86 -0
  104. package/dist/tui/panels.d.ts +9 -0
  105. package/dist/tui/panels.js +50 -0
  106. package/dist/tui/theme.d.ts +28 -0
  107. package/dist/tui/theme.js +76 -0
  108. package/dist/types.d.ts +28 -0
  109. package/dist/types.js +1 -0
  110. package/dist/utils/config.d.ts +6 -0
  111. package/dist/utils/config.js +24 -0
  112. package/dist/utils/copy.d.ts +9 -0
  113. package/dist/utils/copy.js +43 -0
  114. package/dist/utils/cwd.d.ts +6 -0
  115. package/dist/utils/cwd.js +30 -0
  116. package/dist/utils/deps.d.ts +1 -0
  117. package/dist/utils/deps.js +19 -0
  118. package/dist/utils/project.d.ts +5 -0
  119. package/dist/utils/project.js +30 -0
  120. package/dist/utils/theme.d.ts +1 -0
  121. package/dist/utils/theme.js +24 -0
  122. package/package.json +52 -0
  123. package/registry.json +1133 -0
  124. package/templates/theme.css +81 -0
@@ -0,0 +1,114 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { GripVertical, GripHorizontal } from 'lucide-react';
4
+
5
+ export interface ResizablePanelProps {
6
+ panel1: React.ReactNode;
7
+ panel2: React.ReactNode;
8
+ direction?: 'horizontal' | 'vertical';
9
+ initialSplit?: number; // percentage (0 to 100)
10
+ minSplit?: number;
11
+ maxSplit?: number;
12
+ className?: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export const ResizablePanel: React.FC<ResizablePanelProps> = ({
17
+ panel1,
18
+ panel2,
19
+ direction = 'horizontal',
20
+ initialSplit = 50,
21
+ minSplit = 20,
22
+ maxSplit = 80,
23
+ className = '',
24
+ disabled = false
25
+ }) => {
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+ const [split, setSplit] = useState(initialSplit);
28
+ const [isDragging, setIsDragging] = useState(false);
29
+
30
+ // Apply constraints
31
+ const constrainedSplit = Math.max(minSplit, Math.min(maxSplit, split));
32
+
33
+ const handleDragStart = () => {
34
+ if (disabled) return;
35
+ setIsDragging(true);
36
+ document.body.style.userSelect = 'none';
37
+ };
38
+
39
+ const handleDrag = (_e: unknown, info: { delta: { x: number; y: number } }) => {
40
+ if (disabled || !containerRef.current) return;
41
+
42
+ const containerRect = containerRef.current.getBoundingClientRect();
43
+
44
+ if (direction === 'horizontal') {
45
+ const deltaX = info.delta.x;
46
+ const percentageChange = (deltaX / containerRect.width) * 100;
47
+ setSplit(prev => {
48
+ const next = prev + percentageChange;
49
+ return Math.max(minSplit, Math.min(maxSplit, next));
50
+ });
51
+ } else {
52
+ const deltaY = info.delta.y;
53
+ const percentageChange = (deltaY / containerRect.height) * 100;
54
+ setSplit(prev => {
55
+ const next = prev + percentageChange;
56
+ return Math.max(minSplit, Math.min(maxSplit, next));
57
+ });
58
+ }
59
+ };
60
+
61
+ const handleDragEnd = () => {
62
+ setIsDragging(false);
63
+ document.body.style.userSelect = '';
64
+ // Optional: Snap to edge if very close, but we have minSplit
65
+ };
66
+
67
+ const isHorizontal = direction === 'horizontal';
68
+
69
+ return (
70
+ <div
71
+ ref={containerRef}
72
+ className={`flex w-full h-full overflow-hidden bg-bg-app rounded-xl border border-border-app ${isHorizontal ? 'flex-row' : 'flex-col'} ${className}`}
73
+ >
74
+ {/* Panel 1 */}
75
+ <div
76
+ style={{ [isHorizontal ? 'width' : 'height']: `${constrainedSplit}%` }}
77
+ className="flex-shrink-0 h-full overflow-auto scrollbar-thin relative"
78
+ >
79
+ {panel1}
80
+ </div>
81
+
82
+ {/* Drag Handle */}
83
+ <motion.div
84
+ drag={isHorizontal ? 'x' : 'y'}
85
+ dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }} // Reset visual drag, we handle logic in onDrag
86
+ dragElastic={0}
87
+ dragMomentum={false}
88
+ onDragStart={handleDragStart}
89
+ onDrag={handleDrag}
90
+ onDragEnd={handleDragEnd}
91
+ className={`relative z-10 flex items-center justify-center bg-bg-card transition-colors duration-200
92
+ ${isHorizontal ? 'w-2 cursor-col-resize hover:bg-accent/20' : 'h-2 cursor-row-resize hover:bg-accent/20'}
93
+ ${isDragging ? 'bg-accent/50' : 'border-border-app'}
94
+ ${isHorizontal ? 'border-x' : 'border-y'}
95
+ ${disabled ? 'pointer-events-none opacity-50' : ''}
96
+ `}
97
+ >
98
+ {isHorizontal ? (
99
+ <GripVertical className={`w-3 h-3 text-text-muted transition-colors ${isDragging ? 'text-white' : ''}`} />
100
+ ) : (
101
+ <GripHorizontal className={`w-3 h-3 text-text-muted transition-colors ${isDragging ? 'text-white' : ''}`} />
102
+ )}
103
+ </motion.div>
104
+
105
+ {/* Panel 2 */}
106
+ <div
107
+ style={{ [isHorizontal ? 'width' : 'height']: `${100 - constrainedSplit}%` }}
108
+ className="flex-1 overflow-auto scrollbar-thin relative"
109
+ >
110
+ {panel2}
111
+ </div>
112
+ </div>
113
+ );
114
+ };
@@ -0,0 +1,103 @@
1
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ export interface ScrollPanelProps {
5
+ children: React.ReactNode;
6
+ height?: number | string;
7
+ showScrollbar?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ export const ScrollPanel: React.FC<ScrollPanelProps> = ({
12
+ children,
13
+ height = 280,
14
+ showScrollbar = true,
15
+ className = ''
16
+ }) => {
17
+ const containerRef = useRef<HTMLDivElement>(null);
18
+ const [scrollRatio, setScrollRatio] = useState(0);
19
+ const [thumbHeight, setThumbHeight] = useState(40);
20
+ const [trackHeight, setTrackHeight] = useState(0);
21
+ const [isDragging, setIsDragging] = useState(false);
22
+
23
+ const updateMetrics = useCallback(() => {
24
+ const el = containerRef.current;
25
+ if (!el) return;
26
+ const { scrollTop, scrollHeight, clientHeight } = el;
27
+ const maxScroll = scrollHeight - clientHeight;
28
+ const ratio = maxScroll > 0 ? scrollTop / maxScroll : 0;
29
+ const thumb = Math.max((clientHeight / scrollHeight) * clientHeight, 28);
30
+ setScrollRatio(ratio);
31
+ setThumbHeight(thumb);
32
+ setTrackHeight(Math.max(clientHeight - thumb - 8, 0));
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ const el = containerRef.current;
37
+ if (!el) return;
38
+ updateMetrics();
39
+ el.addEventListener('scroll', updateMetrics);
40
+ window.addEventListener('resize', updateMetrics);
41
+ return () => {
42
+ el.removeEventListener('scroll', updateMetrics);
43
+ window.removeEventListener('resize', updateMetrics);
44
+ };
45
+ }, [updateMetrics, children]);
46
+
47
+ const handleThumbDrag = (clientY: number) => {
48
+ const el = containerRef.current;
49
+ if (!el) return;
50
+ const rect = el.getBoundingClientRect();
51
+ const travel = Math.max(rect.height - thumbHeight - 8, 0);
52
+ const relativeY = Math.min(Math.max(clientY - rect.top - thumbHeight / 2 - 4, 0), travel);
53
+ const ratio = travel > 0 ? relativeY / travel : 0;
54
+ const maxScroll = el.scrollHeight - el.clientHeight;
55
+ el.scrollTop = ratio * maxScroll;
56
+ };
57
+
58
+ return (
59
+ <div
60
+ className={`relative flex rounded-2xl border border-border-app bg-bg-card overflow-hidden ${className}`}
61
+ style={{ height }}
62
+ >
63
+ <div
64
+ ref={containerRef}
65
+ className={`flex-1 overflow-y-auto overflow-x-hidden p-4 scroll-smooth ${
66
+ showScrollbar ? '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden' : ''
67
+ }`}
68
+ >
69
+ {children}
70
+ </div>
71
+
72
+ {showScrollbar && (
73
+ <div
74
+ className="w-3 flex-shrink-0 relative my-2 mr-2 rounded-full bg-bg-app/80"
75
+ aria-hidden
76
+ >
77
+ <motion.div
78
+ className={`absolute inset-x-0.5 rounded-full transition-colors ${
79
+ isDragging ? 'bg-accent' : 'bg-accent/50 hover:bg-accent/70'
80
+ } cursor-grab active:cursor-grabbing`}
81
+ style={{
82
+ height: thumbHeight,
83
+ top: 4 + scrollRatio * trackHeight
84
+ }}
85
+ onPointerDown={(e) => {
86
+ e.preventDefault();
87
+ setIsDragging(true);
88
+ handleThumbDrag(e.clientY);
89
+ const onMove = (ev: PointerEvent) => handleThumbDrag(ev.clientY);
90
+ const onUp = () => {
91
+ setIsDragging(false);
92
+ window.removeEventListener('pointermove', onMove);
93
+ window.removeEventListener('pointerup', onUp);
94
+ };
95
+ window.addEventListener('pointermove', onMove);
96
+ window.addEventListener('pointerup', onUp);
97
+ }}
98
+ />
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ };
@@ -0,0 +1,154 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { User, Bell, Shield, Palette } from 'lucide-react';
4
+ import { Panel } from './Panel';
5
+ import { GlowInput } from './GlowInput';
6
+ import { MorphingSwitch } from './MorphingSwitch';
7
+ import { GlassButton } from './GlassButton';
8
+ import { Skeleton } from './Skeleton';
9
+
10
+ export type SettingsSection = 'profile' | 'notifications' | 'security' | 'appearance';
11
+
12
+ export interface SettingsPageProps {
13
+ defaultSection?: SettingsSection;
14
+ onSave?: (section: SettingsSection) => void;
15
+ isLoading?: boolean;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ }
19
+
20
+ const SECTIONS: Array<{ id: SettingsSection; label: string; icon: React.ReactNode }> = [
21
+ { id: 'profile', label: 'Perfil', icon: <User className="w-4 h-4" /> },
22
+ { id: 'notifications', label: 'Notificaciones', icon: <Bell className="w-4 h-4" /> },
23
+ { id: 'security', label: 'Seguridad', icon: <Shield className="w-4 h-4" /> },
24
+ { id: 'appearance', label: 'Apariencia', icon: <Palette className="w-4 h-4" /> },
25
+ ];
26
+
27
+ export const SettingsPage: React.FC<SettingsPageProps> = ({
28
+ defaultSection = 'profile',
29
+ onSave,
30
+ isLoading = false,
31
+ disabled = false,
32
+ className = ''
33
+ }) => {
34
+ const [active, setActive] = useState<SettingsSection>(defaultSection);
35
+ const [name, setName] = useState('Alexis Jardin');
36
+ const [email, setEmail] = useState('alex@alexui.dev');
37
+ const [emailNotifs, setEmailNotifs] = useState(true);
38
+ const [pushNotifs, setPushNotifs] = useState(false);
39
+ const [twoFactor, setTwoFactor] = useState(true);
40
+ const [compactMode, setCompactMode] = useState(false);
41
+
42
+ if (isLoading) {
43
+ return (
44
+ <div className={`w-full flex gap-4 ${className}`}>
45
+ <Skeleton variant="rect" className="w-40 h-64 rounded-2xl" />
46
+ <Skeleton variant="rect" className="flex-1 h-64 rounded-2xl" />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <motion.div
53
+ initial={{ opacity: 0, y: 10 }}
54
+ animate={{ opacity: 1, y: 0 }}
55
+ className={`w-full flex flex-col md:flex-row gap-4 ${className}`}
56
+ >
57
+ <nav className="flex md:flex-col gap-1 p-2 glass rounded-2xl border border-border-app md:w-44 flex-shrink-0 overflow-x-auto">
58
+ {SECTIONS.map((section) => (
59
+ <button
60
+ key={section.id}
61
+ type="button"
62
+ disabled={disabled}
63
+ onClick={() => setActive(section.id)}
64
+ className={`flex items-center gap-2 px-3 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-colors cursor-pointer disabled:opacity-40 ${
65
+ active === section.id
66
+ ? 'bg-accent/15 text-accent border border-accent/25'
67
+ : 'text-text-muted hover:text-text-main hover:bg-bg-app/50 border border-transparent'
68
+ }`}
69
+ >
70
+ {section.icon}
71
+ {section.label}
72
+ </button>
73
+ ))}
74
+ </nav>
75
+
76
+ <div className="flex-1 min-w-0">
77
+ <AnimatePresence mode="wait">
78
+ <motion.div
79
+ key={active}
80
+ initial={{ opacity: 0, x: 8 }}
81
+ animate={{ opacity: 1, x: 0 }}
82
+ exit={{ opacity: 0, x: -8 }}
83
+ transition={{ duration: 0.18 }}
84
+ className="flex flex-col gap-4"
85
+ >
86
+ {active === 'profile' && (
87
+ <Panel title="Información personal" subtitle="Actualizá tu perfil público" defaultOpen collapsible={false}>
88
+ <div className="flex flex-col gap-4">
89
+ <GlowInput label="Nombre" value={name} onChange={(e) => setName(e.target.value)} disabled={disabled} />
90
+ <GlowInput label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} disabled={disabled} />
91
+ <GlassButton variant="primary" size="sm" disabled={disabled} onClick={() => onSave?.('profile')}>
92
+ Guardar perfil
93
+ </GlassButton>
94
+ </div>
95
+ </Panel>
96
+ )}
97
+
98
+ {active === 'notifications' && (
99
+ <Panel title="Alertas" subtitle="Elegí cómo querés recibir novedades" defaultOpen collapsible={false}>
100
+ <div className="flex flex-col gap-4">
101
+ <label className="flex items-center justify-between gap-3 text-sm">
102
+ <span className="text-text-main font-semibold">Email semanal</span>
103
+ <MorphingSwitch checked={emailNotifs} onChange={setEmailNotifs} disabled={disabled} />
104
+ </label>
105
+ <label className="flex items-center justify-between gap-3 text-sm">
106
+ <span className="text-text-main font-semibold">Push en tiempo real</span>
107
+ <MorphingSwitch checked={pushNotifs} onChange={setPushNotifs} disabled={disabled} />
108
+ </label>
109
+ <GlassButton variant="primary" size="sm" disabled={disabled} onClick={() => onSave?.('notifications')}>
110
+ Guardar preferencias
111
+ </GlassButton>
112
+ </div>
113
+ </Panel>
114
+ )}
115
+
116
+ {active === 'security' && (
117
+ <Panel title="Seguridad" subtitle="Protegé tu cuenta" defaultOpen collapsible={false}>
118
+ <div className="flex flex-col gap-4">
119
+ <label className="flex items-center justify-between gap-3 text-sm">
120
+ <span className="text-text-main font-semibold">Autenticación 2FA</span>
121
+ <MorphingSwitch checked={twoFactor} onChange={setTwoFactor} disabled={disabled} />
122
+ </label>
123
+ <GlassButton variant="secondary" size="sm" disabled={disabled}>
124
+ Cambiar contraseña
125
+ </GlassButton>
126
+ <GlassButton variant="primary" size="sm" disabled={disabled} onClick={() => onSave?.('security')}>
127
+ Guardar seguridad
128
+ </GlassButton>
129
+ </div>
130
+ </Panel>
131
+ )}
132
+
133
+ {active === 'appearance' && (
134
+ <Panel title="Apariencia" subtitle="Personalizá la interfaz" defaultOpen collapsible={false}>
135
+ <div className="flex flex-col gap-4">
136
+ <label className="flex items-center justify-between gap-3 text-sm">
137
+ <span className="text-text-main font-semibold">Modo compacto</span>
138
+ <MorphingSwitch checked={compactMode} onChange={setCompactMode} disabled={disabled} />
139
+ </label>
140
+ <p className="text-xs text-text-muted">
141
+ Usá el Color Theme Manager del catálogo para exportar tu paleta CSS.
142
+ </p>
143
+ <GlassButton variant="primary" size="sm" disabled={disabled} onClick={() => onSave?.('appearance')}>
144
+ Guardar apariencia
145
+ </GlassButton>
146
+ </div>
147
+ </Panel>
148
+ )}
149
+ </motion.div>
150
+ </AnimatePresence>
151
+ </div>
152
+ </motion.div>
153
+ );
154
+ };
@@ -0,0 +1,182 @@
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { UserPlus } from 'lucide-react';
4
+ import { GlowInput } from './GlowInput';
5
+ import { MorphingSwitch } from './MorphingSwitch';
6
+ import { MagneticButton } from './MagneticButton';
7
+ import { Skeleton } from './Skeleton';
8
+
9
+ export interface SignupFormProps {
10
+ onSubmit?: (data: { name: string; email: string; password: string; acceptTerms: boolean }) => void;
11
+ onLoginClick?: () => void;
12
+ className?: string;
13
+ isLoading?: boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export const SignupForm: React.FC<SignupFormProps> = ({
18
+ onSubmit,
19
+ onLoginClick,
20
+ className = '',
21
+ isLoading = false,
22
+ disabled = false
23
+ }) => {
24
+ const [name, setName] = useState('');
25
+ const [email, setEmail] = useState('');
26
+ const [password, setPassword] = useState('');
27
+ const [confirmPassword, setConfirmPassword] = useState('');
28
+ const [acceptTerms, setAcceptTerms] = useState(false);
29
+ const [errors, setErrors] = useState<Record<string, string>>({});
30
+
31
+ const validate = () => {
32
+ const next: Record<string, string> = {};
33
+ if (!name.trim()) next.name = 'El nombre es obligatorio';
34
+ if (!email) next.email = 'El correo es obligatorio';
35
+ else if (!email.includes('@')) next.email = 'Correo electrónico no válido';
36
+ if (!password) next.password = 'La contraseña es obligatoria';
37
+ else if (password.length < 6) next.password = 'Mínimo 6 caracteres';
38
+ if (password !== confirmPassword) next.confirmPassword = 'Las contraseñas no coinciden';
39
+ if (!acceptTerms) next.terms = 'Debés aceptar los términos';
40
+ setErrors(next);
41
+ return Object.keys(next).length === 0;
42
+ };
43
+
44
+ const handleSubmit = (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ if (disabled || !validate()) return;
47
+ onSubmit?.({ name: name.trim(), email, password, acceptTerms });
48
+ };
49
+
50
+ const containerVariants = {
51
+ hidden: { opacity: 0, scale: 0.95, y: 15 },
52
+ show: {
53
+ opacity: 1,
54
+ scale: 1,
55
+ y: 0,
56
+ transition: {
57
+ type: 'spring' as const,
58
+ stiffness: 300,
59
+ damping: 25,
60
+ staggerChildren: 0.07,
61
+ delayChildren: 0.08
62
+ }
63
+ }
64
+ };
65
+
66
+ const itemVariants = {
67
+ hidden: { opacity: 0, y: 10 },
68
+ show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 300, damping: 25 } }
69
+ };
70
+
71
+ return (
72
+ <motion.div
73
+ variants={containerVariants}
74
+ initial="hidden"
75
+ animate="show"
76
+ className={`w-full max-w-md glass rounded-3xl p-8 bg-bg-card/70 border border-border-app shadow-2xl ${className}`}
77
+ >
78
+ {isLoading ? (
79
+ <div className="flex flex-col gap-5">
80
+ <Skeleton variant="circle" className="w-10 h-10 mx-auto" />
81
+ <Skeleton variant="rect" className="w-full h-11" />
82
+ <Skeleton variant="rect" className="w-full h-11" />
83
+ <Skeleton variant="rect" className="w-full h-11" />
84
+ <Skeleton variant="rect" className="w-full h-12" />
85
+ </div>
86
+ ) : (
87
+ <form onSubmit={handleSubmit} className="flex flex-col gap-5">
88
+ <motion.div variants={itemVariants} className="text-center flex flex-col items-center gap-1">
89
+ <div className="w-10 h-10 rounded-2xl bg-accent/15 border border-accent/20 flex items-center justify-center text-accent mb-2">
90
+ <UserPlus className="w-5 h-5" />
91
+ </div>
92
+ <h2 className="text-2xl font-extrabold tracking-tight text-text-main font-display">
93
+ Crear cuenta
94
+ </h2>
95
+ <p className="text-xs text-text-muted">
96
+ Completá tus datos para registrarte en AlexUI
97
+ </p>
98
+ </motion.div>
99
+
100
+ <motion.div variants={itemVariants}>
101
+ <GlowInput
102
+ label="Nombre completo"
103
+ value={name}
104
+ onChange={(e) => setName(e.target.value)}
105
+ error={errors.name}
106
+ placeholder="Alexis Jardin"
107
+ disabled={disabled}
108
+ />
109
+ </motion.div>
110
+
111
+ <motion.div variants={itemVariants}>
112
+ <GlowInput
113
+ label="Correo electrónico"
114
+ type="email"
115
+ value={email}
116
+ onChange={(e) => setEmail(e.target.value)}
117
+ error={errors.email}
118
+ placeholder="alex@ejemplo.com"
119
+ disabled={disabled}
120
+ />
121
+ </motion.div>
122
+
123
+ <motion.div variants={itemVariants}>
124
+ <GlowInput
125
+ label="Contraseña"
126
+ type="password"
127
+ value={password}
128
+ onChange={(e) => setPassword(e.target.value)}
129
+ error={errors.password}
130
+ placeholder="••••••••"
131
+ disabled={disabled}
132
+ />
133
+ </motion.div>
134
+
135
+ <motion.div variants={itemVariants}>
136
+ <GlowInput
137
+ label="Confirmar contraseña"
138
+ type="password"
139
+ value={confirmPassword}
140
+ onChange={(e) => setConfirmPassword(e.target.value)}
141
+ error={errors.confirmPassword}
142
+ placeholder="••••••••"
143
+ disabled={disabled}
144
+ />
145
+ </motion.div>
146
+
147
+ <motion.div variants={itemVariants} className="flex items-center gap-2 text-xs">
148
+ <MorphingSwitch
149
+ checked={acceptTerms}
150
+ onChange={setAcceptTerms}
151
+ disabled={disabled}
152
+ />
153
+ <span className="font-semibold text-text-muted select-none">
154
+ Acepto los términos y condiciones
155
+ </span>
156
+ </motion.div>
157
+ {errors.terms && (
158
+ <p className="text-xs text-red-400 font-semibold -mt-3 px-1">{errors.terms}</p>
159
+ )}
160
+
161
+ <motion.div variants={itemVariants}>
162
+ <MagneticButton type="submit" className="w-full py-3.5" range={60} disabled={disabled || isLoading}>
163
+ Registrarme
164
+ </MagneticButton>
165
+ </motion.div>
166
+
167
+ <motion.p variants={itemVariants} className="text-center text-xs text-text-muted">
168
+ ¿Ya tenés cuenta?{' '}
169
+ <button
170
+ type="button"
171
+ onClick={onLoginClick}
172
+ disabled={disabled}
173
+ className="font-bold text-accent hover:text-accent-hover transition-colors disabled:opacity-40"
174
+ >
175
+ Iniciar sesión
176
+ </button>
177
+ </motion.p>
178
+ </form>
179
+ )}
180
+ </motion.div>
181
+ );
182
+ };
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ export interface SkeletonProps {
5
+ className?: string;
6
+ variant?: 'text' | 'rect' | 'circle';
7
+ }
8
+
9
+ export const Skeleton: React.FC<SkeletonProps> = ({ className = '', variant = 'rect' }) => {
10
+ const roundedClass =
11
+ variant === 'circle'
12
+ ? 'rounded-full'
13
+ : variant === 'text'
14
+ ? 'rounded-md h-4 w-3/4'
15
+ : 'rounded-xl';
16
+
17
+ return (
18
+ <div
19
+ className={`relative overflow-hidden bg-bg-app border border-border-app/40 ${roundedClass} ${className}`}
20
+ >
21
+ {/* Repeating shimmer sweep background overlay */}
22
+ <motion.div
23
+ animate={{
24
+ x: ['-100%', '100%']
25
+ }}
26
+ transition={{
27
+ repeat: Infinity,
28
+ duration: 1.6,
29
+ ease: 'linear'
30
+ }}
31
+ className="absolute inset-0 bg-gradient-to-r from-transparent via-accent/5 to-transparent z-0"
32
+ style={{
33
+ width: '50%'
34
+ }}
35
+ />
36
+
37
+ {/* Solid pulse spacer (guarantees layout size) */}
38
+ <div className="w-full h-full opacity-0 pointer-events-none select-none">Spacer</div>
39
+ </div>
40
+ );
41
+ };
@@ -0,0 +1,95 @@
1
+ import React, { useRef } from 'react';
2
+ import { motion, useMotionValue, useTransform } from 'framer-motion';
3
+
4
+ export interface SliderProps {
5
+ value: number;
6
+ onChange: (value: number) => void;
7
+ min?: number;
8
+ max?: number;
9
+ step?: number;
10
+ label?: string;
11
+ showValue?: boolean;
12
+ variant?: 'accent' | 'success' | 'warning' | 'danger';
13
+ disabled?: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ const variantColors = {
18
+ accent: { track: 'bg-accent', glow: 'shadow-[0_0_12px_var(--color-accent-glow)]' },
19
+ success: { track: 'bg-green-500', glow: 'shadow-[0_0_12px_rgba(34,197,94,0.4)]' },
20
+ warning: { track: 'bg-yellow-500', glow: 'shadow-[0_0_12px_rgba(234,179,8,0.4)]' },
21
+ danger: { track: 'bg-red-500', glow: 'shadow-[0_0_12px_rgba(239,68,68,0.4)]' }
22
+ };
23
+
24
+ export const Slider: React.FC<SliderProps> = ({
25
+ value,
26
+ onChange,
27
+ min = 0,
28
+ max = 100,
29
+ step = 1,
30
+ label,
31
+ showValue = true,
32
+ variant = 'accent',
33
+ disabled = false,
34
+ className = ''
35
+ }) => {
36
+ const trackRef = useRef<HTMLDivElement>(null);
37
+ const percentage = ((value - min) / (max - min)) * 100;
38
+ const colors = variantColors[variant];
39
+
40
+ const updateFromClientX = (clientX: number) => {
41
+ if (disabled || !trackRef.current) return;
42
+ const rect = trackRef.current.getBoundingClientRect();
43
+ const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
44
+ const raw = min + ratio * (max - min);
45
+ const stepped = Math.round(raw / step) * step;
46
+ onChange(Math.min(max, Math.max(min, stepped)));
47
+ };
48
+
49
+ const thumbX = useMotionValue(percentage);
50
+ const fillWidth = useTransform(thumbX, (v) => `${v}%`);
51
+
52
+ React.useEffect(() => {
53
+ thumbX.set(percentage);
54
+ }, [percentage, thumbX]);
55
+
56
+ return (
57
+ <div className={`w-full flex flex-col gap-2 select-none ${disabled ? 'opacity-40 cursor-not-allowed' : ''} ${className}`}>
58
+ {(label || showValue) && (
59
+ <div className="flex items-center justify-between text-xs font-bold">
60
+ {label && <span className="text-text-muted font-mono uppercase tracking-wider">{label}</span>}
61
+ {showValue && <span className="text-accent font-display">{value}</span>}
62
+ </div>
63
+ )}
64
+
65
+ <div
66
+ ref={trackRef}
67
+ className={`relative h-3 rounded-full bg-bg-app border border-border-app/80 ${disabled ? 'pointer-events-none' : 'cursor-pointer'}`}
68
+ onPointerDown={(e) => {
69
+ if (disabled) return;
70
+ updateFromClientX(e.clientX);
71
+ const onMove = (ev: PointerEvent) => updateFromClientX(ev.clientX);
72
+ const onUp = () => {
73
+ window.removeEventListener('pointermove', onMove);
74
+ window.removeEventListener('pointerup', onUp);
75
+ };
76
+ window.addEventListener('pointermove', onMove);
77
+ window.addEventListener('pointerup', onUp);
78
+ }}
79
+ >
80
+ <motion.div
81
+ className={`absolute inset-y-0 left-0 rounded-full ${colors.track} ${colors.glow}`}
82
+ style={{ width: fillWidth }}
83
+ />
84
+
85
+ <motion.div
86
+ className={`absolute top-1/2 -translate-y-1/2 w-5 h-5 rounded-full bg-bg-card border-2 border-accent ${colors.glow}`}
87
+ style={{ left: fillWidth, x: '-50%' }}
88
+ whileHover={disabled ? undefined : { scale: 1.15 }}
89
+ whileTap={disabled ? undefined : { scale: 0.95 }}
90
+ transition={{ type: 'spring' as const, stiffness: 500, damping: 28 }}
91
+ />
92
+ </div>
93
+ </div>
94
+ );
95
+ };