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>
This commit is contained in:
Christian LeDoux
2026-07-07 20:42:46 -04:00
commit 7a5cacc54c
268 changed files with 15766 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
// Onboarding / first connect — welcome → connect gitea → choose repo → bootstrap
function OnboardingScreen({ onDone }) {
const DS = window.CommiTeaDesignSystem_20e63b;
const { Button, Input, Radio, Tag, Badge, Icon } = DS;
const [step, setStep] = React.useState(0);
const [conn, setConn] = React.useState('idle'); // idle | testing | ok
const [repo, setRepo] = React.useState('stephen/commitea');
const [boot, setBoot] = React.useState(-1); // -1 idle, 0..2 running, 3 done
React.useEffect(() => {
if (conn !== 'testing') return;
const t = setTimeout(() => setConn('ok'), 1100);
return () => clearTimeout(t);
}, [conn]);
React.useEffect(() => {
if (boot < 0 || boot >= 3) return;
const t = setTimeout(() => setBoot(boot + 1), 700);
return () => clearTimeout(t);
}, [boot]);
const STEPS = ['Welcome', 'Connect', 'Repo', 'Bootstrap'];
const BOOT_TASKS = [
'Create stephen/pm-state (the sidecar)',
'Apply the label schema to stephen/commitea',
'Install a webhook · endpoint :48731',
];
const Frame = ({ children, footer }) => (
<div style={{
background: 'var(--surface-card)', border: '1px solid var(--line-1)', borderRadius: 'var(--radius-3)',
boxShadow: 'var(--shadow-jade-line), var(--shadow-2)', padding: '30px 36px',
display: 'flex', flexDirection: 'column', gap: 16, width: '100%',
}}>
{children}
{footer ? <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', borderTop: '1px solid var(--line-1)', paddingTop: 16 }}>{footer}</div> : null}
</div>
);
return (
<div style={{
minHeight: '100vh', background: 'var(--surface-app)', display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', padding: 32, gap: 22,
}} data-screen-label="onboarding">
{/* brand */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<img src="../../assets/logo-icon.png" width="34" height="34" alt="" style={{ borderRadius: 8, display: 'block' }} />
<span style={{ font: '400 27px/1 var(--font-serif-display)', color: 'var(--ink-1)' }}>
Commi<span style={{ color: 'var(--accent-text)' }}>Tea</span>
</span>
</div>
{/* stepper */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
{STEPS.map((s, i) => (
<div key={s} style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
<span style={{
width: 20, height: 20, borderRadius: '50%', textAlign: 'center',
font: '500 10.5px/20px var(--font-mono)',
background: i < step ? 'var(--accent)' : i === step ? 'var(--spruce-2)' : 'var(--paper-2)',
color: i < step ? 'var(--ink-inverse)' : i === step ? 'var(--accent-text)' : 'var(--ink-3)',
}}>{i < step ? '\u2713' : i + 1}</span>
<span style={{ font: `${i === step ? 600 : 400} 12px/1 var(--font-sans)`, color: i === step ? 'var(--ink-1)' : 'var(--ink-3)' }}>{s}</span>
</span>
{i < STEPS.length - 1 ? <span style={{ width: 24, height: 1, background: 'var(--line-2)' }}></span> : null}
</div>
))}
</div>
<div style={{ width: 'min(540px, 100%)' }}>
{step === 0 ? (
<Frame footer={<Button iconRight="arrow-right" onClick={() => setStep(1)}>Begin</Button>}>
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>Good morning.</h1>
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-2)', margin: 0 }}>
I'm Reginald, your project manager. I interview you instead of making you fill in forms,
I forecast in honest ranges, and I never do the arithmetic myself — there's a scheduler for that.
</p>
<p style={{ font: 'var(--text-body)', color: 'var(--ink-2)', margin: 0 }}>
Your plans live in your own Gitea as ordinary issues and labels. Delete me and nothing human is lost.
</p>
</Frame>
) : null}
{step === 1 ? (
<Frame footer={<>
<Button variant="ghost" onClick={() => setStep(0)}>Back</Button>
<Button iconRight="arrow-right" disabled={conn !== 'ok'} onClick={() => setStep(2)}>Continue</Button>
</>}>
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>Your Gitea</h1>
<Input label="Base URL" icon="link" mono defaultValue="https://gitea.stephenmann.io" />
<Input label="Access token" icon="keyboard" mono type="password" defaultValue="ct_9f2e81c4a7d6" hint="Scopes: repo, issue. Nothing more." />
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Button variant="secondary" icon={conn === 'testing' ? 'loader-circle' : 'zap'} disabled={conn === 'testing'} onClick={() => setConn('testing')}>
{conn === 'testing' ? 'Ringing the bell…' : 'Test connection'}
</Button>
{conn === 'ok' ? <Badge tone="ok" dot>connected · 3 repos visible</Badge> : null}
</div>
</Frame>
) : null}
{step === 2 ? (
<Frame footer={<>
<Button variant="ghost" onClick={() => setStep(1)}>Back</Button>
<Button iconRight="arrow-right" onClick={() => setStep(3)}>Continue</Button>
</>}>
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>Which repo shall I manage?</h1>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{['stephen/commitea', 'stephen/novelpad', 'stephen/infra'].map((r) => (
<label key={r} style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', cursor: 'pointer',
background: repo === r ? 'var(--spruce-1)' : 'var(--paper-0)',
border: `1px solid ${repo === r ? 'var(--spruce-3)' : 'var(--line-1)'}`,
borderRadius: 'var(--radius-2)',
}}>
<Radio name="repo" checked={repo === r} onChange={() => setRepo(r)} />
<Icon name="git-branch" size={14} style={{ color: 'var(--ink-3)' }} />
<span style={{ font: 'var(--text-data)', color: 'var(--ink-1)' }}>{r}</span>
</label>
))}
</div>
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>One to start. You can add more later in Settings.</p>
</Frame>
) : null}
{step === 3 ? (
<Frame footer={boot === 3 ? <>
<Button variant="ghost" onClick={() => onDone('focus')}>Just look around</Button>
<Button icon="sparkles" onClick={() => onDone('capture')}>Start a capture</Button>
</> : <>
<Button variant="ghost" onClick={() => setStep(2)} disabled={boot >= 0}>Back</Button>
<Button disabled={boot >= 0} onClick={() => setBoot(0)}>Make it so</Button>
</>}>
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>
{boot === 3 ? 'All set.' : 'With your approval'}
</h1>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{BOOT_TASKS.map((t, i) => (
<div key={t} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ display: 'inline-flex', color: boot > i ? 'var(--ok)' : boot === i ? 'var(--warn)' : 'var(--ink-3)' }}>
<Icon name={boot > i ? 'circle-check' : boot === i ? 'loader-circle' : 'circle-dashed'} size={15} />
</span>
<span style={{ font: 'var(--text-body)', color: boot > i ? 'var(--ink-1)' : 'var(--ink-2)', whiteSpace: 'nowrap' }}>{t}</span>
</div>
))}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, paddingLeft: 25 }}>
{['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d', 'p/1', 'p/2', 'p/3', 'p/4', 'deadline/hard'].map((l) => <Tag key={l} label={l} />)}
</div>
</div>
{boot === 3 ? (
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
The pot is empty. Tell me what you're planning and I'll draw up the tickets.
</p>
) : (
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
No bot comments, no body frontmatter, no synthetic issues — ever. Labels are the only footprint.
</p>
)}
</Frame>
) : null}
</div>
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>first run · everything reversible</span>
</div>
);
}
Object.assign(window, { OnboardingScreen });