283 lines
13 KiB
JavaScript
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 });
|