import { lazy, type ComponentType, type ReactNode } from 'react';
import { Skeleton } from '@/components/skeleton';
import type { Messages } from './i18n';
import { OverviewPage } from './pages/overview';
/**
* The pages that are not a component.
*
* There used to be no such list: `app.tsx` resolved the path through an if/else
* chain and `chrome/ui/sidebar.tsx` hard-coded the same five links a second time,
* so adding a page meant editing two files and keeping their order in step by
* hand. Both now read this array, which is also the order the rail draws.
*
* A component page is *not* here — those come from the registry, which already
* owns its own ordering and grouping. `app.tsx` falls through to `findEntry()`
* when nothing here matches.
*/
/** Which block of the rail a route belongs to. `null` = reachable, not listed. */
export type NavGroup = 'foundations';
export interface RouteDef {
/** Path without the leading slash. `''` is the site root. */
slug: string;
nav: NavGroup | null;
/** Rail label. Takes the catalogue rather than a key so TypeScript checks it. */
label: (m: Messages) => string;
/** Breadcrumb title — several routes share one. */
title: (m: Messages) => string;
Component: ComponentType;
/**
* What `app.tsx` shows while this route's chunk is in flight, when the plain
* spinner is not enough. Only the icons page declares one: it is by far the
* largest chunk, and a grid of the shape that is coming holds the scroll
* position instead of collapsing the page to one line and then reflowing it.
*/
fallback?: ReactNode;
}
/**
* The icons page's placeholder — the same 7rem auto-fill grid its cells sit in.
*
* Eager on purpose: it is three elements and it lives in the chunk that is
* *waiting*, so putting it in the lazy module would mean it arrived with the
* thing it was meant to stand in for.
*/
function IconGridSkeleton() {
return (
{Array.from({ length: 24 }, (_, i) => (
))}
);
}
/*
* Everything but the overview is lazy.
*
* This is not premature: the site compiled to a single 1.6 MB chunk, and the
* icons page alone accounts for most of it — it renders the whole 250-icon set,
* so it defeats the tree-shaking every other importer of `@/icons` relies on.
* The parity page is the other one worth splitting; it pulls the whole
* `figma-spec.json` capture.
*
* The overview stays eager because it *is* the first paint, and it imports the
* registry — which every component page needs too, so deferring it would buy a
* spinner and no bytes.
*
* `.then(...)` unwraps a named export: these pages predate this file and export
* `TokensPage` rather than a default, which is the better shape for everything
* except `React.lazy`.
*/
const IntroductionPage = lazy(() =>
import('./pages/introduction').then((m) => ({ default: m.IntroductionPage }))
);
const TokensPage = lazy(() => import('./pages/tokens').then((m) => ({ default: m.TokensPage })));
const IconsPage = lazy(() => import('./pages/icons').then((m) => ({ default: m.IconsPage })));
const ButtonsPage = lazy(() => import('./pages/buttons').then((m) => ({ default: m.ButtonsPage })));
const AlertsPage = lazy(() => import('./pages/alerts').then((m) => ({ default: m.AlertsPage })));
const AvatarsPage = lazy(() =>
import('./pages/avatars').then((m) => ({ default: m.AvatarsPage }))
);
const BadgesPage = lazy(() => import('./pages/badges').then((m) => ({ default: m.BadgesPage })));
const ToastsPage = lazy(() => import('./pages/toasts').then((m) => ({ default: m.ToastsPage })));
const ParityPage = lazy(() => import('./pages/parity').then((m) => ({ default: m.ParityPage })));
export const ROUTES: readonly RouteDef[] = [
{
slug: '',
nav: 'foundations',
label: (m) => m.nav.overview,
title: (m) => m.header.components,
Component: OverviewPage,
},
{
slug: 'introduction',
nav: 'foundations',
label: (m) => m.nav.introduction,
title: (m) => m.header.foundations,
Component: IntroductionPage,
},
{
slug: 'tokens',
nav: 'foundations',
label: (m) => m.nav.tokens,
title: (m) => m.header.foundations,
Component: TokensPage,
},
{
slug: 'icons',
nav: 'foundations',
label: (m) => m.nav.icons,
title: (m) => m.header.foundations,
Component: IconsPage,
fallback: ,
},
{
/* Plural — the registry entry for the component itself is `button`. */
slug: 'buttons',
nav: 'foundations',
label: (m) => m.nav.buttons,
title: (m) => m.header.foundations,
Component: ButtonsPage,
},
{
/* Plural — the registry entry for the component itself is `alert`. */
slug: 'alerts',
nav: 'foundations',
label: (m) => m.nav.alerts,
title: (m) => m.header.foundations,
Component: AlertsPage,
},
{
/* Plural — the registry entry for the component itself is `avatar`. */
slug: 'avatars',
nav: 'foundations',
label: (m) => m.nav.avatars,
title: (m) => m.header.foundations,
Component: AvatarsPage,
},
{
/* Plural — the registry entry for the component itself is `badge`. */
slug: 'badges',
nav: 'foundations',
label: (m) => m.nav.badges,
title: (m) => m.header.foundations,
Component: BadgesPage,
},
{
/* Plural — the registry entry for the component itself is `toast`. */
slug: 'toasts',
nav: 'foundations',
label: (m) => m.nav.toasts,
title: (m) => m.header.foundations,
Component: ToastsPage,
},
{
slug: 'parity',
nav: 'foundations',
label: (m) => m.nav.parity,
title: (m) => m.header.foundations,
Component: ParityPage,
},
];
export function findRoute(slug: string): RouteDef | undefined {
return ROUTES.find((r) => r.slug === slug);
}
/** The rail's fixed block, in declaration order. */
export const NAV_ROUTES: readonly RouteDef[] = ROUTES.filter((r) => r.nav !== null);