Add storefront component migration: cards, badges, progress bars, animations, and React wrapper package

Major feature migration from dt-shopify-storefront into the design system:

- Card system: per-card color modes (normal/emphasis/warning/success/other),
  progress bars, full-bleed card body (card-body-flush), image bevel clip-paths,
  and visible structural borders at bevel corners
- Badge system: dt-card-badge with position-aware offsets for cards vs media frames
- Media frames: dt-bevel-media with inner surface fill, image clipping, and
  "MISSING MEDIA" placeholder for empty containers
- Interactive bevel buttons: dt-btn with hover/selected states and pulse animation
- Menu items: dt-menu-item with active states and level indentation
- Animations: scale-in, fade-in, slide-up, pulse, ping, spin keyframes
- Feature legend: CSS grid for product feature icons with state colors
- Scrollbar styling for DT brand
- New @dangerousthings/react package wrapping web CSS components
- New @dangerousthings/tailwind-preset package
- New @dangerousthings/hex-background package
- Desktop showcase rewritten in React with Tailwind CSS
- Mobile showcase updated with new screens (animations, filters, advanced cards)
- Tokens: added mode color tokens, RGB variants, and selected-state tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-05 13:26:03 -08:00
parent 47e8085581
commit e74c285b70
109 changed files with 7196 additions and 1026 deletions

View File

