The last three P3 screens, retiring every placeholder: - DirectivesScreen — jade consequence diff (propose-approve: Make it so / Amend / Withdraw, resolving moves it into the ledger) + append-only ledger (seq, who/when/why, verbatim quote, status badge). - SettingsScreen — gitea connection + managed repos, sync switches, model router, read-only label schema, rituals, appearance radios (wired to the shared theme), single danger action. - OnboardingScreen — full-window first run: welcome → connect (test gate) → repo pick → propose-approve bootstrap; onDone routes into the app. Shell early-returns it (no rail/chat), matching the design. Shell routes directives/settings and the firstrun full-window flow. DIRECTIVES fixture added. typecheck + 14 e2e green (incl. directive resolve, appearance↔theme sync, onboarding test gate); all three screenshot-verified. Closes P3-8. P3 (UI views) complete — all 14 screens live on fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
6.1 KiB
TypeScript
105 lines
6.1 KiB
TypeScript
import React from 'react'
|
||
|
||
import { DIRECTIVES, type DirectivePending, type DirectiveEntry } from '../../data/fixtures.js'
|
||
import { Card, Button, Badge, Icon } from '../ui/index.js'
|
||
|
||
// Directive log — append-only ledger + the consequence diff (propose-approve)
|
||
export function DirectivesScreen() {
|
||
const [pending, setPending] = React.useState<DirectivePending | null>(DIRECTIVES.pending)
|
||
const [entries, setEntries] = React.useState<DirectiveEntry[]>(DIRECTIVES.entries)
|
||
|
||
const resolve = (status: string) => {
|
||
setEntries((e) => [{
|
||
seq: pending!.seq, who: pending!.who, when: pending!.when, what: pending!.what, why: 'pilot demo on the 14th',
|
||
status, consequence: status === 'applied' ? '#78 +5d · Beta 80% Mar 5–14' : 'withdrawn before apply',
|
||
}, ...e]);
|
||
setPending(null);
|
||
};
|
||
|
||
const toneColor: Record<string, string> = { ok: 'var(--ok)', warn: 'var(--warn)', info: 'var(--info)', danger: 'var(--danger)' };
|
||
const statusBadge: Record<string, { tone: 'ok' | 'neutral' | 'info'; label: string }> = {
|
||
applied: { tone: 'ok', label: 'applied' },
|
||
withdrawn: { tone: 'neutral', label: 'withdrawn' },
|
||
superseded: { tone: 'info', label: 'superseded' },
|
||
};
|
||
|
||
return (
|
||
<div style={{ maxWidth: 760, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14 }}>
|
||
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Directives</h1>
|
||
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0' }}>append-only · JSONL in pm-state · who, when, what, why</p>
|
||
</header>
|
||
|
||
{pending ? (
|
||
<Card overline={`Awaiting your word · directive #00${pending.seq}`} title="Consequence diff" jade
|
||
footer={<>
|
||
<Button onClick={() => resolve('applied')}>Make it so</Button>
|
||
<Button variant="secondary" onClick={() => {}}>Amend</Button>
|
||
<Button variant="ghost" onClick={() => resolve('withdrawn')}>Withdraw</Button>
|
||
</>}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||
<p style={{ font: 'var(--text-body)', margin: 0, color: 'var(--ink-2)' }}>
|
||
<span style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)' }}>{pending.who}</span>
|
||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}> · {pending.when}</span>
|
||
<br />“{pending.what}”
|
||
</p>
|
||
<div style={{ borderTop: '1px solid var(--line-1)' }}>
|
||
{pending.diff.map((r) => (
|
||
<div key={r.change} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 0', borderBottom: '1px solid var(--line-1)' }}>
|
||
<span style={{ width: 7, height: 7, borderRadius: '50%', background: toneColor[r.tone], flexShrink: 0 }}></span>
|
||
<span style={{ font: 'var(--text-small)', color: 'var(--ink-1)', flex: 1 }}>{r.change}</span>
|
||
<span style={{ font: '400 12px var(--font-mono)', color: 'var(--ink-3)', whiteSpace: 'nowrap' }}>
|
||
{r.from} <span style={{ color: 'var(--ink-2)' }}>→</span> <span style={{ color: r.tone === 'warn' ? 'var(--warn)' : 'var(--ink-1)' }}>{r.to}</span>
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
||
Cheap, as consequences go. Shall I make it so?
|
||
</p>
|
||
</div>
|
||
</Card>
|
||
) : (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, font: 'var(--text-small)', color: 'var(--ink-3)', padding: '2px 2px' }}>
|
||
<Icon name="circle-check" size={14} style={{ color: 'var(--ok)' }} />
|
||
Nothing awaits your word. Directives are given in chat; consequences appear here first.
|
||
</div>
|
||
)}
|
||
|
||
<Card overline="The ledger" flush>
|
||
<div>
|
||
{entries.map((e, i) => {
|
||
const sb = statusBadge[e.status];
|
||
return (
|
||
<div key={e.seq} style={{ display: 'flex', gap: 14, padding: '14px 20px', borderTop: i === 0 ? 'none' : '1px solid var(--line-1)' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
||
<span style={{ font: '500 11px var(--font-mono)', color: 'var(--ink-3)' }}>#00{e.seq}</span>
|
||
<span style={{ width: 1, flex: 1, background: 'var(--line-1)' }}></span>
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||
<span style={{
|
||
width: 20, height: 20, borderRadius: '50%', background: 'var(--spruce-2)', color: 'var(--accent-text)',
|
||
font: '600 8.5px/20px var(--font-sans)', textAlign: 'center', flexShrink: 0,
|
||
}}>{e.who.split(' ').map((w: string) => w[0]).join('')}</span>
|
||
<span style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)', whiteSpace: 'nowrap' }}>{e.who}</span>
|
||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', whiteSpace: 'nowrap' }}>{e.when}</span>
|
||
{e.why ? <span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', whiteSpace: 'nowrap' }}>· why: {e.why}</span> : null}
|
||
<span style={{ marginLeft: 'auto' }}><Badge tone={sb.tone}>{sb.label}</Badge></span>
|
||
</div>
|
||
<p style={{ font: 'var(--text-body)', color: 'var(--ink-1)', margin: 0 }}>“{e.what}”</p>
|
||
<p style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-2)', margin: 0 }}>{e.consequence}</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</Card>
|
||
|
||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||
Entries are never edited. Corrections are new entries — the ledger remembers everything, politely.
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|