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
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # alexui CLI
2
+
3
+ **AlexUI by Alexis Jardin** — instalá componentes glassmorphic en tu proyecto React (estilo shadcn: copiás el código fuente).
4
+
5
+ ## Requisitos
6
+
7
+ - Node.js 18+
8
+ - Proyecto React con Tailwind CSS v4
9
+
10
+ ## Instalación
11
+
12
+ ```bash
13
+ npx alexui@latest init
14
+ npx alexui@latest add dock
15
+ ```
16
+
17
+ O global:
18
+
19
+ ```bash
20
+ npm install -g alexui
21
+ alexui tui
22
+ ```
23
+
24
+ ## Comandos
25
+
26
+ | Comando | Descripción |
27
+ |---------|-------------|
28
+ | `alexui init` | Crea `alexui.json` y opcionalmente el CSS de tema |
29
+ | `alexui add <id>` | Copia componente(s) y dependencias internas |
30
+ | `alexui list` | Lista los 86 componentes del registry |
31
+ | `alexui tui` | Modo interactivo con @clack/prompts |
32
+
33
+ ### Proyecto externo
34
+
35
+ ```bash
36
+ alexui --cwd ../mi-app add loginform
37
+ ```
38
+
39
+ ## Configuración (`alexui.json`)
40
+
41
+ ```json
42
+ {
43
+ "componentDir": "src/components/ui",
44
+ "tailwindCss": "src/index.css"
45
+ }
46
+ ```
47
+
48
+ ## Catálogo y documentación
49
+
50
+ - Sitio: [alexui.dev](https://alexui.dev) *(actualizar cuando esté en producción)*
51
+ - Preview en vivo, docs y prompts IA por componente
52
+
53
+ ## Licencia
54
+
55
+ MIT — © 2026 Alexis Jardin. Los componentes copiados a tu proyecto son tuyos para modificar.
56
+
57
+ Templates Pro y licencias comerciales: ver sitio AlexUI Pro (próximamente).
@@ -0,0 +1,307 @@
1
+ import React, { useState, useMemo, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { MoreHorizontal, ArrowUpDown, Edit2, Trash2, Eye } from 'lucide-react';
4
+ import { Skeleton } from './Skeleton';
5
+ import { TableControls } from './TableControls';
6
+ import { TablePagination } from './TablePagination';
7
+
8
+ export interface TableRowData {
9
+ id: string | number;
10
+ name: string;
11
+ email: string;
12
+ role: string;
13
+ status: 'Active' | 'Inactive' | 'Pending';
14
+ avatarUrl?: string;
15
+ }
16
+
17
+ export interface ActionTableProps {
18
+ data: TableRowData[];
19
+ onAction?: (action: string, row: TableRowData) => void;
20
+ className?: string;
21
+ isLoading?: boolean;
22
+ showControls?: boolean;
23
+ showPagination?: boolean;
24
+ itemsPerPage?: number;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ export const ActionTable: React.FC<ActionTableProps> = ({
29
+ data,
30
+ onAction,
31
+ className = '',
32
+ isLoading = false,
33
+ showControls = true,
34
+ showPagination = true,
35
+ itemsPerPage = 4,
36
+ disabled = false
37
+ }) => {
38
+ const [searchTerm, setSearchTerm] = useState('');
39
+ const [statusFilter, setStatusFilter] = useState('All');
40
+ const [currentPage, setCurrentPage] = useState(1);
41
+ const [sortField, setSortField] = useState<keyof TableRowData | null>(null);
42
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
43
+ const [activeDropdownId, setActiveDropdownId] = useState<string | number | null>(null);
44
+
45
+ // Reset page when search or filter changes
46
+ useEffect(() => {
47
+ setCurrentPage(1);
48
+ }, [searchTerm, statusFilter]);
49
+
50
+ // Sort handler
51
+ const handleSort = (field: keyof TableRowData) => {
52
+ let direction: 'asc' | 'desc' = 'asc';
53
+ if (sortField === field && sortDirection === 'asc') {
54
+ direction = 'desc';
55
+ }
56
+ setSortField(field);
57
+ setSortDirection(direction);
58
+ };
59
+
60
+ const toggleDropdown = (id: string | number, e: React.MouseEvent) => {
61
+ e.stopPropagation();
62
+ setActiveDropdownId((prev) => (prev === id ? null : id));
63
+ };
64
+
65
+ // Close dropdowns on body click
66
+ useEffect(() => {
67
+ const closeAll = () => setActiveDropdownId(null);
68
+ document.addEventListener('click', closeAll);
69
+ return () => document.removeEventListener('click', closeAll);
70
+ }, []);
71
+
72
+ // Filter and Sort Data
73
+ const processedData = useMemo(() => {
74
+ let result = [...data];
75
+
76
+ // 1. Search Filter
77
+ if (searchTerm) {
78
+ const query = searchTerm.toLowerCase();
79
+ result = result.filter(
80
+ (row) =>
81
+ row.name.toLowerCase().includes(query) ||
82
+ row.email.toLowerCase().includes(query) ||
83
+ row.role.toLowerCase().includes(query)
84
+ );
85
+ }
86
+
87
+ // 2. Status Filter
88
+ if (statusFilter !== 'All') {
89
+ result = result.filter((row) => row.status === statusFilter);
90
+ }
91
+
92
+ // 3. Sorting
93
+ if (sortField) {
94
+ result.sort((a, b) => {
95
+ const aVal = String(a[sortField] || '').toLowerCase();
96
+ const bVal = String(b[sortField] || '').toLowerCase();
97
+
98
+ if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
99
+ if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
100
+ return 0;
101
+ });
102
+ }
103
+
104
+ return result;
105
+ }, [data, searchTerm, statusFilter, sortField, sortDirection]);
106
+
107
+ // Paginate Data
108
+ const totalPages = Math.ceil(processedData.length / itemsPerPage);
109
+ const paginatedData = useMemo(() => {
110
+ if (!showPagination) return processedData;
111
+ const startIndex = (currentPage - 1) * itemsPerPage;
112
+ return processedData.slice(startIndex, startIndex + itemsPerPage);
113
+ }, [processedData, currentPage, itemsPerPage, showPagination]);
114
+
115
+ // Animation constants
116
+ const containerVariants = {
117
+ hidden: { opacity: 0 },
118
+ show: {
119
+ opacity: 1,
120
+ transition: { staggerChildren: 0.04 }
121
+ }
122
+ };
123
+
124
+ const rowVariants = {
125
+ hidden: { opacity: 0, y: 8 },
126
+ show: { opacity: 1, y: 0 }
127
+ };
128
+
129
+ return (
130
+ <div className={`w-full flex flex-col gap-4 overflow-visible ${className}`}>
131
+
132
+ {/* Search & Filter Controls */}
133
+ {showControls && (
134
+ <TableControls
135
+ searchTerm={searchTerm}
136
+ onSearchChange={setSearchTerm}
137
+ statusFilter={statusFilter}
138
+ onStatusFilterChange={setStatusFilter}
139
+ disabled={disabled || isLoading}
140
+ />
141
+ )}
142
+
143
+ {/* Main Table Container */}
144
+ <div className="w-full overflow-visible rounded-2xl glass border border-border-app bg-bg-card/90 shadow-md relative z-10">
145
+ <div className="w-full overflow-x-auto relative">
146
+ <table className="w-full border-collapse text-left text-sm text-text-muted">
147
+ <thead>
148
+ <tr className="border-b border-border-app/50 text-xs font-semibold uppercase tracking-wider text-text-muted bg-bg-app/30">
149
+ <th className="p-4 cursor-pointer select-none hover:text-text-main" onClick={() => handleSort('name')}>
150
+ <span className="flex items-center gap-1.5">
151
+ Nombre <ArrowUpDown className="w-3.5 h-3.5" />
152
+ </span>
153
+ </th>
154
+ <th className="p-4">Email</th>
155
+ <th className="p-4 cursor-pointer select-none hover:text-text-main" onClick={() => handleSort('role')}>
156
+ <span className="flex items-center gap-1.5">
157
+ Rol <ArrowUpDown className="w-3.5 h-3.5" />
158
+ </span>
159
+ </th>
160
+ <th className="p-4 cursor-pointer select-none hover:text-text-main" onClick={() => handleSort('status')}>
161
+ <span className="flex items-center gap-1.5">
162
+ Estado <ArrowUpDown className="w-3.5 h-3.5" />
163
+ </span>
164
+ </th>
165
+ <th className="p-4 text-right">Acciones</th>
166
+ </tr>
167
+ </thead>
168
+
169
+ {/* Staggered Rows */}
170
+ <motion.tbody
171
+ key={`${currentPage}-${statusFilter}-${searchTerm}`}
172
+ variants={containerVariants}
173
+ initial="hidden"
174
+ animate="show"
175
+ className="divide-y divide-border-app/30 overflow-visible"
176
+ >
177
+ {isLoading ? (
178
+ Array(itemsPerPage).fill(0).map((_, idx) => (
179
+ <tr key={idx} className="border-b border-border-app/30 bg-bg-card/10">
180
+ <td className="p-4 flex items-center gap-3">
181
+ <Skeleton variant="circle" className="w-10 h-10 flex-shrink-0" />
182
+ <Skeleton variant="text" className="w-24 h-4" />
183
+ </td>
184
+ <td className="p-4">
185
+ <Skeleton variant="text" className="w-32 h-3" />
186
+ </td>
187
+ <td className="p-4">
188
+ <Skeleton variant="text" className="w-20 h-4" />
189
+ </td>
190
+ <td className="p-4">
191
+ <Skeleton variant="text" className="w-16 h-5 rounded-full" />
192
+ </td>
193
+ <td className="p-4 text-right">
194
+ <Skeleton variant="circle" className="w-6 h-6 inline-block" />
195
+ </td>
196
+ </tr>
197
+ ))
198
+ ) : paginatedData.length === 0 ? (
199
+ <tr>
200
+ <td colSpan={5} className="p-8 text-center text-xs font-bold text-text-muted italic">
201
+ No se encontraron registros que coincidan con la búsqueda
202
+ </td>
203
+ </tr>
204
+ ) : (
205
+ paginatedData.map((row) => (
206
+ <motion.tr
207
+ key={row.id}
208
+ variants={rowVariants}
209
+ className="hover:bg-bg-app hover:text-text-main transition-colors duration-200 group relative overflow-visible"
210
+ >
211
+ {/* Name & larger avatar preview */}
212
+ <td className="p-4 font-bold text-text-main flex items-center gap-3">
213
+ <div className="w-10 h-10 rounded-full border border-border-app/40 overflow-hidden bg-accent/10 flex-shrink-0 flex items-center justify-center shadow-xs">
214
+ {row.avatarUrl ? (
215
+ <img src={row.avatarUrl} alt={row.name} className="w-full h-full object-cover" />
216
+ ) : (
217
+ <span className="text-xs font-bold text-accent">
218
+ {row.name.charAt(0)}
219
+ </span>
220
+ )}
221
+ </div>
222
+ <span>{row.name}</span>
223
+ </td>
224
+
225
+ <td className="p-4 font-mono text-xs">{row.email}</td>
226
+ <td className="p-4 text-xs font-semibold">{row.role}</td>
227
+
228
+ {/* Status indicator badges */}
229
+ <td className="p-4">
230
+ <span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold ${
231
+ row.status === 'Active'
232
+ ? 'bg-emerald-500/10 text-emerald-600 border border-emerald-500/20'
233
+ : row.status === 'Inactive'
234
+ ? 'bg-red-500/10 text-red-600 border border-red-500/20'
235
+ : 'bg-amber-500/10 text-amber-600 border border-amber-500/20'
236
+ }`}>
237
+ <span className={`w-1.5 h-1.5 rounded-full ${
238
+ row.status === 'Active' ? 'bg-emerald-500' : row.status === 'Inactive' ? 'bg-red-500' : 'bg-amber-500'
239
+ }`} />
240
+ {row.status}
241
+ </span>
242
+ </td>
243
+
244
+ {/* Dropdown Action button */}
245
+ <td className="p-4 text-right relative overflow-visible">
246
+ <button
247
+ onClick={(e) => toggleDropdown(row.id, e)}
248
+ className="p-1 rounded-lg hover:bg-bg-app border border-transparent hover:border-border-app transition-colors text-text-muted hover:text-text-main cursor-pointer"
249
+ >
250
+ <MoreHorizontal className="w-4 h-4" />
251
+ </button>
252
+
253
+ {/* Actions Dropdown */}
254
+ <AnimatePresence>
255
+ {activeDropdownId === row.id && (
256
+ <motion.div
257
+ initial={{ opacity: 0, scale: 0.9, y: 5 }}
258
+ animate={{ opacity: 1, scale: 1, y: 0 }}
259
+ exit={{ opacity: 0, scale: 0.9, y: 5, transition: { duration: 0.1 } }}
260
+ className="absolute right-4 mt-2 w-36 glass bg-bg-card rounded-xl border border-border-app shadow-2xl p-1 z-50 text-left"
261
+ >
262
+ <button
263
+ onClick={() => onAction && onAction('view', row)}
264
+ className="w-full px-3 py-2 text-xs font-bold text-text-main hover:bg-accent/10 hover:text-accent rounded-lg flex items-center gap-2 cursor-pointer transition-colors duration-150"
265
+ >
266
+ <Eye className="w-3.5 h-3.5 text-accent" />
267
+ <span>Ver Detalles</span>
268
+ </button>
269
+ <button
270
+ onClick={() => onAction && onAction('edit', row)}
271
+ className="w-full px-3 py-2 text-xs font-bold text-text-main hover:bg-accent/10 hover:text-accent rounded-lg flex items-center gap-2 cursor-pointer transition-colors duration-150"
272
+ >
273
+ <Edit2 className="w-3.5 h-3.5 text-warning" />
274
+ <span>Editar</span>
275
+ </button>
276
+ <div className="h-[1px] bg-border-app/50 my-1" />
277
+ <button
278
+ onClick={() => onAction && onAction('delete', row)}
279
+ className="w-full px-3 py-2 text-xs font-bold text-red-500 hover:bg-red-500/10 hover:text-red-500 rounded-lg flex items-center gap-2 cursor-pointer transition-colors duration-150"
280
+ >
281
+ <Trash2 className="w-3.5 h-3.5" />
282
+ <span>Eliminar</span>
283
+ </button>
284
+ </motion.div>
285
+ )}
286
+ </AnimatePresence>
287
+ </td>
288
+ </motion.tr>
289
+ ))
290
+ )}
291
+ </motion.tbody>
292
+ </table>
293
+ </div>
294
+ </div>
295
+
296
+ {/* Pagination Controls */}
297
+ {showPagination && (
298
+ <TablePagination
299
+ currentPage={currentPage}
300
+ totalPages={totalPages}
301
+ onPageChange={setCurrentPage}
302
+ />
303
+ )}
304
+
305
+ </div>
306
+ );
307
+ };
@@ -0,0 +1,124 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { AlertCircle, CheckCircle, Info, AlertTriangle, X } from 'lucide-react';
4
+
5
+ export type AlertVariant = 'success' | 'info' | 'warning' | 'danger';
6
+
7
+ export interface AlertBannerProps {
8
+ title?: string;
9
+ children: React.ReactNode;
10
+ variant?: AlertVariant;
11
+ dismissible?: boolean;
12
+ onDismiss?: () => void;
13
+ icon?: React.ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ const variantConfig: Record<AlertVariant, { bg: string; border: string; text: string; icon: React.ReactNode }> = {
18
+ success: {
19
+ bg: 'bg-green-500/10',
20
+ border: 'border-green-500/25',
21
+ text: 'text-green-600 dark:text-green-400',
22
+ icon: <CheckCircle className="w-5 h-5" />
23
+ },
24
+ info: {
25
+ bg: 'bg-blue-500/10',
26
+ border: 'border-blue-500/25',
27
+ text: 'text-blue-600 dark:text-blue-400',
28
+ icon: <Info className="w-5 h-5" />
29
+ },
30
+ warning: {
31
+ bg: 'bg-yellow-500/10',
32
+ border: 'border-yellow-500/25',
33
+ text: 'text-yellow-600 dark:text-yellow-400',
34
+ icon: <AlertTriangle className="w-5 h-5" />
35
+ },
36
+ danger: {
37
+ bg: 'bg-red-500/10',
38
+ border: 'border-red-500/25',
39
+ text: 'text-red-600 dark:text-red-400',
40
+ icon: <AlertCircle className="w-5 h-5" />
41
+ }
42
+ };
43
+
44
+ export const AlertBanner: React.FC<AlertBannerProps> = ({
45
+ title,
46
+ children,
47
+ variant = 'info',
48
+ dismissible = false,
49
+ onDismiss,
50
+ icon,
51
+ className = ''
52
+ }) => {
53
+ const config = variantConfig[variant];
54
+
55
+ return (
56
+ <motion.div
57
+ initial={{ opacity: 0, y: -8, scale: 0.98 }}
58
+ animate={{ opacity: 1, y: 0, scale: 1 }}
59
+ exit={{ opacity: 0, y: -8, scale: 0.98 }}
60
+ transition={{ type: 'spring' as const, stiffness: 350, damping: 26 }}
61
+ className={`relative flex gap-3 p-4 rounded-2xl border backdrop-blur-sm ${config.bg} ${config.border} ${className}`}
62
+ role="alert"
63
+ >
64
+ <div className={`flex-shrink-0 mt-0.5 ${config.text}`}>
65
+ {icon || config.icon}
66
+ </div>
67
+
68
+ <div className="flex-1 min-w-0">
69
+ {title && (
70
+ <h4 className={`font-extrabold text-sm mb-1 font-display ${config.text}`}>
71
+ {title}
72
+ </h4>
73
+ )}
74
+ <div className="text-sm text-text-main leading-relaxed">
75
+ {children}
76
+ </div>
77
+ </div>
78
+
79
+ {dismissible && onDismiss && (
80
+ <button
81
+ type="button"
82
+ onClick={onDismiss}
83
+ className="flex-shrink-0 p-1 rounded-lg text-text-muted hover:text-text-main hover:bg-bg-app/60 transition-colors cursor-pointer"
84
+ aria-label="Cerrar alerta"
85
+ >
86
+ <X className="w-4 h-4" />
87
+ </button>
88
+ )}
89
+ </motion.div>
90
+ );
91
+ };
92
+
93
+ export interface AlertStackProps {
94
+ alerts: Array<{
95
+ id: string;
96
+ title?: string;
97
+ message: string;
98
+ variant?: AlertVariant;
99
+ }>;
100
+ onDismiss?: (id: string) => void;
101
+ className?: string;
102
+ }
103
+
104
+ export const AlertStack: React.FC<AlertStackProps> = ({
105
+ alerts,
106
+ onDismiss,
107
+ className = ''
108
+ }) => (
109
+ <div className={`flex flex-col gap-3 w-full ${className}`}>
110
+ <AnimatePresence mode="popLayout">
111
+ {alerts.map((alert) => (
112
+ <AlertBanner
113
+ key={alert.id}
114
+ title={alert.title}
115
+ variant={alert.variant}
116
+ dismissible={!!onDismiss}
117
+ onDismiss={() => onDismiss?.(alert.id)}
118
+ >
119
+ {alert.message}
120
+ </AlertBanner>
121
+ ))}
122
+ </AnimatePresence>
123
+ </div>
124
+ );
@@ -0,0 +1,95 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { ChevronDown } from 'lucide-react';
4
+
5
+ export interface AccordionItem {
6
+ id: string | number;
7
+ title: string;
8
+ content: React.ReactNode;
9
+ }
10
+
11
+ export interface AnimatedAccordionProps {
12
+ items: AccordionItem[];
13
+ allowMultiple?: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ export const AnimatedAccordion: React.FC<AnimatedAccordionProps> = ({
18
+ items,
19
+ allowMultiple = false,
20
+ className = ''
21
+ }) => {
22
+ const [openIds, setOpenIds] = useState<Set<string | number>>(new Set());
23
+
24
+ const handleToggle = (id: string | number) => {
25
+ const nextOpen = new Set(openIds);
26
+ if (nextOpen.has(id)) {
27
+ nextOpen.delete(id);
28
+ } else {
29
+ if (!allowMultiple) {
30
+ nextOpen.clear();
31
+ }
32
+ nextOpen.add(id);
33
+ }
34
+ setOpenIds(nextOpen);
35
+ };
36
+
37
+ return (
38
+ <div className={`flex flex-col gap-3 w-full ${className}`}>
39
+ {items.map((item) => {
40
+ const isOpen = openIds.has(item.id);
41
+ return (
42
+ <div
43
+ key={item.id}
44
+ className="border border-border-app rounded-2xl overflow-hidden bg-bg-card transition-all duration-300 shadow-sm"
45
+ >
46
+ {/* Accordion Header */}
47
+ <button
48
+ onClick={() => handleToggle(item.id)}
49
+ className="w-full flex items-center justify-between p-5 text-left font-bold text-text-main hover:bg-bg-app/50 transition-colors duration-200 cursor-pointer"
50
+ >
51
+ <span>{item.title}</span>
52
+ <motion.div
53
+ animate={{ rotate: isOpen ? 180 : 0 }}
54
+ transition={{ duration: 0.2 }}
55
+ className="text-text-muted"
56
+ >
57
+ <ChevronDown className="w-5 h-5" />
58
+ </motion.div>
59
+ </button>
60
+
61
+ {/* Accordion Content wrapper */}
62
+ <AnimatePresence initial={false}>
63
+ {isOpen && (
64
+ <motion.div
65
+ initial={{ height: 0, opacity: 0 }}
66
+ animate={{
67
+ height: 'auto',
68
+ opacity: 1,
69
+ transition: {
70
+ height: { type: 'spring', stiffness: 300, damping: 25 },
71
+ opacity: { duration: 0.2, delay: 0.05 }
72
+ }
73
+ }}
74
+ exit={{
75
+ height: 0,
76
+ opacity: 0,
77
+ transition: {
78
+ height: { type: 'spring', stiffness: 300, damping: 25 },
79
+ opacity: { duration: 0.15 }
80
+ }
81
+ }}
82
+ className="overflow-hidden"
83
+ >
84
+ <div className="p-5 pt-0 text-sm text-text-muted border-t border-border-app/50 leading-relaxed">
85
+ {item.content}
86
+ </div>
87
+ </motion.div>
88
+ )}
89
+ </AnimatePresence>
90
+ </div>
91
+ );
92
+ })}
93
+ </div>
94
+ );
95
+ };