feat(web): PageShell narrow variant for dense single-column tabs

Adds a 'narrow' prop that wraps the body in 'max-w-2xl mx-auto'
while keeping the header chrome at the standard 5xl. Settings is
the first consumer — its rows are dense text and look adrift at
full width. The header still aligns with the other tabs so the
title position stays consistent.

Covered by 2 SSR tests (narrow path adds the inner wrapper, default
path doesn't).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 15:23:06 +08:00
parent 3c3a3f57d3
commit 7697ea5fcb
3 changed files with 32 additions and 3 deletions

View File

@ -8,7 +8,7 @@ import { PageShell } from "@/components/page-shell";
export default async function SettingsPage() {
const op = await getSeededOperator();
return (
<PageShell title="Settings">
<PageShell title="Settings" narrow>
<Card>
<CardHeader>
<CardTitle>Operator</CardTitle>

View File

@ -56,4 +56,25 @@ describe("PageShell", () => {
expect(html).toContain("Settings");
expect(html).toContain("just a card");
});
it("constrains the body to max-w-2xl when narrow is set", () => {
const html = renderToStaticMarkup(
<PageShell title="Settings" narrow>
<p>card</p>
</PageShell>,
);
// Outer chrome stays at 5xl so the header keeps its alignment;
// an inner wrapper drops the body to 2xl.
expect(html).toMatch(/max-w-5xl/);
expect(html).toMatch(/max-w-2xl mx-auto/);
});
it("does NOT add the inner narrow wrapper by default", () => {
const html = renderToStaticMarkup(
<PageShell title="Reminders">
<p>list</p>
</PageShell>,
);
expect(html).not.toMatch(/max-w-2xl/);
});
});

View File

@ -8,6 +8,10 @@ interface PageShellProps {
* button. When omitted, the header row collapses to just the H1
* on desktop and renders nothing on mobile. */
action?: ReactNode;
/** Constrain the body to `max-w-2xl` for dense, single-column
* content (Settings, etc). The header stays at the standard
* 5xl chrome so the title aligns with other tabs. */
narrow?: boolean;
children: ReactNode;
}
@ -18,7 +22,7 @@ interface PageShellProps {
* this so the header pattern stays consistent without each page
* repeating the same wrapper markup.
*/
export function PageShell({ title, action, children }: PageShellProps) {
export function PageShell({ title, action, narrow, children }: PageShellProps) {
return (
<div className="px-4 py-6 sm:px-6 sm:py-8 max-w-5xl mx-auto space-y-6">
<div className="flex items-center justify-end sm:justify-between gap-4">
@ -27,7 +31,11 @@ export function PageShell({ title, action, children }: PageShellProps) {
</h1>
{action}
</div>
{children}
{narrow ? (
<div className="max-w-2xl mx-auto space-y-6">{children}</div>
) : (
children
)}
</div>
);
}