- 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>
173 lines
10 KiB
Plaintext
173 lines
10 KiB
Plaintext
// Issue detail — human intent (gitea) on the left, machine-derived (pm-state) on the right
|
|
function IssueScreen({ issue, onBack, onOpenIssue }) {
|
|
const DS = window.CommiTeaDesignSystem_20e63b;
|
|
const { Card, Tag, Badge, Button, Icon } = DS;
|
|
const det = window.CT_DATA.issueDetail[issue.id] || {
|
|
state: 'triage',
|
|
assignee: 'Stephen',
|
|
milestone: 'Beta',
|
|
body: '',
|
|
comments: [],
|
|
lifecycle: [
|
|
{ stage: 'Diagnosis', event: 'issue opened', when: 'Feb 4 · 10:20', icon: 'circle-dot', done: true },
|
|
{ stage: 'Triage', event: 'labeled · milestoned Beta', when: 'Feb 5 · 09:12', icon: 'tag', done: true },
|
|
{ stage: 'Work start', event: 'first branch or commit ref', when: 'pending', icon: 'git-commit-horizontal', done: false },
|
|
{ stage: 'Deploy', event: 'PR merged', when: 'pending', icon: 'git-merge', done: false },
|
|
{ stage: 'Complete', event: 'issue closed', when: 'pending', icon: 'circle-check', done: false },
|
|
],
|
|
forecast: { p80: 'starts wk of Feb 16', note: 'queue position from scheduler' },
|
|
blocks: [],
|
|
blockedBy: [],
|
|
note: null,
|
|
};
|
|
const stateBadge = {
|
|
steeping: { tone: 'warn', label: `steeping${issue.days || issue.steeping ? ' ' + (issue.days || issue.steeping) : ''}` },
|
|
triage: { tone: 'neutral', label: 'triage' },
|
|
review: { tone: 'info', label: 'in review' },
|
|
done: { tone: 'ok', label: 'done' },
|
|
}[det.state] || { tone: 'neutral', label: det.state };
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{/* breadcrumb + header */}
|
|
<div>
|
|
<button type="button" onClick={onBack} style={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 6, background: 'none', border: 'none',
|
|
font: '500 12.5px var(--font-sans)', color: 'var(--ink-2)', cursor: 'pointer', padding: '2px 0', marginBottom: 10,
|
|
}}>
|
|
<Icon name="arrow-left" size={14} /> Back
|
|
</button>
|
|
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14, display: 'flex', alignItems: 'flex-start', gap: 16 }}>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<p style={{ font: '400 13px var(--font-mono)', color: 'var(--ink-3)', margin: '0 0 6px' }}>#{issue.id} · stephen/commitea</p>
|
|
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>{issue.title}</h1>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
|
|
<Badge tone={stateBadge.tone} dot>{stateBadge.label}</Badge>
|
|
{(issue.labels || []).map((l) => <Tag key={l} label={l} />)}
|
|
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
|
<Icon name="milestone" size={12} />{det.milestone}
|
|
</span>
|
|
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
|
<Icon name="user" size={12} />{det.assignee}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Button variant="secondary" icon="arrow-up-right">Open in Gitea</Button>
|
|
</header>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: 16, alignItems: 'start' }}>
|
|
{/* left: human intent */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<Card overline="Description">
|
|
{det.body ? (
|
|
<p style={{ font: 'var(--text-body)', color: 'var(--ink-1)', margin: 0, lineHeight: 1.6 }}>{det.body}</p>
|
|
) : (
|
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-3)', margin: 0 }}>
|
|
No description was written. I have opinions about that, but I'll keep them warm.
|
|
</p>
|
|
)}
|
|
</Card>
|
|
|
|
<Card overline={`Comments · ${det.comments.length}`} flush>
|
|
<div>
|
|
{det.comments.map((c, i) => (
|
|
<div key={i} style={{ display: 'flex', gap: 12, padding: '14px 20px', borderBottom: '1px solid var(--line-1)' }}>
|
|
<span style={{
|
|
width: 24, height: 24, borderRadius: '50%', background: 'var(--spruce-2)', color: 'var(--accent-text)',
|
|
font: '600 9px/24px var(--font-sans)', textAlign: 'center', flexShrink: 0,
|
|
}}>{c.who.split(' ').map((w) => w[0]).join('')}</span>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
|
<span style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)', whiteSpace: 'nowrap' }}>{c.who}</span>
|
|
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', whiteSpace: 'nowrap' }}>{c.when}</span>
|
|
</div>
|
|
<p style={{ font: 'var(--text-body)', color: 'var(--ink-1)', margin: '4px 0 0' }}>{c.text}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<div style={{ display: 'flex', gap: 8, padding: '14px 20px', alignItems: 'flex-end' }}>
|
|
<textarea placeholder="Comment — this writes to gitea, as you" rows={2} style={{
|
|
flex: 1, resize: 'none', font: 'var(--text-body)', color: 'var(--ink-1)', lineHeight: 1.5,
|
|
background: 'var(--paper-0)', border: '1px solid var(--line-2)', borderRadius: 'var(--radius-2)',
|
|
padding: '8px 11px', outline: 'none',
|
|
}}></textarea>
|
|
<Button size="sm" variant="secondary">Comment</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{det.note ? (
|
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0, padding: '0 2px' }}>{det.note}</p>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* right: machine-derived sidecar */}
|
|
<Card overline="Machine-derived" flush>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
{/* lifecycle */}
|
|
<div style={{ padding: '14px 20px', display: 'flex', flexDirection: 'column', gap: 0 }}>
|
|
{det.lifecycle.map((s, i) => (
|
|
<div key={s.stage} style={{ display: 'flex', gap: 10 }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
<span style={{ display: 'inline-flex', color: s.done ? 'var(--ok)' : 'var(--ink-3)', padding: '2px 0' }}>
|
|
<Icon name={s.icon} size={14} />
|
|
</span>
|
|
{i < det.lifecycle.length - 1 ? <span style={{ width: 1, flex: 1, minHeight: 14, background: s.done ? 'var(--spruce-3)' : 'var(--line-1)' }}></span> : null}
|
|
</div>
|
|
<div style={{ paddingBottom: i < det.lifecycle.length - 1 ? 12 : 0 }}>
|
|
<div style={{ font: `500 12px var(--font-sans)`, color: s.done ? 'var(--ink-1)' : 'var(--ink-3)' }}>{s.stage}</div>
|
|
<div style={{ font: '400 10.5px var(--font-mono)', color: 'var(--ink-3)', marginTop: 2, whiteSpace: 'nowrap' }}>{s.event}</div>
|
|
<div style={{ font: '400 10.5px var(--font-mono)', color: s.done ? 'var(--ink-2)' : 'var(--ink-3)', whiteSpace: 'nowrap' }}>{s.when}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{/* forecast */}
|
|
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--line-1)' }}>
|
|
<div style={{ font: 'var(--text-overline)', letterSpacing: 'var(--letter-spacing-wide)', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 6 }}>Forecast</div>
|
|
<div style={{ font: '500 13px var(--font-mono)', color: 'var(--ink-1)' }}>80% {det.forecast.p80}</div>
|
|
<div style={{ font: '400 10.5px var(--font-mono)', color: 'var(--ink-3)', marginTop: 3 }}>{det.forecast.note}</div>
|
|
</div>
|
|
{/* dependencies */}
|
|
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--line-1)' }}>
|
|
<div style={{ font: 'var(--text-overline)', letterSpacing: 'var(--letter-spacing-wide)', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 6 }}>Dependencies</div>
|
|
{det.blocks.length === 0 && det.blockedBy.length === 0 ? (
|
|
<div style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>none</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{det.blocks.length ? (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>blocks</span>
|
|
{det.blocks.map((b) => (
|
|
<button key={b} type="button"
|
|
onClick={() => onOpenIssue({ id: b, title: b === 91 ? 'Webhook listener: reconcile on reconnect' : 'Monte Carlo engine: percentile bands', labels: b === 91 ? ['est/3d', 'p/2'] : ['est/5d', 'p/1'] })}
|
|
style={{ font: '500 11px var(--font-mono)', color: 'var(--accent-text)', background: 'var(--spruce-1)', border: '1px solid var(--spruce-2)', borderRadius: 'var(--radius-1)', padding: '2px 7px', cursor: 'pointer' }}>
|
|
#{b}
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{det.blockedBy.length ? (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>blocked by</span>
|
|
{det.blockedBy.map((b) => <span key={b} style={{ font: '500 11px var(--font-mono)', color: 'var(--ink-2)' }}>#{b}</span>)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* provenance note */}
|
|
<div style={{ padding: '10px 20px 14px', borderTop: '1px solid var(--line-1)' }}>
|
|
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
|
Lives in pm-state. Your repo never sees any of it.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
Object.assign(window, { IssueScreen });
|