Ports the nine remaining design-handoff screens to typed TSX and wires them into the shell, retiring the placeholders for every primary view. - The pot (#17 / P3-4): BoardScreen 5-column kanban with live search + empty state, Tabs into GanttView (state bars, critical inset rules, dotted 80% tails, today rule) and DepsGraph (layered DAG, bezier edges, spruce critical path, milestone terminal). - Runway (#18 / P3-5): RunwayScreen (range bars via RunwayBar, capacity, calibration teaser), CalibrationScreen (scatter + honest diagonal + ×1.18 fit, per-label/per-person bias), MilestoneScreen (stats strip, cone, grouped issues). - Issue + Inbox (#19 / P3-6): IssueScreen (human intent left, machine- derived lifecycle/forecast/deps right, "writes as you" composer), InboxScreen (day-grouped, filter tabs, unread dots, mark-all-read). - Capture (#20 / P3-7): CaptureScreen dump→interview→review→filed state machine with running clock. Shell: openIssue now carries an IssueRef; adds readIds (inbox) + issue state; routes board/runway/calibration/milestone/issue/inbox/capture and the runway→calibration/milestone + issue back-stack. Focus/Standup pass IssueRefs. Select/Input gain defaultValue (Capture's editable tray). Fixtures mirrored from data.js. typecheck + 11 e2e green; all nine screens screenshot-verified in light. Closes P3-4, P3-5, P3-6, P3-7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
4.0 KiB
TypeScript
81 lines
4.0 KiB
TypeScript
import React from 'react'
|
|
|
|
import { RunwayBar } from '../charts/chart.js'
|
|
import { Card, Badge, Tag, Icon, IconButton } from '../ui/index.js'
|
|
import { RUNWAY, CAPACITY } from '../../data/fixtures.js'
|
|
|
|
// Runway — capacity vs milestone dates; ranges, never points
|
|
export function RunwayScreen({
|
|
onOpenCalibration,
|
|
onOpenMilestone,
|
|
}: {
|
|
onOpenCalibration: () => void
|
|
onOpenMilestone: () => void
|
|
}) {
|
|
return (
|
|
<div style={{ 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 }}>Runway</h1>
|
|
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0' }}>capacity vs milestone dates · calibrated on 27 closed issues</p>
|
|
</header>
|
|
|
|
<Card overline="Milestones" flush>
|
|
<div>
|
|
{RUNWAY.map((m, i) => (
|
|
<div key={m.name} onClick={onOpenMilestone} style={{
|
|
display: 'grid', gridTemplateColumns: '160px 1fr 150px 90px', gap: 16, alignItems: 'center', cursor: 'pointer',
|
|
padding: '14px 20px', borderTop: i === 0 ? 'none' : '1px solid var(--line-1)',
|
|
}}
|
|
onMouseEnter={(e: React.MouseEvent<HTMLDivElement>) => { e.currentTarget.style.background = 'var(--paper-2)'; }}
|
|
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => { e.currentTarget.style.background = 'transparent'; }}>
|
|
<div>
|
|
<div style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)', display: 'flex', alignItems: 'center', gap: 7 }}>
|
|
<Icon name="milestone" size={14} style={{ color: 'var(--ink-3)' }} />{m.name}
|
|
</div>
|
|
<div style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', marginTop: 3 }}>
|
|
due {m.due}{m.hard ? ' ' : ''}
|
|
</div>
|
|
{m.hard ? <Tag label="deadline/hard" style={{ marginTop: 5 }} /> : null}
|
|
</div>
|
|
<RunwayBar m={m} />
|
|
<span style={{ font: '400 12px var(--font-mono)', color: 'var(--ink-2)' }}>80% {m.p80}</span>
|
|
<Badge tone={m.tone} dot>{m.note}</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, alignItems: 'start' }}>
|
|
<Card overline="Capacity" flush>
|
|
<div>
|
|
{CAPACITY.map((p, i) => (
|
|
<div key={p.who} style={{
|
|
display: 'flex', alignItems: 'center', gap: 12, padding: '12px 20px',
|
|
borderTop: i === 0 ? 'none' : '1px solid var(--line-1)',
|
|
}}>
|
|
<span style={{
|
|
width: 26, height: 26, borderRadius: '50%', background: 'var(--spruce-2)', color: 'var(--accent-text)',
|
|
font: '600 10px/26px var(--font-sans)', textAlign: 'center', flexShrink: 0,
|
|
}}>{p.who.split(' ').map((w: string) => w[0]).join('')}</span>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ font: 'var(--text-body-strong)' }}>{p.who}</div>
|
|
<div style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', marginTop: 2 }}>{p.slices}</div>
|
|
</div>
|
|
<span style={{ font: '400 12px var(--font-mono)', color: 'var(--ink-2)' }}>{p.hours}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
<Card overline="Calibration" actions={<IconButton icon="arrow-up-right" label="Full report" size="sm" onClick={onOpenCalibration} />}>
|
|
<p style={{ font: 'var(--text-body)', margin: '0 0 8px' }}>
|
|
Your estimates run <strong>18% optimistic</strong> on <code>est/3d</code> and above. Smaller tickets are honest.
|
|
</p>
|
|
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
|
I widen the cone accordingly. No judgement — it's the most common shape of hope.
|
|
</p>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|