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