Files
commitea/apps/desktop/src/renderer/src/components/screens/directives-screen.tsx
Croissant Le Doux 0fc53d03be feat(desktop): Directives, Settings, Onboarding — P3 complete (#21)
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>
2026-07-08 16:22:59 -04:00

105 lines
6.1 KiB
TypeScript
Raw 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.

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 514' : '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>
);
}