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:
@@ -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"
|
||||
|
||||
6
packages/showcase/desktop/postcss.config.mjs
Normal file
6
packages/showcase/desktop/postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
100
packages/showcase/desktop/src/renderer/App.tsx
Normal file
100
packages/showcase/desktop/src/renderer/App.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
6
packages/showcase/desktop/src/renderer/main.tsx
Normal file
6
packages/showcase/desktop/src/renderer/main.tsx
Normal 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 />);
|
||||
150
packages/showcase/desktop/src/renderer/pages/AnimationsPage.tsx
Normal file
150
packages/showcase/desktop/src/renderer/pages/AnimationsPage.tsx
Normal 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,
|
||||
}}>
|
||||
▼
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
103
packages/showcase/desktop/src/renderer/pages/BevelsPage.tsx
Normal file
103
packages/showcase/desktop/src/renderer/pages/BevelsPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
122
packages/showcase/desktop/src/renderer/pages/FormsPage.tsx
Normal file
122
packages/showcase/desktop/src/renderer/pages/FormsPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
packages/showcase/desktop/src/renderer/pages/GlowsPage.tsx
Normal file
81
packages/showcase/desktop/src/renderer/pages/GlowsPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
103
packages/showcase/desktop/src/renderer/pages/HomePage.tsx
Normal file
103
packages/showcase/desktop/src/renderer/pages/HomePage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
99
packages/showcase/desktop/src/renderer/pages/TokensPage.tsx
Normal file
99
packages/showcase/desktop/src/renderer/pages/TokensPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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">'
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
10
packages/showcase/desktop/tailwind.config.js
Normal file
10
packages/showcase/desktop/tailwind.config.js
Normal 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,
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
"outDir": "dist/renderer",
|
||||
"rootDir": "src/renderer",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/renderer"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
packages/showcase/mobile/.expo/README.md
Normal file
13
packages/showcase/mobile/.expo/README.md
Normal 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.
|
||||
3
packages/showcase/mobile/.expo/devices.json
Normal file
3
packages/showcase/mobile/.expo/devices.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"devices": []
|
||||
}
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
90
packages/showcase/mobile/src/screens/AnimationsScreen.tsx
Normal file
90
packages/showcase/mobile/src/screens/AnimationsScreen.tsx
Normal 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({ duration: 600 })
|
||||
</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>
|
||||
);
|
||||
}
|
||||
111
packages/showcase/mobile/src/screens/CardsAdvancedScreen.tsx
Normal file
111
packages/showcase/mobile/src/screens/CardsAdvancedScreen.tsx
Normal 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 (0–1)."
|
||||
>
|
||||
{[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={{val}}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
116
packages/showcase/mobile/src/screens/FiltersScreen.tsx
Normal file
116
packages/showcase/mobile/src/screens/FiltersScreen.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user