/** * `PageHeader`, `Page`, `Stack` — the screen scaffolding every feature was * hand-rolling as a flex column plus an ad-hoc heading block. * * `PageHeader` owns a DOM contract `page-header.test.tsx` asserts: exactly one * `

` whose accessible name is the title VERBATIM. The nav-section eyebrow a * screen shows above its title is therefore a SIBLING of the `

`, never a * child — nesting it would fold it into the computed name and break every * heading assertion. The `

` is also permanently focusable * (`tabIndex={-1}`), which is what makes it addressable as a focus target * through {@link PageHeaderProps.titleRef} or `id`. */ import type { CSSProperties, ReactElement, ReactNode, Ref } from 'react'; /** The wrapper props both layout primitives share, ref included. */ interface LayoutElementProps { readonly className?: string; readonly style?: CSSProperties; /** A consumer ref for the wrapper element itself. */ readonly ref?: Ref; } export interface PageHeaderProps { /** The screen's title. It is the `

`'s accessible name, VERBATIM. */ readonly title: string; /** The screen's existing nav-section label, rendered ABOVE the h1 — never inside it. */ readonly eyebrow?: string; readonly description?: string; readonly actions?: ReactNode; /** * A consumer ref for the `

`. Published so a caller that wants to move * focus to the new screen's title — after a route change, say — has the * element without querying for it; the heading is focusable for that reason. */ readonly titleRef?: Ref; /** * An id for the `

`, not for the `
` root. The heading is the element * a caller has a reason to address — an `aria-labelledby` pointing at it names * the region from the screen's own title — and it is the other way to reach it * as a focus target. */ readonly id?: string; } export function PageHeader({ title, eyebrow, description, actions, titleRef, id, }: PageHeaderProps): ReactElement { return (
{eyebrow === undefined ? null :
{eyebrow}
}

{title}

{description === undefined ? null :

{description}

}
{actions === undefined ? null :
{actions}
}
); } export interface PageLayoutProps extends LayoutElementProps { readonly children: ReactNode; } /** The screen wrapper: the max-width, the gutters, and the vertical rhythm. */ export function Page({ children, className, style, ref }: PageLayoutProps): ReactElement { return (
{children}
); } export interface StackProps extends LayoutElementProps { readonly children: ReactNode; /** The gap step; 4 is the default and needs no modifier class. */ readonly gap?: 2 | 3 | 4 | 6; } /** The modifier class per non-default gap step; gap 4 is `.tai-stack`'s own gap. */ const STACK_GAP_CLASS: Record<2 | 3 | 6, string> = { 2: 'tai-stack-2', 3: 'tai-stack-3', 6: 'tai-stack-6', }; /** A vertical flex column on the spacing scale. */ export function Stack({ children, gap = 4, className, style, ref }: StackProps): ReactElement { const classes = ['tai-stack']; if (gap !== 4) classes.push(STACK_GAP_CLASS[gap]); if (className !== undefined) classes.push(className); return (
{children}
); }