Files
commitea/docs/design/ui_kits/app/CaptureScreen.js.txt
Christian LeDoux 7a5cacc54c Scaffold CommiTea: yarn workspaces, Electron shell, core label schema, design system
- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens
  (preflight off; tokens/base.css owns the reset); boots to a Reginald
  placeholder proving fonts/tokens/core wiring
- packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard)
  with pessimistic conflict resolution + 15 unit tests
- docs/design: full design handoff (tokens, 16 component contracts,
  interactive 14-screen prototype, Reginald voice rules)
- docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic
  scheduler + Monte Carlo, directive log)
- Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local
  store is a rebuildable cache over gitea REST/webhooks (better-sqlite3
  in main process, arriving in P1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:42:46 -04:00

186 lines
9.4 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Capture interview — braindump → interview → approved ticket set (< 2 min)
function CaptureScreen({ onDone }) {
const DS = window.CommiTeaDesignSystem_20e63b;
const { Button, Card, Tag, Badge, Select, Icon } = DS;
const [stage, setStage] = React.useState('dump'); // dump | interview | review | filed
const [dump, setDump] = React.useState(
'auth is flaky \u2014 token refresh dies silently, sometimes session storage goes stale. ' +
'also the webhook debounce thing keeps double-firing. and we owe docs for auth setup'
);
const [qi, setQi] = React.useState(0);
const [log, setLog] = React.useState([]);
const [split, setSplit] = React.useState(null);
const [webEst, setWebEst] = React.useState(null);
const [secs, setSecs] = React.useState(0);
const running = stage === 'interview' || stage === 'review';
React.useEffect(() => {
if (!running) return;
const t = setInterval(() => setSecs((s) => s + 1), 1000);
return () => clearInterval(t);
}, [running]);
const clock = `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`;
const QUESTIONS = [
{ q: 'The auth work \u2014 one ticket, or shall I split token refresh from session storage? They fail differently.',
chips: ['One ticket', 'Split them'], set: (a) => setSplit(a === 'Split them') },
{ q: 'The webhook double-fire \u2014 how long? I should mention your "quick" has averaged two days.',
chips: ['est/1d', 'est/2d', 'est/3d'], set: (a) => setWebEst(a) },
{ q: 'Milestone Beta, I presume? It has room, provided the auth work stays under four days.',
chips: ['Beta', 'New milestone'], set: () => {} },
];
const answer = (a) => {
QUESTIONS[qi].set(a);
setLog((l) => [...l, { q: QUESTIONS[qi].q, a }]);
if (qi + 1 < QUESTIONS.length) setQi(qi + 1);
else setStage('review');
};
// draft tickets build as the interview progresses
const tickets = [];
if (split === true) {
tickets.push({ title: 'Token refresh: retry with backoff', est: 'est/2d', p: 'p/2' });
tickets.push({ title: 'Session storage: stale reads on wake', est: 'est/1d', p: 'p/3' });
} else if (split === false) {
tickets.push({ title: 'Auth: token refresh + session storage', est: 'est/3d', p: 'p/2' });
}
if (webEst) tickets.push({ title: 'Webhook debounce: double-fire guard', est: webEst, p: 'p/1', dep: 'blocked by auth work' });
if (stage === 'review' || stage === 'filed') {
tickets.push({ title: 'Docs: auth setup guide', est: 'est/1d', p: 'p/4', byReginald: true });
}
const totalDays = tickets.reduce((n, t) => n + parseInt(t.est.replace('est/', '')), 0);
const estOptions = ['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d'].map((v) => ({ value: v, label: v }));
const pOptions = ['p/1', 'p/2', 'p/3', 'p/4'].map((v) => ({ value: v, label: v }));
const Tray = ({ editable }) => (
<Card overline="The tray" title={tickets.length ? `${tickets.length} draft${tickets.length > 1 ? 's' : ''}` : 'Empty, for now'} flush>
<div>
{tickets.length === 0 ? (
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-3)', margin: 0, padding: '14px 20px 18px' }}>
Tickets appear here as we talk.
</p>
) : tickets.map((t, i) => (
<div key={t.title} style={{
display: 'flex', flexDirection: 'column', gap: 8,
padding: '12px 20px', borderTop: i === 0 ? 'none' : '1px solid var(--line-1)',
}}>
<div style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)' }}>{t.title}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
{editable ? (
<>
<Select options={estOptions} defaultValue={t.est} style={{ width: 96 }} />
<Select options={pOptions} defaultValue={t.p} style={{ width: 76 }} />
</>
) : (
<>
<Tag label={t.est} />
<Tag label={t.p} />
</>
)}
{t.dep ? <span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="network" size={11} />{t.dep}</span> : null}
{t.byReginald ? <Badge tone="jade">added by Reginald</Badge> : null}
</div>
</div>
))}
</div>
</Card>
);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
<div>
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Capture</h1>
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0', whiteSpace: 'nowrap' }}>braindump → approved tickets</p>
</div>
{stage !== 'dump' ? (
<div style={{ textAlign: 'right' }}>
<div style={{ font: '400 24px/1 var(--font-serif-display)', color: secs > 120 ? 'var(--warn)' : 'var(--ink-1)' }}>{clock}</div>
<div style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', marginTop: 4 }}>budget 2:00</div>
</div>
) : null}
</header>
{stage === 'dump' ? (
<Card overline="Braindump" title="Tell me what you're planning" jade>
<textarea
value={dump}
onChange={(e) => setDump(e.target.value)}
rows={5}
style={{
width: '100%', resize: 'vertical', font: 'var(--text-body)', color: 'var(--ink-1)',
background: 'var(--paper-0)', border: '1px solid var(--line-2)', borderRadius: 'var(--radius-2)',
padding: '10px 12px', outline: 'none', lineHeight: 1.55,
}}
></textarea>
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 14px' }}>
Sentences, fragments, grievances — all welcome. I'll sort it into tickets and only ask what I can't infer.
</p>
<Button icon="sparkles" onClick={() => setStage('interview')}>Brew tickets</Button>
</Card>
) : null}
{stage === 'interview' ? (
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 14, alignItems: 'start' }}>
<Card overline={`Interview \u00b7 ${qi + 1} of ${QUESTIONS.length}`} jade>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{log.map((e, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 6, opacity: 0.66 }}>
<span style={{ font: 'var(--text-agent)', color: 'var(--ink-2)' }}>{e.q}</span>
<span style={{ alignSelf: 'flex-start', font: 'var(--text-small)', background: 'var(--paper-2)', borderRadius: 'var(--radius-round)', padding: '4px 11px' }}>{e.a}</span>
</div>
))}
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-1)', margin: 0 }}>{QUESTIONS[qi].q}</p>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{QUESTIONS[qi].chips.map((c) => (
<Button key={c} variant="secondary" size="sm" onClick={() => answer(c)}>{c}</Button>
))}
</div>
</div>
</Card>
<Tray />
</div>
) : null}
{stage === 'review' ? (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.1fr', gap: 14, alignItems: 'start' }}>
<Card overline="Consequence" title={`${tickets.length} tickets \u00b7 ~${totalDays}d of work`} jade
footer={<>
<Button onClick={() => setStage('filed')}>Approve all</Button>
<Button variant="ghost" onClick={() => { setStage('dump'); setQi(0); setLog([]); setSplit(null); setWebEst(null); setSecs(0); }}>Discard</Button>
</>}>
<p style={{ font: 'var(--text-body)', margin: '0 0 8px' }}>
Beta's 80% window moves <span style={{ font: 'var(--text-data)' }}>Mar 312 → Mar 514</span>. Capacity absorbs the rest.
</p>
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
I added the docs ticket you mentioned and wired the dependency. Shall I make it so?
</p>
</Card>
<Tray editable />
</div>
) : null}
{stage === 'filed' ? (
<Card jade style={{ maxWidth: 560 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'flex-start' }}>
<span style={{ color: 'var(--ok)', display: 'inline-flex', alignItems: 'center', gap: 8, font: 'var(--text-title)' }}>
<Icon name="circle-check" size={22} /> Filed
</span>
<p style={{ font: 'var(--text-body)', margin: 0 }}>
{tickets.length} issues opened in gitea with <span style={{ font: 'var(--text-data)' }}>est/*</span> and <span style={{ font: 'var(--text-data)' }}>p/*</span> labels — nothing else touched.
</p>
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
Elapsed {clock} — under budget. No bot comments, no synthetic issues; your repo remains yours.
</p>
<Button iconRight="arrow-right" onClick={onDone}>To morning service</Button>
</div>
</Card>
) : null}
</div>
);
}
Object.assign(window, { CaptureScreen });