Files
PIG-Demo/src/router.tsx
T
karti-ai 6c2f3899c8
ci / web (push) Successful in 2m42s
ci / python (push) Successful in 2m35s
Move the canonical repo to Gitea; GitHub becomes a private archive
git.karti.ai/PIG/PIG-Demo is now the source of truth, public and anonymously
cloneable. Every self-link in the site and the docs repoints there — Gitea
serves file paths at /src/branch/main/, not /blob/main/, so those needed
rewriting rather than a hostname swap.

CI moves with it. An archived GitHub repo is read-only and its Actions stop
firing, so leaving the workflow there would have meant a repo whose gates
silently never run. .github/ is deleted rather than kept for reference: a
workflow that can never execute is worse than no workflow, because it looks
like coverage.

The Gitea workflow is not a copy. That runner is aarch64 and installs pnpm
through corepack from `packageManager` rather than pnpm/action-setup, uses
checkout@v4 and setup-node@v4, and fetches uv from astral.sh directly. It is
also configured `container.network: host` — nothing here needs a service
container, but the comment says so, because that setting cost the sibling repo
three failed runs.

The header and footer icon changed from the GitHub mark to a neutral one. A
GitHub logo pointing at a Gitea instance is a small lie about where the code
lives, on a site whose argument is that you can go and check it.

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

245 lines
8.1 KiB
TypeScript

