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,164 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import gsap from 'gsap';
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
4
|
+
|
|
5
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
6
|
+
|
|
7
|
+
export interface TimelineItem {
|
|
8
|
+
id: string | number;
|
|
9
|
+
date: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TimelineGSAPProps {
|
|
15
|
+
items: TimelineItem[];
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TimelineGSAP: React.FC<TimelineGSAPProps> = ({ items, className = '' }) => {
|
|
20
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const cardRefs = useRef<HTMLDivElement[]>([]);
|
|
23
|
+
const dotRefs = useRef<HTMLDivElement[]>([]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!containerRef.current || !progressBarRef.current) return;
|
|
27
|
+
|
|
28
|
+
const cards = cardRefs.current.filter(Boolean);
|
|
29
|
+
const dots = dotRefs.current.filter(Boolean);
|
|
30
|
+
|
|
31
|
+
const ctx = gsap.context(() => {
|
|
32
|
+
// 1. Progress Bar Fill Animation linked to scroll
|
|
33
|
+
gsap.fromTo(
|
|
34
|
+
progressBarRef.current,
|
|
35
|
+
{ scaleY: 0 },
|
|
36
|
+
{
|
|
37
|
+
scaleY: 1,
|
|
38
|
+
ease: 'none',
|
|
39
|
+
scrollTrigger: {
|
|
40
|
+
trigger: containerRef.current,
|
|
41
|
+
start: 'top 30%',
|
|
42
|
+
end: 'bottom 70%',
|
|
43
|
+
scrub: true,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// 2. Milestone Cards & Dots reveal animations
|
|
49
|
+
cards.forEach((card, idx) => {
|
|
50
|
+
const dot = dots[idx];
|
|
51
|
+
const isLeft = idx % 2 === 0;
|
|
52
|
+
|
|
53
|
+
// Slide in from left or right depending on column
|
|
54
|
+
gsap.fromTo(
|
|
55
|
+
card,
|
|
56
|
+
{
|
|
57
|
+
opacity: 0,
|
|
58
|
+
x: isLeft ? -50 : 50,
|
|
59
|
+
filter: 'blur(4px)'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
opacity: 1,
|
|
63
|
+
x: 0,
|
|
64
|
+
filter: 'blur(0px)',
|
|
65
|
+
duration: 0.8,
|
|
66
|
+
scrollTrigger: {
|
|
67
|
+
trigger: card,
|
|
68
|
+
start: 'top 80%',
|
|
69
|
+
toggleActions: 'play none none reverse',
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Scale up indicator dot
|
|
75
|
+
if (dot) {
|
|
76
|
+
gsap.fromTo(
|
|
77
|
+
dot,
|
|
78
|
+
{ scale: 0, opacity: 0 },
|
|
79
|
+
{
|
|
80
|
+
scale: 1,
|
|
81
|
+
opacity: 1,
|
|
82
|
+
duration: 0.4,
|
|
83
|
+
scrollTrigger: {
|
|
84
|
+
trigger: dot,
|
|
85
|
+
start: 'top 80%',
|
|
86
|
+
toggleActions: 'play none none reverse'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
}, containerRef);
|
|
94
|
+
|
|
95
|
+
return () => ctx.revert();
|
|
96
|
+
}, [items]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div ref={containerRef} className={`relative w-full max-w-4xl mx-auto py-12 px-4 ${className}`}>
|
|
100
|
+
|
|
101
|
+
{/* Central Axis Timeline Line */}
|
|
102
|
+
<div className="absolute left-1/2 transform -translate-x-1/2 top-0 bottom-0 w-[2px] bg-border-app/40 rounded-full">
|
|
103
|
+
{/* Animated fill indicator */}
|
|
104
|
+
<div
|
|
105
|
+
ref={progressBarRef}
|
|
106
|
+
className="w-full h-full bg-gradient-to-b from-accent via-indigo-500 to-pink-500 origin-top rounded-full"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Timeline entries list */}
|
|
111
|
+
<div className="relative flex flex-col gap-16 overflow-visible">
|
|
112
|
+
{items.map((item, idx) => {
|
|
113
|
+
const isLeft = idx % 2 === 0;
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
key={item.id}
|
|
117
|
+
className={`flex w-full overflow-visible items-center justify-between ${
|
|
118
|
+
isLeft ? 'flex-row' : 'flex-row-reverse'
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
|
|
122
|
+
{/* Milestone card side wrapper */}
|
|
123
|
+
<div className="w-[45%] flex flex-col items-stretch overflow-visible">
|
|
124
|
+
<div
|
|
125
|
+
ref={(el) => {
|
|
126
|
+
if (el) cardRefs.current[idx] = el;
|
|
127
|
+
}}
|
|
128
|
+
className={`p-6 rounded-2xl glass bg-bg-card border border-border-app shadow-md transition-colors duration-300 relative ${
|
|
129
|
+
isLeft ? 'text-right' : 'text-left'
|
|
130
|
+
}`}
|
|
131
|
+
>
|
|
132
|
+
<span className="text-xs font-mono font-bold text-accent tracking-wider block mb-1">
|
|
133
|
+
{item.date}
|
|
134
|
+
</span>
|
|
135
|
+
<h3 className="font-extrabold text-base text-text-main mb-2">
|
|
136
|
+
{item.title}
|
|
137
|
+
</h3>
|
|
138
|
+
<p className="text-xs text-text-muted leading-relaxed">
|
|
139
|
+
{item.description}
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Indicator Dot center wrapper */}
|
|
145
|
+
<div className="relative z-10 w-[10%] flex justify-center items-center">
|
|
146
|
+
<div
|
|
147
|
+
ref={(el) => {
|
|
148
|
+
if (el) dotRefs.current[idx] = el;
|
|
149
|
+
}}
|
|
150
|
+
className="w-5 h-5 rounded-full bg-bg-card border-4 border-accent flex items-center justify-center shadow-lg transition-transform duration-300"
|
|
151
|
+
>
|
|
152
|
+
<div className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Empty placeholder spacer side */}
|
|
157
|
+
<div className="w-[45%]" />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
2
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export type ToastType = 'success' | 'error' | 'info';
|
|
7
|
+
|
|
8
|
+
export interface Toast {
|
|
9
|
+
id: string;
|
|
10
|
+
message: string;
|
|
11
|
+
type: ToastType;
|
|
12
|
+
duration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ToastContextType {
|
|
16
|
+
toast: (message: string, type?: ToastType, duration?: number) => void;
|
|
17
|
+
removeToast: (id: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
export const useToast = () => {
|
|
23
|
+
const context = useContext(ToastContext);
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error('useToast must be used within a ToastProvider');
|
|
26
|
+
}
|
|
27
|
+
return context;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
31
|
+
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
32
|
+
|
|
33
|
+
const removeToast = useCallback((id: string) => {
|
|
34
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const toast = useCallback((message: string, type: ToastType = 'info', duration = 3000) => {
|
|
38
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
39
|
+
setToasts((prev) => [...prev, { id, message, type, duration }]);
|
|
40
|
+
|
|
41
|
+
if (duration > 0) {
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
removeToast(id);
|
|
44
|
+
}, duration);
|
|
45
|
+
}
|
|
46
|
+
}, [removeToast]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ToastContext.Provider value={{ toast, removeToast }}>
|
|
50
|
+
{children}
|
|
51
|
+
|
|
52
|
+
{/* Toast Portal Container */}
|
|
53
|
+
<div className="fixed bottom-6 right-6 z-50 flex flex-col gap-3 max-w-sm w-full pointer-events-none">
|
|
54
|
+
<AnimatePresence>
|
|
55
|
+
{toasts.map((t) => (
|
|
56
|
+
<ToastItem key={t.id} toast={t} onClose={removeToast} />
|
|
57
|
+
))}
|
|
58
|
+
</AnimatePresence>
|
|
59
|
+
</div>
|
|
60
|
+
</ToastContext.Provider>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
interface ToastItemProps {
|
|
65
|
+
toast: Toast;
|
|
66
|
+
onClose: (id: string) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ToastItem: React.FC<ToastItemProps> = ({ toast, onClose }) => {
|
|
70
|
+
const { id, message, type } = toast;
|
|
71
|
+
|
|
72
|
+
const iconMap = {
|
|
73
|
+
success: <CheckCircle className="w-5 h-5 text-success" />,
|
|
74
|
+
error: <AlertCircle className="w-5 h-5 text-error" />,
|
|
75
|
+
info: <Info className="w-5 h-5 text-info" />
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const borderMap = {
|
|
79
|
+
success: 'border-success/20 dark:border-success/30',
|
|
80
|
+
error: 'border-error/20 dark:border-error/30',
|
|
81
|
+
info: 'border-info/20 dark:border-info/30'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<motion.div
|
|
86
|
+
layout
|
|
87
|
+
initial={{ opacity: 0, y: 15, scale: 0.9 }}
|
|
88
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
89
|
+
exit={{ opacity: 0, scale: 0.85, transition: { duration: 0.15 } }}
|
|
90
|
+
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
|
91
|
+
className={`pointer-events-auto flex items-center justify-between gap-3 p-4 rounded-xl glass bg-bg-card/90 border shadow-lg ${borderMap[type]}`}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-center gap-3">
|
|
94
|
+
<div className="flex-shrink-0">
|
|
95
|
+
{iconMap[type]}
|
|
96
|
+
</div>
|
|
97
|
+
<p className="text-xs font-semibold text-text-main leading-snug">
|
|
98
|
+
{message}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => onClose(id)}
|
|
103
|
+
className="flex-shrink-0 text-text-muted hover:text-text-main p-0.5 rounded-lg transition-colors cursor-pointer"
|
|
104
|
+
aria-label="Cerrar notificación"
|
|
105
|
+
>
|
|
106
|
+
<X className="w-4 h-4" />
|
|
107
|
+
</button>
|
|
108
|
+
</motion.div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface ToggleOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ToggleButtonProps {
|
|
12
|
+
options: ToggleOption[];
|
|
13
|
+
value: string;
|
|
14
|
+
onChange: (value: string) => void;
|
|
15
|
+
size?: 'sm' | 'md' | 'lg';
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sizeStyles = {
|
|
21
|
+
sm: 'p-1 gap-0.5 text-[10px]',
|
|
22
|
+
md: 'p-1.5 gap-1 text-xs',
|
|
23
|
+
lg: 'p-2 gap-1.5 text-sm'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const itemPadding = {
|
|
27
|
+
sm: 'px-2.5 py-1',
|
|
28
|
+
md: 'px-3.5 py-1.5',
|
|
29
|
+
lg: 'px-4 py-2'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
|
33
|
+
options,
|
|
34
|
+
value,
|
|
35
|
+
onChange,
|
|
36
|
+
size = 'md',
|
|
37
|
+
disabled = false,
|
|
38
|
+
className = ''
|
|
39
|
+
}) => {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className={`inline-flex items-center rounded-xl bg-bg-app/60 border border-border-app ${sizeStyles[size]} ${
|
|
43
|
+
disabled ? 'opacity-40 cursor-not-allowed' : ''
|
|
44
|
+
} ${className}`}
|
|
45
|
+
role="group"
|
|
46
|
+
>
|
|
47
|
+
{options.map((opt) => {
|
|
48
|
+
const isSelected = value === opt.value;
|
|
49
|
+
const isDisabled = disabled || opt.disabled;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
key={opt.value}
|
|
54
|
+
type="button"
|
|
55
|
+
disabled={isDisabled}
|
|
56
|
+
onClick={() => !isDisabled && onChange(opt.value)}
|
|
57
|
+
className={`relative inline-flex items-center gap-1.5 rounded-lg font-bold transition-colors ${
|
|
58
|
+
itemPadding[size]
|
|
59
|
+
} ${isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'} ${
|
|
60
|
+
isSelected ? 'text-white' : 'text-text-muted hover:text-text-main'
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{isSelected && (
|
|
64
|
+
<motion.div
|
|
65
|
+
layoutId="toggle-button-active"
|
|
66
|
+
className="absolute inset-0 bg-accent rounded-lg shadow-[0_0_12px_var(--color-accent-glow)]"
|
|
67
|
+
transition={{ type: 'spring' as const, stiffness: 400, damping: 30 }}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<span className="relative z-10 flex items-center gap-1.5">
|
|
71
|
+
{opt.icon}
|
|
72
|
+
{opt.label}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
|
5
|
+
|
|
6
|
+
export interface TooltipProps {
|
|
7
|
+
content: React.ReactNode;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
placement?: TooltipPlacement;
|
|
10
|
+
delay?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const Tooltip: React.FC<TooltipProps> = ({
|
|
15
|
+
content,
|
|
16
|
+
children,
|
|
17
|
+
placement = 'top',
|
|
18
|
+
delay = 200,
|
|
19
|
+
className = ''
|
|
20
|
+
}) => {
|
|
21
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
22
|
+
const [activePlacement, setActivePlacement] = useState<'top' | 'bottom' | 'left' | 'right'>('top');
|
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
25
|
+
|
|
26
|
+
const handleMouseEnter = () => {
|
|
27
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
28
|
+
|
|
29
|
+
// Calculate position if "auto" is selected
|
|
30
|
+
if (containerRef.current) {
|
|
31
|
+
if (placement === 'auto') {
|
|
32
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
33
|
+
const topSpace = rect.top;
|
|
34
|
+
const bottomSpace = window.innerHeight - rect.bottom;
|
|
35
|
+
const leftSpace = rect.left;
|
|
36
|
+
const rightSpace = window.innerWidth - rect.right;
|
|
37
|
+
|
|
38
|
+
// Choose the side with the maximum pixel clearance in the viewport
|
|
39
|
+
const spaces = [
|
|
40
|
+
{ side: 'top', space: topSpace },
|
|
41
|
+
{ side: 'bottom', space: bottomSpace },
|
|
42
|
+
{ side: 'left', space: leftSpace },
|
|
43
|
+
{ side: 'right', space: rightSpace }
|
|
44
|
+
];
|
|
45
|
+
spaces.sort((a, b) => b.space - a.space);
|
|
46
|
+
setActivePlacement(spaces[0].side as 'top' | 'bottom' | 'left' | 'right');
|
|
47
|
+
} else {
|
|
48
|
+
setActivePlacement(placement);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
timeoutRef.current = setTimeout(() => {
|
|
53
|
+
setIsVisible(true);
|
|
54
|
+
}, delay);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleMouseLeave = () => {
|
|
58
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
59
|
+
setIsVisible(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Styles mapping based on selected active placement
|
|
63
|
+
const placementClasses = {
|
|
64
|
+
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2.5',
|
|
65
|
+
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2.5',
|
|
66
|
+
left: 'right-full top-1/2 -translate-y-1/2 mr-2.5',
|
|
67
|
+
right: 'left-full top-1/2 -translate-y-1/2 ml-2.5'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const arrowClasses = {
|
|
71
|
+
top: 'top-full left-1/2 -translate-x-1/2 border-t-bg-card border-x-transparent border-b-transparent -mt-[1px]',
|
|
72
|
+
bottom: 'bottom-full left-1/2 -translate-x-1/2 border-b-bg-card border-x-transparent border-t-transparent -mb-[1px]',
|
|
73
|
+
left: 'left-full top-1/2 -translate-y-1/2 border-l-bg-card border-y-transparent border-r-transparent -ml-[1px]',
|
|
74
|
+
right: 'right-full top-1/2 -translate-y-1/2 border-r-bg-card border-y-transparent border-l-transparent -mr-[1px]'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Motion animation presets
|
|
78
|
+
const animationVariants = {
|
|
79
|
+
hidden: { opacity: 0, scale: 0.95 },
|
|
80
|
+
visible: {
|
|
81
|
+
opacity: 1,
|
|
82
|
+
scale: 1,
|
|
83
|
+
transition: { type: 'spring' as const, stiffness: 400, damping: 22 }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
ref={containerRef}
|
|
90
|
+
onMouseEnter={handleMouseEnter}
|
|
91
|
+
onMouseLeave={handleMouseLeave}
|
|
92
|
+
onFocus={handleMouseEnter}
|
|
93
|
+
onBlur={handleMouseLeave}
|
|
94
|
+
className={`relative inline-block ${className}`}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
|
|
98
|
+
<AnimatePresence>
|
|
99
|
+
{isVisible && (
|
|
100
|
+
<motion.div
|
|
101
|
+
initial="hidden"
|
|
102
|
+
animate="visible"
|
|
103
|
+
exit="hidden"
|
|
104
|
+
variants={animationVariants}
|
|
105
|
+
className={`absolute z-100 pointer-events-none select-none ${placementClasses[activePlacement]}`}
|
|
106
|
+
>
|
|
107
|
+
{/* Tooltip Content box */}
|
|
108
|
+
<div className="bg-bg-card text-text-main text-xs font-bold px-3 py-2 rounded-lg border border-border-app shadow-xl whitespace-nowrap relative">
|
|
109
|
+
{content}
|
|
110
|
+
|
|
111
|
+
{/* Tooltip Arrow tip */}
|
|
112
|
+
<div
|
|
113
|
+
className={`absolute w-0 h-0 border-[5px] ${arrowClasses[activePlacement]}`}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</motion.div>
|
|
117
|
+
)}
|
|
118
|
+
</AnimatePresence>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { ChevronRight, Folder, FolderOpen, File } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
children?: TreeNode[];
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TreeProps {
|
|
13
|
+
nodes: TreeNode[];
|
|
14
|
+
selectedId?: string;
|
|
15
|
+
onSelect?: (id: string) => void;
|
|
16
|
+
defaultExpandedIds?: string[];
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface TreeItemProps {
|
|
22
|
+
node: TreeNode;
|
|
23
|
+
depth: number;
|
|
24
|
+
selectedId?: string;
|
|
25
|
+
expandedIds: Set<string>;
|
|
26
|
+
onToggle: (id: string) => void;
|
|
27
|
+
onSelect?: (id: string) => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TreeItem: React.FC<TreeItemProps> = ({
|
|
32
|
+
node,
|
|
33
|
+
depth,
|
|
34
|
+
selectedId,
|
|
35
|
+
expandedIds,
|
|
36
|
+
onToggle,
|
|
37
|
+
onSelect,
|
|
38
|
+
disabled
|
|
39
|
+
}) => {
|
|
40
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
41
|
+
const isExpanded = expandedIds.has(node.id);
|
|
42
|
+
const isSelected = selectedId === node.id;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex flex-col">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (hasChildren) onToggle(node.id);
|
|
51
|
+
onSelect?.(node.id);
|
|
52
|
+
}}
|
|
53
|
+
className={`flex items-center gap-2 py-1.5 px-2 rounded-lg text-left text-sm font-semibold transition-all w-full ${
|
|
54
|
+
disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'
|
|
55
|
+
} ${isSelected ? 'bg-accent/10 text-accent' : 'text-text-muted hover:bg-bg-app hover:text-text-main'}`}
|
|
56
|
+
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
|
57
|
+
>
|
|
58
|
+
{hasChildren ? (
|
|
59
|
+
<motion.span
|
|
60
|
+
animate={{ rotate: isExpanded ? 90 : 0 }}
|
|
61
|
+
transition={{ type: 'spring' as const, stiffness: 400, damping: 24 }}
|
|
62
|
+
className="flex-shrink-0"
|
|
63
|
+
>
|
|
64
|
+
<ChevronRight className="w-3.5 h-3.5" />
|
|
65
|
+
</motion.span>
|
|
66
|
+
) : (
|
|
67
|
+
<span className="w-3.5 flex-shrink-0" />
|
|
68
|
+
)}
|
|
69
|
+
<span className="flex-shrink-0 text-text-muted">
|
|
70
|
+
{node.icon ?? (hasChildren ? (isExpanded ? <FolderOpen className="w-4 h-4 text-accent" /> : <Folder className="w-4 h-4" />) : <File className="w-4 h-4" />)}
|
|
71
|
+
</span>
|
|
72
|
+
<span className="truncate">{node.label}</span>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
<AnimatePresence initial={false}>
|
|
76
|
+
{hasChildren && isExpanded && (
|
|
77
|
+
<motion.div
|
|
78
|
+
initial={{ height: 0, opacity: 0 }}
|
|
79
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
80
|
+
exit={{ height: 0, opacity: 0 }}
|
|
81
|
+
transition={{ type: 'spring' as const, stiffness: 350, damping: 28 }}
|
|
82
|
+
className="overflow-hidden"
|
|
83
|
+
>
|
|
84
|
+
{node.children!.map((child) => (
|
|
85
|
+
<TreeItem
|
|
86
|
+
key={child.id}
|
|
87
|
+
node={child}
|
|
88
|
+
depth={depth + 1}
|
|
89
|
+
selectedId={selectedId}
|
|
90
|
+
expandedIds={expandedIds}
|
|
91
|
+
onToggle={onToggle}
|
|
92
|
+
onSelect={onSelect}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</motion.div>
|
|
97
|
+
)}
|
|
98
|
+
</AnimatePresence>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Tree: React.FC<TreeProps> = ({
|
|
104
|
+
nodes,
|
|
105
|
+
selectedId,
|
|
106
|
+
onSelect,
|
|
107
|
+
defaultExpandedIds = [],
|
|
108
|
+
disabled = false,
|
|
109
|
+
className = ''
|
|
110
|
+
}) => {
|
|
111
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(defaultExpandedIds));
|
|
112
|
+
|
|
113
|
+
const handleToggle = (id: string) => {
|
|
114
|
+
setExpandedIds((prev) => {
|
|
115
|
+
const next = new Set(prev);
|
|
116
|
+
if (next.has(id)) next.delete(id);
|
|
117
|
+
else next.add(id);
|
|
118
|
+
return next;
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className={`flex flex-col gap-0.5 min-w-[220px] ${className}`} role="tree">
|
|
124
|
+
{nodes.map((node) => (
|
|
125
|
+
<TreeItem
|
|
126
|
+
key={node.id}
|
|
127
|
+
node={node}
|
|
128
|
+
depth={0}
|
|
129
|
+
selectedId={selectedId}
|
|
130
|
+
expandedIds={expandedIds}
|
|
131
|
+
onToggle={handleToggle}
|
|
132
|
+
onSelect={onSelect}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
/>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface AddOptions {
|
|
2
|
+
overwrite?: boolean;
|
|
3
|
+
yes?: boolean;
|
|
4
|
+
/** Modo embebido en TUI: sin intro/outro y sin exit en cancelación */
|
|
5
|
+
embed?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function runAdd(componentArgs: string[], cwd?: string, options?: AddOptions): Promise<void>;
|