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,212 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+
4
+ // 1. INDIVIDUAL CHECKBOX COMPONENT
5
+ export interface CheckboxProps {
6
+ checked: boolean;
7
+ onChange: (checked: boolean) => void;
8
+ label?: string;
9
+ disabled?: boolean;
10
+ isInvalid?: boolean;
11
+ className?: string;
12
+ id?: string;
13
+ }
14
+
15
+ export const Checkbox: React.FC<CheckboxProps> = ({
16
+ checked,
17
+ onChange,
18
+ label,
19
+ disabled = false,
20
+ isInvalid = false,
21
+ className = '',
22
+ id
23
+ }) => {
24
+ const checkboxId = id || `check-${Math.random().toString(36).substring(2, 9)}`;
25
+
26
+ const handleToggle = () => {
27
+ if (disabled) return;
28
+ onChange(!checked);
29
+ };
30
+
31
+ const getBorderColor = () => {
32
+ if (isInvalid) return 'border-error';
33
+ if (checked && !disabled) return 'border-accent';
34
+ return 'border-border-app hover:border-text-muted/50';
35
+ };
36
+
37
+ return (
38
+ <div className={`flex items-center gap-3 select-none ${className}`}>
39
+ <button
40
+ id={checkboxId}
41
+ type="button"
42
+ onClick={handleToggle}
43
+ disabled={disabled}
44
+ className={`w-5.5 h-5.5 rounded-md border flex items-center justify-center transition-all duration-200 focus:outline-hidden ${getBorderColor()} ${
45
+ disabled ? 'opacity-40 cursor-not-allowed bg-bg-app/10' : 'cursor-pointer'
46
+ }`}
47
+ style={{
48
+ boxShadow: checked && !disabled && !isInvalid
49
+ ? '0 0 8px var(--color-accent-glow)'
50
+ : isInvalid
51
+ ? '0 0 8px var(--color-error-glow)'
52
+ : 'none',
53
+ backgroundColor: checked && !disabled && !isInvalid
54
+ ? 'var(--color-accent)'
55
+ : 'transparent'
56
+ }}
57
+ role="checkbox"
58
+ aria-checked={checked}
59
+ >
60
+ <AnimatePresence mode="wait">
61
+ {checked && (
62
+ <motion.svg
63
+ initial={{ scale: 0.6, opacity: 0 }}
64
+ animate={{ scale: 1, opacity: 1 }}
65
+ exit={{ scale: 0.6, opacity: 0 }}
66
+ transition={{ type: 'spring', stiffness: 500, damping: 25 }}
67
+ className="w-3.5 h-3.5 text-white stroke-2"
68
+ fill="none"
69
+ viewBox="0 0 24 24"
70
+ stroke="currentColor"
71
+ >
72
+ <motion.path
73
+ initial={{ pathLength: 0 }}
74
+ animate={{ pathLength: 1 }}
75
+ transition={{ duration: 0.2 }}
76
+ strokeLinecap="round"
77
+ strokeLinejoin="round"
78
+ d="M5 13l4 4L19 7"
79
+ />
80
+ </motion.svg>
81
+ )}
82
+ </AnimatePresence>
83
+ </button>
84
+
85
+ {label && (
86
+ <label
87
+ htmlFor={checkboxId}
88
+ onClick={handleToggle}
89
+ className={`text-sm font-medium ${
90
+ disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer text-text-main'
91
+ } ${isInvalid ? 'text-error' : ''}`}
92
+ >
93
+ {label}
94
+ </label>
95
+ )}
96
+ </div>
97
+ );
98
+ };
99
+
100
+ // 2. CHECKBOX GROUP COMPONENT
101
+ export interface CheckboxOption {
102
+ value: string;
103
+ label: string;
104
+ disabled?: boolean;
105
+ }
106
+
107
+ export interface CheckboxGroupProps {
108
+ options: CheckboxOption[];
109
+ value: string[];
110
+ onChange: (value: string[]) => void;
111
+ orientation?: 'horizontal' | 'vertical';
112
+ className?: string;
113
+ disabled?: boolean;
114
+ }
115
+
116
+ export const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
117
+ options,
118
+ value,
119
+ onChange,
120
+ orientation = 'horizontal',
121
+ className = '',
122
+ disabled = false
123
+ }) => {
124
+ const handleOptionChange = (optionValue: string, checked: boolean) => {
125
+ if (disabled) return;
126
+ if (checked) {
127
+ onChange([...value, optionValue]);
128
+ } else {
129
+ onChange(value.filter((val) => val !== optionValue));
130
+ }
131
+ };
132
+
133
+ return (
134
+ <div
135
+ className={`flex ${
136
+ orientation === 'horizontal' ? 'flex-row flex-wrap gap-5' : 'flex-col gap-3.5'
137
+ } ${className}`}
138
+ >
139
+ {options.map((opt) => (
140
+ <Checkbox
141
+ key={opt.value}
142
+ checked={value.includes(opt.value)}
143
+ onChange={(checked) => handleOptionChange(opt.value, checked)}
144
+ label={opt.label}
145
+ disabled={disabled || opt.disabled}
146
+ />
147
+ ))}
148
+ </div>
149
+ );
150
+ };
151
+
152
+ // 3. INTERACTIVE CHECKLIST COMPONENT (Todo item lists)
153
+ export interface ChecklistItem {
154
+ id: string | number;
155
+ label: string;
156
+ checked: boolean;
157
+ disabled?: boolean;
158
+ }
159
+
160
+ export interface ChecklistProps {
161
+ items: ChecklistItem[];
162
+ onChange: (items: ChecklistItem[]) => void;
163
+ className?: string;
164
+ }
165
+
166
+ export const Checklist: React.FC<ChecklistProps> = ({
167
+ items,
168
+ onChange,
169
+ className = ''
170
+ }) => {
171
+ const handleItemToggle = (itemId: string | number) => {
172
+ const nextItems = items.map((item) => {
173
+ if (item.id === itemId) {
174
+ return { ...item, checked: !item.checked };
175
+ }
176
+ return item;
177
+ });
178
+ onChange(nextItems);
179
+ };
180
+
181
+ return (
182
+ <div className={`flex flex-col gap-2 bg-bg-card/40 border border-border-app/40 rounded-2xl p-4 w-full ${className}`}>
183
+ {items.map((item) => (
184
+ <div
185
+ key={item.id}
186
+ className={`flex items-center justify-between p-2.5 rounded-xl border border-transparent transition-all duration-300 ${
187
+ item.checked
188
+ ? 'bg-accent/5 border-accent/10'
189
+ : 'hover:bg-bg-app/40'
190
+ } ${item.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
191
+ >
192
+ <Checkbox
193
+ checked={item.checked}
194
+ onChange={() => handleItemToggle(item.id)}
195
+ label={item.label}
196
+ disabled={item.disabled}
197
+ className="flex-1"
198
+ />
199
+ {item.checked && (
200
+ <motion.span
201
+ initial={{ scale: 0, opacity: 0 }}
202
+ animate={{ scale: 1, opacity: 1 }}
203
+ className="text-[10px] bg-accent/20 border border-accent/20 px-2 py-0.5 rounded-full text-accent font-bold font-mono"
204
+ >
205
+ Completado
206
+ </motion.span>
207
+ )}
208
+ </div>
209
+ ))}
210
+ </div>
211
+ );
212
+ };
@@ -0,0 +1,152 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X } from 'lucide-react';
4
+
5
+ export interface ChipsInputProps {
6
+ value: string[];
7
+ onChange: (value: string[]) => void;
8
+ placeholder?: string;
9
+ label?: string;
10
+ disabled?: boolean;
11
+ isInvalid?: boolean;
12
+ separator?: ','; // support comma as separator
13
+ blacklist?: string[]; // block certain values
14
+ className?: string;
15
+ }
16
+
17
+ export const ChipsInput: React.FC<ChipsInputProps> = ({
18
+ value,
19
+ onChange,
20
+ placeholder = 'Escribe y presiona Enter...',
21
+ label,
22
+ disabled = false,
23
+ isInvalid = false,
24
+ separator = ',',
25
+ blacklist = [],
26
+ className = ''
27
+ }) => {
28
+ const [inputValue, setInputValue] = useState('');
29
+ const [isFocused, setIsFocused] = useState(false);
30
+ const inputRef = useRef<HTMLInputElement>(null);
31
+
32
+ const addChip = (text: string) => {
33
+ const trimmed = text.trim();
34
+ if (!trimmed) return;
35
+
36
+ // Check duplicate
37
+ if (value.includes(trimmed)) return;
38
+
39
+ // Check blacklist / key blocking filter
40
+ if (blacklist.includes(trimmed.toLowerCase())) return;
41
+
42
+ onChange([...value, trimmed]);
43
+ setInputValue('');
44
+ };
45
+
46
+ const removeChip = (indexToRemove: number) => {
47
+ onChange(value.filter((_, idx) => idx !== indexToRemove));
48
+ };
49
+
50
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
51
+ if (e.key === 'Enter') {
52
+ e.preventDefault();
53
+ addChip(inputValue);
54
+ } else if (e.key === 'Backspace' && inputValue === '' && value.length > 0) {
55
+ removeChip(value.length - 1);
56
+ }
57
+ };
58
+
59
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
60
+ const val = e.target.value;
61
+
62
+ if (separator === ',' && val.endsWith(',')) {
63
+ addChip(val.slice(0, -1));
64
+ } else {
65
+ setInputValue(val);
66
+ }
67
+ };
68
+
69
+ return (
70
+ <div className={`w-full flex flex-col gap-1.5 ${className}`}>
71
+ {label && (
72
+ <span className="text-xs font-bold text-text-muted uppercase tracking-wider px-1">
73
+ {label}
74
+ </span>
75
+ )}
76
+
77
+ {/* Input container wrapper */}
78
+ <div className="relative rounded-xl overflow-hidden transition-all duration-300">
79
+
80
+ {/* Glow border ring */}
81
+ <motion.div
82
+ animate={{
83
+ opacity: isFocused && !disabled ? 1 : 0,
84
+ scale: isFocused && !disabled ? 1 : 0.98,
85
+ }}
86
+ transition={{ duration: 0.25 }}
87
+ className="absolute -inset-[1px] bg-gradient-to-r from-accent to-pink-500 rounded-xl pointer-events-none z-0 blur-[1px]"
88
+ />
89
+
90
+ {/* Chips Area Container */}
91
+ <div
92
+ onClick={() => !disabled && inputRef.current?.focus()}
93
+ className={`relative bg-bg-card/60 border rounded-xl z-10 p-2 flex flex-wrap gap-2 items-center transition-all duration-300 ${
94
+ isFocused && !disabled ? 'border-transparent' : 'border-border-app'
95
+ } ${disabled ? 'opacity-40 cursor-not-allowed select-none bg-bg-app/10' : 'cursor-text'}`}
96
+ style={{
97
+ boxShadow: isFocused && !disabled && !isInvalid
98
+ ? '0 0 10px var(--color-accent-glow)'
99
+ : isInvalid
100
+ ? '0 0 10px var(--color-error-glow)'
101
+ : 'none'
102
+ }}
103
+ >
104
+ {/* Render Chips List */}
105
+ <AnimatePresence>
106
+ {value.map((chip, idx) => (
107
+ <motion.div
108
+ key={chip}
109
+ initial={{ scale: 0.8, opacity: 0 }}
110
+ animate={{ scale: 1, opacity: 1 }}
111
+ exit={{ scale: 0.8, opacity: 0 }}
112
+ transition={{ type: 'spring', stiffness: 500, damping: 25 }}
113
+ className="flex items-center gap-1 bg-accent/10 border border-accent/20 px-2.5 py-1 rounded-lg text-xs font-semibold text-accent"
114
+ >
115
+ <span>{chip}</span>
116
+ <button
117
+ type="button"
118
+ onClick={(e) => {
119
+ e.stopPropagation();
120
+ if (!disabled) removeChip(idx);
121
+ }}
122
+ disabled={disabled}
123
+ className={`p-0.5 rounded-md hover:bg-accent/20 transition-colors focus:outline-hidden ${
124
+ disabled ? 'cursor-not-allowed' : 'cursor-pointer'
125
+ }`}
126
+ >
127
+ <X className="w-3 h-3" />
128
+ </button>
129
+ </motion.div>
130
+ ))}
131
+ </AnimatePresence>
132
+
133
+ {/* Typing input field */}
134
+ <input
135
+ ref={inputRef}
136
+ type="text"
137
+ value={inputValue}
138
+ onChange={handleInputChange}
139
+ onKeyDown={handleKeyDown}
140
+ onFocus={() => setIsFocused(true)}
141
+ onBlur={() => setIsFocused(false)}
142
+ disabled={disabled}
143
+ placeholder={value.length === 0 ? placeholder : ''}
144
+ className={`flex-1 min-w-[120px] bg-transparent py-1 px-2 text-sm text-text-main placeholder-text-muted/50 focus:outline-hidden ${
145
+ disabled ? 'cursor-not-allowed' : ''
146
+ }`}
147
+ />
148
+ </div>
149
+ </div>
150
+ </div>
151
+ );
152
+ };
@@ -0,0 +1,240 @@
1
+ import React, { useRef, useState, useEffect } from 'react';
2
+
3
+ export interface CircularKnobProps {
4
+ value: number;
5
+ onChange: (value: number) => void;
6
+ min?: number;
7
+ max?: number;
8
+ size?: number;
9
+ unit?: string;
10
+ label?: string;
11
+ variant?: '360' | 'semicircle'; // 360 full range, semicircle 180 (or 270 standard knob)
12
+ disabled?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ export const CircularKnob: React.FC<CircularKnobProps> = ({
17
+ value,
18
+ onChange,
19
+ min = 0,
20
+ max = 100,
21
+ size = 120,
22
+ unit = '%',
23
+ label = 'Volumen',
24
+ variant = 'semicircle',
25
+ disabled = false,
26
+ className = ''
27
+ }) => {
28
+ const knobRef = useRef<SVGSVGElement>(null);
29
+ const [isDragging, setIsDragging] = useState(false);
30
+
31
+ // Normalize value to percentage
32
+ const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
33
+
34
+ // Knob configuration depending on variant
35
+ // 'semicircle' (standard knob) goes from -135deg to +135deg (total 270deg span, gap at the bottom)
36
+ // '360' goes from 0deg to 360deg (starting at top)
37
+ const startAngle = variant === 'semicircle' ? -135 : 0;
38
+ const endAngle = variant === 'semicircle' ? 135 : 360;
39
+ const totalAngleSpan = endAngle - startAngle;
40
+
41
+ const currentAngle = startAngle + percentage * totalAngleSpan;
42
+
43
+ const updateValueFromAngle = (clientX: number, clientY: number) => {
44
+ if (!knobRef.current) return;
45
+ const rect = knobRef.current.getBoundingClientRect();
46
+ const cx = rect.left + rect.width / 2;
47
+ const cy = rect.top + rect.height / 2;
48
+
49
+ const dx = clientX - cx;
50
+ const dy = clientY - cy;
51
+
52
+ // Angle in degrees from -180 to 180 where 0 is right, 90 is bottom, -90 is top
53
+ let angle = Math.atan2(dy, dx) * (180 / Math.PI);
54
+
55
+ // Normalize angle relative to the top (0 deg is top, clockwise positive)
56
+ let angleFromTop = angle + 90;
57
+ if (angleFromTop < -180) angleFromTop += 360;
58
+ if (angleFromTop > 180) angleFromTop -= 360;
59
+
60
+ let targetPercent = 0;
61
+ if (variant === 'semicircle') {
62
+ // Standard active zone: -135 to 135.
63
+ // Gap is at the bottom (between 135 and 225 relative to top, i.e., 135 to 180 and -180 to -135)
64
+ let targetAngle = angleFromTop;
65
+ if (targetAngle > 135) targetAngle = 135;
66
+ if (targetAngle < -135) targetAngle = -135;
67
+
68
+ targetPercent = (targetAngle + 135) / 270;
69
+ } else {
70
+ // 360 mode: 0 to 360.
71
+ let angleFromTop360 = angle + 90;
72
+ if (angleFromTop360 < 0) angleFromTop360 += 360;
73
+ targetPercent = angleFromTop360 / 360;
74
+ }
75
+
76
+ const nextValue = min + targetPercent * (max - min);
77
+ onChange(Math.round(nextValue));
78
+ };
79
+
80
+ const handleMouseDown = (e: React.MouseEvent) => {
81
+ if (disabled) return;
82
+ setIsDragging(true);
83
+ updateValueFromAngle(e.clientX, e.clientY);
84
+ };
85
+
86
+ const handleTouchStart = (e: React.TouchEvent) => {
87
+ if (disabled) return;
88
+ setIsDragging(true);
89
+ updateValueFromAngle(e.touches[0].clientX, e.touches[0].clientY);
90
+ };
91
+
92
+ useEffect(() => {
93
+ const handleMouseMove = (e: MouseEvent) => {
94
+ if (!isDragging) return;
95
+ updateValueFromAngle(e.clientX, e.clientY);
96
+ };
97
+
98
+ const handleTouchMove = (e: TouchEvent) => {
99
+ if (!isDragging) return;
100
+ // Prevent scrolling while dragging knob
101
+ if (e.cancelable) e.preventDefault();
102
+ updateValueFromAngle(e.touches[0].clientX, e.touches[0].clientY);
103
+ };
104
+
105
+ const handleMouseUp = () => {
106
+ setIsDragging(false);
107
+ };
108
+
109
+ if (isDragging) {
110
+ window.addEventListener('mousemove', handleMouseMove);
111
+ window.addEventListener('mouseup', handleMouseUp);
112
+ window.addEventListener('touchmove', handleTouchMove, { passive: false });
113
+ window.addEventListener('touchend', handleMouseUp);
114
+ }
115
+
116
+ return () => {
117
+ window.removeEventListener('mousemove', handleMouseMove);
118
+ window.removeEventListener('mouseup', handleMouseUp);
119
+ window.removeEventListener('touchmove', handleTouchMove);
120
+ window.removeEventListener('touchend', handleMouseUp);
121
+ };
122
+ }, [isDragging]);
123
+
124
+ // SVG parameters for progress arc
125
+ const radius = size * 0.4;
126
+ const strokeWidth = size * 0.07;
127
+ const center = size / 2;
128
+ const circumference = 2 * Math.PI * radius;
129
+
130
+ // Semicircle stroke length adjustments
131
+ // If 270deg, length is 3/4 of circle
132
+ const arcLength = variant === 'semicircle' ? circumference * 0.75 : circumference;
133
+ const strokeDashoffset = arcLength - percentage * arcLength;
134
+ const strokeDasharray = `${arcLength} ${circumference}`;
135
+
136
+ // Rotate rotation mapping of progress bar
137
+ const arcRotation = variant === 'semicircle' ? 135 : -90;
138
+
139
+ return (
140
+ <div className={`flex flex-col items-center gap-2 select-none ${className}`}>
141
+
142
+ {/* Knob SVG container */}
143
+ <div
144
+ className={`relative ${
145
+ disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-grab'
146
+ } ${isDragging ? 'cursor-grabbing' : ''}`}
147
+ style={{ width: size, height: size }}
148
+ >
149
+ <svg
150
+ ref={knobRef}
151
+ width={size}
152
+ height={size}
153
+ onMouseDown={handleMouseDown}
154
+ onTouchStart={handleTouchStart}
155
+ className="w-full h-full overflow-visible"
156
+ >
157
+ {/* Subtle Outer Glow Ring on drag */}
158
+ {isDragging && !disabled && (
159
+ <circle
160
+ cx={center}
161
+ cy={center}
162
+ r={radius + strokeWidth / 2 + 4}
163
+ className="fill-none stroke-accent/5 stroke-2 blur-[2px]"
164
+ />
165
+ )}
166
+
167
+ {/* Background Arc Track */}
168
+ <circle
169
+ cx={center}
170
+ cy={center}
171
+ r={radius}
172
+ className="fill-none stroke-border-app/30"
173
+ strokeWidth={strokeWidth}
174
+ strokeDasharray={arcLength === circumference ? undefined : strokeDasharray}
175
+ style={{
176
+ transform: `rotate(${arcRotation}deg)`,
177
+ transformOrigin: '50% 50%',
178
+ strokeLinecap: 'round'
179
+ }}
180
+ />
181
+
182
+ {/* Glowing Active Progress Arc */}
183
+ <circle
184
+ cx={center}
185
+ cy={center}
186
+ r={radius}
187
+ className="fill-none stroke-accent transition-all duration-75"
188
+ strokeWidth={strokeWidth}
189
+ strokeDasharray={strokeDasharray}
190
+ strokeDashoffset={strokeDashoffset}
191
+ style={{
192
+ transform: `rotate(${arcRotation}deg)`,
193
+ transformOrigin: '50% 50%',
194
+ strokeLinecap: 'round',
195
+ filter: isDragging ? 'drop-shadow(0px 0px 4px var(--color-accent))' : 'none'
196
+ }}
197
+ />
198
+
199
+ {/* Inner Rotating Knob Dial Disc */}
200
+ <g
201
+ style={{
202
+ transform: `rotate(${currentAngle}deg)`,
203
+ transformOrigin: '50% 50%'
204
+ }}
205
+ className="transition-transform duration-75"
206
+ >
207
+ <circle
208
+ cx={center}
209
+ cy={center}
210
+ r={radius - strokeWidth / 2 - 2}
211
+ className="fill-bg-card stroke-border-app/50"
212
+ strokeWidth="1"
213
+ />
214
+ {/* Indicator Dot on the dial edge */}
215
+ <circle
216
+ cx={center}
217
+ cy={center - radius + strokeWidth + 4}
218
+ r="3.5"
219
+ className="fill-accent"
220
+ />
221
+ </g>
222
+ </svg>
223
+
224
+ {/* Value Display in the center of the Knob */}
225
+ <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
226
+ <span className="text-sm font-extrabold text-text-main font-mono leading-none flex items-baseline">
227
+ {value}
228
+ <span className="text-[10px] text-text-muted font-bold ml-0.5">{unit}</span>
229
+ </span>
230
+ {label && (
231
+ <span className="text-[9px] font-bold text-text-muted/70 uppercase tracking-widest mt-1">
232
+ {label}
233
+ </span>
234
+ )}
235
+ </div>
236
+ </div>
237
+
238
+ </div>
239
+ );
240
+ };
@@ -0,0 +1,67 @@
1
+ import React, { useMemo } from 'react';
2
+
3
+ export interface CodeVisualizerProps {
4
+ code: string;
5
+ className?: string;
6
+ }
7
+
8
+ // Custom Lexer / Parser using regex to tokenize TypeScript/JSX code on the fly
9
+ const highlightCode = (rawCode: string) => {
10
+ if (!rawCode) return '';
11
+
12
+ // Escape HTML entities to prevent unescaped injections
13
+ const escaped = rawCode
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;');
17
+
18
+ // Unified regular expression parser matching all core tokens sequentially:
19
+ // Group 1: Comments (line & block)
20
+ // Group 2: Strings (single, double, or backticks)
21
+ // Group 3: JavaScript/TypeScript Keywords
22
+ // Group 4: React / PascalCase Components (e.g., <Card3D / </Card3D)
23
+ // Group 5: HTML standard tags (e.g., <div / </div)
24
+ // Group 6: JSX Properties (e.g., isLoading= / onClick=)
25
+ // Group 7: Numeric values
26
+ const tokenRegex = /(\/\/.*|\/\*[\s\S]*?\*\/)|('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`)|(\b(?:export|import|const|let|var|function|return|if|else|switch|case|break|default|from|true|false|null|undefined|interface|type|extends|as|typeof|new|await|async|try|catch|finally|throw|class|implements|public|private|protected|readonly)\b)|(&lt;\/?[A-Z][a-zA-Z0-9_]*\b)|(&lt;\/?[a-z][a-zA-Z0-9_]*\b)|(\b[a-zA-Z0-9_]+(?=\s*=))|(\b\d+\b)/g;
27
+
28
+ return escaped.replace(tokenRegex, (match, comment, str, keyword, reactComp, htmlTag, jsxProp, num) => {
29
+ if (comment) {
30
+ return `<span class="text-text-muted/50 italic select-none">${match}</span>`;
31
+ }
32
+ if (str) {
33
+ return `<span class="text-amber-600 dark:text-amber-300 font-medium">${match}</span>`;
34
+ }
35
+ if (keyword) {
36
+ return `<span class="text-accent font-bold">${match}</span>`;
37
+ }
38
+ if (reactComp) {
39
+ return `<span class="text-pink-600 dark:text-pink-400 font-extrabold">${match}</span>`;
40
+ }
41
+ if (htmlTag) {
42
+ return `<span class="text-sky-600 dark:text-sky-400 font-bold">${match}</span>`;
43
+ }
44
+ if (jsxProp) {
45
+ return `<span class="text-purple-600 dark:text-purple-400 italic">${match}</span>`;
46
+ }
47
+ if (num) {
48
+ return `<span class="text-indigo-600 dark:text-indigo-400">${match}</span>`;
49
+ }
50
+ return match;
51
+ });
52
+ };
53
+
54
+ export const CodeVisualizer: React.FC<CodeVisualizerProps> = ({ code, className = '' }) => {
55
+ const highlightedHtml = useMemo(() => {
56
+ return highlightCode(code);
57
+ }, [code]);
58
+
59
+ return (
60
+ <pre className={`w-full h-full max-h-[500px] overflow-auto p-5 text-left font-mono text-xs text-text-main bg-bg-card/90 rounded-2xl leading-relaxed select-text border border-border-app/40 ${className}`}>
61
+ <code
62
+ className="block whitespace-pre select-all"
63
+ dangerouslySetInnerHTML={{ __html: highlightedHtml }}
64
+ />
65
+ </pre>
66
+ );
67
+ };