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