'use client' /* ============================================================================ * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first) * ---------------------------------------------------------------------------- * `PageLayout` and its `TitleBlock` are a FINALIZED, locked component. They are * the canonical, stable page chrome for OpenFrame surfaces and their visual + * behavioral contract is intentionally complete. Treat this file as read-only. * * Do NOT: change the markup/CSS, swap the title typography (`text-h2`) or * subtitle (`text-h6`), re-architect this to delegate to another primitive, * add/rename props, or "unify"/"refactor"/"simplify" it. Do NOT restyle to * match some other surface. * * Why this rule exists (the incident it prevents): a refactor once re-styled * this layout (bumped the title to `text-h1`, rerouted it through a new * `PageHeader`/`PageWithHeader` chain) to "unify" page chrome. That silently * changed the look of every page rendered through `PageLayout` and had to be * fully reverted. The current code IS the reverted, correct baseline. * * MANY consumers depend on the EXACT current output — not only OpenFrame pages * but also `DevSectionPage`, `DocViewer`, and the multi-platform hub (through * its own local `PageWithHeader`). A change here ripples across all of them. * * If a new design genuinely needs different chrome: build a SEPARATE new * component for it. Never mutate this one. If you believe an edit here is * unavoidable, STOP and get explicit human sign-off first. * * SANCTIONED EXCEPTION (2026-06, explicit human sign-off): the OPTIONAL * `titleSize` prop, forwarded to `TitleBlock`. Defaults to `'h2'` — the frozen * baseline is unchanged for every existing caller. `titleSize="h1"` opts the * title up to `text-h1` (the unified Help Center pages). Additive + default- * preserving; do NOT change the default or anything else here. * * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL * `titleWrap` prop, forwarded to `TitleBlock`. Defaults to `false` (frozen * single-line truncate). Content detail pages with CMS titles pass it to let * long titles wrap instead of ellipsis-clip. Additive + default-preserving. * * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL * `subtitleRow` prop, forwarded to `TitleBlock`. Defaults to `'when-set'` (the * subtitle row renders only when there is text for it). Pages whose subtitle is * optional record data use it to choose how the row behaves while loading. * Additive + default-preserving. * ========================================================================== */ import React from 'react' import { cn } from '../../utils/cn' import type { ActionsMenuGroup } from '../ui/actions-menu' import { type PageActionButton } from '../ui/page-actions' import { TitleBlock } from './title-block' export interface PageLayoutProps { children: React.ReactNode title?: string subtitle?: string image?: { src: string; alt?: string } backButton?: { label?: string; onClick: () => void } actions?: PageActionButton[] actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary' menuActions?: ActionsMenuGroup[] /** Desktop-only slot (e.g. a `TabSelector`) rendered with the actions. Hidden on mobile. */ selector?: React.ReactNode /** Header visual variant. `card` adds a card background, border, and padding on mobile. */ headerVariant?: 'plain' | 'card' className?: string contentClassName?: string showHeader?: boolean /** Title typography size, forwarded to `TitleBlock`. Default `'h2'` (frozen * baseline). Pass `'h1'` for the unified Help Center pages. */ titleSize?: 'h1' | 'h2' /** Optional node rendered inline next to the title (e.g. a status `Tag`), forwarded to `TitleBlock`. */ titleAdornment?: React.ReactNode /** When true, the title/subtitle render as line-box-accurate skeleton bars (forwarded to * `TitleBlock`). Header height stays identical to the loaded state — for page skeletons. */ loading?: boolean /** Render the header ACTIONS as placeholders (their set depends on data still loading). */ loadingActions?: boolean /** When the subtitle row occupies the layout (forwarded to `TitleBlock`): * `'when-set'` (default), `'while-loading'` (skeleton bar while loading, collapses * if the record has no subtitle), or `'always'` (never collapses). */ subtitleRow?: 'when-set' | 'while-loading' | 'always' /** When true, a long title wraps onto multiple lines instead of the frozen single-line * ellipsis clamp (forwarded to `TitleBlock`). For content detail pages whose h1 is CMS * data of arbitrary length. Additive + default-preserving. */ titleWrap?: boolean } /** * Page layout container with consistent spacing, header, and actions. * * Uses `--spacing-system-l` as the gap between sections. */ export function PageLayout({ children, title, subtitle, image, backButton, actions, actionsVariant = 'icon-buttons', menuActions, selector, headerVariant, className, contentClassName, showHeader = true, titleSize, titleAdornment, loading, loadingActions, subtitleRow, titleWrap, }: PageLayoutProps) { const hasActions = actions && actions.length > 0 const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons' const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector || loading) return (