diff --git a/apps/storybook/package.json b/apps/storybook/package.json
index 3b93a26..386914c 100644
--- a/apps/storybook/package.json
+++ b/apps/storybook/package.json
@@ -46,6 +46,7 @@
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.0",
- "vitest": "^4.0.16"
+ "vitest": "^4.0.16",
+ "recharts": "2.15.4"
}
}
diff --git a/apps/storybook/tests/Chart.test.tsx b/apps/storybook/tests/Chart.test.tsx
new file mode 100644
index 0000000..4f0dd39
--- /dev/null
+++ b/apps/storybook/tests/Chart.test.tsx
@@ -0,0 +1,92 @@
+import { render } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { ChartContainer } from '@trycompai/design-system';
+import { Area, AreaChart } from 'recharts';
+
+// jsdom has no ResizeObserver, which recharts' ResponsiveContainer needs.
+vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ },
+);
+
+describe('ChartContainer', () => {
+ it('renders a style block for a safe config', () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+ const style = container.querySelector('style');
+ expect(style?.innerHTML).toContain('--color-revenue: #22c55e;');
+ });
+
+ it('drops config keys that could break out of the style element', () => {
+ const { container } = render(
+
': {
+ label: 'Evil',
+ color: 'red',
+ },
+ revenue: { label: 'Revenue', color: '#22c55e' },
+ }}
+ >
+
+
+
+ ,
+ );
+ const style = container.querySelector('style');
+ expect(style?.innerHTML).not.toContain('');
+ expect(style?.innerHTML).not.toContain('onerror');
+ expect(style?.innerHTML).toContain('--color-revenue: #22c55e;');
+ expect(container.querySelector('img')).toBeNull();
+ });
+
+ it('drops color values that could break out of the style element', () => {
+ const { container } = render(
+ ',
+ },
+ }}
+ >
+
+
+
+ ,
+ );
+ const style = container.querySelector('style');
+ expect(style?.innerHTML).not.toContain('');
+ expect(style?.innerHTML).not.toContain('--color-revenue');
+ expect(container.querySelector('script')).toBeNull();
+ });
+
+ it('does not render a style block for an unsafe id', () => {
+ const { container } = render(
+
+
+
+
+ ,
+ );
+ expect(container.querySelector('style')).toBeNull();
+ expect(container.querySelector('script')).toBeNull();
+ });
+});
diff --git a/packages/design-system/package.json b/packages/design-system/package.json
index e01a6ce..55d56d7 100644
--- a/packages/design-system/package.json
+++ b/packages/design-system/package.json
@@ -23,7 +23,6 @@
"default": "./lib/utils.ts"
},
"./globals.css": "./src/styles/globals.css",
- "./fonts/*": "./src/fonts/*",
"./tailwind.config": {
"types": "./tailwind.config.ts",
"import": "./tailwind.config.ts",
diff --git a/packages/design-system/src/components/organisms/chart.tsx b/packages/design-system/src/components/organisms/chart.tsx
index cfaa47b..ecde28c 100644
--- a/packages/design-system/src/components/organisms/chart.tsx
+++ b/packages/design-system/src/components/organisms/chart.tsx
@@ -61,10 +61,20 @@ function ChartContainer({
);
}
+const CHART_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
+const CHART_CONFIG_KEY_PATTERN = /^[a-zA-Z0-9_-]+$/;
+
+// Colors interpolated into the