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,191 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Search, Command } from 'lucide-react';
4
+
5
+ export interface CommandItem {
6
+ id: string;
7
+ label: string;
8
+ icon?: React.ReactNode;
9
+ group?: string;
10
+ }
11
+
12
+ export interface CommandMenuProps {
13
+ isOpen: boolean;
14
+ onClose: () => void;
15
+ commands: CommandItem[];
16
+ onSelect: (id: string) => void;
17
+ placeholder?: string;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ export const CommandMenu: React.FC<CommandMenuProps> = ({
22
+ isOpen,
23
+ onClose,
24
+ commands,
25
+ onSelect,
26
+ placeholder = "Escribe un comando o busca...",
27
+ disabled = false
28
+ }) => {
29
+ const [query, setQuery] = useState('');
30
+ const [activeIndex, setActiveIndex] = useState(0);
31
+ const inputRef = useRef<HTMLInputElement>(null);
32
+
33
+ // Filter commands based on query
34
+ const filteredCommands = commands.filter(cmd =>
35
+ cmd.label.toLowerCase().includes(query.toLowerCase()) ||
36
+ (cmd.group && cmd.group.toLowerCase().includes(query.toLowerCase()))
37
+ );
38
+
39
+ // Groups for rendering
40
+ const groups = Array.from(new Set(filteredCommands.map(c => c.group || 'General')));
41
+
42
+ // Reset state when opened
43
+ useEffect(() => {
44
+ if (isOpen) {
45
+ setQuery('');
46
+ setActiveIndex(0);
47
+ // Small delay to allow animation to start before focusing
48
+ setTimeout(() => inputRef.current?.focus(), 100);
49
+ }
50
+ }, [isOpen]);
51
+
52
+ // Handle Keyboard Navigation
53
+ useEffect(() => {
54
+ if (!isOpen || disabled) return;
55
+
56
+ const handleKeyDown = (e: KeyboardEvent) => {
57
+ if (e.key === 'ArrowDown') {
58
+ e.preventDefault();
59
+ setActiveIndex(prev => (prev < filteredCommands.length - 1 ? prev + 1 : prev));
60
+ } else if (e.key === 'ArrowUp') {
61
+ e.preventDefault();
62
+ setActiveIndex(prev => (prev > 0 ? prev - 1 : prev));
63
+ } else if (e.key === 'Enter') {
64
+ e.preventDefault();
65
+ if (filteredCommands[activeIndex]) {
66
+ onSelect(filteredCommands[activeIndex].id);
67
+ onClose();
68
+ }
69
+ } else if (e.key === 'Escape') {
70
+ e.preventDefault();
71
+ onClose();
72
+ }
73
+ };
74
+
75
+ window.addEventListener('keydown', handleKeyDown);
76
+ return () => window.removeEventListener('keydown', handleKeyDown);
77
+ }, [isOpen, filteredCommands, activeIndex, onSelect, onClose, disabled]);
78
+
79
+ // Global Ctrl+K / Cmd+K listener to open (handled by parent usually, but we can prevent default here just in case)
80
+ // Actually, parent should handle it.
81
+
82
+ if (disabled) return null;
83
+
84
+ return (
85
+ <AnimatePresence>
86
+ {isOpen && (
87
+ <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]">
88
+ {/* Backdrop */}
89
+ <motion.div
90
+ initial={{ opacity: 0 }}
91
+ animate={{ opacity: 1 }}
92
+ exit={{ opacity: 0 }}
93
+ className="absolute inset-0 bg-black/40 backdrop-blur-sm"
94
+ onClick={onClose}
95
+ />
96
+
97
+ {/* Modal */}
98
+ <motion.div
99
+ initial={{ opacity: 0, scale: 0.95, y: -20 }}
100
+ animate={{ opacity: 1, scale: 1, y: 0 }}
101
+ exit={{ opacity: 0, scale: 0.95, y: -20 }}
102
+ transition={{ duration: 0.2, ease: "easeOut" }}
103
+ className="relative w-full max-w-xl bg-bg-card/90 backdrop-blur-xl border border-border-app rounded-2xl shadow-2xl overflow-hidden flex flex-col"
104
+ >
105
+ {/* Header / Input */}
106
+ <div className="flex items-center px-4 py-3 border-b border-border-app gap-3">
107
+ <Search className="w-5 h-5 text-text-muted" />
108
+ <input
109
+ ref={inputRef}
110
+ type="text"
111
+ value={query}
112
+ onChange={(e) => {
113
+ setQuery(e.target.value);
114
+ setActiveIndex(0);
115
+ }}
116
+ placeholder={placeholder}
117
+ className="flex-1 bg-transparent border-none outline-none text-text-main placeholder:text-text-muted/50 font-medium text-lg"
118
+ />
119
+ <div className="flex items-center gap-1 text-[10px] text-text-muted font-bold bg-bg-app px-2 py-1 rounded-md border border-border-app">
120
+ <Command className="w-3 h-3" />
121
+ <span>K</span>
122
+ </div>
123
+ </div>
124
+
125
+ {/* List */}
126
+ <div className="max-h-[60vh] overflow-y-auto p-2 scrollbar-thin">
127
+ {filteredCommands.length === 0 ? (
128
+ <div className="py-8 text-center text-text-muted text-sm">
129
+ No se encontraron resultados para "{query}"
130
+ </div>
131
+ ) : (
132
+ groups.map(group => {
133
+ const groupCommands = filteredCommands.filter(c => (c.group || 'General') === group);
134
+ if (groupCommands.length === 0) return null;
135
+
136
+ return (
137
+ <div key={group} className="mb-2 last:mb-0">
138
+ <div className="px-3 py-2 text-xs font-bold text-text-muted uppercase tracking-wider">
139
+ {group}
140
+ </div>
141
+ <div className="flex flex-col gap-1">
142
+ {groupCommands.map((cmd) => {
143
+ const globalIndex = filteredCommands.findIndex(c => c.id === cmd.id);
144
+ const isActive = globalIndex === activeIndex;
145
+
146
+ return (
147
+ <div
148
+ key={cmd.id}
149
+ onMouseEnter={() => setActiveIndex(globalIndex)}
150
+ onClick={() => {
151
+ onSelect(cmd.id);
152
+ onClose();
153
+ }}
154
+ className={`flex items-center gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-colors duration-150 ${
155
+ isActive
156
+ ? 'bg-accent/10 text-accent'
157
+ : 'text-text-main hover:bg-bg-app'
158
+ }`}
159
+ >
160
+ <div className={`flex items-center justify-center w-8 h-8 rounded-lg ${isActive ? 'bg-accent/20' : 'bg-bg-app border border-border-app'}`}>
161
+ {cmd.icon ? cmd.icon : <Command className="w-4 h-4" />}
162
+ </div>
163
+ <span className="font-medium text-sm flex-1">{cmd.label}</span>
164
+ {isActive && (
165
+ <span className="text-[10px] uppercase font-bold text-accent">Enter ↵</span>
166
+ )}
167
+ </div>
168
+ );
169
+ })}
170
+ </div>
171
+ </div>
172
+ );
173
+ })
174
+ )}
175
+ </div>
176
+
177
+ {/* Footer */}
178
+ <div className="px-4 py-2 border-t border-border-app bg-bg-app/50 flex items-center justify-between text-[10px] text-text-muted">
179
+ <div className="flex gap-4">
180
+ <span className="flex items-center gap-1">↑↓ Navegar</span>
181
+ <span className="flex items-center gap-1">↵ Seleccionar</span>
182
+ <span className="flex items-center gap-1">Esc Cerrar</span>
183
+ </div>
184
+ <span className="font-mono opacity-50">AlexUI</span>
185
+ </div>
186
+ </motion.div>
187
+ </div>
188
+ )}
189
+ </AnimatePresence>
190
+ );
191
+ };
@@ -0,0 +1,152 @@
1
+ import React, { useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+ import { AlertTriangle, Info, Trash2, X } from 'lucide-react';
5
+
6
+ export type ConfirmVariant = 'danger' | 'warning' | 'info';
7
+
8
+ export interface ConfirmDialogProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ onConfirm: () => void;
12
+ title: string;
13
+ message: React.ReactNode;
14
+ confirmLabel?: string;
15
+ cancelLabel?: string;
16
+ variant?: ConfirmVariant;
17
+ loading?: boolean;
18
+ className?: string;
19
+ }
20
+
21
+ const variantConfig: Record<ConfirmVariant, { icon: React.ReactNode; accent: string; button: string }> = {
22
+ danger: {
23
+ icon: <Trash2 className="w-6 h-6" />,
24
+ accent: 'text-red-500 bg-red-500/10 border-red-500/25',
25
+ button: 'bg-red-500 hover:bg-red-600 text-white border-red-500/30'
26
+ },
27
+ warning: {
28
+ icon: <AlertTriangle className="w-6 h-6" />,
29
+ accent: 'text-yellow-500 bg-yellow-500/10 border-yellow-500/25',
30
+ button: 'bg-yellow-500 hover:bg-yellow-600 text-white border-yellow-500/30'
31
+ },
32
+ info: {
33
+ icon: <Info className="w-6 h-6" />,
34
+ accent: 'text-accent bg-accent/10 border-accent/25',
35
+ button: 'bg-accent hover:bg-accent-hover text-white border-accent/30'
36
+ }
37
+ };
38
+
39
+ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
40
+ isOpen,
41
+ onClose,
42
+ onConfirm,
43
+ title,
44
+ message,
45
+ confirmLabel = 'Confirmar',
46
+ cancelLabel = 'Cancelar',
47
+ variant = 'danger',
48
+ loading = false,
49
+ className = ''
50
+ }) => {
51
+ const config = variantConfig[variant];
52
+
53
+ useEffect(() => {
54
+ const handleKeyDown = (e: KeyboardEvent) => {
55
+ if (e.key === 'Escape' && isOpen) onClose();
56
+ };
57
+ if (isOpen) {
58
+ window.addEventListener('keydown', handleKeyDown);
59
+ document.body.style.overflow = 'hidden';
60
+ }
61
+ return () => {
62
+ window.removeEventListener('keydown', handleKeyDown);
63
+ document.body.style.overflow = '';
64
+ };
65
+ }, [isOpen, onClose]);
66
+
67
+ if (typeof document === 'undefined') return null;
68
+
69
+ return createPortal(
70
+ <AnimatePresence>
71
+ {isOpen && (
72
+ <div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
73
+ <motion.div
74
+ initial={{ opacity: 0 }}
75
+ animate={{ opacity: 1 }}
76
+ exit={{ opacity: 0 }}
77
+ className="fixed inset-0 bg-black/40 backdrop-blur-md"
78
+ onClick={onClose}
79
+ />
80
+
81
+ <motion.div
82
+ initial={{ scale: 0.95, opacity: 0, y: 15 }}
83
+ animate={{
84
+ scale: 1,
85
+ opacity: 1,
86
+ y: 0,
87
+ transition: {
88
+ type: 'spring',
89
+ stiffness: 350,
90
+ damping: 25
91
+ }
92
+ }}
93
+ exit={{
94
+ scale: 0.95,
95
+ opacity: 0,
96
+ y: 15,
97
+ transition: {
98
+ duration: 0.2
99
+ }
100
+ }}
101
+ className={`relative z-10 w-full max-w-md rounded-2xl glass bg-bg-card/90 shadow-2xl border border-border-app p-6 flex flex-col gap-5 ${className}`}
102
+ role="alertdialog"
103
+ aria-modal="true"
104
+ >
105
+ <button
106
+ type="button"
107
+ onClick={onClose}
108
+ className="absolute top-4 right-4 p-1.5 rounded-lg text-text-muted hover:text-text-main hover:bg-bg-app transition-colors cursor-pointer"
109
+ aria-label="Cerrar"
110
+ >
111
+ <X className="w-4 h-4" />
112
+ </button>
113
+
114
+ <div className="flex items-start gap-4">
115
+ <div className={`p-3 rounded-xl border flex-shrink-0 ${config.accent}`}>
116
+ {config.icon}
117
+ </div>
118
+ <div className="flex flex-col gap-2 pr-6">
119
+ <h3 className="font-extrabold text-lg text-text-main font-display leading-tight">
120
+ {title}
121
+ </h3>
122
+ <div className="text-sm text-text-muted leading-relaxed">
123
+ {message}
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div className="flex items-center justify-end gap-3 pt-2 border-t border-border-app/50">
129
+ <button
130
+ type="button"
131
+ onClick={onClose}
132
+ disabled={loading}
133
+ className="px-4 py-2 rounded-xl text-sm font-bold text-text-muted hover:text-text-main hover:bg-bg-app border border-border-app transition-colors cursor-pointer disabled:opacity-50"
134
+ >
135
+ {cancelLabel}
136
+ </button>
137
+ <button
138
+ type="button"
139
+ onClick={onConfirm}
140
+ disabled={loading}
141
+ className={`px-5 py-2 rounded-xl text-sm font-bold border transition-colors cursor-pointer disabled:opacity-50 ${config.button}`}
142
+ >
143
+ {loading ? 'Procesando...' : confirmLabel}
144
+ </button>
145
+ </div>
146
+ </motion.div>
147
+ </div>
148
+ )}
149
+ </AnimatePresence>,
150
+ document.body
151
+ );
152
+ };
@@ -0,0 +1,192 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+ import { ChevronRight } from 'lucide-react';
5
+
6
+ export interface ContextMenuItem {
7
+ id: string;
8
+ label: string;
9
+ icon?: React.ReactNode;
10
+ shortcut?: string;
11
+ danger?: boolean;
12
+ disabled?: boolean;
13
+ divider?: boolean;
14
+ subItems?: ContextMenuItem[];
15
+ }
16
+
17
+ export interface ContextMenuProps {
18
+ children: React.ReactNode;
19
+ items: ContextMenuItem[];
20
+ onAction: (id: string) => void;
21
+ disabled?: boolean;
22
+ }
23
+
24
+ export const ContextMenu: React.FC<ContextMenuProps> = ({
25
+ children,
26
+ items,
27
+ onAction,
28
+ disabled = false
29
+ }) => {
30
+ const [isOpen, setIsOpen] = useState(false);
31
+ const [position, setPosition] = useState({ x: 0, y: 0 });
32
+ const [activeSubMenu, setActiveSubMenu] = useState<string | null>(null);
33
+ const [mounted, setMounted] = useState(false);
34
+
35
+ const menuRef = useRef<HTMLDivElement>(null);
36
+
37
+ useEffect(() => {
38
+ setMounted(true);
39
+ }, []);
40
+
41
+ const handleContextMenu = (e: React.MouseEvent) => {
42
+ if (disabled) return;
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+
46
+ // Calculate position to prevent overflowing screen
47
+ const x = e.clientX;
48
+ const y = e.clientY;
49
+
50
+ setPosition({ x, y });
51
+ setIsOpen(true);
52
+ setActiveSubMenu(null);
53
+ };
54
+
55
+ useEffect(() => {
56
+ if (!isOpen) return;
57
+
58
+ const handleClickOutside = (e: MouseEvent) => {
59
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
60
+ setIsOpen(false);
61
+ }
62
+ };
63
+
64
+ const handleEscape = (e: KeyboardEvent) => {
65
+ if (e.key === 'Escape') setIsOpen(false);
66
+ };
67
+
68
+ // Close on scroll or resize
69
+ const handleScrollOrResize = () => setIsOpen(false);
70
+
71
+ document.addEventListener('mousedown', handleClickOutside);
72
+ document.addEventListener('keydown', handleEscape);
73
+ window.addEventListener('scroll', handleScrollOrResize, { capture: true });
74
+ window.addEventListener('resize', handleScrollOrResize);
75
+
76
+ return () => {
77
+ document.removeEventListener('mousedown', handleClickOutside);
78
+ document.removeEventListener('keydown', handleEscape);
79
+ window.removeEventListener('scroll', handleScrollOrResize, { capture: true });
80
+ window.removeEventListener('resize', handleScrollOrResize);
81
+ };
82
+ }, [isOpen]);
83
+
84
+ // Adjust position if it overflows the viewport
85
+ useEffect(() => {
86
+ if (isOpen && menuRef.current) {
87
+ const rect = menuRef.current.getBoundingClientRect();
88
+ const viewportWidth = window.innerWidth;
89
+ const viewportHeight = window.innerHeight;
90
+
91
+ let newX = position.x;
92
+ let newY = position.y;
93
+
94
+ if (position.x + rect.width > viewportWidth) {
95
+ newX = viewportWidth - rect.width - 10;
96
+ }
97
+ if (position.y + rect.height > viewportHeight) {
98
+ newY = viewportHeight - rect.height - 10;
99
+ }
100
+
101
+ if (newX !== position.x || newY !== position.y) {
102
+ setPosition({ x: newX, y: newY });
103
+ }
104
+ }
105
+ }, [isOpen, position]);
106
+
107
+ const renderItems = (itemsList: ContextMenuItem[]) => {
108
+ return itemsList.map((item, idx) => {
109
+ if (item.divider) {
110
+ return <div key={`div-${idx}`} className="my-1 h-px bg-border-app/50 w-full" />;
111
+ }
112
+
113
+ const hasSubItems = item.subItems && item.subItems.length > 0;
114
+ const isSubOpen = activeSubMenu === item.id;
115
+
116
+ return (
117
+ <div
118
+ key={item.id}
119
+ className="relative"
120
+ onMouseEnter={() => hasSubItems && setActiveSubMenu(item.id)}
121
+ onMouseLeave={() => hasSubItems && setActiveSubMenu(null)}
122
+ >
123
+ <button
124
+ disabled={item.disabled}
125
+ onClick={(e) => {
126
+ if (hasSubItems) return;
127
+ e.stopPropagation();
128
+ onAction(item.id);
129
+ setIsOpen(false);
130
+ }}
131
+ className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg transition-colors ${
132
+ item.disabled
133
+ ? 'opacity-50 cursor-not-allowed text-text-muted'
134
+ : item.danger
135
+ ? 'text-red-400 hover:bg-red-400/10'
136
+ : 'text-text-main hover:bg-accent/10 hover:text-accent'
137
+ } ${isSubOpen && !item.disabled ? 'bg-bg-app' : ''}`}
138
+ >
139
+ <div className="flex items-center gap-2">
140
+ {item.icon && <span className="w-4 h-4 flex items-center justify-center opacity-70">{item.icon}</span>}
141
+ <span>{item.label}</span>
142
+ </div>
143
+ {item.shortcut && (
144
+ <span className="text-[10px] text-text-muted font-mono tracking-widest opacity-60">
145
+ {item.shortcut}
146
+ </span>
147
+ )}
148
+ {hasSubItems && <ChevronRight className="w-4 h-4 opacity-50" />}
149
+ </button>
150
+
151
+ {/* Submenu rendering */}
152
+ {hasSubItems && isSubOpen && !item.disabled && (
153
+ <div className="absolute top-0 left-[calc(100%+4px)] min-w-[180px] p-1.5 glass bg-bg-card/95 border border-border-app rounded-xl shadow-xl z-50 animate-in fade-in zoom-in-95 duration-100">
154
+ {renderItems(item.subItems!)}
155
+ </div>
156
+ )}
157
+ </div>
158
+ );
159
+ });
160
+ };
161
+
162
+ return (
163
+ <div className="relative inline-block w-full h-full" onContextMenu={handleContextMenu}>
164
+ {children}
165
+
166
+ {/* We use standard relative/absolute portals, but since this is inline, we use fixed position overlay for the menu */}
167
+ {mounted && createPortal(
168
+ <AnimatePresence>
169
+ {isOpen && !disabled && (
170
+ <div className="fixed inset-0 z-[999] pointer-events-none">
171
+ <motion.div
172
+ ref={menuRef}
173
+ initial={{ opacity: 0, scale: 0.95, filter: 'blur(4px)' }}
174
+ animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
175
+ exit={{ opacity: 0, scale: 0.95, filter: 'blur(4px)' }}
176
+ transition={{ duration: 0.15, ease: "easeOut" }}
177
+ className="absolute min-w-[220px] p-1.5 glass bg-bg-card/90 backdrop-blur-2xl border border-border-app shadow-[0_10px_40px_-10px_rgba(0,0,0,0.5)] rounded-xl pointer-events-auto flex flex-col"
178
+ style={{
179
+ left: `${position.x}px`,
180
+ top: `${position.y}px`,
181
+ }}
182
+ >
183
+ {renderItems(items)}
184
+ </motion.div>
185
+ </div>
186
+ )}
187
+ </AnimatePresence>,
188
+ document.body
189
+ )}
190
+ </div>
191
+ );
192
+ };
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Users, Download, Layers, TrendingUp } from 'lucide-react';
4
+ import { GlassCard } from './GlassCard';
5
+ import { ChartShowcase } from './ChartShowcase';
6
+ import { ActionTable, type TableRowData } from './ActionTable';
7
+ import { Badge } from './Badge';
8
+ import { Skeleton } from './Skeleton';
9
+
10
+ export interface DashboardStat {
11
+ id: string;
12
+ label: string;
13
+ value: string;
14
+ change?: string;
15
+ icon?: React.ReactNode;
16
+ }
17
+
18
+ export interface DashboardLayoutProps {
19
+ title?: string;
20
+ subtitle?: string;
21
+ stats?: DashboardStat[];
22
+ tableData?: TableRowData[];
23
+ onTableAction?: (action: string, row: TableRowData) => void;
24
+ isLoading?: boolean;
25
+ disabled?: boolean;
26
+ className?: string;
27
+ }
28
+
29
+ const DEFAULT_STATS: DashboardStat[] = [
30
+ { id: 'users', label: 'Usuarios activos', value: '2.4k', change: '+12%', icon: <Users className="w-4 h-4" /> },
31
+ { id: 'downloads', label: 'Descargas CLI', value: '18.2k', change: '+8%', icon: <Download className="w-4 h-4" /> },
32
+ { id: 'components', label: 'Componentes', value: '86', change: 'V3.5', icon: <Layers className="w-4 h-4" /> },
33
+ { id: 'growth', label: 'Crecimiento', value: '34%', change: '+4%', icon: <TrendingUp className="w-4 h-4" /> },
34
+ ];
35
+
36
+ const DEFAULT_TABLE: TableRowData[] = [
37
+ { id: 1, name: 'Alexis Jardin', email: 'alex@alexui.dev', role: 'Admin', status: 'Active' },
38
+ { id: 2, name: 'Sofía López', email: 'sofia@alexui.dev', role: 'Editor', status: 'Active' },
39
+ { id: 3, name: 'Mateo Díaz', email: 'mateo@alexui.dev', role: 'Viewer', status: 'Pending' },
40
+ { id: 4, name: 'Lucía Vega', email: 'lucia@alexui.dev', role: 'Editor', status: 'Inactive' },
41
+ ];
42
+
43
+ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
44
+ title = 'Panel de control',
45
+ subtitle = 'Resumen de actividad y métricas del catálogo',
46
+ stats = DEFAULT_STATS,
47
+ tableData = DEFAULT_TABLE,
48
+ onTableAction,
49
+ isLoading = false,
50
+ disabled = false,
51
+ className = ''
52
+ }) => {
53
+ if (isLoading) {
54
+ return (
55
+ <div className={`w-full flex flex-col gap-4 ${className}`}>
56
+ <Skeleton variant="text" className="w-48 h-6" />
57
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
58
+ {Array.from({ length: 4 }).map((_, i) => (
59
+ <Skeleton key={i} variant="rect" className="h-24 rounded-2xl" />
60
+ ))}
61
+ </div>
62
+ <Skeleton variant="rect" className="h-48 rounded-2xl" />
63
+ <Skeleton variant="rect" className="h-40 rounded-2xl" />
64
+ </div>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <motion.div
70
+ initial={{ opacity: 0, y: 12 }}
71
+ animate={{ opacity: 1, y: 0 }}
72
+ transition={{ duration: 0.3 }}
73
+ className={`w-full flex flex-col gap-5 ${className}`}
74
+ >
75
+ <div className="flex flex-col gap-1">
76
+ <h2 className="text-lg font-extrabold text-text-main font-display">{title}</h2>
77
+ <p className="text-xs text-text-muted">{subtitle}</p>
78
+ </div>
79
+
80
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
81
+ {stats.map((stat, index) => (
82
+ <motion.div
83
+ key={stat.id}
84
+ initial={{ opacity: 0, y: 8 }}
85
+ animate={{ opacity: 1, y: 0 }}
86
+ transition={{ delay: index * 0.05 }}
87
+ >
88
+ <GlassCard padding="sm" hoverable className="h-full">
89
+ <div className="flex items-start justify-between gap-2">
90
+ <div className="p-2 rounded-xl bg-accent/10 text-accent">
91
+ {stat.icon}
92
+ </div>
93
+ {stat.change && (
94
+ <Badge variant="success" size="sm">{stat.change}</Badge>
95
+ )}
96
+ </div>
97
+ <p className="text-xl font-extrabold text-text-main font-display mt-3">{stat.value}</p>
98
+ <p className="text-[10px] text-text-muted font-medium mt-0.5">{stat.label}</p>
99
+ </GlassCard>
100
+ </motion.div>
101
+ ))}
102
+ </div>
103
+
104
+ <ChartShowcase type="bar" className="!p-4" />
105
+
106
+ <ActionTable
107
+ data={tableData}
108
+ onAction={onTableAction}
109
+ showPagination={false}
110
+ itemsPerPage={4}
111
+ disabled={disabled}
112
+ />
113
+ </motion.div>
114
+ );
115
+ };