Skip to content

Commit cb73174

Browse files
Jeffrey Lauwersclaude
andcommitted
Add interactive token controls to Design Tokens page
Features: - Added TokenControls component with theme/mode/density selectors - Users can now manually switch between configurations - Token values update live when configuration changes - Dropdown controls for: Theme (start/rijkshuisstijl/denhaag), Mode (light/dark), Density (default/compact) - Automatically loads and applies selected token stylesheet - Triggers TokenTable refresh after stylesheet loads This replaces the automatic detection approach with explicit user control, making it easier to see how tokens change across different configurations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 1754d73 commit cb73174

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

packages/storybook/src/DesignTokens.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { Meta } from '@storybook/blocks';
22
import { TokenTable } from './components/TokenTable';
3+
import { TokenControls } from './components/TokenControls';
34

45
<Meta title="Foundations/Design Tokens" />
56

67
# Design Tokens
78

89
Design tokens are the single source of truth for colors, typography, spacing, borders, and other visual properties. All tokens are available as CSS custom properties and respond to theme switching.
910

11+
<TokenControls />
12+
1013
---
1114

1215
## Colors
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
interface TokenControlsProps {
4+
onRefresh?: () => void;
5+
}
6+
7+
export function TokenControls({ onRefresh }: TokenControlsProps) {
8+
const [theme, setTheme] = useState('start');
9+
const [mode, setMode] = useState('light');
10+
const [density, setDensity] = useState('default');
11+
12+
// Load current values on mount
13+
useEffect(() => {
14+
if (typeof window === 'undefined') return;
15+
16+
const currentConfig = document
17+
.querySelector('[data-dsn-tokens]')
18+
?.getAttribute('data-dsn-tokens');
19+
20+
if (currentConfig) {
21+
const [t, m, d] = currentConfig.split('-');
22+
setTheme(t || 'start');
23+
setMode(m || 'light');
24+
setDensity(d || 'default');
25+
}
26+
}, []);
27+
28+
const applyTokens = (
29+
newTheme: string,
30+
newMode: string,
31+
newDensity: string
32+
) => {
33+
if (typeof window === 'undefined') return;
34+
35+
// Remove old token stylesheet
36+
const oldLink = document.querySelector('[data-dsn-theme-css]');
37+
if (oldLink) {
38+
oldLink.remove();
39+
}
40+
41+
// Create new token config
42+
const config = `${newTheme}-${newMode}-${newDensity}`;
43+
44+
// Add new stylesheet
45+
const link = document.createElement('link');
46+
link.rel = 'stylesheet';
47+
link.href = `./design-tokens/dist/css/${config}.css`;
48+
link.setAttribute('data-dsn-theme-css', config);
49+
link.setAttribute('data-dsn-tokens', config);
50+
document.head.appendChild(link);
51+
52+
// Wait for stylesheet to load, then trigger refresh
53+
link.addEventListener('load', () => {
54+
if (onRefresh) {
55+
// Use multiple delays to catch all updates
56+
setTimeout(onRefresh, 50);
57+
setTimeout(onRefresh, 150);
58+
setTimeout(onRefresh, 300);
59+
}
60+
61+
// Dispatch custom event for TokenTable to listen to
62+
window.dispatchEvent(new CustomEvent('storybook-globals-updated'));
63+
});
64+
65+
// Update data attribute
66+
document.body.setAttribute('data-dsn-tokens', config);
67+
};
68+
69+
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
70+
const newTheme = e.target.value;
71+
setTheme(newTheme);
72+
applyTokens(newTheme, mode, density);
73+
};
74+
75+
const handleModeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
76+
const newMode = e.target.value;
77+
setMode(newMode);
78+
applyTokens(theme, newMode, density);
79+
};
80+
81+
const handleDensityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
82+
const newDensity = e.target.value;
83+
setDensity(newDensity);
84+
applyTokens(theme, mode, newDensity);
85+
};
86+
87+
return (
88+
<div
89+
style={{
90+
display: 'flex',
91+
gap: '16px',
92+
padding: '16px',
93+
background: 'var(--dsn-color-neutral-bg-subtle, #f6f6f6)',
94+
borderRadius: '8px',
95+
marginBottom: '32px',
96+
flexWrap: 'wrap',
97+
}}
98+
>
99+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
100+
<label
101+
htmlFor="theme-select"
102+
style={{
103+
fontSize: '11px',
104+
fontWeight: 600,
105+
textTransform: 'uppercase',
106+
letterSpacing: '0.05em',
107+
color: 'var(--dsn-color-neutral-color-subtle, #666)',
108+
}}
109+
>
110+
Theme
111+
</label>
112+
<select
113+
id="theme-select"
114+
value={theme}
115+
onChange={handleThemeChange}
116+
style={{
117+
padding: '8px 12px',
118+
borderRadius: '4px',
119+
border:
120+
'1px solid var(--dsn-color-neutral-border-default, #868686)',
121+
background: 'var(--dsn-color-neutral-bg-document, #fff)',
122+
fontSize: '14px',
123+
fontFamily: 'var(--dsn-text-font-family-default, sans-serif)',
124+
cursor: 'pointer',
125+
}}
126+
>
127+
<option value="start">Start (Blue)</option>
128+
<option value="rijkshuisstijl">Rijkshuisstijl</option>
129+
<option value="denhaag">Den Haag</option>
130+
</select>
131+
</div>
132+
133+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
134+
<label
135+
htmlFor="mode-select"
136+
style={{
137+
fontSize: '11px',
138+
fontWeight: 600,
139+
textTransform: 'uppercase',
140+
letterSpacing: '0.05em',
141+
color: 'var(--dsn-color-neutral-color-subtle, #666)',
142+
}}
143+
>
144+
Mode
145+
</label>
146+
<select
147+
id="mode-select"
148+
value={mode}
149+
onChange={handleModeChange}
150+
style={{
151+
padding: '8px 12px',
152+
borderRadius: '4px',
153+
border:
154+
'1px solid var(--dsn-color-neutral-border-default, #868686)',
155+
background: 'var(--dsn-color-neutral-bg-document, #fff)',
156+
fontSize: '14px',
157+
fontFamily: 'var(--dsn-text-font-family-default, sans-serif)',
158+
cursor: 'pointer',
159+
}}
160+
>
161+
<option value="light">Light</option>
162+
<option value="dark">Dark</option>
163+
</select>
164+
</div>
165+
166+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
167+
<label
168+
htmlFor="density-select"
169+
style={{
170+
fontSize: '11px',
171+
fontWeight: 600,
172+
textTransform: 'uppercase',
173+
letterSpacing: '0.05em',
174+
color: 'var(--dsn-color-neutral-color-subtle, #666)',
175+
}}
176+
>
177+
Density
178+
</label>
179+
<select
180+
id="density-select"
181+
value={density}
182+
onChange={handleDensityChange}
183+
style={{
184+
padding: '8px 12px',
185+
borderRadius: '4px',
186+
border:
187+
'1px solid var(--dsn-color-neutral-border-default, #868686)',
188+
background: 'var(--dsn-color-neutral-bg-document, #fff)',
189+
fontSize: '14px',
190+
fontFamily: 'var(--dsn-text-font-family-default, sans-serif)',
191+
cursor: 'pointer',
192+
}}
193+
>
194+
<option value="default">Default (Fluid)</option>
195+
<option value="compact">Compact (Fixed)</option>
196+
</select>
197+
</div>
198+
199+
<div
200+
style={{
201+
display: 'flex',
202+
alignItems: 'flex-end',
203+
marginLeft: 'auto',
204+
}}
205+
>
206+
<p
207+
style={{
208+
margin: 0,
209+
fontSize: '13px',
210+
color: 'var(--dsn-color-neutral-color-subtle, #666)',
211+
fontFamily: 'var(--dsn-text-font-family-default, sans-serif)',
212+
}}
213+
>
214+
Select a configuration to see token values update below
215+
</p>
216+
</div>
217+
</div>
218+
);
219+
}

0 commit comments

Comments
 (0)