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,137 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { KeyRound, ArrowLeft, CheckCircle } from 'lucide-react';
|
|
4
|
+
import { GlowInput } from './GlowInput';
|
|
5
|
+
import { MagneticButton } from './MagneticButton';
|
|
6
|
+
import { Skeleton } from './Skeleton';
|
|
7
|
+
|
|
8
|
+
export interface ForgotPasswordProps {
|
|
9
|
+
onSubmit?: (email: string) => void;
|
|
10
|
+
onBackToLogin?: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ForgotPassword: React.FC<ForgotPasswordProps> = ({
|
|
17
|
+
onSubmit,
|
|
18
|
+
onBackToLogin,
|
|
19
|
+
className = '',
|
|
20
|
+
isLoading = false,
|
|
21
|
+
disabled = false
|
|
22
|
+
}) => {
|
|
23
|
+
const [email, setEmail] = useState('');
|
|
24
|
+
const [error, setError] = useState<string | undefined>();
|
|
25
|
+
const [sent, setSent] = useState(false);
|
|
26
|
+
|
|
27
|
+
const validate = () => {
|
|
28
|
+
if (!email) {
|
|
29
|
+
setError('El correo es obligatorio');
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!email.includes('@')) {
|
|
33
|
+
setError('Correo electrónico no válido');
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
setError(undefined);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
if (disabled || !validate()) return;
|
|
43
|
+
onSubmit?.(email);
|
|
44
|
+
setSent(true);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<motion.div
|
|
49
|
+
initial={{ opacity: 0, scale: 0.95, y: 12 }}
|
|
50
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
51
|
+
transition={{ type: 'spring', stiffness: 300, damping: 26 }}
|
|
52
|
+
className={`w-full max-w-md glass rounded-3xl p-8 bg-bg-card/70 border border-border-app shadow-2xl ${className}`}
|
|
53
|
+
>
|
|
54
|
+
{isLoading ? (
|
|
55
|
+
<div className="flex flex-col gap-5">
|
|
56
|
+
<Skeleton variant="circle" className="w-10 h-10 mx-auto" />
|
|
57
|
+
<Skeleton variant="text" className="w-48 h-5 mx-auto" />
|
|
58
|
+
<Skeleton variant="rect" className="w-full h-11" />
|
|
59
|
+
<Skeleton variant="rect" className="w-full h-12" />
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<AnimatePresence mode="wait">
|
|
63
|
+
{sent ? (
|
|
64
|
+
<motion.div
|
|
65
|
+
key="success"
|
|
66
|
+
initial={{ opacity: 0, y: 8 }}
|
|
67
|
+
animate={{ opacity: 1, y: 0 }}
|
|
68
|
+
exit={{ opacity: 0, y: -8 }}
|
|
69
|
+
className="flex flex-col items-center gap-4 text-center"
|
|
70
|
+
>
|
|
71
|
+
<div className="w-12 h-12 rounded-2xl bg-green-500/15 border border-green-500/25 text-green-500 flex items-center justify-center">
|
|
72
|
+
<CheckCircle className="w-6 h-6" />
|
|
73
|
+
</div>
|
|
74
|
+
<h2 className="text-xl font-extrabold text-text-main font-display">Revisá tu correo</h2>
|
|
75
|
+
<p className="text-sm text-text-muted leading-relaxed">
|
|
76
|
+
Enviamos un enlace de recuperación a <span className="text-text-main font-semibold">{email}</span>.
|
|
77
|
+
</p>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={onBackToLogin}
|
|
81
|
+
className="mt-2 text-sm font-bold text-accent hover:text-accent-hover transition-colors flex items-center gap-1.5"
|
|
82
|
+
>
|
|
83
|
+
<ArrowLeft className="w-4 h-4" />
|
|
84
|
+
Volver al login
|
|
85
|
+
</button>
|
|
86
|
+
</motion.div>
|
|
87
|
+
) : (
|
|
88
|
+
<motion.form
|
|
89
|
+
key="form"
|
|
90
|
+
initial={{ opacity: 0, y: 8 }}
|
|
91
|
+
animate={{ opacity: 1, y: 0 }}
|
|
92
|
+
exit={{ opacity: 0, y: -8 }}
|
|
93
|
+
onSubmit={handleSubmit}
|
|
94
|
+
className="flex flex-col gap-5"
|
|
95
|
+
>
|
|
96
|
+
<div className="text-center flex flex-col items-center gap-1">
|
|
97
|
+
<div className="w-10 h-10 rounded-2xl bg-accent/15 border border-accent/20 flex items-center justify-center text-accent mb-2">
|
|
98
|
+
<KeyRound className="w-5 h-5" />
|
|
99
|
+
</div>
|
|
100
|
+
<h2 className="text-2xl font-extrabold tracking-tight text-text-main font-display">
|
|
101
|
+
¿Olvidaste tu contraseña?
|
|
102
|
+
</h2>
|
|
103
|
+
<p className="text-xs text-text-muted max-w-xs">
|
|
104
|
+
Ingresá tu email y te enviaremos instrucciones para restablecerla.
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<GlowInput
|
|
109
|
+
label="Correo electrónico"
|
|
110
|
+
type="email"
|
|
111
|
+
value={email}
|
|
112
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
113
|
+
error={error}
|
|
114
|
+
placeholder="alex@ejemplo.com"
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<MagneticButton type="submit" className="w-full py-3.5" range={60} disabled={disabled || isLoading}>
|
|
119
|
+
Enviar enlace
|
|
120
|
+
</MagneticButton>
|
|
121
|
+
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={onBackToLogin}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
className="text-xs font-bold text-text-muted hover:text-accent transition-colors flex items-center justify-center gap-1.5 disabled:opacity-40"
|
|
127
|
+
>
|
|
128
|
+
<ArrowLeft className="w-3.5 h-3.5" />
|
|
129
|
+
Volver al login
|
|
130
|
+
</button>
|
|
131
|
+
</motion.form>
|
|
132
|
+
)}
|
|
133
|
+
</AnimatePresence>
|
|
134
|
+
)}
|
|
135
|
+
</motion.div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
5
|
+
required?: boolean;
|
|
6
|
+
hint?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Label: React.FC<LabelProps> = ({
|
|
10
|
+
children,
|
|
11
|
+
required = false,
|
|
12
|
+
hint,
|
|
13
|
+
className = '',
|
|
14
|
+
...props
|
|
15
|
+
}) => (
|
|
16
|
+
<label
|
|
17
|
+
className={`flex flex-col gap-0.5 text-sm font-bold text-text-main ${className}`}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<span className="inline-flex items-center gap-1">
|
|
21
|
+
{children}
|
|
22
|
+
{required && <span className="text-red-400" aria-hidden>*</span>}
|
|
23
|
+
</span>
|
|
24
|
+
{hint && <span className="text-[11px] font-medium text-text-muted">{hint}</span>}
|
|
25
|
+
</label>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export interface FormFieldProps {
|
|
29
|
+
label: string;
|
|
30
|
+
htmlFor?: string;
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
description?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const FormField: React.FC<FormFieldProps> = ({
|
|
39
|
+
label,
|
|
40
|
+
htmlFor,
|
|
41
|
+
children,
|
|
42
|
+
description,
|
|
43
|
+
error,
|
|
44
|
+
required = false,
|
|
45
|
+
className = ''
|
|
46
|
+
}) => {
|
|
47
|
+
const autoId = useId();
|
|
48
|
+
const fieldId = htmlFor ?? autoId;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={`flex flex-col gap-2 w-full ${className}`}>
|
|
52
|
+
<Label htmlFor={fieldId} required={required}>
|
|
53
|
+
{label}
|
|
54
|
+
</Label>
|
|
55
|
+
|
|
56
|
+
{description && (
|
|
57
|
+
<p className="text-xs text-text-muted -mt-1">{description}</p>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
<div className="w-full">
|
|
61
|
+
{React.isValidElement(children)
|
|
62
|
+
? React.cloneElement(children as React.ReactElement<{ id?: string }>, { id: fieldId })
|
|
63
|
+
: children}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<AnimatePresence>
|
|
67
|
+
{error && (
|
|
68
|
+
<motion.p
|
|
69
|
+
initial={{ opacity: 0, y: -4 }}
|
|
70
|
+
animate={{ opacity: 1, y: 0 }}
|
|
71
|
+
exit={{ opacity: 0, y: -4 }}
|
|
72
|
+
className="text-xs text-red-400 font-semibold px-1"
|
|
73
|
+
role="alert"
|
|
74
|
+
>
|
|
75
|
+
{error}
|
|
76
|
+
</motion.p>
|
|
77
|
+
)}
|
|
78
|
+
</AnimatePresence>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
type MotionSafeButtonProps = Omit<
|
|
5
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
6
|
+
'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'onDrag' | 'onDragEnd' | 'onDragStart'
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export interface GlassButtonProps extends MotionSafeButtonProps {
|
|
10
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
leftIcon?: React.ReactNode;
|
|
13
|
+
rightIcon?: React.ReactNode;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const GlassButton: React.FC<GlassButtonProps> = ({
|
|
18
|
+
variant = 'secondary',
|
|
19
|
+
size = 'md',
|
|
20
|
+
leftIcon,
|
|
21
|
+
rightIcon,
|
|
22
|
+
children,
|
|
23
|
+
className = '',
|
|
24
|
+
disabled,
|
|
25
|
+
...props
|
|
26
|
+
}) => {
|
|
27
|
+
|
|
28
|
+
// Custom theme-aligned class mapping
|
|
29
|
+
const variantStyles = {
|
|
30
|
+
primary: 'bg-accent text-white hover:enabled:bg-accent-hover border-accent/20 shadow-[0_4px_12px_rgba(var(--color-accent),0.2)]',
|
|
31
|
+
secondary: 'glass bg-bg-card/50 text-text-main border-border-app hover:enabled:border-accent hover:enabled:text-accent shadow-xs',
|
|
32
|
+
ghost: 'bg-transparent text-text-muted hover:enabled:text-text-main hover:enabled:bg-bg-app border-transparent'
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const sizeStyles = {
|
|
36
|
+
sm: 'px-3 py-1.5 text-xs rounded-lg gap-1.5',
|
|
37
|
+
md: 'px-5 py-2.5 text-sm rounded-xl gap-2',
|
|
38
|
+
lg: 'px-7 py-3.5 text-base rounded-2xl gap-2.5'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<motion.button
|
|
43
|
+
whileTap={disabled ? undefined : { scale: 0.96 }}
|
|
44
|
+
transition={{ type: 'spring', stiffness: 500, damping: 20 }}
|
|
45
|
+
className={`inline-flex items-center justify-center font-bold tracking-wide transition-colors duration-300 border focus:outline-hidden ${
|
|
46
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none' : 'cursor-pointer'
|
|
47
|
+
} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
|
|
52
|
+
<span>{children}</span>
|
|
53
|
+
{rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
|
|
54
|
+
</motion.button>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export type GlassCardVariant = 'default' | 'elevated' | 'outline' | 'ghost';
|
|
5
|
+
export type GlassCardPadding = 'none' | 'sm' | 'md' | 'lg';
|
|
6
|
+
|
|
7
|
+
export interface GlassCardProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
footer?: React.ReactNode;
|
|
12
|
+
headerAction?: React.ReactNode;
|
|
13
|
+
variant?: GlassCardVariant;
|
|
14
|
+
padding?: GlassCardPadding;
|
|
15
|
+
hoverable?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const variantStyles: Record<GlassCardVariant, string> = {
|
|
20
|
+
default: 'glass bg-bg-card/80 border-border-app shadow-sm',
|
|
21
|
+
elevated: 'glass bg-bg-card/90 border-border-app shadow-lg shadow-black/10',
|
|
22
|
+
outline: 'bg-transparent border-border-app',
|
|
23
|
+
ghost: 'bg-bg-app/40 border-transparent'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const paddingStyles: Record<GlassCardPadding, string> = {
|
|
27
|
+
none: '',
|
|
28
|
+
sm: 'p-4',
|
|
29
|
+
md: 'p-5',
|
|
30
|
+
lg: 'p-6'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const GlassCard: React.FC<GlassCardProps> = ({
|
|
34
|
+
children,
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
footer,
|
|
38
|
+
headerAction,
|
|
39
|
+
variant = 'default',
|
|
40
|
+
padding = 'md',
|
|
41
|
+
hoverable = false,
|
|
42
|
+
className = ''
|
|
43
|
+
}) => {
|
|
44
|
+
const hasHeader = Boolean(title || description || headerAction);
|
|
45
|
+
const bodyPadding = hasHeader || footer ? '' : paddingStyles[padding];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<motion.div
|
|
49
|
+
whileHover={hoverable ? { y: -2, scale: 1.01 } : undefined}
|
|
50
|
+
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
|
|
51
|
+
className={`rounded-2xl border flex flex-col overflow-hidden ${variantStyles[variant]} ${className}`}
|
|
52
|
+
>
|
|
53
|
+
{hasHeader && (
|
|
54
|
+
<div className={`flex items-start justify-between gap-3 border-b border-border-app/50 ${paddingStyles[padding]} pb-4`}>
|
|
55
|
+
<div className="flex flex-col gap-1 min-w-0">
|
|
56
|
+
{title && (
|
|
57
|
+
<h3 className="font-extrabold text-text-main font-display leading-tight truncate">
|
|
58
|
+
{title}
|
|
59
|
+
</h3>
|
|
60
|
+
)}
|
|
61
|
+
{description && (
|
|
62
|
+
<p className="text-xs text-text-muted leading-relaxed">
|
|
63
|
+
{description}
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
{headerAction && <div className="flex-shrink-0">{headerAction}</div>}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<div className={`flex-1 ${bodyPadding || (hasHeader || footer ? `${paddingStyles[padding]} pt-4` : '')}`}>
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{footer && (
|
|
76
|
+
<div className={`border-t border-border-app/50 bg-bg-app/20 ${paddingStyles[padding]} pt-4`}>
|
|
77
|
+
{footer}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</motion.div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface GlassInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
error?: string;
|
|
6
|
+
leftIcon?: React.ReactNode;
|
|
7
|
+
rightIcon?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GlassInput: React.FC<GlassInputProps> = ({
|
|
11
|
+
error,
|
|
12
|
+
leftIcon,
|
|
13
|
+
rightIcon,
|
|
14
|
+
id,
|
|
15
|
+
onFocus,
|
|
16
|
+
onBlur,
|
|
17
|
+
className = '',
|
|
18
|
+
disabled,
|
|
19
|
+
...props
|
|
20
|
+
}) => {
|
|
21
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
22
|
+
const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
|
|
23
|
+
|
|
24
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
25
|
+
if (disabled) return;
|
|
26
|
+
setIsFocused(true);
|
|
27
|
+
if (onFocus) onFocus(e);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
31
|
+
setIsFocused(false);
|
|
32
|
+
if (onBlur) onBlur(e);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`relative w-full flex flex-col gap-1.5 ${className}`}>
|
|
37
|
+
|
|
38
|
+
{/* Input Outer Wrapper */}
|
|
39
|
+
<div className="relative rounded-xl overflow-hidden transition-all duration-300">
|
|
40
|
+
|
|
41
|
+
{/* Focus Glow border ring */}
|
|
42
|
+
<motion.div
|
|
43
|
+
animate={{
|
|
44
|
+
opacity: isFocused && !disabled ? 1 : 0,
|
|
45
|
+
scale: isFocused && !disabled ? 1 : 0.96,
|
|
46
|
+
}}
|
|
47
|
+
transition={{ duration: 0.25 }}
|
|
48
|
+
className="absolute -inset-[1px] bg-gradient-to-r from-accent to-pink-500 rounded-xl pointer-events-none z-0 blur-[1px]"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
{/* Input container */}
|
|
52
|
+
<div className={`relative bg-bg-card/60 border rounded-xl z-10 flex items-center transition-colors duration-300 ${
|
|
53
|
+
isFocused && !disabled ? 'border-transparent' : 'border-border-app'
|
|
54
|
+
} ${disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : ''}`}>
|
|
55
|
+
|
|
56
|
+
{/* Left Icon */}
|
|
57
|
+
{leftIcon && (
|
|
58
|
+
<div className="pl-4 flex items-center justify-center text-text-muted/70">
|
|
59
|
+
{leftIcon}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Actual input element */}
|
|
64
|
+
<input
|
|
65
|
+
id={inputId}
|
|
66
|
+
onFocus={handleFocus}
|
|
67
|
+
onBlur={handleBlur}
|
|
68
|
+
disabled={disabled}
|
|
69
|
+
className={`w-full bg-transparent py-3 text-sm text-text-main placeholder-text-muted/50 focus:outline-hidden z-10 relative ${
|
|
70
|
+
leftIcon ? 'pl-2.5' : 'pl-4'
|
|
71
|
+
} ${rightIcon ? 'pr-2.5' : 'pr-4'} ${disabled ? 'cursor-not-allowed' : ''}`}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
{/* Right Icon */}
|
|
76
|
+
{rightIcon && (
|
|
77
|
+
<div className="pr-4 flex items-center justify-center text-text-muted/70">
|
|
78
|
+
{rightIcon}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Error validation label */}
|
|
85
|
+
{error && (
|
|
86
|
+
<motion.span
|
|
87
|
+
initial={{ opacity: 0, y: -4 }}
|
|
88
|
+
animate={{ opacity: 1, y: 0 }}
|
|
89
|
+
className="text-xs text-red-500 font-semibold px-2"
|
|
90
|
+
>
|
|
91
|
+
{error}
|
|
92
|
+
</motion.span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export interface GlassmorphicModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
title: string;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
footer?: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GlassmorphicModal: React.FC<GlassmorphicModalProps> = ({
|
|
16
|
+
isOpen,
|
|
17
|
+
onClose,
|
|
18
|
+
title,
|
|
19
|
+
children,
|
|
20
|
+
footer,
|
|
21
|
+
className = ''
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
// Close on Escape key press
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') onClose();
|
|
28
|
+
};
|
|
29
|
+
if (isOpen) {
|
|
30
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
31
|
+
document.body.style.overflow = 'hidden'; // Lock body scroll
|
|
32
|
+
}
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
35
|
+
document.body.style.overflow = ''; // Unlock scroll
|
|
36
|
+
};
|
|
37
|
+
}, [isOpen, onClose]);
|
|
38
|
+
|
|
39
|
+
if (typeof document === 'undefined') return null;
|
|
40
|
+
|
|
41
|
+
return createPortal(
|
|
42
|
+
<AnimatePresence>
|
|
43
|
+
{isOpen && (
|
|
44
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
45
|
+
|
|
46
|
+
{/* Backdrop blur layer */}
|
|
47
|
+
<motion.div
|
|
48
|
+
initial={{ opacity: 0 }}
|
|
49
|
+
animate={{ opacity: 1 }}
|
|
50
|
+
exit={{ opacity: 0 }}
|
|
51
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-md"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
{/* Modal Container */}
|
|
55
|
+
<motion.div
|
|
56
|
+
initial={{ scale: 0.95, opacity: 0, y: 15 }}
|
|
57
|
+
animate={{
|
|
58
|
+
scale: 1,
|
|
59
|
+
opacity: 1,
|
|
60
|
+
y: 0,
|
|
61
|
+
transition: {
|
|
62
|
+
type: 'spring',
|
|
63
|
+
stiffness: 350,
|
|
64
|
+
damping: 25
|
|
65
|
+
}
|
|
66
|
+
}}
|
|
67
|
+
exit={{
|
|
68
|
+
scale: 0.95,
|
|
69
|
+
opacity: 0,
|
|
70
|
+
y: 15,
|
|
71
|
+
transition: {
|
|
72
|
+
duration: 0.2
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
className={`relative w-full max-w-lg rounded-2xl glass bg-bg-card/90 shadow-2xl border border-border-app z-10 overflow-hidden flex flex-col ${className}`}
|
|
76
|
+
>
|
|
77
|
+
{/* Header */}
|
|
78
|
+
<div className="flex items-center justify-between p-5 border-b border-border-app/50">
|
|
79
|
+
<h3 className="font-extrabold text-lg text-text-main">
|
|
80
|
+
{title}
|
|
81
|
+
</h3>
|
|
82
|
+
<button
|
|
83
|
+
onClick={onClose}
|
|
84
|
+
className="p-1 rounded-lg hover:bg-bg-app text-text-muted hover:text-text-main transition-colors cursor-pointer"
|
|
85
|
+
aria-label="Cerrar modal"
|
|
86
|
+
>
|
|
87
|
+
<X className="w-5 h-5" />
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Content */}
|
|
92
|
+
<div className="p-6 overflow-y-auto max-h-[60vh] text-sm text-text-muted leading-relaxed">
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Footer */}
|
|
97
|
+
{footer && (
|
|
98
|
+
<div className="p-5 border-t border-border-app/50 bg-bg-app/20 flex justify-end gap-3">
|
|
99
|
+
{footer}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</motion.div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</AnimatePresence>,
|
|
106
|
+
document.body
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface GlowInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
5
|
+
label: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const GlowInput: React.FC<GlowInputProps> = ({
|
|
10
|
+
label,
|
|
11
|
+
error,
|
|
12
|
+
id,
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
onFocus,
|
|
16
|
+
onBlur,
|
|
17
|
+
className = '',
|
|
18
|
+
disabled,
|
|
19
|
+
...props
|
|
20
|
+
}) => {
|
|
21
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
22
|
+
const [hasText, setHasText] = useState(false);
|
|
23
|
+
|
|
24
|
+
const inputId = id || (label ? `input-${label.toLowerCase().replace(/\s+/g, '-')}` : 'glow-input');
|
|
25
|
+
|
|
26
|
+
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
27
|
+
if (disabled) return;
|
|
28
|
+
setIsFocused(true);
|
|
29
|
+
if (onFocus) onFocus(e);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
33
|
+
setIsFocused(false);
|
|
34
|
+
setHasText(e.target.value.length > 0);
|
|
35
|
+
if (onBlur) onBlur(e);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
39
|
+
if (disabled) return;
|
|
40
|
+
setHasText(e.target.value.length > 0);
|
|
41
|
+
if (onChange) onChange(e);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isActive = isFocused || hasText || (value && String(value).length > 0) || !!props.placeholder;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={`relative w-full flex flex-col gap-1.5 ${className}`}>
|
|
48
|
+
|
|
49
|
+
{/* Input wrapper with border glow container */}
|
|
50
|
+
<div className="relative rounded-xl overflow-hidden transition-all duration-300">
|
|
51
|
+
|
|
52
|
+
{/* Animated Glow Border background */}
|
|
53
|
+
<motion.div
|
|
54
|
+
animate={{
|
|
55
|
+
opacity: isFocused && !disabled ? 1 : 0,
|
|
56
|
+
scale: isFocused && !disabled ? 1 : 0.95,
|
|
57
|
+
}}
|
|
58
|
+
transition={{ duration: 0.3 }}
|
|
59
|
+
className="absolute -inset-[1px] bg-gradient-to-r from-accent to-pink-500 rounded-xl pointer-events-none z-0 blur-[2px]"
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
{/* Input content holder */}
|
|
63
|
+
<div className={`relative bg-bg-card border border-border-app rounded-xl z-10 transition-colors duration-300 ${
|
|
64
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : ''
|
|
65
|
+
}`}>
|
|
66
|
+
|
|
67
|
+
{/* Floating Label */}
|
|
68
|
+
<motion.label
|
|
69
|
+
htmlFor={inputId}
|
|
70
|
+
initial={{ y: 14, scale: 1 }}
|
|
71
|
+
animate={{
|
|
72
|
+
y: isActive ? 4 : 14,
|
|
73
|
+
scale: isActive ? 0.75 : 1,
|
|
74
|
+
color: isFocused && !disabled
|
|
75
|
+
? 'var(--color-accent)'
|
|
76
|
+
: 'var(--color-text-muted)'
|
|
77
|
+
}}
|
|
78
|
+
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
|
|
79
|
+
className="absolute left-4 origin-top-left pointer-events-none select-none text-sm z-20 font-medium"
|
|
80
|
+
>
|
|
81
|
+
{label}
|
|
82
|
+
</motion.label>
|
|
83
|
+
|
|
84
|
+
<input
|
|
85
|
+
id={inputId}
|
|
86
|
+
value={value}
|
|
87
|
+
onChange={handleChange}
|
|
88
|
+
onFocus={handleFocus}
|
|
89
|
+
onBlur={handleBlur}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
className={`w-full bg-transparent px-4 pb-2 pt-7 text-sm text-text-main focus:outline-hidden z-10 relative ${
|
|
92
|
+
disabled ? 'cursor-not-allowed' : ''
|
|
93
|
+
}`}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Error message */}
|
|
100
|
+
{error && (
|
|
101
|
+
<motion.span
|
|
102
|
+
initial={{ opacity: 0, y: -5 }}
|
|
103
|
+
animate={{ opacity: 1, y: 0 }}
|
|
104
|
+
className="text-xs text-red-500 font-medium px-2"
|
|
105
|
+
>
|
|
106
|
+
{error}
|
|
107
|
+
</motion.span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|