/** * scaffold-layout/generate.ts — Build PageTemplate.tsx. * * ONLY PageTemplate: the application chrome (header, sidebar, mobile shell, * bottom nav) is rendered by @atlashub/smartstack itself. The generated * App.tsx mounts `` alone and the package wraps it — so a * locally emitted AppShell/Sidebar was never mounted, carried no mobile * branch, and drifted away from the package on every release. They are no * longer emitted (see SKILL.md § "Chrome comes from the package"). * * Pure function — no I/O. */ import type { GeneratedFile, ScaffoldLayoutInput } from './types.js'; const HEADER = `/* AUTO-GENERATED — re-run scaffold-layout to refresh. Add @customised at the top to opt out. */`; function pageTemplate(): string { return `${HEADER} import { useEffect, type ReactNode, Fragment } from 'react'; import { Link } from 'react-router-dom'; import { ChevronRight } from 'lucide-react'; export interface BreadcrumbItem { readonly label: string; readonly href?: string; } export interface PageTemplateProps { title: string; subtitle?: string | null; icon?: ReactNode; actions?: ReactNode; breadcrumbs?: BreadcrumbItem[]; children: ReactNode; className?: string; contentClassName?: string; /** * Page content width. The ONLY two sanctioned layouts — no 3/4, no ad-hoc widths. * - 'standard' (default): full-width fluid. Lists, tables, dashboards, config. * - 'focused': a centered column capped at --content-max-narrow (responsive). * Single-task flows: create/edit forms, wizards, empty/onboarding. * The horizontal gutter (--content-gutter-x) lives on the SCROLL CONTAINER * of the package chrome (desktop shell
, mobile shell content area), * NOT here — applied ONCE, so it never doubles when PageTemplate nests * inside an already-padded shell. */ width?: 'standard' | 'focused'; } export function PageTemplate({ title, subtitle, icon, actions, breadcrumbs, children, className, contentClassName, width = 'standard', }: PageTemplateProps) { useEffect(() => { const prev = document.title; document.title = title; return () => { document.title = prev; }; }, [title]); return (
{breadcrumbs && breadcrumbs.length > 0 && ( )} {/* The row WRAPS, and the title block carries a real flex-basis so that it can. Without both, the actions block is the only thing that can give way: it shrinks past its own min-content and the button labels break in two ("Grilles / concurrentes") — which is exactly what a tablet used to show. The actions block itself is neither shrink-0 (that pins it at max-content and pushes a horizontal scrollbar onto the page) nor min-w-0 (that lets it shrink below a button); its default min-content is ONE button wide, so it degrades by stacking its buttons instead. */}
{/* h-8 tracks the text-2xl line box: the icon belongs to the FIRST line of the title, not to the middle of title + subtitle. */} {icon && {icon}} {/* Title and subtitle share this column, so the subtitle lines up with the title instead of hanging to its left, under the icon. */}

{title}

{subtitle &&

{subtitle}

}
{actions &&
{actions}
}
{children}
); } `; } export function generate(_spec: ScaffoldLayoutInput): GeneratedFile[] { return [{ path: 'src/components/ui/PageTemplate.tsx', content: pageTemplate() }]; }