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,72 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface CollapsibleProps {
|
|
6
|
+
trigger: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Collapsible: React.FC<CollapsibleProps> = ({
|
|
16
|
+
trigger,
|
|
17
|
+
children,
|
|
18
|
+
defaultOpen = false,
|
|
19
|
+
open: controlledOpen,
|
|
20
|
+
onOpenChange,
|
|
21
|
+
disabled = false,
|
|
22
|
+
className = ''
|
|
23
|
+
}) => {
|
|
24
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
25
|
+
const isControlled = controlledOpen !== undefined;
|
|
26
|
+
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
27
|
+
|
|
28
|
+
const setOpen = (next: boolean) => {
|
|
29
|
+
if (disabled) return;
|
|
30
|
+
if (!isControlled) setInternalOpen(next);
|
|
31
|
+
onOpenChange?.(next);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={`w-full ${className}`}>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => setOpen(!isOpen)}
|
|
39
|
+
disabled={disabled}
|
|
40
|
+
aria-expanded={isOpen}
|
|
41
|
+
className={`w-full flex items-center justify-between gap-3 text-left transition-colors rounded-xl ${
|
|
42
|
+
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-bg-app/40'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
<span className="flex-1 min-w-0">{trigger}</span>
|
|
46
|
+
<motion.span
|
|
47
|
+
animate={{ rotate: isOpen ? 180 : 0 }}
|
|
48
|
+
transition={{ type: 'spring', stiffness: 350, damping: 24 }}
|
|
49
|
+
className="text-text-muted flex-shrink-0"
|
|
50
|
+
>
|
|
51
|
+
<ChevronDown className="w-4 h-4" />
|
|
52
|
+
</motion.span>
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
<AnimatePresence initial={false}>
|
|
56
|
+
{isOpen && (
|
|
57
|
+
<motion.div
|
|
58
|
+
initial={{ height: 0, opacity: 0 }}
|
|
59
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
60
|
+
exit={{ height: 0, opacity: 0 }}
|
|
61
|
+
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
|
|
62
|
+
className="overflow-hidden"
|
|
63
|
+
>
|
|
64
|
+
<div className="pt-3 text-sm text-text-muted leading-relaxed">
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</motion.div>
|
|
68
|
+
)}
|
|
69
|
+
</AnimatePresence>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import { X, Sparkles, Download, RotateCcw, Check, Palette } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export interface ColorPalette {
|
|
7
|
+
accent: string;
|
|
8
|
+
success: string;
|
|
9
|
+
warning: string;
|
|
10
|
+
error: string;
|
|
11
|
+
info: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ColorTheme {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
light: ColorPalette;
|
|
18
|
+
dark: ColorPalette;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 10 Curated Premium Themes
|
|
22
|
+
export const PRESETS_THEMES: ColorTheme[] = [
|
|
23
|
+
{
|
|
24
|
+
id: 'default',
|
|
25
|
+
name: 'Default Indigo',
|
|
26
|
+
light: { accent: '#6366f1', success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' },
|
|
27
|
+
dark: { accent: '#818cf8', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#60a5fa' }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'emerald',
|
|
31
|
+
name: 'Emerald Forest',
|
|
32
|
+
light: { accent: '#059669', success: '#10b981', warning: '#d97706', error: '#dc2626', info: '#2563eb' },
|
|
33
|
+
dark: { accent: '#34d399', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#60a5fa' }
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'cyberpunk',
|
|
37
|
+
name: 'Cyberpunk Pink',
|
|
38
|
+
light: { accent: '#db2777', success: '#10b981', warning: '#ea580c', error: '#dc2626', info: '#8b5cf6' },
|
|
39
|
+
dark: { accent: '#f472b6', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#c084fc' }
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'ocean',
|
|
43
|
+
name: 'Ocean Blue',
|
|
44
|
+
light: { accent: '#2563eb', success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#0ea5e9' },
|
|
45
|
+
dark: { accent: '#60a5fa', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#38bdf8' }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'sunset',
|
|
49
|
+
name: 'Sunset Amber',
|
|
50
|
+
light: { accent: '#ea580c', success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' },
|
|
51
|
+
dark: { accent: '#fb923c', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#60a5fa' }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'violet',
|
|
55
|
+
name: 'Midnight Violet',
|
|
56
|
+
light: { accent: '#7c3aed', success: '#059669', warning: '#ea580c', error: '#dc2626', info: '#2563eb' },
|
|
57
|
+
dark: { accent: '#a78bfa', success: '#34d399', warning: '#fb923c', error: '#f87171', info: '#60a5fa' }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'teal',
|
|
61
|
+
name: 'Teal Breeze',
|
|
62
|
+
light: { accent: '#0d9488', success: '#10b981', warning: '#d97706', error: '#e11d48', info: '#0284c7' },
|
|
63
|
+
dark: { accent: '#2dd4bf', success: '#34d399', warning: '#fbbf24', error: '#fda4af', info: '#38bdf8' }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'crimson',
|
|
67
|
+
name: 'Crimson Blood',
|
|
68
|
+
light: { accent: '#dc2626', success: '#10b981', warning: '#d97706', error: '#b91c1c', info: '#2563eb' },
|
|
69
|
+
dark: { accent: '#f87171', success: '#34d399', warning: '#fbbf24', error: '#ef4444', info: '#60a5fa' }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'neon',
|
|
73
|
+
name: 'Glow Neon',
|
|
74
|
+
light: { accent: '#65a30d', success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' },
|
|
75
|
+
dark: { accent: '#a3e635', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#60a5fa' }
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'slate',
|
|
79
|
+
name: 'Monochrome Slate',
|
|
80
|
+
light: { accent: '#475569', success: '#10b981', warning: '#f59e0b', error: '#ef4444', info: '#3b82f6' },
|
|
81
|
+
dark: { accent: '#94a3b8', success: '#34d399', warning: '#fbbf24', error: '#f87171', info: '#60a5fa' }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Helper functions for dynamic theme calculations
|
|
86
|
+
const darkenHex = (hex: string, percent: number) => {
|
|
87
|
+
const num = parseInt(hex.replace("#", ""), 16);
|
|
88
|
+
const amt = Math.round(2.55 * percent);
|
|
89
|
+
const R = (num >> 16) - amt;
|
|
90
|
+
const G = (num >> 8 & 0x00FF) - amt;
|
|
91
|
+
const B = (num & 0x0000FF) - amt;
|
|
92
|
+
return "#" + (0x1000000 + (R < 255 ? R < 0 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 0 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 0 ? 0 : B : 255)).toString(16).slice(1);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const hexToRgba = (hex: string, alpha: number) => {
|
|
96
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
97
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
98
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
99
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export interface ColorThemeManagerProps {
|
|
103
|
+
isOpen: boolean;
|
|
104
|
+
onClose: () => void;
|
|
105
|
+
selectedThemeId: string;
|
|
106
|
+
setSelectedThemeId: (id: string) => void;
|
|
107
|
+
onThemeChange?: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const ColorThemeManager: React.FC<ColorThemeManagerProps> = ({
|
|
111
|
+
isOpen,
|
|
112
|
+
onClose,
|
|
113
|
+
selectedThemeId,
|
|
114
|
+
setSelectedThemeId,
|
|
115
|
+
onThemeChange,
|
|
116
|
+
}) => {
|
|
117
|
+
const [customColors, setCustomColors] = useState<ColorPalette>({
|
|
118
|
+
accent: '#6366f1',
|
|
119
|
+
success: '#10b981',
|
|
120
|
+
warning: '#f59e0b',
|
|
121
|
+
error: '#ef4444',
|
|
122
|
+
info: '#3b82f6'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Track if current browser dark mode is active
|
|
126
|
+
const [isDark, setIsDark] = useState(false);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
// Initial load
|
|
130
|
+
const storedTheme = localStorage.getItem('alexui-theme-id') || 'default';
|
|
131
|
+
setSelectedThemeId(storedTheme);
|
|
132
|
+
|
|
133
|
+
const storedCustom = localStorage.getItem('alexui-custom-colors');
|
|
134
|
+
if (storedCustom) {
|
|
135
|
+
try {
|
|
136
|
+
setCustomColors(JSON.parse(storedCustom));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(e);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const checkDarkMode = () => {
|
|
143
|
+
setIsDark(document.documentElement.classList.contains('dark'));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
checkDarkMode();
|
|
147
|
+
|
|
148
|
+
// Listen for theme changes from switcher
|
|
149
|
+
const observer = new MutationObserver(checkDarkMode);
|
|
150
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
151
|
+
|
|
152
|
+
return () => observer.disconnect();
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const applyColors = useCallback((palette: ColorPalette) => {
|
|
156
|
+
const root = document.documentElement;
|
|
157
|
+
root.style.setProperty('--color-accent', palette.accent);
|
|
158
|
+
root.style.setProperty('--color-accent-hover', darkenHex(palette.accent, 10));
|
|
159
|
+
root.style.setProperty('--color-accent-glow', hexToRgba(palette.accent, 0.18));
|
|
160
|
+
root.style.setProperty('--color-success', palette.success);
|
|
161
|
+
root.style.setProperty('--color-error', palette.error);
|
|
162
|
+
root.style.setProperty('--color-warning', palette.warning);
|
|
163
|
+
root.style.setProperty('--color-info', palette.info);
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const onThemeChangeRef = useRef(onThemeChange);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
onThemeChangeRef.current = onThemeChange;
|
|
169
|
+
}, [onThemeChange]);
|
|
170
|
+
|
|
171
|
+
// Update styles when theme, mode, or custom color changes
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (selectedThemeId === 'custom') {
|
|
174
|
+
applyColors(customColors);
|
|
175
|
+
localStorage.setItem('alexui-theme-id', 'custom');
|
|
176
|
+
localStorage.setItem('alexui-custom-colors', JSON.stringify(customColors));
|
|
177
|
+
} else {
|
|
178
|
+
const theme = PRESETS_THEMES.find(t => t.id === selectedThemeId) || PRESETS_THEMES[0];
|
|
179
|
+
const palette = isDark ? theme.dark : theme.light;
|
|
180
|
+
applyColors(palette);
|
|
181
|
+
localStorage.setItem('alexui-theme-id', selectedThemeId);
|
|
182
|
+
}
|
|
183
|
+
onThemeChangeRef.current?.();
|
|
184
|
+
}, [selectedThemeId, customColors, isDark, applyColors]);
|
|
185
|
+
|
|
186
|
+
const handleCustomColorChange = (key: keyof ColorPalette, value: string) => {
|
|
187
|
+
setCustomColors(prev => ({ ...prev, [key]: value }));
|
|
188
|
+
setSelectedThemeId('custom');
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleResetCustom = () => {
|
|
192
|
+
const defaultCustom = {
|
|
193
|
+
accent: '#6366f1',
|
|
194
|
+
success: '#10b981',
|
|
195
|
+
warning: '#f59e0b',
|
|
196
|
+
error: '#ef4444',
|
|
197
|
+
info: '#3b82f6'
|
|
198
|
+
};
|
|
199
|
+
setCustomColors(defaultCustom);
|
|
200
|
+
setSelectedThemeId('default');
|
|
201
|
+
localStorage.removeItem('alexui-custom-colors');
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleDownloadCSS = () => {
|
|
205
|
+
const activePalette = selectedThemeId === 'custom'
|
|
206
|
+
? customColors
|
|
207
|
+
: (isDark
|
|
208
|
+
? (PRESETS_THEMES.find(t => t.id === selectedThemeId)?.dark || PRESETS_THEMES[0].dark)
|
|
209
|
+
: (PRESETS_THEMES.find(t => t.id === selectedThemeId)?.light || PRESETS_THEMES[0].light)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const cssContent = `:root {
|
|
213
|
+
--color-accent: ${activePalette.accent};
|
|
214
|
+
--color-accent-hover: ${darkenHex(activePalette.accent, 10)};
|
|
215
|
+
--color-accent-glow: ${hexToRgba(activePalette.accent, 0.18)};
|
|
216
|
+
--color-success: ${activePalette.success};
|
|
217
|
+
--color-error: ${activePalette.error};
|
|
218
|
+
--color-warning: ${activePalette.warning};
|
|
219
|
+
--color-info: ${activePalette.info};
|
|
220
|
+
}`;
|
|
221
|
+
|
|
222
|
+
const blob = new Blob([cssContent], { type: 'text/css' });
|
|
223
|
+
const url = URL.createObjectURL(blob);
|
|
224
|
+
const link = document.createElement('a');
|
|
225
|
+
link.href = url;
|
|
226
|
+
link.download = `alexui-theme-${selectedThemeId}.css`;
|
|
227
|
+
document.body.appendChild(link);
|
|
228
|
+
link.click();
|
|
229
|
+
document.body.removeChild(link);
|
|
230
|
+
URL.revokeObjectURL(url);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Close on Escape key press
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
236
|
+
if (e.key === 'Escape') onClose();
|
|
237
|
+
};
|
|
238
|
+
if (isOpen) {
|
|
239
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
240
|
+
document.body.style.overflow = 'hidden';
|
|
241
|
+
}
|
|
242
|
+
return () => {
|
|
243
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
244
|
+
document.body.style.overflow = '';
|
|
245
|
+
};
|
|
246
|
+
}, [isOpen, onClose]);
|
|
247
|
+
|
|
248
|
+
if (typeof document === 'undefined') return null;
|
|
249
|
+
|
|
250
|
+
return createPortal(
|
|
251
|
+
<AnimatePresence>
|
|
252
|
+
{isOpen && (
|
|
253
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
254
|
+
|
|
255
|
+
{/* Backdrop blur layer */}
|
|
256
|
+
<motion.div
|
|
257
|
+
initial={{ opacity: 0 }}
|
|
258
|
+
animate={{ opacity: 1 }}
|
|
259
|
+
exit={{ opacity: 0 }}
|
|
260
|
+
onClick={onClose}
|
|
261
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-md cursor-pointer"
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
{/* Modal Container */}
|
|
265
|
+
<motion.div
|
|
266
|
+
initial={{ scale: 0.95, opacity: 0, y: 15 }}
|
|
267
|
+
animate={{
|
|
268
|
+
scale: 1,
|
|
269
|
+
opacity: 1,
|
|
270
|
+
y: 0,
|
|
271
|
+
transition: { type: 'spring', stiffness: 350, damping: 25 }
|
|
272
|
+
}}
|
|
273
|
+
exit={{
|
|
274
|
+
scale: 0.95,
|
|
275
|
+
opacity: 0,
|
|
276
|
+
y: 15,
|
|
277
|
+
transition: { duration: 0.2 }
|
|
278
|
+
}}
|
|
279
|
+
className="relative w-full max-w-2xl rounded-2xl glass bg-bg-card/90 shadow-2xl border border-border-app z-10 overflow-hidden flex flex-col max-h-[90vh]"
|
|
280
|
+
>
|
|
281
|
+
{/* Header */}
|
|
282
|
+
<div className="flex items-center justify-between p-5 border-b border-border-app/50">
|
|
283
|
+
<div className="flex items-center gap-2 text-accent">
|
|
284
|
+
<Palette className="w-5 h-5" />
|
|
285
|
+
<h3 className="font-extrabold text-lg text-text-main font-display">
|
|
286
|
+
Personalizar Temas de Color
|
|
287
|
+
</h3>
|
|
288
|
+
</div>
|
|
289
|
+
<button
|
|
290
|
+
onClick={onClose}
|
|
291
|
+
className="p-1 rounded-lg hover:bg-bg-app text-text-muted hover:text-text-main transition-colors cursor-pointer"
|
|
292
|
+
aria-label="Cerrar personalizador"
|
|
293
|
+
>
|
|
294
|
+
<X className="w-5 h-5" />
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* Scrollable Content */}
|
|
299
|
+
<div className="p-6 overflow-y-auto flex flex-col gap-6">
|
|
300
|
+
|
|
301
|
+
{/* Presets Grid */}
|
|
302
|
+
<div>
|
|
303
|
+
<h4 className="text-xs font-bold uppercase tracking-wider text-text-muted mb-3 font-display">
|
|
304
|
+
Temas Predefinidos (10 Paletas Premium)
|
|
305
|
+
</h4>
|
|
306
|
+
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
|
307
|
+
{PRESETS_THEMES.map((theme) => {
|
|
308
|
+
const palette = isDark ? theme.dark : theme.light;
|
|
309
|
+
const isSelected = selectedThemeId === theme.id;
|
|
310
|
+
return (
|
|
311
|
+
<button
|
|
312
|
+
key={theme.id}
|
|
313
|
+
onClick={() => setSelectedThemeId(theme.id)}
|
|
314
|
+
className={`p-3 rounded-xl bg-bg-app border text-left flex flex-col gap-2 items-center justify-center transition-all duration-300 cursor-pointer ${
|
|
315
|
+
isSelected
|
|
316
|
+
? 'border-accent ring-2 ring-accent/20 bg-accent/5'
|
|
317
|
+
: 'border-border-app hover:border-border-app/80 hover:bg-bg-card'
|
|
318
|
+
}`}
|
|
319
|
+
>
|
|
320
|
+
<span className="text-[10px] font-bold text-text-main truncate w-full text-center">
|
|
321
|
+
{theme.name}
|
|
322
|
+
</span>
|
|
323
|
+
{/* Color preview circles */}
|
|
324
|
+
<div className="flex gap-0.5 items-center justify-center">
|
|
325
|
+
<div className="w-3.5 h-3.5 rounded-full border border-black/10" style={{ backgroundColor: palette.accent }} title="Acento" />
|
|
326
|
+
<div className="w-2.5 h-2.5 rounded-full border border-black/10" style={{ backgroundColor: palette.success }} title="Éxito" />
|
|
327
|
+
<div className="w-2.5 h-2.5 rounded-full border border-black/10" style={{ backgroundColor: palette.error }} title="Error" />
|
|
328
|
+
</div>
|
|
329
|
+
</button>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{/* Custom Theme Editor */}
|
|
336
|
+
<div className="border-t border-border-app/50 pt-5">
|
|
337
|
+
<div className="flex items-center justify-between mb-4">
|
|
338
|
+
<div className="flex items-center gap-2">
|
|
339
|
+
<Sparkles className="w-4 h-4 text-accent animate-pulse" />
|
|
340
|
+
<h4 className="text-xs font-bold uppercase tracking-wider text-text-muted font-display">
|
|
341
|
+
Creador de Tema Customizado (En Tiempo Real)
|
|
342
|
+
</h4>
|
|
343
|
+
</div>
|
|
344
|
+
{selectedThemeId === 'custom' && (
|
|
345
|
+
<button
|
|
346
|
+
onClick={handleResetCustom}
|
|
347
|
+
className="flex items-center gap-1 text-[10px] font-bold text-red-500 hover:text-red-600 transition-colors cursor-pointer"
|
|
348
|
+
>
|
|
349
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
350
|
+
<span>Reiniciar</span>
|
|
351
|
+
</button>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div className="bg-bg-app/40 rounded-2xl p-4 border border-border-app/40 grid grid-cols-1 sm:grid-cols-5 gap-4">
|
|
356
|
+
{/* Accent Color */}
|
|
357
|
+
<div className="flex flex-col items-center gap-2">
|
|
358
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">Acento</span>
|
|
359
|
+
<label className="relative w-12 h-12 rounded-full border-2 border-border-app cursor-pointer overflow-hidden shadow-sm hover:scale-105 transition-transform flex items-center justify-center" style={{ backgroundColor: customColors.accent }}>
|
|
360
|
+
<input
|
|
361
|
+
type="color"
|
|
362
|
+
value={customColors.accent}
|
|
363
|
+
onChange={(e) => handleCustomColorChange('accent', e.target.value)}
|
|
364
|
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
|
365
|
+
/>
|
|
366
|
+
{selectedThemeId === 'custom' && <Check className="w-4 h-4 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />}
|
|
367
|
+
</label>
|
|
368
|
+
<span className="text-[10px] font-mono font-medium text-text-main uppercase">{customColors.accent}</span>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
{/* Success Color */}
|
|
372
|
+
<div className="flex flex-col items-center gap-2">
|
|
373
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">Éxito</span>
|
|
374
|
+
<label className="relative w-12 h-12 rounded-full border-2 border-border-app cursor-pointer overflow-hidden shadow-sm hover:scale-105 transition-transform flex items-center justify-center" style={{ backgroundColor: customColors.success }}>
|
|
375
|
+
<input
|
|
376
|
+
type="color"
|
|
377
|
+
value={customColors.success}
|
|
378
|
+
onChange={(e) => handleCustomColorChange('success', e.target.value)}
|
|
379
|
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
|
380
|
+
/>
|
|
381
|
+
{selectedThemeId === 'custom' && <Check className="w-4 h-4 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />}
|
|
382
|
+
</label>
|
|
383
|
+
<span className="text-[10px] font-mono font-medium text-text-main uppercase">{customColors.success}</span>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{/* Warning Color */}
|
|
387
|
+
<div className="flex flex-col items-center gap-2">
|
|
388
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">Advertencia</span>
|
|
389
|
+
<label className="relative w-12 h-12 rounded-full border-2 border-border-app cursor-pointer overflow-hidden shadow-sm hover:scale-105 transition-transform flex items-center justify-center" style={{ backgroundColor: customColors.warning }}>
|
|
390
|
+
<input
|
|
391
|
+
type="color"
|
|
392
|
+
value={customColors.warning}
|
|
393
|
+
onChange={(e) => handleCustomColorChange('warning', e.target.value)}
|
|
394
|
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
|
395
|
+
/>
|
|
396
|
+
{selectedThemeId === 'custom' && <Check className="w-4 h-4 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />}
|
|
397
|
+
</label>
|
|
398
|
+
<span className="text-[10px] font-mono font-medium text-text-main uppercase">{customColors.warning}</span>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
{/* Error Color */}
|
|
402
|
+
<div className="flex flex-col items-center gap-2">
|
|
403
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">Error</span>
|
|
404
|
+
<label className="relative w-12 h-12 rounded-full border-2 border-border-app cursor-pointer overflow-hidden shadow-sm hover:scale-105 transition-transform flex items-center justify-center" style={{ backgroundColor: customColors.error }}>
|
|
405
|
+
<input
|
|
406
|
+
type="color"
|
|
407
|
+
value={customColors.error}
|
|
408
|
+
onChange={(e) => handleCustomColorChange('error', e.target.value)}
|
|
409
|
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
|
410
|
+
/>
|
|
411
|
+
{selectedThemeId === 'custom' && <Check className="w-4 h-4 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />}
|
|
412
|
+
</label>
|
|
413
|
+
<span className="text-[10px] font-mono font-medium text-text-main uppercase">{customColors.error}</span>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* Info Color */}
|
|
417
|
+
<div className="flex flex-col items-center gap-2">
|
|
418
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">Información</span>
|
|
419
|
+
<label className="relative w-12 h-12 rounded-full border-2 border-border-app cursor-pointer overflow-hidden shadow-sm hover:scale-105 transition-transform flex items-center justify-center" style={{ backgroundColor: customColors.info }}>
|
|
420
|
+
<input
|
|
421
|
+
type="color"
|
|
422
|
+
value={customColors.info}
|
|
423
|
+
onChange={(e) => handleCustomColorChange('info', e.target.value)}
|
|
424
|
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
|
425
|
+
/>
|
|
426
|
+
{selectedThemeId === 'custom' && <Check className="w-4 h-4 text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]" />}
|
|
427
|
+
</label>
|
|
428
|
+
<span className="text-[10px] font-mono font-medium text-text-main uppercase">{customColors.info}</span>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{/* Footer */}
|
|
436
|
+
<div className="p-5 border-t border-border-app/50 bg-bg-app/20 flex justify-between gap-3">
|
|
437
|
+
<button
|
|
438
|
+
onClick={handleDownloadCSS}
|
|
439
|
+
className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-app bg-bg-card hover:border-accent text-xs font-semibold hover:text-accent transition-all cursor-pointer"
|
|
440
|
+
>
|
|
441
|
+
<Download className="w-4 h-4" />
|
|
442
|
+
<span>Descargar CSS del Tema</span>
|
|
443
|
+
</button>
|
|
444
|
+
|
|
445
|
+
<button
|
|
446
|
+
onClick={onClose}
|
|
447
|
+
className="px-5 py-2.5 rounded-xl bg-accent hover:bg-accent-hover text-white text-xs font-bold transition-all cursor-pointer shadow-md"
|
|
448
|
+
>
|
|
449
|
+
Listo, aplicar
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
</motion.div>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</AnimatePresence>,
|
|
456
|
+
document.body
|
|
457
|
+
);
|
|
458
|
+
};
|