@etherealui/react 0.0.0-stage → 0.0.2

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/index.d.mts +583 -0
  4. package/dist/index.d.ts +583 -0
  5. package/dist/index.js +5189 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +5189 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +83 -3
  10. package/src/blocks/app-shell.tsx +441 -0
  11. package/src/blocks/billing-card.tsx +2 -0
  12. package/src/blocks/data-table.tsx +479 -0
  13. package/src/blocks/filter-bar.tsx +134 -0
  14. package/src/blocks/header-navbar.tsx +7 -0
  15. package/src/blocks/metric-grid.tsx +5 -0
  16. package/src/blocks/profile-form.tsx +2 -0
  17. package/src/blocks/security-settings.tsx +2 -0
  18. package/src/blocks/settings-blocks.tsx +406 -0
  19. package/src/blocks/sidebar.tsx +6 -0
  20. package/src/blocks/stat-card.tsx +7 -0
  21. package/src/blocks/stat-cards.tsx +212 -0
  22. package/src/blocks/team-members.tsx +5 -0
  23. package/src/index.ts +114 -0
  24. package/src/primitives/accordion.tsx +61 -0
  25. package/src/primitives/announcement.tsx +33 -0
  26. package/src/primitives/avatar.tsx +156 -0
  27. package/src/primitives/badge.tsx +108 -0
  28. package/src/primitives/breadcrumb.tsx +45 -0
  29. package/src/primitives/button-group.tsx +79 -0
  30. package/src/primitives/button.tsx +165 -0
  31. package/src/primitives/carousel.tsx +84 -0
  32. package/src/primitives/chart.tsx +831 -0
  33. package/src/primitives/checkbox.tsx +26 -0
  34. package/src/primitives/chip.tsx +37 -0
  35. package/src/primitives/close-button.tsx +29 -0
  36. package/src/primitives/color-picker.tsx +62 -0
  37. package/src/primitives/combobox.tsx +123 -0
  38. package/src/primitives/command-palette.tsx +216 -0
  39. package/src/primitives/context-menu.tsx +84 -0
  40. package/src/primitives/date-calendar.tsx +76 -0
  41. package/src/primitives/date-picker.tsx +400 -0
  42. package/src/primitives/dialog.tsx +279 -0
  43. package/src/primitives/divider.tsx +31 -0
  44. package/src/primitives/drawer.tsx +95 -0
  45. package/src/primitives/dropdown-menu.tsx +164 -0
  46. package/src/primitives/file-upload.tsx +123 -0
  47. package/src/primitives/icon-button.tsx +23 -0
  48. package/src/primitives/input.tsx +124 -0
  49. package/src/primitives/modal.tsx +26 -0
  50. package/src/primitives/pagination.tsx +58 -0
  51. package/src/primitives/popover.tsx +28 -0
  52. package/src/primitives/progress.tsx +145 -0
  53. package/src/primitives/radio-group.tsx +63 -0
  54. package/src/primitives/select.tsx +207 -0
  55. package/src/primitives/skeleton.tsx +61 -0
  56. package/src/primitives/slider.tsx +44 -0
  57. package/src/primitives/switch.tsx +146 -0
  58. package/src/primitives/table.tsx +47 -0
  59. package/src/primitives/tabs.tsx +86 -0
  60. package/src/primitives/textarea.tsx +40 -0
  61. package/src/primitives/toast.tsx +71 -0
  62. package/src/primitives/tooltip.tsx +58 -0
  63. package/src/styles/globals.css +49 -0
  64. package/src/templates/control-tower.tsx +41 -0
  65. package/src/templates/executive-analytics.tsx +272 -0
  66. package/src/templates/workspace-settings.tsx +179 -0
  67. package/src/utils/cn.ts +7 -0
