Add desktop (Electron) and mobile (Expo) showcase apps

New workspace packages for demonstrating design system components:
- Desktop: Electron + Vite app with brand/theme switching
- Mobile: Expo React Native app with brand switching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-04 10:35:19 -08:00
parent 852a3af854
commit da84736b4b
100 changed files with 15874 additions and 326 deletions

View File

@@ -0,0 +1,18 @@
appId: com.dangerousthings.showcase-desktop
productName: DT Design System Showcase
directories:
output: release
buildResources: resources
npmRebuild: false
artifactName: "dt-showcase-desktop-${version}-${arch}.${ext}"
files:
- dist/**/*
- package.json
- "!node_modules"
mac:
target: [dmg, zip]
win:
target: [nsis, portable]
linux:
target: [AppImage, deb]
category: Development

View File

@@ -0,0 +1,38 @@
{
"name": "@dangerousthings/showcase-desktop",
"version": "0.0.0",
"private": true,
"description": "Desktop showcase for the Dangerous Things design system",
"author": {
"name": "Dangerous Things",
"email": "info@dangerousthings.com"
},
"homepage": "https://github.com/dangerous-tac0s/dt-design-system",
"main": "dist/main/main.js",
"scripts": {
"dev": "vite --config vite.config.ts",
"dev:electron": "npm run build:main && concurrently \"vite --config vite.config.ts\" \"wait-on http://localhost:5173 && electron .\"",
"build": "tsc -p tsconfig.main.json && vite build --config vite.config.ts",
"build:main": "tsc -p tsconfig.main.json",
"start": "electron .",
"package": "npm run build && electron-builder",
"package:dir": "npm run build && electron-builder --dir",
"package:win": "npm run build && electron-builder --win",
"package:mac": "npm run build && electron-builder --mac",
"package:linux": "npm run build && electron-builder --linux",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.main.json",
"clean": "rm -rf dist release"
},
"dependencies": {
"@dangerousthings/tokens": "*",
"@dangerousthings/web": "*"
},
"devDependencies": {
"concurrently": "^9.0.0",
"electron": "33.4.11",
"electron-builder": "^25.0.0",
"typescript": "^5.8.3",
"vite": "^6.0.0",
"wait-on": "^8.0.0"
}
}

View File

@@ -0,0 +1,30 @@
import { app, BrowserWindow } from 'electron';
import path from 'path';
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
backgroundColor: '#000000',
titleBarStyle: 'hiddenInset',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
win.loadFile(path.join(__dirname, '../renderer/index.html'));
}
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

View File

@@ -0,0 +1 @@
// Minimal preload — no IPC needed for the showcase

View File

@@ -0,0 +1,77 @@
import { themes } from '@dangerousthings/tokens';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
let currentBrand: ThemeBrand = 'dt';
let currentMode: ThemeMode = 'dark';
export function initBrandSwitcher(container: HTMLElement) {
// Brand buttons
for (const theme of themes) {
const btn = document.createElement('button');
btn.textContent = theme.name;
btn.className = 'brand-btn';
btn.dataset.brand = theme.id;
if (theme.id === currentBrand) btn.classList.add('active');
btn.addEventListener('click', () => switchBrand(theme.id));
container.appendChild(btn);
}
// Mode switcher
const modeContainer = document.createElement('div');
modeContainer.className = 'mode-switcher';
const modes: ThemeMode[] = ['dark', 'light', 'auto'];
for (const mode of modes) {
const btn = document.createElement('button');
btn.textContent = mode;
btn.className = 'mode-btn';
btn.dataset.mode = mode;
if (mode === currentMode) btn.classList.add('active');
btn.addEventListener('click', () => switchMode(mode));
modeContainer.appendChild(btn);
}
container.appendChild(modeContainer);
// Set initial state
applyTheme();
}
function switchBrand(id: ThemeBrand) {
currentBrand = id;
applyTheme();
updateBrandButtons();
}
function switchMode(mode: ThemeMode) {
currentMode = mode;
applyTheme();
updateModeButtons();
}
function applyTheme() {
document.documentElement.setAttribute('data-brand', currentBrand);
document.documentElement.setAttribute('data-theme', currentMode);
}
function updateBrandButtons() {
document.querySelectorAll('.brand-btn').forEach((btn) => {
const el = btn as HTMLButtonElement;
el.classList.toggle('active', el.dataset.brand === currentBrand);
});
}
function updateModeButtons() {
document.querySelectorAll('.mode-btn').forEach((btn) => {
const el = btn as HTMLButtonElement;
el.classList.toggle('active', el.dataset.mode === currentMode);
});
}
export function getCurrentBrand(): ThemeBrand {
return currentBrand;
}
export function getCurrentMode(): ThemeMode {
return currentMode;
}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" data-brand="dt" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DT Design System — Desktop Showcase</title>
</head>
<body>
<div id="app">
<nav id="sidebar"></nav>
<main id="content"></main>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,56 @@
import '@dangerousthings/web/index.css';
import './style.css';
import { initRouter } from './utils/router';
import { initBrandSwitcher } from './brand-switcher';
import { renderHomePage } from './pages/home';
import { renderBevelsPage } from './pages/bevels';
import { renderGlowsPage } from './pages/glows';
import { renderFormsPage } from './pages/forms';
import { renderTokensPage } from './pages/tokens';
const routes: Record<string, (container: HTMLElement) => void> = {
'': renderHomePage,
'bevels': renderBevelsPage,
'glows': renderGlowsPage,
'forms': renderFormsPage,
'tokens': renderTokensPage,
};
const sidebar = document.getElementById('sidebar')!;
const content = document.getElementById('content')!;
// Build sidebar navigation
const navTitle = document.createElement('div');
navTitle.className = 'nav-title';
navTitle.textContent = 'DT DESIGN SYSTEM';
sidebar.appendChild(navTitle);
const brandContainer = document.createElement('div');
brandContainer.className = 'brand-switcher-container';
sidebar.appendChild(brandContainer);
initBrandSwitcher(brandContainer);
const navLinks = document.createElement('div');
navLinks.className = 'nav-links';
const pages = [
{ hash: '', label: 'Home' },
{ hash: 'bevels', label: 'Bevels' },
{ hash: 'glows', label: 'Glows' },
{ hash: 'forms', label: 'Forms' },
{ hash: 'tokens', label: 'Tokens' },
];
for (const page of pages) {
const link = document.createElement('a');
link.href = `#/${page.hash}`;
link.textContent = page.label;
link.className = 'nav-link';
navLinks.appendChild(link);
}
sidebar.appendChild(navLinks);
// Initialize router
initRouter(routes, content);

