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,266 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Bell, LogOut, Settings, User, Menu, X } from 'lucide-react';
4
+ import { ThemeSwitcher } from './ThemeSwitcher';
5
+
6
+ export interface NavItem {
7
+ label: string;
8
+ href: string;
9
+ }
10
+
11
+ export interface UserProfile {
12
+ name: string;
13
+ email: string;
14
+ avatarUrl: string;
15
+ }
16
+
17
+ export interface InteractiveNavbarProps {
18
+ brandName?: string;
19
+ navItems?: NavItem[];
20
+ showThemeToggle?: boolean;
21
+ showNotifications?: boolean;
22
+ showUserMenu?: boolean;
23
+ notificationsCount?: number;
24
+ userProfile?: UserProfile;
25
+ onLogout?: () => void;
26
+ className?: string;
27
+ }
28
+
29
+ export const InteractiveNavbar: React.FC<InteractiveNavbarProps> = ({
30
+ brandName = 'AlexUI',
31
+ navItems = [
32
+ { label: 'Inicio', href: '#' },
33
+ { label: 'Componentes', href: '#' },
34
+ { label: 'Documentación', href: '#' }
35
+ ],
36
+ showThemeToggle = true,
37
+ showNotifications = true,
38
+ showUserMenu = true,
39
+ notificationsCount = 3,
40
+ userProfile = {
41
+ name: 'Alexis Jardin',
42
+ email: 'alex@jardin.com',
43
+ avatarUrl: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop'
44
+ },
45
+ onLogout,
46
+ className = ''
47
+ }) => {
48
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
49
+ const [isNotifDropdownOpen, setIsNotifDropdownOpen] = useState(false);
50
+ const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
51
+
52
+ const notifRef = useRef<HTMLDivElement>(null);
53
+ const userRef = useRef<HTMLDivElement>(null);
54
+
55
+ // Close dropdowns on click outside
56
+ useEffect(() => {
57
+ const handleClickOutside = (event: MouseEvent) => {
58
+ if (notifRef.current && !notifRef.current.contains(event.target as Node)) {
59
+ setIsNotifDropdownOpen(false);
60
+ }
61
+ if (userRef.current && !userRef.current.contains(event.target as Node)) {
62
+ setIsUserDropdownOpen(false);
63
+ }
64
+ };
65
+ document.addEventListener('mousedown', handleClickOutside);
66
+ return () => document.removeEventListener('mousedown', handleClickOutside);
67
+ }, []);
68
+
69
+ // Fictional notifications list
70
+ const mockNotifications = [
71
+ { id: 1, text: 'Sofía Romero te ha enviado un mensaje.', time: 'Hace 5 min' },
72
+ { id: 2, text: 'Nueva versión V1.5 de AlexUI liberada.', time: 'Hace 2 horas' },
73
+ { id: 3, text: 'Se completó la simulación de carga con éxito.', time: 'Hace 1 día' }
74
+ ];
75
+
76
+ return (
77
+ <nav className={`w-full sticky top-0 z-40 glass bg-bg-card/75 border-b border-border-app px-6 py-3.5 flex items-center justify-between transition-all duration-300 ${className}`}>
78
+
79
+ {/* Brand Logo & Mobile Trigger */}
80
+ <div className="flex items-center gap-3">
81
+ <button
82
+ onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
83
+ className="md:hidden p-1.5 rounded-lg text-text-muted hover:bg-bg-app hover:text-text-main transition-colors cursor-pointer"
84
+ >
85
+ {isMobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
86
+ </button>
87
+ <span className="font-extrabold text-lg tracking-tight bg-gradient-to-r from-accent to-pink-500 bg-clip-text text-transparent font-display">
88
+ {brandName}
89
+ </span>
90
+ </div>
91
+
92
+ {/* Desktop Navigation Link items */}
93
+ <div className="hidden md:flex items-center gap-6">
94
+ {navItems.map((item, idx) => (
95
+ <a
96
+ key={idx}
97
+ href={item.href}
98
+ className="text-xs font-bold text-text-muted hover:text-accent transition-colors duration-300"
99
+ >
100
+ {item.label}
101
+ </a>
102
+ ))}
103
+ </div>
104
+
105
+ {/* Right Side Control actions */}
106
+ <div className="flex items-center gap-4">
107
+
108
+ {/* Dynamic Theme switcher */}
109
+ {showThemeToggle && (
110
+ <div className="flex items-center justify-center">
111
+ <ThemeSwitcher />
112
+ </div>
113
+ )}
114
+
115
+ {/* Notifications Icon with count indicator badge & dropdown */}
116
+ {showNotifications && (
117
+ <div ref={notifRef} className="relative">
118
+ <button
119
+ onClick={() => setIsNotifDropdownOpen(!isNotifDropdownOpen)}
120
+ className={`relative p-2 rounded-xl border transition-all duration-300 cursor-pointer ${
121
+ isNotifDropdownOpen
122
+ ? 'bg-accent/10 border-accent/30 text-accent shadow-xs'
123
+ : 'bg-bg-card border-border-app text-text-muted hover:text-text-main'
124
+ }`}
125
+ aria-label="Notificaciones"
126
+ >
127
+ <Bell className="w-4 h-4" />
128
+ {notificationsCount > 0 && (
129
+ <span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full text-[9px] font-extrabold text-white flex items-center justify-center animate-pulse">
130
+ {notificationsCount}
131
+ </span>
132
+ )}
133
+ </button>
134
+
135
+ <AnimatePresence>
136
+ {isNotifDropdownOpen && (
137
+ <motion.div
138
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
139
+ animate={{
140
+ opacity: 1,
141
+ y: 6,
142
+ scale: 1,
143
+ transition: { type: 'spring', stiffness: 350, damping: 25 }
144
+ }}
145
+ exit={{ opacity: 0, y: 10, scale: 0.95, transition: { duration: 0.15 } }}
146
+ className="absolute right-0 w-80 glass rounded-2xl shadow-2xl py-3 border border-border-app z-50 flex flex-col gap-2.5"
147
+ >
148
+ <h4 className="text-[10px] font-bold uppercase tracking-wider text-text-muted px-4 font-display">
149
+ Notificaciones Recientes
150
+ </h4>
151
+ <div className="flex flex-col max-h-64 overflow-y-auto">
152
+ {mockNotifications.map((n) => (
153
+ <div
154
+ key={n.id}
155
+ className="px-4 py-2.5 text-xs text-text-muted hover:bg-bg-app hover:text-text-main border-b border-border-app/30 last:border-0 transition-colors duration-200 cursor-pointer flex flex-col gap-1"
156
+ >
157
+ <p className="leading-snug">{n.text}</p>
158
+ <span className="text-[9px] font-mono text-text-muted/60">{n.time}</span>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </motion.div>
163
+ )}
164
+ </AnimatePresence>
165
+ </div>
166
+ )}
167
+
168
+ {/* User Account Avatar & Profile Actions dropdown menu */}
169
+ {showUserMenu && (
170
+ <div ref={userRef} className="relative">
171
+ <button
172
+ onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
173
+ className={`flex items-center justify-center w-9 h-9 rounded-xl overflow-hidden border-2 cursor-pointer shadow-sm hover:scale-105 transition-all duration-300 ${
174
+ isUserDropdownOpen ? 'border-accent' : 'border-border-app'
175
+ }`}
176
+ >
177
+ <img
178
+ src={userProfile.avatarUrl}
179
+ alt={userProfile.name}
180
+ className="w-full h-full object-cover"
181
+ />
182
+ </button>
183
+
184
+ <AnimatePresence>
185
+ {isUserDropdownOpen && (
186
+ <motion.div
187
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
188
+ animate={{
189
+ opacity: 1,
190
+ y: 6,
191
+ scale: 1,
192
+ transition: { type: 'spring', stiffness: 350, damping: 25 }
193
+ }}
194
+ exit={{ opacity: 0, y: 10, scale: 0.95, transition: { duration: 0.15 } }}
195
+ className="absolute right-0 w-56 glass rounded-2xl shadow-2xl p-2 border border-border-app z-50 flex flex-col gap-1"
196
+ >
197
+ {/* User Profile Info section */}
198
+ <div className="px-3.5 py-2.5 border-b border-border-app/50 flex flex-col gap-0.5">
199
+ <span className="text-xs font-bold text-text-main truncate">{userProfile.name}</span>
200
+ <span className="text-[10px] text-text-muted truncate font-mono">{userProfile.email}</span>
201
+ </div>
202
+
203
+ {/* Actions Links */}
204
+ <button
205
+ onClick={() => setIsUserDropdownOpen(false)}
206
+ className="w-full text-left px-3.5 py-2 rounded-xl text-xs font-bold text-text-muted hover:bg-bg-app hover:text-text-main flex items-center gap-2.5 transition-colors cursor-pointer"
207
+ >
208
+ <User className="w-4 h-4 text-text-muted/70" />
209
+ <span>Mi Perfil</span>
210
+ </button>
211
+
212
+ <button
213
+ onClick={() => setIsUserDropdownOpen(false)}
214
+ className="w-full text-left px-3.5 py-2 rounded-xl text-xs font-bold text-text-muted hover:bg-bg-app hover:text-text-main flex items-center gap-2.5 transition-colors cursor-pointer"
215
+ >
216
+ <Settings className="w-4 h-4 text-text-muted/70" />
217
+ <span>Ajustes</span>
218
+ </button>
219
+
220
+ {/* Divider */}
221
+ <div className="h-[1px] bg-border-app/50 my-1" />
222
+
223
+ <button
224
+ onClick={() => {
225
+ setIsUserDropdownOpen(false);
226
+ if (onLogout) onLogout();
227
+ }}
228
+ className="w-full text-left px-3.5 py-2 rounded-xl text-xs font-extrabold text-red-500 hover:bg-red-500/10 flex items-center gap-2.5 transition-colors cursor-pointer"
229
+ >
230
+ <LogOut className="w-4 h-4 text-red-500/80" />
231
+ <span>Cerrar Sesión</span>
232
+ </button>
233
+ </motion.div>
234
+ )}
235
+ </AnimatePresence>
236
+ </div>
237
+ )}
238
+
239
+ </div>
240
+
241
+ {/* Mobile Menu Dropdown drawer */}
242
+ <AnimatePresence>
243
+ {isMobileMenuOpen && (
244
+ <motion.div
245
+ initial={{ opacity: 0, height: 0 }}
246
+ animate={{ opacity: 1, height: 'auto' }}
247
+ exit={{ opacity: 0, height: 0 }}
248
+ className="absolute top-full left-0 w-full glass bg-bg-card/95 border-b border-border-app md:hidden z-30 flex flex-col p-4 gap-2"
249
+ >
250
+ {navItems.map((item, idx) => (
251
+ <a
252
+ key={idx}
253
+ href={item.href}
254
+ className="px-4 py-2.5 rounded-xl text-xs font-bold text-text-muted hover:bg-bg-app hover:text-text-main transition-all"
255
+ onClick={() => setIsMobileMenuOpen(false)}
256
+ >
257
+ {item.label}
258
+ </a>
259
+ ))}
260
+ </motion.div>
261
+ )}
262
+ </AnimatePresence>
263
+
264
+ </nav>
265
+ );
266
+ };
@@ -0,0 +1,211 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, LayoutGroup } from 'framer-motion';
3
+ import {
4
+ ChevronLeft,
5
+ ChevronRight,
6
+ Home,
7
+ User,
8
+ Settings,
9
+ MessageSquare,
10
+ HelpCircle,
11
+ LogOut
12
+ } from 'lucide-react';
13
+ import { ThemeSwitcher } from './ThemeSwitcher';
14
+
15
+ export interface SidebarItem {
16
+ id: string;
17
+ label: string;
18
+ icon: React.ReactNode;
19
+ count?: number;
20
+ }
21
+
22
+ export interface InteractiveSidebarProps {
23
+ items?: SidebarItem[];
24
+ activeId?: string;
25
+ onSelect?: (id: string) => void;
26
+ showCollapseButton?: boolean;
27
+ showUserSession?: boolean;
28
+ showThemeToggle?: boolean;
29
+ userProfile?: {
30
+ name: string;
31
+ email: string;
32
+ avatarUrl: string;
33
+ };
34
+ onLogout?: () => void;
35
+ className?: string;
36
+ }
37
+
38
+ export const InteractiveSidebar: React.FC<InteractiveSidebarProps> = ({
39
+ items = [
40
+ { id: 'home', label: 'Inicio', icon: <Home className="w-5 h-5" /> },
41
+ { id: 'profile', label: 'Mi Perfil', icon: <User className="w-5 h-5" /> },
42
+ { id: 'messages', label: 'Mensajería', icon: <MessageSquare className="w-5 h-5" />, count: 4 },
43
+ { id: 'settings', label: 'Configuración', icon: <Settings className="w-5 h-5" /> },
44
+ { id: 'help', label: 'Soporte y Ayuda', icon: <HelpCircle className="w-5 h-5" /> }
45
+ ],
46
+ activeId = 'home',
47
+ onSelect,
48
+ showCollapseButton = true,
49
+ showUserSession = true,
50
+ showThemeToggle = true,
51
+ userProfile = {
52
+ name: 'Alexis Jardin',
53
+ email: 'alex@jardin.com',
54
+ avatarUrl: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop'
55
+ },
56
+ onLogout,
57
+ className = ''
58
+ }) => {
59
+ const [isCollapsed, setIsCollapsed] = useState(false);
60
+
61
+ const handleSelect = (id: string) => {
62
+ if (onSelect) onSelect(id);
63
+ };
64
+
65
+ return (
66
+ <LayoutGroup id="sidebarLayoutGroup">
67
+ <motion.aside
68
+ animate={{ width: isCollapsed ? 80 : 256 }}
69
+ transition={{ type: 'spring', stiffness: 300, damping: 25 }}
70
+ className={`h-full min-h-[500px] flex flex-col justify-between p-4 glass bg-bg-card/75 border border-border-app rounded-3xl transition-colors duration-300 select-none overflow-hidden relative ${className}`}
71
+ >
72
+
73
+ {/* Upper Brand / Collapse trigger section */}
74
+ <div className="flex flex-col gap-5">
75
+
76
+ <div className="flex items-center justify-between px-2.5 h-10">
77
+ {/* Brand Logo text (hide when collapsed) */}
78
+ {!isCollapsed && (
79
+ <motion.span
80
+ initial={{ opacity: 0, scale: 0.9 }}
81
+ animate={{ opacity: 1, scale: 1 }}
82
+ exit={{ opacity: 0 }}
83
+ className="font-extrabold tracking-tight bg-gradient-to-r from-accent to-pink-500 bg-clip-text text-transparent font-display text-sm"
84
+ >
85
+ ALEXUI MENÚ
86
+ </motion.span>
87
+ )}
88
+
89
+ {/* Collapse toggle arrow */}
90
+ {showCollapseButton && (
91
+ <button
92
+ onClick={() => setIsCollapsed(!isCollapsed)}
93
+ className={`p-1.5 rounded-xl border border-border-app bg-bg-card hover:border-accent hover:text-accent text-text-muted transition-colors cursor-pointer ${
94
+ isCollapsed ? 'mx-auto' : ''
95
+ }`}
96
+ aria-label={isCollapsed ? "Expandir" : "Colapsar"}
97
+ >
98
+ {isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
99
+ </button>
100
+ )}
101
+ </div>
102
+
103
+ {/* Navigation Links Items List */}
104
+ <div className="flex flex-col gap-1">
105
+ {items.map((item) => {
106
+ const isSelected = activeId === item.id;
107
+ return (
108
+ <button
109
+ key={item.id}
110
+ onClick={() => handleSelect(item.id)}
111
+ className={`w-full relative py-3 px-3 rounded-xl flex items-center justify-between text-left font-bold text-xs transition-colors duration-300 cursor-pointer ${
112
+ isSelected ? 'text-accent' : 'text-text-muted hover:text-text-main'
113
+ }`}
114
+ >
115
+
116
+ {/* Sliding active pill background (layout sharing) */}
117
+ {isSelected && (
118
+ <motion.div
119
+ layoutId="sidebarActivePill"
120
+ className="absolute inset-0 bg-accent/10 border-l-2 border-accent rounded-xl z-0"
121
+ transition={{ type: 'spring', stiffness: 350, damping: 25 }}
122
+ />
123
+ )}
124
+
125
+ {/* Icon & Label */}
126
+ <div className="flex items-center gap-3 z-10 relative">
127
+ <span className="flex-shrink-0">{item.icon}</span>
128
+ {!isCollapsed && (
129
+ <motion.span
130
+ initial={{ opacity: 0, x: -10 }}
131
+ animate={{ opacity: 1, x: 0 }}
132
+ className="truncate"
133
+ >
134
+ {item.label}
135
+ </motion.span>
136
+ )}
137
+ </div>
138
+
139
+ {/* Badge Notification count (hide label when collapsed, maybe render dot) */}
140
+ {item.count && item.count > 0 && (
141
+ <span className={`z-10 relative bg-accent text-white font-extrabold rounded-full flex items-center justify-center shrink-0 ${
142
+ isCollapsed
143
+ ? 'w-2 h-2 p-0 absolute top-2 right-2 border border-bg-card'
144
+ : 'text-[9px] px-2 py-0.5 min-w-5 h-5'
145
+ }`}>
146
+ {!isCollapsed && item.count}
147
+ </span>
148
+ )}
149
+ </button>
150
+ );
151
+ })}
152
+ </div>
153
+ </div>
154
+
155
+ {/* Bottom User Profile Section & Theme Toggle */}
156
+ <div className="flex flex-col gap-4">
157
+
158
+ {/* Theme switcher embedded */}
159
+ {showThemeToggle && (
160
+ <div className={`flex items-center justify-center p-1 rounded-2xl bg-bg-app/40 border border-border-app/30 ${
161
+ isCollapsed ? 'w-full' : 'px-3 py-1.5'
162
+ }`}>
163
+ <ThemeSwitcher />
164
+ </div>
165
+ )}
166
+
167
+ {/* User profile actions summary */}
168
+ {showUserSession && (
169
+ <div className={`flex items-center justify-between border-t border-border-app/50 pt-4 px-1 ${
170
+ isCollapsed ? 'flex-col gap-3 justify-center' : ''
171
+ }`}>
172
+ {/* Profile Avatar + info */}
173
+ <div className="flex items-center gap-2.5 overflow-hidden">
174
+ <img
175
+ src={userProfile.avatarUrl}
176
+ alt={userProfile.name}
177
+ className="w-9 h-9 rounded-xl border border-border-app object-cover shrink-0"
178
+ />
179
+ {!isCollapsed && (
180
+ <motion.div
181
+ initial={{ opacity: 0 }}
182
+ animate={{ opacity: 1 }}
183
+ className="flex flex-col overflow-hidden shrink"
184
+ >
185
+ <span className="text-[11px] font-extrabold text-text-main truncate leading-tight">{userProfile.name}</span>
186
+ <span className="text-[9px] font-mono text-text-muted truncate leading-none mt-0.5">{userProfile.email}</span>
187
+ </motion.div>
188
+ )}
189
+ </div>
190
+
191
+ {/* Logout Button */}
192
+ <button
193
+ onClick={() => {
194
+ if (onLogout) onLogout();
195
+ }}
196
+ className={`p-2 rounded-xl text-red-500 hover:bg-red-500/10 transition-colors cursor-pointer shrink-0 ${
197
+ isCollapsed ? 'mx-auto' : ''
198
+ }`}
199
+ title="Cerrar Sesión"
200
+ >
201
+ <LogOut className="w-4 h-4" />
202
+ </button>
203
+ </div>
204
+ )}
205
+
206
+ </div>
207
+
208
+ </motion.aside>
209
+ </LayoutGroup>
210
+ );
211
+ };
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+
3
+ export type KbdSize = 'sm' | 'md';
4
+
5
+ export interface KbdProps {
6
+ children: React.ReactNode;
7
+ size?: KbdSize;
8
+ className?: string;
9
+ }
10
+
11
+ const sizeStyles: Record<KbdSize, string> = {
12
+ sm: 'text-[10px] px-1.5 py-0.5 min-w-[1.25rem]',
13
+ md: 'text-xs px-2 py-1 min-w-[1.5rem]'
14
+ };
15
+
16
+ export const Kbd: React.FC<KbdProps> = ({
17
+ children,
18
+ size = 'md',
19
+ className = ''
20
+ }) => (
21
+ <kbd
22
+ className={`inline-flex items-center justify-center font-mono font-bold rounded-md border border-border-app bg-bg-app/80 text-text-muted shadow-[0_1px_0_rgba(255,255,255,0.06),inset_0_0_0_1px_rgba(255,255,255,0.04)] ${sizeStyles[size]} ${className}`}
23
+ >
24
+ {children}
25
+ </kbd>
26
+ );
27
+
28
+ export interface KbdGroupProps {
29
+ keys: string[];
30
+ separator?: string;
31
+ size?: KbdSize;
32
+ className?: string;
33
+ }
34
+
35
+ export const KbdGroup: React.FC<KbdGroupProps> = ({
36
+ keys,
37
+ separator = '+',
38
+ size = 'md',
39
+ className = ''
40
+ }) => (
41
+ <span className={`inline-flex items-center gap-1 ${className}`}>
42
+ {keys.map((key, index) => (
43
+ <React.Fragment key={`${key}-${index}`}>
44
+ {index > 0 && (
45
+ <span className="text-[10px] font-bold text-text-muted/60 select-none">{separator}</span>
46
+ )}
47
+ <Kbd size={size}>{key}</Kbd>
48
+ </React.Fragment>
49
+ ))}
50
+ </span>
51
+ );
@@ -0,0 +1,118 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Play } from 'lucide-react';
4
+ import { Skeleton } from './Skeleton';
5
+
6
+ export interface LiteYouTubeProps {
7
+ videoId: string;
8
+ title: string;
9
+ params?: string; // Optional parameters like rel=0, controls=1, etc.
10
+ posterSize?: 'maxresdefault' | 'hqdefault' | 'sddefault';
11
+ className?: string;
12
+ isLoading?: boolean;
13
+ }
14
+
15
+ export const LiteYouTube: React.FC<LiteYouTubeProps> = ({
16
+ videoId,
17
+ title,
18
+ params = 'rel=0&showinfo=0&controls=1',
19
+ posterSize = 'maxresdefault',
20
+ className = '',
21
+ isLoading = false
22
+ }) => {
23
+ const [isPlaying, setIsPlaying] = useState(false);
24
+ const [posterUrl, setPosterUrl] = useState(`https://i.ytimg.com/vi/${videoId}/${posterSize}.jpg`);
25
+
26
+ // Dynamically add preconnections to speed up the YouTube player load on click
27
+ useEffect(() => {
28
+ if (isLoading) return;
29
+
30
+ const pc1 = document.createElement('link');
31
+ pc1.rel = 'preconnect';
32
+ pc1.href = 'https://www.youtube-nocookie.com';
33
+
34
+ const pc2 = document.createElement('link');
35
+ pc2.rel = 'preconnect';
36
+ pc2.href = 'https://i.ytimg.com';
37
+
38
+ document.head.appendChild(pc1);
39
+ document.head.appendChild(pc2);
40
+
41
+ return () => {
42
+ document.head.removeChild(pc1);
43
+ document.head.removeChild(pc2);
44
+ };
45
+ }, [videoId, isLoading]);
46
+
47
+ // Fallback to hqdefault in case maxresdefault doesn't exist
48
+ const handlePosterError = () => {
49
+ if (posterUrl.includes('maxresdefault')) {
50
+ setPosterUrl(`https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`);
51
+ }
52
+ };
53
+
54
+ const handlePlay = () => {
55
+ setIsPlaying(true);
56
+ };
57
+
58
+ const embedUrl = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=0&${params}`;
59
+
60
+ if (isLoading) {
61
+ return (
62
+ <Skeleton
63
+ className={`w-full aspect-video rounded-2xl border border-border-app shadow-lg ${className}`}
64
+ />
65
+ );
66
+ }
67
+
68
+ return (
69
+ <div
70
+ className={`relative w-full aspect-video rounded-2xl overflow-hidden bg-black border border-border-app shadow-lg group ${className}`}
71
+ >
72
+ {!isPlaying ? (
73
+ <div
74
+ onClick={handlePlay}
75
+ className="absolute inset-0 w-full h-full cursor-pointer flex items-center justify-center select-none"
76
+ >
77
+ {/* Static Image Thumbnail */}
78
+ <img
79
+ src={posterUrl}
80
+ alt={title}
81
+ onError={handlePosterError}
82
+ className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
83
+ loading="lazy"
84
+ />
85
+
86
+ {/* Vignette Overlay */}
87
+ <div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-black/20" />
88
+
89
+ {/* Title Header */}
90
+ <span className="absolute top-4 left-4 right-4 text-sm font-bold text-white drop-shadow-md truncate opacity-90 group-hover:opacity-100 transition-opacity duration-200">
91
+ {title}
92
+ </span>
93
+
94
+ {/* Custom Animated Play Button */}
95
+ <motion.div
96
+ whileHover={{ scale: 1.15 }}
97
+ whileTap={{ scale: 0.95 }}
98
+ className="relative z-10 w-16 h-16 rounded-full bg-accent text-white flex items-center justify-center shadow-2xl transition-colors hover:bg-accent-hover group-hover:shadow-[0_0_25px_rgba(99,102,241,0.6)] cursor-pointer"
99
+ >
100
+ {/* Glowing background ripple */}
101
+ <div className="absolute -inset-1 rounded-full bg-accent/30 animate-ping opacity-70 group-hover:opacity-100" />
102
+
103
+ <Play className="w-6 h-6 fill-white ml-1" />
104
+ </motion.div>
105
+ </div>
106
+ ) : (
107
+ /* Actual Iframe embed injected only on-click */
108
+ <iframe
109
+ src={embedUrl}
110
+ title={title}
111
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
112
+ allowFullScreen
113
+ className="absolute inset-0 w-full h-full border-0 z-10"
114
+ />
115
+ )}
116
+ </div>
117
+ );
118
+ };