From 7697ea5fcbd5f11981d258a71e1c2f60981afc55 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 15:23:06 +0800 Subject: [PATCH] feat(web): PageShell narrow variant for dense single-column tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/web/src/app/settings/page.tsx | 2 +- apps/web/src/components/page-shell.test.tsx | 21 +++++++++++++++++++++ apps/web/src/components/page-shell.tsx | 12 ++++++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index bf54258..9ab34a9 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -8,7 +8,7 @@ import { PageShell } from "@/components/page-shell"; export default async function SettingsPage() { const op = await getSeededOperator(); return ( - + Operator diff --git a/apps/web/src/components/page-shell.test.tsx b/apps/web/src/components/page-shell.test.tsx index 63cd7a2..ec5946f 100644 --- a/apps/web/src/components/page-shell.test.tsx +++ b/apps/web/src/components/page-shell.test.tsx @@ -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( + +

card

+
, + ); + // 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( + +

list

+
, + ); + expect(html).not.toMatch(/max-w-2xl/); + }); }); diff --git a/apps/web/src/components/page-shell.tsx b/apps/web/src/components/page-shell.tsx index ad022fc..2486b62 100644 --- a/apps/web/src/components/page-shell.tsx +++ b/apps/web/src/components/page-shell.tsx @@ -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 (
@@ -27,7 +31,11 @@ export function PageShell({ title, action, children }: PageShellProps) { {action}
- {children} + {narrow ? ( +
{children}
+ ) : ( + children + )}
); }