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,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface TabItem {
|
|
5
|
+
id: string | number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SlidingTabsProps {
|
|
10
|
+
tabs: TabItem[];
|
|
11
|
+
activeTab: string | number;
|
|
12
|
+
onChange: (id: string | number) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
pillColor?: string; // Optional background color override
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SlidingTabs: React.FC<SlidingTabsProps> = ({
|
|
18
|
+
tabs,
|
|
19
|
+
activeTab,
|
|
20
|
+
onChange,
|
|
21
|
+
className = '',
|
|
22
|
+
pillColor = 'bg-accent'
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={`inline-flex bg-bg-card/50 border border-border-app p-1 rounded-xl glass ${className}`}
|
|
27
|
+
>
|
|
28
|
+
{tabs.map((tab) => {
|
|
29
|
+
const isActive = tab.id === activeTab;
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
key={tab.id}
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={() => onChange(tab.id)}
|
|
35
|
+
className={`relative px-4 py-2 rounded-lg text-xs font-bold transition-colors duration-200 select-none cursor-pointer focus:outline-hidden ${
|
|
36
|
+
isActive ? 'text-white' : 'text-text-muted hover:text-text-main'
|
|
37
|
+
}`}
|
|
38
|
+
>
|
|
39
|
+
{/* Sliding Pill Background overlay */}
|
|
40
|
+
{isActive && (
|
|
41
|
+
<motion.div
|
|
42
|
+
layoutId="activeTabPill"
|
|
43
|
+
className={`absolute inset-0 rounded-lg -z-5 shadow-sm ${pillColor}`}
|
|
44
|
+
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<span className="relative z-10">{tab.label}</span>
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Reorder, useDragControls } from 'framer-motion';
|
|
3
|
+
import { GripVertical } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface SortableItem {
|
|
6
|
+
id: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SortableListProps<T extends SortableItem> {
|
|
11
|
+
items: T[];
|
|
12
|
+
onReorder: (newOrder: T[]) => void;
|
|
13
|
+
renderItem: (item: T, dragControls?: any) => React.ReactNode;
|
|
14
|
+
axis?: 'y' | 'x';
|
|
15
|
+
useDragHandle?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SortableList = <T extends SortableItem>({
|
|
21
|
+
items,
|
|
22
|
+
onReorder,
|
|
23
|
+
renderItem,
|
|
24
|
+
axis = 'y',
|
|
25
|
+
useDragHandle = false,
|
|
26
|
+
className = '',
|
|
27
|
+
disabled = false
|
|
28
|
+
}: SortableListProps<T>) => {
|
|
29
|
+
return (
|
|
30
|
+
<Reorder.Group
|
|
31
|
+
axis={axis}
|
|
32
|
+
values={items}
|
|
33
|
+
onReorder={onReorder}
|
|
34
|
+
className={`flex ${axis === 'y' ? 'flex-col gap-2' : 'flex-row gap-4'} ${className}`}
|
|
35
|
+
>
|
|
36
|
+
{items.map((item) => (
|
|
37
|
+
<SortableListItem
|
|
38
|
+
key={item.id}
|
|
39
|
+
item={item}
|
|
40
|
+
renderItem={renderItem}
|
|
41
|
+
useDragHandle={useDragHandle}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
/>
|
|
44
|
+
))}
|
|
45
|
+
</Reorder.Group>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Internal wrapper to properly use dragControls per item
|
|
50
|
+
const SortableListItem = <T extends SortableItem>({
|
|
51
|
+
item,
|
|
52
|
+
renderItem,
|
|
53
|
+
useDragHandle,
|
|
54
|
+
disabled
|
|
55
|
+
}: {
|
|
56
|
+
item: T;
|
|
57
|
+
renderItem: (item: T, dragControls?: any) => React.ReactNode;
|
|
58
|
+
useDragHandle: boolean;
|
|
59
|
+
disabled: boolean;
|
|
60
|
+
}) => {
|
|
61
|
+
const controls = useDragControls();
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Reorder.Item
|
|
65
|
+
value={item}
|
|
66
|
+
id={item.id}
|
|
67
|
+
dragListener={!disabled && !useDragHandle}
|
|
68
|
+
dragControls={controls}
|
|
69
|
+
className={`relative bg-bg-card rounded-xl border border-border-app overflow-hidden shadow-sm transition-shadow hover:shadow-md ${disabled ? 'opacity-50' : 'cursor-grab active:cursor-grabbing'}`}
|
|
70
|
+
whileDrag={{
|
|
71
|
+
scale: 1.02,
|
|
72
|
+
boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
|
73
|
+
zIndex: 50,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex items-center w-full h-full">
|
|
77
|
+
{useDragHandle && (
|
|
78
|
+
<div
|
|
79
|
+
className="p-3 cursor-grab active:cursor-grabbing hover:bg-bg-app flex items-center justify-center border-r border-border-app h-full text-text-muted"
|
|
80
|
+
onPointerDown={(e) => !disabled && controls.start(e)}
|
|
81
|
+
>
|
|
82
|
+
<GripVertical className="w-5 h-5" />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<div className="flex-1 w-full h-full">
|
|
86
|
+
{renderItem(item, controls)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</Reorder.Item>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Plus, X } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface SpeedDialAction {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: React.ReactNode;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
colorClass?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SpeedDialProps {
|
|
14
|
+
actions: SpeedDialAction[];
|
|
15
|
+
direction?: 'up' | 'down' | 'left' | 'right';
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ACTION_GAP = 52;
|
|
21
|
+
const FAB_SIZE = 56;
|
|
22
|
+
|
|
23
|
+
const getActionStyle = (
|
|
24
|
+
direction: NonNullable<SpeedDialProps['direction']>,
|
|
25
|
+
index: number
|
|
26
|
+
): React.CSSProperties => {
|
|
27
|
+
const offset = (index + 1) * ACTION_GAP;
|
|
28
|
+
|
|
29
|
+
switch (direction) {
|
|
30
|
+
case 'up':
|
|
31
|
+
return { bottom: FAB_SIZE + 8 + index * ACTION_GAP, left: '50%', transform: 'translateX(-50%)' };
|
|
32
|
+
case 'down':
|
|
33
|
+
return { top: FAB_SIZE + 8 + index * ACTION_GAP, left: '50%', transform: 'translateX(-50%)' };
|
|
34
|
+
case 'left':
|
|
35
|
+
return { right: FAB_SIZE + 8 + index * ACTION_GAP, top: '50%', transform: 'translateY(-50%)' };
|
|
36
|
+
case 'right':
|
|
37
|
+
return { left: FAB_SIZE + 8 + index * ACTION_GAP, top: '50%', transform: 'translateY(-50%)' };
|
|
38
|
+
default:
|
|
39
|
+
return { bottom: offset, left: '50%', transform: 'translateX(-50%)' };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getReserveSpace = (
|
|
44
|
+
direction: NonNullable<SpeedDialProps['direction']>,
|
|
45
|
+
actionCount: number,
|
|
46
|
+
isOpen: boolean
|
|
47
|
+
) => {
|
|
48
|
+
if (!isOpen || actionCount === 0) return {};
|
|
49
|
+
|
|
50
|
+
const space = actionCount * ACTION_GAP + 16;
|
|
51
|
+
|
|
52
|
+
switch (direction) {
|
|
53
|
+
case 'up':
|
|
54
|
+
return { paddingTop: space };
|
|
55
|
+
case 'down':
|
|
56
|
+
return { paddingBottom: space };
|
|
57
|
+
case 'left':
|
|
58
|
+
return { paddingRight: space };
|
|
59
|
+
case 'right':
|
|
60
|
+
return { paddingLeft: space };
|
|
61
|
+
default:
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const SpeedDial: React.FC<SpeedDialProps> = ({
|
|
67
|
+
actions,
|
|
68
|
+
direction = 'up',
|
|
69
|
+
disabled = false,
|
|
70
|
+
className = ''
|
|
71
|
+
}) => {
|
|
72
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
73
|
+
|
|
74
|
+
const toggle = () => {
|
|
75
|
+
if (disabled) return;
|
|
76
|
+
setIsOpen((prev) => !prev);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleAction = (action: SpeedDialAction) => {
|
|
80
|
+
action.onClick?.();
|
|
81
|
+
setIsOpen(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={`relative inline-flex items-center justify-center ${className}`}
|
|
87
|
+
style={getReserveSpace(direction, actions.length, isOpen && !disabled)}
|
|
88
|
+
>
|
|
89
|
+
<AnimatePresence>
|
|
90
|
+
{isOpen && !disabled && actions.map((action, index) => (
|
|
91
|
+
<motion.div
|
|
92
|
+
key={action.id}
|
|
93
|
+
initial={{ opacity: 0, scale: 0.6 }}
|
|
94
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
95
|
+
exit={{ opacity: 0, scale: 0.6 }}
|
|
96
|
+
transition={{ duration: 0.16, ease: [0.16, 1, 0.3, 1], delay: index * 0.03 }}
|
|
97
|
+
className="absolute flex items-center gap-2"
|
|
98
|
+
style={{ ...getActionStyle(direction, index), zIndex: 10 }}
|
|
99
|
+
>
|
|
100
|
+
<span className="text-[10px] font-bold text-text-main bg-bg-card border border-border-app px-2 py-1 rounded-lg shadow-sm whitespace-nowrap">
|
|
101
|
+
{action.label}
|
|
102
|
+
</span>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={() => handleAction(action)}
|
|
106
|
+
className={`w-11 h-11 rounded-full flex items-center justify-center border shadow-lg transition-transform hover:scale-110 cursor-pointer ${
|
|
107
|
+
action.colorClass ?? 'bg-bg-card text-accent border-accent/30'
|
|
108
|
+
}`}
|
|
109
|
+
aria-label={action.label}
|
|
110
|
+
>
|
|
111
|
+
{action.icon}
|
|
112
|
+
</button>
|
|
113
|
+
</motion.div>
|
|
114
|
+
))}
|
|
115
|
+
</AnimatePresence>
|
|
116
|
+
|
|
117
|
+
<motion.button
|
|
118
|
+
type="button"
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
onClick={toggle}
|
|
121
|
+
whileTap={disabled ? undefined : { scale: 0.92 }}
|
|
122
|
+
animate={{ rotate: isOpen ? 45 : 0 }}
|
|
123
|
+
transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}
|
|
124
|
+
className={`relative z-20 w-14 h-14 rounded-full bg-accent text-white flex items-center justify-center shadow-[0_0_20px_var(--color-accent-glow)] border border-accent/30 ${
|
|
125
|
+
disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer hover:bg-accent-hover'
|
|
126
|
+
}`}
|
|
127
|
+
aria-label={isOpen ? 'Cerrar menú' : 'Abrir menú de acciones'}
|
|
128
|
+
aria-expanded={isOpen}
|
|
129
|
+
>
|
|
130
|
+
{isOpen ? <X className="w-6 h-6" /> : <Plus className="w-6 h-6" />}
|
|
131
|
+
</motion.button>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface SpinnerProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
size?: 'sm' | 'md' | 'lg';
|
|
7
|
+
color?: string; // Tailwind border color class
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Spinner: React.FC<SpinnerProps> = ({
|
|
11
|
+
className = '',
|
|
12
|
+
size = 'md',
|
|
13
|
+
color = 'border-accent'
|
|
14
|
+
}) => {
|
|
15
|
+
const sizeClass =
|
|
16
|
+
size === 'sm'
|
|
17
|
+
? 'w-5 h-5 border-2'
|
|
18
|
+
: size === 'lg'
|
|
19
|
+
? 'w-12 h-12 border-4'
|
|
20
|
+
: 'w-8 h-8 border-3';
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={`relative flex items-center justify-center ${className}`}>
|
|
24
|
+
{/* Outer spinning ring */}
|
|
25
|
+
<motion.div
|
|
26
|
+
animate={{ rotate: 360 }}
|
|
27
|
+
transition={{
|
|
28
|
+
repeat: Infinity,
|
|
29
|
+
duration: 0.8,
|
|
30
|
+
ease: 'linear'
|
|
31
|
+
}}
|
|
32
|
+
className={`rounded-full border-t-transparent ${color} ${sizeClass}`}
|
|
33
|
+
/>
|
|
34
|
+
{/* Central glow core */}
|
|
35
|
+
<div className={`absolute rounded-full bg-accent/10 filter blur-xs animate-pulse ${
|
|
36
|
+
size === 'sm' ? 'w-2 h-2' : size === 'lg' ? 'w-6 h-6' : 'w-4 h-4'
|
|
37
|
+
}`} />
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Check } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface StepItem {
|
|
6
|
+
id: string | number;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StepperProps {
|
|
12
|
+
steps: StepItem[];
|
|
13
|
+
currentStep: number;
|
|
14
|
+
onStepClick?: (index: number) => void;
|
|
15
|
+
direction?: 'horizontal' | 'vertical';
|
|
16
|
+
className?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Stepper: React.FC<StepperProps> = ({
|
|
21
|
+
steps,
|
|
22
|
+
currentStep,
|
|
23
|
+
onStepClick,
|
|
24
|
+
direction = 'horizontal',
|
|
25
|
+
className = '',
|
|
26
|
+
disabled = false
|
|
27
|
+
}) => {
|
|
28
|
+
const isHorizontal = direction === 'horizontal';
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`flex ${isHorizontal ? 'flex-row items-start' : 'flex-col'} w-full ${className}`}>
|
|
32
|
+
{steps.map((step, index) => {
|
|
33
|
+
const isCompleted = index < currentStep;
|
|
34
|
+
const isActive = index === currentStep;
|
|
35
|
+
const isLast = index === steps.length - 1;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
key={step.id}
|
|
40
|
+
className={`relative flex ${isHorizontal ? 'flex-1 flex-col items-center' : 'flex-row items-start gap-4 mb-8 last:mb-0'}`}
|
|
41
|
+
>
|
|
42
|
+
{/* Step Circle & Line Container */}
|
|
43
|
+
<div className={`flex items-center ${isHorizontal ? 'w-full justify-center' : 'flex-col h-full'}`}>
|
|
44
|
+
|
|
45
|
+
{/* Line connector (before circle) - Horizontal only needs right line, Vertical needs bottom line */}
|
|
46
|
+
{/* Actually, it's easier to attach the line to the circle pointing to the next one */}
|
|
47
|
+
<div className="relative flex items-center justify-center z-10">
|
|
48
|
+
<motion.button
|
|
49
|
+
animate={{
|
|
50
|
+
backgroundColor: isCompleted ? 'var(--color-accent)' : isActive ? 'var(--color-bg-app)' : 'var(--color-bg-app)',
|
|
51
|
+
borderColor: (isCompleted || isActive) ? 'var(--color-accent)' : 'var(--color-border-app)',
|
|
52
|
+
scale: isActive ? 1.1 : 1
|
|
53
|
+
}}
|
|
54
|
+
transition={{ duration: 0.3 }}
|
|
55
|
+
disabled={disabled || !onStepClick}
|
|
56
|
+
onClick={() => onStepClick && onStepClick(index)}
|
|
57
|
+
className={`w-10 h-10 rounded-full border-2 flex items-center justify-center font-bold text-sm transition-shadow ${
|
|
58
|
+
isActive ? 'shadow-[0_0_15px_rgba(var(--color-accent-rgb),0.5)] text-accent' :
|
|
59
|
+
isCompleted ? 'text-white' : 'text-text-muted'
|
|
60
|
+
} ${(!disabled && onStepClick) ? 'cursor-pointer hover:scale-105' : 'cursor-default'}`}
|
|
61
|
+
>
|
|
62
|
+
<AnimatePresence mode="wait">
|
|
63
|
+
{isCompleted ? (
|
|
64
|
+
<motion.div
|
|
65
|
+
key="check"
|
|
66
|
+
initial={{ scale: 0, opacity: 0 }}
|
|
67
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
68
|
+
exit={{ scale: 0, opacity: 0 }}
|
|
69
|
+
transition={{ duration: 0.2 }}
|
|
70
|
+
>
|
|
71
|
+
<Check className="w-5 h-5 text-bg-app" strokeWidth={3} />
|
|
72
|
+
</motion.div>
|
|
73
|
+
) : (
|
|
74
|
+
<motion.span
|
|
75
|
+
key="number"
|
|
76
|
+
initial={{ opacity: 0 }}
|
|
77
|
+
animate={{ opacity: 1 }}
|
|
78
|
+
exit={{ opacity: 0 }}
|
|
79
|
+
>
|
|
80
|
+
{index + 1}
|
|
81
|
+
</motion.span>
|
|
82
|
+
)}
|
|
83
|
+
</AnimatePresence>
|
|
84
|
+
</motion.button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Connecting Line */}
|
|
88
|
+
{!isLast && (
|
|
89
|
+
<div
|
|
90
|
+
className={`absolute -z-10 ${isHorizontal ? 'top-5 left-[50%] w-full h-1 -translate-y-1/2' : 'top-10 left-5 w-1 h-full -translate-x-1/2'} bg-bg-app overflow-hidden`}
|
|
91
|
+
>
|
|
92
|
+
<motion.div
|
|
93
|
+
className="absolute top-0 left-0 bg-accent w-full h-full"
|
|
94
|
+
initial={{ scaleX: isHorizontal ? 0 : 1, scaleY: isHorizontal ? 1 : 0, originX: 0, originY: 0 }}
|
|
95
|
+
animate={{
|
|
96
|
+
scaleX: isHorizontal ? (isCompleted ? 1 : 0) : 1,
|
|
97
|
+
scaleY: isHorizontal ? 1 : (isCompleted ? 1 : 0)
|
|
98
|
+
}}
|
|
99
|
+
transition={{ duration: 0.4, ease: "easeInOut" }}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Labels */}
|
|
106
|
+
<div className={`${isHorizontal ? 'text-center mt-4' : 'flex flex-col pt-2 pb-4'} z-10`}>
|
|
107
|
+
<motion.h4
|
|
108
|
+
animate={{ color: isActive ? 'var(--color-text-main)' : isCompleted ? 'var(--color-text-main)' : 'var(--color-text-muted)' }}
|
|
109
|
+
className="font-bold text-sm font-display"
|
|
110
|
+
>
|
|
111
|
+
{step.title}
|
|
112
|
+
</motion.h4>
|
|
113
|
+
{step.description && (
|
|
114
|
+
<p className="text-xs text-text-muted mt-1 max-w-[150px]">
|
|
115
|
+
{step.description}
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface TabMenuItem {
|
|
5
|
+
id: string | number;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TabMenuProps {
|
|
11
|
+
items: TabMenuItem[];
|
|
12
|
+
activeId: string | number;
|
|
13
|
+
onChange: (id: string | number) => void;
|
|
14
|
+
variant?: 'pill' | 'underline';
|
|
15
|
+
className?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TabMenu: React.FC<TabMenuProps> = ({
|
|
20
|
+
items,
|
|
21
|
+
activeId,
|
|
22
|
+
onChange,
|
|
23
|
+
variant = 'pill',
|
|
24
|
+
className = '',
|
|
25
|
+
disabled = false
|
|
26
|
+
}) => {
|
|
27
|
+
const isPill = variant === 'pill';
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={`flex items-center gap-1 bg-bg-card/40 border border-border-app/40 p-1.5 rounded-2xl w-fit ${
|
|
31
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : ''
|
|
32
|
+
} ${className}`}>
|
|
33
|
+
{items.map((item) => {
|
|
34
|
+
const isActive = item.id === activeId;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
key={item.id}
|
|
39
|
+
onClick={() => !disabled && onChange(item.id)}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
className={`relative flex items-center gap-2 px-4 py-2 text-xs font-bold transition-all duration-300 focus:outline-hidden rounded-xl ${
|
|
42
|
+
isActive
|
|
43
|
+
? 'text-accent'
|
|
44
|
+
: 'text-text-muted hover:text-text-main'
|
|
45
|
+
} ${disabled ? 'cursor-not-allowed pointer-events-none' : 'cursor-pointer'}`}
|
|
46
|
+
>
|
|
47
|
+
{/* Sliding Pill Backdrop selection */}
|
|
48
|
+
{isActive && isPill && (
|
|
49
|
+
<motion.div
|
|
50
|
+
layoutId="tabmenu-active-pill"
|
|
51
|
+
className="absolute inset-0 bg-accent/10 border border-accent/15 rounded-xl -z-10"
|
|
52
|
+
transition={{ type: 'spring', stiffness: 380, damping: 26 }}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Sliding Underline Selection */}
|
|
57
|
+
{isActive && !isPill && (
|
|
58
|
+
<motion.div
|
|
59
|
+
layoutId="tabmenu-active-line"
|
|
60
|
+
className="absolute bottom-0 left-2 right-2 h-[2px] bg-accent rounded-full -z-10"
|
|
61
|
+
transition={{ type: 'spring', stiffness: 380, damping: 26 }}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
|
66
|
+
<span>{item.label}</span>
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Search } from 'lucide-react';
|
|
3
|
+
import { GlowSelect } from './GlowSelect';
|
|
4
|
+
|
|
5
|
+
export interface TableControlsProps {
|
|
6
|
+
searchTerm: string;
|
|
7
|
+
onSearchChange: (value: string) => void;
|
|
8
|
+
statusFilter: string;
|
|
9
|
+
onStatusFilterChange: (value: string) => void;
|
|
10
|
+
showStatusFilter?: boolean;
|
|
11
|
+
showSearch?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TableControls: React.FC<TableControlsProps> = ({
|
|
17
|
+
searchTerm,
|
|
18
|
+
onSearchChange,
|
|
19
|
+
statusFilter,
|
|
20
|
+
onStatusFilterChange,
|
|
21
|
+
showStatusFilter = true,
|
|
22
|
+
showSearch = true,
|
|
23
|
+
className = '',
|
|
24
|
+
disabled = false
|
|
25
|
+
}) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className={`w-full relative z-20 flex flex-col sm:flex-row gap-4 items-center justify-between p-4 glass bg-bg-card/90 border border-border-app rounded-2xl transition-colors duration-300 ${className} ${
|
|
28
|
+
disabled ? 'opacity-80' : ''
|
|
29
|
+
}`}>
|
|
30
|
+
|
|
31
|
+
{/* Search Input Box */}
|
|
32
|
+
{showSearch && (
|
|
33
|
+
<div className={`relative w-full sm:max-w-xs rounded-xl overflow-hidden group transition-all duration-300 ${
|
|
34
|
+
disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : ''
|
|
35
|
+
}`}>
|
|
36
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-text-muted/60 group-focus-within:text-accent transition-colors duration-200">
|
|
37
|
+
<Search className="w-4 h-4" />
|
|
38
|
+
</div>
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
placeholder="Buscar registros..."
|
|
42
|
+
value={searchTerm}
|
|
43
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
className={`w-full bg-bg-app/40 border border-border-app/50 hover:border-border-app rounded-xl pl-9 pr-4 py-2.5 text-xs text-text-main placeholder-text-muted/50 focus:outline-hidden focus:border-accent transition-all duration-300 ${
|
|
46
|
+
disabled ? 'cursor-not-allowed' : ''
|
|
47
|
+
}`}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
|
|
52
|
+
{/* Status Filter Selector */}
|
|
53
|
+
{showStatusFilter && (
|
|
54
|
+
<div className="flex flex-col gap-1.5 w-full sm:w-44 sm:flex-shrink-0">
|
|
55
|
+
<span className="text-[10px] uppercase font-bold text-text-muted font-mono tracking-wider px-0.5">
|
|
56
|
+
Filtrar por
|
|
57
|
+
</span>
|
|
58
|
+
<GlowSelect
|
|
59
|
+
options={[
|
|
60
|
+
{ value: 'All', label: 'Todos los estados' },
|
|
61
|
+
{ value: 'Active', label: 'Activos' },
|
|
62
|
+
{ value: 'Inactive', label: 'Inactivos' },
|
|
63
|
+
{ value: 'Pending', label: 'Pendientes' }
|
|
64
|
+
]}
|
|
65
|
+
value={statusFilter}
|
|
66
|
+
onChange={onStatusFilterChange}
|
|
67
|
+
size="sm"
|
|
68
|
+
disabled={disabled}
|
|
69
|
+
className="w-full"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
|
|
5
|
+
export interface TablePaginationProps {
|
|
6
|
+
currentPage: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
onPageChange: (page: number) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TablePagination: React.FC<TablePaginationProps> = ({
|
|
13
|
+
currentPage,
|
|
14
|
+
totalPages,
|
|
15
|
+
onPageChange,
|
|
16
|
+
className = ''
|
|
17
|
+
}) => {
|
|
18
|
+
if (totalPages <= 1) return null;
|
|
19
|
+
|
|
20
|
+
const pages = Array.from({ length: totalPages }, (_, idx) => idx + 1);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={`w-full flex items-center justify-between p-4 glass bg-bg-card/50 border border-border-app rounded-2xl transition-colors duration-300 ${className}`}>
|
|
24
|
+
|
|
25
|
+
{/* Page Info */}
|
|
26
|
+
<span className="text-[10px] font-bold text-text-muted font-mono tracking-wider">
|
|
27
|
+
PÁGINA {currentPage} DE {totalPages}
|
|
28
|
+
</span>
|
|
29
|
+
|
|
30
|
+
{/* Pagination control buttons */}
|
|
31
|
+
<div className="flex items-center gap-1.5">
|
|
32
|
+
|
|
33
|
+
{/* Previous Button */}
|
|
34
|
+
<motion.button
|
|
35
|
+
whileTap={{ scale: 0.94 }}
|
|
36
|
+
onClick={() => onPageChange(Math.max(currentPage - 1, 1))}
|
|
37
|
+
disabled={currentPage === 1}
|
|
38
|
+
className={`p-2 rounded-xl border transition-all duration-300 cursor-pointer ${
|
|
39
|
+
currentPage === 1
|
|
40
|
+
? 'bg-transparent border-border-app/20 text-text-muted/30 cursor-not-allowed'
|
|
41
|
+
: 'bg-bg-card border-border-app text-text-muted hover:border-accent hover:text-accent shadow-xs'
|
|
42
|
+
}`}
|
|
43
|
+
aria-label="Página anterior"
|
|
44
|
+
>
|
|
45
|
+
<ChevronLeft className="w-4 h-4" />
|
|
46
|
+
</motion.button>
|
|
47
|
+
|
|
48
|
+
{/* Numeric page triggers */}
|
|
49
|
+
<div className="hidden sm:flex items-center gap-1">
|
|
50
|
+
{pages.map((p) => {
|
|
51
|
+
const isSelected = p === currentPage;
|
|
52
|
+
return (
|
|
53
|
+
<motion.button
|
|
54
|
+
key={p}
|
|
55
|
+
whileTap={{ scale: 0.94 }}
|
|
56
|
+
onClick={() => onPageChange(p)}
|
|
57
|
+
className={`w-8 h-8 rounded-xl border text-xs font-bold transition-all duration-300 cursor-pointer ${
|
|
58
|
+
isSelected
|
|
59
|
+
? 'bg-accent border-accent text-white shadow-md'
|
|
60
|
+
: 'bg-bg-card border-border-app text-text-muted hover:border-accent hover:text-accent shadow-xs'
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{p}
|
|
64
|
+
</motion.button>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Next Button */}
|
|
70
|
+
<motion.button
|
|
71
|
+
whileTap={{ scale: 0.94 }}
|
|
72
|
+
onClick={() => onPageChange(Math.min(currentPage + 1, totalPages))}
|
|
73
|
+
disabled={currentPage === totalPages}
|
|
74
|
+
className={`p-2 rounded-xl border transition-all duration-300 cursor-pointer ${
|
|
75
|
+
currentPage === totalPages
|
|
76
|
+
? 'bg-transparent border-border-app/20 text-text-muted/30 cursor-not-allowed'
|
|
77
|
+
: 'bg-bg-card border-border-app text-text-muted hover:border-accent hover:text-accent shadow-xs'
|
|
78
|
+
}`}
|
|
79
|
+
aria-label="Página siguiente"
|
|
80
|
+
>
|
|
81
|
+
<ChevronRight className="w-4 h-4" />
|
|
82
|
+
</motion.button>
|
|
83
|
+
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|