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,91 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
5
|
+
|
|
6
|
+
export interface PopoverProps {
|
|
7
|
+
trigger: React.ReactNode;
|
|
8
|
+
content: React.ReactNode;
|
|
9
|
+
placement?: PopoverPlacement;
|
|
10
|
+
isOpen?: boolean;
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
closeOnClickOutside?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const placementClasses: Record<PopoverPlacement, string> = {
|
|
18
|
+
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
|
19
|
+
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
|
20
|
+
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
21
|
+
right: 'left-full top-1/2 -translate-y-1/2 ml-2'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Popover: React.FC<PopoverProps> = ({
|
|
25
|
+
trigger,
|
|
26
|
+
content,
|
|
27
|
+
placement = 'bottom',
|
|
28
|
+
isOpen: controlledOpen,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
closeOnClickOutside = true,
|
|
31
|
+
disabled = false,
|
|
32
|
+
className = ''
|
|
33
|
+
}) => {
|
|
34
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
35
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
const isControlled = controlledOpen !== undefined;
|
|
38
|
+
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
39
|
+
|
|
40
|
+
const setOpen = (open: boolean) => {
|
|
41
|
+
if (disabled) return;
|
|
42
|
+
if (!isControlled) setInternalOpen(open);
|
|
43
|
+
onOpenChange?.(open);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!isOpen || !closeOnClickOutside) return;
|
|
48
|
+
|
|
49
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
50
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
56
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
57
|
+
}, [isOpen, closeOnClickOutside]);
|
|
58
|
+
|
|
59
|
+
const animationVariants = {
|
|
60
|
+
hidden: { opacity: 0, scale: 0.92, y: placement === 'top' ? 6 : placement === 'bottom' ? -6 : 0 },
|
|
61
|
+
visible: {
|
|
62
|
+
opacity: 1,
|
|
63
|
+
scale: 1,
|
|
64
|
+
y: 0,
|
|
65
|
+
transition: { type: 'spring' as const, stiffness: 400, damping: 26 }
|
|
66
|
+
},
|
|
67
|
+
exit: { opacity: 0, scale: 0.92, transition: { duration: 0.12 } }
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={containerRef} className={`relative inline-block ${className}`}>
|
|
72
|
+
<div onClick={() => setOpen(!isOpen)} className={disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'}>
|
|
73
|
+
{trigger}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<AnimatePresence>
|
|
77
|
+
{isOpen && !disabled && (
|
|
78
|
+
<motion.div
|
|
79
|
+
variants={animationVariants}
|
|
80
|
+
initial="hidden"
|
|
81
|
+
animate="visible"
|
|
82
|
+
exit="exit"
|
|
83
|
+
className={`absolute z-50 min-w-[200px] p-4 glass rounded-2xl border border-border-app shadow-xl ${placementClasses[placement]}`}
|
|
84
|
+
>
|
|
85
|
+
{content}
|
|
86
|
+
</motion.div>
|
|
87
|
+
)}
|
|
88
|
+
</AnimatePresence>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { Check, Sparkles } from 'lucide-react';
|
|
4
|
+
import { GlassCard } from './GlassCard';
|
|
5
|
+
import { GlassButton } from './GlassButton';
|
|
6
|
+
import { Badge } from './Badge';
|
|
7
|
+
|
|
8
|
+
export interface PricingPlan {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
price: string;
|
|
12
|
+
period?: string;
|
|
13
|
+
description: string;
|
|
14
|
+
features: string[];
|
|
15
|
+
highlighted?: boolean;
|
|
16
|
+
ctaLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PricingTableProps {
|
|
20
|
+
plans?: PricingPlan[];
|
|
21
|
+
onSelectPlan?: (planId: string) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PLANS: PricingPlan[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'free',
|
|
29
|
+
name: 'Starter',
|
|
30
|
+
price: '$0',
|
|
31
|
+
period: '/mes',
|
|
32
|
+
description: 'Para explorar el catálogo y copiar componentes.',
|
|
33
|
+
features: ['10 componentes/mes', 'Docs estáticas', 'Tema default'],
|
|
34
|
+
ctaLabel: 'Empezar gratis',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'pro',
|
|
38
|
+
name: 'Pro',
|
|
39
|
+
price: '$29',
|
|
40
|
+
period: '/mes',
|
|
41
|
+
description: 'Para equipos que construyen productos con AlexUI.',
|
|
42
|
+
features: ['Catálogo completo', 'CLI alexui', 'Prompts IA', 'Temas custom'],
|
|
43
|
+
highlighted: true,
|
|
44
|
+
ctaLabel: 'Elegir Pro',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'enterprise',
|
|
48
|
+
name: 'Enterprise',
|
|
49
|
+
price: 'Custom',
|
|
50
|
+
description: 'Soporte dedicado y componentes a medida.',
|
|
51
|
+
features: ['SLA prioritario', 'Bloques exclusivos', 'Onboarding', 'SSO'],
|
|
52
|
+
ctaLabel: 'Contactar ventas',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export const PricingTable: React.FC<PricingTableProps> = ({
|
|
57
|
+
plans = DEFAULT_PLANS,
|
|
58
|
+
onSelectPlan,
|
|
59
|
+
disabled = false,
|
|
60
|
+
className = ''
|
|
61
|
+
}) => (
|
|
62
|
+
<div className={`w-full grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}>
|
|
63
|
+
{plans.map((plan, index) => (
|
|
64
|
+
<motion.div
|
|
65
|
+
key={plan.id}
|
|
66
|
+
initial={{ opacity: 0, y: 12 }}
|
|
67
|
+
animate={{ opacity: 1, y: 0 }}
|
|
68
|
+
transition={{ delay: index * 0.06 }}
|
|
69
|
+
className={plan.highlighted ? 'md:-mt-2 md:mb-2' : ''}
|
|
70
|
+
>
|
|
71
|
+
<GlassCard
|
|
72
|
+
variant={plan.highlighted ? 'elevated' : 'default'}
|
|
73
|
+
padding="lg"
|
|
74
|
+
hoverable={!plan.highlighted}
|
|
75
|
+
className={`h-full flex flex-col ${plan.highlighted ? 'ring-2 ring-accent/40' : ''}`}
|
|
76
|
+
headerAction={
|
|
77
|
+
plan.highlighted ? (
|
|
78
|
+
<Badge variant="accent" size="sm" dot pulse>
|
|
79
|
+
Popular
|
|
80
|
+
</Badge>
|
|
81
|
+
) : undefined
|
|
82
|
+
}
|
|
83
|
+
title={plan.name}
|
|
84
|
+
description={plan.description}
|
|
85
|
+
footer={
|
|
86
|
+
<GlassButton
|
|
87
|
+
variant={plan.highlighted ? 'primary' : 'secondary'}
|
|
88
|
+
className="w-full"
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
leftIcon={plan.highlighted ? <Sparkles className="w-4 h-4" /> : undefined}
|
|
91
|
+
onClick={() => onSelectPlan?.(plan.id)}
|
|
92
|
+
>
|
|
93
|
+
{plan.ctaLabel ?? 'Seleccionar'}
|
|
94
|
+
</GlassButton>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
<div className="flex items-baseline gap-1 mb-4">
|
|
98
|
+
<span className="text-3xl font-extrabold text-text-main font-display">{plan.price}</span>
|
|
99
|
+
{plan.period && <span className="text-xs text-text-muted">{plan.period}</span>}
|
|
100
|
+
</div>
|
|
101
|
+
<ul className="flex flex-col gap-2.5">
|
|
102
|
+
{plan.features.map((feature) => (
|
|
103
|
+
<li key={feature} className="flex items-start gap-2 text-sm text-text-muted">
|
|
104
|
+
<Check className="w-4 h-4 text-accent flex-shrink-0 mt-0.5" />
|
|
105
|
+
<span>{feature}</span>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
</GlassCard>
|
|
110
|
+
</motion.div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface ProgressBarProps {
|
|
5
|
+
value?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
showValue?: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
striped?: boolean;
|
|
10
|
+
indeterminate?: boolean;
|
|
11
|
+
glow?: boolean;
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
variant?: 'accent' | 'success' | 'warning' | 'danger' | 'info';
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ProgressBar: React.FC<ProgressBarProps> = ({
|
|
19
|
+
value = 0,
|
|
20
|
+
max = 100,
|
|
21
|
+
showValue = true,
|
|
22
|
+
label,
|
|
23
|
+
striped = false,
|
|
24
|
+
indeterminate = false,
|
|
25
|
+
glow = true,
|
|
26
|
+
size = 'md',
|
|
27
|
+
variant = 'accent',
|
|
28
|
+
disabled = false,
|
|
29
|
+
className = ''
|
|
30
|
+
}) => {
|
|
31
|
+
const percentage = indeterminate ? 100 : Math.min(100, Math.max(0, (value / max) * 100));
|
|
32
|
+
|
|
33
|
+
const sizeClasses = {
|
|
34
|
+
sm: 'h-1.5 text-[9px]',
|
|
35
|
+
md: 'h-3 text-[10px]',
|
|
36
|
+
lg: 'h-5 text-xs'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const variantStyles = {
|
|
40
|
+
accent: {
|
|
41
|
+
bg: 'bg-accent',
|
|
42
|
+
text: 'text-accent',
|
|
43
|
+
glow: 'shadow-[0_0_12px_var(--color-accent)]',
|
|
44
|
+
border: 'border-accent/20'
|
|
45
|
+
},
|
|
46
|
+
success: {
|
|
47
|
+
bg: 'bg-green-500',
|
|
48
|
+
text: 'text-green-500',
|
|
49
|
+
glow: 'shadow-[0_0_12px_rgba(34,197,94,0.5)]',
|
|
50
|
+
border: 'border-green-500/20'
|
|
51
|
+
},
|
|
52
|
+
warning: {
|
|
53
|
+
bg: 'bg-yellow-500',
|
|
54
|
+
text: 'text-yellow-500',
|
|
55
|
+
glow: 'shadow-[0_0_12px_rgba(234,179,8,0.5)]',
|
|
56
|
+
border: 'border-yellow-500/20'
|
|
57
|
+
},
|
|
58
|
+
danger: {
|
|
59
|
+
bg: 'bg-red-500',
|
|
60
|
+
text: 'text-red-500',
|
|
61
|
+
glow: 'shadow-[0_0_12px_rgba(239,68,68,0.5)]',
|
|
62
|
+
border: 'border-red-500/20'
|
|
63
|
+
},
|
|
64
|
+
info: {
|
|
65
|
+
bg: 'bg-blue-500',
|
|
66
|
+
text: 'text-blue-500',
|
|
67
|
+
glow: 'shadow-[0_0_12px_rgba(59,130,246,0.5)]',
|
|
68
|
+
border: 'border-blue-500/20'
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const currentStyle = variantStyles[variant];
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className={`w-full flex flex-col gap-1.5 select-none ${
|
|
76
|
+
disabled ? 'opacity-40 cursor-not-allowed pointer-events-none' : ''
|
|
77
|
+
} ${className}`}>
|
|
78
|
+
|
|
79
|
+
{/* Top Labels */}
|
|
80
|
+
{(label || (showValue && !indeterminate)) && (
|
|
81
|
+
<div className="flex items-center justify-between px-1">
|
|
82
|
+
{label && (
|
|
83
|
+
<span className="text-xs font-bold text-text-main flex items-center gap-1.5">
|
|
84
|
+
<span className={`w-1.5 h-1.5 rounded-full ${currentStyle.bg}`} />
|
|
85
|
+
{label}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{showValue && !indeterminate && (
|
|
89
|
+
<span className={`text-[10px] font-black font-mono tracking-wider ${currentStyle.text}`}>
|
|
90
|
+
{Math.round(percentage)}%
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Progress Track Container */}
|
|
97
|
+
<div className={`w-full bg-bg-card/45 border border-border-app/40 rounded-full relative overflow-hidden p-[2px] ${
|
|
98
|
+
sizeClasses[size]
|
|
99
|
+
} ${currentStyle.border}`}>
|
|
100
|
+
|
|
101
|
+
{/* Animated Bar Fill */}
|
|
102
|
+
<motion.div
|
|
103
|
+
initial={{ width: 0 }}
|
|
104
|
+
animate={
|
|
105
|
+
indeterminate
|
|
106
|
+
? { left: ['-100%', '100%'], width: '40%' }
|
|
107
|
+
: { width: `${percentage}%` }
|
|
108
|
+
}
|
|
109
|
+
transition={
|
|
110
|
+
indeterminate
|
|
111
|
+
? { repeat: Infinity, duration: 1.5, ease: 'easeInOut' }
|
|
112
|
+
: { type: 'spring', stiffness: 100, damping: 15 }
|
|
113
|
+
}
|
|
114
|
+
className={`h-full rounded-full relative overflow-hidden transition-all z-10 ${
|
|
115
|
+
currentStyle.bg
|
|
116
|
+
} ${glow && !disabled ? currentStyle.glow : ''} ${
|
|
117
|
+
indeterminate ? 'absolute top-[2px] bottom-[2px]' : 'w-full'
|
|
118
|
+
}`}
|
|
119
|
+
>
|
|
120
|
+
{/* Animated Glow Flow overlay */}
|
|
121
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent -translate-x-full animate-[shimmer_2s_infinite]" />
|
|
122
|
+
|
|
123
|
+
{/* Striped Background Style */}
|
|
124
|
+
{striped && (
|
|
125
|
+
<div
|
|
126
|
+
className="absolute inset-0 opacity-15"
|
|
127
|
+
style={{
|
|
128
|
+
backgroundImage: 'linear-gradient(45deg, #fff 25%, transparent 25%, transparent 50%, #fff 50%, #fff 75%, transparent 75%, transparent)',
|
|
129
|
+
backgroundSize: '16px 16px',
|
|
130
|
+
animation: 'progress-bar-stripes 1s linear infinite'
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</motion.div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* CSS Keyframes injected inline if not globally specified */}
|
|
138
|
+
<style>{`
|
|
139
|
+
@keyframes progress-bar-stripes {
|
|
140
|
+
from { background-position: 16px 0; }
|
|
141
|
+
to { background-position: 0 0; }
|
|
142
|
+
}
|
|
143
|
+
@keyframes shimmer {
|
|
144
|
+
100% {
|
|
145
|
+
transform: translateX(100%);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
`}</style>
|
|
149
|
+
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
// V2.0 Radio components - triggered recompile
|
|
5
|
+
// 1. CLASSIC RADIO BUTTON COMPONENT
|
|
6
|
+
export interface RadioButtonProps {
|
|
7
|
+
checked: boolean;
|
|
8
|
+
onChange: () => void;
|
|
9
|
+
label?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
isInvalid?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const RadioButton: React.FC<RadioButtonProps> = ({
|
|
18
|
+
checked,
|
|
19
|
+
onChange,
|
|
20
|
+
label,
|
|
21
|
+
disabled = false,
|
|
22
|
+
isInvalid = false,
|
|
23
|
+
className = '',
|
|
24
|
+
id
|
|
25
|
+
}) => {
|
|
26
|
+
const radioId = id || `radio-${Math.random().toString(36).substring(2, 9)}`;
|
|
27
|
+
|
|
28
|
+
const getBorderColor = () => {
|
|
29
|
+
if (isInvalid) return 'border-error';
|
|
30
|
+
if (checked && !disabled) return 'border-accent';
|
|
31
|
+
return 'border-border-app hover:border-text-muted/50';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={`flex items-center gap-3 select-none ${className}`}>
|
|
36
|
+
<button
|
|
37
|
+
id={radioId}
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={onChange}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
className={`w-5.5 h-5.5 rounded-full border flex items-center justify-center transition-all duration-200 focus:outline-hidden ${getBorderColor()} ${
|
|
42
|
+
disabled ? 'opacity-40 cursor-not-allowed bg-bg-app/10' : 'cursor-pointer'
|
|
43
|
+
}`}
|
|
44
|
+
style={{
|
|
45
|
+
boxShadow: checked && !disabled && !isInvalid
|
|
46
|
+
? '0 0 8px var(--color-accent-glow)'
|
|
47
|
+
: isInvalid
|
|
48
|
+
? '0 0 8px var(--color-error-glow)'
|
|
49
|
+
: 'none'
|
|
50
|
+
}}
|
|
51
|
+
role="radio"
|
|
52
|
+
aria-checked={checked}
|
|
53
|
+
>
|
|
54
|
+
{checked && (
|
|
55
|
+
<motion.div
|
|
56
|
+
initial={{ scale: 0 }}
|
|
57
|
+
animate={{ scale: 1 }}
|
|
58
|
+
transition={{ type: 'spring', stiffness: 500, damping: 20 }}
|
|
59
|
+
className={`w-2.5 h-2.5 rounded-full ${isInvalid ? 'bg-error' : 'bg-accent'}`}
|
|
60
|
+
style={{ backgroundColor: disabled ? 'var(--color-text-muted)' : undefined }}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
{label && (
|
|
66
|
+
<label
|
|
67
|
+
htmlFor={radioId}
|
|
68
|
+
onClick={onChange}
|
|
69
|
+
className={`text-sm font-medium ${
|
|
70
|
+
disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer text-text-main'
|
|
71
|
+
} ${isInvalid ? 'text-error' : ''}`}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</label>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 2. RADIO GROUP COMPONENT (Includes layoutId animation gliding between selections)
|
|
81
|
+
export interface RadioOption {
|
|
82
|
+
value: string;
|
|
83
|
+
label: string;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RadioGroupProps {
|
|
88
|
+
options: RadioOption[];
|
|
89
|
+
value: string;
|
|
90
|
+
onChange: (value: string) => void;
|
|
91
|
+
orientation?: 'horizontal' | 'vertical';
|
|
92
|
+
className?: string;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
name?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
98
|
+
options,
|
|
99
|
+
value,
|
|
100
|
+
onChange,
|
|
101
|
+
orientation = 'horizontal',
|
|
102
|
+
className = '',
|
|
103
|
+
disabled = false,
|
|
104
|
+
name
|
|
105
|
+
}) => {
|
|
106
|
+
const groupName = name || `radiogroup-${Math.random().toString(36).substring(2, 9)}`;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={`flex ${
|
|
111
|
+
orientation === 'horizontal' ? 'flex-row flex-wrap gap-5' : 'flex-col gap-3.5'
|
|
112
|
+
} ${className}`}
|
|
113
|
+
role="radiogroup"
|
|
114
|
+
>
|
|
115
|
+
{options.map((opt) => (
|
|
116
|
+
<RadioButton
|
|
117
|
+
key={opt.value}
|
|
118
|
+
checked={value === opt.value}
|
|
119
|
+
onChange={() => !disabled && !opt.disabled && onChange(opt.value)}
|
|
120
|
+
label={opt.label}
|
|
121
|
+
disabled={disabled || opt.disabled}
|
|
122
|
+
name={groupName}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// 3. PREMIUM GLASSMORPHIC RADIO CARDS (Selectable cards list)
|
|
130
|
+
export interface RadioCardOption {
|
|
131
|
+
value: string;
|
|
132
|
+
title: string;
|
|
133
|
+
description: string;
|
|
134
|
+
icon?: React.ReactNode;
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface RadioCardGroupProps {
|
|
139
|
+
options: RadioCardOption[];
|
|
140
|
+
value: string;
|
|
141
|
+
onChange: (value: string) => void;
|
|
142
|
+
className?: string;
|
|
143
|
+
disabled?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const RadioCardGroup: React.FC<RadioCardGroupProps> = ({
|
|
147
|
+
options,
|
|
148
|
+
value,
|
|
149
|
+
onChange,
|
|
150
|
+
className = '',
|
|
151
|
+
disabled = false
|
|
152
|
+
}) => {
|
|
153
|
+
return (
|
|
154
|
+
<div className={`grid grid-cols-1 md:grid-cols-2 gap-4 w-full ${className}`}>
|
|
155
|
+
{options.map((opt) => {
|
|
156
|
+
const isSelected = value === opt.value;
|
|
157
|
+
const cardDisabled = disabled || opt.disabled;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<button
|
|
161
|
+
key={opt.value}
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() => !cardDisabled && onChange(opt.value)}
|
|
164
|
+
disabled={cardDisabled}
|
|
165
|
+
className={`relative p-5 rounded-2xl border text-left flex items-start gap-4 transition-all duration-300 focus:outline-hidden ${
|
|
166
|
+
isSelected && !cardDisabled
|
|
167
|
+
? 'bg-accent/5 border-accent shadow-md'
|
|
168
|
+
: 'bg-bg-card/40 border-border-app hover:border-border-app/80'
|
|
169
|
+
} ${cardDisabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
170
|
+
style={{
|
|
171
|
+
boxShadow: isSelected && !cardDisabled
|
|
172
|
+
? '0 0 16px var(--color-accent-glow)'
|
|
173
|
+
: 'none'
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{/* selection indicator circle */}
|
|
177
|
+
<div className={`w-5 h-5 rounded-full border flex items-center justify-center flex-shrink-0 mt-0.5 transition-colors ${
|
|
178
|
+
isSelected && !cardDisabled ? 'border-accent' : 'border-border-app'
|
|
179
|
+
}`}>
|
|
180
|
+
{isSelected && (
|
|
181
|
+
<motion.div
|
|
182
|
+
layoutId={`active-indicator-${opt.value}`}
|
|
183
|
+
className="w-2.5 h-2.5 rounded-full bg-accent"
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Optional icon */}
|
|
189
|
+
{opt.icon && (
|
|
190
|
+
<div className={`p-2 rounded-xl flex-shrink-0 flex items-center justify-center transition-colors ${
|
|
191
|
+
isSelected && !cardDisabled ? 'bg-accent/10 text-accent' : 'bg-bg-app text-text-muted'
|
|
192
|
+
}`}>
|
|
193
|
+
{opt.icon}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Title & Description */}
|
|
198
|
+
<div className="flex-1 flex flex-col gap-1 pr-2">
|
|
199
|
+
<span className="text-sm font-extrabold text-text-main leading-none">
|
|
200
|
+
{opt.title}
|
|
201
|
+
</span>
|
|
202
|
+
<span className="text-xs text-text-muted leading-normal">
|
|
203
|
+
{opt.description}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
</button>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
import { Star } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export interface RatingProps {
|
|
6
|
+
value?: number;
|
|
7
|
+
onChange?: (value: number) => void;
|
|
8
|
+
max?: number;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
showValue?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizeMap = {
|
|
17
|
+
sm: 'w-4 h-4',
|
|
18
|
+
md: 'w-6 h-6',
|
|
19
|
+
lg: 'w-8 h-8'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Rating: React.FC<RatingProps> = ({
|
|
23
|
+
value = 0,
|
|
24
|
+
onChange,
|
|
25
|
+
max = 5,
|
|
26
|
+
size = 'md',
|
|
27
|
+
readOnly = false,
|
|
28
|
+
showValue = false,
|
|
29
|
+
disabled = false,
|
|
30
|
+
className = ''
|
|
31
|
+
}) => {
|
|
32
|
+
const [hoverValue, setHoverValue] = useState<number | null>(null);
|
|
33
|
+
const displayValue = hoverValue ?? value;
|
|
34
|
+
const isInteractive = !readOnly && !disabled && !!onChange;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={`inline-flex flex-col gap-1.5 select-none ${disabled ? 'opacity-40 cursor-not-allowed' : ''} ${className}`}>
|
|
38
|
+
<div
|
|
39
|
+
className="inline-flex items-center gap-1"
|
|
40
|
+
onMouseLeave={() => isInteractive && setHoverValue(null)}
|
|
41
|
+
role={isInteractive ? 'slider' : undefined}
|
|
42
|
+
aria-valuenow={value}
|
|
43
|
+
aria-valuemin={0}
|
|
44
|
+
aria-valuemax={max}
|
|
45
|
+
>
|
|
46
|
+
{Array.from({ length: max }, (_, i) => {
|
|
47
|
+
const starValue = i + 1;
|
|
48
|
+
const isFilled = starValue <= displayValue;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<motion.button
|
|
52
|
+
key={starValue}
|
|
53
|
+
type="button"
|
|
54
|
+
disabled={!isInteractive}
|
|
55
|
+
onClick={() => onChange?.(starValue)}
|
|
56
|
+
onMouseEnter={() => isInteractive && setHoverValue(starValue)}
|
|
57
|
+
whileHover={isInteractive ? { scale: 1.15, rotate: -8 } : undefined}
|
|
58
|
+
whileTap={isInteractive ? { scale: 0.9 } : undefined}
|
|
59
|
+
transition={{ type: 'spring' as const, stiffness: 500, damping: 22 }}
|
|
60
|
+
className={`relative p-0.5 ${isInteractive ? 'cursor-pointer' : 'cursor-default'}`}
|
|
61
|
+
aria-label={`${starValue} de ${max} estrellas`}
|
|
62
|
+
>
|
|
63
|
+
<Star
|
|
64
|
+
className={`${sizeMap[size]} transition-colors duration-200 ${
|
|
65
|
+
isFilled
|
|
66
|
+
? 'text-yellow-400 fill-yellow-400 drop-shadow-[0_0_6px_rgba(250,204,21,0.5)]'
|
|
67
|
+
: 'text-border-app fill-transparent'
|
|
68
|
+
}`}
|
|
69
|
+
/>
|
|
70
|
+
</motion.button>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{showValue && (
|
|
76
|
+
<span className="text-[10px] font-bold text-text-muted font-mono">
|
|
77
|
+
{value.toFixed(1)} / {max}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|