Files
whynot-design/examples/whynot-control/Screens.jsx
tegwick 9419f166ce
Some checks failed
ci / check (push) Has been cancelled
ci / release (push) Has been cancelled
Seeded claude design
2026-05-23 16:34:14 +02:00

283 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: '140px 1fr 100px',
gap: 20,
padding: '14px 4px',
borderBottom: '1px solid var(--border-soft)',
alignItems: 'flex-start',
}}>
<span style={{ font: '400 11px var(--ff-mono)', color: 'var(--fg-3)', paddingTop: 2 }}>{item.ts}</span>
<p style={{ margin: 0, font: '400 14px/1.5 var(--ff-sans)', color: 'var(--fg-1)' }}>{item.text}</p>
<span style={{ font: '500 10px/1 var(--ff-mono)', letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--fg-3)', textAlign: 'right', paddingTop: 4 }}>{item.from}</span>
</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)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-2)',
padding: '20px 22px',
display: 'flex',
flexDirection: 'column',
gap: 10,
position: 'relative',
cursor: 'pointer',
transition: 'border-color 120ms ease',
borderColor: hover ? 'var(--ink)' : 'var(--border)',
}}>
{hover && <span style={{ position: 'absolute', left: -1, top: -1, bottom: -1, width: 2, background: 'var(--ink)', borderRadius: '2px 0 0 2px' }}></span>}
<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: '500 17px/1.35 var(--ff-sans)', margin: '4px 0 8px', color: 'var(--fg-1)' }}>{p.pitch}</h3>
<div style={{ display: 'grid', gridTemplateColumns: '110px 1fr', gap: '6px 12px', fontSize: 13, color: 'var(--fg-1)' }}>
<span style={{ font: '500 11px/1.5 var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Learning q.</span>
<span style={{ lineHeight: 1.45 }}>{p.learning}</span>
<span style={{ font: '500 11px/1.5 var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>Smallest test</span>
<span style={{ lineHeight: 1.45 }}>{p.test}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', paddingTop: 12, marginTop: 4, borderTop: '1px solid var(--border-soft)', font: '500 11px var(--ff-mono)', letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--fg-3)' }}>
<span> {p.target}</span>
<span>{p.signal} signal</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: 8, marginBottom: 24, 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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
{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>}
/>
<table style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<thead>
<tr>
<th style={{ width: 80 }}>ID</th>
<th style={{ width: 90 }}>Prototype</th>
<th style={{ width: 72 }}>Level</th>
<th>What happened</th>
<th style={{ width: 110 }}>Source</th>
<th style={{ width: 90 }}>Date</th>
</tr>
</thead>
<tbody>
{SIGNALS.map(s => (
<tr key={s.id}>
<td><code className="mono">{s.id}</code></td>
<td><code className="mono">{s.proto}</code></td>
<td><StageDot level={s.level} /></td>
<td style={{ color: 'var(--fg-1)', fontSize: 13, lineHeight: 1.5 }}>{s.what}</td>
<td style={{ font: '400 12px var(--ff-mono)', color: 'var(--fg-2)' }}>{s.source}</td>
<td style={{ font: '400 12px var(--ff-mono)', color: 'var(--fg-3)' }}>{s.date}</td>
</tr>
))}
</tbody>
</table>
</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 });