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,108 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Calendar as CalendarIcon } from 'lucide-react';
|
|
4
|
+
import { Calendar, type CalendarProps } from './Calendar';
|
|
5
|
+
|
|
6
|
+
export interface DatePickerProps extends Omit<CalendarProps, 'className'> {
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
triggerClassName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DatePicker: React.FC<DatePickerProps> = ({
|
|
13
|
+
value = null,
|
|
14
|
+
onChange,
|
|
15
|
+
selectsRange = false,
|
|
16
|
+
startDate = null,
|
|
17
|
+
endDate = null,
|
|
18
|
+
onChangeRange,
|
|
19
|
+
placeholder = 'Seleccionar fecha',
|
|
20
|
+
disabled = false,
|
|
21
|
+
className = '',
|
|
22
|
+
triggerClassName = '',
|
|
23
|
+
...calendarProps
|
|
24
|
+
}) => {
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handleOutsideClick = (e: MouseEvent) => {
|
|
30
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
31
|
+
setIsOpen(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
if (isOpen) {
|
|
35
|
+
document.addEventListener('mousedown', handleOutsideClick);
|
|
36
|
+
}
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener('mousedown', handleOutsideClick);
|
|
39
|
+
};
|
|
40
|
+
}, [isOpen]);
|
|
41
|
+
|
|
42
|
+
const formatDate = (date: Date) => {
|
|
43
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
44
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
45
|
+
const year = date.getFullYear();
|
|
46
|
+
return `${day}/${month}/${year}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getInputValue = () => {
|
|
50
|
+
if (selectsRange) {
|
|
51
|
+
if (startDate && endDate) {
|
|
52
|
+
return `${formatDate(startDate)} - ${formatDate(endDate)}`;
|
|
53
|
+
}
|
|
54
|
+
if (startDate) {
|
|
55
|
+
return `${formatDate(startDate)} - ...`;
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
return value ? formatDate(value) : '';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div ref={containerRef} className={`relative w-full min-w-[220px] ${className}`}>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
67
|
+
disabled={disabled}
|
|
68
|
+
className={`relative w-full flex items-center justify-between bg-bg-card border border-border-app rounded-xl px-4 py-3 text-sm text-left transition-all duration-300 hover:border-accent/50 ${
|
|
69
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : 'cursor-pointer'
|
|
70
|
+
} ${isOpen ? 'border-accent shadow-[0_0_12px_var(--color-accent-glow)]' : ''} ${triggerClassName}`}
|
|
71
|
+
>
|
|
72
|
+
<span className={getInputValue() ? 'text-text-main font-semibold' : 'text-text-muted font-normal'}>
|
|
73
|
+
{getInputValue() || placeholder}
|
|
74
|
+
</span>
|
|
75
|
+
<CalendarIcon size={16} className="text-text-muted flex-shrink-0" />
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
<AnimatePresence>
|
|
79
|
+
{isOpen && !disabled && (
|
|
80
|
+
<motion.div
|
|
81
|
+
initial={{ opacity: 0, y: 8, scale: 0.95 }}
|
|
82
|
+
animate={{ opacity: 1, y: 4, scale: 1 }}
|
|
83
|
+
exit={{ opacity: 0, y: 8, scale: 0.95 }}
|
|
84
|
+
transition={{ duration: 0.15 }}
|
|
85
|
+
className="absolute z-50 mt-1.5 left-0 shadow-xl rounded-2xl overflow-hidden glass border border-border-app/50"
|
|
86
|
+
>
|
|
87
|
+
<Calendar
|
|
88
|
+
value={value}
|
|
89
|
+
onChange={(date) => {
|
|
90
|
+
onChange?.(date);
|
|
91
|
+
setIsOpen(false);
|
|
92
|
+
}}
|
|
93
|
+
selectsRange={selectsRange}
|
|
94
|
+
startDate={startDate}
|
|
95
|
+
endDate={endDate}
|
|
96
|
+
onChangeRange={(start, end) => {
|
|
97
|
+
onChangeRange?.(start, end);
|
|
98
|
+
if (start && end) setIsOpen(false);
|
|
99
|
+
}}
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
{...calendarProps}
|
|
102
|
+
/>
|
|
103
|
+
</motion.div>
|
|
104
|
+
)}
|
|
105
|
+
</AnimatePresence>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface DividerProps {
|
|
5
|
+
orientation?: 'horizontal' | 'vertical';
|
|
6
|
+
label?: string;
|
|
7
|
+
variant?: 'solid' | 'dashed' | 'gradient';
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Divider: React.FC<DividerProps> = ({
|
|
12
|
+
orientation = 'horizontal',
|
|
13
|
+
label,
|
|
14
|
+
variant = 'solid',
|
|
15
|
+
className = ''
|
|
16
|
+
}) => {
|
|
17
|
+
const lineClass =
|
|
18
|
+
variant === 'dashed'
|
|
19
|
+
? 'border-dashed border-border-app'
|
|
20
|
+
: variant === 'gradient'
|
|
21
|
+
? 'bg-gradient-to-r from-transparent via-border-app to-transparent h-px border-0'
|
|
22
|
+
: 'bg-border-app';
|
|
23
|
+
|
|
24
|
+
if (orientation === 'vertical') {
|
|
25
|
+
return (
|
|
26
|
+
<motion.div
|
|
27
|
+
initial={{ scaleY: 0 }}
|
|
28
|
+
animate={{ scaleY: 1 }}
|
|
29
|
+
transition={{ type: 'spring' as const, stiffness: 300, damping: 24 }}
|
|
30
|
+
className={`w-px self-stretch min-h-[24px] ${variant === 'dashed' ? 'border-l border-border-app' : lineClass} ${className}`}
|
|
31
|
+
role="separator"
|
|
32
|
+
aria-orientation="vertical"
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (label) {
|
|
38
|
+
return (
|
|
39
|
+
<div className={`flex items-center gap-3 w-full ${className}`} role="separator">
|
|
40
|
+
<motion.div
|
|
41
|
+
initial={{ scaleX: 0 }}
|
|
42
|
+
animate={{ scaleX: 1 }}
|
|
43
|
+
className={`flex-1 h-px origin-left ${lineClass}`}
|
|
44
|
+
/>
|
|
45
|
+
<span className="text-[10px] font-extrabold uppercase tracking-widest text-text-muted font-mono whitespace-nowrap">
|
|
46
|
+
{label}
|
|
47
|
+
</span>
|
|
48
|
+
<motion.div
|
|
49
|
+
initial={{ scaleX: 0 }}
|
|
50
|
+
animate={{ scaleX: 1 }}
|
|
51
|
+
className={`flex-1 h-px origin-right ${lineClass}`}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<motion.div
|
|
59
|
+
initial={{ scaleX: 0 }}
|
|
60
|
+
animate={{ scaleX: 1 }}
|
|
61
|
+
transition={{ type: 'spring' as const, stiffness: 300, damping: 24 }}
|
|
62
|
+
className={`w-full h-px origin-left ${lineClass} ${className}`}
|
|
63
|
+
role="separator"
|
|
64
|
+
aria-orientation="horizontal"
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { motion, useMotionValue, useSpring, useTransform, MotionValue } from 'framer-motion';
|
|
3
|
+
import { Skeleton } from './Skeleton';
|
|
4
|
+
|
|
5
|
+
export interface DockProps {
|
|
6
|
+
items: {
|
|
7
|
+
icon: React.ReactNode;
|
|
8
|
+
label: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
}[];
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Dock: React.FC<DockProps> = ({ items, isLoading = false }) => {
|
|
15
|
+
const mouseX = useMotionValue(Infinity);
|
|
16
|
+
|
|
17
|
+
if (isLoading) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="glass mx-auto flex h-16 items-center gap-4 rounded-2xl px-4 shadow-xl"
|
|
21
|
+
style={{ width: 'fit-content' }}
|
|
22
|
+
>
|
|
23
|
+
{Array(5).fill(0).map((_, idx) => (
|
|
24
|
+
<Skeleton key={idx} variant="circle" className="w-10 h-10" />
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<motion.div
|
|
32
|
+
onMouseMove={(e) => mouseX.set(e.pageX)}
|
|
33
|
+
onMouseLeave={() => mouseX.set(Infinity)}
|
|
34
|
+
className="glass mx-auto flex h-16 items-end gap-4 rounded-2xl px-4 pb-3 shadow-xl transition-all duration-300"
|
|
35
|
+
style={{ width: 'fit-content' }}
|
|
36
|
+
>
|
|
37
|
+
{items.map((item, idx) => (
|
|
38
|
+
<DockIcon key={idx} mouseX={mouseX} label={item.label} onClick={item.onClick}>
|
|
39
|
+
{item.icon}
|
|
40
|
+
</DockIcon>
|
|
41
|
+
))}
|
|
42
|
+
</motion.div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface DockIconProps {
|
|
47
|
+
mouseX: MotionValue<number>;
|
|
48
|
+
label: string;
|
|
49
|
+
onClick?: () => void;
|
|
50
|
+
children: React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DockIcon: React.FC<DockIconProps> = ({ mouseX, label, onClick, children }) => {
|
|
54
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
55
|
+
|
|
56
|
+
// Calculate distance from mouse to icon center
|
|
57
|
+
const distance = useTransform(mouseX, (val) => {
|
|
58
|
+
const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
|
|
59
|
+
return val - bounds.x - bounds.width / 2;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Scale calculations: closer to mouse = larger size
|
|
63
|
+
const widthTransform = useTransform(distance, [-150, 0, 150], [40, 72, 40]);
|
|
64
|
+
const heightTransform = useTransform(distance, [-150, 0, 150], [40, 72, 40]);
|
|
65
|
+
|
|
66
|
+
const width = useSpring(widthTransform, {
|
|
67
|
+
mass: 0.1,
|
|
68
|
+
stiffness: 150,
|
|
69
|
+
damping: 12,
|
|
70
|
+
});
|
|
71
|
+
const height = useSpring(heightTransform, {
|
|
72
|
+
mass: 0.1,
|
|
73
|
+
stiffness: 150,
|
|
74
|
+
damping: 12,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<motion.div
|
|
79
|
+
ref={ref}
|
|
80
|
+
style={{ width, height }}
|
|
81
|
+
onClick={onClick}
|
|
82
|
+
className="group relative flex cursor-pointer items-center justify-center rounded-full bg-bg-card border border-border-app text-text-main shadow-md hover:border-accent hover:shadow-[0_0_15px_rgba(99,102,241,0.3)] transition-colors duration-200"
|
|
83
|
+
>
|
|
84
|
+
{/* Tooltip */}
|
|
85
|
+
<span className="absolute -top-10 scale-0 rounded bg-text-main px-2 py-1 text-xs text-bg-app transition-all duration-200 group-hover:scale-100 shadow-md">
|
|
86
|
+
{label}
|
|
87
|
+
</span>
|
|
88
|
+
<div className="flex h-1/2 w-1/2 items-center justify-center">
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
</motion.div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface DragItem {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status: string; // the column/list identifier
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ColumnDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
colorClass: string; // Tailwind colors like 'bg-accent/10 text-accent border-accent/20'
|
|
15
|
+
headerColor: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DragDropListsProps {
|
|
19
|
+
initialItems: DragItem[];
|
|
20
|
+
columns: ColumnDefinition[];
|
|
21
|
+
onChange?: (items: DragItem[]) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DragDropLists: React.FC<DragDropListsProps> = ({
|
|
27
|
+
initialItems,
|
|
28
|
+
columns,
|
|
29
|
+
onChange,
|
|
30
|
+
className = '',
|
|
31
|
+
disabled = false
|
|
32
|
+
}) => {
|
|
33
|
+
const [items, setItems] = useState<DragItem[]>(initialItems);
|
|
34
|
+
const [draggedId, setDraggedId] = useState<string | null>(null);
|
|
35
|
+
const [isOverColumn, setIsOverColumn] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
38
|
+
if (disabled) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
setDraggedId(id);
|
|
43
|
+
e.dataTransfer.setData('text/plain', id);
|
|
44
|
+
// Visual drag feedback
|
|
45
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleDragOver = (e: React.DragEvent, columnId: string) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
if (isOverColumn !== columnId) {
|
|
52
|
+
setIsOverColumn(columnId);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
// Reset over column state when leaving
|
|
59
|
+
setIsOverColumn(null);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleDrop = (e: React.DragEvent, targetColumnId: string) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
setIsOverColumn(null);
|
|
65
|
+
if (disabled) return;
|
|
66
|
+
|
|
67
|
+
const id = e.dataTransfer.getData('text/plain') || draggedId;
|
|
68
|
+
if (!id) return;
|
|
69
|
+
|
|
70
|
+
const updatedItems = items.map((item) => {
|
|
71
|
+
if (item.id === id) {
|
|
72
|
+
return { ...item, status: targetColumnId };
|
|
73
|
+
}
|
|
74
|
+
return item;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
setItems(updatedItems);
|
|
78
|
+
setDraggedId(null);
|
|
79
|
+
if (onChange) {
|
|
80
|
+
onChange(updatedItems);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleDragEnd = () => {
|
|
85
|
+
setDraggedId(null);
|
|
86
|
+
setIsOverColumn(null);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 w-full ${className}`}>
|
|
91
|
+
{columns.map((col) => {
|
|
92
|
+
const colItems = items.filter((item) => item.status === col.id);
|
|
93
|
+
const isTarget = isOverColumn === col.id;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
key={col.id}
|
|
98
|
+
onDragOver={(e) => handleDragOver(e, col.id)}
|
|
99
|
+
onDragLeave={handleDragLeave}
|
|
100
|
+
onDrop={(e) => handleDrop(e, col.id)}
|
|
101
|
+
className={`flex flex-col h-[400px] rounded-2xl border transition-all duration-300 ${
|
|
102
|
+
isTarget
|
|
103
|
+
? 'bg-accent/5 border-accent shadow-[0_0_20px_rgba(var(--color-accent-rgb),0.08)] scale-[1.01]'
|
|
104
|
+
: 'bg-bg-card border-border-app'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{/* Column Header */}
|
|
108
|
+
<div className="flex items-center justify-between p-4 border-b border-border-app">
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<span className={`w-2.5 h-2.5 rounded-full ${col.headerColor}`} />
|
|
111
|
+
<h3 className="font-bold text-text-main text-sm font-display">{col.title}</h3>
|
|
112
|
+
</div>
|
|
113
|
+
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-bg-app border border-border-app text-text-muted">
|
|
114
|
+
{colItems.length}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Column Body / Draggable Items Area */}
|
|
119
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
|
|
120
|
+
<AnimatePresence mode="popLayout">
|
|
121
|
+
{colItems.length === 0 ? (
|
|
122
|
+
<motion.div
|
|
123
|
+
initial={{ opacity: 0 }}
|
|
124
|
+
animate={{ opacity: 0.5 }}
|
|
125
|
+
exit={{ opacity: 0 }}
|
|
126
|
+
className="flex flex-col items-center justify-center h-full text-center text-text-muted text-xs border-2 border-dashed border-border-app/50 rounded-xl p-8"
|
|
127
|
+
>
|
|
128
|
+
Arrastrá un elemento acá
|
|
129
|
+
</motion.div>
|
|
130
|
+
) : (
|
|
131
|
+
colItems.map((item) => (
|
|
132
|
+
<motion.div
|
|
133
|
+
layout
|
|
134
|
+
key={item.id}
|
|
135
|
+
initial={{ opacity: 0, y: 10 }}
|
|
136
|
+
animate={{ opacity: 1, y: 0 }}
|
|
137
|
+
exit={{ opacity: 0, scale: 0.95 }}
|
|
138
|
+
transition={{ type: 'spring', stiffness: 350, damping: 30 }}
|
|
139
|
+
draggable={!disabled}
|
|
140
|
+
onDragStart={(e) => handleDragStart(e as unknown as React.DragEvent, item.id)}
|
|
141
|
+
onDragEnd={handleDragEnd}
|
|
142
|
+
className={`p-4 bg-bg-app rounded-xl border border-border-app shadow-sm select-none transition-all duration-200 ${
|
|
143
|
+
disabled ? 'opacity-50 cursor-default' : 'cursor-grab active:cursor-grabbing hover:border-accent/30 hover:shadow-md'
|
|
144
|
+
} ${draggedId === item.id ? 'opacity-30 border-dashed border-accent/40' : ''}`}
|
|
145
|
+
>
|
|
146
|
+
<h4 className="font-bold text-sm text-text-main">{item.title}</h4>
|
|
147
|
+
{item.description && (
|
|
148
|
+
<p className="text-xs text-text-muted mt-1 leading-relaxed">{item.description}</p>
|
|
149
|
+
)}
|
|
150
|
+
</motion.div>
|
|
151
|
+
))
|
|
152
|
+
)}
|
|
153
|
+
</AnimatePresence>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export type DrawerPosition = 'left' | 'right' | 'top' | 'bottom';
|
|
7
|
+
|
|
8
|
+
export interface DrawerProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
position?: DrawerPosition;
|
|
12
|
+
title?: React.ReactNode;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
footer?: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
size?: 'sm' | 'md' | 'lg' | 'full';
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Drawer: React.FC<DrawerProps> = ({
|
|
21
|
+
isOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
position = 'right',
|
|
24
|
+
title,
|
|
25
|
+
children,
|
|
26
|
+
footer,
|
|
27
|
+
className = '',
|
|
28
|
+
size = 'md',
|
|
29
|
+
disabled = false
|
|
30
|
+
}) => {
|
|
31
|
+
const [mounted, setMounted] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setMounted(true);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Prevent scrolling on body when open
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isOpen) {
|
|
40
|
+
document.body.style.overflow = 'hidden';
|
|
41
|
+
} else {
|
|
42
|
+
document.body.style.overflow = '';
|
|
43
|
+
}
|
|
44
|
+
return () => {
|
|
45
|
+
document.body.style.overflow = '';
|
|
46
|
+
};
|
|
47
|
+
}, [isOpen]);
|
|
48
|
+
|
|
49
|
+
// Handle escape key
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
52
|
+
if (e.key === 'Escape') onClose();
|
|
53
|
+
};
|
|
54
|
+
if (isOpen) window.addEventListener('keydown', handleEscape);
|
|
55
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
56
|
+
}, [isOpen, onClose]);
|
|
57
|
+
|
|
58
|
+
if (disabled) return null;
|
|
59
|
+
|
|
60
|
+
// Size mapping (width for horizontal, height for vertical)
|
|
61
|
+
const sizeMap = {
|
|
62
|
+
sm: '300px',
|
|
63
|
+
md: '450px',
|
|
64
|
+
lg: '600px',
|
|
65
|
+
full: '100%'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getPanelStyles = () => {
|
|
69
|
+
const baseStyles = "fixed bg-bg-card/90 backdrop-blur-2xl shadow-2xl flex flex-col z-[100] border-border-app";
|
|
70
|
+
|
|
71
|
+
switch (position) {
|
|
72
|
+
case 'left':
|
|
73
|
+
return `${baseStyles} top-0 left-0 h-full border-r`;
|
|
74
|
+
case 'right':
|
|
75
|
+
return `${baseStyles} top-0 right-0 h-full border-l`;
|
|
76
|
+
case 'top':
|
|
77
|
+
return `${baseStyles} top-0 left-0 w-full border-b rounded-b-3xl`;
|
|
78
|
+
case 'bottom':
|
|
79
|
+
return `${baseStyles} bottom-0 left-0 w-full border-t rounded-t-3xl`;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getDimensionStyles = () => {
|
|
84
|
+
const dimension = sizeMap[size];
|
|
85
|
+
if (position === 'left' || position === 'right') return { width: dimension, maxWidth: '100vw' };
|
|
86
|
+
return { height: dimension, maxHeight: '90vh' };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const animationVariants = {
|
|
90
|
+
hidden: {
|
|
91
|
+
x: position === 'left' ? '-100%' : position === 'right' ? '100%' : 0,
|
|
92
|
+
y: position === 'top' ? '-100%' : position === 'bottom' ? '100%' : 0,
|
|
93
|
+
},
|
|
94
|
+
visible: {
|
|
95
|
+
x: 0,
|
|
96
|
+
y: 0,
|
|
97
|
+
transition: { type: 'spring' as const, damping: 25, stiffness: 200 }
|
|
98
|
+
},
|
|
99
|
+
exit: {
|
|
100
|
+
x: position === 'left' ? '-100%' : position === 'right' ? '100%' : 0,
|
|
101
|
+
y: position === 'top' ? '-100%' : position === 'bottom' ? '100%' : 0,
|
|
102
|
+
transition: { damping: 25, stiffness: 200 }
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!mounted) return null;
|
|
107
|
+
|
|
108
|
+
return createPortal(
|
|
109
|
+
<AnimatePresence>
|
|
110
|
+
{isOpen && (
|
|
111
|
+
<div className="fixed inset-0 z-[999] flex">
|
|
112
|
+
{/* Backdrop */}
|
|
113
|
+
<motion.div
|
|
114
|
+
initial={{ opacity: 0 }}
|
|
115
|
+
animate={{ opacity: 1 }}
|
|
116
|
+
exit={{ opacity: 0 }}
|
|
117
|
+
transition={{ duration: 0.3 }}
|
|
118
|
+
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
|
119
|
+
onClick={onClose}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
{/* Drawer Panel */}
|
|
123
|
+
<motion.div
|
|
124
|
+
variants={animationVariants}
|
|
125
|
+
initial="hidden"
|
|
126
|
+
animate="visible"
|
|
127
|
+
exit="exit"
|
|
128
|
+
className={`${getPanelStyles()} ${className}`}
|
|
129
|
+
style={getDimensionStyles()}
|
|
130
|
+
>
|
|
131
|
+
{/* Header */}
|
|
132
|
+
<div className="flex items-center justify-between p-5 border-b border-border-app">
|
|
133
|
+
<div className="font-bold text-lg text-text-main font-display">
|
|
134
|
+
{title}
|
|
135
|
+
</div>
|
|
136
|
+
<button
|
|
137
|
+
onClick={onClose}
|
|
138
|
+
className="p-2 rounded-xl text-text-muted hover:text-text-main hover:bg-bg-app transition-colors"
|
|
139
|
+
>
|
|
140
|
+
<X className="w-5 h-5" />
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Content Area */}
|
|
145
|
+
<div className="flex-1 overflow-y-auto p-5 scrollbar-thin">
|
|
146
|
+
{children}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Footer */}
|
|
150
|
+
{footer && (
|
|
151
|
+
<div className="p-5 border-t border-border-app bg-bg-app/30">
|
|
152
|
+
{footer}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
</motion.div>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</AnimatePresence>,
|
|
159
|
+
document.body
|
|
160
|
+
);
|
|
161
|
+
};
|