Frontend: site chrome, demo shell, pages, and the contract gates
Five parallel lanes plus an integration pass. The header, gallery, router and sitemap are all generated from the demo registry, so adding src/demos/<slug>/ puts a demo everywhere with zero edits to shared files — which is the whole reason demo nine cannot break demo one. check-demos enforces the twelve contract rules: 142 checks over one live demo. Two worth naming. The shell may not mention a specific slug, because an 'if (slug === wordle)' in src/components/demo/ is a contract bug wearing a patch. And a spec-status demo must ship a real specification — task, actions, grader, counterweight, eval command — since a coming-soon card reads worse than an honest empty gallery. Bundle budget holds: entry 108.79 kB gzipped against a 160 kB ceiling, the demo chunk 21.15 kB against 90 kB. recharts is 108 kB gzipped and lives behind a lazy import so it never touches the entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import { Eye, Trophy } from 'lucide-react';
|
||||
import type { DemoStep } from '@/lib/demo-kit/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatOrDash } from './format';
|
||||
|
||||
@@ -80,7 +81,7 @@ export function BlindCompare<T>({
|
||||
<p className="nums text-xs text-muted">Same puzzle, same seed ({seed}).</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{sides.map(({ id, run }) => {
|
||||
const last = run.steps[run.steps.length - 1];
|
||||
const picked = vote === id;
|
||||
@@ -107,13 +108,9 @@ export function BlindCompare<T>({
|
||||
</div>
|
||||
|
||||
{!revealed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commit(id)}
|
||||
className="tap w-full rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors duration-2 ease-enter hover:bg-primary/90"
|
||||
>
|
||||
<Button size="touch" className="w-full" onClick={() => commit(id)}>
|
||||
Agent {id} is better
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<dl className="space-y-1 text-sm">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
@@ -136,7 +133,7 @@ export function BlindCompare<T>({
|
||||
<dt className="text-muted">Reward</dt>
|
||||
<dd className="nums flex items-center gap-1 text-right font-mono font-semibold">
|
||||
{revealed && winner === id ? (
|
||||
<Trophy className="h-3.5 w-3.5 text-positive" aria-hidden="true" />
|
||||
<Trophy className="size-3.5 text-positive" aria-hidden="true" />
|
||||
) : null}
|
||||
{formatOrDash(run.total)}
|
||||
</dd>
|
||||
@@ -150,21 +147,13 @@ export function BlindCompare<T>({
|
||||
|
||||
{!revealed ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commit('tie')}
|
||||
className="tap rounded-lg border border-border px-3 text-sm font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
|
||||
>
|
||||
<Button variant="outline" size="touch" onClick={() => commit('tie')}>
|
||||
Too close to call
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealed(true)}
|
||||
className="tap inline-flex items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-muted transition-colors duration-2 ease-enter hover:text-fg"
|
||||
>
|
||||
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="touch" className="text-muted" onClick={() => setRevealed(true)}>
|
||||
<Eye aria-hidden="true" />
|
||||
Just show me
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p role="status" className="card bg-surface-2 p-3 text-sm leading-relaxed">
|
||||
|
||||
Reference in New Issue
Block a user