View File

@@ -0,0 +1,126 @@
import { el, section, row, label, codeLabel } from '../utils/dom';
export function renderBevelsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Bevels'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Angular clip-path patterns from the DT design system. Active on the DT brand.'));
// Card Bevels
const card1 = el('div', { className: 'card', style: 'padding: var(--space-6);' });
card1.appendChild(el('div', { className: 'card-title' }, 'CARD TITLE'));
card1.appendChild(el('div', { className: 'card-body' }, 'Dual bottom bevels — bottom-right (bevel-md) and bottom-left (bevel-sm). Uses the dual-element technique with ::before pseudo-element.'));
const card2 = el('div', { className: 'card', style: 'padding: var(--space-6);' });
card2.appendChild(el('div', { className: 'card-body' }, 'Card without title — no header bar, just the beveled container.'));
container.appendChild(
section('Card Bevels', 'Dual bottom bevels using clip-path. Outer bg = border color, ::before = surface fill.',
card1,
codeLabel('.card or .dt-bevel-card'),
card2,
codeLabel('.card (no .card-title child)'),
),
);
// Button Bevels
container.appendChild(
section('Button Bevels', 'Top-right corner cut. Filled buttons use direct clip-path, outline uses dual-element.',
row(
el('button', { className: 'btn-primary' }, 'PRIMARY'),
el('button', { className: 'btn-secondary' }, 'SECONDARY'),
el('button', { className: 'btn-danger' }, 'DANGER'),
),
codeLabel('.btn-primary | .btn-secondary | .btn-danger'),
),
);
// Badge Bevels
container.appendChild(
section('Badge Bevels', 'Top-right bevel with dual-element technique. Color variants via CSS custom properties.',
row(
el('span', { className: 'badge' }, 'Default'),
el('span', { className: 'badge badge-success' }, 'Success'),
el('span', { className: 'badge badge-error' }, 'Error'),
el('span', { className: 'badge badge-warning' }, 'Warning'),
el('span', { className: 'badge badge-info' }, 'Info'),
),
codeLabel('.badge | .badge-success | .badge-error | .badge-warning | .badge-info'),
),
);
// Media Frame
const mediaFrame = el('div', {
className: 'dt-bevel-media',
style: 'background: var(--color-primary); width: 100%; aspect-ratio: 16/9; display: flex; align-items: center; justify-content: center;',
});
mediaFrame.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'MEDIA FRAME'));
container.appendChild(
section('Media Frame Bevels', 'Diagonal opposite corners (top-left + bottom-right).',
mediaFrame,
codeLabel('.dt-bevel-media'),
),
);
// Modal Bevel
const modalDemo = el('div', {
className: 'dt-bevel-modal',
style: 'background: var(--color-primary); padding: var(--space-8); text-align: center;',
});
const modalInner = el('div', { style: 'background: var(--color-surface); padding: var(--space-6);' });
modalInner.appendChild(el('div', { style: 'font-weight: 700; text-transform: uppercase;' }, 'Modal Content'));
modalInner.appendChild(el('div', { style: 'color: var(--color-text-muted); margin-top: var(--space-2);' }, 'Large dual bottom bevels'));
modalDemo.appendChild(modalInner);
container.appendChild(
section('Modal Bevels', 'Dual bottom bevels at bevel-lg scale.',
modalDemo,
codeLabel('.dt-bevel-modal'),
),
);
// Drawer Bevels
const drawerRight = el('div', {
className: 'dt-bevel-drawer-right',
style: 'background: var(--color-primary); padding: var(--space-6); width: 200px; height: 120px; display: flex; align-items: center; justify-content: center;',
});
drawerRight.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'RIGHT'));
const drawerLeft = el('div', {
className: 'dt-bevel-drawer-left',
style: 'background: var(--color-primary); padding: var(--space-6); width: 200px; height: 120px; display: flex; align-items: center; justify-content: center;',
});
drawerLeft.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700;' }, 'LEFT'));
container.appendChild(
section('Drawer Bevels', 'Exposed-edge bevels for sliding panels.',
row(drawerRight, drawerLeft),
codeLabel('.dt-bevel-drawer-right | .dt-bevel-drawer-left'),
),
);
// Small Bevel Utility
const smallBevel = el('div', {
className: 'dt-bevel-sm',
style: 'background: var(--color-primary); width: 60px; height: 60px; display: flex; align-items: center; justify-content: center;',
});
smallBevel.appendChild(el('span', { style: 'color: var(--color-bg); font-weight: 700; font-size: 0.75rem;' }, 'SM'));
container.appendChild(
section('Small Bevel Utility', 'For compact elements — arrows, stepper buttons.',
row(smallBevel),
codeLabel('.dt-bevel-sm'),
),
);
// Accent Top
const accentDemo = el('div', {
className: 'dt-accent-top',
style: 'padding: var(--space-4); background: var(--color-surface); border: 1px solid var(--color-border);',
});
accentDemo.appendChild(el('span', { style: 'font-weight: 600; text-transform: uppercase;' }, 'Thick Top Border Accent'));
container.appendChild(
section('Accent Top', 'Used on accordion headers and menu items.',
accentDemo,
codeLabel('.dt-accent-top'),
),
);
}

