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