@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.
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- 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
|
+
}
|