alexui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/components/ActionTable.tsx +307 -0
- package/components/AlertBanner.tsx +124 -0
- package/components/AnimatedAccordion.tsx +95 -0
- package/components/Autocomplete.tsx +144 -0
- package/components/Avatar.tsx +123 -0
- package/components/Badge.tsx +80 -0
- package/components/Breadcrumb.tsx +74 -0
- package/components/Calendar.tsx +340 -0
- package/components/Card3D.tsx +117 -0
- package/components/Carousel3D.tsx +193 -0
- package/components/CascadeSelect.tsx +232 -0
- package/components/ChartShowcase.tsx +700 -0
- package/components/Checkbox.tsx +212 -0
- package/components/ChipsInput.tsx +152 -0
- package/components/CircularKnob.tsx +240 -0
- package/components/CodeVisualizer.tsx +67 -0
- package/components/Collapsible.tsx +72 -0
- package/components/ColorThemeManager.tsx +458 -0
- package/components/CommandMenu.tsx +191 -0
- package/components/ConfirmDialog.tsx +152 -0
- package/components/ContextMenu.tsx +192 -0
- package/components/DashboardLayout.tsx +115 -0
- package/components/DatePicker.tsx +108 -0
- package/components/Divider.tsx +67 -0
- package/components/Dock.tsx +93 -0
- package/components/DragDropLists.tsx +160 -0
- package/components/Drawer.tsx +161 -0
- package/components/DropdownPlus.tsx +304 -0
- package/components/EmptyState.tsx +49 -0
- package/components/ErrorPage.tsx +62 -0
- package/components/FileDropzone.tsx +206 -0
- package/components/ForgotPassword.tsx +137 -0
- package/components/FormField.tsx +81 -0
- package/components/GlassButton.tsx +56 -0
- package/components/GlassCard.tsx +82 -0
- package/components/GlassInput.tsx +96 -0
- package/components/GlassmorphicModal.tsx +108 -0
- package/components/GlowInput.tsx +111 -0
- package/components/GlowSelect.tsx +203 -0
- package/components/GlowTextArea.tsx +105 -0
- package/components/HorizontalTimeline.tsx +121 -0
- package/components/HoverCard.tsx +105 -0
- package/components/ImageLightbox.tsx +259 -0
- package/components/InputGroup.tsx +118 -0
- package/components/InputOTP.tsx +147 -0
- package/components/InteractiveNavbar.tsx +266 -0
- package/components/InteractiveSidebar.tsx +211 -0
- package/components/Kbd.tsx +51 -0
- package/components/LiteYouTube.tsx +118 -0
- package/components/LoaderCollection.tsx +368 -0
- package/components/LoginForm.tsx +192 -0
- package/components/MagneticButton.tsx +101 -0
- package/components/MaskedInput.tsx +79 -0
- package/components/MentionInput.tsx +413 -0
- package/components/MorphingSwitch.tsx +86 -0
- package/components/MultiSelect.tsx +158 -0
- package/components/NumberInput.tsx +203 -0
- package/components/Panel.tsx +104 -0
- package/components/PasswordInput.tsx +203 -0
- package/components/Popover.tsx +91 -0
- package/components/PricingTable.tsx +113 -0
- package/components/ProgressBar.tsx +152 -0
- package/components/RadioButton.tsx +211 -0
- package/components/Rating.tsx +82 -0
- package/components/ResizablePanel.tsx +114 -0
- package/components/ScrollPanel.tsx +103 -0
- package/components/SettingsPage.tsx +154 -0
- package/components/SignupForm.tsx +182 -0
- package/components/Skeleton.tsx +41 -0
- package/components/Slider.tsx +95 -0
- package/components/SlidingTabs.tsx +54 -0
- package/components/SortableList.tsx +91 -0
- package/components/SpeedDial.tsx +134 -0
- package/components/Spinner.tsx +40 -0
- package/components/Stepper.tsx +124 -0
- package/components/TabMenu.tsx +72 -0
- package/components/TableControls.tsx +77 -0
- package/components/TablePagination.tsx +88 -0
- package/components/TextEditor.tsx +329 -0
- package/components/TextReveal.tsx +99 -0
- package/components/ThemeSwitcher.tsx +133 -0
- package/components/TimelineGSAP.tsx +164 -0
- package/components/ToastSystem.tsx +110 -0
- package/components/ToggleButton.tsx +79 -0
- package/components/Tooltip.tsx +121 -0
- package/components/Tree.tsx +138 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +110 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +76 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +60 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +38 -0
- package/dist/tui/browse.d.ts +3 -0
- package/dist/tui/browse.js +139 -0
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/format.js +52 -0
- package/dist/tui/main.d.ts +1 -0
- package/dist/tui/main.js +86 -0
- package/dist/tui/panels.d.ts +9 -0
- package/dist/tui/panels.js +50 -0
- package/dist/tui/theme.d.ts +28 -0
- package/dist/tui/theme.js +76 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +24 -0
- package/dist/utils/copy.d.ts +9 -0
- package/dist/utils/copy.js +43 -0
- package/dist/utils/cwd.d.ts +6 -0
- package/dist/utils/cwd.js +30 -0
- package/dist/utils/deps.d.ts +1 -0
- package/dist/utils/deps.js +19 -0
- package/dist/utils/project.d.ts +5 -0
- package/dist/utils/project.js +30 -0
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +24 -0
- package/package.json +52 -0
- package/registry.json +1133 -0
- package/templates/theme.css +81 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface CalendarProps {
|
|
6
|
+
value?: Date | null;
|
|
7
|
+
onChange?: (date: Date | null) => void;
|
|
8
|
+
selectsRange?: boolean;
|
|
9
|
+
startDate?: Date | null;
|
|
10
|
+
endDate?: Date | null;
|
|
11
|
+
onChangeRange?: (start: Date | null, end: Date | null) => void;
|
|
12
|
+
minDate?: Date;
|
|
13
|
+
maxDate?: Date;
|
|
14
|
+
excludeDates?: Date[];
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const WEEKDAYS = ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'];
|
|
20
|
+
const MONTHS = [
|
|
21
|
+
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
|
22
|
+
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const Calendar: React.FC<CalendarProps> = ({
|
|
26
|
+
value = null,
|
|
27
|
+
onChange,
|
|
28
|
+
selectsRange = false,
|
|
29
|
+
startDate = null,
|
|
30
|
+
endDate = null,
|
|
31
|
+
onChangeRange,
|
|
32
|
+
minDate,
|
|
33
|
+
maxDate,
|
|
34
|
+
excludeDates = [],
|
|
35
|
+
disabled = false,
|
|
36
|
+
className = ''
|
|
37
|
+
}) => {
|
|
38
|
+
const [currentDate, setCurrentDate] = useState(() => {
|
|
39
|
+
if (selectsRange && startDate) return new Date(startDate);
|
|
40
|
+
if (value) return new Date(value);
|
|
41
|
+
return new Date();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const [viewMode, setViewMode] = useState<'days' | 'months' | 'years'>('days');
|
|
45
|
+
|
|
46
|
+
const currentYear = currentDate.getFullYear();
|
|
47
|
+
const currentMonth = currentDate.getMonth();
|
|
48
|
+
|
|
49
|
+
const handlePrevMonth = () => {
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
setCurrentDate(new Date(currentYear, currentMonth - 1, 1));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleNextMonth = () => {
|
|
55
|
+
if (disabled) return;
|
|
56
|
+
setCurrentDate(new Date(currentYear, currentMonth + 1, 1));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getDaysInMonth = (year: number, month: number) => {
|
|
60
|
+
return new Date(year, month + 1, 0).getDate();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getFirstDayOfMonth = (year: number, month: number) => {
|
|
64
|
+
return new Date(year, month, 1).getDay();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const isDateBlocked = (date: Date) => {
|
|
68
|
+
if (minDate && new Date(date.getFullYear(), date.getMonth(), date.getDate()) < new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())) return true;
|
|
69
|
+
if (maxDate && new Date(date.getFullYear(), date.getMonth(), date.getDate()) > new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate())) return true;
|
|
70
|
+
return excludeDates.some(
|
|
71
|
+
(d) => d.getDate() === date.getDate() &&
|
|
72
|
+
d.getMonth() === date.getMonth() &&
|
|
73
|
+
d.getFullYear() === date.getFullYear()
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isSameDay = (d1: Date | null, d2: Date | null) => {
|
|
78
|
+
if (!d1 || !d2) return false;
|
|
79
|
+
return d1.getDate() === d2.getDate() &&
|
|
80
|
+
d1.getMonth() === d2.getMonth() &&
|
|
81
|
+
d1.getFullYear() === d2.getFullYear();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isBetweenDays = (date: Date, start: Date | null, end: Date | null) => {
|
|
85
|
+
if (!start || !end) return false;
|
|
86
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
87
|
+
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
|
|
88
|
+
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();
|
|
89
|
+
return d > s && d < e;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleDayClick = (day: number, isCurrentMonth: 'prev' | 'current' | 'next') => {
|
|
93
|
+
if (disabled) return;
|
|
94
|
+
|
|
95
|
+
let clickedDate: Date;
|
|
96
|
+
if (isCurrentMonth === 'prev') {
|
|
97
|
+
clickedDate = new Date(currentYear, currentMonth - 1, day);
|
|
98
|
+
} else if (isCurrentMonth === 'next') {
|
|
99
|
+
clickedDate = new Date(currentYear, currentMonth + 1, day);
|
|
100
|
+
} else {
|
|
101
|
+
clickedDate = new Date(currentYear, currentMonth, day);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isDateBlocked(clickedDate)) return;
|
|
105
|
+
|
|
106
|
+
if (selectsRange) {
|
|
107
|
+
if (!startDate || (startDate && endDate)) {
|
|
108
|
+
if (onChangeRange) onChangeRange(clickedDate, null);
|
|
109
|
+
} else {
|
|
110
|
+
if (clickedDate < startDate) {
|
|
111
|
+
if (onChangeRange) onChangeRange(clickedDate, null);
|
|
112
|
+
} else {
|
|
113
|
+
if (onChangeRange) onChangeRange(startDate, clickedDate);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
if (onChange) onChange(clickedDate);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Generate 42 cells (6 rows * 7 days)
|
|
122
|
+
const renderCells = () => {
|
|
123
|
+
const firstDayIndex = getFirstDayOfMonth(currentYear, currentMonth);
|
|
124
|
+
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
|
|
125
|
+
const daysInPrevMonth = getDaysInMonth(currentYear, currentMonth - 1);
|
|
126
|
+
|
|
127
|
+
const cells: React.ReactNode[] = [];
|
|
128
|
+
|
|
129
|
+
// Prev month days
|
|
130
|
+
for (let i = firstDayIndex - 1; i >= 0; i--) {
|
|
131
|
+
const day = daysInPrevMonth - i;
|
|
132
|
+
const date = new Date(currentYear, currentMonth - 1, day);
|
|
133
|
+
const blocked = isDateBlocked(date);
|
|
134
|
+
cells.push(
|
|
135
|
+
<button
|
|
136
|
+
key={`prev-${day}`}
|
|
137
|
+
onClick={() => handleDayClick(day, 'prev')}
|
|
138
|
+
disabled={blocked || disabled}
|
|
139
|
+
type="button"
|
|
140
|
+
className={`h-9 w-9 text-xs text-text-muted/40 font-medium rounded-lg hover:bg-white/5 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed`}
|
|
141
|
+
>
|
|
142
|
+
{day}
|
|
143
|
+
</button>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Current month days
|
|
148
|
+
for (let day = 1; day <= daysInCurrentMonth; day++) {
|
|
149
|
+
const date = new Date(currentYear, currentMonth, day);
|
|
150
|
+
const isSelected = selectsRange
|
|
151
|
+
? isSameDay(date, startDate) || isSameDay(date, endDate)
|
|
152
|
+
: isSameDay(date, value);
|
|
153
|
+
const isInRange = selectsRange && isBetweenDays(date, startDate, endDate);
|
|
154
|
+
const isStart = selectsRange && isSameDay(date, startDate);
|
|
155
|
+
const isEnd = selectsRange && isSameDay(date, endDate);
|
|
156
|
+
const blocked = isDateBlocked(date);
|
|
157
|
+
const isToday = isSameDay(date, new Date());
|
|
158
|
+
|
|
159
|
+
cells.push(
|
|
160
|
+
<button
|
|
161
|
+
key={`curr-${day}`}
|
|
162
|
+
onClick={() => handleDayClick(day, 'current')}
|
|
163
|
+
disabled={blocked || disabled}
|
|
164
|
+
type="button"
|
|
165
|
+
className={`h-9 w-9 text-xs font-bold rounded-lg relative transition-all duration-200 cursor-pointer select-none disabled:opacity-25 disabled:cursor-not-allowed ${
|
|
166
|
+
isSelected
|
|
167
|
+
? 'bg-accent text-white shadow-[0_0_12px_var(--color-accent)]'
|
|
168
|
+
: isInRange
|
|
169
|
+
? 'bg-accent/15 text-accent border border-accent/20 rounded-none first:rounded-l-lg last:rounded-r-lg'
|
|
170
|
+
: 'text-text-main hover:bg-white/5'
|
|
171
|
+
} ${isToday && !isSelected ? 'border border-accent/40 text-accent font-extrabold' : ''}`}
|
|
172
|
+
>
|
|
173
|
+
{day}
|
|
174
|
+
{isStart && endDate && (
|
|
175
|
+
<div className="absolute right-0 top-0 bottom-0 w-1/2 bg-accent/10 -z-10" />
|
|
176
|
+
)}
|
|
177
|
+
{isEnd && startDate && (
|
|
178
|
+
<div className="absolute left-0 top-0 bottom-0 w-1/2 bg-accent/10 -z-10" />
|
|
179
|
+
)}
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Next month days to complete 42 cells
|
|
185
|
+
const remainingCells = 42 - cells.length;
|
|
186
|
+
for (let day = 1; day <= remainingCells; day++) {
|
|
187
|
+
const date = new Date(currentYear, currentMonth + 1, day);
|
|
188
|
+
const blocked = isDateBlocked(date);
|
|
189
|
+
cells.push(
|
|
190
|
+
<button
|
|
191
|
+
key={`next-${day}`}
|
|
192
|
+
onClick={() => handleDayClick(day, 'next')}
|
|
193
|
+
disabled={blocked || disabled}
|
|
194
|
+
type="button"
|
|
195
|
+
className={`h-9 w-9 text-xs text-text-muted/40 font-medium rounded-lg hover:bg-white/5 transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed`}
|
|
196
|
+
>
|
|
197
|
+
{day}
|
|
198
|
+
</button>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return cells;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleMonthSelect = (mIdx: number) => {
|
|
206
|
+
setCurrentDate(new Date(currentYear, mIdx, 1));
|
|
207
|
+
setViewMode('days');
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleYearSelect = (year: number) => {
|
|
211
|
+
setCurrentDate(new Date(year, currentMonth, 1));
|
|
212
|
+
setViewMode('days');
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const renderMonthGrid = () => {
|
|
216
|
+
return (
|
|
217
|
+
<div className="grid grid-cols-3 gap-2 py-4">
|
|
218
|
+
{MONTHS.map((m, idx) => (
|
|
219
|
+
<button
|
|
220
|
+
key={m}
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() => handleMonthSelect(idx)}
|
|
223
|
+
className={`py-3 text-xs font-bold rounded-xl hover:bg-white/5 transition-all text-text-main cursor-pointer ${
|
|
224
|
+
currentMonth === idx ? 'bg-accent/20 text-accent border border-accent/30' : ''
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
{m}
|
|
228
|
+
</button>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const renderYearGrid = () => {
|
|
235
|
+
const years: number[] = [];
|
|
236
|
+
const baseYear = Math.floor(currentYear / 12) * 12;
|
|
237
|
+
for (let i = -1; i < 11; i++) {
|
|
238
|
+
years.push(baseYear + i);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="grid grid-cols-3 gap-2 py-4">
|
|
243
|
+
{years.map((y) => (
|
|
244
|
+
<button
|
|
245
|
+
key={y}
|
|
246
|
+
type="button"
|
|
247
|
+
onClick={() => handleYearSelect(y)}
|
|
248
|
+
className={`py-3 text-xs font-bold rounded-xl hover:bg-white/5 transition-all text-text-main cursor-pointer ${
|
|
249
|
+
currentYear === y ? 'bg-accent/20 text-accent border border-accent/30' : ''
|
|
250
|
+
}`}
|
|
251
|
+
>
|
|
252
|
+
{y}
|
|
253
|
+
</button>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<div className={`p-4 bg-bg-card/40 border border-border-app/40 rounded-2xl w-[290px] select-none ${
|
|
261
|
+
disabled ? 'opacity-40 pointer-events-none' : ''
|
|
262
|
+
} ${className}`}>
|
|
263
|
+
|
|
264
|
+
{/* Calendar Header */}
|
|
265
|
+
<div className="flex items-center justify-between pb-3 border-b border-border-app/30">
|
|
266
|
+
<div className="flex items-center gap-1.5">
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
onClick={() => setViewMode(viewMode === 'months' ? 'days' : 'months')}
|
|
270
|
+
className="text-xs font-black text-text-main hover:text-accent cursor-pointer transition-colors"
|
|
271
|
+
>
|
|
272
|
+
{MONTHS[currentMonth]}
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
onClick={() => setViewMode(viewMode === 'years' ? 'days' : 'years')}
|
|
277
|
+
className="text-xs font-mono font-bold text-text-muted hover:text-accent cursor-pointer transition-colors"
|
|
278
|
+
>
|
|
279
|
+
{currentYear}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{viewMode === 'days' && (
|
|
284
|
+
<div className="flex items-center gap-1">
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={handlePrevMonth}
|
|
288
|
+
className="p-1 rounded-lg hover:bg-white/5 text-text-muted hover:text-text-main transition-colors cursor-pointer"
|
|
289
|
+
>
|
|
290
|
+
<ChevronLeft size={16} />
|
|
291
|
+
</button>
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
onClick={handleNextMonth}
|
|
295
|
+
className="p-1 rounded-lg hover:bg-white/5 text-text-muted hover:text-text-main transition-colors cursor-pointer"
|
|
296
|
+
>
|
|
297
|
+
<ChevronRight size={16} />
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* Calendar Body */}
|
|
304
|
+
{viewMode === 'days' && (
|
|
305
|
+
<>
|
|
306
|
+
<div className="grid grid-cols-7 gap-1 text-center py-2">
|
|
307
|
+
{WEEKDAYS.map((day) => (
|
|
308
|
+
<span key={day} className="text-[10px] font-black uppercase text-text-muted/60 tracking-wider">
|
|
309
|
+
{day}
|
|
310
|
+
</span>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
<div className="grid grid-cols-7 gap-1 text-center">
|
|
314
|
+
{renderCells()}
|
|
315
|
+
</div>
|
|
316
|
+
</>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{viewMode === 'months' && renderMonthGrid()}
|
|
320
|
+
{viewMode === 'years' && renderYearGrid()}
|
|
321
|
+
|
|
322
|
+
{/* Clear/Reset Range support */}
|
|
323
|
+
{selectsRange && (startDate || endDate) && (
|
|
324
|
+
<div className="mt-3 pt-2 border-t border-border-app/30 flex justify-between items-center">
|
|
325
|
+
<span className="text-[9px] font-mono text-text-muted">
|
|
326
|
+
{startDate ? `${startDate.getDate()}/${startDate.getMonth() + 1}` : ''}
|
|
327
|
+
{endDate ? ` - ${endDate.getDate()}/${endDate.getMonth() + 1}` : ''}
|
|
328
|
+
</span>
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
onClick={() => onChangeRange && onChangeRange(null, null)}
|
|
332
|
+
className="text-[10px] font-bold text-red-500 hover:text-red-400 cursor-pointer transition-colors flex items-center gap-1"
|
|
333
|
+
>
|
|
334
|
+
<X size={10} /> Limpiar
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
|
3
|
+
import { Skeleton } from './Skeleton';
|
|
4
|
+
|
|
5
|
+
export interface Card3DProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
glowColor?: string; // e.g. "rgba(99,102,241,0.15)"
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Card3D: React.FC<Card3DProps> = ({ children, className = '', glowColor = 'rgba(99, 102, 241, 0.2)', isLoading = false }) => {
|
|
13
|
+
const cardRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
// Track relative cursor position inside the card (-0.5 to 0.5 range)
|
|
16
|
+
const rotateX = useMotionValue(0);
|
|
17
|
+
const rotateY = useMotionValue(0);
|
|
18
|
+
|
|
19
|
+
// Spotlight position
|
|
20
|
+
const glowX = useMotionValue(0);
|
|
21
|
+
const glowY = useMotionValue(0);
|
|
22
|
+
const glowOpacity = useMotionValue(0);
|
|
23
|
+
|
|
24
|
+
// Smooth springs for tilt
|
|
25
|
+
const springX = useSpring(rotateX, { stiffness: 150, damping: 20 });
|
|
26
|
+
const springY = useSpring(rotateY, { stiffness: 150, damping: 20 });
|
|
27
|
+
|
|
28
|
+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
29
|
+
if (!cardRef.current) return;
|
|
30
|
+
|
|
31
|
+
const rect = cardRef.current.getBoundingClientRect();
|
|
32
|
+
const width = rect.width;
|
|
33
|
+
const height = rect.height;
|
|
34
|
+
|
|
35
|
+
// Position of cursor relative to element
|
|
36
|
+
const mouseX = e.clientX - rect.left;
|
|
37
|
+
const mouseY = e.clientY - rect.top;
|
|
38
|
+
|
|
39
|
+
// Convert to percentage offsets (-0.5 to 0.5)
|
|
40
|
+
const xPct = (mouseX / width) - 0.5;
|
|
41
|
+
const yPct = (mouseY / height) - 0.5;
|
|
42
|
+
|
|
43
|
+
// Tilt limits: maximum rotate angle
|
|
44
|
+
rotateX.set(-yPct * 20); // Tilt up/down
|
|
45
|
+
rotateY.set(xPct * 20); // Tilt left/right
|
|
46
|
+
|
|
47
|
+
// Glow coordinates
|
|
48
|
+
glowX.set(mouseX);
|
|
49
|
+
glowY.set(mouseY);
|
|
50
|
+
glowOpacity.set(1);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleMouseLeave = () => {
|
|
54
|
+
rotateX.set(0);
|
|
55
|
+
rotateY.set(0);
|
|
56
|
+
glowOpacity.set(0);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
style={{ perspective: 1000 }}
|
|
62
|
+
className="inline-block"
|
|
63
|
+
>
|
|
64
|
+
<motion.div
|
|
65
|
+
ref={cardRef}
|
|
66
|
+
onMouseMove={handleMouseMove}
|
|
67
|
+
onMouseLeave={handleMouseLeave}
|
|
68
|
+
style={{
|
|
69
|
+
rotateX: springX,
|
|
70
|
+
rotateY: springY,
|
|
71
|
+
transformStyle: 'preserve-3d',
|
|
72
|
+
}}
|
|
73
|
+
className={`relative overflow-hidden rounded-2xl border border-border-app bg-bg-card p-6 shadow-lg transition-colors duration-300 select-none ${className}`}
|
|
74
|
+
>
|
|
75
|
+
{/* Dynamic Glow Layer */}
|
|
76
|
+
<motion.div
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
top: 0,
|
|
80
|
+
left: 0,
|
|
81
|
+
width: '100%',
|
|
82
|
+
height: '100%',
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
background: useTransform(
|
|
85
|
+
[glowX, glowY, glowOpacity],
|
|
86
|
+
([x, y]) => `radial-gradient(400px circle at ${x}px ${y}px, ${glowColor}, transparent 80%)`
|
|
87
|
+
),
|
|
88
|
+
opacity: glowOpacity,
|
|
89
|
+
}}
|
|
90
|
+
className="z-10 transition-opacity duration-300"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Card Contents (can utilize transformZ to lift objects) */}
|
|
94
|
+
<div style={{ transform: 'translateZ(30px)', transformStyle: 'preserve-3d' }} className="relative z-20 w-full h-full">
|
|
95
|
+
{isLoading ? (
|
|
96
|
+
<div className="flex flex-col h-full justify-between gap-4">
|
|
97
|
+
<div>
|
|
98
|
+
<div className="flex items-center justify-between mb-4">
|
|
99
|
+
<Skeleton variant="text" className="w-16 h-3" />
|
|
100
|
+
<Skeleton variant="circle" className="w-4 h-4" />
|
|
101
|
+
</div>
|
|
102
|
+
<Skeleton variant="text" className="w-3/4 h-5 mb-2" />
|
|
103
|
+
<Skeleton variant="text" className="w-1/2 h-3" />
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex items-center justify-between mt-4 border-t border-border-app/40 pt-3">
|
|
106
|
+
<Skeleton variant="text" className="w-12 h-3" />
|
|
107
|
+
<Skeleton variant="circle" className="w-6 h-6" />
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
) : (
|
|
111
|
+
children
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</motion.div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface Carousel3DProps {
|
|
6
|
+
items: React.ReactNode[];
|
|
7
|
+
autoPlay?: boolean;
|
|
8
|
+
interval?: number;
|
|
9
|
+
className?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getRelativeOffset(index: number, current: number, length: number): number {
|
|
14
|
+
let offset = index - current;
|
|
15
|
+
const half = Math.floor(length / 2);
|
|
16
|
+
if (offset > half) offset -= length;
|
|
17
|
+
if (offset < -half) offset += length;
|
|
18
|
+
return offset;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getItemStyle(offset: number, length: number) {
|
|
22
|
+
const isCenter = offset === 0;
|
|
23
|
+
const absOffset = Math.abs(offset);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
zIndex: length - absOffset,
|
|
27
|
+
scale: isCenter ? 1 : Math.max(0.62, 1 - absOffset * 0.14),
|
|
28
|
+
opacity: isCenter ? 1 : Math.max(0.2, 0.85 - absOffset * 0.3),
|
|
29
|
+
x: `${offset * 42}%`,
|
|
30
|
+
rotateY: Math.max(-35, Math.min(35, offset * -28)),
|
|
31
|
+
translateZ: isCenter ? 0 : -70 - absOffset * 55,
|
|
32
|
+
filter: isCenter ? 'blur(0px)' : `blur(${Math.min(3, absOffset * 1.2)}px)`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Carousel3D: React.FC<Carousel3DProps> = ({
|
|
37
|
+
items,
|
|
38
|
+
autoPlay = false,
|
|
39
|
+
interval = 4000,
|
|
40
|
+
className = '',
|
|
41
|
+
disabled = false,
|
|
42
|
+
}) => {
|
|
43
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
44
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
45
|
+
|
|
46
|
+
const goNext = useCallback(() => {
|
|
47
|
+
if (disabled || items.length === 0) return;
|
|
48
|
+
setCurrentIndex((prev) => (prev + 1) % items.length);
|
|
49
|
+
}, [disabled, items.length]);
|
|
50
|
+
|
|
51
|
+
const goPrev = useCallback(() => {
|
|
52
|
+
if (disabled || items.length === 0) return;
|
|
53
|
+
setCurrentIndex((prev) => (prev - 1 + items.length) % items.length);
|
|
54
|
+
}, [disabled, items.length]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!autoPlay || disabled || items.length < 2) return;
|
|
58
|
+
const timer = window.setInterval(goNext, interval);
|
|
59
|
+
return () => window.clearInterval(timer);
|
|
60
|
+
}, [autoPlay, disabled, goNext, interval, items.length]);
|
|
61
|
+
|
|
62
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
63
|
+
if (disabled) return;
|
|
64
|
+
if (event.key === 'ArrowRight') {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
goNext();
|
|
67
|
+
}
|
|
68
|
+
if (event.key === 'ArrowLeft') {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
goPrev();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const itemStyles = useMemo(
|
|
75
|
+
() => items.map((_, index) => getItemStyle(getRelativeOffset(index, currentIndex, items.length), items.length)),
|
|
76
|
+
[items, currentIndex],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (items.length === 0) {
|
|
80
|
+
return (
|
|
81
|
+
<div className={`flex items-center justify-center h-[400px] text-text-muted text-sm ${className}`}>
|
|
82
|
+
Sin elementos en el carrusel
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={containerRef}
|
|
90
|
+
tabIndex={disabled ? -1 : 0}
|
|
91
|
+
onKeyDown={handleKeyDown}
|
|
92
|
+
role="region"
|
|
93
|
+
aria-roledescription="carousel"
|
|
94
|
+
aria-label="Carrusel 3D"
|
|
95
|
+
className={`relative w-full max-w-4xl mx-auto h-[420px] flex flex-col items-center justify-center outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded-2xl ${className}`}
|
|
96
|
+
>
|
|
97
|
+
<div className="relative w-full max-w-md h-[300px] flex items-center justify-center perspective-[1200px]">
|
|
98
|
+
<div className="relative w-full h-full transform-style-3d">
|
|
99
|
+
<AnimatePresence initial={false}>
|
|
100
|
+
{items.map((item, index) => {
|
|
101
|
+
const offset = getRelativeOffset(index, currentIndex, items.length);
|
|
102
|
+
const styles = itemStyles[index];
|
|
103
|
+
const isCenter = offset === 0;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<motion.div
|
|
107
|
+
key={index}
|
|
108
|
+
className={`absolute inset-0 select-none ${disabled ? 'cursor-not-allowed' : isCenter ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer'}`}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
if (disabled || isCenter) return;
|
|
111
|
+
setCurrentIndex(index);
|
|
112
|
+
}}
|
|
113
|
+
drag={isCenter && !disabled ? 'x' : false}
|
|
114
|
+
dragConstraints={{ left: 0, right: 0 }}
|
|
115
|
+
dragElastic={0.15}
|
|
116
|
+
onDragEnd={(_event, info) => {
|
|
117
|
+
if (disabled) return;
|
|
118
|
+
if (info.offset.x < -50) goNext();
|
|
119
|
+
else if (info.offset.x > 50) goPrev();
|
|
120
|
+
}}
|
|
121
|
+
whileHover={!disabled && isCenter ? { scale: 1.02 } : undefined}
|
|
122
|
+
animate={{
|
|
123
|
+
x: styles.x,
|
|
124
|
+
scale: styles.scale,
|
|
125
|
+
opacity: styles.opacity,
|
|
126
|
+
rotateY: styles.rotateY,
|
|
127
|
+
z: styles.translateZ,
|
|
128
|
+
zIndex: styles.zIndex,
|
|
129
|
+
filter: styles.filter,
|
|
130
|
+
}}
|
|
131
|
+
transition={{ type: 'spring', stiffness: 260, damping: 28 }}
|
|
132
|
+
style={{ transformStyle: 'preserve-3d' }}
|
|
133
|
+
>
|
|
134
|
+
<div
|
|
135
|
+
className="absolute -bottom-8 left-1/2 w-[80%] h-5 rounded-full bg-black/40 pointer-events-none"
|
|
136
|
+
style={{
|
|
137
|
+
opacity: isCenter ? 0.55 : Math.max(0.08, 0.5 - Math.abs(offset) * 0.15),
|
|
138
|
+
transform: `translateX(-50%) scale(${isCenter ? 1 : 0.75 - Math.abs(offset) * 0.08})`,
|
|
139
|
+
filter: `blur(${isCenter ? 8 : 5}px)`,
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
<div className="w-full h-full">{item}</div>
|
|
143
|
+
</motion.div>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</AnimatePresence>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="absolute top-1/2 left-0 right-0 flex justify-between px-2 -translate-y-1/2 pointer-events-none z-50">
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={goPrev}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
aria-label="Anterior"
|
|
156
|
+
className="w-11 h-11 rounded-full glass bg-bg-card/60 border border-border-app flex items-center justify-center pointer-events-auto hover:bg-accent/20 hover:text-accent transition-colors disabled:opacity-40 shadow-lg"
|
|
157
|
+
>
|
|
158
|
+
<ChevronLeft className="w-5 h-5" />
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={goNext}
|
|
163
|
+
disabled={disabled}
|
|
164
|
+
aria-label="Siguiente"
|
|
165
|
+
className="w-11 h-11 rounded-full glass bg-bg-card/60 border border-border-app flex items-center justify-center pointer-events-auto hover:bg-accent/20 hover:text-accent transition-colors disabled:opacity-40 shadow-lg"
|
|
166
|
+
>
|
|
167
|
+
<ChevronRight className="w-5 h-5" />
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{items.length > 1 && (
|
|
172
|
+
<div className="flex gap-2 mt-4 z-50" role="tablist" aria-label="Slides">
|
|
173
|
+
{items.map((_, index) => (
|
|
174
|
+
<button
|
|
175
|
+
key={index}
|
|
176
|
+
type="button"
|
|
177
|
+
role="tab"
|
|
178
|
+
aria-selected={index === currentIndex}
|
|
179
|
+
aria-label={`Ir a slide ${index + 1}`}
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
onClick={() => !disabled && setCurrentIndex(index)}
|
|
182
|
+
className={`h-2 rounded-full transition-all duration-300 ${
|
|
183
|
+
index === currentIndex
|
|
184
|
+
? 'w-6 bg-accent'
|
|
185
|
+
: 'w-2 bg-border-app hover:bg-accent/50'
|
|
186
|
+
} disabled:opacity-40`}
|
|
187
|
+
/>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
};
|