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:
18
packages/showcase/desktop/electron-builder.yml
Normal file
18
packages/showcase/desktop/electron-builder.yml
Normal 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
|
||||
38
packages/showcase/desktop/package.json
Normal file
38
packages/showcase/desktop/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
30
packages/showcase/desktop/src/main/main.ts
Normal file
30
packages/showcase/desktop/src/main/main.ts
Normal 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();
|
||||
});
|
||||
1
packages/showcase/desktop/src/main/preload.ts
Normal file
1
packages/showcase/desktop/src/main/preload.ts
Normal file
@@ -0,0 +1 @@
|
||||
// Minimal preload — no IPC needed for the showcase
|
||||
77
packages/showcase/desktop/src/renderer/brand-switcher.ts
Normal file
77
packages/showcase/desktop/src/renderer/brand-switcher.ts
Normal 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;
|
||||
}
|
||||
15
packages/showcase/desktop/src/renderer/index.html
Normal file
15
packages/showcase/desktop/src/renderer/index.html
Normal 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>
|
||||
56
packages/showcase/desktop/src/renderer/main.ts
Normal file
56
packages/showcase/desktop/src/renderer/main.ts
Normal 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);
|
||||
126
packages/showcase/desktop/src/renderer/pages/bevels.ts
Normal file
126
packages/showcase/desktop/src/renderer/pages/bevels.ts
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
199
packages/showcase/desktop/src/renderer/pages/forms.ts
Normal file
199
packages/showcase/desktop/src/renderer/pages/forms.ts
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
97
packages/showcase/desktop/src/renderer/pages/glows.ts
Normal file
97
packages/showcase/desktop/src/renderer/pages/glows.ts
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
33
packages/showcase/desktop/src/renderer/pages/home.ts
Normal file
33
packages/showcase/desktop/src/renderer/pages/home.ts
Normal 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">'
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
141
packages/showcase/desktop/src/renderer/pages/tokens.ts
Normal file
141
packages/showcase/desktop/src/renderer/pages/tokens.ts
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
305
packages/showcase/desktop/src/renderer/style.css
Normal file
305
packages/showcase/desktop/src/renderer/style.css
Normal 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);
|
||||
}
|
||||
46
packages/showcase/desktop/src/renderer/utils/dom.ts
Normal file
46
packages/showcase/desktop/src/renderer/utils/dom.ts
Normal 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);
|
||||
}
|
||||
28
packages/showcase/desktop/src/renderer/utils/router.ts
Normal file
28
packages/showcase/desktop/src/renderer/utils/router.ts
Normal 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();
|
||||
}
|
||||
10
packages/showcase/desktop/tsconfig.json
Normal file
10
packages/showcase/desktop/tsconfig.json
Normal 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"]
|
||||
}
|
||||
14
packages/showcase/desktop/tsconfig.main.json
Normal file
14
packages/showcase/desktop/tsconfig.main.json
Normal 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"]
|
||||
}
|
||||
16
packages/showcase/desktop/vite.config.ts
Normal file
16
packages/showcase/desktop/vite.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user