Files
PIG-Demo/src/components/demo/DemoCard.tsx
T
karti-ai eb88138d15 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
2026-08-28 16:09:59 -07:00

94 lines
3.6 KiB
TypeScript

import type { ComponentType } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import type { DemoMeta } from '@/lib/demo-kit/types';
import { VERTICAL_LABELS } from '@/lib/demo-kit/registry';
import { Badge } from '@/components/ui/badge';
import { DemoIcon } from '@/components/site/DemoIcon';
import { cn } from '@/lib/utils';
export interface DemoCardProps<T> {
meta: DemoMeta;
/** Defaults to the canonical demo route. */
href?: string;
/**
* The demo's OWN board, drawn compact, as the thumbnail. A screenshot would
* go stale the first time the board changed and nobody would notice; this
* cannot, because it is the same component the demo page renders.
*/
Surface?: ComponentType<{ state: T; compact?: boolean }>;
/** A representative state for the thumbnail — usually a solved board. */
thumbnailState?: T;
className?: string;
}
export function DemoCard<T>({ meta, href, Surface, thumbnailState, className }: DemoCardProps<T>) {
const to = href ?? `/demos/${meta.slug}`;
const isSpec = meta.status === 'spec';
const showSurface = Surface !== undefined && thumbnailState !== undefined;
return (
<article
className={cn(
'card group relative flex flex-col overflow-hidden transition-colors duration-2 ease-enter hover:border-brand/40',
className,
)}
>
<div className="flex items-start gap-3 p-4 pb-3">
<span className="grid size-10 shrink-0 place-items-center rounded-lg bg-accent-subtle text-accent-fg">
<DemoIcon name={meta.icon} className="size-5" />
</span>
<div className="min-w-0 flex-1">
<h3 className="text-base font-semibold leading-tight">
{/* Stretched link: the whole card is the hit target, but there is
still exactly ONE link in the accessibility tree for it. */}
<Link to={to} className="after:absolute after:inset-0 after:content-['']">
{meta.title}
</Link>
</h3>
<p className="mt-0.5 text-sm leading-snug text-muted">{meta.tagline}</p>
</div>
{isSpec ? (
<Badge variant="outline" className="shrink-0 uppercase tracking-wide">
Spec
</Badge>
) : null}
</div>
{showSurface ? (
<div className="mx-4 overflow-hidden rounded-lg bg-surface-2 p-3">
{/* Decorative: the title and tagline already name the demo, and a
board with no run behind it is not information. */}
<div aria-hidden="true">
<Surface state={thumbnailState as T} compact />
</div>
</div>
) : null}
<dl className="mt-3 flex flex-wrap gap-x-4 gap-y-1 px-4 text-xs">
<div className="flex gap-1">
<dt className="text-muted">For</dt>
<dd className="font-medium">{meta.persona}</dd>
</div>
<div className="flex gap-1">
<dt className="text-muted">Vertical</dt>
<dd className="font-medium">{VERTICAL_LABELS[meta.vertical]}</dd>
</div>
</dl>
<p className="mt-2 px-4 pb-4 text-xs leading-relaxed text-muted">
<span className="font-medium text-fg">Reward: </span>
{meta.rewardLine}
</p>
<p className="mt-auto flex items-center gap-1 border-t border-border px-4 py-2.5 text-sm font-medium text-accent-fg">
{isSpec ? 'Read the specification' : 'Open the demo'}
<ArrowRight
className="size-4 transition-transform duration-2 ease-enter group-hover:translate-x-0.5"
aria-hidden="true"
/>
</p>
</article>
);
}