/**
* Routes, built from the registry.
*
* There is exactly one demo route — `/demos/:slug` — and it resolves through
* `@/lib/demo-kit/registry`. Adding a demo to this site is creating a directory
* under `src/demos/`; it is not, and must never become, an edit to this file.
* The same goes for `/verticals/:slug`, which groups whatever the registry
* found rather than a list written down anywhere.
*
* Every route below `/` sits under one layout with a root error boundary, and
* the demo route adds its own on top. That nesting is the point: a demo whose
* chunk fails to load, or whose module is malformed, renders a card inside the
* normal page chrome. A single broken demo taking the whole site to a white
* screen would be the most expensive bug this repo could ship, given what the
* site is arguing.
*/
import type { ReactNode } from 'react';
import {
createBrowserRouter,
isRouteErrorResponse,
Link,
Outlet,
ScrollRestoration,
useRouteError,
type LoaderFunctionArgs,
type RouteObject,
} from 'react-router-dom';
import { getDemo, loadDemoModule } from '@/lib/demo-kit/registry';
import { DemoErrorBoundary } from '@/components/demo/DemoErrorBoundary';
import { SiteFooter } from '@/components/site/SiteFooter';
import { SiteHeader } from '@/components/site/SiteHeader';
import { SkipLink } from '@/components/site/SkipLink';
import * as s from '@/content/styles';
import Home from '@/pages/Home';
/**
* The one element that renders on every route.
*
* The header, the footer and the skip link live here rather than in each page,
* so a new page cannot ship without them. `#main` is this wrapper, not the
* page's own `<main>`: pages own their landmark, and a second `<main>` around
* theirs would be invalid HTML and a duplicate landmark in a screen reader's
* rotor. The wrapper is only what the skip link focuses.
*/
export function RootLayout(): ReactNode {
return (
<div className="flex min-h-dvh flex-col bg-bg text-fg">
<SkipLink />
<SiteHeader />
{/* Restores scroll on Back, and puts a fresh route at the top rather
than halfway down the previous page. */}
<ScrollRestoration />
<div id="main" tabIndex={-1} className="flex-1 outline-none">
<Outlet />
</div>
<SiteFooter />
</div>
);
}
function ErrorCard({
heading,
body,
detail,
}: {
heading: string;
body: string;
detail?: string;
}): ReactNode {
return (
<main className={`${s.shell} py-16 sm:py-24`}>
<div className="card max-w-2xl p-6 sm:p-8" role="alert">
<p className={`${s.eyebrow} text-danger`}>Something broke</p>
<h1 className={`${s.h2} mt-2`}>{heading}</h1>
<p className={`${s.prose} mt-3`}>{body}</p>
{detail ? (
<pre className="mt-4 overflow-x-auto rounded-md border border-border bg-surface-2 p-3 text-xs text-muted">
<code>{detail}</code>
</pre>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
<Link to="/gallery" className={s.btnPrimary}>
Back to the demos
</Link>
<Link to="/" className={s.btnSecondary}>
Home
</Link>
</div>
</div>
</main>
);
}
/** Turns whatever react-router threw into something a person can read. */
function describeError(error: unknown): { heading: string; body: string; detail?: string } {
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return {
heading: 'That page does not exist',
body: 'The link may be from an older version of the site, or the demo it pointed at has been renamed.',
detail: typeof error.data === 'string' ? error.data : undefined,
};
}
return {
heading: `${error.status} ${error.statusText}`,
body: 'The page could not be loaded.',
detail: typeof error.data === 'string' ? error.data : undefined,
};
}
if (error instanceof Error) {
return {
heading: 'This page failed to load',
body: 'The rest of the site still works. If this keeps happening, the source is public and the issue is reproducible from it.',
detail: error.message,
};
}
return {
heading: 'This page failed to load',
body: 'The rest of the site still works.',
};
}
function RootErrorBoundary(): ReactNode {
const error = useRouteError();
const described = describeError(error);
return (
<ErrorCard
heading={described.heading}
body={described.body}
{...(described.detail === undefined ? {} : { detail: described.detail })}
/>
);
}
function DemoRouteError(): ReactNode {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return (
<ErrorCard
heading="No demo by that name"
body="Every demo on this site is a directory in the repository, so a missing one is usually a renamed slug rather than a deleted page."
{...(typeof error.data === 'string' ? { detail: error.data } : {})}
/>
);
}
const described = describeError(error);
return (
<ErrorCard
heading="This demo failed to load"
body="Only this demo is affected — the others are separate bundles and still work."
{...(described.detail === undefined ? {} : { detail: described.detail })}
/>
);
}
/** Shown while a lazy route's chunk is in flight. */
function RouteFallback(): ReactNode {
return (
<div
className="mx-auto w-full max-w-canvas px-4 py-24 sm:px-6"
role="status"
aria-live="polite"
>
<p className="text-sm text-muted">Loading</p>
</div>
);
}
/**
* Validates the slug and starts the demo's chunk during the navigation rather
* than after the page mounts. `loadDemoModule` caches the promise, so the page
* calling it again resolves from cache instead of fetching twice.
*/
async function demoLoader({ params }: LoaderFunctionArgs) {
const slug = params.slug;
if (typeof slug !== 'string' || slug === '') {
throw new Response('No demo slug in the URL.', { status: 404 });
}
const meta = getDemo(slug);
if (!meta) {
throw new Response(`No demo named "${slug}".`, { status: 404 });
}
return { meta, module: await loadDemoModule(slug) };
}
export const routes: RouteObject[] = [
{
path: '/',
element: <RootLayout />,
errorElement: <RootErrorBoundary />,
hydrateFallbackElement: <RouteFallback />,
children: [
// Home is the landing page and is imported eagerly on purpose: making the
// first paint wait on a second network round trip to save bytes on a page
// almost every visitor sees is the wrong trade.
{ index: true, element: <Home /> },
// Two paths, one page. `/gallery` is what the site's own links use;
// `/demos` is the shape people guess and the one older links used, and a
// redirect would cost a round trip to say the same thing.
{
path: 'gallery',
lazy: async () => ({ Component: (await import('@/pages/Gallery')).default }),
},
{
path: 'demos',
lazy: async () => ({ Component: (await import('@/pages/Gallery')).default }),
},
{
path: 'demos/:slug',
loader: demoLoader,
errorElement: <DemoRouteError />,
// Two boundaries, because they catch different things. `errorElement`
// above catches a loader or chunk failure; this one catches a demo whose
// own Surface throws while rendering a board state, which the router
// never sees.
lazy: async () => {
const { default: DemoPage } = await import('@/pages/DemoPage');
return {
Component: () => (
<DemoErrorBoundary>
<DemoPage />
</DemoErrorBoundary>
),
};
},
},
{
path: 'verticals/:slug',
lazy: async () => ({ Component: (await import('@/pages/Vertical')).default }),
},
{
path: 'honesty',
lazy: async () => ({ Component: (await import('@/pages/Honesty')).default }),
},
{
path: '*',
lazy: async () => ({ Component: (await import('@/pages/NotFound')).default }),
},
],
},
];
export const router = createBrowserRouter(routes);