Files
whynot-design/designbook/ui_kits/whynot-control/Screens.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

275 lines
13 KiB
JavaScript

// =============================================================
// Screens — Inbox, PrototypesIndex, PrototypeDetail, SignalsIndex, DocView, BetasIndex, DecisionsIndex
// =============================================================
function Inbox({ onCapture }) {
const [draft, setDraft] = React.useState('');
return (
<div>
<PageHeader
eyebrow="whynot-control / inbox"
title="Inbox"
lede="Temporary capture for rough ideas, weird observations, user comments, market hints, and product fragments. Capture is not commitment."
/>
<div style={{
border: '1px solid var(--border)',
borderRadius: 'var(--r-2)',
padding: 16,
background: 'var(--paper)',
marginBottom: 28,
display: 'flex', flexDirection: 'column', gap: 10,
}}>
<Eyebrow>Capture</Eyebrow>
<textarea
value={draft}
onChange={e => setDraft(e.target.value)}
placeholder="An idea, an observation, a fragment. No filter, no judgement, no commitment."
style={{
font: '400 14px/1.5 var(--ff-sans)',
border: 'none', outline: 'none', resize: 'none',
minHeight: 64, padding: 0, background: 'transparent', color: 'var(--fg-1)',
}}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ font: '400 11px var(--ff-mono)', color: 'var(--fg-3)', marginRight: 'auto' }}>
to capture · stored in <code className="mono">inbox/</code>
</span>
<Button variant="ghost" onClick={() => setDraft('')}>Discard</Button>
<Button variant="primary" icon="inbox" onClick={() => { onCapture && onCapture(draft); setDraft(''); }}>Capture</Button>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
<Eyebrow>Recent · 7</Eyebrow>
<div style={{ flex: 1, borderTop: '1px solid var(--border-soft)' }}></div>
<span style={{ font: '400 11px var(--ff-mono)', color: 'var(--fg-3)' }}> newest first</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{INBOX.map(item => (
<div key={item.id} style={{
display: 'grid',
gridTemplateColumns: '120px 1fr',
gap: '4px 24px',
padding: '20px 0',
borderBottom: '1px solid var(--border-soft)',
alignItems: 'baseline',
}}>
<span style={{ font: '400 11px var(--ff-mono)', color: 'var(--fg-3)' }}>{item.ts}</span>
<span style={{ font: '500 10px/1 var(--ff-mono)', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>{item.from}</span>
<p style={{ gridColumn: '1 / -1', margin: '4px 0 0', font: '400 15px/1.6 var(--ff-sans)', color: 'var(--fg-1)', maxWidth: '60ch' }}>{item.text}</p>
</div>
))}
</div>
</div>
);
}
function PrototypeListCard({ p, onOpen }) {
const [hover, setHover] = React.useState(false);
return (
<article
onClick={() => onOpen(p.id)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
background: 'var(--paper)',
borderTop: '1px solid var(--border-soft)',
padding: '24px 0 28px',
display: 'flex',
flexDirection: 'column',
gap: 12,
position: 'relative',
cursor: 'pointer',
transition: 'background 120ms ease',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<Eyebrow style={{ color: hover ? 'var(--fg-1)' : 'var(--fg-3)' }}>{p.id} · Prototype</Eyebrow>
<StageDot level={p.signal} label={p.stageLabel} />
</div>
<h3 style={{ font: '400 22px/1.3 var(--ff-sans)', letterSpacing: '-0.01em', margin: '2px 0 8px', color: 'var(--fg-1)', maxWidth: '52ch' }}>{p.pitch}</h3>
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr', gap: '10px 16px', fontSize: 14, color: 'var(--fg-2)', maxWidth: '60ch' }}>
<span style={{ font: '500 11px/1.7 var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Learning q.</span>
<span style={{ lineHeight: 1.55 }}>{p.learning}</span>
<span style={{ font: '500 11px/1.7 var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Smallest test</span>
<span style={{ lineHeight: 1.55 }}>{p.test}</span>
</div>
<div style={{ display: 'flex', gap: 24, marginTop: 4, font: '500 11px var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>
<span> {p.target}</span>
<span>{p.signal} signal</span>
<span style={{ marginLeft: 'auto', color: hover ? 'var(--fg-1)' : 'var(--fg-3)' }}>Open </span>
</div>
</article>
);
}
function PrototypesIndex({ onOpen }) {
const [filter, setFilter] = React.useState('All');
const filters = ['All', 'Experiment', 'Signal review', 'Parked'];
const list = filter === 'All' ? PROTOTYPES : PROTOTYPES.filter(p => p.stageLabel === filter);
return (
<div>
<PageHeader
eyebrow="whynot-control / prototypes"
title="Prototypes"
lede="Structured prototype cards. A prototype card defines a learning question and the smallest useful test."
actions={<Button variant="primary" icon="plus">New prototype</Button>}
/>
<div style={{ display: 'flex', gap: 10, marginBottom: 8, alignItems: 'center' }}>
{filters.map(f => (
<Tag key={f} active={filter === f} style={{ cursor: 'pointer' }} >
<span onClick={() => setFilter(f)}>{f}</span>
</Tag>
))}
<span style={{ marginLeft: 'auto', font: '400 11px var(--ff-mono)', color: 'var(--fg-3)' }}>{list.length} of {PROTOTYPES.length}</span>
</div>
<div>
{list.map(p => <PrototypeListCard key={p.id} p={p} onOpen={onOpen} />)}
</div>
</div>
);
}
function PrototypeDetail({ id, onBack }) {
const p = PROTOTYPES.find(p => p.id === id) || PROTOTYPES[0];
const stageIdx = { 'parked': 0, 'experiment': 3, 'signal': 4, 'experiment-active': 3 }[p.stage] ?? 3;
return (
<div>
<a onClick={onBack} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, font: '400 12px var(--ff-mono)', color: 'var(--fg-2)', textDecoration: 'none', marginBottom: 18, cursor: 'pointer' }}>
<Icon name="arrow-left" size={14} /> Back to prototypes
</a>
<PageHeader
eyebrow={`${p.id} · Prototype`}
title={p.pitch}
actions={
<React.Fragment>
<Button variant="secondary" icon="archive">Park</Button>
<Button variant="primary" icon="arrow-right">Promote {p.target}</Button>
</React.Fragment>
}
/>
<PipelineStrip activeIdx={stageIdx} />
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 32 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
<Field label="Learning question" value={p.learning} />
<Field label="Smallest useful test" value={p.test} />
<Field label="Expected signal" value="At least one person asks for a concrete next step, gives specific use-case feedback, or identifies a realistic context where the idea would matter." />
<Field label="Risks" value={p.risks} />
</div>
<aside style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<SidebarField label="Stage" value={<Tag active>{p.stageLabel}</Tag>} />
<SidebarField label="Signal" value={<StageDot level={p.signal} />} />
<SidebarField label="Target" value={<code className="mono"> {p.target}</code>} />
<SidebarField label="Audience" value="Potential early users, collaborators, or customers." />
<SidebarField label="Agentic suitability" value="Agents may help turn rough notes into a sharper prototype card." />
<div style={{ marginTop: 6, border: '1px dashed var(--border-strong)', borderRadius: 4, padding: 14 }}>
<Eyebrow style={{ display: 'block', marginBottom: 8 }}>Caveat</Eyebrow>
<p style={{ margin: 0, font: '400 13px/1.55 var(--ff-sans)', color: 'var(--fg-2)' }}>
A prototype can be interesting and still be parked. <code className="mono">whynot</code> exists to reduce uncertainty, not create more obligations.
</p>
</div>
</aside>
</div>
</div>
);
}
function Field({ label, value }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<Eyebrow>{label}</Eyebrow>
<p style={{ margin: 0, font: '400 15px/1.55 var(--ff-sans)', color: 'var(--fg-1)' }}>{value}</p>
</div>
);
}
function SidebarField({ label, value }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<Eyebrow>{label}</Eyebrow>
<div style={{ font: '400 13px/1.5 var(--ff-sans)', color: 'var(--fg-1)' }}>{value}</div>
</div>
);
}
function SignalsIndex() {
return (
<div>
<PageHeader
eyebrow="whynot-control / signals"
title="Signals"
lede="Market-signal and feedback records. A signal is evidence. Record what happened, who did it, and how strong the evidence is."
actions={<Button variant="primary" icon="plus">Record signal</Button>}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{SIGNALS.map(s => (
<div key={s.id} style={{
display: 'grid',
gridTemplateColumns: '90px 90px 1fr',
gap: '6px 24px',
padding: '20px 0',
borderBottom: '1px solid var(--border-soft)',
alignItems: 'baseline',
}}>
<code className="mono" style={{ background: 'none', padding: 0, color: 'var(--fg-3)', font: '400 11px var(--ff-mono)' }}>{s.id}</code>
<code className="mono" style={{ background: 'none', padding: 0, color: 'var(--fg-2)', font: '400 11px var(--ff-mono)' }}>{s.proto}</code>
<StageDot level={s.level} />
<p style={{ gridColumn: '1 / -1', margin: '4px 0 0', font: '400 14px/1.55 var(--ff-sans)', color: 'var(--fg-1)', maxWidth: '60ch' }}>{s.what}</p>
<span style={{ gridColumn: '1 / -1', font: '400 11px var(--ff-mono)', color: 'var(--fg-3)' }}>{s.source} · {s.date}</span>
</div>
))}
</div>
</div>
);
}
function BetasIndex() {
return (
<div>
<PageHeader
eyebrow="whynot-control / betas"
title="Betas"
lede="Closed beta plans and beta review notes. A beta should have a clear learning question, entry criteria, and exit outcome."
/>
<div style={{
border: '1px dashed var(--border-strong)',
padding: 32,
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
textAlign: 'center',
color: 'var(--fg-3)',
borderRadius: 4,
}}>
<Icon name="users" size={20} />
<div style={{ font: '500 14px var(--ff-sans)', color: 'var(--fg-2)' }}>One beta plan in draft.</div>
<div style={{ font: '400 12px var(--ff-mono)', color: 'var(--fg-3)' }}>WNO-021 · Concierge triage · pending Binky approval</div>
<a href="#" style={{ font: '500 12px var(--ff-mono)', color: 'var(--fg-1)', marginTop: 4 }}>Open draft </a>
</div>
</div>
);
}
function DecisionsIndex() {
const decisions = [
{ id: 'DEC-001', title: 'Shorten organisation name from whywhynot to whynot.', status: 'Accepted', date: '2026-01-08' },
{ id: 'DEC-002', title: 'Maintain A1 Incubating until first prototype candidates review.', status: 'Open', date: '—' },
{ id: 'DEC-003', title: 'Initial promotion targets: Helix, Coulomb, Sloppers, Plenitude, Binky, Tegwick.', status: 'Open', date: '—' },
];
return (
<div>
<PageHeader eyebrow="whynot-control / decisions" title="Decisions" lede="A promotion record is required before any prototype moves to Helix, Coulomb, Sloppers, Plenitude, Binky, or Tegwick." />
<div style={{ display: 'flex', flexDirection: 'column' }}>
{decisions.map(d => (
<div key={d.id} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 130px 100px', gap: 20, alignItems: 'baseline', padding: '16px 4px', borderBottom: '1px solid var(--border-soft)' }}>
<code className="mono" style={{ background: 'none', padding: 0, color: 'var(--fg-1)' }}>{d.id}</code>
<span style={{ font: '500 15px var(--ff-sans)', color: 'var(--fg-1)' }}>{d.title}</span>
<Tag active={d.status === 'Accepted'} draft={d.status === 'Open'}>{d.status}</Tag>
<span style={{ font: '400 12px var(--ff-mono)', color: 'var(--fg-3)', textAlign: 'right' }}>{d.date}</span>
</div>
))}
</div>
</div>
);
}
Object.assign(window, { Inbox, PrototypesIndex, PrototypeDetail, SignalsIndex, BetasIndex, DecisionsIndex, Field, SidebarField, PrototypeListCard });