@olympusoss/canvas 2.20.2 → 3.1.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 (214) hide show
  1. package/README.md +69 -35
  2. package/package.json +45 -177
  3. package/src/cn.ts +3 -0
  4. package/src/index.ts +12 -603
  5. package/src/theme.ts +62 -0
  6. package/src/tokens.ts +11 -0
  7. package/styles/base.css +17 -0
  8. package/styles/canvas.css +77 -52
  9. package/styles/components/alert.css +66 -0
  10. package/styles/components/app-shell.css +46 -0
  11. package/styles/components/avatar.css +22 -0
  12. package/styles/components/badge.css +83 -0
  13. package/styles/components/breadcrumb.css +35 -0
  14. package/styles/components/button-group.css +23 -0
  15. package/styles/components/button.css +107 -0
  16. package/styles/components/calendar.css +73 -0
  17. package/styles/components/card.css +58 -0
  18. package/styles/components/checkbox.css +55 -0
  19. package/styles/components/code-block.css +18 -0
  20. package/styles/components/combobox.css +75 -0
  21. package/styles/components/command.css +94 -0
  22. package/styles/components/data-table.css +142 -0
  23. package/styles/components/dialog.css +72 -0
  24. package/styles/components/dropdown.css +54 -0
  25. package/styles/components/empty-state.css +17 -0
  26. package/styles/components/field.css +27 -0
  27. package/styles/components/filter-panel.css +58 -0
  28. package/styles/components/form.css +27 -0
  29. package/styles/components/icon.css +8 -0
  30. package/styles/components/input-group.css +45 -0
  31. package/styles/components/input.css +56 -0
  32. package/styles/components/kbd.css +15 -0
  33. package/styles/components/page-header.css +52 -0
  34. package/styles/components/pagination.css +48 -0
  35. package/styles/components/popover.css +14 -0
  36. package/styles/components/radio.css +28 -0
  37. package/styles/components/row-menu.css +69 -0
  38. package/styles/components/section-card.css +49 -0
  39. package/styles/components/select.css +57 -0
  40. package/styles/components/separator.css +32 -0
  41. package/styles/components/sheet.css +70 -0
  42. package/styles/components/sidebar.css +146 -0
  43. package/styles/components/skeleton.css +32 -0
  44. package/styles/components/spinner.css +26 -0
  45. package/styles/components/stat-card.css +71 -0
  46. package/styles/components/stepper.css +63 -0
  47. package/styles/components/switch.css +45 -0
  48. package/styles/components/tabs.css +40 -0
  49. package/styles/components/textarea.css +31 -0
  50. package/styles/components/toast.css +95 -0
  51. package/styles/components/tooltip.css +53 -0
  52. package/styles/components/topbar.css +24 -0
  53. package/styles/components/typography.css +105 -0
  54. package/styles/patterns/backdrops.css +35 -0
  55. package/styles/patterns/density.css +66 -0
  56. package/styles/patterns/focus.css +22 -0
  57. package/styles/patterns/glass.css +85 -0
  58. package/styles/patterns/high-contrast.css +70 -0
  59. package/styles/patterns/reduced-motion.css +12 -0
  60. package/styles/patterns/scrollbar.css +10 -0
  61. package/styles/reset.css +89 -0
  62. package/styles/tokens/colors.css +106 -0
  63. package/styles/tokens/motion.css +33 -0
  64. package/styles/tokens/radius.css +10 -0
  65. package/styles/tokens/shadows.css +35 -0
  66. package/styles/tokens/spacing.css +19 -0
  67. package/styles/tokens/typography.css +6 -0
  68. package/styles/tokens/z-index.css +12 -0
  69. package/styles/utilities/display.css +66 -0
  70. package/styles/utilities/flexbox.css +240 -0
  71. package/styles/utilities/gap.css +288 -0
  72. package/styles/utilities/grid.css +138 -0
  73. package/styles/utilities/position.css +78 -0
  74. package/styles/utilities/sizing.css +138 -0
  75. package/tsconfig.json +20 -21
  76. package/src/components/atoms/README.md +0 -11
  77. package/src/components/atoms/aspect-ratio.tsx +0 -32
  78. package/src/components/atoms/avatar.tsx +0 -98
  79. package/src/components/atoms/badge.tsx +0 -44
  80. package/src/components/atoms/brand-mark.tsx +0 -74
  81. package/src/components/atoms/button.tsx +0 -105
  82. package/src/components/atoms/checkbox.tsx +0 -63
  83. package/src/components/atoms/flex-box.tsx +0 -105
  84. package/src/components/atoms/icon.tsx +0 -34
  85. package/src/components/atoms/input.tsx +0 -92
  86. package/src/components/atoms/label.tsx +0 -41
  87. package/src/components/atoms/logo.tsx +0 -89
  88. package/src/components/atoms/progress.tsx +0 -55
  89. package/src/components/atoms/radio-group.tsx +0 -122
  90. package/src/components/atoms/scroll-area.tsx +0 -106
  91. package/src/components/atoms/section.tsx +0 -48
  92. package/src/components/atoms/separator.tsx +0 -45
  93. package/src/components/atoms/skeleton.tsx +0 -17
  94. package/src/components/atoms/slider.tsx +0 -93
  95. package/src/components/atoms/spinner.tsx +0 -47
  96. package/src/components/atoms/switch.tsx +0 -60
  97. package/src/components/atoms/textarea.tsx +0 -78
  98. package/src/components/atoms/toggle.tsx +0 -80
  99. package/src/components/charts/activity-heatmap.tsx +0 -186
  100. package/src/components/charts/axes.tsx +0 -21
  101. package/src/components/charts/chart-container.tsx +0 -254
  102. package/src/components/charts/chart-legend.tsx +0 -67
  103. package/src/components/charts/chart-tooltip.tsx +0 -161
  104. package/src/components/charts/chart-types.tsx +0 -49
  105. package/src/components/charts/containers.tsx +0 -11
  106. package/src/components/charts/data.tsx +0 -16
  107. package/src/components/charts/details.tsx +0 -25
  108. package/src/components/charts/dot-pulse.tsx +0 -61
  109. package/src/components/charts/gauge.tsx +0 -106
  110. package/src/components/charts/grids.tsx +0 -8
  111. package/src/components/charts/index.ts +0 -62
  112. package/src/components/charts/labeled-bar-list.tsx +0 -85
  113. package/src/components/charts/metric-breakdown.tsx +0 -316
  114. package/src/components/charts/references.tsx +0 -8
  115. package/src/components/charts/service-health-list.tsx +0 -85
  116. package/src/components/charts/sparkline-area.tsx +0 -80
  117. package/src/components/charts/sparkline.tsx +0 -52
  118. package/src/components/charts/stacked-bar.tsx +0 -104
  119. package/src/components/charts/text.tsx +0 -10
  120. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  121. package/src/components/charts/world-heat-map.tsx +0 -184
  122. package/src/components/molecules/README.md +0 -12
  123. package/src/components/molecules/action-bar.tsx +0 -73
  124. package/src/components/molecules/activity-item.tsx +0 -74
  125. package/src/components/molecules/alert.tsx +0 -86
  126. package/src/components/molecules/animated-background.tsx +0 -92
  127. package/src/components/molecules/auth-shell.tsx +0 -95
  128. package/src/components/molecules/brand-lockup.tsx +0 -48
  129. package/src/components/molecules/breadcrumb.tsx +0 -157
  130. package/src/components/molecules/button-group.tsx +0 -104
  131. package/src/components/molecules/calendar.tsx +0 -217
  132. package/src/components/molecules/card.tsx +0 -102
  133. package/src/components/molecules/client-brand.tsx +0 -95
  134. package/src/components/molecules/code-block.tsx +0 -86
  135. package/src/components/molecules/countdown-button.tsx +0 -92
  136. package/src/components/molecules/empty-state.tsx +0 -56
  137. package/src/components/molecules/error-state.tsx +0 -42
  138. package/src/components/molecules/field-display.tsx +0 -35
  139. package/src/components/molecules/input-otp.tsx +0 -74
  140. package/src/components/molecules/launcher-card.tsx +0 -152
  141. package/src/components/molecules/loading-state.tsx +0 -36
  142. package/src/components/molecules/notification-item.tsx +0 -67
  143. package/src/components/molecules/notification-list.tsx +0 -45
  144. package/src/components/molecules/number-badge.tsx +0 -53
  145. package/src/components/molecules/or-separator.tsx +0 -38
  146. package/src/components/molecules/page-header.tsx +0 -88
  147. package/src/components/molecules/page-tabs.tsx +0 -94
  148. package/src/components/molecules/pagination.tsx +0 -150
  149. package/src/components/molecules/password-input.tsx +0 -83
  150. package/src/components/molecules/password-strength-meter.tsx +0 -104
  151. package/src/components/molecules/phone-input.tsx +0 -200
  152. package/src/components/molecules/search-bar.tsx +0 -64
  153. package/src/components/molecules/secret-field.tsx +0 -158
  154. package/src/components/molecules/section-card.tsx +0 -91
  155. package/src/components/molecules/social-buttons.tsx +0 -165
  156. package/src/components/molecules/stat-card.tsx +0 -100
  157. package/src/components/molecules/status-badge.tsx +0 -42
  158. package/src/components/molecules/stepper.tsx +0 -96
  159. package/src/components/molecules/table.tsx +0 -157
  160. package/src/components/molecules/terminal.tsx +0 -74
  161. package/src/components/molecules/toggle-group.tsx +0 -145
  162. package/src/components/molecules/tooltip.tsx +0 -155
  163. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  164. package/src/components/organisms/README.md +0 -14
  165. package/src/components/organisms/accordion.tsx +0 -154
  166. package/src/components/organisms/alert-dialog.tsx +0 -277
  167. package/src/components/organisms/carousel.tsx +0 -244
  168. package/src/components/organisms/collapsible.tsx +0 -69
  169. package/src/components/organisms/command.tsx +0 -144
  170. package/src/components/organisms/context-menu.tsx +0 -339
  171. package/src/components/organisms/dashboard-grid.tsx +0 -369
  172. package/src/components/organisms/data-table.tsx +0 -330
  173. package/src/components/organisms/dialog.tsx +0 -312
  174. package/src/components/organisms/drawer.tsx +0 -123
  175. package/src/components/organisms/dropdown-menu.tsx +0 -440
  176. package/src/components/organisms/editors/code-editor.tsx +0 -144
  177. package/src/components/organisms/editors/index.ts +0 -4
  178. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  179. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  180. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  181. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  182. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  183. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  184. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  185. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  186. package/src/components/organisms/error-boundary.tsx +0 -61
  187. package/src/components/organisms/form.tsx +0 -174
  188. package/src/components/organisms/hover-card.tsx +0 -115
  189. package/src/components/organisms/menubar.tsx +0 -498
  190. package/src/components/organisms/navbar.tsx +0 -104
  191. package/src/components/organisms/navigation-menu.tsx +0 -235
  192. package/src/components/organisms/popover.tsx +0 -149
  193. package/src/components/organisms/resizable.tsx +0 -58
  194. package/src/components/organisms/schema-form.tsx +0 -232
  195. package/src/components/organisms/select.tsx +0 -309
  196. package/src/components/organisms/sheet.tsx +0 -265
  197. package/src/components/organisms/sidebar.tsx +0 -1040
  198. package/src/components/organisms/sonner.tsx +0 -96
  199. package/src/components/organisms/tabs.tsx +0 -133
  200. package/src/components/organisms/theme-provider.tsx +0 -101
  201. package/src/hooks/use-mobile.tsx +0 -19
  202. package/src/lib/portal-container.tsx +0 -35
  203. package/src/lib/utils.ts +0 -6
  204. package/src/native.ts +0 -23
  205. package/src/tokens/colors.ts +0 -91
  206. package/src/tokens/index.ts +0 -3
  207. package/src/tokens/spacing.ts +0 -55
  208. package/src/tokens/typography.ts +0 -27
  209. package/styles/dashboard-grid.css +0 -47
  210. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  211. package/styles/glass.css +0 -175
  212. package/styles/leaflet.css +0 -13
  213. package/styles/tokens.css +0 -317
  214. package/tailwind.config.ts +0 -70
