@olympusoss/canvas 2.20.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/package.json +41 -177
  2. package/src/cn.ts +3 -0
  3. package/src/index.ts +12 -603
  4. package/src/theme.ts +41 -0
  5. package/src/tokens.ts +11 -0
  6. package/styles/base.css +17 -0
  7. package/styles/canvas.css +69 -52
  8. package/styles/components/alert.css +66 -0
  9. package/styles/components/app-shell.css +46 -0
  10. package/styles/components/avatar.css +15 -0
  11. package/styles/components/badge.css +83 -0
  12. package/styles/components/breadcrumb.css +35 -0
  13. package/styles/components/button-group.css +23 -0
  14. package/styles/components/button.css +107 -0
  15. package/styles/components/calendar.css +73 -0
  16. package/styles/components/card.css +58 -0
  17. package/styles/components/checkbox.css +55 -0
  18. package/styles/components/code-block.css +18 -0
  19. package/styles/components/combobox.css +75 -0
  20. package/styles/components/command.css +94 -0
  21. package/styles/components/data-table.css +142 -0
  22. package/styles/components/dialog.css +72 -0
  23. package/styles/components/dropdown.css +54 -0
  24. package/styles/components/empty-state.css +17 -0
  25. package/styles/components/field.css +27 -0
  26. package/styles/components/filter-panel.css +58 -0
  27. package/styles/components/form.css +27 -0
  28. package/styles/components/icon.css +8 -0
  29. package/styles/components/input-group.css +45 -0
  30. package/styles/components/input.css +56 -0
  31. package/styles/components/kbd.css +15 -0
  32. package/styles/components/page-header.css +52 -0
  33. package/styles/components/pagination.css +48 -0
  34. package/styles/components/popover.css +14 -0
  35. package/styles/components/radio.css +28 -0
  36. package/styles/components/row-menu.css +69 -0
  37. package/styles/components/section-card.css +49 -0
  38. package/styles/components/select.css +57 -0
  39. package/styles/components/separator.css +32 -0
  40. package/styles/components/sheet.css +70 -0
  41. package/styles/components/sidebar.css +146 -0
  42. package/styles/components/skeleton.css +32 -0
  43. package/styles/components/spinner.css +26 -0
  44. package/styles/components/stat-card.css +71 -0
  45. package/styles/components/stepper.css +63 -0
  46. package/styles/components/switch.css +45 -0
  47. package/styles/components/tabs.css +40 -0
  48. package/styles/components/textarea.css +31 -0
  49. package/styles/components/toast.css +95 -0
  50. package/styles/components/tooltip.css +53 -0
  51. package/styles/components/topbar.css +24 -0
  52. package/styles/components/typography.css +105 -0
  53. package/styles/patterns/backdrops.css +35 -0
  54. package/styles/patterns/density.css +66 -0
  55. package/styles/patterns/focus.css +38 -0
  56. package/styles/patterns/glass.css +85 -0
  57. package/styles/patterns/high-contrast.css +70 -0
  58. package/styles/patterns/reduced-motion.css +12 -0
  59. package/styles/patterns/scrollbar.css +10 -0
  60. package/styles/reset.css +89 -0
  61. package/styles/tokens/colors.css +106 -0
  62. package/styles/tokens/motion.css +33 -0
  63. package/styles/tokens/radius.css +10 -0
  64. package/styles/tokens/shadows.css +35 -0
  65. package/styles/tokens/spacing.css +19 -0
  66. package/styles/tokens/typography.css +6 -0
  67. package/styles/tokens/z-index.css +12 -0
  68. package/tsconfig.json +20 -21
  69. package/README.md +0 -60
  70. package/src/components/atoms/README.md +0 -11
  71. package/src/components/atoms/aspect-ratio.tsx +0 -32
  72. package/src/components/atoms/avatar.tsx +0 -98
  73. package/src/components/atoms/badge.tsx +0 -44
  74. package/src/components/atoms/brand-mark.tsx +0 -74
  75. package/src/components/atoms/button.tsx +0 -105
  76. package/src/components/atoms/checkbox.tsx +0 -63
  77. package/src/components/atoms/flex-box.tsx +0 -105
  78. package/src/components/atoms/icon.tsx +0 -34
  79. package/src/components/atoms/input.tsx +0 -92
  80. package/src/components/atoms/label.tsx +0 -41
  81. package/src/components/atoms/logo.tsx +0 -89
  82. package/src/components/atoms/progress.tsx +0 -55
  83. package/src/components/atoms/radio-group.tsx +0 -122
  84. package/src/components/atoms/scroll-area.tsx +0 -106
  85. package/src/components/atoms/section.tsx +0 -48
  86. package/src/components/atoms/separator.tsx +0 -45
  87. package/src/components/atoms/skeleton.tsx +0 -17
  88. package/src/components/atoms/slider.tsx +0 -93
  89. package/src/components/atoms/spinner.tsx +0 -47
  90. package/src/components/atoms/switch.tsx +0 -60
  91. package/src/components/atoms/textarea.tsx +0 -78
  92. package/src/components/atoms/toggle.tsx +0 -80
  93. package/src/components/charts/activity-heatmap.tsx +0 -186
  94. package/src/components/charts/axes.tsx +0 -21
  95. package/src/components/charts/chart-container.tsx +0 -254
  96. package/src/components/charts/chart-legend.tsx +0 -67
  97. package/src/components/charts/chart-tooltip.tsx +0 -161
  98. package/src/components/charts/chart-types.tsx +0 -49
  99. package/src/components/charts/containers.tsx +0 -11
  100. package/src/components/charts/data.tsx +0 -16
  101. package/src/components/charts/details.tsx +0 -25
  102. package/src/components/charts/dot-pulse.tsx +0 -61
  103. package/src/components/charts/gauge.tsx +0 -106
  104. package/src/components/charts/grids.tsx +0 -8
  105. package/src/components/charts/index.ts +0 -62
  106. package/src/components/charts/labeled-bar-list.tsx +0 -85
  107. package/src/components/charts/metric-breakdown.tsx +0 -316
  108. package/src/components/charts/references.tsx +0 -8
  109. package/src/components/charts/service-health-list.tsx +0 -85
  110. package/src/components/charts/sparkline-area.tsx +0 -80
  111. package/src/components/charts/sparkline.tsx +0 -52
  112. package/src/components/charts/stacked-bar.tsx +0 -104
  113. package/src/components/charts/text.tsx +0 -10
  114. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  115. package/src/components/charts/world-heat-map.tsx +0 -184
  116. package/src/components/molecules/README.md +0 -12
  117. package/src/components/molecules/action-bar.tsx +0 -73
  118. package/src/components/molecules/activity-item.tsx +0 -74
  119. package/src/components/molecules/alert.tsx +0 -86
  120. package/src/components/molecules/animated-background.tsx +0 -92
  121. package/src/components/molecules/auth-shell.tsx +0 -95
  122. package/src/components/molecules/brand-lockup.tsx +0 -48
  123. package/src/components/molecules/breadcrumb.tsx +0 -157
  124. package/src/components/molecules/button-group.tsx +0 -104
  125. package/src/components/molecules/calendar.tsx +0 -217
  126. package/src/components/molecules/card.tsx +0 -102
  127. package/src/components/molecules/client-brand.tsx +0 -95
  128. package/src/components/molecules/code-block.tsx +0 -86
  129. package/src/components/molecules/countdown-button.tsx +0 -92
  130. package/src/components/molecules/empty-state.tsx +0 -56
  131. package/src/components/molecules/error-state.tsx +0 -42
  132. package/src/components/molecules/field-display.tsx +0 -35
  133. package/src/components/molecules/input-otp.tsx +0 -74
  134. package/src/components/molecules/launcher-card.tsx +0 -152
  135. package/src/components/molecules/loading-state.tsx +0 -36
  136. package/src/components/molecules/notification-item.tsx +0 -67
  137. package/src/components/molecules/notification-list.tsx +0 -45
  138. package/src/components/molecules/number-badge.tsx +0 -53
  139. package/src/components/molecules/or-separator.tsx +0 -38
  140. package/src/components/molecules/page-header.tsx +0 -88
  141. package/src/components/molecules/page-tabs.tsx +0 -94
  142. package/src/components/molecules/pagination.tsx +0 -150
  143. package/src/components/molecules/password-input.tsx +0 -83
  144. package/src/components/molecules/password-strength-meter.tsx +0 -104
  145. package/src/components/molecules/phone-input.tsx +0 -200
  146. package/src/components/molecules/search-bar.tsx +0 -64
  147. package/src/components/molecules/secret-field.tsx +0 -158
  148. package/src/components/molecules/section-card.tsx +0 -91
  149. package/src/components/molecules/social-buttons.tsx +0 -165
  150. package/src/components/molecules/stat-card.tsx +0 -100
  151. package/src/components/molecules/status-badge.tsx +0 -42
  152. package/src/components/molecules/stepper.tsx +0 -96
  153. package/src/components/molecules/table.tsx +0 -157
  154. package/src/components/molecules/terminal.tsx +0 -74
  155. package/src/components/molecules/toggle-group.tsx +0 -145
  156. package/src/components/molecules/tooltip.tsx +0 -155
  157. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  158. package/src/components/organisms/README.md +0 -14
  159. package/src/components/organisms/accordion.tsx +0 -154
  160. package/src/components/organisms/alert-dialog.tsx +0 -277
  161. package/src/components/organisms/carousel.tsx +0 -244
  162. package/src/components/organisms/collapsible.tsx +0 -69
  163. package/src/components/organisms/command.tsx +0 -144
  164. package/src/components/organisms/context-menu.tsx +0 -339
  165. package/src/components/organisms/dashboard-grid.tsx +0 -369
  166. package/src/components/organisms/data-table.tsx +0 -330
  167. package/src/components/organisms/dialog.tsx +0 -312
  168. package/src/components/organisms/drawer.tsx +0 -123
  169. package/src/components/organisms/dropdown-menu.tsx +0 -440
  170. package/src/components/organisms/editors/code-editor.tsx +0 -144
  171. package/src/components/organisms/editors/index.ts +0 -4
  172. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  173. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  174. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  175. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  176. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  177. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  178. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  179. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  180. package/src/components/organisms/error-boundary.tsx +0 -61
  181. package/src/components/organisms/form.tsx +0 -174
  182. package/src/components/organisms/hover-card.tsx +0 -115
  183. package/src/components/organisms/menubar.tsx +0 -498
  184. package/src/components/organisms/navbar.tsx +0 -104
  185. package/src/components/organisms/navigation-menu.tsx +0 -235
  186. package/src/components/organisms/popover.tsx +0 -149
  187. package/src/components/organisms/resizable.tsx +0 -58
  188. package/src/components/organisms/schema-form.tsx +0 -232
  189. package/src/components/organisms/select.tsx +0 -309
  190. package/src/components/organisms/sheet.tsx +0 -265
  191. package/src/components/organisms/sidebar.tsx +0 -1040
  192. package/src/components/organisms/sonner.tsx +0 -96
  193. package/src/components/organisms/tabs.tsx +0 -133
  194. package/src/components/organisms/theme-provider.tsx +0 -101
  195. package/src/hooks/use-mobile.tsx +0 -19
  196. package/src/lib/portal-container.tsx +0 -35
  197. package/src/lib/utils.ts +0 -6
  198. package/src/native.ts +0 -23
  199. package/src/tokens/colors.ts +0 -91
  200. package/src/tokens/index.ts +0 -3
  201. package/src/tokens/spacing.ts +0 -55
  202. package/src/tokens/typography.ts +0 -27
  203. package/styles/dashboard-grid.css +0 -47
  204. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  205. package/styles/glass.css +0 -175
  206. package/styles/leaflet.css +0 -13
  207. package/styles/tokens.css +0 -317
  208. package/tailwind.config.ts +0 -70
