/**
* `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 (
);
}
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}
);
}