View File

@@ -0,0 +1,199 @@
import { el, section, row, label, codeLabel } from '../utils/dom';
export function renderFormsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Forms'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'DT-branded form components. Angular styling with focus effects.'));
// Text Inputs
const normalInput = el('input', { type: 'text', placeholder: 'Normal input — click to focus', style: 'max-width: 400px;' }) as HTMLInputElement;
const errorInput = el('input', { type: 'text', className: 'error', placeholder: 'Error state input', style: 'max-width: 400px;' }) as HTMLInputElement;
const textarea = el('textarea', { placeholder: 'Textarea element', style: 'max-width: 400px; min-height: 80px;' }) as HTMLTextAreaElement;
container.appendChild(
section('Text Input', 'Sharp corners + focus glow bar. 2px solid border, no border-radius.',
normalInput,
codeLabel('input[type="text"] — focus glow bar on [data-brand="dt"]'),
el('br'),
errorInput,
codeLabel('input.error — red focus bar on [data-brand="dt"]'),
el('br'),
textarea,
codeLabel('textarea — same styling'),
),
);
// Checkboxes
function checkbox(labelText: string, checked: boolean, disabled = false): HTMLElement {
const wrapper = el('label', { style: 'display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 4px 0;' });
const input = document.createElement('input');
input.type = 'checkbox';
if (checked) input.checked = true;
if (disabled) input.disabled = true;
wrapper.appendChild(input);
wrapper.appendChild(el('span', {}, labelText));
return wrapper;
}
container.appendChild(
section('Checkbox', 'Beveled hexagon shape via clip-path. 30% corner cuts.',
checkbox('Unchecked checkbox', false),
checkbox('Checked checkbox', true),
checkbox('Disabled checked', true, true),
checkbox('Disabled unchecked', false, true),
codeLabel('input[type="checkbox"] — hex bevel on [data-brand="dt"]'),
),
);
// Switch / Toggle
function switchEl(labelText: string, checked: boolean, disabled = false): HTMLElement {
const wrapper = el('div', { style: 'display: flex; align-items: center; gap: 12px; margin-bottom: 8px;' });
const sw = el('label', { className: 'dt-switch' });
const input = document.createElement('input');
input.type = 'checkbox';
if (checked) input.checked = true;
if (disabled) input.disabled = true;
sw.appendChild(input);
sw.appendChild(el('div', { className: 'dt-switch-track' }));
sw.appendChild(el('div', { className: 'dt-switch-thumb' }));
wrapper.appendChild(sw);
wrapper.appendChild(el('span', {}, labelText));
return wrapper;
}
container.appendChild(
section('Switch / Toggle', 'Angular track + sliding square thumb. 48x26 track, 20x20 thumb.',
switchEl('Off', false),
switchEl('On', true),
switchEl('Disabled (on)', true, true),
switchEl('Disabled (off)', false, true),
codeLabel('.dt-switch > input + .dt-switch-track + .dt-switch-thumb'),
),
);
// Radio Buttons
function radioGroup(name: string, options: { value: string; label: string; checked?: boolean; disabled?: boolean }[]): HTMLElement {
const group = el('div', { style: 'display: flex; flex-direction: column; gap: 4px;' });
for (const opt of options) {
const wrapper = el('label', { className: `dt-radio-option${opt.checked ? ' selected' : ''}`, style: 'cursor: pointer;' });
const input = document.createElement('input');
input.type = 'radio';
input.name = name;
input.value = opt.value;
if (opt.checked) input.checked = true;
if (opt.disabled) input.disabled = true;
input.addEventListener('change', () => {
group.querySelectorAll('.dt-radio-option').forEach((o) => o.classList.remove('selected'));
if (input.checked) wrapper.classList.add('selected');
});
wrapper.appendChild(input);
wrapper.appendChild(el('span', {}, opt.label));
group.appendChild(wrapper);
}
return group;
}
container.appendChild(
section('Radio Button', 'Hexagonal indicator via clip-path. Flat-top hexagon shape.',
radioGroup('demo-radio', [
{ value: 'nfc', label: 'NFC (Near Field Communication)', checked: true },
{ value: 'rfid', label: 'RFID (Radio Frequency ID)' },
{ value: 'ble', label: 'BLE (Bluetooth Low Energy)' },
]),
codeLabel('input[type="radio"] — hex indicator on [data-brand="dt"]'),
),
);
// Progress Bar
function progressBar(value: number, showLabel = false): HTMLElement {
const wrapper = el('div');
const bar = el('div', { className: 'dt-progress' });
const fill = el('div', { className: 'dt-progress-fill', style: `width: ${value * 100}%;` });
bar.appendChild(fill);
wrapper.appendChild(bar);
if (showLabel) {
wrapper.appendChild(el('div', { className: 'dt-progress-label' }, `${Math.round(value * 100)}%`));
}
return wrapper;
}
container.appendChild(
section('Progress Bar', 'Angular, no border-radius. 4px default height.',
label('25%'),
progressBar(0.25),
label('50% WITH LABEL'),
progressBar(0.5, true),
label('75%'),
progressBar(0.75),
label('100%'),
progressBar(1.0, true),
codeLabel('.dt-progress > .dt-progress-fill'),
),
);
// Accordion
function accordion(items: { title: string; content: string }[]): HTMLElement {
const acc = el('div', { className: 'dt-accordion' });
for (const item of items) {
const header = el('button', { className: 'dt-accordion-header', 'aria-expanded': 'false' });
header.appendChild(document.createTextNode(item.title));
header.appendChild(el('span', { className: 'dt-accordion-chevron' }));
const content = el('div', { className: 'dt-accordion-content', 'data-open': 'false' });
content.appendChild(el('div', { style: 'padding: var(--space-4);' }, item.content));
header.addEventListener('click', () => {
const expanded = header.getAttribute('aria-expanded') === 'true';
header.setAttribute('aria-expanded', String(!expanded));
content.setAttribute('data-open', String(!expanded));
});
acc.appendChild(header);
acc.appendChild(content);
}
return acc;
}
container.appendChild(
section('Accordion', 'Click headers to expand. 5px top border, chevron rotation.',
accordion([
{ title: 'Size', content: 'Small, Medium, Large options available.' },
{ title: 'Chip Type', content: 'NTAG, DESFire, MIFARE Classic.' },
{ title: 'Frequency', content: '13.56 MHz (HF), 125 kHz (LF).' },
]),
codeLabel('.dt-accordion > .dt-accordion-header + .dt-accordion-content'),
),
);
// Quantity Stepper
function stepper(initialValue: number, min: number, max: number): HTMLElement {
let value = initialValue;
const wrapper = el('div', { className: 'dt-stepper' });
const minusBtn = el('button', { className: 'dt-stepper-btn' }, '\u2212') as HTMLButtonElement;
const valueEl = el('span', { className: 'dt-stepper-value' }, String(value));
const plusBtn = el('button', { className: 'dt-stepper-btn' }, '+') as HTMLButtonElement;
function update() {
valueEl.textContent = String(value);
minusBtn.disabled = value <= min;
plusBtn.disabled = value >= max;
}
minusBtn.addEventListener('click', () => { value = Math.max(min, value - 1); update(); });
plusBtn.addEventListener('click', () => { value = Math.min(max, value + 1); update(); });
wrapper.appendChild(minusBtn);
wrapper.appendChild(valueEl);
wrapper.appendChild(plusBtn);
update();
return wrapper;
}
container.appendChild(
section('Quantity Stepper', 'Beveled +/- buttons with center display.',
stepper(1, 0, 10),
codeLabel('.dt-stepper > .dt-stepper-btn + .dt-stepper-value + .dt-stepper-btn'),
),
);
}

