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,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>;