/**
* 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. */}