AI web starter: bands, review split pane, approve gate, server stub (checkpoint)
React + Vite + Tailwind starter with the no-AI shell, band legend, review split pane with drafts and verify, and a server stub that refuses approval while flagged fields are unverified. Band self-check and AI page check. Both page checks share one Chromium via tests/conftest.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3eb3fdf00e
commit
d4d3f83fb1
24 files changed
+3429
-73
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import { useStore } from '../store'
|
||||
|
||||
// batch state -> [label, colour token, pill colour]
|
||||
export const STATES: Record<string, [string, string, string]> = {
|
||||
completed: ['Completed', 'var(--ok)', 'bg-ok'],
|
||||
inprogress: ['In progress', 'var(--warn)', 'bg-warn'],
|
||||
notcompleted: ['Not completed', 'var(--bad)', 'bg-bad'],
|
||||
cleared: ['Cleared', 'var(--muted)', 'bg-muted'],
|
||||
}
|
||||
|
||||
function Kpi({ label, n, colour, live }: { label: string; n: number; colour: string; live?: boolean }) {
|
||||
return (
|
||||
<div role="group" aria-label={label} className="flex items-center gap-3 rounded-2xl border border-line bg-glass-strong px-4 py-4 shadow-sm">
|
||||
<div className={`h-[42px] w-[42px] shrink-0 rounded-xl ${live ? 'animate-pulse' : ''}`} style={{ background: colour }} />
|
||||
<div>
|
||||
<div className="font-head text-[26px] leading-tight font-extrabold tabular-nums">{n}</div>
|
||||
<div className="mt-0.5 text-[11.5px] font-semibold text-muted">{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Overview() {
|
||||
const { batches } = useStore()
|
||||
const count = Object.fromEntries(Object.keys(STATES).map((k) => [k, batches.filter((b) => b.state === k).length]))
|
||||
const total = Object.values(count).reduce((a, x) => a + x, 0)
|
||||
let acc = 0
|
||||
const stops = Object.keys(STATES).flatMap((k) => {
|
||||
if (!count[k]) return []
|
||||
const from = (acc / total) * 100
|
||||
acc += count[k]
|
||||
return [`${STATES[k][1]} ${from}% ${(acc / total) * 100}%`]
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 grid grid-cols-4 gap-3.5 max-[820px]:grid-cols-2">
|
||||
<Kpi label="Total batches" n={batches.length} colour="var(--accent)" />
|
||||
<Kpi label="Completed" n={count.completed} colour="var(--ok)" />
|
||||
<Kpi label="In progress" n={count.inprogress} colour="var(--warn)" live={count.inprogress > 0} />
|
||||
<Kpi label="Not completed" n={count.notcompleted} colour="var(--bad)" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3.5 max-[820px]:grid-cols-1">
|
||||
<div className="card">
|
||||
<div className="card-title">Batch status split</div>
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<div role="img" className="relative h-[150px] w-[150px] rounded-full"
|
||||
aria-label={'Batch status: ' + (total ? Object.keys(STATES).map((k) => `${STATES[k][0]} ${count[k]}`).join(', ') : 'no batches yet')}
|
||||
style={{ background: total ? `conic-gradient(${stops.join(',')})` : 'conic-gradient(var(--border) 0 100%)' }}>
|
||||
<div className="absolute inset-[22px] flex flex-col items-center justify-center rounded-full" style={{ background: 'var(--bg-bottom)' }}>
|
||||
<div className="font-head text-[26px] font-extrabold">{batches.length}</div>
|
||||
<div className="text-[10px] font-bold uppercase text-muted">batches</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul aria-label="Batch status">
|
||||
{Object.keys(STATES).map((k) => (
|
||||
<li key={k} className="mb-2 flex items-center gap-2 text-[12.5px] font-semibold">
|
||||
<span className="h-2.5 w-2.5 rounded-[3px]" style={{ background: STATES[k][1] }} />
|
||||
{STATES[k][0]} · {count[k]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-title">Recent batches</div>
|
||||
{batches.length === 0 && <div className="py-10 text-center text-[13px] text-muted">No batches yet</div>}
|
||||
{batches.slice(0, 6).map((b) => {
|
||||
const s = STATES[b.state] ?? [b.state, '', 'bg-warn']
|
||||
return (
|
||||
<div key={b.batch_id} className="mb-2 flex items-center gap-2 text-[12.5px] font-semibold">
|
||||
#{b.batch_id} · {b.date}
|
||||
<span className={`pill ml-auto ${s[2]}`}>{s[0]}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user