@@ -1,80 +0,0 @@
1
- "use client";
2
-
3
- import * as TogglePrimitive from "@radix-ui/react-toggle";
4
- import { cva, type VariantProps } from "class-variance-authority";
5
- import * as React from "react";
6
-
7
- import { cn } from "../../lib/utils";
8
-
9
- const toggleVariants = cva(
10
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-brand data-[state=on]:text-brand-foreground data-[state=on]:hover:bg-brand/90 data-[state=on]:hover:text-brand-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11
- {
12
- variants: {
13
- variant: {
14
- default: "bg-transparent",
15
- outline:
16
- "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17
- },
18
- size: {
19
- default: "h-9 px-2 min-w-9",
20
- sm: "h-8 px-1.5 min-w-8",
21
- lg: "h-10 px-2.5 min-w-10",
22
- },
23
- },
24
- defaultVariants: {
25
- variant: "default",
26
- size: "default",
27
- },
28
- },
29
- );
30
-
31
- export interface ToggleProps
32
- extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
33
- VariantProps<typeof toggleVariants> {
34
- /**
35
- * `default` (filled when on) or `outline` (bordered).
36
- * @default "default"
37
- */
38
- variant?: "default" | "outline";
39
- /**
40
- * Square size — `sm` (32px), `default` (36px), or `lg` (40px).
41
- * @default "default"
42
- */
43
- size?: "default" | "sm" | "lg";
44
- /** Controlled pressed state. Pair with `onPressedChange`. */
45
- pressed?: boolean;
46
- /**
47
- * Initial pressed state for uncontrolled usage.
48
- * @default false
49
- */
50
- defaultPressed?: boolean;
51
- /** Fires when the user toggles the pressed state. */
52
- onPressedChange?: (pressed: boolean) => void;
53
- /**
54
- * Disable the toggle.
55
- * @default false
56
- */
57
- disabled?: boolean;
58
- /**
59
- * Render as a Radix Slot.
60
- * @default false
61
- */
62
- asChild?: boolean;
63
- /** Toggle content (typically an icon or short label). */
64
- children?: React.ReactNode;
65
- className?: string;
66
- }
67
-
68
- const Toggle = React.forwardRef<React.ElementRef<typeof TogglePrimitive.Root>, ToggleProps>(
69
- ({ className, variant, size, ...props }, ref) => (
70
- <TogglePrimitive.Root
71
- ref={ref}
72
- className={cn(toggleVariants({ variant, size, className }))}
73
- {...props}
74
- />
75
- ),
76
- );
77
-
78
- Toggle.displayName = TogglePrimitive.Root.displayName;
79
-
80
- export { Toggle, toggleVariants };
@@ -1,186 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
-
5
- const DEFAULT_WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
6
-
7
- export interface ActivityHeatmapProps extends React.HTMLAttributes<HTMLDivElement> {
8
- /**
9
- * Cell values in row-major order. Each entry is a number in `[0, 1]`
10
- * where `0` paints the lowest tint and `1` paints the highest. Values are
11
- * clamped on render. The grid dimensions come from `data.length` and
12
- * `data[0].length`; pass jagged arrays only if you accept short rows.
13
- */
14
- data: number[][];
15
- /**
16
- * CSS variable name (without leading `--`) used for the cell hue. Default
17
- * `chart-1`. Cells render as `hsl(var(--{colorVar}) / opacity)`.
18
- */
19
- colorVar?: string;
20
- /** Pixel height of each cell row. Default `14`. */
21
- cellHeight?: number;
22
- /** Pixel gap between cells. Default `2`. */
23
- gap?: number;
24
- /** Pixel border-radius on each cell. Default `3`. */
25
- cellRadius?: number;
26
- /**
27
- * Render the cell `title` attribute (browser tooltip on hover) for each
28
- * coordinate. Receives `(rowIndex, colIndex, value)` and should return a
29
- * string. Returns nothing → no title set.
30
- */
31
- cellTitle?: (row: number, col: number, value: number) => string | undefined;
32
- /**
33
- * Optional left-side row labels (Y-axis). When provided, must align with
34
- * `data.length`. Empty / nullish entries render as blank cells so the
35
- * label column stays aligned with the grid.
36
- */
37
- rowLabels?: React.ReactNode[];
38
- /**
39
- * Optional bottom-side column labels (X-axis). When provided, must align
40
- * with `data[0].length`. Pass empty strings (or `null`) for indices you
41
- * want to leave blank — useful for sparse hour ticks (e.g. only label
42
- * 0/6/12/18/23 across a 24-column matrix).
43
- */
44
- colLabels?: React.ReactNode[];
45
- /**
46
- * Show a "Fewer ↔ More" gradient legend below the grid. Pass `true` for
47
- * the default labels, or an object to override one or both ends. The
48
- * gradient mirrors the cell-opacity ramp (`0.08` → `0.93`).
49
- * @default false
50
- */
51
- legend?: boolean | { fromLabel?: React.ReactNode; toLabel?: React.ReactNode };
52
- }
53
-
54
- /**
55
- * CSS-grid heatmap of opacity-tinted cells. Useful for time-of-day × day-of-week
56
- * matrices (token issuance, sign-in concentration, queue depth) where a full
57
- * chart would be overkill. Rendering is a flat `display: grid` — no canvas, no
58
- * SVG, fully interactive via hover titles.
59
- *
60
- * Optionally renders row labels on the left, sparse column labels below, and a
61
- * `Fewer ↔ More` gradient legend — toggle each with `rowLabels` / `colLabels` /
62
- * `legend` props.
63
- */
64
- export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapProps>(
65
- (
66
- {
67
- data,
68
- colorVar = "chart-1",
69
- cellHeight = 14,
70
- gap = 2,
71
- cellRadius = 3,
72
- cellTitle,
73
- rowLabels,
74
- colLabels,
75
- legend = false,
76
- className,
77
- ...props
78
- },
79
- ref,
80
- ) => {
81
- const cols = data[0]?.length ?? 0;
82
- const legendObj = typeof legend === "object" ? legend : null;
83
- const fromLabel = legendObj?.fromLabel ?? "Fewer";
84
- const toLabel = legendObj?.toLabel ?? "More";
85
- const showLegend = legend !== false;
86
- // 7-row grids default to weekday labels (Mon–Sun) — covers the common
87
- // GitHub-style yearly contribution pattern without each consumer
88
- // re-declaring the array.
89
- const resolvedRowLabels =
90
- rowLabels ?? (data.length === 7 ? DEFAULT_WEEKDAY_LABELS.slice() : undefined);
91
-
92
- return (
93
- <div ref={ref} className={cn("w-full", className)} {...props}>
94
- <div className="flex gap-2">
95
- {resolvedRowLabels && resolvedRowLabels.length > 0 && (
96
- <div
97
- className="grid text-[10px] tabular-nums text-muted-foreground"
98
- style={{
99
- gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
100
- rowGap: gap,
101
- }}
102
- aria-hidden
103
- >
104
- {Array.from({ length: data.length }, (_, i) => (
105
- <span key={`row-label-${i}`} className="flex items-center leading-none">
106
- {resolvedRowLabels[i] ?? ""}
107
- </span>
108
- ))}
109
- </div>
110
- )}
111
- <div className="flex-1">
112
- <div
113
- style={{
114
- display: "grid",
115
- gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
116
- gap,
117
- }}
118
- >
119
- {data.map((row, r) => (
120
- <div
121
- key={`r-${r}`}
122
- style={{
123
- display: "grid",
124
- gridTemplateColumns: `repeat(${cols}, 1fr)`,
125
- gap,
126
- }}
127
- >
128
- {row.map((raw, c) => {
129
- const v = Math.max(0, Math.min(1, raw));
130
- const opacity = 0.08 + v * 0.85;
131
- const title = cellTitle?.(r, c, v);
132
- return (
133
- <div
134
- key={`c-${r}-${c}`}
135
- data-cell=""
136
- title={title}
137
- aria-hidden
138
- style={{
139
- borderRadius: cellRadius,
140
- background: `hsl(var(--${colorVar}) / ${opacity})`,
141
- }}
142
- />
143
- );
144
- })}
145
- </div>
146
- ))}
147
- </div>
148
- {colLabels && colLabels.length > 0 && (
149
- <div
150
- className="mt-2 grid text-[10px] tabular-nums text-muted-foreground"
151
- style={{
152
- gridTemplateColumns: `repeat(${cols}, 1fr)`,
153
- columnGap: gap,
154
- }}
155
- >
156
- {Array.from({ length: cols }, (_, i) => {
157
- const label = colLabels[i];
158
- const empty = label === undefined || label === null || label === "";
159
- return (
160
- <span key={`col-label-${i}`} className="text-center" aria-hidden={empty}>
161
- {empty ? "" : label}
162
- </span>
163
- );
164
- })}
165
- </div>
166
- )}
167
- </div>
168
- </div>
169
- {showLegend && (
170
- <div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
171
- <span>{fromLabel}</span>
172
- <div
173
- className="h-2 flex-1 rounded-full"
174
- style={{
175
- background: `linear-gradient(90deg, hsl(var(--${colorVar}) / 0.08) 0%, hsl(var(--${colorVar}) / 0.93) 100%)`,
176
- }}
177
- aria-hidden
178
- />
179
- <span>{toLabel}</span>
180
- </div>
181
- )}
182
- </div>
183
- );
184
- },
185
- );
186
- ActivityHeatmap.displayName = "ActivityHeatmap";
@@ -1,21 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Axis re-exports — pure pass-throughs of the Recharts primitives. Wrapping
5
- * them in function components breaks Recharts' `findAllByType` introspection
6
- * (chart types like LineChart look for `child.type === XAxis` to detect axes;
7
- * our wrapper would have a different type, so the chart can't find them and
8
- * crashes with "xAxisId requires a corresponding xAxisId on the targeted
9
- * graphical component" errors).
10
- *
11
- * Theming is applied via CSS selectors on the `<ChartContainer>` wrapping div
12
- * (`[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground`, etc.).
13
- */
14
- export {
15
- CartesianAxis,
16
- PolarAngleAxis,
17
- PolarRadiusAxis,
18
- XAxis,
19
- YAxis,
20
- ZAxis,
21
- } from "recharts";
@@ -1,254 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as RechartsPrimitive from "recharts";
5
-
6
- import { cn } from "../../lib/utils";
7
-
8
- // Format: { THEME_NAME: CSS_SELECTOR }
9
- const THEMES = { light: "", dark: ".dark" } as const;
10
-
11
- /**
12
- * Recharts data primitives that should auto-cycle through `--chart-N`
13
- * colours when the consumer didn't pass an explicit `fill` / `stroke`.
14
- *
15
- * We compare by reference (the actual class component from `recharts`) — the
16
- * deep-clone walker uses `child.type === T` to detect them. This works because
17
- * canvas re-exports these symbols from `recharts` directly (see `data.tsx`).
18
- */
19
- const PALETTE_TARGETS = new Set<unknown>([
20
- RechartsPrimitive.Line,
21
- RechartsPrimitive.Bar,
22
- RechartsPrimitive.Area,
23
- RechartsPrimitive.Pie,
24
- RechartsPrimitive.Scatter,
25
- RechartsPrimitive.Radar,
26
- RechartsPrimitive.RadialBar,
27
- RechartsPrimitive.Funnel,
28
- ]);
29
-
30
- const PALETTE_SIZE = 5;
31
-
32
- function nextColour(counter: { i: number }) {
33
- const idx = counter.i++ % PALETTE_SIZE;
34
- return `hsl(var(--chart-${idx + 1}))`;
35
- }
36
-
37
- /**
38
- * Walk a Pie's `<Cell>` children and inject per-cell palette fills when none
39
- * is set, so each slice gets a distinct hue without requiring the consumer to
40
- * write `<Cell fill={...} />` for every entry.
41
- */
42
- function paintPieCells(children: React.ReactNode, counter: { i: number }): React.ReactNode {
43
- return React.Children.map(children, (child) => {
44
- if (!React.isValidElement(child)) return child;
45
- const cellProps = child.props as { fill?: unknown };
46
- if (child.type === RechartsPrimitive.Cell && cellProps.fill === undefined) {
47
- return React.cloneElement(child, { fill: nextColour(counter) } as Partial<typeof cellProps>);
48
- }
49
- return child;
50
- });
51
- }
52
-
53
- /**
54
- * Inject per-row `fill` on Funnel data when the consumer didn't supply one.
55
- * Recharts Funnel reads each datum's `fill` to colour the stage, so we map
56
- * the array and assign palette colours in order.
57
- */
58
- function paintFunnelData<T extends { fill?: unknown }>(data: T[], counter: { i: number }): T[] {
59
- return data.map((row) => (row.fill === undefined ? { ...row, fill: nextColour(counter) } : row));
60
- }
61
-
62
- /**
63
- * Recursively walk the children, cloning any data primitive that's missing
64
- * `fill`/`stroke` and injecting an `hsl(var(--chart-N))` default. Counter is
65
- * passed by reference so order is deterministic across the entire tree.
66
- */
67
- function applyPalette(children: React.ReactNode, counter: { i: number }): React.ReactNode {
68
- return React.Children.map(children, (child) => {
69
- if (!React.isValidElement(child)) return child;
70
- const props = child.props as {
71
- fill?: unknown;
72
- stroke?: unknown;
73
- children?: React.ReactNode;
74
- data?: unknown;
75
- label?: unknown;
76
- };
77
-
78
- // Pie: walk Cell children and assign per-slice palette colours so each
79
- // slice is distinct. Also default `label={true}` so slices render their
80
- // data-key value at the perimeter without needing a `<LabelList>`.
81
- if (child.type === RechartsPrimitive.Pie) {
82
- const next: Record<string, unknown> = {};
83
- if (props.children !== undefined) {
84
- next.children = paintPieCells(props.children, counter);
85
- } else if (props.fill === undefined && props.stroke === undefined) {
86
- const colour = nextColour(counter);
87
- next.fill = colour;
88
- next.stroke = colour;
89
- }
90
- if (props.label === undefined) next.label = true;
91
- return React.cloneElement(child, next as Partial<typeof props>);
92
- }
93
-
94
- // Funnel: distribute palette colours across the data array (Recharts
95
- // reads `fill` from each datum, not from the <Funnel> element).
96
- if (child.type === RechartsPrimitive.Funnel && Array.isArray(props.data)) {
97
- return React.cloneElement(child, {
98
- data: paintFunnelData(props.data as { fill?: unknown }[], counter),
99
- } as Partial<typeof props>);
100
- }
101
-
102
- if (PALETTE_TARGETS.has(child.type)) {
103
- if (props.fill === undefined && props.stroke === undefined) {
104
- const colour = nextColour(counter);
105
- // Pie/Radar/Area also benefit from a matching stroke; the
106
- // component decides which one applies (Bar reads fill, Line
107
- // reads stroke, etc.).
108
- return React.cloneElement(child, {
109
- fill: colour,
110
- stroke: colour,
111
- } as Partial<typeof props>);
112
- }
113
- return child;
114
- }
115
-
116
- // Recurse into children of layout / chart-type / fragment elements so
117
- // data primitives nested inside (e.g. <BarChart><Bar/></BarChart>)
118
- // still get assigned a palette colour.
119
- if (props.children !== undefined) {
120
- return React.cloneElement(
121
- child,
122
- {} as Partial<typeof props>,
123
- applyPalette(props.children, counter),
124
- );
125
- }
126
- return child;
127
- });
128
- }
129
-
130
- export type ChartConfig = {
131
- [k in string]: {
132
- label?: React.ReactNode;
133
- icon?: React.ComponentType;
134
- } & (
135
- | { color?: string; theme?: never }
136
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
137
- );
138
- };
139
-
140
- interface ChartContextProps {
141
- config: ChartConfig;
142
- }
143
-
144
- const ChartContext = React.createContext<ChartContextProps | null>(null);
145
-
146
- export function useChart() {
147
- const context = React.useContext(ChartContext);
148
-
149
- if (!context) {
150
- throw new Error("useChart must be used within a <ChartContainer />");
151
- }
152
-
153
- return context;
154
- }
155
-
156
- const ChartContainer = React.forwardRef<
157
- HTMLDivElement,
158
- React.ComponentProps<"div"> & {
159
- config: ChartConfig;
160
- children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
161
- }
162
- >(({ id, className, children, config, ...props }, ref) => {
163
- const uniqueId = React.useId();
164
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
165
-
166
- const themedChildren = React.useMemo(
167
- () => applyPalette(children, { i: 0 }) as typeof children,
168
- [children],
169
- );
170
-
171
- return (
172
- <ChartContext.Provider value={{ config }}>
173
- <div
174
- data-chart={chartId}
175
- ref={ref}
176
- className={cn(
177
- "flex aspect-video w-full justify-center text-xs [&_.recharts-cartesian-axis-line]:stroke-border [&_.recharts-cartesian-axis-tick-line]:stroke-transparent [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
178
- className,
179
- )}
180
- {...props}
181
- >
182
- <ChartStyle id={chartId} config={config} />
183
- <RechartsPrimitive.ResponsiveContainer>
184
- {themedChildren}
185
- </RechartsPrimitive.ResponsiveContainer>
186
- </div>
187
- </ChartContext.Provider>
188
- );
189
- });
190
- ChartContainer.displayName = "Chart";
191
-
192
- export const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
193
- const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
194
-
195
- if (!colorConfig.length) {
196
- return null;
197
- }
198
-
199
- return (
200
- <style
201
- dangerouslySetInnerHTML={{
202
- __html: Object.entries(THEMES)
203
- .map(
204
- ([theme, prefix]) => `
205
- ${prefix} [data-chart=${id}] {
206
- ${colorConfig
207
- .map(([key, itemConfig]) => {
208
- const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
209
- return color ? ` --color-${key}: ${color};` : null;
210
- })
211
- .join("\n")}
212
- }
213
- `,
214
- )
215
- .join("\n"),
216
- }}
217
- />
218
- );
219
- };
220
-
221
- /**
222
- * Look up the canvas config entry that matches a Recharts payload item.
223
- * Used by tooltip and legend components to resolve labels / icons / colours
224
- * from the ChartConfig prop.
225
- */
226
- export function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
227
- /* c8 ignore next 3 -- defensive guard: payload is always a recharts item object at call sites */
228
- if (typeof payload !== "object" || payload === null) {
229
- return undefined;
230
- }
231
-
232
- const payloadPayload =
233
- "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
234
- ? payload.payload
235
- : undefined;
236
-
237
- let configLabelKey: string = key;
238
-
239
- /* c8 ignore next 3 -- defensive name-key lookup: real recharts payloads don't carry this shape */
240
- if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
241
- configLabelKey = payload[key as keyof typeof payload] as string;
242
- } else if (
243
- /* c8 ignore next 3 -- defensive nested-name lookup: only reachable with custom dataset shape */
244
- payloadPayload &&
245
- key in payloadPayload &&
246
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
247
- ) {
248
- configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
249
- }
250
-
251
- return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
252
- }
253
-
254
- export { ChartContainer };
@@ -1,67 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as RechartsPrimitive from "recharts";
5
-
6
- import { cn } from "../../lib/utils";
7
- import { getPayloadConfigFromPayload, useChart } from "./chart-container";
8
-
9
- const ChartLegend = RechartsPrimitive.Legend;
10
-
11
- const ChartLegendContent = React.forwardRef<
12
- HTMLDivElement,
13
- React.ComponentProps<"div"> &
14
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
15
- hideIcon?: boolean;
16
- nameKey?: string;
17
- }
18
- >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
19
- const { config } = useChart();
20
-
21
- if (!payload?.length) {
22
- return null;
23
- }
24
-
25
- return (
26
- <div
27
- ref={ref}
28
- className={cn(
29
- "flex items-center justify-center gap-4",
30
- verticalAlign === "top" ? "pb-3" : "pt-3",
31
- className,
32
- )}
33
- >
34
- {payload
35
- .filter((item) => item.type !== "none")
36
- .map((item) => {
37
- /* c8 ignore next -- prefers nameKey when provided; remaining fallbacks are recharts-internal edge cases */
38
- const key = `${nameKey || item.dataKey || "value"}`;
39
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
40
-
41
- return (
42
- <div
43
- key={item.value}
44
- className={cn(
45
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
46
- )}
47
- >
48
- {itemConfig?.icon && !hideIcon ? (
49
- <itemConfig.icon />
50
- ) : (
51
- <div
52
- className="h-2 w-2 shrink-0 rounded-[2px]"
53
- style={{
54
- backgroundColor: item.color,
55
- }}
56
- />
57
- )}
58
- {itemConfig?.label}
59
- </div>
60
- );
61
- })}
62
- </div>
63
- );
64
- });
65
- ChartLegendContent.displayName = "ChartLegend";
66
-
67
- export { ChartLegend, ChartLegendContent };