Skip to content

Instantly share code, notes, and snippets.

@tomhermans
Created May 27, 2025 20:57
Show Gist options
  • Save tomhermans/20166b514a86ffe12ef8193d4960788b to your computer and use it in GitHub Desktop.
Save tomhermans/20166b514a86ffe12ef8193d4960788b to your computer and use it in GitHub Desktop.
Untitled
<script src="https://cdnjs.cloudflare.com/ajax/libs/svelte/4.2.0/svelte.min.js"></script>
<div id="app"></div>
// Token store using Svelte runes equivalent (state management)
class TokenStore {
constructor() {
this.tokens = {
colors: {
blue: {
100: '#e6f3ff', 200: '#bde0ff', 300: '#94d3ff', 400: '#5eb8ff',
500: '#2196f3', 600: '#1976d2', 700: '#1565c0', 800: '#0d47a1', 900: '#003d7a'
},
neutral: {
100: '#f8f9fa', 200: '#e9ecef', 300: '#dee2e6', 400: '#ced4da',
500: '#adb5bd', 600: '#6c757d', 700: '#495057', 800: '#343a40', 900: '#212529'
},
red: {
100: '#ffebee', 200: '#ffcdd2', 300: '#ef9a9a', 400: '#e57373',
500: '#f44336', 600: '#e53935', 700: '#d32f2f', 800: '#c62828', 900: '#b71c1c'
}
},
sizes: {
xs: '0.25rem', sm: '0.5rem', md: '1rem',
lg: '1.5rem', xl: '2rem', '2xl': '3rem', '3xl': '4rem'
},
fonts: {
sans: 'system-ui, -apple-system, sans-serif',
serif: 'Georgia, serif',
mono: '"Courier New", monospace'
},
weights: {
normal: '400', medium: '500', semibold: '600', bold: '700'
}
};
this.sectionOverrides = {};
this.selectedSection = null;
this.panelOpen = false;
this.activeTab = 'colors';
this.subscribers = [];
this.updateRootProperties();
}
subscribe(callback) {
this.subscribers.push(callback);
return () => {
this.subscribers = this.subscribers.filter(sub => sub !== callback);
};
}
notify() {
this.subscribers.forEach(callback => callback());
}
updateToken(category, key, value) {
if (this.tokens[category]) {
this.tokens[category][key] = value;
this.updateRootProperties();
this.notify();
}
}
updateColorToken(color, shade, value) {
if (this.tokens.colors[color]) {
this.tokens.colors[color][shade] = value;
this.updateRootProperties();
this.notify();
}
}
setSectionOverride(sectionId, property, value) {
if (!this.sectionOverrides[sectionId]) {
this.sectionOverrides[sectionId] = {};
}
this.sectionOverrides[sectionId][property] = value;
this.applySectionStyles(sectionId);
this.notify();
}
selectSection(sectionId) {
this.selectedSection = sectionId;
this.panelOpen = true;
this.notify();
}
closePanel() {
this.panelOpen = false;
this.selectedSection = null;
this.notify();
}
setActiveTab(tab) {
this.activeTab = tab;
this.notify();
}
updateRootProperties() {
const root = document.documentElement;
// Update color tokens
Object.entries(this.tokens.colors).forEach(([color, shades]) => {
Object.entries(shades).forEach(([shade, value]) => {
root.style.setProperty(`--clr-${color}-${shade}`, value);
});
});
// Update size tokens
Object.entries(this.tokens.sizes).forEach(([key, value]) => {
root.style.setProperty(`--size-${key}`, value);
});
// Update font tokens
Object.entries(this.tokens.fonts).forEach(([key, value]) => {
root.style.setProperty(`--font-${key}`, value);
});
// Update weight tokens
Object.entries(this.tokens.weights).forEach(([key, value]) => {
root.style.setProperty(`--weight-${key}`, value);
});
}
applySectionStyles(sectionId) {
const element = document.getElementById(sectionId);
if (!element || !this.sectionOverrides[sectionId]) return;
const overrides = this.sectionOverrides[sectionId];
Object.entries(overrides).forEach(([property, value]) => {
element.style.setProperty(property, value);
});
}
}
const store = new TokenStore();
function render() {
const app = document.getElementById('app');
app.innerHTML = `
<div class="app">
<!-- Global Controls -->
<div class="global-controls">
<h3>Global Tokens</h3>
<div class="control-group">
<label>Primary Blue Shade</label>
<select id="global-blue-primary">
${Object.keys(store.tokens.colors.blue).map(shade =>
`<option value="${shade}" ${shade === '500' ? 'selected' : ''}>Blue ${shade}</option>`
).join('')}
</select>
</div>
<div class="control-group">
<label>Base Font</label>
<select id="global-font">
${Object.entries(store.tokens.fonts).map(([key, value]) =>
`<option value="${key}" ${key === 'sans' ? 'selected' : ''}>${key}</option>`
).join('')}
</select>
</div>
<div class="control-group">
<label>Base Size Scale</label>
<select id="global-size">
${Object.keys(store.tokens.sizes).map(size =>
`<option value="${size}" ${size === 'md' ? 'selected' : ''}>${size}</option>`
).join('')}
</select>
</div>
</div>
<!-- Section Panel -->
<div class="section-panel ${store.panelOpen ? '' : 'closed'}">
<div class="panel-header">
<h3>Section: ${store.selectedSection || 'None'}</h3>
<button class="close-btn" onclick="store.closePanel()">&times;</button>
</div>
<div class="panel-tabs">
<button class="tab-btn ${store.activeTab === 'colors' ? 'active' : ''}"
onclick="store.setActiveTab('colors')">Colors</button>
<button class="tab-btn ${store.activeTab === 'sizes' ? 'active' : ''}"
onclick="store.setActiveTab('sizes')">Sizes</button>
<button class="tab-btn ${store.activeTab === 'fonts' ? 'active' : ''}"
onclick="store.setActiveTab('fonts')">Fonts</button>
</div>
<div class="tab-content ${store.activeTab === 'colors' ? 'active' : ''}">
<div class="control-grid">
<div class="control-item">
<label>Background</label>
<select onchange="setSectionColor('background-color', this.value)">
<option value="">Default</option>
${Object.entries(store.tokens.colors).map(([color, shades]) =>
Object.keys(shades).map(shade =>
`<option value="var(--clr-${color}-${shade})">
<span class="color-swatch" style="background: var(--clr-${color}-${shade})"></span>
${color} ${shade}
</option>`
).join('')
).join('')}
</select>
</div>
<div class="control-item">
<label>Text Color</label>
<select onchange="setSectionColor('color', this.value)">
<option value="">Default</option>
${Object.entries(store.tokens.colors).map(([color, shades]) =>
Object.keys(shades).map(shade =>
`<option value="var(--clr-${color}-${shade})">${color} ${shade}</option>`
).join('')
).join('')}
</select>
</div>
<div class="control-item">
<label>Border Color</label>
<select onchange="setSectionColor('border-color', this.value)">
<option value="">Default</option>
${Object.entries(store.tokens.colors).map(([color, shades]) =>
Object.keys(shades).map(shade =>
`<option value="var(--clr-${color}-${shade})">${color} ${shade}</option>`
).join('')
).join('')}
</select>
</div>
</div>
</div>
<div class="tab-content ${store.activeTab === 'sizes' ? 'active' : ''}">
<div class="control-grid">
<div class="control-item">
<label>Padding</label>
<select onchange="setSectionSize('padding', this.value)">
<option value="">Default</option>
${Object.keys(store.tokens.sizes).map(size =>
`<option value="var(--size-${size})">${size}</option>`
).join('')}
</select>
</div>
<div class="control-item">
<label>Margin</label>
<select onchange="setSectionSize('margin-bottom', this.value)">
<option value="">Default</option>
${Object.keys(store.tokens.sizes).map(size =>
`<option value="var(--size-${size})">${size}</option>`
).join('')}
</select>
</div>
<div class="control-item">
<label>Border Radius</label>
<select onchange="setSectionSize('border-radius', this.value)">
<option value="">Default</option>
${Object.keys(store.tokens.sizes).map(size =>
`<option value="var(--size-${size})">${size}</option>`
).join('')}
</select>
</div>
<div class="control-item">
<label>Border Width</label>
<select onchange="setSectionSize('border-width', this.value)">
<option value="">Default</option>
${Object.keys(store.tokens.sizes).map(size =>
`<option value="var(--size-${size})">${size}</option>`
).join('')}
</select>
</div>
</div>
</div>
<div class="tab-content ${store.activeTab === 'fonts' ? 'active' : ''}">
<div class="control-grid">
<div class="control-item">
<label>Font Family</label>
<select onchange="setSectionFont('font-family', this.value)">
<option value="">Default</option>
${Object.entries(store.tokens.fonts).map(([key, value]) =>
`<option value="var(--font-${key})">${key}</option>`
).join('')}
</select>
</div>
<div class="control-item">
<label>Font Weight</label>
<select onchange="setSectionFont('font-weight', this.value)">
<option value="">Default</option>
${Object.entries(store.tokens.weights).map(([key, value]) =>
`<option value="var(--weight-${key})">${key}</option>`
).join('')}
</select>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="content-section ${store.selectedSection === 'section-1' ? 'selected' : ''}"
id="section-1" onclick="store.selectSection('section-1')">
<div class="section-label">Section 1</div>
<h2>Hero Section</h2>
<p>This is a hero section that uses the design tokens. Click to customize its appearance using the section panel above.</p>
<p>Background uses --clr-blue-200 by default, but can be overridden per section.</p>
</div>
<div class="content-section ${store.selectedSection === 'section-2' ? 'selected' : ''}"
id="section-2" onclick="store.selectSection('section-2')"
style="background-color: var(--clr-neutral-200);">
<div class="section-label">Section 2</div>
<h2>Content Section</h2>
<p>This section starts with a different background (--clr-neutral-200). You can override any property through the panel.</p>
<p>Changes to global tokens affect all sections unless specifically overridden.</p>
</div>
<div class="content-section ${store.selectedSection === 'section-3' ? 'selected' : ''}"
id="section-3" onclick="store.selectSection('section-3')"
style="background-color: var(--clr-red-100); font-family: var(--font-serif);">
<div class="section-label">Section 3</div>
<h2>Featured Section</h2>
<p>This section demonstrates different default styling - red background and serif font.</p>
<p>Try changing the global font tokens to see how it affects sections that haven't been overridden.</p>
</div>
</div>
</div>
`;
// Apply existing section overrides
Object.keys(store.sectionOverrides).forEach(sectionId => {
store.applySectionStyles(sectionId);
});
}
// Global helper functions
window.setSectionColor = function(property, value) {
if (store.selectedSection && value) {
store.setSectionOverride(store.selectedSection, property, value);
}
};
window.setSectionSize = function(property, value) {
if (store.selectedSection && value) {
store.setSectionOverride(store.selectedSection, property, value);
}
};
window.setSectionFont = function(property, value) {
if (store.selectedSection && value) {
store.setSectionOverride(store.selectedSection, property, value);
}
};
// Global controls event listeners
document.addEventListener('change', function(e) {
if (e.target.id === 'global-blue-primary') {
// This would update the primary blue used throughout
store.updateColorToken('blue', '500', store.tokens.colors.blue[e.target.value]);
} else if (e.target.id === 'global-font') {
// Update the default font
document.documentElement.style.setProperty('--font-sans', store.tokens.fonts[e.target.value]);
}
});
// Subscribe to store changes and re-render
store.subscribe(render);
// Initial render
render();
:root {
/* Default tokens - will be overridden by JavaScript */
--clr-blue-100: #e6f3ff;
--clr-blue-200: #bde0ff;
--clr-blue-300: #94d3ff;
--clr-blue-400: #5eb8ff;
--clr-blue-500: #2196f3;
--clr-blue-600: #1976d2;
--clr-blue-700: #1565c0;
--clr-blue-800: #0d47a1;
--clr-blue-900: #003d7a;
--clr-neutral-100: #f8f9fa;
--clr-neutral-200: #e9ecef;
--clr-neutral-300: #dee2e6;
--clr-neutral-400: #ced4da;
--clr-neutral-500: #adb5bd;
--clr-neutral-600: #6c757d;
--clr-neutral-700: #495057;
--clr-neutral-800: #343a40;
--clr-neutral-900: #212529;
--clr-red-100: #ffebee;
--clr-red-200: #ffcdd2;
--clr-red-300: #ef9a9a;
--clr-red-400: #e57373;
--clr-red-500: #f44336;
--clr-red-600: #e53935;
--clr-red-700: #d32f2f;
--clr-red-800: #c62828;
--clr-red-900: #b71c1c;
--size-xs: 0.25rem;
--size-sm: 0.5rem;
--size-md: 1rem;
--size-lg: 1.5rem;
--size-xl: 2rem;
--size-2xl: 3rem;
--size-3xl: 4rem;
--font-sans: system-ui, -apple-system, sans-serif;
--font-serif: Georgia, serif;
--font-mono: "Courier New", monospace;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background-color: var(--clr-neutral-100);
color: var(--clr-neutral-800);
line-height: 1.6;
}
.app {
min-height: 100vh;
position: relative;
}
/* Global Controls */
.global-controls {
position: fixed;
top: 1rem;
left: 1rem;
background: white;
border: 1px solid var(--clr-neutral-300);
border-radius: var(--size-sm);
padding: var(--size-md);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 250px;
}
.global-controls h3 {
margin-bottom: var(--size-sm);
font-size: 0.875rem;
font-weight: var(--weight-semibold);
color: var(--clr-neutral-700);
}
.global-controls .control-group {
margin-bottom: var(--size-sm);
}
.global-controls label {
display: block;
font-size: 0.75rem;
margin-bottom: 2px;
color: var(--clr-neutral-600);
}
.global-controls select {
width: 100%;
padding: 4px;
border: 1px solid var(--clr-neutral-300);
border-radius: 3px;
font-size: 0.75rem;
}
/* Section Panel */
.section-panel {
position: fixed;
top: 0;
left: 50%;
transform: translateX(-50%);
background: white;
border: 1px solid var(--clr-neutral-300);
border-top: none;
border-radius: 0 0 var(--size-sm) var(--size-sm);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 999;
transition: transform 0.3s ease;
min-width: 400px;
}
.section-panel.closed {
transform: translateX(-50%) translateY(-100%);
}
.panel-header {
padding: var(--size-sm) var(--size-md);
border-bottom: 1px solid var(--clr-neutral-200);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--clr-neutral-50);
}
.panel-header h3 {
font-size: 0.875rem;
font-weight: var(--weight-semibold);
}
.close-btn {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
}
.close-btn:hover {
background: var(--clr-neutral-200);
}
.panel-tabs {
display: flex;
border-bottom: 1px solid var(--clr-neutral-200);
}
.tab-btn {
background: none;
border: none;
padding: var(--size-sm) var(--size-md);
cursor: pointer;
font-size: 0.75rem;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab-btn:hover {
background: var(--clr-neutral-100);
}
.tab-btn.active {
border-bottom-color: var(--clr-blue-500);
background: var(--clr-blue-50);
}
.tab-content {
padding: var(--size-md);
display: none;
}
.tab-content.active {
display: block;
}
.control-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: var(--size-sm);
}
.control-item {
display: flex;
flex-direction: column;
}
.control-item label {
font-size: 0.75rem;
margin-bottom: 2px;
color: var(--clr-neutral-600);
}
.control-item select,
.control-item input {
padding: 4px 6px;
border: 1px solid var(--clr-neutral-300);
border-radius: 3px;
font-size: 0.75rem;
}
.color-swatch {
width: 20px;
height: 20px;
border-radius: 3px;
border: 1px solid var(--clr-neutral-300);
display: inline-block;
margin-right: 4px;
vertical-align: middle;
}
/* Main Content */
.main-content {
padding: 6rem 2rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.content-section {
background: var(--clr-blue-200);
color: var(--clr-neutral-800);
padding: var(--size-lg);
margin-bottom: var(--size-lg);
border-radius: var(--size-sm);
border: 2px solid transparent;
font-family: var(--font-sans);
font-weight: var(--weight-normal);
position: relative;
cursor: pointer;
transition: all 0.2s;
}
.content-section:hover {
border-color: var(--clr-blue-400);
}
.content-section.selected {
border-color: var(--clr-blue-500);
box-shadow: 0 0 0 2px var(--clr-blue-200);
}
.section-label {
position: absolute;
top: -8px;
left: var(--size-sm);
background: var(--clr-blue-500);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.65rem;
font-weight: var(--weight-medium);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment