/* ============================================================================ PAGE KIT - section primitives ---------------------------------------------------------------------------- The handful of layout pieces every page module repeats: the centred content column, a vertical rhythm for sections, a section header, the page hero and the empty state that renders while the CMS has no entry yet. RULES THIS FILE FOLLOWS ----------------------- * tier-2 semantic tokens only - no hex, no tier-1 primitive, no raw type size (the type role utilities carry family/size/leading/tracking/weight) * logical properties only (ps-/pe-/ms-/me-/start-/end-, text-start) so the Arabic pages mirror without a single `rtl:` override * no component here duplicates Button / Input / Card - those are imported from ~/components/ui by the pages themselves ========================================================================= */ import type { ReactNode } from "react"; export function cx(...parts: Array): string { return parts.filter(Boolean).join(" "); } /* --------------------------------------------------------------------------- LAYOUT ------------------------------------------------------------------------ */ export interface ContainerProps { children: ReactNode; className?: string; } /** The centred content column every section shares. */ export function Container({ children, className }: ContainerProps) { return (
{children}
); } export interface PageSectionProps { children: ReactNode; id?: string; /** Flip this section (and only this section) to the other surface. */ surface?: "dark" | "light"; className?: string; /** Content is placed directly in the section, without the centred column. */ bleed?: boolean; "aria-labelledby"?: string; } /** * One band of the page. `surface` re-points the whole tier-2 palette for the * subtree, so a light band needs no colour classes of its own. */ export function PageSection({ children, id, surface, className, bleed = false, ...rest }: PageSectionProps) { return (
{bleed ? children : {children}}
); } /** A readable measure for long-form prose. */ export function Prose({ children, className }: ContainerProps) { return (
{children}
); } /* --------------------------------------------------------------------------- HEADINGS ------------------------------------------------------------------------ */ export interface SectionHeaderProps { title: string; id?: string; eyebrow?: string; lead?: string; titleAs?: "h1" | "h2" | "h3"; /** Figma centres the home page's section headers; inner pages read start. */ align?: "start" | "center"; className?: string; } export function SectionHeader({ title, id, eyebrow, lead, titleAs = "h2", align = "start", className, }: SectionHeaderProps) { const Title = titleAs; const centred = align === "center"; return (
{eyebrow ? (

{eyebrow}

) : null} {title} {lead ? (

{lead}

) : null}
); } /* --------------------------------------------------------------------------- HERO ------------------------------------------------------------------------ */ export interface PageHeroProps { title: string; eyebrow?: string; lead?: string; /** Buttons, rendered inline-end of the headline on wide viewports. */ actions?: ReactNode; /** Wide picture below the headline row, as in Figma 253:4800. */ media?: { src: string; alt: string; width?: number; height?: number }; className?: string; } /** * The page hero - Figma "Hero header" (253:4800): headline on the inline-start * side, the call-to-action group on the inline-end side, and a full-width * picture underneath. The picture is only rendered when the CMS supplies one; * an empty media box would otherwise ship as a grey rectangle. */ export function PageHero({ title, eyebrow, lead, actions, media, className, }: PageHeroProps) { return (
{eyebrow ? (

{eyebrow}

) : null}

{title}

{lead ? (

{lead}

) : null}
{actions ? (
{actions}
) : null}
{media ? ( {media.alt} ) : null}
); } /* --------------------------------------------------------------------------- PANELS ------------------------------------------------------------------------ */ export interface InfoPanelProps { title: string; children?: ReactNode; /** Small line above the title - a number, a partner name, a role. */ eyebrow?: string; titleAs?: "h2" | "h3" | "h4"; className?: string; } /** * A titled block of text with no destination - a pillar, a workstream, a * principle. Deliberately NOT a Card: every card in the design system is a * link, and these are not. */ export function InfoPanel({ title, children, eyebrow, titleAs = "h3", className, }: InfoPanelProps) { const Title = titleAs; return (
{eyebrow ? (

{eyebrow}

) : null} {title} {children ? (
{children}
) : null}
); } export interface EmptyStateProps { /** Localised - never an English default. */ message: string; title?: string; className?: string; children?: ReactNode; } /** * What renders where the CMS has nothing yet. It states plainly that the * content is still to come rather than showing an empty grid, and it is always * passed a localised string by the caller. */ export function EmptyState({ message, title, className, children }: EmptyStateProps) { return (
{title ? (

{title}

) : null}

{message}

{children}
); } /* --------------------------------------------------------------------------- SMALL PIECES ------------------------------------------------------------------------ */ export interface ValueChipsProps { values: readonly string[]; label?: string; className?: string; } /** The "What guides us" row: Dialogue · Community Leadership · Learning · … */ export function ValueChips({ values, label, className }: ValueChipsProps) { if (values.length === 0) return null; return (
{label ? (

{label}

) : null}
); } export interface StatStripProps { stats: readonly { value: string; label: string }[]; className?: string; } /** * The figures band. `value` is authored in the CMS as a string, already * formatted for its locale, because the site must never invent or re-derive a * project figure - see the note the pages render when this list is empty. */ export function StatStrip({ stats, className }: StatStripProps) { if (stats.length === 0) return null; return (
{stats.map((stat) => ( /* Source order is term-then-description, as the spec requires; the reversed column paints the figure above its label without repeating the label for a screen reader. */
{stat.label}
{stat.value}
))}
); }