@@ -1,106 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export interface GaugeProps extends React.HTMLAttributes<HTMLDivElement> {
6
- /** Percentage to fill (0–100). Values outside the range are clamped. */
7
- value: number;
8
- /** Pixel size (width = height). Default `160`. */
9
- size?: number;
10
- /** Pixel stroke width of the ring. Default `14`. */
11
- strokeWidth?: number;
12
- /**
13
- * CSS variable name (without leading `--`) used for the filled arc. Default
14
- * `chart-1`.
15
- */
16
- colorVar?: string;
17
- /**
18
- * Center value text. Defaults to `"{value}%"`. Pass a custom node to render
19
- * something richer (e.g. a unit label).
20
- */
21
- valueLabel?: React.ReactNode;
22
- /** Small label rendered below the centre value. Optional. */
23
- caption?: React.ReactNode;
24
- /** Accessible label describing the gauge. Default `"Gauge"`. */
25
- "aria-label"?: string;
26
- }
27
-
28
- /**
29
- * Single-arc circular gauge with a centered value. Useful for adoption
30
- * percentages, health scores, completion ratios, and similar 0–100 metrics.
31
- * Renders a complete background ring plus a filled arc; the arc is animated
32
- * via `transition-[stroke-dashoffset]` so consumers get smooth updates when
33
- * `value` changes.
34
- */
35
- export const Gauge = React.forwardRef<HTMLDivElement, GaugeProps>(
36
- (
37
- {
38
- value,
39
- size = 160,
40
- strokeWidth = 14,
41
- colorVar = "chart-1",
42
- valueLabel,
43
- caption,
44
- className,
45
- "aria-label": ariaLabel = "Gauge",
46
- ...props
47
- },
48
- ref,
49
- ) => {
50
- const clamped = Math.max(0, Math.min(100, value));
51
- const r = (size - strokeWidth) / 2;
52
- const cx = size / 2;
53
- const c = 2 * Math.PI * r;
54
- const offset = c - (clamped / 100) * c;
55
- const display = valueLabel ?? `${Math.round(clamped)}%`;
56
- return (
57
- <div
58
- ref={ref}
59
- role="meter"
60
- aria-label={ariaLabel}
61
- aria-valuenow={Math.round(clamped)}
62
- aria-valuemin={0}
63
- aria-valuemax={100}
64
- className={cn("relative inline-flex flex-col items-center", className)}
65
- style={{ width: size, height: caption ? "auto" : size }}
66
- {...props}
67
- >
68
- <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden>
69
- <circle
70
- cx={cx}
71
- cy={cx}
72
- r={r}
73
- fill="none"
74
- stroke="hsl(var(--muted))"
75
- strokeWidth={strokeWidth}
76
- />
77
- <circle
78
- cx={cx}
79
- cy={cx}
80
- r={r}
81
- fill="none"
82
- stroke={`hsl(var(--${colorVar}))`}
83
- strokeWidth={strokeWidth}
84
- strokeDasharray={c}
85
- strokeDashoffset={offset}
86
- strokeLinecap="round"
87
- transform={`rotate(-90 ${cx} ${cx})`}
88
- className="transition-[stroke-dashoffset] duration-500"
89
- />
90
- </svg>
91
- <div
92
- className="pointer-events-none absolute inset-x-0 flex flex-col items-center justify-center"
93
- style={{ top: 0, height: size }}
94
- >
95
- <span className="font-mono text-3xl font-bold tabular-nums">{display}</span>
96
- {caption && (
97
- <span className="mt-1 text-[11px] uppercase tracking-[0.04em] text-muted-foreground">
98
- {caption}
99
- </span>
100
- )}
101
- </div>
102
- </div>
103
- );
104
- },
105
- );
106
- Gauge.displayName = "Gauge";
@@ -1,8 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Grid re-exports — pure pass-throughs. Theming via CSS in `<ChartContainer>`
5
- * (`[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50` etc.).
6
- * Wrapping breaks Recharts' child-by-type detection.
7
- */
8
- export { CartesianGrid, PolarGrid } from "recharts";
@@ -1,62 +0,0 @@
1
- // Container + style emitter (canvas-specific)
2
-
3
- // Chrome — canvas-token defaults
4
- export {
5
- CartesianAxis,
6
- PolarAngleAxis,
7
- PolarRadiusAxis,
8
- XAxis,
9
- YAxis,
10
- ZAxis,
11
- } from "./axes";
12
- export {
13
- type ChartConfig,
14
- ChartContainer,
15
- ChartStyle,
16
- useChart,
17
- } from "./chart-container";
18
- export { ChartLegend, ChartLegendContent } from "./chart-legend";
19
- export { ChartTooltip, ChartTooltipContent } from "./chart-tooltip";
20
- // Chart-type wrappers (`margin` defaults; auto-palette runs in ChartContainer)
21
- export {
22
- AreaChart,
23
- BarChart,
24
- ComposedChart,
25
- FunnelChart,
26
- LineChart,
27
- PieChart,
28
- RadarChart,
29
- RadialBarChart,
30
- Sankey,
31
- ScatterChart,
32
- SunburstChart,
33
- Treemap,
34
- } from "./chart-types";
35
- // Container-level — pure pass-through
36
- export { Brush, Layer, ResponsiveContainer, Surface } from "./containers";
37
- // Data primitives (auto-cycle palette)
38
- export { Area, Bar, Funnel, Line, Pie, Radar, RadialBar, Scatter } from "./data";
39
- // Detail primitives — pure pass-through
40
- export {
41
- ChartCell,
42
- ChartCustomized,
43
- Cross,
44
- Curve,
45
- Dot,
46
- ErrorBar,
47
- Polygon,
48
- Rectangle,
49
- Sector,
50
- Trapezoid,
51
- } from "./details";
52
- export { CartesianGrid, PolarGrid } from "./grids";
53
- export { ReferenceArea, ReferenceDot, ReferenceLine } from "./references";
54
- // Text + labels (Label aliased to avoid collision with canvas form Label)
55
- export { ChartLabel, LabelList, Text } from "./text";
56
-
57
- // Geographic — Leaflet-based heat-map (peer-optional `leaflet` + `react-leaflet`)
58
- export {
59
- WorldHeatMap,
60
- type WorldHeatMapPoint,
61
- type WorldHeatMapProps,
62
- } from "./world-heat-map";
@@ -1,85 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export interface LabeledBarListItem {
6
- /** Row label. Used as the React key. */
7
- label: React.ReactNode;
8
- /** Numeric value rendered in the right cap; bar fill scales against the max. */
9
- value: number;
10
- /** Optional leading element — e.g. a flag, avatar, or `<Icon />`. */
11
- leading?: React.ReactNode;
12
- }
13
-
14
- export interface LabeledBarListProps extends React.HTMLAttributes<HTMLDivElement> {
15
- /** Rows rendered top-to-bottom. */
16
- items: LabeledBarListItem[];
17
- /**
18
- * CSS variable name (without leading `--`) used for the bar fill. Default
19
- * `chart-1`.
20
- */
21
- colorVar?: string;
22
- /** Format the value rendered to the right of each label. Default `toLocaleString`. */
23
- valueFormatter?: (value: number) => string;
24
- /** Pixel height of the row's bar track. Default `4`. */
25
- barHeight?: number;
26
- /** Caption rendered below the list. */
27
- caption?: React.ReactNode;
28
- }
29
-
30
- /**
31
- * Vertical list of labeled rows with a horizontal progress bar per row. Useful
32
- * for "top regions", "schema usage", "provider connections" — anywhere a small
33
- * set of named values needs proportional comparison without a full chart.
34
- */
35
- export const LabeledBarList = React.forwardRef<HTMLDivElement, LabeledBarListProps>(
36
- (
37
- {
38
- items,
39
- colorVar = "chart-1",
40
- valueFormatter = (v) => v.toLocaleString(),
41
- barHeight = 4,
42
- caption,
43
- className,
44
- ...props
45
- },
46
- ref,
47
- ) => {
48
- const max = Math.max(1, ...items.map((it) => it.value));
49
- return (
50
- <div ref={ref} className={cn("w-full", className)} {...props}>
51
- <ul className="flex flex-col gap-2.5">
52
- {items.map((item, i) => {
53
- const pct = Math.max(0, Math.min(100, (item.value / max) * 100));
54
- return (
55
- <li key={`${i}-${item.value}`} className="flex flex-col gap-1.5">
56
- <div className="flex items-center gap-2 text-[13px]">
57
- {item.leading && <span className="shrink-0">{item.leading}</span>}
58
- <span className="flex-1 truncate">{item.label}</span>
59
- <span className="font-mono text-xs text-muted-foreground tabular-nums">
60
- {valueFormatter(item.value)}
61
- </span>
62
- </div>
63
- <div
64
- className="overflow-hidden rounded-full bg-muted"
65
- style={{ height: barHeight }}
66
- aria-hidden
67
- >
68
- <div
69
- className="h-full rounded-full"
70
- style={{
71
- width: `${pct}%`,
72
- background: `hsl(var(--${colorVar}))`,
73
- }}
74
- />
75
- </div>
76
- </li>
77
- );
78
- })}
79
- </ul>
80
- {caption && <p className="mt-3 text-xs text-muted-foreground">{caption}</p>}
81
- </div>
82
- );
83
- },
84
- );
85
- LabeledBarList.displayName = "LabeledBarList";
@@ -1,316 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export type MetricBreakdownTone = "success" | "warning" | "error" | "neutral";
6
-
7
- const TONE_HSL: Record<MetricBreakdownTone, string> = {
8
- success: "143 70% 40%",
9
- warning: "38 92% 50%",
10
- error: "0 80% 60%",
11
- neutral: "var(--muted-foreground)",
12
- };
13
-
14
- export interface MetricBreakdownRow {
15
- /** Row label (often a code or category key). Used as the React key. */
16
- label: React.ReactNode;
17
- /** Numeric value rendered to the right of the label. */
18
- value: number;
19
- /**
20
- * Period-over-period change as a percentage. Positive renders as `▲ N%` in
21
- * the success hue, negative as `▼ N%` in the error hue. Omit to hide the
22
- * delta column for that row.
23
- */
24
- delta?: number;
25
- /**
26
- * CSS variable name (without leading `--`) used for the row's swatch and
27
- * bar fill. Falls back to the wrapper's `defaultColorVar`.
28
- */
29
- colorVar?: string;
30
- }
31
-
32
- export interface MetricBreakdownChip {
33
- /** Chip label, typically a code or short tag. Used as the React key. */
34
- label: React.ReactNode;
35
- /** Optional count rendered after a thin separator. */
36
- count?: number;
37
- /** Visual tone. Default `error`. */
38
- tone?: MetricBreakdownTone;
39
- }
40
-
41
- export interface MetricBreakdownProps extends React.HTMLAttributes<HTMLDivElement> {
42
- /** Primary metric value rendered as the headline number. */
43
- value: React.ReactNode;
44
- /** Caption rendered below the headline value (uppercase muted). */
45
- label: React.ReactNode;
46
- /** Secondary metric rendered top-right (e.g. an error rate). */
47
- rate?: React.ReactNode;
48
- /** Caption for the secondary metric (uppercase muted). */
49
- rateLabel?: React.ReactNode;
50
- /** Tone driving the secondary metric color. Default `neutral`. */
51
- rateTone?: MetricBreakdownTone;
52
- /** Sparkline data points. Rendered as an SVG line + area-fill ramp. */
53
- spark?: number[];
54
- /** Unit suffix rendered next to the last value of the sparkline, e.g. `req/s`. */
55
- sparkUnit?: React.ReactNode;
56
- /**
57
- * CSS variable name (without leading `--`) for the sparkline color. Default
58
- * `chart-1`. Also used as the fallback for breakdown rows that omit `colorVar`.
59
- */
60
- defaultColorVar?: string;
61
- /** Pixel height of the sparkline. Default `36`. */
62
- sparkHeight?: number;
63
- /** Category breakdown rows. */
64
- breakdown?: MetricBreakdownRow[];
65
- /** Format breakdown row values. Default `toLocaleString`. */
66
- valueFormatter?: (value: number) => string;
67
- /** Trailing chip row (e.g. recent error codes). */
68
- chips?: MetricBreakdownChip[];
69
- /**
70
- * Label rendered before the chip row (uppercase muted). Default `"Errors"`.
71
- * Pass `null` to hide.
72
- */
73
- chipsLabel?: React.ReactNode;
74
- }
75
-
76
- /**
77
- * Multi-section metric card: a headline value with an optional rate, a small
78
- * inline trend sparkline, a per-category breakdown with delta arrows, and a
79
- * trailing chip row for recent issues.
80
- *
81
- * Designed for "throughput-style" dashboards (token issuance, API request
82
- * volume, job throughput, sign-ups by source): anywhere a single metric needs
83
- * to be decomposed by category and contextualized by trend and notable issues
84
- * in one card. Identity-agnostic; bring your own labels and color tokens.
85
- */
86
- export const MetricBreakdown = React.forwardRef<HTMLDivElement, MetricBreakdownProps>(
87
- (
88
- {
89
- value,
90
- label,
91
- rate,
92
- rateLabel,
93
- rateTone = "neutral",
94
- spark,
95
- sparkUnit,
96
- defaultColorVar = "chart-1",
97
- sparkHeight = 36,
98
- breakdown,
99
- valueFormatter = (v) => v.toLocaleString(),
100
- chips,
101
- chipsLabel = "Errors",
102
- className,
103
- ...props
104
- },
105
- ref,
106
- ) => {
107
- const rateColor =
108
- rateTone === "neutral" ? "hsl(var(--muted-foreground))" : `hsl(${TONE_HSL[rateTone]})`;
109
- const showChipsLabel = chipsLabel !== null && chipsLabel !== undefined && chipsLabel !== "";
110
- return (
111
- <div ref={ref} className={cn("w-full", className)} {...props}>
112
- <div className="mb-2.5 flex items-baseline justify-between gap-3">
113
- <div>
114
- <div className="font-mono text-[22px] font-semibold leading-[1.1] tabular-nums">
115
- {value}
116
- </div>
117
- <div className="text-[11px] uppercase tracking-[0.04em] text-muted-foreground">
118
- {label}
119
- </div>
120
- </div>
121
- {(rate !== undefined || rateLabel) && (
122
- <div className="text-right">
123
- <div
124
- className="font-mono text-[13px] font-medium tabular-nums"
125
- style={{ color: rateColor }}
126
- >
127
- {rate}
128
- </div>
129
- {rateLabel && (
130
- <div className="text-[11px] uppercase tracking-[0.04em] text-muted-foreground">
131
- {rateLabel}
132
- </div>
133
- )}
134
- </div>
135
- )}
136
- </div>
137
-
138
- {spark && spark.length > 1 && (
139
- <MetricBreakdownSpark
140
- data={spark}
141
- height={sparkHeight}
142
- colorVar={defaultColorVar}
143
- unit={sparkUnit}
144
- />
145
- )}
146
-
147
- {breakdown && breakdown.length > 0 && (
148
- <MetricBreakdownRows
149
- rows={breakdown}
150
- defaultColorVar={defaultColorVar}
151
- valueFormatter={valueFormatter}
152
- />
153
- )}
154
-
155
- {chips && chips.length > 0 && (
156
- <div
157
- className={cn(
158
- "flex flex-wrap items-center gap-1.5",
159
- (breakdown && breakdown.length > 0) || spark
160
- ? "mt-3 border-t border-border pt-2.5"
161
- : "",
162
- )}
163
- >
164
- {showChipsLabel && (
165
- <span className="mr-0.5 text-[10.5px] uppercase tracking-[0.04em] text-muted-foreground">
166
- {chipsLabel}
167
- </span>
168
- )}
169
- {chips.map((chip, i) => {
170
- const tone = chip.tone ?? "error";
171
- const hsl = TONE_HSL[tone];
172
- return (
173
- <span
174
- key={`${i}-${typeof chip.label === "string" ? chip.label : i}`}
175
- className="inline-flex items-center gap-1 rounded font-mono text-[10.5px]"
176
- style={{
177
- padding: "2px 6px",
178
- background: `hsl(${hsl} / 0.1)`,
179
- color: `hsl(${hsl})`,
180
- }}
181
- >
182
- {chip.label}
183
- {chip.count !== undefined && <span className="opacity-70">·{chip.count}</span>}
184
- </span>
185
- );
186
- })}
187
- </div>
188
- )}
189
- </div>
190
- );
191
- },
192
- );
193
- MetricBreakdown.displayName = "MetricBreakdown";
194
-
195
- interface MetricBreakdownSparkProps {
196
- data: number[];
197
- height: number;
198
- colorVar: string;
199
- unit?: React.ReactNode;
200
- }
201
-
202
- function MetricBreakdownSpark({ data, height, colorVar, unit }: MetricBreakdownSparkProps) {
203
- const max = Math.max(...data);
204
- const min = Math.min(...data);
205
- const range = max - min || 1;
206
- const w = (data.length - 1) * 10;
207
- const h = height;
208
- const pts = data
209
- .map((v, i) => {
210
- const x = i * 10;
211
- const y = h - 2 - ((v - min) / range) * (h - 6);
212
- return `${x},${y}`;
213
- })
214
- .join(" ");
215
- const area = `0,${h} ${pts} ${w},${h}`;
216
- const last = data[data.length - 1];
217
- const lastY = h - 2 - ((last - min) / range) * (h - 6);
218
- const fillId = React.useId();
219
- return (
220
- <div className="relative mb-3.5" style={{ height }}>
221
- <svg
222
- viewBox={`0 0 ${w} ${h}`}
223
- preserveAspectRatio="none"
224
- className="h-full w-full overflow-visible"
225
- aria-hidden
226
- >
227
- <defs>
228
- <linearGradient id={fillId} x1="0" x2="0" y1="0" y2="1">
229
- <stop offset="0%" stopColor={`hsl(var(--${colorVar}))`} stopOpacity="0.25" />
230
- <stop offset="100%" stopColor={`hsl(var(--${colorVar}))`} stopOpacity="0" />
231
- </linearGradient>
232
- </defs>
233
- <polygon points={area} fill={`url(#${fillId})`} />
234
- <polyline
235
- points={pts}
236
- fill="none"
237
- stroke={`hsl(var(--${colorVar}))`}
238
- strokeWidth="1.5"
239
- vectorEffect="non-scaling-stroke"
240
- />
241
- <circle cx={w} cy={lastY} r="2.5" fill={`hsl(var(--${colorVar}))`} />
242
- </svg>
243
- {unit !== undefined && (
244
- <div
245
- className="absolute right-0 top-0 bg-card px-1 font-mono text-[11px]"
246
- style={{ color: `hsl(var(--${colorVar}))` }}
247
- >
248
- {last}
249
- {typeof unit === "string" ? ` ${unit}` : <> {unit}</>}
250
- </div>
251
- )}
252
- </div>
253
- );
254
- }
255
-
256
- interface MetricBreakdownRowsProps {
257
- rows: MetricBreakdownRow[];
258
- defaultColorVar: string;
259
- valueFormatter: (value: number) => string;
260
- }
261
-
262
- function MetricBreakdownRows({ rows, defaultColorVar, valueFormatter }: MetricBreakdownRowsProps) {
263
- const total = rows.reduce((sum, r) => sum + r.value, 0) || 1;
264
- return (
265
- <div className="flex flex-col gap-2">
266
- {rows.map((row, i) => {
267
- const colorVar = row.colorVar ?? defaultColorVar;
268
- const pct = (row.value / total) * 100;
269
- const hasDelta = row.delta !== undefined;
270
- const up = hasDelta && (row.delta as number) >= 0;
271
- const deltaHsl = up ? TONE_HSL.success : TONE_HSL.error;
272
- return (
273
- <div
274
- key={`${i}-${typeof row.label === "string" ? row.label : i}`}
275
- className="text-[12.5px]"
276
- >
277
- <div className="mb-1 flex items-center gap-2">
278
- <span
279
- className="size-2 shrink-0 rounded-sm"
280
- style={{ background: `hsl(var(--${colorVar}))` }}
281
- aria-hidden
282
- />
283
- <span className="flex-1 truncate font-mono text-[11.5px] text-foreground">
284
- {row.label}
285
- </span>
286
- <span className="font-mono text-[11.5px] tabular-nums">
287
- {valueFormatter(row.value)}
288
- </span>
289
- {hasDelta && (
290
- <span
291
- className="min-w-[38px] text-right font-mono text-[10.5px] tabular-nums"
292
- style={{ color: `hsl(${deltaHsl})` }}
293
- >
294
- {up ? "▲" : "▼"} {Math.abs(row.delta as number)}%
295
- </span>
296
- )}
297
- </div>
298
- <div
299
- className="overflow-hidden rounded-full bg-muted"
300
- style={{ height: 3 }}
301
- aria-hidden
302
- >
303
- <div
304
- className="h-full rounded-full"
305
- style={{
306
- width: `${pct}%`,
307
- background: `hsl(var(--${colorVar}))`,
308
- }}
309
- />
310
- </div>
311
- </div>
312
- );
313
- })}
314
- </div>
315
- );
316
- }
@@ -1,8 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Reference primitives — pure pass-throughs. Theming via CSS in
5
- * `<ChartContainer>` (`[&_.recharts-reference-line_[stroke='#ccc']]:stroke-border`).
6
- * Wrapping breaks Recharts' child-by-type detection.
7
- */
8
- export { ReferenceArea, ReferenceDot, ReferenceLine } from "recharts";
@@ -1,85 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- export type ServiceHealthStatus = "healthy" | "degraded" | "down";
6
-
7
- export interface ServiceHealthItem {
8
- /** Service name shown in the row. Used as the React key. */
9
- name: string;
10
- /** Status — drives the dot color. */
11
- status: ServiceHealthStatus;
12
- /**
13
- * Right-aligned meta cells, monospace, muted-foreground. Pass an array of
14
- * primitives or React nodes — each renders as its own cell.
15
- */
16
- meta?: React.ReactNode[];
17
- }
18
-
19
- export interface ServiceHealthListProps extends React.HTMLAttributes<HTMLDivElement> {
20
- /** Service rows rendered top-to-bottom. */
21
- items: ServiceHealthItem[];
22
- /** Caption rendered below the list. */
23
- caption?: React.ReactNode;
24
- }
25
-
26
- const DOT_TOKENS: Record<ServiceHealthStatus, string> = {
27
- healthy: "143 70% 45%",
28
- degraded: "38 92% 50%",
29
- down: "0 80% 60%",
30
- };
31
-
32
- /**
33
- * Vertical list of services with a colored status dot, name, and optional
34
- * monospace meta cells (latency, uptime, region, …). The dot has a 3-px halo
35
- * matching the status hue at 18% opacity to pull focus.
36
- */
37
- export const ServiceHealthList = React.forwardRef<HTMLDivElement, ServiceHealthListProps>(
38
- ({ items, caption, className, ...props }, ref) => {
39
- return (
40
- <div ref={ref} className={cn("w-full", className)} {...props}>
41
- <ul className="flex flex-col gap-2.5">
42
- {items.map((item) => {
43
- const hsl = DOT_TOKENS[item.status];
44
- const isHealthy = item.status === "healthy";
45
- return (
46
- <li key={item.name} className="flex items-center gap-2.5 text-[13px]">
47
- <span className="relative flex size-2 shrink-0">
48
- {isHealthy && (
49
- <span
50
- aria-hidden
51
- className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-75"
52
- style={{ background: `hsl(${hsl})` }}
53
- />
54
- )}
55
- <span
56
- role="img"
57
- aria-label={`Status: ${item.status}`}
58
- className="relative inline-flex size-2 rounded-full"
59
- style={{
60
- background: `hsl(${hsl})`,
61
- boxShadow: isHealthy
62
- ? `0 0 6px hsl(${hsl}), 0 0 0 3px hsl(${hsl} / 0.18)`
63
- : `0 0 0 3px hsl(${hsl} / 0.18)`,
64
- }}
65
- />
66
- </span>
67
- <span className="flex-1 font-medium">{item.name}</span>
68
- {item.meta?.map((cell, i) => (
69
- <span
70
- key={`${item.name}-meta-${i}`}
71
- className="font-mono text-[11.5px] text-muted-foreground"
72
- >
73
- {cell}
74
- </span>
75
- ))}
76
- </li>
77
- );
78
- })}
79
- </ul>
80
- {caption && <p className="mt-3 text-xs text-muted-foreground">{caption}</p>}
81
- </div>
82
- );
83
- },
84
- );
85
- ServiceHealthList.displayName = "ServiceHealthList";