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