"use client" import * as React from "react" import { ResponsiveContainer, Tooltip, Legend, } from "recharts" import type { LegendPayload, LegendProps, TooltipContentProps } from "recharts" import { cn } from "../../lib/utils" // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const export type ChartConfig = { [k in string]: { label?: React.ReactNode icon?: React.ComponentType } & ( | { color?: string; theme?: never } | { color?: never; theme: Record } ) } type ChartContextProps = { config: ChartConfig } const ChartContext = React.createContext(null) function useChart() { const context = React.useContext(ChartContext) if (!context) { throw new Error("useChart must be used within a ") } return context } /** * `ResponsiveContainer.debounce` (ms) used by every `ChartContainer`. The * sidebar uses `transition-[width] duration-200 ease-out` and dashboards * mount many `ChartContainer`s side-by-side — without a debounce, every * frame of that 200ms width tween triggers a full Recharts SVG re-render * per chart, saturating the main thread and making the rail feel laggy. * * 120ms is long enough that no measure fires during a 200ms transition * (the chart re-renders once, ~120ms after the rail settles) but short * enough that real window resizes still feel responsive. * * Consumers can override per-chart via the `debounce` prop on `ChartContainer`. */ const CHART_RESIZE_DEBOUNCE_MS = 120 function ChartContainer({ id, className, children, config, debounce = CHART_RESIZE_DEBOUNCE_MS, ...props }: React.ComponentProps<"div"> & { config: ChartConfig children: React.ComponentProps< typeof ResponsiveContainer >["children"] /** Forwarded to Recharts `ResponsiveContainer.debounce` (ms). */ debounce?: number }) { const uniqueId = React.useId() const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartContextValue = React.useMemo(() => ({ config }), [config]) return (
{children}
) } /** * Conservative validators for the two values interpolated into a ``) we accept only a documented allowlist of * CSS color syntaxes and reject anything that contains rule-terminating or * markup-sensitive characters. * * Keys come from `ChartConfig` and become CSS custom-property names, so they * are restricted to a safe identifier alphabet. */ const CSS_KEY_PATTERN = /^[A-Za-z0-9_-]+$/ const SAFE_COLOR_PATTERN = new RegExp( [ /^#[0-9a-fA-F]{3,8}$/, // #rgb / #rrggbb / #rrggbbaa /^rgba?\([^;{}<>"'\\]*\)$/, // rgb()/rgba() /^hsla?\([^;{}<>"'\\]*\)$/, // hsl()/hsla() /^hwb\([^;{}<>"'\\]*\)$/, // hwb() /^lab\([^;{}<>"'\\]*\)$/, // lab() /^lch\([^;{}<>"'\\]*\)$/, // lch() /^oklab\([^;{}<>"'\\]*\)$/, // oklab() /^oklch\([^;{}<>"'\\]*\)$/, // oklch() /^color\([^;{}<>"'\\]*\)$/, // color() /^color-mix\([^;{}<>"'\\]*\)$/, // color-mix() /^var\(--[A-Za-z0-9_-]+(?:\s*,[^;{}<>"'\\]+)?\)$/, // var(--token[, fallback]) /^[a-zA-Z]+$/, // named colors + currentColor/transparent ] .map((re) => re.source) .join("|"), ) function sanitizeChartColor(color: string): string | null { const trimmed = color.trim() if (!trimmed) return null // Defence-in-depth: any of these characters could break out of the value // and turn the inline `