@overdoser/react-toolkit 0.0.6 → 0.0.8
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.
- package/AGENTS.md +15 -0
- package/components/Button/Button.d.ts +13 -0
- package/components/Chart/AreaChart.d.ts +85 -0
- package/components/Chart/Axis.d.ts +30 -0
- package/components/Chart/BarChart.d.ts +111 -0
- package/components/Chart/ChartContainer.d.ts +33 -0
- package/components/Chart/ChartLegend.d.ts +9 -0
- package/components/Chart/ChartTooltip.d.ts +32 -0
- package/components/Chart/LineChart.d.ts +112 -0
- package/components/Chart/PieChart.d.ts +100 -0
- package/components/Chart/RadarChart.d.ts +86 -0
- package/components/Chart/Sparkline.d.ts +31 -0
- package/components/Chart/TradingChart.d.ts +89 -0
- package/components/Chart/index.d.ts +18 -0
- package/components/Chart/scales.d.ts +21 -0
- package/components/Chart/trading/indicators.d.ts +28 -0
- package/components/Chart/trading/period.d.ts +19 -0
- package/components/Chart/trading/types.d.ts +122 -0
- package/components/Chart/types.d.ts +60 -0
- package/components/Chart/useChartDimensions.d.ts +12 -0
- package/components/Popover/Popover.d.ts +16 -1
- package/index.css +1 -1
- package/index.d.ts +2 -0
- package/index.js +3427 -1110
- package/llms.txt +373 -2
- package/manifest.json +307 -3
- package/package.json +1 -1
- package/recipes/dashboard-charts.tsx +123 -0
- package/recipes/interactive-area-chart.tsx +226 -0
- package/recipes/interactive-bar-chart.tsx +211 -0
- package/recipes/interactive-line-chart.tsx +221 -0
- package/recipes/interactive-pie-chart.tsx +191 -0
- package/recipes/trading-chart.tsx +188 -0
- package/components/Button/Button.stories.d.ts +0 -17
- package/components/Dropdown/Dropdown.stories.d.ts +0 -8
- package/components/Form/Form.stories.d.ts +0 -11
- package/components/Link/Link.stories.d.ts +0 -9
- package/components/List/List.stories.d.ts +0 -9
- package/components/Modal/Modal.stories.d.ts +0 -9
- package/components/Popover/Popover.stories.d.ts +0 -9
- package/components/Table/Table.stories.d.ts +0 -20
- package/components/Typography/Typography.stories.d.ts +0 -15
- package/components/inputs/Checkbox/Checkbox.stories.d.ts +0 -9
- package/components/inputs/Input/Input.stories.d.ts +0 -13
- package/components/inputs/Radio/Radio.stories.d.ts +0 -7
- package/components/inputs/Select/Select.stories.d.ts +0 -18
- package/components/inputs/Textarea/Textarea.stories.d.ts +0 -10
- 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
|
+
};
|