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,144 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface AutocompleteProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
suggestions: string[];
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
isInvalid?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Autocomplete: React.FC<AutocompleteProps> = ({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
suggestions,
|
|
19
|
+
placeholder = 'Escribe para autocompletar...',
|
|
20
|
+
label,
|
|
21
|
+
disabled = false,
|
|
22
|
+
isInvalid = false,
|
|
23
|
+
className = ''
|
|
24
|
+
}) => {
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
// Close suggestions when clicking outside
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
32
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
33
|
+
setIsOpen(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
37
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Filter suggestions reactively in real time
|
|
41
|
+
const filteredSuggestions = useMemo(() => {
|
|
42
|
+
if (!value) return [];
|
|
43
|
+
const query = value.toLowerCase().trim();
|
|
44
|
+
if (query.length === 0) return [];
|
|
45
|
+
|
|
46
|
+
return suggestions.filter(
|
|
47
|
+
(s) => s.toLowerCase().includes(query) && s.toLowerCase() !== query
|
|
48
|
+
);
|
|
49
|
+
}, [value, suggestions]);
|
|
50
|
+
|
|
51
|
+
// Handle suggestion selection
|
|
52
|
+
const handleSelect = (suggestion: string) => {
|
|
53
|
+
onChange(suggestion);
|
|
54
|
+
setIsOpen(false);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
58
|
+
const val = e.target.value;
|
|
59
|
+
onChange(val);
|
|
60
|
+
setIsOpen(true);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleFocus = () => {
|
|
64
|
+
setIsFocused(true);
|
|
65
|
+
setIsOpen(true);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleBlur = () => {
|
|
69
|
+
setIsFocused(false);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div ref={containerRef} className={`relative w-full flex flex-col gap-1.5 ${className}`}>
|
|
74
|
+
{label && (
|
|
75
|
+
<span className="text-xs font-bold text-text-muted uppercase tracking-wider px-1">
|
|
76
|
+
{label}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Input Outer wrapper */}
|
|
81
|
+
<div className="relative rounded-xl overflow-visible transition-all duration-300">
|
|
82
|
+
|
|
83
|
+
{/* Glow border ring */}
|
|
84
|
+
<motion.div
|
|
85
|
+
animate={{
|
|
86
|
+
opacity: isFocused && !disabled ? 1 : 0,
|
|
87
|
+
scale: isFocused && !disabled ? 1 : 0.98,
|
|
88
|
+
}}
|
|
89
|
+
transition={{ duration: 0.25 }}
|
|
90
|
+
className="absolute -inset-[1px] bg-gradient-to-r from-accent to-pink-500 rounded-xl pointer-events-none z-0 blur-[1px]"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Input container */}
|
|
94
|
+
<div className={`relative bg-bg-card/60 border rounded-xl z-10 flex items-center transition-colors duration-300 ${
|
|
95
|
+
isFocused && !disabled ? 'border-transparent' : 'border-border-app'
|
|
96
|
+
} ${disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : ''}`}>
|
|
97
|
+
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
value={value}
|
|
101
|
+
onChange={handleInputChange}
|
|
102
|
+
onFocus={handleFocus}
|
|
103
|
+
onBlur={handleBlur}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
className={`w-full bg-transparent py-3 px-4 text-sm text-text-main placeholder-text-muted/50 focus:outline-hidden z-10 relative ${
|
|
107
|
+
disabled ? 'cursor-not-allowed' : ''
|
|
108
|
+
}`}
|
|
109
|
+
style={{
|
|
110
|
+
boxShadow: isFocused && !disabled && !isInvalid
|
|
111
|
+
? '0 0 10px var(--color-accent-glow)'
|
|
112
|
+
: isInvalid
|
|
113
|
+
? '0 0 10px var(--color-error-glow)'
|
|
114
|
+
: 'none'
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Suggestions Popover panel */}
|
|
120
|
+
<AnimatePresence>
|
|
121
|
+
{isOpen && filteredSuggestions.length > 0 && !disabled && (
|
|
122
|
+
<motion.ul
|
|
123
|
+
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
124
|
+
animate={{ opacity: 1, y: 4, scale: 1 }}
|
|
125
|
+
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
126
|
+
transition={{ type: 'spring', stiffness: 350, damping: 25 }}
|
|
127
|
+
className="absolute left-0 w-full glass bg-bg-card rounded-xl shadow-2xl py-1.5 z-50 border border-border-app/50 max-h-48 overflow-y-auto"
|
|
128
|
+
>
|
|
129
|
+
{filteredSuggestions.map((suggestion, idx) => (
|
|
130
|
+
<li
|
|
131
|
+
key={idx}
|
|
132
|
+
onClick={() => handleSelect(suggestion)}
|
|
133
|
+
className="px-4 py-2.5 text-xs font-semibold text-text-main hover:bg-accent/10 hover:text-accent transition-colors duration-200 cursor-pointer"
|
|
134
|
+
>
|
|
135
|
+
{suggestion}
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</motion.ul>
|
|
139
|
+
)}
|
|
140
|
+
</AnimatePresence>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
5
|
+
export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
|
|
6
|
+
|
|
7
|
+
export interface AvatarProps {
|
|
8
|
+
src?: string;
|
|
9
|
+
alt?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
size?: AvatarSize;
|
|
12
|
+
status?: AvatarStatus;
|
|
13
|
+
showStatus?: boolean;
|
|
14
|
+
ring?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sizeMap: Record<AvatarSize, { container: string; text: string; status: string }> = {
|
|
19
|
+
xs: { container: 'w-7 h-7', text: 'text-[9px]', status: 'w-2 h-2 border' },
|
|
20
|
+
sm: { container: 'w-9 h-9', text: 'text-[10px]', status: 'w-2.5 h-2.5 border-2' },
|
|
21
|
+
md: { container: 'w-12 h-12', text: 'text-xs', status: 'w-3 h-3 border-2' },
|
|
22
|
+
lg: { container: 'w-16 h-16', text: 'text-sm', status: 'w-3.5 h-3.5 border-2' },
|
|
23
|
+
xl: { container: 'w-24 h-24', text: 'text-lg', status: 'w-4 h-4 border-2' }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const statusColors: Record<AvatarStatus, string> = {
|
|
27
|
+
online: 'bg-green-500',
|
|
28
|
+
offline: 'bg-text-muted',
|
|
29
|
+
busy: 'bg-red-500',
|
|
30
|
+
away: 'bg-yellow-500'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getInitials = (name?: string) => {
|
|
34
|
+
if (!name) return '?';
|
|
35
|
+
const parts = name.trim().split(/\s+/);
|
|
36
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
37
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Avatar: React.FC<AvatarProps> = ({
|
|
41
|
+
src,
|
|
42
|
+
alt,
|
|
43
|
+
name,
|
|
44
|
+
size = 'md',
|
|
45
|
+
status = 'online',
|
|
46
|
+
showStatus = false,
|
|
47
|
+
ring = false,
|
|
48
|
+
className = ''
|
|
49
|
+
}) => {
|
|
50
|
+
const [hasError, setHasError] = useState(false);
|
|
51
|
+
const styles = sizeMap[size];
|
|
52
|
+
const showImage = src && !hasError;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<motion.div
|
|
56
|
+
whileHover={{ scale: 1.04 }}
|
|
57
|
+
transition={{ type: 'spring' as const, stiffness: 400, damping: 20 }}
|
|
58
|
+
className={`relative inline-flex flex-shrink-0 ${className}`}
|
|
59
|
+
>
|
|
60
|
+
<div
|
|
61
|
+
className={`${styles.container} rounded-full overflow-hidden flex items-center justify-center font-extrabold font-display select-none ${
|
|
62
|
+
ring ? 'ring-2 ring-accent/40 ring-offset-2 ring-offset-bg-app' : ''
|
|
63
|
+
} ${showImage ? 'bg-bg-app' : 'bg-accent/15 text-accent border border-accent/20'}`}
|
|
64
|
+
>
|
|
65
|
+
{showImage ? (
|
|
66
|
+
<img
|
|
67
|
+
src={src}
|
|
68
|
+
alt={alt || name || 'Avatar'}
|
|
69
|
+
className="w-full h-full object-cover"
|
|
70
|
+
onError={() => setHasError(true)}
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<span className={styles.text}>{getInitials(name || alt)}</span>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{showStatus && (
|
|
78
|
+
<span
|
|
79
|
+
className={`absolute bottom-0 right-0 rounded-full border-bg-card ${styles.status} ${statusColors[status]}`}
|
|
80
|
+
title={status}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
</motion.div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export interface AvatarGroupProps {
|
|
88
|
+
avatars: Array<Pick<AvatarProps, 'src' | 'name' | 'alt'>>;
|
|
89
|
+
max?: number;
|
|
90
|
+
size?: AvatarSize;
|
|
91
|
+
className?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|
95
|
+
avatars,
|
|
96
|
+
max = 4,
|
|
97
|
+
size = 'sm',
|
|
98
|
+
className = ''
|
|
99
|
+
}) => {
|
|
100
|
+
const visible = avatars.slice(0, max);
|
|
101
|
+
const overflow = avatars.length - max;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={`flex items-center ${className}`}>
|
|
105
|
+
{visible.map((avatar, index) => (
|
|
106
|
+
<div
|
|
107
|
+
key={`${avatar.name || avatar.alt || index}-${index}`}
|
|
108
|
+
className={index > 0 ? '-ml-3' : ''}
|
|
109
|
+
style={{ zIndex: visible.length - index }}
|
|
110
|
+
>
|
|
111
|
+
<Avatar {...avatar} size={size} ring />
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
{overflow > 0 && (
|
|
115
|
+
<div className="-ml-3 z-0">
|
|
116
|
+
<div className={`${sizeMap[size].container} rounded-full bg-bg-card border border-border-app flex items-center justify-center text-[10px] font-bold text-text-muted`}>
|
|
117
|
+
+{overflow}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant = 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
5
|
+
export type BadgeSize = 'sm' | 'md' | 'lg';
|
|
6
|
+
|
|
7
|
+
export interface BadgeProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
variant?: BadgeVariant;
|
|
10
|
+
size?: BadgeSize;
|
|
11
|
+
dot?: boolean;
|
|
12
|
+
pulse?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const variantStyles: Record<BadgeVariant, string> = {
|
|
17
|
+
accent: 'bg-accent/15 text-accent border-accent/25',
|
|
18
|
+
success: 'bg-green-500/15 text-green-500 border-green-500/25',
|
|
19
|
+
warning: 'bg-yellow-500/15 text-yellow-500 border-yellow-500/25',
|
|
20
|
+
danger: 'bg-red-500/15 text-red-500 border-red-500/25',
|
|
21
|
+
info: 'bg-blue-500/15 text-blue-500 border-blue-500/25',
|
|
22
|
+
neutral: 'bg-bg-app text-text-muted border-border-app'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sizeStyles: Record<BadgeSize, string> = {
|
|
26
|
+
sm: 'text-[9px] px-2 py-0.5 gap-1',
|
|
27
|
+
md: 'text-[10px] px-2.5 py-1 gap-1.5',
|
|
28
|
+
lg: 'text-xs px-3 py-1.5 gap-2'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Badge: React.FC<BadgeProps> = ({
|
|
32
|
+
children,
|
|
33
|
+
variant = 'accent',
|
|
34
|
+
size = 'md',
|
|
35
|
+
dot = false,
|
|
36
|
+
pulse = false,
|
|
37
|
+
className = ''
|
|
38
|
+
}) => {
|
|
39
|
+
return (
|
|
40
|
+
<motion.span
|
|
41
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
42
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
43
|
+
transition={{ type: 'spring' as const, stiffness: 400, damping: 24 }}
|
|
44
|
+
className={`inline-flex items-center font-extrabold uppercase tracking-wider rounded-full border whitespace-nowrap ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
|
|
45
|
+
>
|
|
46
|
+
{dot && (
|
|
47
|
+
<span className="relative flex h-2 w-2">
|
|
48
|
+
{pulse && (
|
|
49
|
+
<span className={`absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping ${
|
|
50
|
+
variant === 'accent' ? 'bg-accent' :
|
|
51
|
+
variant === 'success' ? 'bg-green-500' :
|
|
52
|
+
variant === 'warning' ? 'bg-yellow-500' :
|
|
53
|
+
variant === 'danger' ? 'bg-red-500' :
|
|
54
|
+
variant === 'info' ? 'bg-blue-500' : 'bg-text-muted'
|
|
55
|
+
}`} />
|
|
56
|
+
)}
|
|
57
|
+
<span className={`relative inline-flex rounded-full h-2 w-2 ${
|
|
58
|
+
variant === 'accent' ? 'bg-accent' :
|
|
59
|
+
variant === 'success' ? 'bg-green-500' :
|
|
60
|
+
variant === 'warning' ? 'bg-yellow-500' :
|
|
61
|
+
variant === 'danger' ? 'bg-red-500' :
|
|
62
|
+
variant === 'info' ? 'bg-blue-500' : 'bg-text-muted'
|
|
63
|
+
}`} />
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
{children}
|
|
67
|
+
</motion.span>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export interface BadgeGroupProps {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
className?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const BadgeGroup: React.FC<BadgeGroupProps> = ({ children, className = '' }) => (
|
|
77
|
+
<div className={`flex flex-wrap items-center gap-2 ${className}`}>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { ChevronRight, Home } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface BreadcrumbItem {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
href?: string;
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BreadcrumbProps {
|
|
13
|
+
items: BreadcrumbItem[];
|
|
14
|
+
onNavigate?: (id: string) => void;
|
|
15
|
+
showHome?: boolean;
|
|
16
|
+
separator?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Breadcrumb: React.FC<BreadcrumbProps> = ({
|
|
21
|
+
items,
|
|
22
|
+
onNavigate,
|
|
23
|
+
showHome = true,
|
|
24
|
+
separator,
|
|
25
|
+
className = ''
|
|
26
|
+
}) => {
|
|
27
|
+
const Sep = separator ?? <ChevronRight className="w-3.5 h-3.5 text-text-muted/50 flex-shrink-0" />;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<nav aria-label="Breadcrumb" className={`flex items-center flex-wrap gap-1.5 text-sm ${className}`}>
|
|
31
|
+
{showHome && (
|
|
32
|
+
<>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={() => onNavigate?.('home')}
|
|
36
|
+
className="inline-flex items-center gap-1 text-text-muted hover:text-accent transition-colors cursor-pointer"
|
|
37
|
+
>
|
|
38
|
+
<Home className="w-3.5 h-3.5" />
|
|
39
|
+
<span className="font-semibold">Inicio</span>
|
|
40
|
+
</button>
|
|
41
|
+
{items.length > 0 && Sep}
|
|
42
|
+
</>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{items.map((item, index) => {
|
|
46
|
+
const isLast = index === items.length - 1;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<React.Fragment key={item.id}>
|
|
50
|
+
{isLast ? (
|
|
51
|
+
<motion.span
|
|
52
|
+
layoutId="breadcrumb-active"
|
|
53
|
+
className="inline-flex items-center gap-1.5 font-extrabold text-accent font-display"
|
|
54
|
+
>
|
|
55
|
+
{item.icon}
|
|
56
|
+
{item.label}
|
|
57
|
+
</motion.span>
|
|
58
|
+
) : (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => onNavigate?.(item.id)}
|
|
62
|
+
className="inline-flex items-center gap-1.5 text-text-muted hover:text-text-main font-semibold transition-colors cursor-pointer"
|
|
63
|
+
>
|
|
64
|
+
{item.icon}
|
|
65
|
+
{item.label}
|
|
66
|
+
</button>
|
|
67
|
+
)}
|
|
68
|
+
{!isLast && Sep}
|
|
69
|
+
</React.Fragment>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</nav>
|
|
73
|
+
);
|
|
74
|
+
};
|