Author the design language once in the canonical React designbook and project it
one-way onto each stack: React -> designbook/ -> ir/ -> adapters/<stack>/.
Phase 0 — contracts & governance (T01-T03):
- ir/SCHEMA.md + ir/schema/{component,tokens}.schema.json — neutral IR contract
(W3C DTCG tokens; React prop -> HTML attribute mapping; non-portable props flagged).
- adapters/ADAPTER_CONTRACT.md — inputs, drift-report + parity-result shapes,
idempotency rules, CI exit codes (0 ok / 2 usage / 3 drift / 4 parity / 5 internal).
- .claude/rules/designbook-propagation.md + DesignSystemIntroduction.md §5.1 —
one-way directionality + drift-resolution workflow.
T04 — canonical React designbook + the missing pull tool:
- The bundled /design-sync skill only PUSHES repo->cloud; it cannot populate
designbook/. Added scripts/designbook_pull.py + `make designbook-pull`, which drives
the local claude binary headless (acceptEdits) so DesignSync fetch+write runs in a
subprocess (contents never hit the orchestrator's context). Pulled 44 files;
excludes the _whynot-design-seed/ self-copy. Corrected the docs that wrongly called
/design-sync the pull.
T05 — IR extractor (scripts/ir-extract.mjs + `make ir`):
- ir/tokens.json (80 tokens, DTCG, var() -> {ref} alias resolution); ir/components/*.json
(10 contracts parsed from .jsx signatures: enum/boolean/number inference, prop->attr
map, style/callback marked non-portable); ir/exemplars/*.
T06 — Lit token adapter (adapters/lit/ + `make adapt-lit`):
- Full-gen tokens into src/styles/colors_and_type.css :root (marker-bounded, idempotent
no-op on re-run; hand-authored type CSS preserved).
NOTE: token regen synced Lit to canonical React — fonts IBM Plex -> system stacks and 8
status tokens added. This is a VISUAL change: review and run `pnpm test:visual:update`
before merge. Remaining: T07 scaffold+drift, T08 parity, T09 runbook, T10 2nd-adapter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
164 lines
7.1 KiB
JavaScript
164 lines
7.1 KiB
JavaScript
// =============================================================
|
|
// Chrome — TopNav, Sidebar, PageHeader, PipelineStrip
|
|
// =============================================================
|
|
|
|
function TopNav({ onNew }) {
|
|
return (
|
|
<nav style={{
|
|
height: 56,
|
|
background: 'rgba(255,255,255,0.92)',
|
|
borderBottom: '1px solid var(--border)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 28,
|
|
padding: '0 24px',
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 10,
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<img src="../../assets/whynot-logo.png" alt="" style={{ width: 22, height: 22 }} />
|
|
<span style={{ font: '500 14px var(--ff-sans)' }}>whynot</span>
|
|
<span style={{ font: '400 12px var(--ff-mono)', color: 'var(--fg-3)', letterSpacing: '0.04em' }}>/ control</span>
|
|
</div>
|
|
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div style={{
|
|
font: '400 12px var(--ff-mono)',
|
|
color: 'var(--fg-3)',
|
|
border: '1px solid var(--border)',
|
|
padding: '6px 10px',
|
|
borderRadius: 'var(--r-1)',
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
minWidth: 240,
|
|
}}>
|
|
<Icon name="search" size={14} />
|
|
<span>Search ideas, prototypes, signals…</span>
|
|
<span style={{ marginLeft: 'auto', padding: '1px 5px', border: '1px solid var(--border)', borderRadius: 2, fontSize: 10 }}>⌘ K</span>
|
|
</div>
|
|
<Button variant="primary" icon="plus" onClick={onNew}>New idea</Button>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
const NAV_ITEMS = [
|
|
{ key: 'inbox', label: 'Inbox', icon: 'inbox', count: 7 },
|
|
{ key: 'prototypes', label: 'Prototypes', icon: 'flask-conical', count: 4 },
|
|
{ key: 'signals', label: 'Signals', icon: 'activity', count: 12 },
|
|
{ key: 'betas', label: 'Betas', icon: 'users', count: 1 },
|
|
{ key: 'decisions', label: 'Decisions', icon: 'check-square', count: 3 },
|
|
];
|
|
|
|
const DOC_ITEMS = [
|
|
{ key: 'intent', label: 'INTENT.md' },
|
|
{ key: 'scope', label: 'SCOPE.md' },
|
|
{ key: 'operating', label: 'OPERATING_MODEL.md' },
|
|
{ key: 'pipeline', label: 'PROTOTYPE_PIPELINE.md' },
|
|
{ key: 'agent', label: 'AGENT_RULES.md' },
|
|
];
|
|
|
|
function Sidebar({ current, onNav }) {
|
|
const itemStyle = (active) => ({
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
padding: '6px 10px',
|
|
color: active ? 'var(--fg-1)' : 'var(--fg-3)',
|
|
background: 'transparent',
|
|
borderLeft: active ? '2px solid var(--ink)' : '2px solid transparent',
|
|
paddingLeft: active ? 10 : 12,
|
|
font: active ? '500 13px var(--ff-sans)' : '400 13px var(--ff-sans)',
|
|
cursor: 'pointer',
|
|
textDecoration: 'none',
|
|
transition: 'color 120ms ease, border-color 120ms ease',
|
|
});
|
|
return (
|
|
<aside style={{
|
|
width: 200, flex: 'none',
|
|
background: 'transparent',
|
|
borderRight: 'none',
|
|
padding: '32px 0 32px 8px',
|
|
display: 'flex', flexDirection: 'column', gap: 32,
|
|
height: 'calc(100vh - 56px)',
|
|
position: 'sticky', top: 56,
|
|
overflowY: 'auto',
|
|
}}>
|
|
<div>
|
|
<Eyebrow style={{ paddingLeft: 12, marginBottom: 10, display: 'block', opacity: 0.7 }}>Work</Eyebrow>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
{NAV_ITEMS.map(item => (
|
|
<a key={item.key} onClick={() => onNav(item.key)} style={itemStyle(current === item.key)}>
|
|
<span>{item.label}</span>
|
|
<span style={{ marginLeft: 'auto', font: '400 11px var(--ff-mono)', color: 'var(--ink-5)' }}>{item.count}</span>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Eyebrow style={{ paddingLeft: 12, marginBottom: 10, display: 'block', opacity: 0.7 }}>Control docs</Eyebrow>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
{DOC_ITEMS.map(item => (
|
|
<a key={item.key} onClick={() => onNav('doc:' + item.key)} style={{ ...itemStyle(current === 'doc:' + item.key), font: current === 'doc:' + item.key ? '500 12px var(--ff-mono)' : '400 12px var(--ff-mono)' }}>
|
|
<span>{item.label}</span>
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div style={{ marginTop: 'auto', padding: '0 12px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span style={{ width: 5, height: 5, borderRadius: 999, background: 'var(--ink-4)' }}></span>
|
|
<span style={{ font: '400 11px var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>A1 · Incubating</span>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function PageHeader({ eyebrow, title, lede, actions }) {
|
|
return (
|
|
<header style={{ marginBottom: 48, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
|
|
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 24 }}>
|
|
<h1 style={{ font: '400 36px/1.1 var(--ff-sans)', letterSpacing: '-0.02em', margin: 0, flex: 1 }}>{title}</h1>
|
|
{actions && <div style={{ display: 'flex', gap: 8 }}>{actions}</div>}
|
|
</div>
|
|
{lede && <p style={{ font: '400 16px/1.6 var(--ff-sans)', color: 'var(--fg-2)', margin: '4px 0 0', maxWidth: '56ch' }}>{lede}</p>}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function PipelineStrip({ activeIdx = 3 }) {
|
|
const stages = [
|
|
{ num: 'Stage 0', name: 'Raw idea', meta: 'inbox/' },
|
|
{ num: 'Stage 1', name: 'Triage', meta: '2026-02-12' },
|
|
{ num: 'Stage 2', name: 'Prototype card', meta: 'prototypes/' },
|
|
{ num: 'Stage 3', name: 'Experiment', meta: 'ends 2026-04-01' },
|
|
{ num: 'Stage 4', name: 'Signal review', meta: '— pending' },
|
|
];
|
|
return (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 0, position: 'relative', margin: '0 0 32px' }}>
|
|
{stages.map((s, i) => {
|
|
const state = i < activeIdx ? 'done' : i === activeIdx ? 'active' : 'pending';
|
|
const topColor = state === 'done' ? 'var(--ink)' : state === 'active' ? 'var(--hi-2)' : 'var(--border)';
|
|
return (
|
|
<div key={i} style={{
|
|
padding: '10px 12px 14px',
|
|
borderTop: `2px solid ${topColor}`,
|
|
display: 'flex', flexDirection: 'column', gap: 4,
|
|
position: 'relative',
|
|
}}>
|
|
<span style={{ font: '500 10px/1 var(--ff-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', color: state === 'pending' ? 'var(--fg-3)' : 'var(--fg-1)' }}>{s.num}</span>
|
|
<span style={{ font: '500 14px/1.25 var(--ff-sans)', color: state === 'pending' ? 'var(--fg-3)' : 'var(--fg-1)' }}>{s.name}</span>
|
|
<span style={{ font: '400 11px/1.35 var(--ff-mono)', color: 'var(--fg-3)' }}>{s.meta}</span>
|
|
{i > 0 && (
|
|
<span style={{ position: 'absolute', top: -8, right: -7, font: '400 14px var(--ff-mono)', color: state === 'pending' ? 'var(--ink-5)' : 'var(--ink)' }}>→</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, { TopNav, Sidebar, PageHeader, PipelineStrip, NAV_ITEMS, DOC_ITEMS });
|