alexui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +57 -0
  2. package/components/ActionTable.tsx +307 -0
  3. package/components/AlertBanner.tsx +124 -0
  4. package/components/AnimatedAccordion.tsx +95 -0
  5. package/components/Autocomplete.tsx +144 -0
  6. package/components/Avatar.tsx +123 -0
  7. package/components/Badge.tsx +80 -0
  8. package/components/Breadcrumb.tsx +74 -0
  9. package/components/Calendar.tsx +340 -0
  10. package/components/Card3D.tsx +117 -0
  11. package/components/Carousel3D.tsx +193 -0
  12. package/components/CascadeSelect.tsx +232 -0
  13. package/components/ChartShowcase.tsx +700 -0
  14. package/components/Checkbox.tsx +212 -0
  15. package/components/ChipsInput.tsx +152 -0
  16. package/components/CircularKnob.tsx +240 -0
  17. package/components/CodeVisualizer.tsx +67 -0
  18. package/components/Collapsible.tsx +72 -0
  19. package/components/ColorThemeManager.tsx +458 -0
  20. package/components/CommandMenu.tsx +191 -0
  21. package/components/ConfirmDialog.tsx +152 -0
  22. package/components/ContextMenu.tsx +192 -0
  23. package/components/DashboardLayout.tsx +115 -0
  24. package/components/DatePicker.tsx +108 -0
  25. package/components/Divider.tsx +67 -0
  26. package/components/Dock.tsx +93 -0
  27. package/components/DragDropLists.tsx +160 -0
  28. package/components/Drawer.tsx +161 -0
  29. package/components/DropdownPlus.tsx +304 -0
  30. package/components/EmptyState.tsx +49 -0
  31. package/components/ErrorPage.tsx +62 -0
  32. package/components/FileDropzone.tsx +206 -0
  33. package/components/ForgotPassword.tsx +137 -0
  34. package/components/FormField.tsx +81 -0
  35. package/components/GlassButton.tsx +56 -0
  36. package/components/GlassCard.tsx +82 -0
  37. package/components/GlassInput.tsx +96 -0
  38. package/components/GlassmorphicModal.tsx +108 -0
  39. package/components/GlowInput.tsx +111 -0
  40. package/components/GlowSelect.tsx +203 -0
  41. package/components/GlowTextArea.tsx +105 -0
  42. package/components/HorizontalTimeline.tsx +121 -0
  43. package/components/HoverCard.tsx +105 -0
  44. package/components/ImageLightbox.tsx +259 -0
  45. package/components/InputGroup.tsx +118 -0
  46. package/components/InputOTP.tsx +147 -0
  47. package/components/InteractiveNavbar.tsx +266 -0
  48. package/components/InteractiveSidebar.tsx +211 -0
  49. package/components/Kbd.tsx +51 -0
  50. package/components/LiteYouTube.tsx +118 -0
  51. package/components/LoaderCollection.tsx +368 -0
  52. package/components/LoginForm.tsx +192 -0
  53. package/components/MagneticButton.tsx +101 -0
  54. package/components/MaskedInput.tsx +79 -0
  55. package/components/MentionInput.tsx +413 -0
  56. package/components/MorphingSwitch.tsx +86 -0
  57. package/components/MultiSelect.tsx +158 -0
  58. package/components/NumberInput.tsx +203 -0
  59. package/components/Panel.tsx +104 -0
  60. package/components/PasswordInput.tsx +203 -0
  61. package/components/Popover.tsx +91 -0
  62. package/components/PricingTable.tsx +113 -0
  63. package/components/ProgressBar.tsx +152 -0
  64. package/components/RadioButton.tsx +211 -0
  65. package/components/Rating.tsx +82 -0
  66. package/components/ResizablePanel.tsx +114 -0
  67. package/components/ScrollPanel.tsx +103 -0
  68. package/components/SettingsPage.tsx +154 -0
  69. package/components/SignupForm.tsx +182 -0
  70. package/components/Skeleton.tsx +41 -0
  71. package/components/Slider.tsx +95 -0
  72. package/components/SlidingTabs.tsx +54 -0
  73. package/components/SortableList.tsx +91 -0
  74. package/components/SpeedDial.tsx +134 -0
  75. package/components/Spinner.tsx +40 -0
  76. package/components/Stepper.tsx +124 -0
  77. package/components/TabMenu.tsx +72 -0
  78. package/components/TableControls.tsx +77 -0
  79. package/components/TablePagination.tsx +88 -0
  80. package/components/TextEditor.tsx +329 -0
  81. package/components/TextReveal.tsx +99 -0
  82. package/components/ThemeSwitcher.tsx +133 -0
  83. package/components/TimelineGSAP.tsx +164 -0
  84. package/components/ToastSystem.tsx +110 -0
  85. package/components/ToggleButton.tsx +79 -0
  86. package/components/Tooltip.tsx +121 -0
  87. package/components/Tree.tsx +138 -0
  88. package/dist/commands/add.d.ts +7 -0
  89. package/dist/commands/add.js +110 -0
  90. package/dist/commands/init.d.ts +5 -0
  91. package/dist/commands/init.js +76 -0
  92. package/dist/commands/list.d.ts +1 -0
  93. package/dist/commands/list.js +32 -0
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.js +60 -0
  96. package/dist/registry.d.ts +6 -0
  97. package/dist/registry.js +38 -0
  98. package/dist/tui/browse.d.ts +3 -0
  99. package/dist/tui/browse.js +139 -0
  100. package/dist/tui/format.d.ts +11 -0
  101. package/dist/tui/format.js +52 -0
  102. package/dist/tui/main.d.ts +1 -0
  103. package/dist/tui/main.js +86 -0
  104. package/dist/tui/panels.d.ts +9 -0
  105. package/dist/tui/panels.js +50 -0
  106. package/dist/tui/theme.d.ts +28 -0
  107. package/dist/tui/theme.js +76 -0
  108. package/dist/types.d.ts +28 -0
  109. package/dist/types.js +1 -0
  110. package/dist/utils/config.d.ts +6 -0
  111. package/dist/utils/config.js +24 -0
  112. package/dist/utils/copy.d.ts +9 -0
  113. package/dist/utils/copy.js +43 -0
  114. package/dist/utils/cwd.d.ts +6 -0
  115. package/dist/utils/cwd.js +30 -0
  116. package/dist/utils/deps.d.ts +1 -0
  117. package/dist/utils/deps.js +19 -0
  118. package/dist/utils/project.d.ts +5 -0
  119. package/dist/utils/project.js +30 -0
  120. package/dist/utils/theme.d.ts +1 -0
  121. package/dist/utils/theme.js +24 -0
  122. package/package.json +52 -0
  123. package/registry.json +1133 -0
  124. package/templates/theme.css +81 -0
@@ -0,0 +1,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
+ };