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,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
|
+
};
|