View File

@@ -0,0 +1,97 @@
import { el, section, row, codeLabel } from '../utils/dom';
export function renderGlowsPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Glows'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Neon drop-shadow and text-shadow effects. Active on the DT brand.'));
// Button Glows
container.appendChild(
section('Button Glows', 'filter: drop-shadow() follows clip-path shape. Hover for enhanced glow.',
row(
el('button', { className: 'btn-primary' }, 'PRIMARY GLOW'),
el('button', { className: 'btn-danger' }, 'DANGER GLOW'),
),
codeLabel('.btn-primary / .btn-danger — automatic on [data-brand="dt"]'),
el('br'),
row(
el('button', { className: 'btn-secondary' }, 'SECONDARY (HOVER)'),
),
codeLabel('.btn-secondary — glow on hover'),
),
);
// Link Glow
const link = el('a', { href: '#', style: 'color: var(--color-primary); font-weight: 600; font-size: 1.125rem;' }, 'Hover this link for text glow');
container.appendChild(
section('Link Glow', 'text-shadow on hover for anchor elements.',
row(link),
codeLabel('a:hover — automatic on [data-brand="dt"]'),
),
);
// Terminal Inset
const terminal = el('div', { className: 'terminal dt-accent-top' });
terminal.appendChild(el('code', {}, '$ dt-web-theme --install\n> Installing design tokens...\n> Applying cyberpunk aesthetic...\n> Done. Welcome to the future.'));
container.appendChild(
section('Terminal Inset Glow', 'Inset + outer box-shadow. No clip-path so box-shadow works directly.',
terminal,
codeLabel('.terminal — automatic on [data-brand="dt"]'),
),
);
// Card Hover Glow
const card = el('div', { className: 'card', style: 'padding: var(--space-6); cursor: pointer;' });
card.appendChild(el('div', { className: 'card-title' }, 'HOVER ME'));
card.appendChild(el('div', { className: 'card-body' }, 'Cards get a drop-shadow glow on hover.'));
container.appendChild(
section('Card Hover Glow', 'filter: drop-shadow() respects the beveled clip-path.',
card,
codeLabel('.card:hover — automatic on [data-brand="dt"]'),
),
);
// Input Focus Glow
const input = el('input', {
type: 'text',
className: 'input',
placeholder: 'Click to focus — see the glow bar',
style: 'max-width: 400px;',
}) as HTMLInputElement;
container.appendChild(
section('Input Focus Glow', 'box-shadow: 0 4px 0 1px — bright bar beneath the input.',
input,
codeLabel('.input:focus — automatic on [data-brand="dt"]'),
),
);
// Generic Glow Utilities
const glowBox = el('div', {
className: 'dt-glow',
style: 'background: var(--color-primary); padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; color: var(--color-bg);',
}, '.dt-glow');
const glowStrong = el('div', {
className: 'dt-glow-strong',
style: 'background: var(--color-primary); padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; color: var(--color-bg);',
}, '.dt-glow-strong');
const glowInset = el('div', {
className: 'dt-glow-inset',
style: 'padding: var(--space-4) var(--space-6); display: inline-block; font-weight: 700; border: 1px solid var(--color-border);',
}, '.dt-glow-inset');
const textGlow = el('div', {
className: 'dt-text-glow',
style: 'font-size: 1.25rem; font-weight: 700; color: var(--color-primary); display: inline-block;',
}, '.dt-text-glow');
container.appendChild(
section('Glow Utilities', 'Generic utility classes for applying glow effects to any element.',
row(glowBox, glowStrong),
codeLabel('.dt-glow | .dt-glow-strong'),
row(glowInset, textGlow),
codeLabel('.dt-glow-inset | .dt-text-glow'),
),
);
}

