CommiTea foundation + P3 UI: gitea client, e2e harness, all product screens #35
@@ -37,10 +37,12 @@ export class AppPage {
|
||||
)
|
||||
}
|
||||
|
||||
/** Capture a full-page screenshot for visual review; returns the path. */
|
||||
/** Capture a full-page screenshot for visual review; returns the path.
|
||||
* `animations: 'disabled'` fast-forwards finite CSS animations to their end
|
||||
* state, so fade-in screens (e.g. Standup) capture settled, not mid-fade. */
|
||||
async screenshot(name: string): Promise<string> {
|
||||
const path = join(SCREENS_DIR, `${name}.png`)
|
||||
await this.page.screenshot({ path, fullPage: true })
|
||||
await this.page.screenshot({ path, fullPage: true, animations: 'disabled' })
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
test('boots into the shell: rail, main, and Reginald panel', async ({ app, window }) => {
|
||||
test('boots into the shell with the Focus screen', async ({ app, window }) => {
|
||||
await app.expectLoaded()
|
||||
await expect(window.getByText('Reginald', { exact: true })).toBeVisible()
|
||||
// default view is Morning service
|
||||
await expect(window.getByRole('heading', { name: 'Morning service' })).toBeVisible()
|
||||
await app.screenshot('shell-light')
|
||||
})
|
||||
|
||||
test('Focus shows Now/Next/Later cards and the burn-up cone', async ({ window }) => {
|
||||
for (const slot of ['Now', 'Next', 'Later']) {
|
||||
await expect(window.getByText(slot, { exact: true })).toBeVisible()
|
||||
}
|
||||
await expect(window.getByText('Fix lifecycle inference on merge events')).toBeVisible()
|
||||
await expect(window.getByText(/scope · 42 issues/)).toBeVisible()
|
||||
await expect(window.getByText(/80% this lands/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Standup renders the drift / plan / nag letter', async ({ app, window }) => {
|
||||
await app.nav('Standup').click()
|
||||
await expect(window.getByRole('heading', { name: 'Morning standup' })).toBeVisible()
|
||||
await expect(window.getByText('Overnight drift')).toBeVisible()
|
||||
await expect(window.getByText("Today's plan")).toBeVisible()
|
||||
await expect(window.getByText('Stephen', { exact: true })).toBeVisible()
|
||||
await expect(window.getByText(/Four days is a long steep/)).toBeVisible()
|
||||
await app.screenshot('standup')
|
||||
})
|
||||
|
||||
test('rail navigation switches the main view', async ({ app, window }) => {
|
||||
await app.nav('The pot').click()
|
||||
await expect(window.getByRole('heading', { name: 'The pot' })).toBeVisible()
|
||||
@@ -19,7 +37,6 @@ test('rail navigation switches the main view', async ({ app, window }) => {
|
||||
})
|
||||
|
||||
test('Evening service toggle flips the document theme', async ({ app, window }) => {
|
||||
// the switch's real input is visually hidden — click the label text to toggle
|
||||
await window.getByText('Evening service', { exact: true }).click()
|
||||
await expect(window.locator('html')).toHaveAttribute('data-theme', 'dark')
|
||||
await expect(window.getByRole('switch', { name: 'Evening service' })).toBeChecked()
|
||||
@@ -39,11 +56,10 @@ test('chat composer echoes a canned reply (fixture)', async ({ window }) => {
|
||||
await expect(window.getByText(/Noted and logged as a directive/)).toBeVisible()
|
||||
})
|
||||
|
||||
test('issue drill-in and back-stack of one', async ({ window }) => {
|
||||
await window.getByRole('button', { name: 'Preview an issue page' }).click()
|
||||
test('issue drill-in from a Focus card and back-stack of one', async ({ window }) => {
|
||||
await window.getByRole('link', { name: 'Fix lifecycle inference on merge events' }).click()
|
||||
await expect(window.getByRole('heading', { name: 'Issue' })).toBeVisible()
|
||||
await window.getByRole('button', { name: 'Back' }).click()
|
||||
// returns to the view we drilled in from
|
||||
await expect(window.getByRole('heading', { name: 'Morning service' })).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
121
apps/desktop/src/renderer/src/components/charts/chart.tsx
Normal file
121
apps/desktop/src/renderer/src/components/charts/chart.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Charts — geometry, not decoration. Ported from the handoff's Chart.js. The
|
||||
* fixed sample paths here stand in for scheduler/Monte Carlo output (P2); the
|
||||
* shapes (cone from today, 80% band, actual polyline, today rule) are final.
|
||||
*/
|
||||
|
||||
export interface BurnUpConeProps {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function BurnUpCone({ width = 640, height = 220 }: BurnUpConeProps) {
|
||||
const pad = { l: 34, r: 96, t: 16, b: 26 }
|
||||
const W = width - pad.l - pad.r
|
||||
const H = height - pad.t - pad.b
|
||||
const x = (f: number) => pad.l + f * W
|
||||
const y = (f: number) => pad.t + (1 - f) * H
|
||||
|
||||
const today = 0.58
|
||||
const actual: number[][] = [
|
||||
[0, 0],
|
||||
[0.08, 0.05],
|
||||
[0.18, 0.13],
|
||||
[0.26, 0.16],
|
||||
[0.36, 0.27],
|
||||
[0.46, 0.38],
|
||||
[0.58, 0.47],
|
||||
]
|
||||
const coneHi: number[][] = [
|
||||
[0.58, 0.47],
|
||||
[0.72, 0.66],
|
||||
[0.86, 0.88],
|
||||
[0.95, 1.0],
|
||||
]
|
||||
const coneLo: number[][] = [
|
||||
[0.58, 0.47],
|
||||
[0.74, 0.58],
|
||||
[0.9, 0.74],
|
||||
[1.0, 0.86],
|
||||
]
|
||||
const mid: number[][] = [
|
||||
[0.58, 0.47],
|
||||
[0.76, 0.63],
|
||||
[0.92, 0.83],
|
||||
[1.0, 0.93],
|
||||
]
|
||||
const pts = (arr: number[][]) => arr.map(([a, b]) => `${x(a)},${y(b)}`).join(' ')
|
||||
const cone = [...coneHi, ...[...coneLo].reverse()]
|
||||
|
||||
return (
|
||||
<svg width="100%" viewBox={`0 0 ${width} ${height}`} style={{ display: 'block' }}>
|
||||
{/* gridlines */}
|
||||
{[0, 0.25, 0.5, 0.75, 1].map((f) => (
|
||||
<line key={f} x1={pad.l} x2={width - pad.r} y1={y(f)} y2={y(f)} stroke="var(--line-1)" strokeWidth="1" />
|
||||
))}
|
||||
{/* scope */}
|
||||
<line x1={pad.l} x2={width - pad.r} y1={y(1)} y2={y(1)} stroke="var(--line-2)" strokeWidth="1.5" />
|
||||
<text x={pad.l} y={y(1) - 6} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>
|
||||
scope · 42 issues
|
||||
</text>
|
||||
{/* cone */}
|
||||
<polygon points={pts(cone)} fill="var(--cone-fill)" />
|
||||
<polyline points={pts(coneHi)} fill="none" stroke="var(--cone-line)" strokeWidth="1.2" strokeDasharray="3 3" />
|
||||
<polyline points={pts(coneLo)} fill="none" stroke="var(--cone-line)" strokeWidth="1.2" strokeDasharray="3 3" />
|
||||
<polyline points={pts(mid)} fill="none" stroke="var(--cone-line)" strokeWidth="1.4" />
|
||||
{/* actual */}
|
||||
<polyline points={pts(actual)} fill="none" stroke="var(--cone-actual)" strokeWidth="2" />
|
||||
<circle cx={x(0.58)} cy={y(0.47)} r="3.5" fill="var(--cone-actual)" />
|
||||
{/* today rule */}
|
||||
<line x1={x(today)} x2={x(today)} y1={pad.t} y2={height - pad.b} stroke="var(--jade)" strokeWidth="1" />
|
||||
<text x={x(today) + 5} y={pad.t + 10} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--jade-7)' }}>
|
||||
today
|
||||
</text>
|
||||
{/* 80% band label */}
|
||||
<text x={width - pad.r + 10} y={y(0.95)} style={{ font: '500 11.5px var(--font-mono)', fill: 'var(--ink-1)' }}>
|
||||
80%
|
||||
</text>
|
||||
<text x={width - pad.r + 10} y={y(0.95) + 14} style={{ font: '400 11px var(--font-mono)', fill: 'var(--ink-2)' }}>
|
||||
Mar 3–12
|
||||
</text>
|
||||
{/* x labels */}
|
||||
<text x={pad.l} y={height - 8} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>
|
||||
Jan 6
|
||||
</text>
|
||||
<text x={width - pad.r - 34} y={height - 8} style={{ font: '400 10.5px var(--font-mono)', fill: 'var(--ink-3)' }}>
|
||||
Mar 15
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export interface RunwayBarMilestone {
|
||||
tone: 'ok' | 'warn'
|
||||
pos: number
|
||||
spread: number
|
||||
}
|
||||
|
||||
/** Milestone due-date vs forecast-range position. Used by the Runway screen (P3-5). */
|
||||
export function RunwayBar({ m }: { m: RunwayBarMilestone }) {
|
||||
const toneColor = m.tone === 'warn' ? 'var(--warn)' : 'var(--ok)'
|
||||
const left = Math.max(0, (m.pos - m.spread / 2) * 100)
|
||||
const w = Math.min(100 - left, m.spread * 100)
|
||||
return (
|
||||
<div style={{ position: 'relative', height: 22, background: 'var(--paper-2)', borderRadius: 4, overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${left}%`,
|
||||
width: `${w}%`,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
background: 'var(--cone-fill)',
|
||||
borderLeft: `1.5px solid ${toneColor}`,
|
||||
borderRight: `1.5px solid ${toneColor}`,
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'absolute', left: `${m.pos * 100}%`, top: 0, bottom: 0, width: 2, background: toneColor }} />
|
||||
<div style={{ position: 'absolute', left: 'calc(88% - 1px)', top: 0, bottom: 0, width: 2, background: 'var(--ink-1)' }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react'
|
||||
|
||||
import { FOCUS, type FocusIssue, TODAY } from '../../data/fixtures.js'
|
||||
import { BurnUpCone } from '../charts/chart.js'
|
||||
import { Badge, Button, Card, IconButton, Tag } from '../ui/index.js'
|
||||
|
||||
/**
|
||||
* Morning service — the Now/Next/Later focus cards + the milestone burn-up cone.
|
||||
* Data is fixture (data.js) until P2's scheduler + Monte Carlo feed it.
|
||||
*/
|
||||
export function FocusScreen({ onOpenIssue }: { onOpenIssue: (id: number) => void }) {
|
||||
const FocusRow = ({ slot, issue, jade }: { slot: string; issue: FocusIssue; jade?: boolean }) => (
|
||||
<Card
|
||||
overline={slot}
|
||||
jade={jade}
|
||||
title={
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onOpenIssue(issue.id)
|
||||
}}
|
||||
style={{ color: 'inherit', border: 'none' }}
|
||||
>
|
||||
{issue.title}
|
||||
</a>
|
||||
}
|
||||
actions={<IconButton icon="ellipsis" label="More" size="sm" />}
|
||||
footer={
|
||||
jade ? (
|
||||
<>
|
||||
<Button size="sm">Start</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
Defer
|
||||
</Button>
|
||||
<span style={{ marginLeft: 'auto', font: 'var(--text-caption)', color: 'var(--ink-3)' }}>
|
||||
scheduler pick · critical path
|
||||
</span>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{ font: 'var(--text-data)', color: 'var(--ink-3)' }}>#{issue.id}</span>
|
||||
{issue.labels.map((l) => (
|
||||
<Tag key={l} label={l} />
|
||||
))}
|
||||
{issue.steeping ? (
|
||||
<Badge tone="warn" dot>
|
||||
steeping {issue.steeping}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>{issue.rationale}</p>
|
||||
</Card>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<header
|
||||
style={{
|
||||
borderBottom: 'var(--rule-double)',
|
||||
paddingBottom: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Morning service</h1>
|
||||
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0', whiteSpace: 'nowrap' }}>
|
||||
{TODAY} · reconcile 3.2s
|
||||
</p>
|
||||
</div>
|
||||
<Badge tone="ok" dot>
|
||||
ahead of forecast
|
||||
</Badge>
|
||||
</header>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr 1fr', gap: 14, alignItems: 'start' }}>
|
||||
<FocusRow slot="Now" issue={FOCUS.now} jade />
|
||||
<FocusRow slot="Next" issue={FOCUS.next} />
|
||||
<FocusRow slot="Later" issue={FOCUS.later} />
|
||||
</div>
|
||||
|
||||
<Card
|
||||
overline="Milestone · Beta"
|
||||
title={<>80% this lands <span style={{ whiteSpace: 'nowrap' }}>Mar 3–12</span></>}
|
||||
actions={<IconButton icon="chart-line" label="Open runway" size="sm" />}
|
||||
>
|
||||
<BurnUpCone />
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||||
The cone has narrowed since Friday. I’m quietly pleased.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import React from 'react'
|
||||
|
||||
import { STANDUP } from '../../data/fixtures.js'
|
||||
import { Badge, Button, Icon } from '../ui/index.js'
|
||||
|
||||
const standupCSS = `
|
||||
@keyframes ct-standup-settle {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.ct-standup-section { opacity: 1; }
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.ct-standup-section { animation: ct-standup-settle 320ms var(--ease-out) both; }
|
||||
}
|
||||
`
|
||||
;(function inject() {
|
||||
if (typeof document !== 'undefined' && !document.getElementById('ct-standup-css')) {
|
||||
const s = document.createElement('style')
|
||||
s.id = 'ct-standup-css'
|
||||
s.textContent = standupCSS
|
||||
document.head.appendChild(s)
|
||||
}
|
||||
})()
|
||||
|
||||
const toneColor: Record<string, string> = {
|
||||
ok: 'var(--ok)',
|
||||
warn: 'var(--warn)',
|
||||
danger: 'var(--danger)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Morning standup — a typeset letter from Reginald: overnight drift, today's
|
||||
* plan, stale-blocker nag. Sections settle in once, reduced-motion-safe. Data
|
||||
* is fixture (data.js) until the scheduler + lifecycle inference feed it.
|
||||
*/
|
||||
export function StandupScreen({
|
||||
onBegin,
|
||||
onOpenIssue,
|
||||
}: {
|
||||
onBegin: () => void
|
||||
onOpenIssue: (id: number) => void
|
||||
}) {
|
||||
const s = STANDUP
|
||||
|
||||
const Section = ({ overline, order, children }: { overline: string; order: number; children: React.ReactNode }) => (
|
||||
<section
|
||||
className="ct-standup-section"
|
||||
style={{ animationDelay: `${order * 90}ms`, display: 'flex', flexDirection: 'column', gap: 12 }}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
font: 'var(--text-overline)',
|
||||
letterSpacing: 'var(--letter-spacing-wide)',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--ink-3)',
|
||||
margin: 0,
|
||||
borderTop: '1px solid var(--line-1)',
|
||||
paddingTop: 14,
|
||||
}}
|
||||
>
|
||||
{overline}
|
||||
</p>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 660, margin: '0 auto' }}>
|
||||
<article
|
||||
style={{
|
||||
background: 'var(--surface-card)',
|
||||
border: '1px solid var(--line-1)',
|
||||
borderRadius: 'var(--radius-3)',
|
||||
boxShadow: 'var(--shadow-jade-line), var(--shadow-1)',
|
||||
padding: '36px 44px 32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 20,
|
||||
}}
|
||||
>
|
||||
<header className="ct-standup-section" style={{ borderBottom: 'var(--rule-double)', paddingBottom: 16 }}>
|
||||
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '0 0 8px' }}>{s.date} · prepared 07:00</p>
|
||||
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Morning standup</h1>
|
||||
</header>
|
||||
|
||||
<Section overline="Overnight drift" order={1}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{s.drift.map((d) => (
|
||||
<div key={d.text} style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: '50%',
|
||||
background: toneColor[d.tone],
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
top: -1,
|
||||
}}
|
||||
/>
|
||||
<span style={{ font: 'var(--text-body)', color: 'var(--ink-1)', flex: 1 }}>{d.text}</span>
|
||||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)', whiteSpace: 'nowrap' }}>
|
||||
{d.delta}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section overline="Today's plan" order={2}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{s.plan.map((p) => (
|
||||
<div key={p.who} style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
<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) => w[0])
|
||||
.join('')}
|
||||
</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)', whiteSpace: 'nowrap' }}>
|
||||
{p.who}
|
||||
</span>
|
||||
<span style={{ font: '400 12px var(--font-mono)', color: 'var(--ink-2)' }}>{p.pick}</span>
|
||||
<span style={{ font: 'var(--text-small)', color: 'var(--ink-2)' }}>{p.title}</span>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '4px 0 0' }}>{p.why}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section overline="Stale blockers" order={3}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onOpenIssue(s.nag.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onOpenIssue(s.nag.id)
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 10,
|
||||
alignItems: 'flex-start',
|
||||
cursor: 'pointer',
|
||||
background: 'var(--warn-tint)',
|
||||
borderRadius: 'var(--radius-2)',
|
||||
padding: '12px 14px',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--warn)', display: 'inline-flex', marginTop: 2 }}>
|
||||
<Icon name="clock" size={15} />
|
||||
</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)' }}>#{s.nag.id}</span>
|
||||
<Badge tone="warn" dot>
|
||||
steeping {s.nag.days}
|
||||
</Badge>
|
||||
<span style={{ font: '400 11.5px var(--font-mono)', color: 'var(--ink-3)' }}>
|
||||
blocks {s.nag.blocks.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '5px 0 0' }}>{s.nag.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<footer
|
||||
className="ct-standup-section"
|
||||
style={{
|
||||
animationDelay: '360ms',
|
||||
borderTop: '1px solid var(--line-1)',
|
||||
paddingTop: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-1)', margin: 0, flex: 1 }}>
|
||||
The kettle’s on. — R.
|
||||
</p>
|
||||
<Button variant="ghost" onClick={onBegin}>
|
||||
Ask about the drift
|
||||
</Button>
|
||||
<Button iconRight="arrow-right" onClick={onBegin}>
|
||||
Begin the day
|
||||
</Button>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react'
|
||||
|
||||
import logoIcon from '../../design/assets/logo-icon.png'
|
||||
import { PrimitivesGallery } from '../gallery.js'
|
||||
import { FocusScreen } from '../screens/focus-screen.js'
|
||||
import { StandupScreen } from '../screens/standup-screen.js'
|
||||
import { Icon, Switch } from '../ui/index.js'
|
||||
import { ChatPanel } from './chat-panel.js'
|
||||
import { PlaceholderScreen } from './placeholder-screen.js'
|
||||
@@ -137,6 +139,10 @@ export function AppShell() {
|
||||
|
||||
const renderScreen = () => {
|
||||
switch (view) {
|
||||
case 'focus':
|
||||
return <FocusScreen onOpenIssue={openIssue} />
|
||||
case 'standup':
|
||||
return <StandupScreen onBegin={() => setView('focus')} onOpenIssue={openIssue} />
|
||||
case 'states':
|
||||
return <StatesScreen onCapture={() => setView('capture')} />
|
||||
case 'primitives':
|
||||
|
||||
@@ -24,3 +24,97 @@ export const CHAT: ChatMessage[] = [
|
||||
|
||||
export const CANNED_REPLY =
|
||||
'Noted and logged as a directive. The scheduler is re-running — I’ll show you the consequence diff in a moment.'
|
||||
|
||||
export const TODAY = 'Tuesday 7 July 2026'
|
||||
|
||||
export interface FocusIssue {
|
||||
id: number
|
||||
title: string
|
||||
labels: string[]
|
||||
steeping?: string
|
||||
rationale: string
|
||||
}
|
||||
|
||||
export interface FocusData {
|
||||
now: FocusIssue
|
||||
next: FocusIssue
|
||||
later: FocusIssue
|
||||
}
|
||||
|
||||
export const FOCUS: FocusData = {
|
||||
now: {
|
||||
id: 87,
|
||||
title: 'Fix lifecycle inference on merge events',
|
||||
labels: ['est/2d', 'p/1'],
|
||||
steeping: '4d',
|
||||
rationale: 'It blocks #91 and #92. I’d take it first — the critical path agrees with me.',
|
||||
},
|
||||
next: {
|
||||
id: 91,
|
||||
title: 'Webhook listener: reconcile on reconnect',
|
||||
labels: ['est/3d', 'p/2'],
|
||||
rationale: 'Ready the moment #87 lands. The estimate is yours; history says add a day.',
|
||||
},
|
||||
later: {
|
||||
id: 78,
|
||||
title: 'Calibration store cold-start distributions',
|
||||
labels: ['est/5d', 'p/3'],
|
||||
rationale: 'Nothing depends on it yet. It can steep.',
|
||||
},
|
||||
}
|
||||
|
||||
export interface DriftItem {
|
||||
tone: 'ok' | 'warn' | 'danger'
|
||||
text: string
|
||||
delta: string
|
||||
}
|
||||
|
||||
export interface PlanItem {
|
||||
who: string
|
||||
pick: string
|
||||
title: string
|
||||
why: string
|
||||
}
|
||||
|
||||
export interface Nag {
|
||||
id: number
|
||||
days: string
|
||||
blocks: string[]
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface StandupData {
|
||||
date: string
|
||||
drift: DriftItem[]
|
||||
plan: PlanItem[]
|
||||
nag: Nag
|
||||
}
|
||||
|
||||
export const STANDUP: StandupData = {
|
||||
date: 'Tuesday 7 July 2026',
|
||||
drift: [
|
||||
{ tone: 'warn', text: '#84 grew a dependency on #92 overnight.', delta: 'reordered · no date impact' },
|
||||
{ tone: 'warn', text: '#92 has sat in review for two days.', delta: 'Beta 80% +1d if idle past Thu' },
|
||||
{ tone: 'ok', text: 'Webhook outage 02:14–02:31; full reconcile ran.', delta: 'nothing lost' },
|
||||
],
|
||||
plan: [
|
||||
{
|
||||
who: 'Stephen',
|
||||
pick: '#87',
|
||||
title: 'Fix lifecycle inference on merge events',
|
||||
why: 'It blocks two others and the critical path runs straight through it.',
|
||||
},
|
||||
{
|
||||
who: 'Ana K.',
|
||||
pick: '#84',
|
||||
title: 'Capacity model: focus factor per person',
|
||||
why: 'Already steeping — finish it before anything new is poured.',
|
||||
},
|
||||
],
|
||||
nag: {
|
||||
id: 87,
|
||||
days: '4d',
|
||||
blocks: ['#91', '#92'],
|
||||
text: 'Four days is a long steep. It blocks #91 and #92 — worth a look before it stains.',
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user