@@ -24,13 +24,24 @@
"clean": "rm -rf dist release"
},
"dependencies": {
"@dangerousthings/react": "*",
"@dangerousthings/tokens": "*",
"@dangerousthings/web": "*"
"@dangerousthings/web": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-icons": "^5.6.0"
},
"devDependencies": {
"@dangerousthings/tailwind-preset": "*",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.0",
"concurrently": "^9.0.0",
"electron": "33.4.11",
"electron-builder": "^25.0.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.8.3",
"vite": "^6.0.0",
"wait-on": "^8.0.0"

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,100 @@
import { useState, useEffect } from 'react';
import { themes } from '@dangerousthings/tokens';
import type { ThemeBrand, ThemeMode } from '@dangerousthings/tokens';
import { HomePage } from './pages/HomePage';
import { BevelsPage } from './pages/BevelsPage';
import { GlowsPage } from './pages/GlowsPage';
import { FormsPage } from './pages/FormsPage';
import { AnimationsPage } from './pages/AnimationsPage';
import { CardsAdvancedPage } from './pages/CardsAdvancedPage';
import { TokensPage } from './pages/TokensPage';
const navPages = [
{ hash: '', label: 'Home' },
{ hash: 'bevels', label: 'Bevels' },
{ hash: 'glows', label: 'Glows' },
{ hash: 'forms', label: 'Forms' },
{ hash: 'animations', label: 'Animations' },
{ hash: 'cards-advanced', label: 'Advanced Cards' },
{ hash: 'tokens', label: 'Tokens' },
];
export function App() {
const [brand, setBrand] = useState<ThemeBrand>('dt');
const [theme, setTheme] = useState<ThemeMode>('dark');
const [currentHash, setCurrentHash] = useState('');
// Sync brand/theme to <html> so CSS custom properties cascade everywhere
useEffect(() => {
document.documentElement.setAttribute('data-brand', brand);
document.documentElement.setAttribute('data-theme', theme);
}, [brand, theme]);
// Hash-based routing
useEffect(() => {
function onHashChange() {
setCurrentHash(window.location.hash.replace('#/', '').replace('#', ''));
}
window.addEventListener('hashchange', onHashChange);
onHashChange();
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
function renderPage() {
switch (currentHash) {
case 'bevels': return <BevelsPage />;
case 'glows': return <GlowsPage />;
case 'forms': return <FormsPage />;
case 'animations': return <AnimationsPage />;
case 'cards-advanced': return <CardsAdvancedPage />;
case 'tokens': return <TokensPage brand={brand} />;
default: return <HomePage />;
}
}
return (
<>
<nav id="sidebar">
<div className="nav-title">DT DESIGN SYSTEM</div>
<div className="brand-switcher-container">
{themes.map(t => (
<button
key={t.id}
className={`brand-btn${t.id === brand ? ' active' : ''}`}
onClick={() => setBrand(t.id)}
type="button"
>
{t.name}
</button>
))}
<div className="mode-switcher">
{(['dark', 'light', 'auto'] as ThemeMode[]).map(m => (
<button
key={m}
className={`mode-btn${m === theme ? ' active' : ''}`}
onClick={() => setTheme(m)}
type="button"
>
{m}
</button>
))}
</div>
</div>
<div className="nav-links">
{navPages.map(p => (
<a
key={p.hash}
href={`#/${p.hash}`}
className={`nav-link${p.hash === currentHash ? ' active' : ''}`}
>
{p.label}
</a>
))}
</div>
</nav>
<main id="content">
{renderPage()}
</main>
</>
);
}

View File

@@ -1,77 +0,0 @@
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,23 @@
import type { ReactNode, CSSProperties } from 'react';
export function Section({ title, description, children }: { title: string; description: string; children: ReactNode }) {
return (
<div className="demo-section">
<h2>{title}</h2>
<p className="demo-description">{description}</p>
{children}
</div>
);
}
export function Row({ children, style }: { children: ReactNode; style?: CSSProperties }) {
return <div className="demo-row" style={style}>{children}</div>;
}
export function CodeLabel({ text }: { text: string }) {
return <div className="code-label">{text}</div>;
}
export function DemoLabel({ text }: { text: string }) {
return <div className="demo-label">{text}</div>;
}

View File

@@ -6,10 +6,7 @@
<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>
<div id="app"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@@ -1,56 +0,0 @@
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,6 @@
import '@dangerousthings/web/index.css';
import './style.css';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('app')!).render(<App />);

View File

@@ -0,0 +1,150 @@
import { useState } from 'react';
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
function AnimBox({ className, label }: { className: string; label: string }) {
return (
<div style={{ textAlign: 'center' }}>
<div
className={className}
style={{
background: 'var(--color-primary)',
width: 80,
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
color: 'var(--color-bg)',
fontSize: '0.7rem',
}}
>
{label}
</div>
</div>
);
}
export function AnimationsPage() {
const [entranceKey, setEntranceKey] = useState(0);
const [staggerKey, setStaggerKey] = useState(0);
const [accordionExpanded, setAccordionExpanded] = useState(false);
return (
<>
<h1 className="page-title">Animations</h1>
<p className="page-subtitle">Entrance animations, interactive effects, stagger containers, and transition utilities.</p>
<Section title="Entrance Animations" description="One-shot animations for elements entering the viewport.">
<Row key={entranceKey}>
<AnimBox className="dt-animate-scale-in" label="SCALE IN" />
<AnimBox className="dt-animate-fade-in" label="FADE IN" />
<AnimBox className="dt-animate-slide-up" label="SLIDE UP" />
</Row>
<button className="btn-secondary" style={{ marginTop: 'var(--space-4)' }} onClick={() => setEntranceKey(k => k + 1)} type="button">
REPLAY
</button>
<CodeLabel text=".dt-animate-scale-in | .dt-animate-fade-in | .dt-animate-slide-up" />
</Section>
<Section title="Interactive Animations" description="Looping animations for active/loading states.">
<Row>
<AnimBox className="dt-animate-pulse" label="PULSE" />
<AnimBox className="dt-animate-ping" label="PING" />
<AnimBox className="dt-animate-spin" label="SPIN" />
</Row>
<CodeLabel text=".dt-animate-pulse | .dt-animate-ping | .dt-animate-spin" />
</Section>
<Section title="Stagger Container" description="Automatic staggered scale-in animation for child elements. Customizable via --dt-stagger-duration and --dt-stagger-interval.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-dt-3"
>
{Array.from({ length: 12 }, (_, i) => (
<DTCard key={i} title={`CARD ${i + 1}`} style={{ textAlign: 'center' }}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>Item #{i + 1}</div>
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY STAGGER
</button>
<CodeLabel text="<DTStaggerContainer> — nth-child delays up to 24 children" />
</Section>
<Section title="Transition Utilities" description="Accordion expand, chevron rotation, and progress bar transitions.">
<div style={{ maxWidth: 400 }}>
<button
className="dt-accent-top"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: 'var(--space-3) var(--space-4)',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
cursor: 'pointer',
color: 'var(--color-text)',
fontWeight: 600,
textTransform: 'uppercase',
}}
aria-expanded={accordionExpanded}
onClick={() => setAccordionExpanded(!accordionExpanded)}
type="button"
>
Click to expand
<span style={{
display: 'inline-block',
transition: 'transform 250ms ease-in-out',
transform: accordionExpanded ? 'rotate(180deg)' : undefined,
}}>
&#x25BC;
</span>
</button>
<div style={{
maxHeight: accordionExpanded ? 200 : 0,
overflow: 'hidden',
transition: 'max-height 250ms ease-in-out',
}}>
<div style={{
padding: 'var(--space-4)',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderTop: 'none',
}}>
This content expands with a smooth max-height transition. The chevron rotates 180 degrees.
</div>
</div>
</div>
<CodeLabel text=".dt-transition-accordion | .dt-transition-chevron | .dt-transition-progress" />
</Section>
<Section title="Scrollbar Styling" description="Thin neon scrollbar scoped under [data-brand='dt'].">
<div
className="dt-scrollbar"
style={{
maxHeight: 120,
overflowY: 'auto',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: 'var(--space-3)',
}}
>
{Array.from({ length: 20 }, (_, i) => (
<div key={i} style={{ padding: '4px 0', color: 'var(--color-text)' }}>
Scrollbar line {i + 1} thin neon scrollbar styled with --color-primary
</div>
))}
</div>
<CodeLabel text=".dt-scrollbar — scrollbar-color: var(--color-primary)" />
</Section>
</>
);
}

View File

@@ -0,0 +1,103 @@
import { DTCard, DTMediaFrame } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens';
import { Section, Row, CodeLabel } from '../components/Section';
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
export function BevelsPage() {
return (
<>
<h1 className="page-title">Bevels</h1>
<p className="page-subtitle">Angular clip-path patterns from the DT design system. Active on the DT brand.</p>
<Section title="Card Bevels" description="Dual bottom bevels using clip-path. Outer bg = border color, ::before = surface fill.">
<DTCard title="CARD TITLE">
<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.</div>
</DTCard>
<CodeLabel text="<DTCard title='...'> or .card" />
<DTCard>
<div className="card-body">Card without title no header bar, just the beveled container.</div>
</DTCard>
<CodeLabel text="<DTCard> (no title prop)" />
</Section>
<Section title="Button Bevels" description="Top-right corner cut. Filled buttons use direct clip-path, outline uses dual-element.">
<Row>
<button className="btn-primary" type="button">PRIMARY</button>
<button className="btn-secondary" type="button">SECONDARY</button>
<button className="btn-danger" type="button">DANGER</button>
</Row>
<CodeLabel text=".btn-primary | .btn-secondary | .btn-danger" />
</Section>
<Section title="Badge Bevels" description="Top-right bevel with dual-element technique. Color variants via CSS custom properties.">
<Row>
<span className="badge">Default</span>
<span className="badge badge-success">Success</span>
<span className="badge badge-error">Error</span>
<span className="badge badge-warning">Warning</span>
<span className="badge badge-info">Info</span>
</Row>
<CodeLabel text=".badge | .badge-success | .badge-error | .badge-warning | .badge-info" />
</Section>
<Section title="Media Frame Bevels" description="Diagonal opposite corners (top-left + bottom-right).">
<DTMediaFrame>
<div style={{ background: 'var(--color-primary)', width: '100%', aspectRatio: '16/9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>MEDIA FRAME</span>
</div>
</DTMediaFrame>
<CodeLabel text="<DTMediaFrame> or .dt-bevel-media" />
</Section>
<Section title="Modal Bevels" description="Dual bottom bevels at bevel-lg scale.">
<div className="dt-bevel-modal" style={{ background: 'var(--color-primary)', padding: 'var(--space-8)', textAlign: 'center' }}>
<div style={{ background: 'var(--color-surface)', padding: 'var(--space-6)' }}>
<div style={{ fontWeight: 700, textTransform: 'uppercase' }}>Modal Content</div>
<div style={{ color: 'var(--color-text-muted)', marginTop: 'var(--space-2)' }}>Large dual bottom bevels</div>
</div>
</div>
<CodeLabel text=".dt-bevel-modal" />
</Section>
<Section title="Drawer Bevels" description="Exposed-edge bevels for sliding panels.">
<Row>
<div className="dt-bevel-drawer-right" style={{ background: 'var(--color-primary)', padding: 'var(--space-6)', width: 200, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>RIGHT</span>
</div>
<div className="dt-bevel-drawer-left" style={{ background: 'var(--color-primary)', padding: 'var(--space-6)', width: 200, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700 }}>LEFT</span>
</div>
</Row>
<CodeLabel text=".dt-bevel-drawer-right | .dt-bevel-drawer-left" />
</Section>
<Section title="Small Bevel Utility" description="For compact elements — arrows, stepper buttons.">
<Row>
<div className="dt-bevel-sm" style={{ background: 'var(--color-primary)', width: 60, height: 60, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--color-bg)', fontWeight: 700, fontSize: '0.75rem' }}>SM</span>
</div>
</Row>
<CodeLabel text=".dt-bevel-sm" />
</Section>
<Section title="Accent Top" description="Used on accordion headers and menu items.">
<div className="dt-accent-top" style={{ padding: 'var(--space-4)', background: 'var(--color-surface)', border: '1px solid var(--color-border)' }}>
<span style={{ fontWeight: 600, textTransform: 'uppercase' }}>Thick Top Border Accent</span>
</div>
<CodeLabel text=".dt-accent-top" />
</Section>
<Section title="Card Color Modes" description="Per-card mode coloring — see Advanced Cards page for full demos.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{modes.map(mode => (
<DTCard key={mode} variant={mode} title={mode.toUpperCase()}>
<div className="card-body" style={{ fontSize: '0.75rem' }}>mode-{mode}</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard variant='normal'> | 'emphasis' | 'warning' | 'success' | 'other'" />
</Section>
</>
);
}

View File

@@ -0,0 +1,210 @@
import { createElement, useState, type CSSProperties } from 'react';
import {
DTCard,
DTButton,
DTStaggerContainer,
DTFeatureLegend,
} from '@dangerousthings/react';
import type { DTFeatureItem } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens';
import { Section, Row, CodeLabel } from '../components/Section';
// Feature icons from dt-shopify-storefront
import {
MdOutlinePhonelinkRing,
MdOutlineVpnKey,
MdOutlineMobileScreenShare,
MdOutlineCreditCard,
MdOutlineCopyAll,
MdOutlineLightbulb,
MdOutlineThermostat,
MdOutlineSensors,
MdOutlineFitbit,
MdOutlineVibration,
MdOutlineExplore,
MdLightbulbOutline,
MdOutlineVisibility,
} from 'react-icons/md';
import { FaUserShield } from 'react-icons/fa';
import { LuBinary } from 'react-icons/lu';
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
// Wrap react-icons to avoid IconType/ReactNode type mismatch
const ico = (C: any) => createElement(C, { style: { fontSize: '2rem' } });
// Full chip feature legend (9 features — from storefront UseCaseLegend)
const chipFeatures: DTFeatureItem[] = [
{ key: 'smartphone', name: 'Smartphone', icon: ico(MdOutlinePhonelinkRing), state: 'supported' },
{ key: 'access_control', name: 'Access Control', icon: ico(MdOutlineVpnKey), state: 'supported' },
{ key: 'digital_security', name: 'Digital Security', icon: ico(FaUserShield), state: 'supported' },
{ key: 'cryptography', name: 'Cryptography', icon: ico(LuBinary), state: 'unsupported' },
{ key: 'data_sharing', name: 'Data Sharing', icon: ico(MdOutlineMobileScreenShare), state: 'supported' },
{ key: 'payment', name: 'Payment', icon: ico(MdOutlineCreditCard), state: 'disabled' },
{ key: 'magic', name: 'UID Magic', icon: ico(MdOutlineCopyAll), state: 'supported' },
{ key: 'illumination', name: 'Illumination', icon: ico(MdOutlineLightbulb), state: 'unsupported' },
{ key: 'temperature', name: 'Temperature', icon: ico(MdOutlineThermostat), state: 'unsupported' },
];
// Full biomagnet feature legend (4 features — from storefront MagnetUseCaseLegend)
const magnetFeatures: DTFeatureItem[] = [
{ key: 'sensing', name: 'Sensing', icon: ico(MdOutlineSensors), state: 'supported' },
{ key: 'lifting', name: 'Lifting', icon: ico(MdOutlineFitbit), state: 'supported' },
{ key: 'haptics', name: 'Haptics', icon: ico(MdOutlineVibration), state: 'supported' },
{ key: 'polarity', name: 'Polarity Detection', icon: ico(MdOutlineExplore), state: 'unsupported' },
];
// Full aesthetic feature legend (2 features — from storefront AestheticUseCaseLegend)
const aestheticFeatures: DTFeatureItem[] = [
{ key: 'illumination', name: 'Illumination', icon: ico(MdLightbulbOutline), state: 'supported' },
{ key: 'prominence', name: 'Prominence', icon: ico(MdOutlineVisibility), state: 'supported' },
];
export function CardsAdvancedPage() {
const [staggerKey, setStaggerKey] = useState(0);
return (
<>
<h1 className="page-title">Advanced Cards</h1>
<p className="page-subtitle">Card color modes, progress bars, badge overlays, and interactive bevel buttons.</p>
<Section title="Card Color Modes" description="Per-card color via variant prop. Sets --dt-card-color, glow color, and accent.">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{modes.map(mode => (
<DTCard key={mode} variant={mode} title={mode.toUpperCase()}>
<div className="card-body" style={{ fontSize: '0.8rem' }}>mode-{mode}</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard variant='normal'> | 'emphasis' | 'warning' | 'success' | 'other'" />
</Section>
<Section title="Card Progress Bar" description="Vertical left-edge bar driven by progress prop (0-100).">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3">
{[0, 25, 50, 75, 100].map((val, i) => (
<DTCard key={val} variant={modes[i % modes.length]} title={`${val}%`} progress={val}>
<div className="card-body" style={{ fontSize: '0.8rem' }}>Progress: {val}%</div>
</DTCard>
))}
</div>
<CodeLabel text="<DTCard progress={50} /> — height driven by --dt-card-progress" />
</Section>
<Section title="Card Badges" description="Bottom-right chip badge on product image area. Inherits card mode color.">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-dt-4">
{[
{ label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=400&fit=crop' },
{ label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' },
{ label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' },
].map(badge => (
<DTCard key={badge.label} variant={badge.mode} title={badge.label + ' PRODUCT'}>
<div className="card-body-flush dt-badge-parent" style={{ aspectRatio: '1' }}>
<img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<span className="dt-card-badge">{badge.label}</span>
</div>
</DTCard>
))}
</div>
<CodeLabel text=".dt-badge-parent > .dt-card-badge — badge positioned on image container" />
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', margin: 'var(--space-4) 0 var(--space-2)' }}>
Also works on beveled media frames:
</p>
<div className="grid grid-cols-3 gap-dt-4">
{[
{ label: 'LAB', mode: 'warning' as DTVariant, img: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=400&fit=crop' },
{ label: 'BUNDLE', mode: 'other' as DTVariant, img: 'https://images.unsplash.com/photo-1518770660439-4636190af475?w=400&h=400&fit=crop' },
{ label: 'NFC', mode: 'normal' as DTVariant, img: 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=400&h=400&fit=crop' },
].map(badge => (
<div key={badge.label} className={`dt-bevel-media mode-${badge.mode}`} style={{ aspectRatio: '1', overflow: 'hidden' }}>
<img src={badge.img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<span className="dt-card-badge">{badge.label}</span>
</div>
))}
</div>
<CodeLabel text=".dt-bevel-media.mode-warning > .dt-card-badge" />
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', margin: 'var(--space-4) 0 var(--space-2)' }}>
Missing image placeholder (no img element):
</p>
<div className="grid grid-cols-3 gap-dt-4">
{(['warning', 'other', 'normal'] as DTVariant[]).map(mode => (
<div key={mode} className={`dt-bevel-media mode-${mode}`} style={{ aspectRatio: '1' }} />
))}
</div>
<CodeLabel text=".dt-bevel-media without child img — shows 'MISSING MEDIA' placeholder" />
</Section>
<Section title="Interactive Bevel Buttons" description="Outlined rectangle by default. Bottom-right bevel appears on hover/select, fills with mode color.">
<Row>
{modes.map(mode => (
<DTButton key={mode} variant={mode}>{mode.toUpperCase()}</DTButton>
))}
</Row>
<CodeLabel text="<DTButton variant='normal'> — hover for bevel + fill" />
<Row style={{ marginTop: 'var(--space-4)' }}>
{modes.map(mode => (
<DTButton key={mode} variant={mode} selected>{mode.toUpperCase()} SEL</DTButton>
))}
</Row>
<CodeLabel text="<DTButton variant='emphasis' selected /> — persistent bevel + fill" />
</Section>
<Section title="Menu Items" description="Filter-style beveled menu items with active/selected states and level indentation.">
<div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((item, i) => (
<button
key={item}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`}
style={i > 1 && i < 4 ? { '--dt-menu-level': '1' } as CSSProperties : undefined}
type="button"
>
{item}
</button>
))}
</div>
<CodeLabel text=".dt-menu-item | .dt-menu-item.active | --dt-menu-level: 1" />
</Section>
<Section title="Feature Legends" description="Product feature grids from the storefront. Icons indicate feature capabilities; color indicates state.">
<DTFeatureLegend features={chipFeatures} title="NFC CHIP FEATURES" variant="normal" columns={5} />
<div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={magnetFeatures} title="BIOMAGNET FEATURES" variant="emphasis" columns={4} />
<div style={{ height: 'var(--space-6)' }} />
<DTFeatureLegend features={aestheticFeatures} title="AESTHETIC FEATURES" variant="other" columns={2} />
<div style={{ height: 'var(--space-4)' }} />
<CodeLabel text="<DTFeatureLegend features={chipFeatures} title='NFC CHIP FEATURES' variant='normal' />" />
<CodeLabel text="<DTFeatureLegend features={magnetFeatures} variant='emphasis' columns={4} />" />
<CodeLabel text="<DTFeatureLegend features={aestheticFeatures} variant='other' columns={2} />" />
<div style={{ marginTop: 'var(--space-3)', display: 'flex', gap: 'var(--space-4)', fontSize: '0.75rem' }}>
<span><span style={{ color: 'var(--mode-normal)' }}></span> Supported</span>
<span><span style={{ color: 'var(--mode-emphasis)' }}></span> Disabled</span>
<span><span style={{ color: 'var(--mode-warning)' }}></span> Unsupported</span>
</div>
</Section>
<Section title="Stagger + Modes" description="Mode-colored cards in a stagger container.">
<DTStaggerContainer
key={staggerKey}
className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-dt-3"
>
{Array.from({ length: 10 }, (_, i) => (
<DTCard key={i} variant={modes[i % modes.length]} title={modes[i % modes.length].toUpperCase()} style={{ textAlign: 'center' }}>
{null}
</DTCard>
))}
</DTStaggerContainer>
<button
className="btn-secondary"
style={{ marginTop: 'var(--space-4)' }}
onClick={() => setStaggerKey(k => k + 1)}
type="button"
>
REPLAY
</button>
<CodeLabel text="<DTStaggerContainer> with <DTCard variant='...' />" />
</Section>
</>
);
}

View File

@@ -0,0 +1,122 @@
import { useState } from 'react';
import {
DTTextInput,
DTCheckbox,
DTSwitch,
DTRadioGroup,
DTProgressBar,
DTAccordion,
DTQuantityStepper,
} from '@dangerousthings/react';
import type { DTAccordionSection } from '@dangerousthings/react';
import { Section, Row, CodeLabel, DemoLabel } from '../components/Section';
export function FormsPage() {
const [checks, setChecks] = useState({ a: false, b: true, c: true, d: false });
const [switches, setSwitches] = useState({ a: false, b: true, c: true, d: false });
const [radioValue, setRadioValue] = useState('nfc');
const [stepperValue, setStepperValue] = useState(1);
const accordionSections: DTAccordionSection[] = [
{ key: 'size', title: 'Size', children: <div style={{ padding: 'var(--space-4)' }}>Small, Medium, Large options available.</div> },
{ key: 'chip', title: 'Chip Type', children: <div style={{ padding: 'var(--space-4)' }}>NTAG, DESFire, MIFARE Classic.</div> },
{ key: 'freq', title: 'Frequency', children: <div style={{ padding: 'var(--space-4)' }}>13.56 MHz (HF), 125 kHz (LF).</div> },
];
return (
<>
<h1 className="page-title">Forms</h1>
<p className="page-subtitle">DT-branded form components using @dangerousthings/react.</p>
<Section title="Text Input" description="Sharp corners + focus glow bar. 2px solid border, no border-radius.">
<DTTextInput placeholder="Normal input — click to focus" style={{ maxWidth: 400 }} />
<CodeLabel text="<DTTextInput /> — focus glow bar" />
<br />
<DTTextInput error placeholder="Error state input" style={{ maxWidth: 400 }} />
<CodeLabel text="<DTTextInput error /> — red focus bar" />
<br />
<textarea placeholder="Textarea element" style={{ maxWidth: 400, minHeight: 80 }} />
<CodeLabel text="textarea — same styling" />
</Section>
<Section title="Checkbox" description="Beveled hexagon shape via clip-path. 30% corner cuts.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<DTCheckbox checked={checks.a} onChange={v => setChecks(p => ({ ...p, a: v }))} label="Unchecked checkbox" />
<DTCheckbox checked={checks.b} onChange={v => setChecks(p => ({ ...p, b: v }))} label="Checked checkbox" />
<DTCheckbox checked={checks.c} onChange={() => {}} disabled label="Disabled checked" />
<DTCheckbox checked={checks.d} onChange={() => {}} disabled label="Disabled unchecked" />
</div>
<CodeLabel text="<DTCheckbox checked onChange label />" />
</Section>
<Section title="Switch / Toggle" description="Angular track + sliding square thumb. 48x26 track, 20x20 thumb.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<DTSwitch checked={switches.a} onChange={v => setSwitches(p => ({ ...p, a: v }))} label="Off" />
<DTSwitch checked={switches.b} onChange={v => setSwitches(p => ({ ...p, b: v }))} label="On" />
<DTSwitch checked={switches.c} onChange={() => {}} disabled label="Disabled (on)" />
<DTSwitch checked={switches.d} onChange={() => {}} disabled label="Disabled (off)" />
</div>
<CodeLabel text="<DTSwitch checked onChange label />" />
</Section>
<Section title="Radio Button" description="Hexagonal indicator via clip-path. Flat-top hexagon shape.">
<DTRadioGroup
options={[
{ value: 'nfc', label: 'NFC (Near Field Communication)' },
{ value: 'rfid', label: 'RFID (Radio Frequency ID)' },
{ value: 'ble', label: 'BLE (Bluetooth Low Energy)' },
]}
value={radioValue}
onChange={setRadioValue}
/>
<CodeLabel text="<DTRadioGroup options value onChange />" />
</Section>
<Section title="Progress Bar" description="Angular, no border-radius. 4px default height.">
<DemoLabel text="25%" />
<DTProgressBar value={0.25} />
<DemoLabel text="50% WITH LABEL" />
<DTProgressBar value={0.5} label="50%" />
<DemoLabel text="75%" />
<DTProgressBar value={0.75} />
<DemoLabel text="100%" />
<DTProgressBar value={1.0} label="100%" />
<CodeLabel text="<DTProgressBar value={0.5} label='50%' />" />
</Section>
<Section title="Accordion" description="Click headers to expand. 5px top border, chevron rotation.">
<DTAccordion sections={accordionSections} />
<CodeLabel text="<DTAccordion sections={[{ key, title, children }]} />" />
</Section>
<Section title="Quantity Stepper" description="Beveled +/- buttons with center display.">
<DTQuantityStepper value={stepperValue} onChange={setStepperValue} min={0} max={10} />
<CodeLabel text="<DTQuantityStepper value onChange min max />" />
</Section>
<Section title="Filter Header" description="Thick top border accent. Active state switches to secondary color.">
<div style={{ maxWidth: 400 }}>
<button className="dt-filter-header" type="button">FILTER CATEGORY</button>
<button className="dt-filter-header active" type="button">ACTIVE FILTER</button>
</div>
<CodeLabel text=".dt-filter-header | .dt-filter-header.active" />
</Section>
<Section title="Menu Items" description="Beveled filter menu items with active state and level indentation.">
<div style={{ maxWidth: 300 }}>
{['All Products', 'NFC Implants', 'RFID Tags', 'Accessories', 'Lab Products'].map((name, i) => (
<button
key={name}
className={`dt-menu-item${i === 0 ? ' active' : ''}${i === 4 ? ' mode-warning' : ''}`}
style={(i === 2 || i === 3) ? { '--dt-menu-level': '1' } as React.CSSProperties : undefined}
type="button"
>
{name}
</button>
))}
</div>
<CodeLabel text=".dt-menu-item | .active | --dt-menu-level: 1" />
</Section>
</>
);
}

View File

@@ -0,0 +1,81 @@
import { DTCard } from '@dangerousthings/react';
import { Section, Row, CodeLabel } from '../components/Section';
export function GlowsPage() {
return (
<>
<h1 className="page-title">Glows</h1>
<p className="page-subtitle">Neon drop-shadow and text-shadow effects. Active on the DT brand.</p>
<Section title="Button Glows" description="filter: drop-shadow() follows clip-path shape. Hover for enhanced glow.">
<Row>
<button className="btn-primary" type="button">PRIMARY GLOW</button>
<button className="btn-danger" type="button">DANGER GLOW</button>
</Row>
<CodeLabel text={'.btn-primary / .btn-danger — automatic on [data-brand="dt"]'} />
<br />
<Row>
<button className="btn-secondary" type="button">SECONDARY (HOVER)</button>
</Row>
<CodeLabel text=".btn-secondary — glow on hover" />
</Section>
<Section title="Link Glow" description="text-shadow on hover. Respects --dt-glow-color when set by a parent mode.">
<Row>
<a href="#" style={{ color: 'var(--color-primary)', fontWeight: 600, fontSize: '1.125rem' }}>
Hover this link for text glow
</a>
</Row>
<CodeLabel text="a:hover — uses var(--dt-glow-color, var(--color-primary))" />
</Section>
<Section title="Mode-Aware Link Glow" description="Links inside mode containers glow with that mode's color.">
<Row>
{(['normal', 'emphasis', 'warning', 'success', 'other'] as const).map(mode => (
<div key={mode} className={`mode-${mode}`} style={{ padding: 'var(--space-3)' }}>
<a href="#" style={{ fontWeight: 600, fontSize: '1rem' }}>{mode.toUpperCase()}</a>
</div>
))}
</Row>
<CodeLabel text=".mode-emphasis a:hover — glows yellow via --dt-glow-color" />
</Section>
<Section title="Terminal Inset Glow" description="Inset + outer box-shadow. No clip-path so box-shadow works directly.">
<div className="terminal dt-accent-top">
<code>{'$ dt-web-theme --install\n> Installing design tokens...\n> Applying cyberpunk aesthetic...\n> Done. Welcome to the future.'}</code>
</div>
<CodeLabel text={'.terminal — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Card Hover Glow" description="filter: drop-shadow() respects the beveled clip-path.">
<DTCard title="HOVER ME" style={{ cursor: 'pointer' }}>
<div className="card-body">Cards get a drop-shadow glow on hover.</div>
</DTCard>
<CodeLabel text={'.card:hover — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Input Focus Glow" description="box-shadow: 0 4px 0 1px — bright bar beneath the input.">
<input
type="text"
className="input"
placeholder="Click to focus — see the glow bar"
style={{ maxWidth: 400 }}
/>
<CodeLabel text={'.input:focus — automatic on [data-brand="dt"]'} />
</Section>
<Section title="Glow Utilities" description="Generic utility classes for applying glow effects to any element.">
<Row>
<div className="dt-glow" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow</div>
<div className="dt-glow-strong" style={{ background: 'var(--color-primary)', padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, color: 'var(--color-bg)' }}>.dt-glow-strong</div>
</Row>
<CodeLabel text=".dt-glow | .dt-glow-strong" />
<Row>
<div className="dt-glow-inset" style={{ padding: 'var(--space-4) var(--space-6)', display: 'inline-block', fontWeight: 700, border: '1px solid var(--color-border)' }}>.dt-glow-inset</div>
<div className="dt-text-glow" style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)', display: 'inline-block' }}>.dt-text-glow</div>
</Row>
<CodeLabel text=".dt-glow-inset | .dt-text-glow" />
</Section>
</>
);
}

View File

@@ -0,0 +1,103 @@
import { DTCard, DTStaggerContainer } from '@dangerousthings/react';
import type { DTVariant } from '@dangerousthings/tokens';
import { Section, CodeLabel } from '../components/Section';
const categories: { hash: string; title: string; desc: string; mode: DTVariant; count: number }[] = [
{ hash: 'bevels', title: 'Bevels', desc: 'Angular clip-path patterns — cards, buttons, badges, media frames, modals, drawers', mode: 'normal', count: 8 },
{ hash: 'glows', title: 'Glows', desc: 'Neon drop-shadow and text-shadow effects for the DT brand', mode: 'emphasis', count: 6 },
{ hash: 'forms', title: 'Forms', desc: 'Text inputs, checkboxes, switches, radios, progress bars, accordions, steppers', mode: 'success', count: 7 },
{ hash: 'animations', title: 'Animations', desc: 'Entrance animations, stagger containers, transition utilities, scrollbar styling', mode: 'other', count: 5 },
{ hash: 'cards-advanced', title: 'Advanced Cards', desc: 'Card color modes, progress bars, badge overlays, interactive bevel buttons, feature legend', mode: 'warning', count: 6 },
{ hash: 'tokens', title: 'Tokens', desc: 'Color palette, typography, spacing, and shape values for the active brand', mode: 'normal', count: 3 },
];
function HexDecor({ color, size = 14, opacity = 0.5 }: { color: string; size?: number; opacity?: number }) {
return (
<svg width={size * 2} height={size * 2} viewBox="0 0 28 28" style={{ opacity }}>
<polygon
points="14,1 25.5,7.5 25.5,20.5 14,27 2.5,20.5 2.5,7.5"
fill={color}
/>
</svg>
);
}
export function HomePage() {
return (
<>
<div style={{ textAlign: 'center', marginBottom: 'var(--space-8)' }}>
<h1 style={{
color: 'var(--color-primary)',
fontSize: '2.5rem',
fontWeight: 900,
letterSpacing: '0.15em',
marginBottom: '0.25rem',
textTransform: 'uppercase',
}}>
DANGEROUS THINGS
</h1>
<p style={{
color: 'var(--mode-emphasis, var(--color-secondary))',
fontSize: '1.25rem',
fontWeight: 700,
letterSpacing: '0.2em',
textTransform: 'uppercase',
marginTop: 0,
}}>
DESIGN SYSTEM
</p>
<p style={{
color: 'var(--color-text-muted)',
fontSize: '0.8rem',
marginTop: 'var(--space-2)',
}}>
React component showcase switch brands and modes in the sidebar
</p>
<div style={{
display: 'flex',
justifyContent: 'center',
gap: 12,
marginTop: 'var(--space-6)',
}}>
<HexDecor color="var(--mode-normal, var(--color-primary))" opacity={0.4} />
<HexDecor color="var(--mode-emphasis, var(--color-secondary))" opacity={0.6} />
<HexDecor color="var(--mode-warning, var(--color-error))" opacity={0.4} />
<HexDecor color="var(--mode-success, var(--color-accent))" opacity={0.6} />
<HexDecor color="var(--mode-other, var(--color-other))" opacity={0.4} />
</div>
</div>
<p style={{
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
letterSpacing: '0.15em',
textTransform: 'uppercase',
marginBottom: 'var(--space-4)',
}}>
COMPONENT CATALOG
</p>
<DTStaggerContainer className="grid grid-cols-1 md:grid-cols-2 gap-dt-4">
{categories.map(cat => (
<a key={cat.hash} href={`#/${cat.hash}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<DTCard variant={cat.mode} title={cat.title.toUpperCase()}>
<div className="card-body">{cat.desc}</div>
{cat.count > 0 && (
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem', marginTop: 'var(--space-2)' }}>
{cat.count} components
</div>
)}
</DTCard>
</a>
))}
</DTStaggerContainer>
<Section title="Quick Start" description="Import the React components and CSS to get started.">
<div className="terminal dt-accent-top">
<code>{`import { DTCard, DTButton } from '@dangerousthings/react';\nimport '@dangerousthings/web/index.css';`}</code>
</div>
</Section>
</>
);
}

View File

@@ -0,0 +1,99 @@
import { brands } from '@dangerousthings/tokens';
import type { ThemeBrand } from '@dangerousthings/tokens';
import { Section, CodeLabel } from '../components/Section';
interface TokensPageProps {
brand: ThemeBrand;
}
export function TokensPage({ brand: brandId }: TokensPageProps) {
const brand = brands[brandId];
if (!brand) return null;
const darkColors = brand.dark;
const darkEntries = Object.entries(darkColors) as [string, string][];
const lightColors = brand.light;
const lightEntries = Object.entries(lightColors) as [string, string][];
return (
<>
<h1 className="page-title">Tokens</h1>
<p className="page-subtitle">Design token values for the active brand. Switch brands in the sidebar.</p>
<Section title={`Color Palette — ${brand.name} (Dark Mode)`} description={brand.description}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--space-3)' }}>
{darkEntries.map(([name, hex]) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: 32, height: 32, background: hex, border: '1px solid rgba(255,255,255,0.15)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 600, fontSize: '0.8125rem' }}>{name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.6875rem', opacity: 0.5 }}>{hex}</div>
</div>
</div>
))}
</div>
</Section>
<Section title={`Color Palette — ${brand.name} (Light Mode)`} description="Light mode color tokens.">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--space-3)' }}>
{lightEntries.map(([name, hex]) => (
<div key={name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: 32, height: 32, background: hex, border: '1px solid rgba(255,255,255,0.15)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 600, fontSize: '0.8125rem' }}>{name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.6875rem', opacity: 0.5 }}>{hex}</div>
</div>
</div>
))}
</div>
</Section>
<Section title="Typography" description="Font family tokens for headings, body text, and monospace.">
<div className="terminal" style={{ marginBottom: 'var(--space-4)' }}>
<code>{`heading: ${brand.typography.heading}\nbody: ${brand.typography.body}\nmono: ${brand.typography.mono}`}</code>
</div>
</Section>
<Section title="Typography Scale" description="Live rendering with the current brand's font families.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
<h1 style={{ fontFamily: 'var(--font-heading)', fontSize: '2rem', fontWeight: 900 }}>Heading 1</h1>
<h2 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.5rem', fontWeight: 700 }}>Heading 2</h2>
<h3 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.25rem', fontWeight: 600 }}>Heading 3</h3>
<p style={{ fontFamily: 'var(--font-body)', fontSize: '1rem' }}>Body text the quick brown fox jumps over the lazy dog</p>
<code style={{ fontFamily: 'var(--font-mono)', fontSize: '0.875rem' }}>{'const monospace = "code";'}</code>
</div>
</Section>
<Section title="Shape Tokens" description="Bevel and radius values determine the visual language — angular (DT) vs rounded (Classic).">
<div className="terminal" style={{ marginBottom: 'var(--space-4)' }}>
<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}`}</code>
</div>
</Section>
<Section title="Spacing Scale" description="Shared spacing tokens across all brands.">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{[
{ name: '--space-1', value: '0.25rem (4px)', width: '1.5rem' },
{ name: '--space-2', value: '0.5rem (8px)', width: '3rem' },
{ name: '--space-3', value: '0.75rem (12px)', width: '4.5rem' },
{ name: '--space-4', value: '1rem (16px)', width: '6rem' },
{ name: '--space-6', value: '1.5rem (24px)', width: '9rem' },
{ name: '--space-8', value: '2rem (32px)', width: '12rem' },
].map(sp => (
<div key={sp.name} style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
<div style={{ width: sp.width, height: 12, background: 'var(--color-primary)', opacity: 0.6 }} />
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', opacity: 0.6 }}>{sp.name}: {sp.value}</span>
</div>
))}
</div>
</Section>
<Section title="CSS Custom Properties" description="These are generated from TypeScript tokens and applied per-brand via data-brand attribute.">
<div className="terminal">
<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};`}</code>
</div>
<CodeLabel text="See packages/tokens/src/scripts/generate-css.ts" />
</Section>
</>
);
}

View File

@@ -1,126 +0,0 @@
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

@@ -1,199 +0,0 @@
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

@@ -1,97 +0,0 @@
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

@@ -1,33 +0,0 @@
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

@@ -1,141 +0,0 @@
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

@@ -1,3 +1,6 @@
@tailwind components;
@tailwind utilities;
/* Showcase App Layout */
* {
@@ -228,12 +231,16 @@ body {
opacity: 0.9;
}
/* Cards */
/* Cards — DT brand padding is set in bevels.css via --_card-pad */
.card {
padding: var(--space-6);
margin-bottom: var(--space-4);
}
/* Classic brand: cards need explicit padding (no bevel CSS provides it) */
[data-brand="classic"] .card {
padding: var(--space-6);
}
.card-title {
font-weight: 900;
font-size: 1rem;

View File

@@ -1,46 +0,0 @@
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

@@ -1,28 +0,0 @@
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 @@
import dtPreset from '@dangerousthings/tailwind-preset';
/** @type {import('tailwindcss').Config} */
export default {
presets: [dtPreset],
content: ['./src/renderer/**/*.{tsx,ts,html}'],
corePlugins: {
preflight: false,
},
};

View File

@@ -4,6 +4,7 @@
"outDir": "dist/renderer",
"rootDir": "src/renderer",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src/renderer"]

View File

@@ -1,9 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
root: 'src/renderer',
base: './',
plugins: [react()],
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,

View File

@@ -0,0 +1,13 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View File

@@ -0,0 +1,3 @@
{
"devices": []
}

View File

@@ -46,3 +46,16 @@ export const dropdownItems = [
{ id: 'd3', title: 'Archive', onPress: () => {} },
{ id: 'd4', title: 'Delete', onPress: () => {} },
];
export const featureLegendItems = [
{ key: 'payment', name: 'Payment', icon: null, state: 'supported' as const },
{ key: 'access', name: 'Access', icon: null, state: 'supported' as const },
{ key: 'clone', name: 'Clone', icon: null, state: 'disabled' as const },
{ key: 'crypto', name: 'Crypto', icon: null, state: 'unsupported' as const },
{ key: 'sensing', name: 'Sensing', icon: null, state: 'supported' as const },
{ key: 'temp', name: 'Temp', icon: null, state: 'supported' as const },
{ key: 'fitness', name: 'Fitness', icon: null, state: 'disabled' as const },
{ key: 'explore', name: 'Explore', icon: null, state: 'supported' as const },
{ key: 'vibration', name: 'Vibration', icon: null, state: 'unsupported' as const },
{ key: 'sharing', name: 'Sharing', icon: null, state: 'supported' as const },
];

View File

@@ -10,6 +10,9 @@ import { FormsScreen } from '../screens/FormsScreen';
import { FeedbackScreen } from '../screens/FeedbackScreen';
import { OverlaysScreen } from '../screens/OverlaysScreen';
import { ThemeScreen } from '../screens/ThemeScreen';
import { AnimationsScreen } from '../screens/AnimationsScreen';
import { CardsAdvancedScreen } from '../screens/CardsAdvancedScreen';
import { FiltersScreen } from '../screens/FiltersScreen';
const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -42,6 +45,11 @@ export function RootNavigator() {
component={CardsScreen}
options={{ title: 'CARDS & LABELS' }}
/>
<Stack.Screen
name="CardsAdvanced"
component={CardsAdvancedScreen}
options={{ title: 'ADVANCED CARDS' }}
/>
<Stack.Screen
name="Forms"
component={FormsScreen}
@@ -57,6 +65,16 @@ export function RootNavigator() {
component={OverlaysScreen}
options={{ title: 'OVERLAYS & NAV' }}
/>
<Stack.Screen
name="Animations"
component={AnimationsScreen}
options={{ title: 'ANIMATIONS' }}
/>
<Stack.Screen
name="Filters"
component={FiltersScreen}
options={{ title: 'FILTERS & FEATURES' }}
/>
<Stack.Screen
name="Theme"
component={ThemeScreen}

View File

@@ -2,8 +2,11 @@ export type RootStackParamList = {
Home: undefined;
Buttons: undefined;
Cards: undefined;
CardsAdvanced: undefined;
Forms: undefined;
Feedback: undefined;
Overlays: undefined;
Animations: undefined;
Filters: undefined;
Theme: undefined;
};

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTCard,
DTLabel,
DTStaggerContainer,
useDTTheme,
useScaleIn,
usePulse,
} from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel';
import { Animated } from 'react-native';
export function AnimationsScreen() {
const theme = useDTTheme();
const scaleAnim = useScaleIn({ duration: 600 });
const pulseAnim = usePulse(true);
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return (
<ScreenContainer>
{/* Stagger Container */}
<DemoSection
title="DTStaggerContainer"
variant="normal"
description="Staggered scale-in entrance animation for child elements."
>
<DTStaggerContainer duration={330} interval={75}>
{modes.map((mode) => (
<DTCard key={mode} mode={mode} title={mode.toUpperCase()} style={{ marginBottom: 12 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered entrance with scale animation
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="<DTStaggerContainer duration={330} interval={75}>" />
</DemoSection>
{/* Stagger with Labels */}
<DemoSection
title="Staggered Labels"
variant="emphasis"
description="Labels with staggered entrance animation."
>
<DTStaggerContainer duration={400} interval={100}>
{modes.map((mode) => (
<View key={mode} style={{ marginBottom: 8 }}>
<DTLabel primaryText={mode.toUpperCase()} mode={mode} />
</View>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTLabel — customizable timing" />
</DemoSection>
{/* Scale-In Hook */}
<DemoSection
title="useScaleIn"
variant="success"
description="Scale 0→1 entrance animation hook for individual elements."
>
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<DTCard mode="success" title="SCALE IN">
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
This card used useScaleIn(&#123; duration: 600 &#125;)
</Text>
</DTCard>
</Animated.View>
<CodeLabel text="const scale = useScaleIn({ duration: 600 })" />
</DemoSection>
{/* Pulse Hook */}
<DemoSection
title="usePulse"
variant="warning"
description="Looping opacity pulse animation for active/loading states."
>
<Animated.View style={{ opacity: pulseAnim }}>
<DTLabel primaryText="PULSING LABEL" mode="warning" />
</Animated.View>
<CodeLabel text="const opacity = usePulse(true)" />
</DemoSection>
</ScreenContainer>
);
}

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTCard,
DTChip,
DTBadgeOverlay,
DTStaggerContainer,
useDTTheme,
} from '@dangerousthings/react-native';
import type { DTVariant } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel';
export function CardsAdvancedScreen() {
const theme = useDTTheme();
const modes: DTVariant[] = ['normal', 'emphasis', 'warning', 'success', 'other'];
return (
<ScreenContainer>
{/* Progress Bar */}
<DemoSection
title="Progress Bar"
variant="normal"
description="Vertical left-edge progress indicator (01)."
>
{[0, 0.25, 0.5, 0.75, 1].map((val, i) => (
<DTCard
key={val}
mode={modes[i]}
title={`${Math.round(val * 100)}% PROGRESS`}
progress={val}
style={{ marginBottom: 12 }}
>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
progress=&#123;{val}&#125;
</Text>
</DTCard>
))}
<CodeLabel text="<DTCard progress={0.5}> — width matches bevelSizeSmall" />
</DemoSection>
{/* Card Badges */}
<DemoSection
title="Card Badges"
variant="other"
description="Bottom-right chip badges. Color matches badge category, not card mode."
>
<DTCard mode="normal" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="warning">LAB</DTChip>
</DTBadgeOverlay>
</DTCard>
<DTCard mode="emphasis" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="other">BUNDLE</DTChip>
</DTBadgeOverlay>
</DTCard>
<DTCard mode="success" title="PRODUCT CARD" style={{ marginBottom: 16 }}>
<View style={{ minHeight: 40 }}>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Badge color independent of card mode
</Text>
</View>
<DTBadgeOverlay position="bottom-right">
<DTChip variant="emphasis">NEW</DTChip>
</DTBadgeOverlay>
</DTCard>
<CodeLabel text="<DTBadgeOverlay position='bottom-right'><DTChip variant='warning'>LAB</DTChip>" />
</DemoSection>
{/* Stagger + Progress */}
<DemoSection
title="Staggered Cards with Progress"
variant="success"
description="Stagger container with progress bars across all modes."
>
<DTStaggerContainer>
{modes.map((mode) => (
<DTCard
key={mode}
mode={mode}
title={mode.toUpperCase()}
progress={Math.random()}
style={{ marginBottom: 12 }}
>
<Text variant="bodySmall" style={{ color: theme.colors.onSurface }}>
Staggered + progress
</Text>
</DTCard>
))}
</DTStaggerContainer>
<CodeLabel text="DTStaggerContainer > DTCard progress" />
</DemoSection>
</ScreenContainer>
);
}

View File

@@ -135,6 +135,20 @@ export function CardsScreen() {
</View>
</DemoSection>
{/* DTCard - Selected & Progress (quick preview) */}
<DemoSection
title="Progress Bar"
variant="warning"
description="Quick preview — see Advanced Cards screen for full demos."
>
<DTCard mode="normal" title="WITH PROGRESS" progress={0.6} style={{ marginBottom: 16 }}>
<Text variant="bodyMedium" style={{ color: theme.colors.onSurface }}>
progress=0.6 vertical left-edge bar
</Text>
</DTCard>
<CodeLabel text="See Advanced Cards for full progress/badge demos" />
</DemoSection>
{/* DTMediaFrame */}
<DemoSection
title="DTMediaFrame"

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import {
DTFeatureLegend,
DTMobileFilterOverlay,
DTButton,
DTAccordion,
useDTTheme,
} from '@dangerousthings/react-native';
import type { DTFeatureItem } from '@dangerousthings/react-native';
import { ScreenContainer } from '../components/ScreenContainer';
import { DemoSection } from '../components/DemoSection';
import { CodeLabel } from '../components/CodeLabel';
const sampleFeatures: DTFeatureItem[] = [
{ key: 'payment', name: 'Payment', icon: null, state: 'supported' },
{ key: 'access', name: 'Access', icon: null, state: 'supported' },
{ key: 'clone', name: 'Clone', icon: null, state: 'disabled' },
{ key: 'crypto', name: 'Crypto', icon: null, state: 'unsupported' },
{ key: 'sensing', name: 'Sensing', icon: null, state: 'supported' },
{ key: 'temp', name: 'Temp', icon: null, state: 'supported' },
{ key: 'fitness', name: 'Fitness', icon: null, state: 'disabled' },
{ key: 'explore', name: 'Explore', icon: null, state: 'supported' },
{ key: 'vibration', name: 'Vibration', icon: null, state: 'unsupported' },
{ key: 'sharing', name: 'Sharing', icon: null, state: 'supported' },
];
export function FiltersScreen() {
const theme = useDTTheme();
const [overlayVisible, setOverlayVisible] = useState(false);
return (
<ScreenContainer>
{/* Feature Legend */}
<DemoSection
title="DTFeatureLegend"
variant="normal"
description="Product feature grid with icons and rotated labels. Color indicates feature state."
>
<DTFeatureLegend
features={sampleFeatures}
variant="normal"
title="NFC FEATURES"
columns={5}
/>
<CodeLabel text='<DTFeatureLegend features={...} variant="normal" columns={5}>' />
</DemoSection>
{/* Feature Legend — other variant */}
<DemoSection
title="Emphasis Variant"
variant="emphasis"
description="Feature legend with emphasis header color."
>
<DTFeatureLegend
features={sampleFeatures.slice(0, 5)}
variant="emphasis"
title="KEY FEATURES"
columns={5}
/>
<CodeLabel text='variant="emphasis" — header uses emphasis color' />
</DemoSection>
{/* Mobile Filter Overlay */}
<DemoSection
title="DTMobileFilterOverlay"
variant="warning"
description="Full-screen slide-up overlay for mobile filter menus."
>
<DTButton
variant="normal"
onPress={() => setOverlayVisible(true)}
>
OPEN FILTER OVERLAY
</DTButton>
<CodeLabel text="<DTMobileFilterOverlay visible onDismiss={...}>" />
<DTMobileFilterOverlay
visible={overlayVisible}
onDismiss={() => setOverlayVisible(false)}
heading="FILTERS"
activeFilterCount={3}
onClearAll={() => setOverlayVisible(false)}
variant="normal"
>
<DTAccordion
sections={[
{
key: 'chip-type',
title: 'CHIP TYPE',
children: (
<View style={{ gap: 8 }}>
<Text style={{ color: theme.colors.onSurface }}>NTAG215</Text>
<Text style={{ color: theme.colors.onSurface }}>NTAG216</Text>
<Text style={{ color: theme.colors.onSurface }}>DESFire EV2</Text>
</View>
),
},
{
key: 'frequency',
title: 'FREQUENCY',
children: (
<View style={{ gap: 8 }}>
<Text style={{ color: theme.colors.onSurface }}>13.56 MHz (HF)</Text>
<Text style={{ color: theme.colors.onSurface }}>125 kHz (LF)</Text>
</View>
),
},
]}
/>
</DTMobileFilterOverlay>
</DemoSection>
</ScreenContainer>
);
}

View File

@@ -56,6 +56,27 @@ const categories: {
route: 'Overlays',
count: 5,
},
{
title: 'ADVANCED CARDS',
subtitle: 'Selected state, progress bar, badge overlays, stagger',
mode: 'emphasis',
route: 'CardsAdvanced',
count: 4,
},
{
title: 'ANIMATIONS',
subtitle: 'DTStaggerContainer, useScaleIn, usePulse',
mode: 'success',
route: 'Animations',
count: 3,
},
{
title: 'FILTERS & FEATURES',
subtitle: 'DTFeatureLegend, DTMobileFilterOverlay',
mode: 'other',
route: 'Filters',
count: 2,
},
{
title: 'THEME REFERENCE',
subtitle: 'Colors, Typography, Spacing',