View File

@@ -0,0 +1,33 @@
import { el, section } from '../utils/dom';
export function renderHomePage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'DT Design System'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Web CSS component showcase — switch brands and modes in the sidebar.'));
const categories = [
{ hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers' },
{ hash: 'glows', title: 'Glows', desc: 'Neon drop-shadow and text-shadow effects for the DT brand' },
{ hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers' },
{ hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand' },
];
for (const cat of categories) {
const link = el('a', { href: `#/${cat.hash}`, className: 'home-card-link' });
const card = el('div', { className: 'card' });
card.appendChild(el('div', { className: 'card-title' }, cat.title.toUpperCase()));
card.appendChild(el('div', { className: 'card-body' }, cat.desc));
link.appendChild(card);
container.appendChild(link);
}
// Quick overview terminal
container.appendChild(
section('Quick Start', 'Include the CSS and set data attributes on your HTML element.',
el('div', { className: 'terminal dt-accent-top' },
el('code', {},
'<link rel="stylesheet" href="@dangerousthings/web">\n<html data-brand="dt" data-theme="dark">'
),
),
),
);
}

View File

@@ -0,0 +1,141 @@
import { brands } from '@dangerousthings/tokens';
import type { ThemeBrand } from '@dangerousthings/tokens';
import { el, section, codeLabel } from '../utils/dom';
import { getCurrentBrand } from '../brand-switcher';
export function renderTokensPage(container: HTMLElement) {
container.appendChild(el('h1', { className: 'page-title' }, 'Tokens'));
container.appendChild(el('p', { className: 'page-subtitle' }, 'Design token values for the active brand. Switch brands in the sidebar.'));
const brandId = getCurrentBrand();
const brand = brands[brandId];
if (!brand) return;
// Color Palette — Dark Mode
const darkColors = brand.dark;
const colorEntries: [string, string][] = Object.entries(darkColors);
const colorsGrid = el('div', { style: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3);' });
for (const [name, hex] of colorEntries) {
const swatch = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
const colorBox = el('div', { style: `width: 32px; height: 32px; background: ${hex}; border: 1px solid rgba(255,255,255,0.15); flex-shrink: 0;` });
const info = el('div');
info.appendChild(el('div', { style: 'font-weight: 600; font-size: 0.8125rem;' }, name));
info.appendChild(el('div', { style: 'font-family: var(--font-mono); font-size: 0.6875rem; opacity: 0.5;' }, hex));
swatch.appendChild(colorBox);
swatch.appendChild(info);
colorsGrid.appendChild(swatch);
}
container.appendChild(
section(`Color Palette — ${brand.name} (Dark Mode)`, brand.description,
colorsGrid,
),
);
// Light Mode
const lightColors = brand.light;
const lightEntries: [string, string][] = Object.entries(lightColors);
const lightGrid = el('div', { style: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3);' });
for (const [name, hex] of lightEntries) {
const swatch = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
const colorBox = el('div', { style: `width: 32px; height: 32px; background: ${hex}; border: 1px solid rgba(255,255,255,0.15); flex-shrink: 0;` });
const info = el('div');
info.appendChild(el('div', { style: 'font-weight: 600; font-size: 0.8125rem;' }, name));
info.appendChild(el('div', { style: 'font-family: var(--font-mono); font-size: 0.6875rem; opacity: 0.5;' }, hex));
swatch.appendChild(colorBox);
swatch.appendChild(info);
lightGrid.appendChild(swatch);
}
container.appendChild(
section(`Color Palette — ${brand.name} (Light Mode)`, 'Light mode color tokens.',
lightGrid,
),
);
// Typography
const typo = el('div', { className: 'terminal', style: 'margin-bottom: var(--space-4);' });
typo.appendChild(el('code', {},
`heading: ${brand.typography.heading}\nbody: ${brand.typography.body}\nmono: ${brand.typography.mono}`
));
container.appendChild(
section('Typography', 'Font family tokens for headings, body text, and monospace.',
typo,
),
);
// Typography Scale Demo
const scaleItems = [
{ tag: 'h1', label: 'Heading 1', style: 'font-family: var(--font-heading); font-size: 2rem; font-weight: 900;' },
{ tag: 'h2', label: 'Heading 2', style: 'font-family: var(--font-heading); font-size: 1.5rem; font-weight: 700;' },
{ tag: 'h3', label: 'Heading 3', style: 'font-family: var(--font-heading); font-size: 1.25rem; font-weight: 600;' },
{ tag: 'p', label: 'Body text — the quick brown fox jumps over the lazy dog', style: 'font-family: var(--font-body); font-size: 1rem;' },
{ tag: 'code', label: 'const monospace = "code";', style: 'font-family: var(--font-mono); font-size: 0.875rem;' },
];
const scaleDemo = el('div', { style: 'display: flex; flex-direction: column; gap: var(--space-3);' });
for (const item of scaleItems) {
scaleDemo.appendChild(el(item.tag, { style: item.style }, item.label));
}
container.appendChild(
section('Typography Scale', 'Live rendering with the current brand\'s font families.',
scaleDemo,
),
);
// Shape Tokens
const shape = el('div', { className: 'terminal', style: 'margin-bottom: var(--space-4);' });
shape.appendChild(el('code', {},
`bevelSm: ${brand.shape.bevelSm}\nbevelMd: ${brand.shape.bevelMd}\nbevelLg: ${brand.shape.bevelLg}\nradiusSm: ${brand.shape.radiusSm}\nradius: ${brand.shape.radius}\nradiusLg: ${brand.shape.radiusLg}`
));
container.appendChild(
section('Shape Tokens', 'Bevel and radius values determine the visual language — angular (DT) vs rounded (Classic).',
shape,
),
);
// Spacing Scale
const spacings = [
{ name: '--space-1', value: '0.25rem (4px)' },
{ name: '--space-2', value: '0.5rem (8px)' },
{ name: '--space-3', value: '0.75rem (12px)' },
{ name: '--space-4', value: '1rem (16px)' },
{ name: '--space-6', value: '1.5rem (24px)' },
{ name: '--space-8', value: '2rem (32px)' },
];
const spacingDemo = el('div', { style: 'display: flex; flex-direction: column; gap: var(--space-2);' });
for (const sp of spacings) {
const row = el('div', { style: 'display: flex; align-items: center; gap: var(--space-3);' });
row.appendChild(el('div', { style: `width: ${sp.name.replace('--', 'var(--') + ')'}; width: calc(${sp.value.split(' ')[0]} * 6); height: 12px; background: var(--color-primary); opacity: 0.6;` }));
row.appendChild(el('span', { style: 'font-family: var(--font-mono); font-size: 0.75rem; opacity: 0.6;' }, `${sp.name}: ${sp.value}`));
spacingDemo.appendChild(row);
}
container.appendChild(
section('Spacing Scale', 'Shared spacing tokens across all brands.',
spacingDemo,
),
);
// CSS Custom Properties reference
const cssRef = el('div', { className: 'terminal' });
cssRef.appendChild(el('code', {},
`/* Generated CSS custom properties */\n--color-bg: ${darkColors.bg};\n--color-primary: ${darkColors.primary};\n--color-secondary: ${darkColors.secondary};\n--color-error: ${darkColors.error};\n--color-success: ${darkColors.success};\n--bevel-sm: ${brand.shape.bevelSm};\n--bevel-md: ${brand.shape.bevelMd};\n--font-heading: ${brand.typography.heading};`
));
container.appendChild(
section('CSS Custom Properties', 'These are generated from TypeScript tokens and applied per-brand via data-brand attribute.',
cssRef,
codeLabel('See packages/tokens/src/scripts/generate-css.ts'),
),
);
}

View File

@@ -0,0 +1,305 @@
/* Showcase App Layout */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--color-bg);
color: var(--color-text-primary);
font-family: var(--font-body);
min-height: 100vh;
}
#app {
display: flex;
min-height: 100vh;
}
/* ---- Sidebar ---- */
#sidebar {
width: 220px;
min-height: 100vh;
background: var(--color-surface);
border-right: 2px solid var(--color-border);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
position: fixed;
top: 0;
left: 0;
overflow-y: auto;
}
.nav-title {
font-family: var(--font-heading);
font-weight: 900;
font-size: 0.875rem;
letter-spacing: 0.1em;
color: var(--color-primary);
text-transform: uppercase;
padding-bottom: var(--space-4);
border-bottom: 2px solid var(--color-primary);
}
.brand-switcher-container {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.brand-btn {
display: block;
width: 100%;
padding: var(--space-2) var(--space-3);
border: 2px solid var(--color-primary);
background: transparent;
color: var(--color-primary);
font-weight: 700;
font-size: 0.75rem;
letter-spacing: 0.05em;
cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast);
}
.brand-btn:hover {
background: rgba(var(--color-primary-rgb), 0.1);
}
.brand-btn.active {
background: var(--color-primary);
color: var(--color-bg);
}
.mode-switcher {
display: flex;
gap: var(--space-1);
margin-top: var(--space-2);
}
.mode-btn {
flex: 1;
padding: var(--space-1) var(--space-2);
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text-secondary);
font-size: 0.625rem;
font-weight: 600;
cursor: pointer;
text-transform: uppercase;
transition: all var(--transition-fast);
}
.mode-btn.active {
background: var(--color-primary);
color: var(--color-bg);
border-color: var(--color-primary);
}
.nav-links {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.nav-link {
display: block;
padding: var(--space-2) var(--space-3);
color: var(--color-text-secondary);
text-decoration: none;
font-weight: 600;
font-size: 0.875rem;
letter-spacing: 0.02em;
text-transform: uppercase;
border-left: 3px solid transparent;
transition: all var(--transition-fast);
}
.nav-link:hover {
color: var(--color-primary);
border-left-color: var(--color-primary);
background: rgba(var(--color-primary-rgb), 0.05);
}
.nav-link.active {
color: var(--color-primary);
border-left-color: var(--color-primary);
}
/* ---- Main Content ---- */
#content {
flex: 1;
margin-left: 220px;
padding: var(--space-8);
max-width: 900px;
}
/* ---- Demo Sections ---- */
.demo-section {
margin-bottom: var(--space-8);
}
.demo-section h2 {
font-family: var(--font-heading);
font-weight: 700;
font-size: 1.125rem;
color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
padding-bottom: var(--space-2);
border-bottom: 2px solid var(--color-primary);
}
.demo-description {
color: var(--color-text-muted);
font-size: 0.875rem;
margin-bottom: var(--space-4);
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
margin-bottom: var(--space-4);
}
.demo-label {
color: var(--color-text-muted);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
margin-top: var(--space-4);
}
.code-label {
color: rgba(var(--color-text-primary-rgb, 255, 255, 255), 0.35);
font-size: 0.625rem;
font-family: var(--font-mono);
margin-top: var(--space-1);
}
/* ---- Shared Component Base Styles ---- */
/* Buttons */
.btn-primary,
.btn-secondary,
.btn-danger {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-3) var(--space-6);
font-weight: 700;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
transition: all var(--transition-fast);
border: none;
}
.btn-primary {
background: var(--color-primary);
color: var(--color-bg);
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.btn-danger {
background: var(--color-error);
color: var(--color-bg);
}
.btn-danger:hover {
opacity: 0.9;
}
/* Cards */
.card {
padding: var(--space-6);
margin-bottom: var(--space-4);
}
.card-title {
font-weight: 900;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card-body {
color: var(--color-text-secondary);
font-size: 0.875rem;
line-height: 1.5;
}
/* Badges */
.badge {
display: inline-block;
padding: var(--space-2) var(--space-3);
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Inputs */
.input,
input[type="text"],
input[type="email"],
input[type="password"] {
padding: var(--space-3) var(--space-4);
font-size: 0.875rem;
width: 100%;
max-width: 400px;
border: 2px solid var(--color-border);
background: var(--color-bg);
color: var(--color-text-primary);
}
.input:focus,
input:focus {
outline: none;
}
/* Terminal */
.terminal {
background: var(--color-surface);
border: 1px solid var(--color-border);
padding: var(--space-6);
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--color-accent, var(--color-primary));
line-height: 1.6;
}
/* Page title */
.page-title {
font-family: var(--font-heading);
font-weight: 900;
font-size: 1.75rem;
color: var(--color-primary);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: var(--space-2);
}
.page-subtitle {
color: var(--color-text-muted);
font-size: 0.875rem;
margin-bottom: var(--space-8);
}

View File

@@ -0,0 +1,46 @@
export function el(
tag: string,
attrs?: Record<string, string>,
...children: (string | HTMLElement)[]
): HTMLElement {
const element = document.createElement(tag);
if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (key === 'className') {
element.className = value;
} else {
element.setAttribute(key, value);
}
}
}
for (const child of children) {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
}
return element;
}
export function section(title: string, description: string, ...children: HTMLElement[]): HTMLElement {
const container = el('div', { className: 'demo-section' });
container.appendChild(el('h2', {}, title));
container.appendChild(el('p', { className: 'demo-description' }, description));
for (const child of children) {
container.appendChild(child);
}
return container;
}
export function row(...children: HTMLElement[]): HTMLElement {
return el('div', { className: 'demo-row' }, ...children);
}
export function label(text: string): HTMLElement {
return el('div', { className: 'demo-label' }, text);
}
export function codeLabel(text: string): HTMLElement {
return el('div', { className: 'code-label' }, text);
}

