Files
PIG-Demo/src/components/site/SiteHeader.tsx
T
karti-ai 6301a1d174 Prerender, social cards, and a yellow that reads as yellow
Two silent bugs in the prerender pass, and the second was caused by the fix for
the first.

`waitForSelector('#root > *')` defaults to waiting for VISIBILITY, and the app's
first child is the skip link, which is hidden until focused. So it burned the
full 30s timeout on every one of 17 routes — twelve minutes of a script that
printed nothing, because its output was buffered behind a pipe — while the page
had rendered the whole time. Switching to `state: 'attached'` then fired too
early instead: useSeo writes the head from an effect, so the title was still
index.html's for a tick, and every route would have baked the homepage's head.
That is the exact bug this script exists to prevent. It now waits for `main`,
then for readyState, then settles.

The board's yellow was --warning, 32 95% 31% — darkened until white text cleared
4.5:1, and at that lightness it renders BROWN. On a board where people arrive
knowing this square should be yellow, a brown square reads as a bug in the
scorer, which on a page arguing "the grader is correct" is the worst thing it
could look like. The fill is now a real yellow and the glyph went dark: more
expected AND higher contrast, 10.02:1 against 5.03:1.

Also measured something the Honesty page had honestly declined to claim. It said
our word list is easier than the original's because our dictionary rule keeps
plurals the original's editor removed by hand. Running the same greedy solver
over both pools, 250 sampled words each: original 2,315 needs 3.552 guesses
(opener RAISE, worst 5), ours 4,603 needs 3.700 (opener TARES, worst 6). The
doubled pool outweighs the plurals. Ours is harder, and the page now says so
with the table.

177 gate checks pass. Entry chunk 106.9 kB gzipped against 160 kB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
2026-08-28 16:27:26 -07:00

556 lines
20 KiB
TypeScript

