@overdoser/react-toolkit 0.0.6 → 0.0.7

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 (47) hide show
  1. package/AGENTS.md +15 -0
  2. package/components/Button/Button.d.ts +13 -0
  3. package/components/Chart/AreaChart.d.ts +85 -0
  4. package/components/Chart/Axis.d.ts +30 -0
  5. package/components/Chart/BarChart.d.ts +111 -0
  6. package/components/Chart/ChartContainer.d.ts +33 -0
  7. package/components/Chart/ChartLegend.d.ts +9 -0
  8. package/components/Chart/ChartTooltip.d.ts +32 -0
  9. package/components/Chart/LineChart.d.ts +112 -0
  10. package/components/Chart/PieChart.d.ts +100 -0
  11. package/components/Chart/RadarChart.d.ts +86 -0
  12. package/components/Chart/Sparkline.d.ts +31 -0
  13. package/components/Chart/TradingChart.d.ts +89 -0
  14. package/components/Chart/index.d.ts +18 -0
  15. package/components/Chart/scales.d.ts +21 -0
  16. package/components/Chart/trading/indicators.d.ts +28 -0
  17. package/components/Chart/trading/period.d.ts +19 -0
  18. package/components/Chart/trading/types.d.ts +122 -0
  19. package/components/Chart/types.d.ts +60 -0
  20. package/components/Chart/useChartDimensions.d.ts +12 -0
  21. package/index.css +1 -1
  22. package/index.d.ts +2 -0
  23. package/index.js +3369 -1077
  24. package/llms.txt +361 -0
  25. package/manifest.json +305 -2
  26. package/package.json +1 -1
  27. package/recipes/dashboard-charts.tsx +123 -0
  28. package/recipes/interactive-area-chart.tsx +226 -0
  29. package/recipes/interactive-bar-chart.tsx +211 -0
  30. package/recipes/interactive-line-chart.tsx +221 -0
  31. package/recipes/interactive-pie-chart.tsx +191 -0
  32. package/recipes/trading-chart.tsx +188 -0
  33. package/components/Button/Button.stories.d.ts +0 -17
  34. package/components/Dropdown/Dropdown.stories.d.ts +0 -8
  35. package/components/Form/Form.stories.d.ts +0 -11
  36. package/components/Link/Link.stories.d.ts +0 -9
  37. package/components/List/List.stories.d.ts +0 -9
  38. package/components/Modal/Modal.stories.d.ts +0 -9
  39. package/components/Popover/Popover.stories.d.ts +0 -9
  40. package/components/Table/Table.stories.d.ts +0 -20
  41. package/components/Typography/Typography.stories.d.ts +0 -15
  42. package/components/inputs/Checkbox/Checkbox.stories.d.ts +0 -9
  43. package/components/inputs/Input/Input.stories.d.ts +0 -13
  44. package/components/inputs/Radio/Radio.stories.d.ts +0 -7
  45. package/components/inputs/Select/Select.stories.d.ts +0 -18
  46. package/components/inputs/Textarea/Textarea.stories.d.ts +0 -10
  47. package/test-setup.d.ts +0 -0
