Files
whynot-design/designbook/ui_kits/whynot-control/Chrome.jsx
tegwick 0d688ca94a feat(designbook): technology-neutral IR + stack-adapter pipeline (WHYNOT-WP-0002 T01-T06)
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>
2026-06-24 12:36:24 +02:00

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 });