View File

@@ -0,0 +1,28 @@
export function initRouter(
routes: Record<string, (container: HTMLElement) => void>,
container: HTMLElement,
) {
function navigate() {
const hash = window.location.hash.replace('#/', '').replace('#', '');
const render = routes[hash] || routes[''];
// Update active nav link
document.querySelectorAll('.nav-link').forEach((link) => {
const href = (link as HTMLAnchorElement).getAttribute('href') || '';
const linkHash = href.replace('#/', '').replace('#', '');
link.classList.toggle('active', linkHash === hash);
});
// Clear content safely
while (container.firstChild) {
container.removeChild(container.firstChild);
}
if (render) {
render(container);
}
}
window.addEventListener('hashchange', navigate);
navigate();
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist/renderer",
"rootDir": "src/renderer",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"noEmit": true
},
"include": ["src/renderer"]
}

View File

@@ -0,0 +1,14 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist/main",
"rootDir": "src/main",
"module": "CommonJS",
"moduleResolution": "node",
"lib": ["ES2022"],
"declaration": false,
"declarationMap": false,
"sourceMap": false
},
"include": ["src/main"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
root: 'src/renderer',
base: './',
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,
},
resolve: {
alias: {
'@dangerousthings/web': path.resolve(__dirname, '../../web/dist'),
},
},
});