@@ -0,0 +1,831 @@
1
+ import * as React from "react"
2
+ import {
3
+ ArrowUpRight,
4
+ TrendingUp,
5
+ Activity,
6
+ PieChart as PieChartIcon,
7
+ Gauge,
8
+ Grid,
9
+ LineChart as LineChartIcon,
10
+ } from "lucide-react"
11
+ import { cn } from "../utils/cn"
12
+
13
+ export interface ChartDataPoint {
14
+ label: string
15
+ value: number
16
+ secondary?: number
17
+ }
18
+
19
+ const DEFAULT_AREA_SERIES: Record<string, ChartDataPoint[]> = {
20
+ "7d": [
21
+ { label: "Mon", value: 14200, secondary: 11800 },
22
+ { label: "Tue", value: 16800, secondary: 13200 },
23
+ { label: "Wed", value: 15900, secondary: 14100 },
24
+ { label: "Thu", value: 19400, secondary: 15600 },
25
+ { label: "Fri", value: 22100, secondary: 17200 },
26
+ { label: "Sat", value: 24600, secondary: 18900 },
27
+ { label: "Sun", value: 28400, secondary: 21400 },
28
+ ],
29
+ "30d": [
30
+ { label: "Oct 05", value: 64000, secondary: 52000 },
31
+ { label: "Oct 10", value: 72000, secondary: 58000 },
32
+ { label: "Oct 15", value: 69500, secondary: 61000 },
33
+ { label: "Oct 20", value: 84200, secondary: 67500 },
34
+ { label: "Oct 25", value: 96800, secondary: 74200 },
35
+ { label: "Oct 30", value: 112400, secondary: 82900 },
36
+ { label: "Nov 05", value: 128430, secondary: 94100 },
37
+ ],
38
+ "90d": [
39
+ { label: "Aug", value: 195000, secondary: 162000 },
40
+ { label: "Sep", value: 248000, secondary: 198000 },
41
+ { label: "Oct", value: 312000, secondary: 244000 },
42
+ { label: "Nov", value: 384500, secondary: 289000 },
43
+ ],
44
+ }
45
+
46
+ export interface AreaStatisticChartProps {
47
+ title?: string
48
+ subtitle?: string
49
+ value?: string
50
+ change?: string
51
+ data?: ChartDataPoint[]
52
+ className?: string
53
+ }
54
+
55
+ export const AreaStatisticChart: React.FC<AreaStatisticChartProps> = ({
56
+ title = "Net Revenue & MRR Velocity",
57
+ subtitle = "Real-time settled volume vs previous billing cycle",
58
+ value = "$128,430",
59
+ change = "+18.4%",
60
+ data,
61
+ className,
62
+ }) => {
63
+ const [range, setRange] = React.useState<"7d" | "30d" | "90d">("30d")
64
+ const [hoverIdx, setHoverIdx] = React.useState<number | null>(null)
65
+
66
+ const series = data || DEFAULT_AREA_SERIES[range]
67
+ const maxVal = Math.max(...series.map((d) => Math.max(d.value, d.secondary || 0)), 1)
68
+ const width = 540
69
+ const height = 160
70
+ const padX = 16
71
+ const padY = 18
72
+
73
+ const getCoords = (val: number, idx: number) => {
74
+ const x = padX + (idx / Math.max(series.length - 1, 1)) * (width - padX * 2)
75
+ const y = height - padY - (val / maxVal) * (height - padY * 2)
76
+ return { x, y }
77
+ }
78
+
79
+ const primaryPoints = series.map((d, i) => getCoords(d.value, i))
80
+ const secondaryPoints = series.map((d, i) =>
81
+ getCoords(d.secondary ?? d.value * 0.75, i)
82
+ )
83
+
84
+ const linePath = primaryPoints
85
+ .map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`)
86
+ .join(" ")
87
+
88
+ const secondaryPath = secondaryPoints
89
+ .map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`)
90
+ .join(" ")
91
+
92
+ const areaPath = `${linePath} L${primaryPoints[primaryPoints.length - 1].x.toFixed(
93
+ 1
94
+ )},${height - padY} L${primaryPoints[0].x.toFixed(1)},${height - padY} Z`
95
+
96
+ const activePoint = hoverIdx !== null ? series[hoverIdx] : series[series.length - 1]
97
+
98
+ return (
99
+ <div
100
+ className={cn(
101
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
102
+ className
103
+ )}
104
+ >
105
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
106
+ <div>
107
+ <div className="flex items-center gap-2">
108
+ <TrendingUp className="h-4 w-4 text-blue-600 dark:text-blue-400" />
109
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
110
+ {title}
111
+ </h3>
112
+ </div>
113
+ <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
114
+ {subtitle}
115
+ </p>
116
+ </div>
117
+
118
+ <div className="inline-flex items-center rounded-xl border border-zinc-200 bg-zinc-50 p-0.5 dark:border-zinc-800 dark:bg-zinc-950 self-start">
119
+ {(["7d", "30d", "90d"] as const).map((r) => (
120
+ <button
121
+ key={r}
122
+ type="button"
123
+ onClick={() => {
124
+ setRange(r)
125
+ setHoverIdx(null)
126
+ }}
127
+ className={cn(
128
+ "rounded-lg px-2.5 py-1 text-[11px] font-semibold uppercase transition-all cursor-pointer",
129
+ range === r
130
+ ? "bg-white text-zinc-900 shadow-2xs dark:bg-zinc-800 dark:text-white"
131
+ : "text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
132
+ )}
133
+ >
134
+ {r}
135
+ </button>
136
+ ))}
137
+ </div>
138
+ </div>
139
+
140
+ {/* Live KPI Readout */}
141
+ <div className="mt-4 flex flex-wrap items-baseline justify-between gap-4 border-b border-zinc-100 pb-4 dark:border-zinc-800/80">
142
+ <div className="flex items-baseline gap-2.5">
143
+ <span className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
144
+ {hoverIdx !== null
145
+ ? `$${activePoint.value.toLocaleString()}`
146
+ : value}
147
+ </span>
148
+ <span className="inline-flex items-center gap-0.5 rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400">
149
+ <ArrowUpRight className="h-3 w-3" />
150
+ {change}
151
+ </span>
152
+ <span className="text-xs text-zinc-400">
153
+ {hoverIdx !== null ? `on ${activePoint.label}` : "current period"}
154
+ </span>
155
+ </div>
156
+
157
+ <div className="flex items-center gap-4 text-[11px] text-zinc-500 dark:text-zinc-400">
158
+ <span className="flex items-center gap-1.5">
159
+ <span className="h-2 w-2 rounded-full bg-blue-600" />
160
+ Current (${activePoint.value.toLocaleString()})
161
+ </span>
162
+ {activePoint.secondary !== undefined && (
163
+ <span className="flex items-center gap-1.5">
164
+ <span className="h-2 w-2 rounded-full bg-zinc-300 dark:bg-zinc-600" />
165
+ Previous (${activePoint.secondary.toLocaleString()})
166
+ </span>
167
+ )}
168
+ </div>
169
+ </div>
170
+
171
+ {/* Interactive SVG Area Canvas */}
172
+ <div className="mt-4">
173
+ <svg
174
+ viewBox={`0 0 ${width} ${height}`}
175
+ className="h-44 w-full overflow-visible select-none"
176
+ onMouseLeave={() => setHoverIdx(null)}
177
+ >
178
+ <defs>
179
+ <linearGradient id="etherealAreaGrad" x1="0" y1="0" x2="0" y2="1">
180
+ <stop offset="0%" stopColor="#2563eb" stopOpacity="0.28" />
181
+ <stop offset="100%" stopColor="#2563eb" stopOpacity="0.0" />
182
+ </linearGradient>
183
+ </defs>
184
+
185
+ {[0.2, 0.5, 0.8].map((ratio) => (
186
+ <line
187
+ key={ratio}
188
+ x1={padX}
189
+ x2={width - padX}
190
+ y1={height * ratio}
191
+ y2={height * ratio}
192
+ stroke="currentColor"
193
+ strokeDasharray="3 3"
194
+ className="text-zinc-200 dark:text-zinc-800"
195
+ />
196
+ ))}
197
+
198
+ <path
199
+ d={secondaryPath}
200
+ fill="none"
201
+ stroke="currentColor"
202
+ strokeWidth="1.75"
203
+ strokeDasharray="4 4"
204
+ className="text-zinc-300 dark:text-zinc-700"
205
+ />
206
+
207
+ <path d={areaPath} fill="url(#etherealAreaGrad)" />
208
+ <path
209
+ d={linePath}
210
+ fill="none"
211
+ stroke="#2563eb"
212
+ strokeWidth="2.5"
213
+ strokeLinecap="round"
214
+ strokeLinejoin="round"
215
+ />
216
+
217
+ {primaryPoints.map((pt, idx) => {
218
+ const isHovered = hoverIdx === idx
219
+ return (
220
+ <g
221
+ key={series[idx].label}
222
+ className="cursor-pointer"
223
+ onMouseEnter={() => setHoverIdx(idx)}
224
+ >
225
+ {isHovered && (
226
+ <line
227
+ x1={pt.x}
228
+ x2={pt.x}
229
+ y1={padY}
230
+ y2={height - padY}
231
+ stroke="#2563eb"
232
+ strokeWidth="1"
233
+ strokeDasharray="2 2"
234
+ />
235
+ )}
236
+ <circle
237
+ cx={pt.x}
238
+ cy={pt.y}
239
+ r={isHovered ? 5 : 3.5}
240
+ className={cn(
241
+ "transition-all",
242
+ isHovered
243
+ ? "fill-blue-600 stroke-white dark:stroke-zinc-900 stroke-2"
244
+ : "fill-white dark:fill-zinc-900 stroke-blue-600 stroke-2"
245
+ )}
246
+ />
247
+ <rect
248
+ x={pt.x - 24}
249
+ y={0}
250
+ width={48}
251
+ height={height}
252
+ fill="transparent"
253
+ />
254
+ </g>
255
+ )
256
+ })}
257
+ </svg>
258
+
259
+ <div className="mt-2 flex items-center justify-between px-2 text-[11px] font-mono text-zinc-400">
260
+ {series.map((item, idx) => (
261
+ <span
262
+ key={item.label}
263
+ className={cn(
264
+ hoverIdx === idx && "font-semibold text-blue-600 dark:text-blue-400"
265
+ )}
266
+ >
267
+ {item.label}
268
+ </span>
269
+ ))}
270
+ </div>
271
+ </div>
272
+ </div>
273
+ )
274
+ }
275
+
276
+ export interface BarStatisticChartProps {
277
+ title?: string
278
+ subtitle?: string
279
+ data?: ChartDataPoint[]
280
+ className?: string
281
+ }
282
+
283
+ const DEFAULT_BAR_DATA: ChartDataPoint[] = [
284
+ { label: "Jan", value: 68, secondary: 52 },
285
+ { label: "Feb", value: 74, secondary: 58 },
286
+ { label: "Mar", value: 64, secondary: 60 },
287
+ { label: "Apr", value: 82, secondary: 66 },
288
+ { label: "May", value: 89, secondary: 72 },
289
+ { label: "Jun", value: 96, secondary: 78 },
290
+ ]
291
+
292
+ export const BarStatisticChart: React.FC<BarStatisticChartProps> = ({
293
+ title = "Monthly Cohort Conversion",
294
+ subtitle = "Enterprise vs self-serve onboarding completion (%)",
295
+ data = DEFAULT_BAR_DATA,
296
+ className,
297
+ }) => {
298
+ const [activeIdx, setActiveIdx] = React.useState<number | null>(null)
299
+
300
+ return (
301
+ <div
302
+ className={cn(
303
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
304
+ className
305
+ )}
306
+ >
307
+ <div className="flex items-center justify-between">
308
+ <div>
309
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
310
+ <Activity className="h-4 w-4 text-blue-600 dark:text-blue-400" />
311
+ {title}
312
+ </h3>
313
+ <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
314
+ {subtitle}
315
+ </p>
316
+ </div>
317
+ <div className="flex items-center gap-3 text-[11px] text-zinc-500">
318
+ <span className="flex items-center gap-1.5">
319
+ <span className="h-2.5 w-2.5 rounded-xs bg-blue-600" />
320
+ Enterprise
321
+ </span>
322
+ <span className="flex items-center gap-1.5">
323
+ <span className="h-2.5 w-2.5 rounded-xs bg-zinc-200 dark:bg-zinc-700" />
324
+ Self-Serve
325
+ </span>
326
+ </div>
327
+ </div>
328
+
329
+ <div className="mt-6 grid grid-cols-6 items-end gap-3 h-44 pt-6 px-2">
330
+ {data.map((d, idx) => {
331
+ const isHovered = activeIdx === idx
332
+ return (
333
+ <div
334
+ key={d.label}
335
+ onMouseEnter={() => setActiveIdx(idx)}
336
+ onMouseLeave={() => setActiveIdx(null)}
337
+ className="group relative flex flex-col items-center gap-2 h-full justify-end cursor-pointer"
338
+ >
339
+ {isHovered && (
340
+ <div className="absolute -top-6 rounded-md bg-zinc-900 px-2 py-0.5 font-mono text-[10px] text-white shadow-sm dark:bg-zinc-100 dark:text-zinc-900">
341
+ {d.value}%
342
+ </div>
343
+ )}
344
+ <div className="w-full flex items-end justify-center gap-1.5 h-full">
345
+ <div
346
+ style={{ height: `${d.value}%` }}
347
+ className="w-full max-w-[18px] rounded-t-md bg-blue-600 transition-all duration-300 group-hover:bg-blue-500"
348
+ />
349
+ {d.secondary !== undefined && (
350
+ <div
351
+ style={{ height: `${d.secondary}%` }}
352
+ className="w-full max-w-[14px] rounded-t-md bg-zinc-200 transition-all duration-300 dark:bg-zinc-700"
353
+ />
354
+ )}
355
+ </div>
356
+ <span className="text-[11px] font-mono text-zinc-500 dark:text-zinc-400">
357
+ {d.label}
358
+ </span>
359
+ </div>
360
+ )
361
+ })}
362
+ </div>
363
+ </div>
364
+ )
365
+ }
366
+
367
+ export interface LineStatisticChartProps {
368
+ title?: string
369
+ subtitle?: string
370
+ className?: string
371
+ }
372
+
373
+ const DEFAULT_LINE_TELEMETRY = [
374
+ { time: "00:00", p95: 22, p50: 11 },
375
+ { time: "04:00", p95: 19, p50: 10 },
376
+ { time: "08:00", p95: 28, p50: 14 },
377
+ { time: "12:00", p95: 34, p50: 16 },
378
+ { time: "16:00", p95: 25, p50: 12 },
379
+ { time: "20:00", p95: 21, p50: 11 },
380
+ { time: "24:00", p95: 18, p50: 9 },
381
+ ]
382
+
383
+ export const LineStatisticChart: React.FC<LineStatisticChartProps> = ({
384
+ title = "Edge API Response Latency (ms)",
385
+ subtitle = "P95 vs P50 global edge worker response time over 24 hours",
386
+ className,
387
+ }) => {
388
+ const [hoverIdx, setHoverIdx] = React.useState<number | null>(null)
389
+ const width = 520
390
+ const height = 150
391
+ const padX = 18
392
+ const padY = 18
393
+ const maxVal = 45
394
+
395
+ const toPoint = (val: number, i: number) => ({
396
+ x: padX + (i / (DEFAULT_LINE_TELEMETRY.length - 1)) * (width - padX * 2),
397
+ y: height - padY - (val / maxVal) * (height - padY * 2),
398
+ })
399
+
400
+ const p95Pts = DEFAULT_LINE_TELEMETRY.map((d, i) => toPoint(d.p95, i))
401
+ const p50Pts = DEFAULT_LINE_TELEMETRY.map((d, i) => toPoint(d.p50, i))
402
+
403
+ const p95Path = p95Pts
404
+ .map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`)
405
+ .join(" ")
406
+ const p50Path = p50Pts
407
+ .map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`)
408
+ .join(" ")
409
+
410
+ const current =
411
+ hoverIdx !== null
412
+ ? DEFAULT_LINE_TELEMETRY[hoverIdx]
413
+ : DEFAULT_LINE_TELEMETRY[DEFAULT_LINE_TELEMETRY.length - 1]
414
+
415
+ return (
416
+ <div
417
+ className={cn(
418
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
419
+ className
420
+ )}
421
+ >
422
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
423
+ <div>
424
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
425
+ <LineChartIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
426
+ {title}
427
+ </h3>
428
+ <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
429
+ {subtitle}
430
+ </p>
431
+ </div>
432
+
433
+ <div className="flex items-center gap-4 text-xs">
434
+ <span className="flex items-center gap-1.5 font-mono text-zinc-700 dark:text-zinc-200">
435
+ <span className="h-2 w-2 rounded-full bg-blue-600" />
436
+ P95: <strong>{current.p95}ms</strong>
437
+ </span>
438
+ <span className="flex items-center gap-1.5 font-mono text-zinc-700 dark:text-zinc-200">
439
+ <span className="h-2 w-2 rounded-full bg-emerald-500" />
440
+ P50: <strong>{current.p50}ms</strong>
441
+ </span>
442
+ </div>
443
+ </div>
444
+
445
+ <div className="mt-4">
446
+ <svg
447
+ viewBox={`0 0 ${width} ${height}`}
448
+ className="h-40 w-full overflow-visible select-none"
449
+ onMouseLeave={() => setHoverIdx(null)}
450
+ >
451
+ {[0.25, 0.5, 0.75].map((r) => (
452
+ <line
453
+ key={r}
454
+ x1={padX}
455
+ x2={width - padX}
456
+ y1={height * r}
457
+ y2={height * r}
458
+ stroke="currentColor"
459
+ strokeDasharray="3 3"
460
+ className="text-zinc-200 dark:text-zinc-800"
461
+ />
462
+ ))}
463
+
464
+ <path
465
+ d={p95Path}
466
+ fill="none"
467
+ stroke="#2563eb"
468
+ strokeWidth="2.25"
469
+ strokeLinecap="round"
470
+ strokeLinejoin="round"
471
+ />
472
+ <path
473
+ d={p50Path}
474
+ fill="none"
475
+ stroke="#10b981"
476
+ strokeWidth="2.25"
477
+ strokeLinecap="round"
478
+ strokeLinejoin="round"
479
+ />
480
+
481
+ {p95Pts.map((pt, i) => (
482
+ <g
483
+ key={DEFAULT_LINE_TELEMETRY[i].time}
484
+ className="cursor-pointer"
485
+ onMouseEnter={() => setHoverIdx(i)}
486
+ >
487
+ <circle
488
+ cx={pt.x}
489
+ cy={pt.y}
490
+ r={hoverIdx === i ? 5 : 3}
491
+ className="fill-blue-600 stroke-white dark:stroke-zinc-900 stroke-2"
492
+ />
493
+ <circle
494
+ cx={p50Pts[i].x}
495
+ cy={p50Pts[i].y}
496
+ r={hoverIdx === i ? 5 : 3}
497
+ className="fill-emerald-500 stroke-white dark:stroke-zinc-900 stroke-2"
498
+ />
499
+ <rect
500
+ x={pt.x - 24}
501
+ y={0}
502
+ width={48}
503
+ height={height}
504
+ fill="transparent"
505
+ />
506
+ </g>
507
+ ))}
508
+ </svg>
509
+
510
+ <div className="mt-2 flex justify-between px-2 font-mono text-[11px] text-zinc-400">
511
+ {DEFAULT_LINE_TELEMETRY.map((d, i) => (
512
+ <span
513
+ key={d.time}
514
+ className={cn(
515
+ hoverIdx === i && "font-semibold text-blue-600 dark:text-blue-400"
516
+ )}
517
+ >
518
+ {d.time}
519
+ </span>
520
+ ))}
521
+ </div>
522
+ </div>
523
+ </div>
524
+ )
525
+ }
526
+
527
+ export interface DonutSegment {
528
+ label: string
529
+ value: number
530
+ color: string
531
+ dotClass: string
532
+ }
533
+
534
+ const DEFAULT_DONUT_SEGMENTS: DonutSegment[] = [
535
+ { label: "Enterprise Cloud", value: 48, color: "#2563eb", dotClass: "bg-blue-600" },
536
+ { label: "Growth Team", value: 32, color: "#8b5cf6", dotClass: "bg-violet-500" },
537
+ { label: "Self-Serve Pro", value: 20, color: "#10b981", dotClass: "bg-emerald-500" },
538
+ ]
539
+
540
+ export const DonutStatisticChart: React.FC<{
541
+ title?: string
542
+ segments?: DonutSegment[]
543
+ className?: string
544
+ }> = ({
545
+ title = "Revenue Distribution by Tier",
546
+ segments = DEFAULT_DONUT_SEGMENTS,
547
+ className,
548
+ }) => {
549
+ const [activeSegment, setActiveSegment] = React.useState<number | null>(null)
550
+ const total = segments.reduce((acc, s) => acc + s.value, 0)
551
+ const radius = 50
552
+ const circumference = 2 * Math.PI * radius
553
+ let cumulativePercent = 0
554
+
555
+ const current = activeSegment !== null ? segments[activeSegment] : null
556
+
557
+ return (
558
+ <div
559
+ className={cn(
560
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
561
+ className
562
+ )}
563
+ >
564
+ <div className="flex items-center gap-2">
565
+ <PieChartIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
566
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
567
+ {title}
568
+ </h3>
569
+ </div>
570
+
571
+ <div className="mt-5 flex flex-col items-center gap-6 sm:flex-row sm:justify-around">
572
+ <div className="relative flex h-36 w-36 items-center justify-center shrink-0">
573
+ <svg className="h-36 w-36 -rotate-90" viewBox="0 0 128 128">
574
+ <circle
575
+ cx="64"
576
+ cy="64"
577
+ r={radius}
578
+ fill="none"
579
+ stroke="currentColor"
580
+ strokeWidth="14"
581
+ className="text-zinc-100 dark:text-zinc-800"
582
+ />
583
+ {segments.map((seg, idx) => {
584
+ const strokeDasharray = `${(seg.value / total) * circumference} ${circumference}`
585
+ const strokeDashoffset = -cumulativePercent * circumference
586
+ cumulativePercent += seg.value / total
587
+
588
+ return (
589
+ <circle
590
+ key={seg.label}
591
+ cx="64"
592
+ cy="64"
593
+ r={radius}
594
+ fill="none"
595
+ stroke={seg.color}
596
+ strokeWidth={activeSegment === idx ? "17" : "14"}
597
+ strokeDasharray={strokeDasharray}
598
+ strokeDashoffset={strokeDashoffset}
599
+ className="transition-all duration-200 cursor-pointer"
600
+ onMouseEnter={() => setActiveSegment(idx)}
601
+ onMouseLeave={() => setActiveSegment(null)}
602
+ />
603
+ )
604
+ })}
605
+ </svg>
606
+ <div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none">
607
+ <span className="text-xl font-bold text-zinc-900 dark:text-white">
608
+ {current ? `${current.value}%` : "100%"}
609
+ </span>
610
+ <span className="text-[10px] text-zinc-400">
611
+ {current ? current.label : "Total Share"}
612
+ </span>
613
+ </div>
614
+ </div>
615
+
616
+ <div className="w-full max-w-xs space-y-2.5">
617
+ {segments.map((seg, idx) => (
618
+ <div
619
+ key={seg.label}
620
+ onMouseEnter={() => setActiveSegment(idx)}
621
+ onMouseLeave={() => setActiveSegment(null)}
622
+ className={cn(
623
+ "flex items-center justify-between rounded-xl border px-3 py-2 text-xs transition-colors cursor-pointer",
624
+ activeSegment === idx
625
+ ? "border-blue-500/40 bg-zinc-50 dark:bg-zinc-800/70"
626
+ : "border-zinc-200/70 dark:border-zinc-800"
627
+ )}
628
+ >
629
+ <span className="flex items-center gap-2 font-medium text-zinc-700 dark:text-zinc-200">
630
+ <span className={cn("h-2.5 w-2.5 rounded-full", seg.dotClass)} />
631
+ {seg.label}
632
+ </span>
633
+ <span className="font-mono font-semibold text-zinc-900 dark:text-white">
634
+ {seg.value}%
635
+ </span>
636
+ </div>
637
+ ))}
638
+ </div>
639
+ </div>
640
+ </div>
641
+ )
642
+ }
643
+
644
+ export const RadialStatisticChart: React.FC<{
645
+ title?: string
646
+ score?: number
647
+ className?: string
648
+ }> = ({
649
+ title = "Workspace Health & SLA Score",
650
+ score = 98,
651
+ className,
652
+ }) => {
653
+ return (
654
+ <div
655
+ className={cn(
656
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
657
+ className
658
+ )}
659
+ >
660
+ <div className="flex items-center justify-between">
661
+ <div className="flex items-center gap-2">
662
+ <Gauge className="h-4 w-4 text-blue-600 dark:text-blue-400" />
663
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
664
+ {title}
665
+ </h3>
666
+ </div>
667
+ <span className="rounded-full bg-emerald-500/10 px-2.5 py-0.5 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400">
668
+ Excellent
669
+ </span>
670
+ </div>
671
+
672
+ <div className="mt-5 flex flex-col items-center gap-6 sm:flex-row sm:justify-around">
673
+ {/* Concentric Activity Rings */}
674
+ <div className="relative flex h-36 w-36 items-center justify-center shrink-0">
675
+ <svg className="h-36 w-36 -rotate-90" viewBox="0 0 128 128">
676
+ <circle
677
+ cx="64"
678
+ cy="64"
679
+ r="52"
680
+ fill="none"
681
+ stroke="currentColor"
682
+ strokeWidth="10"
683
+ className="text-zinc-100 dark:text-zinc-800"
684
+ />
685
+ <circle
686
+ cx="64"
687
+ cy="64"
688
+ r="52"
689
+ fill="none"
690
+ stroke="#2563eb"
691
+ strokeWidth="10"
692
+ strokeDasharray="326"
693
+ strokeDashoffset="22"
694
+ strokeLinecap="round"
695
+ />
696
+ <circle
697
+ cx="64"
698
+ cy="64"
699
+ r="38"
700
+ fill="none"
701
+ stroke="currentColor"
702
+ strokeWidth="10"
703
+ className="text-zinc-100 dark:text-zinc-800"
704
+ />
705
+ <circle
706
+ cx="64"
707
+ cy="64"
708
+ r="38"
709
+ fill="none"
710
+ stroke="#10b981"
711
+ strokeWidth="10"
712
+ strokeDasharray="238"
713
+ strokeDashoffset="28"
714
+ strokeLinecap="round"
715
+ />
716
+ </svg>
717
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
718
+ <span className="text-2xl font-bold text-zinc-900 dark:text-white">
719
+ {score}
720
+ </span>
721
+ <span className="text-[10px] text-zinc-400">/ 100</span>
722
+ </div>
723
+ </div>
724
+
725
+ <div className="w-full max-w-xs space-y-3 text-xs">
726
+ <div className="space-y-1">
727
+ <div className="flex justify-between">
728
+ <span className="text-zinc-500">Global Uptime SLA</span>
729
+ <span className="font-mono font-semibold text-zinc-900 dark:text-white">
730
+ 99.98%
731
+ </span>
732
+ </div>
733
+ <div className="h-1.5 w-full rounded-full bg-zinc-100 dark:bg-zinc-800">
734
+ <div className="h-full w-[98%] rounded-full bg-blue-600" />
735
+ </div>
736
+ </div>
737
+
738
+ <div className="space-y-1">
739
+ <div className="flex justify-between">
740
+ <span className="text-zinc-500">Edge Cache Hit Ratio</span>
741
+ <span className="font-mono font-semibold text-zinc-900 dark:text-white">
742
+ 94.2%
743
+ </span>
744
+ </div>
745
+ <div className="h-1.5 w-full rounded-full bg-zinc-100 dark:bg-zinc-800">
746
+ <div className="h-full w-[94%] rounded-full bg-emerald-500" />
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </div>
751
+ </div>
752
+ )
753
+ }
754
+
755
+ const HEATMAP_DATA = [
756
+ 1, 2, 3, 2, 1, 0, 2, 3, 4, 3, 2, 1, 3, 4, 2, 3,
757
+ 2, 3, 4, 3, 2, 1, 3, 4, 2, 1, 3, 2, 4, 3, 1, 2,
758
+ 0, 1, 2, 4, 3, 2, 1, 3, 4, 4, 3, 2, 3, 4, 3, 4,
759
+ 2, 3, 1, 2, 3, 4, 2, 1, 3, 2, 4, 3, 2, 1, 4, 3,
760
+ ]
761
+
762
+ export const HeatmapStatisticChart: React.FC<{
763
+ title?: string
764
+ subtitle?: string
765
+ className?: string
766
+ }> = ({
767
+ title = "Deployment & Commit Frequency Heatmap",
768
+ subtitle = "64-day production release intensity across workspace repositories",
769
+ className,
770
+ }) => {
771
+ const [hoveredCell, setHoveredCell] = React.useState<number | null>(null)
772
+
773
+ return (
774
+ <div
775
+ className={cn(
776
+ "w-full rounded-2xl border border-zinc-200 bg-white p-5 shadow-2xs dark:border-zinc-800 dark:bg-zinc-900",
777
+ className
778
+ )}
779
+ >
780
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
781
+ <div>
782
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100 flex items-center gap-2">
783
+ <Grid className="h-4 w-4 text-blue-600 dark:text-blue-400" />
784
+ {title}
785
+ </h3>
786
+ <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
787
+ {subtitle}
788
+ </p>
789
+ </div>
790
+
791
+ <span className="font-mono text-xs text-zinc-500 dark:text-zinc-400">
792
+ {hoveredCell !== null
793
+ ? `Day ${hoveredCell + 1}: ${HEATMAP_DATA[hoveredCell] * 6} events`
794
+ : "1,428 total events"}
795
+ </span>
796
+ </div>
797
+
798
+ <div className="mt-5 grid grid-cols-16 gap-1.5">
799
+ {HEATMAP_DATA.map((lvl, idx) => (
800
+ <button
801
+ key={idx}
802
+ type="button"
803
+ onMouseEnter={() => setHoveredCell(idx)}
804
+ onMouseLeave={() => setHoveredCell(null)}
805
+ aria-label={`Day ${idx + 1}`}
806
+ className={cn(
807
+ "h-5 w-full rounded-[4px] transition-transform hover:scale-115 cursor-pointer",
808
+ lvl === 0 && "bg-zinc-100 dark:bg-zinc-800",
809
+ lvl === 1 && "bg-blue-200 dark:bg-blue-950",
810
+ lvl === 2 && "bg-blue-400 dark:bg-blue-800",
811
+ lvl === 3 && "bg-blue-500 dark:bg-blue-600",
812
+ lvl === 4 && "bg-blue-600 dark:bg-blue-400"
813
+ )}
814
+ />
815
+ ))}
816
+ </div>
817
+
818
+ <div className="mt-4 flex items-center justify-between text-[11px] text-zinc-400">
819
+ <span>64 days ago</span>
820
+ <div className="flex items-center gap-1">
821
+ <span>Less</span>
822
+ <span className="h-2.5 w-2.5 rounded-[2px] bg-zinc-100 dark:bg-zinc-800" />
823
+ <span className="h-2.5 w-2.5 rounded-[2px] bg-blue-200 dark:bg-blue-950" />
824
+ <span className="h-2.5 w-2.5 rounded-[2px] bg-blue-400 dark:bg-blue-800" />
825
+ <span className="h-2.5 w-2.5 rounded-[2px] bg-blue-600 dark:bg-blue-400" />
826
+ <span>More</span>
827
+ </div>
828
+ </div>
829
+ </div>
830
+ )
831
+ }