6c2f3899c8
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
245 lines
8.1 KiB
TypeScript
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);
|