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,700 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
export interface ChartDataItem {
|
|
5
|
+
label: string;
|
|
6
|
+
value: number;
|
|
7
|
+
secondary?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ChartShowcaseProps {
|
|
11
|
+
data?: ChartDataItem[];
|
|
12
|
+
type?: 'line' | 'bar' | 'pie' | 'all';
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_DATA: ChartDataItem[] = [
|
|
17
|
+
{ label: 'Ene', value: 340, secondary: 'Registro Inicial' },
|
|
18
|
+
{ label: 'Feb', value: 450, secondary: 'Crecimiento Orgánico' },
|
|
19
|
+
{ label: 'Mar', value: 290, secondary: 'Ajuste de Mercado' },
|
|
20
|
+
{ label: 'Abr', value: 580, secondary: 'Campaña Viral' },
|
|
21
|
+
{ label: 'May', value: 710, secondary: 'Lanzamiento V2' },
|
|
22
|
+
{ label: 'Jun', value: 640, secondary: 'Estabilidad de Temporada' },
|
|
23
|
+
{ label: 'Jul', value: 890, secondary: 'Pico Histórico' }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const ChartShowcase: React.FC<ChartShowcaseProps> = ({
|
|
27
|
+
data = DEFAULT_DATA,
|
|
28
|
+
type = 'all',
|
|
29
|
+
className = ''
|
|
30
|
+
}) => {
|
|
31
|
+
const [activeChart, setActiveChart] = useState<'line' | 'bar' | 'pie'>(
|
|
32
|
+
type === 'all' ? 'line' : type
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`w-full bg-bg-card/30 border border-border-app/40 rounded-3xl p-6 ${className}`}>
|
|
37
|
+
|
|
38
|
+
{/* Chart controls headers */}
|
|
39
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pb-6 border-b border-border-app/30">
|
|
40
|
+
<div>
|
|
41
|
+
<h3 className="text-sm font-black text-text-main font-display flex items-center gap-2">
|
|
42
|
+
<span className="w-2 h-4 bg-accent rounded-full inline-block" />
|
|
43
|
+
Estadísticas y Analíticas
|
|
44
|
+
</h3>
|
|
45
|
+
<p className="text-[11px] text-text-muted mt-0.5 font-medium">
|
|
46
|
+
Representación interactiva sin dependencias externas
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Tab Buttons to toggle between charts */}
|
|
51
|
+
{type === 'all' && (
|
|
52
|
+
<div className="flex bg-bg-app/40 border border-border-app/30 rounded-xl p-1 text-[11px] font-bold">
|
|
53
|
+
{(['line', 'bar', 'pie'] as const).map((mode) => (
|
|
54
|
+
<button
|
|
55
|
+
key={mode}
|
|
56
|
+
onClick={() => setActiveChart(mode)}
|
|
57
|
+
className={`px-3 py-1.5 rounded-lg capitalize transition-all cursor-pointer ${
|
|
58
|
+
activeChart === mode
|
|
59
|
+
? 'bg-accent text-white shadow-sm'
|
|
60
|
+
: 'text-text-muted hover:text-text-main'
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{mode === 'line' ? 'Líneas' : mode === 'bar' ? 'Barras' : 'Torta'}
|
|
64
|
+
</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Render selected chart body */}
|
|
71
|
+
<div className="pt-6 min-h-[300px] flex items-center justify-center relative">
|
|
72
|
+
<AnimatePresence mode="wait">
|
|
73
|
+
{activeChart === 'line' && (
|
|
74
|
+
<motion.div
|
|
75
|
+
key="line"
|
|
76
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
77
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
78
|
+
exit={{ opacity: 0, scale: 0.98 }}
|
|
79
|
+
transition={{ duration: 0.3 }}
|
|
80
|
+
className="w-full h-full flex flex-col justify-end"
|
|
81
|
+
>
|
|
82
|
+
<LineChartComponent data={data} />
|
|
83
|
+
</motion.div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{activeChart === 'bar' && (
|
|
87
|
+
<motion.div
|
|
88
|
+
key="bar"
|
|
89
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
90
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
91
|
+
exit={{ opacity: 0, scale: 0.98 }}
|
|
92
|
+
transition={{ duration: 0.3 }}
|
|
93
|
+
className="w-full h-full"
|
|
94
|
+
>
|
|
95
|
+
<BarChartComponent data={data} />
|
|
96
|
+
</motion.div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{activeChart === 'pie' && (
|
|
100
|
+
<motion.div
|
|
101
|
+
key="pie"
|
|
102
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
103
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
104
|
+
exit={{ opacity: 0, scale: 0.98 }}
|
|
105
|
+
transition={{ duration: 0.3 }}
|
|
106
|
+
className="w-full h-full flex justify-center"
|
|
107
|
+
>
|
|
108
|
+
<PieChartComponent data={data} />
|
|
109
|
+
</motion.div>
|
|
110
|
+
)}
|
|
111
|
+
</AnimatePresence>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ==========================================
|
|
119
|
+
// 1. LINE CHART COMPONENT WITH BEZIER CURVE
|
|
120
|
+
// ==========================================
|
|
121
|
+
const LineChartComponent: React.FC<{ data: ChartDataItem[] }> = ({ data }) => {
|
|
122
|
+
const containerRef = useRef<SVGSVGElement>(null);
|
|
123
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
124
|
+
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
|
|
125
|
+
|
|
126
|
+
const values = data.map((d) => d.value);
|
|
127
|
+
const maxValue = Math.max(...values, 100) * 1.15;
|
|
128
|
+
const minValue = Math.min(...values, 0) * 0.85;
|
|
129
|
+
const valueRange = maxValue - minValue;
|
|
130
|
+
|
|
131
|
+
const width = 600;
|
|
132
|
+
const height = 240;
|
|
133
|
+
const paddingLeft = 45;
|
|
134
|
+
const paddingRight = 20;
|
|
135
|
+
const paddingTop = 20;
|
|
136
|
+
const paddingBottom = 30;
|
|
137
|
+
|
|
138
|
+
const chartWidth = width - paddingLeft - paddingRight;
|
|
139
|
+
const chartHeight = height - paddingTop - paddingBottom;
|
|
140
|
+
|
|
141
|
+
// Calculate coordinates for points
|
|
142
|
+
const points = data.map((item, index) => {
|
|
143
|
+
const x = paddingLeft + (index / (data.length - 1)) * chartWidth;
|
|
144
|
+
const y = paddingTop + chartHeight - ((item.value - minValue) / valueRange) * chartHeight;
|
|
145
|
+
return { x, y, item, index };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Calculate smooth Bezier Curve Path points (Catmull-Rom or cubic spline approximation)
|
|
149
|
+
const getBezierPath = () => {
|
|
150
|
+
if (points.length === 0) return '';
|
|
151
|
+
let d = `M ${points[0].x} ${points[0].y}`;
|
|
152
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
153
|
+
const curr = points[i];
|
|
154
|
+
const next = points[i + 1];
|
|
155
|
+
// Control points for curvature
|
|
156
|
+
const cpX1 = curr.x + (next.x - curr.x) / 2;
|
|
157
|
+
const cpY1 = curr.y;
|
|
158
|
+
const cpX2 = curr.x + (next.x - curr.x) / 2;
|
|
159
|
+
const cpY2 = next.y;
|
|
160
|
+
d += ` C ${cpX1} ${cpY1}, ${cpX2} ${cpY2}, ${next.x} ${next.y}`;
|
|
161
|
+
}
|
|
162
|
+
return d;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const getAreaPath = (linePath: string) => {
|
|
166
|
+
if (!linePath) return '';
|
|
167
|
+
return `${linePath} L ${points[points.length - 1].x} ${height - paddingBottom} L ${points[0].x} ${height - paddingBottom} Z`;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
|
|
171
|
+
if (!containerRef.current) return;
|
|
172
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
173
|
+
const mouseX = e.clientX - rect.left;
|
|
174
|
+
const svgMouseX = (mouseX / rect.width) * width;
|
|
175
|
+
|
|
176
|
+
// Find nearest point
|
|
177
|
+
let nearestIdx = 0;
|
|
178
|
+
let minDistance = Infinity;
|
|
179
|
+
|
|
180
|
+
points.forEach((p, idx) => {
|
|
181
|
+
const dist = Math.abs(p.x - svgMouseX);
|
|
182
|
+
if (dist < minDistance) {
|
|
183
|
+
minDistance = dist;
|
|
184
|
+
nearestIdx = idx;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
setHoveredIndex(nearestIdx);
|
|
189
|
+
setTooltipPos({
|
|
190
|
+
x: points[nearestIdx].x,
|
|
191
|
+
y: points[nearestIdx].y
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleMouseLeave = () => {
|
|
196
|
+
setHoveredIndex(null);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const linePathD = getBezierPath();
|
|
200
|
+
const areaPathD = getAreaPath(linePathD);
|
|
201
|
+
|
|
202
|
+
// Y-axis grid labels
|
|
203
|
+
const gridLinesCount = 4;
|
|
204
|
+
const gridValues = Array.from({ length: gridLinesCount + 1 }).map((_, idx) => {
|
|
205
|
+
const val = minValue + (idx / gridLinesCount) * valueRange;
|
|
206
|
+
const y = paddingTop + chartHeight - (idx / gridLinesCount) * chartHeight;
|
|
207
|
+
return { val: Math.round(val), y };
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="relative w-full overflow-visible">
|
|
212
|
+
|
|
213
|
+
{/* SVG Canvas Container */}
|
|
214
|
+
<svg
|
|
215
|
+
ref={containerRef}
|
|
216
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
217
|
+
className="w-full h-auto overflow-visible select-none"
|
|
218
|
+
onMouseMove={handleMouseMove}
|
|
219
|
+
onMouseLeave={handleMouseLeave}
|
|
220
|
+
>
|
|
221
|
+
<defs>
|
|
222
|
+
{/* Gradient for area under the line */}
|
|
223
|
+
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
|
|
224
|
+
<stop offset="0%" stopColor="var(--color-accent)" stopOpacity="0.28" />
|
|
225
|
+
<stop offset="100%" stopColor="var(--color-accent)" stopOpacity="0.0" />
|
|
226
|
+
</linearGradient>
|
|
227
|
+
{/* Line Stroke gradient */}
|
|
228
|
+
<linearGradient id="lineGradient" x1="0" y1="0" x2="1" y2="0">
|
|
229
|
+
<stop offset="0%" stopColor="var(--color-accent)" />
|
|
230
|
+
<stop offset="100%" stopColor="#ec4899" />
|
|
231
|
+
</linearGradient>
|
|
232
|
+
</defs>
|
|
233
|
+
|
|
234
|
+
{/* Grid lines */}
|
|
235
|
+
{gridValues.map((g, idx) => (
|
|
236
|
+
<g key={idx}>
|
|
237
|
+
<line
|
|
238
|
+
x1={paddingLeft}
|
|
239
|
+
y1={g.y}
|
|
240
|
+
x2={width - paddingRight}
|
|
241
|
+
y2={g.y}
|
|
242
|
+
className="stroke-border-app/30"
|
|
243
|
+
strokeWidth="1"
|
|
244
|
+
strokeDasharray="4 4"
|
|
245
|
+
/>
|
|
246
|
+
<text
|
|
247
|
+
x={paddingLeft - 8}
|
|
248
|
+
y={g.y + 3}
|
|
249
|
+
textAnchor="end"
|
|
250
|
+
className="fill-text-muted/70 text-[9px] font-mono"
|
|
251
|
+
>
|
|
252
|
+
{g.val}
|
|
253
|
+
</text>
|
|
254
|
+
</g>
|
|
255
|
+
))}
|
|
256
|
+
|
|
257
|
+
{/* X-axis indicators */}
|
|
258
|
+
{points.map((p, idx) => (
|
|
259
|
+
<text
|
|
260
|
+
key={idx}
|
|
261
|
+
x={p.x}
|
|
262
|
+
y={height - 10}
|
|
263
|
+
textAnchor="middle"
|
|
264
|
+
className="fill-text-muted text-[10px] font-black"
|
|
265
|
+
>
|
|
266
|
+
{p.item.label}
|
|
267
|
+
</text>
|
|
268
|
+
))}
|
|
269
|
+
|
|
270
|
+
{/* Area fill path with mount animation */}
|
|
271
|
+
<motion.path
|
|
272
|
+
initial={{ pathLength: 0, opacity: 0 }}
|
|
273
|
+
animate={{ pathLength: 1, opacity: 1 }}
|
|
274
|
+
transition={{ duration: 1.2, ease: 'easeOut' }}
|
|
275
|
+
d={areaPathD}
|
|
276
|
+
fill="url(#areaGradient)"
|
|
277
|
+
/>
|
|
278
|
+
|
|
279
|
+
{/* Main Line path */}
|
|
280
|
+
<motion.path
|
|
281
|
+
initial={{ pathLength: 0 }}
|
|
282
|
+
animate={{ pathLength: 1 }}
|
|
283
|
+
transition={{ duration: 1.2, ease: 'easeOut' }}
|
|
284
|
+
d={linePathD}
|
|
285
|
+
fill="none"
|
|
286
|
+
stroke="url(#lineGradient)"
|
|
287
|
+
strokeWidth="3.5"
|
|
288
|
+
strokeLinecap="round"
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
{/* Dot nodes on intersections */}
|
|
292
|
+
{points.map((p, idx) => {
|
|
293
|
+
const isHovered = hoveredIndex === idx;
|
|
294
|
+
return (
|
|
295
|
+
<motion.circle
|
|
296
|
+
key={idx}
|
|
297
|
+
cx={p.x}
|
|
298
|
+
cy={p.y}
|
|
299
|
+
r={3.5}
|
|
300
|
+
animate={{ scale: isHovered ? 1.7 : 1 }}
|
|
301
|
+
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
302
|
+
className="fill-bg-card stroke-accent"
|
|
303
|
+
strokeWidth={isHovered ? 2.5 : 2}
|
|
304
|
+
style={{
|
|
305
|
+
filter: isHovered ? 'drop-shadow(0 0 4px var(--color-accent))' : undefined
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
|
|
311
|
+
{/* Vertical hover marker guide line */}
|
|
312
|
+
{hoveredIndex !== null && (
|
|
313
|
+
<line
|
|
314
|
+
x1={tooltipPos.x}
|
|
315
|
+
y1={paddingTop}
|
|
316
|
+
x2={tooltipPos.x}
|
|
317
|
+
y2={height - paddingBottom}
|
|
318
|
+
className="stroke-accent/20"
|
|
319
|
+
strokeWidth="1.5"
|
|
320
|
+
strokeDasharray="2 2"
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
</svg>
|
|
324
|
+
|
|
325
|
+
{/* Elastic floating tooltip popup */}
|
|
326
|
+
<AnimatePresence>
|
|
327
|
+
{hoveredIndex !== null && (
|
|
328
|
+
<motion.div
|
|
329
|
+
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
330
|
+
animate={{
|
|
331
|
+
opacity: 1,
|
|
332
|
+
y: 0,
|
|
333
|
+
scale: 1,
|
|
334
|
+
// Convert SVG coords to relative percentage values
|
|
335
|
+
left: `${(tooltipPos.x / width) * 100}%`,
|
|
336
|
+
top: `${(tooltipPos.y / height) * 100 - 30}%`
|
|
337
|
+
}}
|
|
338
|
+
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
339
|
+
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
|
|
340
|
+
className="absolute transform -translate-x-1/2 -translate-y-full z-30 pointer-events-none"
|
|
341
|
+
>
|
|
342
|
+
<div className="glass border border-accent/30 rounded-xl px-3 py-1.5 shadow-lg flex flex-col items-center">
|
|
343
|
+
<span className="text-[10px] font-black text-text-main">
|
|
344
|
+
{data[hoveredIndex].label}
|
|
345
|
+
</span>
|
|
346
|
+
<span className="text-xs font-black text-accent">
|
|
347
|
+
{data[hoveredIndex].value} items
|
|
348
|
+
</span>
|
|
349
|
+
{data[hoveredIndex].secondary && (
|
|
350
|
+
<span className="text-[8px] text-text-muted font-bold font-mono">
|
|
351
|
+
{data[hoveredIndex].secondary}
|
|
352
|
+
</span>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
</motion.div>
|
|
356
|
+
)}
|
|
357
|
+
</AnimatePresence>
|
|
358
|
+
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// ==========================================
|
|
364
|
+
// 2. BAR CHART COMPONENT (VERTICAL BARS)
|
|
365
|
+
// ==========================================
|
|
366
|
+
const BarChartComponent: React.FC<{ data: ChartDataItem[] }> = ({ data }) => {
|
|
367
|
+
const containerRef = useRef<SVGSVGElement>(null);
|
|
368
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
369
|
+
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
|
|
370
|
+
|
|
371
|
+
const values = data.map((d) => d.value);
|
|
372
|
+
const maxValue = Math.max(...values, 100) * 1.1;
|
|
373
|
+
|
|
374
|
+
const width = 600;
|
|
375
|
+
const height = 240;
|
|
376
|
+
const paddingLeft = 45;
|
|
377
|
+
const paddingRight = 20;
|
|
378
|
+
const paddingTop = 20;
|
|
379
|
+
const paddingBottom = 30;
|
|
380
|
+
|
|
381
|
+
const chartWidth = width - paddingLeft - paddingRight;
|
|
382
|
+
const chartHeight = height - paddingTop - paddingBottom;
|
|
383
|
+
|
|
384
|
+
const barWidth = (chartWidth / data.length) * 0.6;
|
|
385
|
+
const barGap = (chartWidth / data.length) * 0.4;
|
|
386
|
+
|
|
387
|
+
// Generate path with rounded top corners and a flat bottom
|
|
388
|
+
const getBarPath = (bx: number, by: number, bw: number, bh: number, r: number) => {
|
|
389
|
+
if (bh <= 0) return `M ${bx} ${by} V ${by} H ${bx + bw} V ${by} Z`;
|
|
390
|
+
const radius = Math.min(r, bh, bw / 2);
|
|
391
|
+
return `
|
|
392
|
+
M ${bx} ${by + bh}
|
|
393
|
+
V ${by + radius}
|
|
394
|
+
a ${radius} ${radius} 0 0 1 ${radius} -${radius}
|
|
395
|
+
H ${bx + bw - radius}
|
|
396
|
+
a ${radius} ${radius} 0 0 1 ${radius} ${radius}
|
|
397
|
+
V ${by + bh}
|
|
398
|
+
Z
|
|
399
|
+
`.replace(/\s+/g, ' ').trim();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div className="w-full relative overflow-visible">
|
|
404
|
+
|
|
405
|
+
<svg
|
|
406
|
+
ref={containerRef}
|
|
407
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
408
|
+
className="w-full h-auto overflow-visible select-none"
|
|
409
|
+
>
|
|
410
|
+
<defs>
|
|
411
|
+
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
|
|
412
|
+
<stop offset="0%" stopColor="var(--color-accent)" />
|
|
413
|
+
<stop offset="100%" stopColor="var(--color-accent)" stopOpacity="0.4" />
|
|
414
|
+
</linearGradient>
|
|
415
|
+
<linearGradient id="barHoverGradient" x1="0" y1="0" x2="0" y2="1">
|
|
416
|
+
<stop offset="0%" stopColor="#ec4899" />
|
|
417
|
+
<stop offset="100%" stopColor="var(--color-accent)" />
|
|
418
|
+
</linearGradient>
|
|
419
|
+
</defs>
|
|
420
|
+
|
|
421
|
+
{/* Horizontal background lines */}
|
|
422
|
+
{[0, 1, 2, 3, 4].map((i) => {
|
|
423
|
+
const y = paddingTop + (i / 4) * chartHeight;
|
|
424
|
+
const val = Math.round(maxValue - (i / 4) * maxValue);
|
|
425
|
+
return (
|
|
426
|
+
<g key={i}>
|
|
427
|
+
<line
|
|
428
|
+
x1={paddingLeft}
|
|
429
|
+
y1={y}
|
|
430
|
+
x2={width - paddingRight}
|
|
431
|
+
y2={y}
|
|
432
|
+
className="stroke-border-app/30"
|
|
433
|
+
strokeWidth="1"
|
|
434
|
+
/>
|
|
435
|
+
<text
|
|
436
|
+
x={paddingLeft - 8}
|
|
437
|
+
y={y + 3}
|
|
438
|
+
textAnchor="end"
|
|
439
|
+
className="fill-text-muted/70 text-[9px] font-mono"
|
|
440
|
+
>
|
|
441
|
+
{val}
|
|
442
|
+
</text>
|
|
443
|
+
</g>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
446
|
+
|
|
447
|
+
{/* Solid baseline on the X Axis */}
|
|
448
|
+
<line
|
|
449
|
+
x1={paddingLeft}
|
|
450
|
+
y1={paddingTop + chartHeight}
|
|
451
|
+
x2={width - paddingRight}
|
|
452
|
+
y2={paddingTop + chartHeight}
|
|
453
|
+
className="stroke-border-app/80"
|
|
454
|
+
strokeWidth="1.5"
|
|
455
|
+
/>
|
|
456
|
+
|
|
457
|
+
{/* Bars drawing */}
|
|
458
|
+
{data.map((item, idx) => {
|
|
459
|
+
const x = paddingLeft + idx * (barWidth + barGap) + barGap / 2;
|
|
460
|
+
const barHeight = (item.value / maxValue) * chartHeight;
|
|
461
|
+
const y = paddingTop + chartHeight - barHeight;
|
|
462
|
+
const isHovered = hoveredIdx === idx;
|
|
463
|
+
|
|
464
|
+
const barD = getBarPath(x, y, barWidth, barHeight, 6);
|
|
465
|
+
const initialBarD = getBarPath(x, paddingTop + chartHeight, barWidth, 0, 6);
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<g
|
|
469
|
+
key={idx}
|
|
470
|
+
onMouseEnter={() => {
|
|
471
|
+
setHoveredIdx(idx);
|
|
472
|
+
setTooltipPos({
|
|
473
|
+
x: x + barWidth / 2,
|
|
474
|
+
y: y
|
|
475
|
+
});
|
|
476
|
+
}}
|
|
477
|
+
onMouseLeave={() => setHoveredIdx(null)}
|
|
478
|
+
className="cursor-pointer"
|
|
479
|
+
>
|
|
480
|
+
{/* Rounded-top path */}
|
|
481
|
+
<motion.path
|
|
482
|
+
initial={{ d: initialBarD }}
|
|
483
|
+
animate={{ d: barD }}
|
|
484
|
+
transition={{ type: 'spring', stiffness: 80, damping: 14, delay: idx * 0.03 }}
|
|
485
|
+
className="transition-all"
|
|
486
|
+
fill={isHovered ? 'url(#barHoverGradient)' : 'url(#barGradient)'}
|
|
487
|
+
style={{
|
|
488
|
+
filter: isHovered ? 'drop-shadow(0 0 8px var(--color-accent))' : undefined
|
|
489
|
+
}}
|
|
490
|
+
/>
|
|
491
|
+
|
|
492
|
+
{/* X Axis Labels */}
|
|
493
|
+
<text
|
|
494
|
+
x={x + barWidth / 2}
|
|
495
|
+
y={height - 10}
|
|
496
|
+
textAnchor="middle"
|
|
497
|
+
className={`text-[10px] font-black transition-colors duration-200 ${
|
|
498
|
+
isHovered ? 'fill-accent font-extrabold' : 'fill-text-muted'
|
|
499
|
+
}`}
|
|
500
|
+
>
|
|
501
|
+
{item.label}
|
|
502
|
+
</text>
|
|
503
|
+
</g>
|
|
504
|
+
);
|
|
505
|
+
})}
|
|
506
|
+
</svg>
|
|
507
|
+
|
|
508
|
+
{/* Elastic floating tooltip popup */}
|
|
509
|
+
<AnimatePresence>
|
|
510
|
+
{hoveredIdx !== null && (
|
|
511
|
+
<motion.div
|
|
512
|
+
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
513
|
+
animate={{
|
|
514
|
+
opacity: 1,
|
|
515
|
+
y: 0,
|
|
516
|
+
scale: 1,
|
|
517
|
+
left: `${(tooltipPos.x / width) * 100}%`,
|
|
518
|
+
top: `${(tooltipPos.y / height) * 100 - 30}%`
|
|
519
|
+
}}
|
|
520
|
+
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
|
521
|
+
transition={{ type: 'spring', stiffness: 400, damping: 28 }}
|
|
522
|
+
className="absolute transform -translate-x-1/2 -translate-y-full z-30 pointer-events-none"
|
|
523
|
+
>
|
|
524
|
+
<div className="glass border border-accent/30 rounded-xl px-3 py-1.5 shadow-lg flex flex-col items-center">
|
|
525
|
+
<span className="text-[10px] font-black text-text-main">
|
|
526
|
+
{data[hoveredIdx].label}
|
|
527
|
+
</span>
|
|
528
|
+
<span className="text-xs font-black text-accent">
|
|
529
|
+
{data[hoveredIdx].value} items
|
|
530
|
+
</span>
|
|
531
|
+
{data[hoveredIdx].secondary && (
|
|
532
|
+
<span className="text-[8px] text-text-muted font-bold font-mono">
|
|
533
|
+
{data[hoveredIdx].secondary}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
</motion.div>
|
|
538
|
+
)}
|
|
539
|
+
</AnimatePresence>
|
|
540
|
+
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// ==========================================
|
|
546
|
+
// 3. PIE/DONUT CHART COMPONENT
|
|
547
|
+
// ==========================================
|
|
548
|
+
const PieChartComponent: React.FC<{ data: ChartDataItem[] }> = ({ data }) => {
|
|
549
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
550
|
+
|
|
551
|
+
const total = data.reduce((acc, curr) => acc + curr.value, 0);
|
|
552
|
+
|
|
553
|
+
// Predefined colorful list of visual colors representing theme integration
|
|
554
|
+
const colors = [
|
|
555
|
+
'var(--color-accent)',
|
|
556
|
+
'#3b82f6', // blue
|
|
557
|
+
'#10b981', // green
|
|
558
|
+
'#f59e0b', // amber
|
|
559
|
+
'#ec4899', // pink
|
|
560
|
+
'#8b5cf6', // purple
|
|
561
|
+
'#ef4444' // red
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
const size = 220;
|
|
565
|
+
const radius = 90;
|
|
566
|
+
const center = size / 2;
|
|
567
|
+
|
|
568
|
+
let accumulatedAngle = -90; // Start at the top
|
|
569
|
+
|
|
570
|
+
const slices = data.map((item, idx) => {
|
|
571
|
+
const percentage = item.value / total;
|
|
572
|
+
const angle = percentage * 360;
|
|
573
|
+
const startAngle = accumulatedAngle;
|
|
574
|
+
const endAngle = accumulatedAngle + angle;
|
|
575
|
+
accumulatedAngle = endAngle;
|
|
576
|
+
|
|
577
|
+
const startRad = (startAngle * Math.PI) / 180;
|
|
578
|
+
const endRad = (endAngle * Math.PI) / 180;
|
|
579
|
+
|
|
580
|
+
const x1 = center + radius * Math.cos(startRad);
|
|
581
|
+
const y1 = center + radius * Math.sin(startRad);
|
|
582
|
+
const x2 = center + radius * Math.cos(endRad);
|
|
583
|
+
const y2 = center + radius * Math.sin(endRad);
|
|
584
|
+
|
|
585
|
+
const largeArcFlag = angle > 180 ? 1 : 0;
|
|
586
|
+
|
|
587
|
+
// Curved Wedge Path formula
|
|
588
|
+
const pathData = `M ${center} ${center} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
|
|
589
|
+
|
|
590
|
+
const midAngle = startAngle + angle / 2;
|
|
591
|
+
const midRad = (midAngle * Math.PI) / 180;
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
pathData,
|
|
595
|
+
item,
|
|
596
|
+
percentage,
|
|
597
|
+
midRad,
|
|
598
|
+
color: colors[idx % colors.length],
|
|
599
|
+
index: idx
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<div className="flex flex-col sm:flex-row items-center gap-8 py-2 w-full max-w-[450px] justify-center select-none">
|
|
605
|
+
|
|
606
|
+
{/* SVG Canvas for Donut */}
|
|
607
|
+
<div className="relative" style={{ width: size, height: size }}>
|
|
608
|
+
<svg viewBox={`0 0 ${size} ${size}`} className="w-full h-full overflow-visible">
|
|
609
|
+
{slices.map((slice, idx) => {
|
|
610
|
+
const isHovered = hoveredIdx === idx;
|
|
611
|
+
// Pop out effect on hover (small translation along the wedge midangle vector)
|
|
612
|
+
const popOffset = isHovered ? 8 : 0;
|
|
613
|
+
const dx = popOffset * Math.cos(slice.midRad);
|
|
614
|
+
const dy = popOffset * Math.sin(slice.midRad);
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<g
|
|
618
|
+
key={idx}
|
|
619
|
+
onMouseEnter={() => setHoveredIdx(idx)}
|
|
620
|
+
onMouseLeave={() => setHoveredIdx(null)}
|
|
621
|
+
className="cursor-pointer"
|
|
622
|
+
style={{
|
|
623
|
+
transform: `translate(${dx}px, ${dy}px)`,
|
|
624
|
+
transition: 'transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1)'
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<motion.path
|
|
628
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
629
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
630
|
+
transition={{ type: 'spring', stiffness: 100, damping: 15, delay: idx * 0.04 }}
|
|
631
|
+
d={slice.pathData}
|
|
632
|
+
fill={slice.color}
|
|
633
|
+
stroke="var(--color-bg-card, #1c1917)"
|
|
634
|
+
strokeWidth="2.5"
|
|
635
|
+
className="transition-colors duration-200"
|
|
636
|
+
style={{
|
|
637
|
+
filter: isHovered ? `drop-shadow(0 0 10px ${slice.color}60)` : undefined,
|
|
638
|
+
opacity: hoveredIdx !== null && !isHovered ? 0.6 : 1
|
|
639
|
+
}}
|
|
640
|
+
/>
|
|
641
|
+
</g>
|
|
642
|
+
);
|
|
643
|
+
})}
|
|
644
|
+
|
|
645
|
+
{/* Center mask circle to create Donut hole effect */}
|
|
646
|
+
<circle
|
|
647
|
+
cx={center}
|
|
648
|
+
cy={center}
|
|
649
|
+
r={radius * 0.52}
|
|
650
|
+
className="fill-bg-app border border-border-app"
|
|
651
|
+
style={{ fill: 'var(--color-bg-card)' }}
|
|
652
|
+
/>
|
|
653
|
+
</svg>
|
|
654
|
+
|
|
655
|
+
{/* Center label statistics text */}
|
|
656
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
|
657
|
+
<span className="text-[10px] font-black text-text-muted uppercase tracking-widest">
|
|
658
|
+
Total
|
|
659
|
+
</span>
|
|
660
|
+
<span className="text-xl font-black text-text-main font-mono">
|
|
661
|
+
{total}
|
|
662
|
+
</span>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
{/* Interactive side Legend indicator list */}
|
|
667
|
+
<div className="flex flex-col gap-2 flex-grow min-w-[140px]">
|
|
668
|
+
{slices.map((slice, idx) => {
|
|
669
|
+
const isHovered = hoveredIdx === idx;
|
|
670
|
+
return (
|
|
671
|
+
<div
|
|
672
|
+
key={idx}
|
|
673
|
+
onMouseEnter={() => setHoveredIdx(idx)}
|
|
674
|
+
onMouseLeave={() => setHoveredIdx(null)}
|
|
675
|
+
className={`flex items-center justify-between p-2 rounded-xl border border-transparent cursor-pointer transition-all duration-200 ${
|
|
676
|
+
isHovered
|
|
677
|
+
? 'bg-white/5 border-border-app/30 scale-105'
|
|
678
|
+
: 'hover:bg-white/5'
|
|
679
|
+
}`}
|
|
680
|
+
>
|
|
681
|
+
<div className="flex items-center gap-2">
|
|
682
|
+
<span
|
|
683
|
+
className="w-3 h-3 rounded-md flex-shrink-0"
|
|
684
|
+
style={{ backgroundColor: slice.color }}
|
|
685
|
+
/>
|
|
686
|
+
<span className="text-xs font-bold text-text-main">
|
|
687
|
+
{slice.item.label}
|
|
688
|
+
</span>
|
|
689
|
+
</div>
|
|
690
|
+
<span className="text-xs font-bold font-mono text-text-muted">
|
|
691
|
+
{Math.round(slice.percentage * 100)}%
|
|
692
|
+
</span>
|
|
693
|
+
</div>
|
|
694
|
+
);
|
|
695
|
+
})}
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
};
|