import * as React from 'react'; import { Button } from '@/components/button'; import { Spinner } from '@/components/spinner'; import { MenuIcon } from '@/icons'; import { A11yPanel } from '~/chrome/ui/a11y-panel'; import { RAIL_ID, Sidebar } from '~/chrome/ui/sidebar'; import { useRailVisible } from '~/chrome/hooks/use-rail-visible'; import { Toolbar } from '~/chrome/ui/toolbar'; import { useScrolled } from '~/chrome/hooks/use-scrolled'; import { useThemeClass } from '~/chrome/hooks/use-theme-class'; import { useTour } from '~/chrome/hooks/use-tour'; import { useGroupLabel, useMessages } from './i18n'; import { findEntry } from './registry'; import { findRoute } from './routes'; import { useParam, useRoute } from './router'; /* * Lazy for the same reason the fixed pages in `routes.tsx` are, and it matters * more here: this one entry point pulls the playground, the matrices, the props * tables, the Figma panel and `chrome/lib/codegen.ts` — whose eager glob over every * component and the whole icon barrel is the single heaviest module on the site. * None of that belongs on the landing page, which only renders previews. */ const ComponentPage = React.lazy(() => import('./pages/component').then((m) => ({ default: m.ComponentPage })) ); /** The chrome-free preview a share link points at — see `pages/embed.tsx`. */ const EmbedPage = React.lazy(() => import('./pages/embed').then((m) => ({ default: m.EmbedPage }))); /* * The intro dialog, lazy for the same reason the pages are: it pulls six live * demos, the lockup and the token table, and a returning visitor never opens * it. `useTour().mounted` is what keeps this chunk out of their download * entirely — see `chrome/hooks/use-tour.ts`. */ const Tour = React.lazy(() => import('~/chrome/ui/tour').then((m) => ({ default: m.Tour }))); const EMBED_PREFIX = '/embed/'; /** What the browser tab says, so forty pages stop sharing one title. */ const SITE = 'CBAR Design System'; /** The id `
` carries, and the only thing the skip button needs to know. */ const CONTENT_ID = 'content'; /** Module scope, so the skip button's handler is not a fresh closure per render. */ function focusContent() { const main = document.getElementById(CONTENT_ID); main?.focus(); main?.scrollTo({ top: 0 }); } export function App() { const { path } = useRoute(); const [theme] = useParam('theme', 'light'); const m = useMessages(); const groupLabel = useGroupLabel(); const [navOpen, setNavOpen] = React.useState(false); /* The desktop half of the same control. Two states rather than one, because they are two different things: below `lg` the rail is a drawer that is out or away, from `lg` up it is a column that exists or does not — and the drawer's state must not survive a widening (see `Sidebar`) while this one is remembered across visits. */ const [railVisible, toggleRail] = useRailVisible(); /* Stable, because the rail subscribes to a media query keyed on it — a fresh closure per render would tear that listener down and rebuild it on every one. */ const closeNav = React.useCallback(() => setNavOpen(false), []); /* The header's elevation. The ref lands on `main`, which is the element that actually scrolls — the document never does. */ const [scrolled, mainRef] = useScrolled(); /* The intro dialog. Called here, above the embed branch, because every hook in this component has to run on both paths — and passed `!isEmbed` rather than being moved below it, because on the embed route it must neither render nor spend the one automatic showing (`chrome/hooks/use-tour.ts`). */ const isEmbed = path.startsWith(EMBED_PREFIX); const tour = useTour(!isEmbed); /* The same hook `.storybook/preview.tsx` calls — see `chrome/hooks/use-theme-class` for why the kit's dark mode is a class rather than a media query. */ useThemeClass(theme); /* A fixed page first, then the registry, then 404. Two lookups rather than one table because the two halves are owned differently: `routes.tsx` is a hand-written list, the registry generates its own slugs. Resolved above the embed branch, not below it, so the `document.title` effect underneath can be a hook like any other — every hook in this component has to run on both paths. */ const slug = path.replace(/^\//, ''); const route = findRoute(slug); const entry = route ? undefined : findEntry(slug); /* * The breadcrumb, as the trail it claims to be. * * It used to show one crumb, and the wrong one: on `#/button` the header read * "Design System / Actions", because the title was `groupLabel(entry.group)` — * so the page you were actually on was never named anywhere above the fold. * Now it is section, then page. `RouteDef` already carries both halves * (`title` and `label`, see `routes.tsx`), so nothing new had to be invented * for the fixed pages; a registry entry supplies its group and its name. */ const crumbs: string[] = route ? [route.title(m), route.label(m)] : entry ? [groupLabel(entry.group), entry.name] : [m.header.notFound]; /* Forty pages shared one tab title, which makes a second window of the site unfindable. `SITE` last, the way a page title reads narrowest-first. */ const heading = crumbs[crumbs.length - 1]; React.useEffect(() => { document.title = `${heading} — ${SITE}`; }, [heading]); /* The embed route is a different document, not a different page: no rail, no header, no axe panel — that is the whole point of a link you send to someone who only wants to look at the component. It branches here rather than in `routes.tsx` because those entries all render *inside* the shell. After every hook, so the hook order is the same on both paths. The theme class is already applied above, and `I18nProvider` sits above `App`, so `?theme` and `?lang` still reach the preview. */ if (isEmbed) { return ( ); } let page: React.ReactNode; if (route) { page = ; } else if (entry) { page = ; } else { page = (

{m.notFound.title(path)}

{m.notFound.body}

); } return ( /* `overflow-hidden` plus the `min-h-0` on both children is what makes the shell actually 100vh. A grid item defaults to `min-height: auto`, so without it the row grows to the tallest page, `h-screen` becomes a minimum rather than a bound, `main` never scrolls, and the header — the breadcrumb and both toolbar buttons — scrolls off the top with the rest of the document. */ /* The rail track is an attribute, not a swapped class: `.cbar-shell` in `showcase.css` declares both widths one declaration apart so the collapse can be transitioned. Two Tailwind templates cannot be — `[17rem_1fr]` and `grid-cols-1` are track lists of different lengths, and a browser has nothing to interpolate between them. */
{/* First in the document, so it is the first thing Tab reaches. It is off-screen until focused — see `.cbar-skip` — rather than hidden, which would take it out of the tab order and make it decorative. A ` {/* Below `lg` this contributes no grid item at all: the rail is a kit Sheet, which portals to the body and brings its own overlay, focus trap and Escape handling — the three things the shell used to hand-roll here. */}
{/* CBAR's own documentation pages label a section above its content rather than titling the page twice, so the header is a breadcrumb: the file, then the section, then the page. */}
{/* An ordered list, because a breadcrumb is one — that is what tells a screen reader how many levels there are and which one you are on. */}
{/* `relative` is not decoration: an `sr-only` span is `position:absolute`, and with no positioned ancestor its containing block is the initial one — so it escapes this element's `overflow-y-auto` entirely and reports its static offset (a thousand-odd px down the scrolled content) as document height. That gave the window a second scrollbar beside main's. Positioning main makes it the containing block, so the span is clipped like everything else. */} {/* Every page but the overview is a lazy chunk, so the boundary has to exist. Most routes get the plain spinner: on any connection that matters these resolve within a frame, and a layout that flashes in and is replaced reads worse than one that simply appears. A route that is genuinely heavy — the 250-icon page — declares its own skeleton in `routes.tsx` instead. */} {/* `tabIndex={-1}` is what makes the skip link land: an element with no natural focusability is scrolled to but not focused, so the next Tab would carry on from the rail the user just skipped. */}
) } > {page}
{/* Outside `main` on purpose — it audits `main`, and a panel inside the region it measures would appear in its own results. Renders nothing unless `?a11y=on`, and does not exist at all in a built site. */} {/* Beside the panel rather than inside `main` for the same reason: it is a dialog over the whole shell, not part of the page. `mounted` stays true after the first close so the exit animation has something to play, while a visitor who never opens it never loads the chunk. `fallback={null}` because the only thing that could flash here is a spinner behind a modal nobody asked for yet. */} {tour.mounted ? ( ) : null} ); }