import * as React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, ArrowUpRight, Github, Menu } from 'lucide-react';
import { listDemos } from '@/lib/demo-kit/registry';
import type { DemoMeta } from '@/lib/demo-kit/types';
import { lineup, routes } from '@/content/lineup';
import type { VerticalEntry } from '@/content/verticals';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { ContrastToggle } from '@/components/site/ContrastToggle';
import { DemoIcon } from '@/components/site/DemoIcon';
import { PIG_URL, REPO_URL, VERIFIERS_WORDLE_URL } from '@/components/site/links';
import { ThemeToggle } from '@/components/site/ThemeToggle';
import { Wordmark } from '@/components/site/Wordmark';
import { cn } from '@/lib/utils';
/*
* This header is GENERATED. Nothing in it names a demo.
*
* Adding `src/demos/<slug>/` puts that demo in the Demos panel, in its
* vertical, and (if it is the first live one) behind the CTA, with zero edits
* to this file. The only hand-written lists here are the five concepts under
* "How it works", which are properties of the idea rather than of the lineup.
*/
/**
* The real upstream taskset, quoted rather than paraphrased.
*
* A mega-menu that contains source code instead of more links is the cheapest
* signal on the whole site that this is not a brochure — it is the first thing
* a technical buyer sees, and it is true before they have clicked anything.
*/
const TASKSET_SOURCE = `class WordleConfig(TextArenaConfig):
game: Literal["Wordle-v0"] = "Wordle-v0"
class WordleTaskset(TextArenaTaskset, vf.Taskset[TextArenaTask, WordleConfig]):
pass`;
const CONCEPTS: readonly { term: string; gloss: string }[] = [
{
term: 'Environment',
gloss: 'The task, the legal moves and the grader, packaged so anyone can install and run it.',
},
{
term: 'Rollout',
gloss: 'One episode. The model acts, the environment answers, and every turn is recorded.',
},
{
term: 'Reward',
gloss: 'The number the run is scored on. You write it, so you decide what "good" means.',
},
{
term: 'Harness',
gloss: 'The runner that plays a taskset against a model and keeps the receipts.',
},
{
term: 'Held-out grading',
gloss: 'Scored on problems the model has never seen, which is the only way the score means anything.',
},
];
/**
* The header's Verticals panel shows the LINEUP, not the registry's groups.
*
* `listVerticals()` only ever contains verticals that already have a demo —
* today that is one group, `reference`, which `src/content/verticals.ts` is
* explicit is not an industry and must never be presented as one. The twelve
* proposals each have a real page at `/verticals/<slug>`, so the panel links
* there. `VerticalEntry.slug` is the route's slug; the registry's `Vertical`
* key is a different, shorter string and is deliberately not used for URLs.
*/
type HeaderVertical = VerticalEntry;
function useLineup() {
return React.useMemo(() => {
// `listDemos` already returns a fresh array sorted by order then slug, and
// `listVerticals` already drops the empty verticals. Neither needs redoing
// here — if this file starts re-sorting the lineup, the header and the
// gallery will eventually disagree about what "first" means.
const demos = listDemos();
const live = demos.filter((demo) => demo.status === 'live');
const spec = demos.filter((demo) => demo.status === 'spec');
// Already sorted by rank in `@/content/lineup`. Copied rather than passed
// through, because the panel props are mutable arrays and `lineup` is the
// one every page reads.
const verticals: HeaderVertical[] = [...lineup];
// The CTA follows the lineup rather than naming a slug. Today the first
// live demo IS the word game, so this resolves to the wordle route; when a
// second one ships ahead of it, the button moves with it and this file does
// not change.
const primary = live[0] ?? demos[0];
const ctaHref = primary ? `/demos/${primary.slug}` : '/demos';
return { live, spec, verticals, ctaHref };
}, []);
}
/* ------------------------------------------------------------------ panels */
/**
* Spreads `...rest` onto the Link, and that is not tidiness.
*
* Both `NavigationMenuLink asChild` and `SheetClose asChild` work by cloning
* this element and merging their own `onClick` into it. A component that takes
* `demo` and ignores everything else silently drops those handlers: the row
* still navigates, and the mega panel or the mobile sheet stays open on top of
* the page you just arrived at.
*/
function DemoRow({
demo,
muted = false,
className,
...rest
}: { demo: DemoMeta; muted?: boolean } & Omit<React.ComponentProps<typeof Link>, 'to'>) {
return (
<Link
to={`/demos/${demo.slug}`}
className={cn(
'flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2',
muted && 'opacity-70 hover:opacity-100',
className,
)}
{...rest}
>
<DemoIcon name={demo.icon} className="mt-0.5 size-4 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="flex items-center gap-2 text-sm font-medium text-fg">
{demo.title}
{demo.status === 'spec' ? (
<Badge variant="outline" className="font-normal">
Spec
</Badge>
) : null}
</span>
<span className="text-xs leading-relaxed text-muted">
{/*
A spec demo shows the BUYER, not the technology: "Head of Claims"
says who is meant to care, where a taskset name says nothing to the
person reading this in a boardroom.
*/}
{demo.status === 'spec' ? `For the ${demo.persona}` : demo.tagline}
</span>
</span>
</Link>
);
}
function PanelHeading({ children }: { children: React.ReactNode }) {
return (
<p className="px-2.5 pb-1 text-xs font-semibold uppercase tracking-wider text-muted">
{children}
</p>
);
}
function DemosPanel({ live, spec }: { live: DemoMeta[]; spec: DemoMeta[] }) {
return (
<div className="w-[min(92vw,720px)]">
<div className="grid grid-cols-2 gap-4 p-4">
<section aria-label="Live now" className="flex flex-col gap-0.5">
<PanelHeading>Live now</PanelHeading>
{live.length > 0 ? (
live.map((demo) => (
<NavigationMenuLink asChild key={demo.slug}>
<DemoRow demo={demo} />
</NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">No interactive demos published yet.</p>
)}
</section>
<section
aria-label="Shipping next"
className="flex flex-col gap-0.5 border-l border-border pl-4"
>
<PanelHeading>Shipping next</PanelHeading>
{spec.length > 0 ? (
spec.map((demo) => (
<NavigationMenuLink asChild key={demo.slug}>
<DemoRow demo={demo} muted />
</NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">Nothing queued.</p>
)}
</section>
</div>
<div className="flex items-center justify-between gap-4 border-t border-border bg-surface-2 px-5 py-3">
<p className="text-xs text-muted">
Every demo replays a real rollout from a real environment.
</p>
<NavigationMenuLink asChild>
<Link
to="/demos"
className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
Browse all demos
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</NavigationMenuLink>
</div>
</div>
);
}
function VerticalsPanel({ verticals }: { verticals: HeaderVertical[] }) {
return (
<div className="w-[min(92vw,720px)] p-4">
{verticals.length > 0 ? (
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5">
{verticals.map((vertical) => (
<NavigationMenuLink asChild key={vertical.slug}>
<Link
to={routes.vertical(vertical.slug)}
className="flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2"
>
<DemoIcon name={vertical.icon} className="mt-0.5 size-4 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium text-fg">{vertical.title}</span>
{/* The budget holder, not the reward sentence: a nav row has
one line, and "who buys this" is the more useful half. */}
<span className="text-xs leading-relaxed text-muted">{vertical.persona}</span>
</span>
</Link>
</NavigationMenuLink>
))}
</div>
) : (
<p className="p-2.5 text-xs text-muted">No verticals in the lineup yet.</p>
)}
</div>
);
}
function TasksetSource({ className }: { className?: string }) {
return (
<div className={cn('flex flex-col gap-2 rounded-lg border border-border bg-bg p-3', className)}>
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
The actual taskset
</p>
{/*
Wrapped, not scrolled. The source is quoted verbatim — reformatting it
to fit would make it stop being a quote — and the widest line is a third
wider than this column at any font size worth reading. In a panel whose
entire job is "this is real code", a visible wrap beats a third of the
line hidden behind a scrollbar nobody in a boardroom will drag.
*/}
<pre className="overflow-x-auto whitespace-pre-wrap break-words text-[11px] leading-relaxed text-fg">
<code className="font-mono">{TASKSET_SOURCE}</code>
</pre>
<a
href={VERIFIERS_WORDLE_URL}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
verifiers/environments/wordle
<ArrowUpRight aria-hidden="true" className="size-3.5" />
</a>
</div>
);
}
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
return (
<div
// 820px, not more: the panel hangs off the LEFT edge of the nav, which
// starts about 100px in, so anything wider pushes past the right edge of
// a 1024px laptop and gives the whole page a horizontal scrollbar.
className="w-[min(90vw,820px)] p-4"
>
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-4">
<dl className="flex flex-col gap-3">
{CONCEPTS.map((concept) => (
<div key={concept.term} className="flex flex-col gap-0.5">
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
</div>
))}
<NavigationMenuLink asChild>
<Link
to={ctaHref}
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
See all five on one recorded run
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</NavigationMenuLink>
</dl>
<TasksetSource />
</div>
</div>
);
}
/* ------------------------------------------------------------------ mobile */
function MobileNav({
live,
spec,
verticals,
ctaHref,
}: {
live: DemoMeta[];
spec: DemoMeta[];
verticals: HeaderVertical[];
ctaHref: string;
}) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon-touch" aria-label="Open menu">
<Menu aria-hidden="true" />
</Button>
</SheetTrigger>
{/*
The sheet is a flex column: header, a scrolling body, then a pinned
footer. Scrolling the BODY rather than the content root is what keeps a
long lineup reachable on a short phone, and `overscroll-contain` on it
stops the flick chaining into the page underneath.
*/}
<SheetContent side="right" className="w-[min(92vw,24rem)]">
<SheetHeader>
<SheetTitle>Menu</SheetTitle>
<SheetDescription className="sr-only">
Demos, verticals, and how these environments work.
</SheetDescription>
</SheetHeader>
<SheetBody>
<Accordion type="multiple" defaultValue={['demos']}>
<AccordionItem value="demos">
<AccordionTrigger>Demos</AccordionTrigger>
<AccordionContent className="flex flex-col gap-0.5">
<PanelHeading>Live now</PanelHeading>
{live.map((demo) => (
<SheetClose asChild key={demo.slug}>
<DemoRow demo={demo} />
</SheetClose>
))}
<PanelHeading>Shipping next</PanelHeading>
{spec.map((demo) => (
<SheetClose asChild key={demo.slug}>
<DemoRow demo={demo} muted />
</SheetClose>
))}
<SheetClose asChild>
<Link
to="/demos"
className="tap inline-flex items-center gap-1 p-2.5 text-xs font-medium text-accent-fg"
>
Browse all demos
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</SheetClose>
</AccordionContent>
</AccordionItem>
<AccordionItem value="verticals">
<AccordionTrigger>Verticals</AccordionTrigger>
<AccordionContent className="flex flex-col gap-0.5">
{verticals.map((vertical) => (
<SheetClose asChild key={vertical.slug}>
<Link
to={routes.vertical(vertical.slug)}
className="flex gap-3 rounded-lg p-2.5 hover:bg-surface-2"
>
<DemoIcon name={vertical.icon} className="mt-0.5 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium text-fg">{vertical.title}</span>
<span className="text-xs leading-relaxed text-muted">
{vertical.persona}
</span>
</span>
</Link>
</SheetClose>
))}
</AccordionContent>
</AccordionItem>
<AccordionItem value="how">
<AccordionTrigger>How it works</AccordionTrigger>
<AccordionContent className="flex flex-col gap-3">
<dl className="flex flex-col gap-3">
{CONCEPTS.map((concept) => (
<div key={concept.term} className="flex flex-col gap-0.5">
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
</div>
))}
</dl>
<TasksetSource />
</AccordionContent>
</AccordionItem>
</Accordion>
<a
href={PIG_URL}
target="_blank"
rel="noreferrer noopener"
className="tap mt-2 flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-4 text-muted" />
</a>
<a
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
className="tap flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
>
Source on GitHub
<Github aria-hidden="true" className="size-4 text-muted" />
</a>
<div className="flex items-center gap-1 pt-3">
<ThemeToggle />
<ContrastToggle />
</div>
</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button asChild size="lg" className="w-full">
<Link to={ctaHref}>Play the demo</Link>
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
/* ------------------------------------------------------------------ header */
export function SiteHeader() {
const { live, spec, verticals, ctaHref } = useLineup();
return (
<header
// The height token already folds in --safe-top, so the padding and the
// height come from the same source and a notch cannot push the row off
// the bottom edge of the bar.
className="sticky top-0 z-50 h-[var(--app-header-h)] border-b border-border bg-surface/80 pt-[var(--safe-top)] backdrop-blur-md supports-[backdrop-filter]:bg-surface/70"
>
<div className="mx-auto flex h-full max-w-canvas items-center gap-2 px-4 pl-[max(1rem,var(--safe-left))] pr-[max(1rem,var(--safe-right))]">
<Link
to="/"
className="tap flex shrink-0 items-center rounded-md pr-2 text-base"
aria-label="PIG demo, home"
>
<Wordmark />
</Link>
{/*
`h-full` is not cosmetic. The viewport hangs off the Root's
`top-full`, so a Root only as tall as its 36px triggers drops the
panel INSIDE the header, over its own bottom border.
*/}
<NavigationMenu className="hidden h-full lg:flex" delayDuration={120}>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Demos</NavigationMenuTrigger>
<NavigationMenuContent>
<DemosPanel live={live} spec={spec} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Verticals</NavigationMenuTrigger>
<NavigationMenuContent>
<VerticalsPanel verticals={verticals} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>How it works</NavigationMenuTrigger>
<NavigationMenuContent>
<HowItWorksPanel ctaHref={ctaHref} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href={PIG_URL}
target="_blank"
rel="noreferrer noopener"
className={cn(navigationMenuTriggerStyle(), 'text-muted hover:text-fg')}
>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-3.5" />
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<div className="ml-auto flex items-center gap-1">
<div className="hidden items-center gap-1 lg:flex">
<ThemeToggle />
<ContrastToggle />
<Button asChild variant="ghost" size="icon">
<a
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
aria-label="Source on GitHub"
>
<Github aria-hidden="true" />
</a>
</Button>
</div>
<Button asChild size="touch" className="hidden lg:inline-flex">
<Link to={ctaHref}>Play the demo</Link>
</Button>
<div className="lg:hidden">
<MobileNav live={live} spec={spec} verticals={verticals} ctaHref={ctaHref} />
</div>
</div>
</div>
</header>
);
}