@@ -0,0 +1,226 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ AreaChart,
4
+ Button,
5
+ Dropdown,
6
+ Typography,
7
+ type ChartConfig,
8
+ } from '@overdoser/react-toolkit';
9
+
10
+ interface DayRow extends Record<string, unknown> {
11
+ date: string;
12
+ desktop: number;
13
+ mobile: number;
14
+ }
15
+
16
+ // 30 days of sample traffic. Recipe-sized stand-in for a longer real series.
17
+ const chartData: DayRow[] = [
18
+ { date: '2024-06-01', desktop: 178, mobile: 200 },
19
+ { date: '2024-06-02', desktop: 470, mobile: 410 },
20
+ { date: '2024-06-03', desktop: 103, mobile: 160 },
21
+ { date: '2024-06-04', desktop: 439, mobile: 380 },
22
+ { date: '2024-06-05', desktop: 88, mobile: 140 },
23
+ { date: '2024-06-06', desktop: 294, mobile: 250 },
24
+ { date: '2024-06-07', desktop: 323, mobile: 370 },
25
+ { date: '2024-06-08', desktop: 385, mobile: 320 },
26
+ { date: '2024-06-09', desktop: 438, mobile: 480 },
27
+ { date: '2024-06-10', desktop: 155, mobile: 200 },
28
+ { date: '2024-06-11', desktop: 92, mobile: 150 },
29
+ { date: '2024-06-12', desktop: 492, mobile: 420 },
30
+ { date: '2024-06-13', desktop: 81, mobile: 130 },
31
+ { date: '2024-06-14', desktop: 426, mobile: 380 },
32
+ { date: '2024-06-15', desktop: 307, mobile: 350 },
33
+ { date: '2024-06-16', desktop: 371, mobile: 310 },
34
+ { date: '2024-06-17', desktop: 475, mobile: 520 },
35
+ { date: '2024-06-18', desktop: 107, mobile: 170 },
36
+ { date: '2024-06-19', desktop: 341, mobile: 290 },
37
+ { date: '2024-06-20', desktop: 408, mobile: 450 },
38
+ { date: '2024-06-21', desktop: 169, mobile: 210 },
39
+ { date: '2024-06-22', desktop: 317, mobile: 270 },
40
+ { date: '2024-06-23', desktop: 480, mobile: 530 },
41
+ { date: '2024-06-24', desktop: 132, mobile: 180 },
42
+ { date: '2024-06-25', desktop: 141, mobile: 190 },
43
+ { date: '2024-06-26', desktop: 434, mobile: 380 },
44
+ { date: '2024-06-27', desktop: 448, mobile: 490 },
45
+ { date: '2024-06-28', desktop: 149, mobile: 200 },
46
+ { date: '2024-06-29', desktop: 103, mobile: 160 },
47
+ { date: '2024-06-30', desktop: 446, mobile: 400 },
48
+ ];
49
+
50
+ const config: ChartConfig = {
51
+ desktop: { label: 'Desktop', color: 'var(--crk-chart-1)' },
52
+ mobile: { label: 'Mobile', color: 'var(--crk-chart-2)' },
53
+ };
54
+
55
+ const RANGES = [
56
+ { value: '7d', label: 'Last 7 days', days: 7 },
57
+ { value: '14d', label: 'Last 14 days', days: 14 },
58
+ { value: '30d', label: 'Last 30 days', days: 30 },
59
+ ] as const;
60
+
61
+ type RangeValue = (typeof RANGES)[number]['value'];
62
+
63
+ function useFiltered(range: RangeValue) {
64
+ return useMemo(() => {
65
+ const days = RANGES.find((r) => r.value === range)!.days;
66
+ return chartData.slice(-days);
67
+ }, [range]);
68
+ }
69
+
70
+ const Chevron = ({ direction }: { direction: 'left' | 'right' }) => (
71
+ <svg
72
+ viewBox="0 0 24 24"
73
+ width="14"
74
+ height="14"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth={2.5}
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ aria-hidden="true"
81
+ >
82
+ <polyline points={direction === 'left' ? '15 18 9 12 15 6' : '9 18 15 12 9 6'} />
83
+ </svg>
84
+ );
85
+
86
+ function Chart({ data }: { data: DayRow[] }) {
87
+ return (
88
+ <AreaChart
89
+ data={data}
90
+ xKey="date"
91
+ config={config}
92
+ stacked
93
+ fillGradient
94
+ xFormat={(d) => d.slice(-5)}
95
+ showLegend
96
+ />
97
+ );
98
+ }
99
+
100
+ // ─── Variant 1: Tiles ──────────────────────────────────────────────────────
101
+ //
102
+ // Each preset gets a clickable tile. Best for 2–4 fixed ranges where the
103
+ // user benefits from seeing all options at once.
104
+
105
+ export function InteractiveAreaChartTiles() {
106
+ const [range, setRange] = useState<RangeValue>('30d');
107
+ const data = useFiltered(range);
108
+
109
+ return (
110
+ <div style={panelStyle}>
111
+ <div
112
+ style={{
113
+ display: 'grid',
114
+ gridTemplateColumns: `repeat(${RANGES.length}, 1fr)`,
115
+ borderBottom: '1px solid var(--crk-color-border)',
116
+ }}
117
+ >
118
+ {RANGES.map((r) => {
119
+ const isActive = range === r.value;
120
+ return (
121
+ <button
122
+ key={r.value}
123
+ type="button"
124
+ onClick={() => setRange(r.value)}
125
+ style={{
126
+ padding: 16,
127
+ textAlign: 'left',
128
+ background: isActive ? 'var(--crk-color-primary-light)' : 'transparent',
129
+ border: 'none',
130
+ borderLeft: '4px solid',
131
+ borderLeftColor: isActive ? 'var(--crk-color-primary)' : 'transparent',
132
+ cursor: 'pointer',
133
+ }}
134
+ >
135
+ <Typography variant="span" color="muted">Range</Typography>
136
+ <div style={{ fontSize: 'var(--crk-font-size-2xl)', fontWeight: 600 }}>{r.label}</div>
137
+ </button>
138
+ );
139
+ })}
140
+ </div>
141
+ <div style={{ padding: 16 }}>
142
+ <Chart data={data} />
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ // ─── Variant 2: Navigator ──────────────────────────────────────────────────
149
+ //
150
+ // Prev/next steps through the presets. Handy when space is tight.
151
+
152
+ export function InteractiveAreaChartNavigator() {
153
+ const [index, setIndex] = useState(2);
154
+ const range = RANGES[index];
155
+ const data = useFiltered(range.value);
156
+
157
+ const step = (delta: number) => setIndex((i) => (i + delta + RANGES.length) % RANGES.length);
158
+
159
+ return (
160
+ <div style={panelStyle}>
161
+ <div
162
+ style={{
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ justifyContent: 'space-between',
166
+ padding: 12,
167
+ borderBottom: '1px solid var(--crk-color-border)',
168
+ }}
169
+ >
170
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(-1)} aria-label="Previous range">
171
+ <Chevron direction="left" />
172
+ </Button>
173
+ <Typography variant="span" weight="medium">{range.label}</Typography>
174
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(1)} aria-label="Next range">
175
+ <Chevron direction="right" />
176
+ </Button>
177
+ </div>
178
+ <div style={{ padding: 16 }}>
179
+ <Chart data={data} />
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ // ─── Variant 3: Dropdown ───────────────────────────────────────────────────
186
+ //
187
+ // Uses the toolkit's `<Dropdown>` in select mode. Scales to many presets
188
+ // (custom ranges, etc.).
189
+
190
+ export function InteractiveAreaChartDropdown() {
191
+ const [range, setRange] = useState<RangeValue>('30d');
192
+ const data = useFiltered(range);
193
+
194
+ return (
195
+ <div style={panelStyle}>
196
+ <div
197
+ style={{
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ gap: 12,
201
+ padding: 12,
202
+ borderBottom: '1px solid var(--crk-color-border)',
203
+ }}
204
+ >
205
+ <Typography variant="span" color="muted">Range</Typography>
206
+ <div style={{ width: 220 }}>
207
+ <Dropdown
208
+ options={RANGES.map((r) => ({ value: r.value, label: r.label }))}
209
+ value={range}
210
+ onChange={(v) => setRange(v as RangeValue)}
211
+ />
212
+ </div>
213
+ </div>
214
+ <div style={{ padding: 16 }}>
215
+ <Chart data={data} />
216
+ </div>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ const panelStyle: React.CSSProperties = {
222
+ border: '1px solid var(--crk-color-border)',
223
+ borderRadius: 12,
224
+ overflow: 'hidden',
225
+ background: 'var(--crk-color-bg)',
226
+ };
@@ -0,0 +1,211 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ BarChart,
4
+ Button,
5
+ Dropdown,
6
+ Typography,
7
+ type ChartConfig,
8
+ } from '@overdoser/react-toolkit';
9
+
10
+ interface MonthRow extends Record<string, unknown> {
11
+ month: string;
12
+ desktop: number;
13
+ mobile: number;
14
+ }
15
+
16
+ const chartData: MonthRow[] = [
17
+ { month: 'Jan', desktop: 186, mobile: 80 },
18
+ { month: 'Feb', desktop: 305, mobile: 200 },
19
+ { month: 'Mar', desktop: 237, mobile: 120 },
20
+ { month: 'Apr', desktop: 73, mobile: 190 },
21
+ { month: 'May', desktop: 209, mobile: 130 },
22
+ { month: 'Jun', desktop: 214, mobile: 140 },
23
+ ];
24
+
25
+ const config: ChartConfig = {
26
+ desktop: { label: 'Desktop', color: 'var(--crk-chart-1)' },
27
+ mobile: { label: 'Mobile', color: 'var(--crk-chart-2)' },
28
+ };
29
+
30
+ type SeriesKey = keyof typeof config;
31
+ const seriesKeys = Object.keys(config) as SeriesKey[];
32
+
33
+ function useSeriesTotals() {
34
+ return useMemo(
35
+ () => ({
36
+ desktop: chartData.reduce((s, r) => s + r.desktop, 0),
37
+ mobile: chartData.reduce((s, r) => s + r.mobile, 0),
38
+ }),
39
+ [],
40
+ );
41
+ }
42
+
43
+ const Chevron = ({ direction }: { direction: 'left' | 'right' }) => (
44
+ <svg
45
+ viewBox="0 0 24 24"
46
+ width="14"
47
+ height="14"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ strokeWidth={2.5}
51
+ strokeLinecap="round"
52
+ strokeLinejoin="round"
53
+ aria-hidden="true"
54
+ >
55
+ <polyline points={direction === 'left' ? '15 18 9 12 15 6' : '9 18 15 12 9 6'} />
56
+ </svg>
57
+ );
58
+
59
+ function Chart({ active }: { active: SeriesKey }) {
60
+ return (
61
+ <BarChart
62
+ data={chartData}
63
+ xKey="month"
64
+ config={{ [active]: config[active] }}
65
+ showLegend={false}
66
+ />
67
+ );
68
+ }
69
+
70
+ // ─── Variant 1: Tiles (shadcn-style) ───────────────────────────────────────
71
+ //
72
+ // Each series gets a clickable tile showing its total. Tile background is
73
+ // active-tinted when selected. Best for 2–4 series with meaningful totals.
74
+
75
+ export function InteractiveBarChartTiles() {
76
+ const [active, setActive] = useState<SeriesKey>('desktop');
77
+ const totals = useSeriesTotals();
78
+
79
+ return (
80
+ <div style={panelStyle}>
81
+ <div
82
+ style={{
83
+ display: 'grid',
84
+ gridTemplateColumns: `repeat(${seriesKeys.length}, 1fr)`,
85
+ borderBottom: '1px solid var(--crk-color-border)',
86
+ }}
87
+ >
88
+ {seriesKeys.map((key) => {
89
+ const isActive = active === key;
90
+ return (
91
+ <button
92
+ key={key}
93
+ type="button"
94
+ onClick={() => setActive(key)}
95
+ style={{
96
+ padding: 16,
97
+ textAlign: 'left',
98
+ background: isActive ? 'var(--crk-color-primary-light)' : 'transparent',
99
+ border: 'none',
100
+ borderLeft: '4px solid',
101
+ borderLeftColor: isActive ? config[key].color : 'transparent',
102
+ cursor: 'pointer',
103
+ }}
104
+ >
105
+ <Typography variant="span" color="muted">{config[key].label}</Typography>
106
+ <div style={{ fontSize: 'var(--crk-font-size-2xl)', fontWeight: 600 }}>
107
+ {totals[key].toLocaleString()}
108
+ </div>
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ <div style={{ padding: 16 }}>
114
+ <Chart active={active} />
115
+ </div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ // ─── Variant 2: Navigator (prev / next) ────────────────────────────────────
121
+ //
122
+ // Single label flanked by prev/next buttons. Lightweight when you have many
123
+ // series or limited horizontal space.
124
+
125
+ export function InteractiveBarChartNavigator() {
126
+ const [index, setIndex] = useState(0);
127
+ const active = seriesKeys[index];
128
+
129
+ const step = (delta: number) => {
130
+ setIndex((i) => (i + delta + seriesKeys.length) % seriesKeys.length);
131
+ };
132
+
133
+ return (
134
+ <div style={panelStyle}>
135
+ <div
136
+ style={{
137
+ display: 'flex',
138
+ alignItems: 'center',
139
+ justifyContent: 'space-between',
140
+ padding: 12,
141
+ borderBottom: '1px solid var(--crk-color-border)',
142
+ }}
143
+ >
144
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(-1)} aria-label="Previous series">
145
+ <Chevron direction="left" />
146
+ </Button>
147
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
148
+ <span
149
+ style={{
150
+ width: 10,
151
+ height: 10,
152
+ borderRadius: 999,
153
+ background: config[active].color,
154
+ display: 'inline-block',
155
+ }}
156
+ aria-hidden="true"
157
+ />
158
+ <Typography variant="span" weight="medium">{config[active].label}</Typography>
159
+ </div>
160
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(1)} aria-label="Next series">
161
+ <Chevron direction="right" />
162
+ </Button>
163
+ </div>
164
+ <div style={{ padding: 16 }}>
165
+ <Chart active={active} />
166
+ </div>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ // ─── Variant 3: Dropdown ───────────────────────────────────────────────────
172
+ //
173
+ // Uses the toolkit's `<Dropdown>` in select mode. Scales well to many series
174
+ // without consuming horizontal space.
175
+
176
+ export function InteractiveBarChartDropdown() {
177
+ const [active, setActive] = useState<SeriesKey>('desktop');
178
+
179
+ return (
180
+ <div style={panelStyle}>
181
+ <div
182
+ style={{
183
+ display: 'flex',
184
+ alignItems: 'center',
185
+ gap: 12,
186
+ padding: 12,
187
+ borderBottom: '1px solid var(--crk-color-border)',
188
+ }}
189
+ >
190
+ <Typography variant="span" color="muted">Series</Typography>
191
+ <div style={{ width: 200 }}>
192
+ <Dropdown
193
+ options={seriesKeys.map((key) => ({ value: key, label: config[key].label }))}
194
+ value={active}
195
+ onChange={(v) => setActive(v as SeriesKey)}
196
+ />
197
+ </div>
198
+ </div>
199
+ <div style={{ padding: 16 }}>
200
+ <Chart active={active} />
201
+ </div>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ const panelStyle: React.CSSProperties = {
207
+ border: '1px solid var(--crk-color-border)',
208
+ borderRadius: 12,
209
+ overflow: 'hidden',
210
+ background: 'var(--crk-color-bg)',
211
+ };
@@ -0,0 +1,221 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Dropdown,
5
+ LineChart,
6
+ Typography,
7
+ type ChartConfig,
8
+ } from '@overdoser/react-toolkit';
9
+
10
+ interface DayRow extends Record<string, unknown> {
11
+ date: string;
12
+ desktop: number;
13
+ mobile: number;
14
+ }
15
+
16
+ const chartData: DayRow[] = [
17
+ { date: '2024-04-01', desktop: 222, mobile: 150 },
18
+ { date: '2024-04-02', desktop: 97, mobile: 180 },
19
+ { date: '2024-04-03', desktop: 167, mobile: 120 },
20
+ { date: '2024-04-04', desktop: 242, mobile: 260 },
21
+ { date: '2024-04-05', desktop: 373, mobile: 290 },
22
+ { date: '2024-04-06', desktop: 301, mobile: 340 },
23
+ { date: '2024-04-07', desktop: 245, mobile: 180 },
24
+ { date: '2024-04-08', desktop: 409, mobile: 320 },
25
+ { date: '2024-04-09', desktop: 59, mobile: 110 },
26
+ { date: '2024-04-10', desktop: 261, mobile: 190 },
27
+ { date: '2024-04-11', desktop: 327, mobile: 350 },
28
+ { date: '2024-04-12', desktop: 292, mobile: 210 },
29
+ { date: '2024-04-13', desktop: 342, mobile: 380 },
30
+ { date: '2024-04-14', desktop: 137, mobile: 220 },
31
+ { date: '2024-04-15', desktop: 120, mobile: 170 },
32
+ { date: '2024-04-16', desktop: 138, mobile: 190 },
33
+ { date: '2024-04-17', desktop: 446, mobile: 360 },
34
+ ];
35
+
36
+ const config: ChartConfig = {
37
+ desktop: { label: 'Desktop', color: 'var(--crk-chart-1)' },
38
+ mobile: { label: 'Mobile', color: 'var(--crk-chart-2)' },
39
+ };
40
+
41
+ type SeriesKey = keyof typeof config;
42
+ const seriesKeys = Object.keys(config) as SeriesKey[];
43
+
44
+ function shortDate(d: string) {
45
+ return d.slice(-5); // "MM-DD"
46
+ }
47
+
48
+ function useSeriesTotals() {
49
+ return useMemo(
50
+ () => ({
51
+ desktop: chartData.reduce((s, r) => s + r.desktop, 0),
52
+ mobile: chartData.reduce((s, r) => s + r.mobile, 0),
53
+ }),
54
+ [],
55
+ );
56
+ }
57
+
58
+ const Chevron = ({ direction }: { direction: 'left' | 'right' }) => (
59
+ <svg
60
+ viewBox="0 0 24 24"
61
+ width="14"
62
+ height="14"
63
+ fill="none"
64
+ stroke="currentColor"
65
+ strokeWidth={2.5}
66
+ strokeLinecap="round"
67
+ strokeLinejoin="round"
68
+ aria-hidden="true"
69
+ >
70
+ <polyline points={direction === 'left' ? '15 18 9 12 15 6' : '9 18 15 12 9 6'} />
71
+ </svg>
72
+ );
73
+
74
+ function Chart({ active }: { active: SeriesKey }) {
75
+ return (
76
+ <LineChart
77
+ data={chartData}
78
+ xKey="date"
79
+ config={{ [active]: config[active] }}
80
+ xFormat={shortDate}
81
+ showLegend={false}
82
+ />
83
+ );
84
+ }
85
+
86
+ // ─── Variant 1: Tiles (shadcn-style) ───────────────────────────────────────
87
+ //
88
+ // Each series gets a clickable tile showing its total. Tile background is
89
+ // active-tinted when selected. Best for 2–4 series with meaningful totals.
90
+
91
+ export function InteractiveLineChartTiles() {
92
+ const [active, setActive] = useState<SeriesKey>('desktop');
93
+ const totals = useSeriesTotals();
94
+
95
+ return (
96
+ <div style={panelStyle}>
97
+ <div style={{ display: 'grid', gridTemplateColumns: `repeat(${seriesKeys.length}, 1fr)`, borderBottom: '1px solid var(--crk-color-border)' }}>
98
+ {seriesKeys.map((key) => {
99
+ const isActive = active === key;
100
+ return (
101
+ <button
102
+ key={key}
103
+ type="button"
104
+ onClick={() => setActive(key)}
105
+ style={{
106
+ padding: 16,
107
+ textAlign: 'left',
108
+ background: isActive ? 'var(--crk-color-primary-light)' : 'transparent',
109
+ border: 'none',
110
+ borderLeft: '4px solid',
111
+ borderLeftColor: isActive ? config[key].color : 'transparent',
112
+ cursor: 'pointer',
113
+ }}
114
+ >
115
+ <Typography variant="span" color="muted">{config[key].label}</Typography>
116
+ <div style={{ fontSize: 'var(--crk-font-size-2xl)', fontWeight: 600 }}>
117
+ {totals[key].toLocaleString()}
118
+ </div>
119
+ </button>
120
+ );
121
+ })}
122
+ </div>
123
+ <div style={{ padding: 16 }}>
124
+ <Chart active={active} />
125
+ </div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // ─── Variant 2: Navigator (prev / next) ────────────────────────────────────
131
+ //
132
+ // A single label with prev/next buttons. Lightweight when you have many
133
+ // series or limited horizontal space.
134
+
135
+ export function InteractiveLineChartNavigator() {
136
+ const [index, setIndex] = useState(0);
137
+ const active = seriesKeys[index];
138
+
139
+ const step = (delta: number) => {
140
+ setIndex((i) => (i + delta + seriesKeys.length) % seriesKeys.length);
141
+ };
142
+
143
+ return (
144
+ <div style={panelStyle}>
145
+ <div
146
+ style={{
147
+ display: 'flex',
148
+ alignItems: 'center',
149
+ justifyContent: 'space-between',
150
+ padding: 12,
151
+ borderBottom: '1px solid var(--crk-color-border)',
152
+ }}
153
+ >
154
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(-1)} aria-label="Previous series">
155
+ <Chevron direction="left" />
156
+ </Button>
157
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
158
+ <span
159
+ style={{
160
+ width: 10,
161
+ height: 10,
162
+ borderRadius: 999,
163
+ background: config[active].color,
164
+ display: 'inline-block',
165
+ }}
166
+ aria-hidden="true"
167
+ />
168
+ <Typography variant="span" weight="medium">{config[active].label}</Typography>
169
+ </div>
170
+ <Button variant="ghost" size="sm" iconOnly onClick={() => step(1)} aria-label="Next series">
171
+ <Chevron direction="right" />
172
+ </Button>
173
+ </div>
174
+ <div style={{ padding: 16 }}>
175
+ <Chart active={active} />
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // ─── Variant 3: Dropdown ───────────────────────────────────────────────────
182
+ //
183
+ // Uses the toolkit's `<Dropdown>` in select mode. Scales well to many series
184
+ // without burning horizontal space.
185
+
186
+ export function InteractiveLineChartDropdown() {
187
+ const [active, setActive] = useState<SeriesKey>('desktop');
188
+
189
+ return (
190
+ <div style={panelStyle}>
191
+ <div
192
+ style={{
193
+ display: 'flex',
194
+ alignItems: 'center',
195
+ gap: 12,
196
+ padding: 12,
197
+ borderBottom: '1px solid var(--crk-color-border)',
198
+ }}
199
+ >
200
+ <Typography variant="span" color="muted">Series</Typography>
201
+ <div style={{ width: 200 }}>
202
+ <Dropdown
203
+ options={seriesKeys.map((key) => ({ value: key, label: config[key].label }))}
204
+ value={active}
205
+ onChange={(v) => setActive(v as SeriesKey)}
206
+ />
207
+ </div>
208
+ </div>
209
+ <div style={{ padding: 16 }}>
210
+ <Chart active={active} />
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ const panelStyle: React.CSSProperties = {
217
+ border: '1px solid var(--crk-color-border)',
218
+ borderRadius: 12,
219
+ overflow: 'hidden',
220
+ background: 'var(--crk-color-bg)',
221
+ };