From 8c267eff2583e491e3333997b4b2088772393074 Mon Sep 17 00:00:00 2001 From: Stefan Lazic Date: Thu, 8 Oct 2026 10:37:44 +0100 Subject: [PATCH] desktop layout The shell around every page sets no content width, so each page chose its own. Business Details was centered, Services and Team were pinned left with a dead right half, and Reports, the dashboard, Current Appointment and Settings stretched edge to edge. I added a shared PageContainer with three widths (narrow, default, wide), always centered, and used it on those pages and on the settings pages. Two form layouts had their own bugs: Business Details (your screenshot): six cards in a two-column grid, so a short card next to a tall one left a big gap. It's now two independent columns of similar height. Loyalty: "Reward Rules" and "Rules & Expiry" each sat alone in the left column with an empty right side. They're now side by side. On phones, Business Details now stacks its cards in a slightly different order (Branding moves down). Tablets get a slightly wider column than before. Two things I didn't touch: Customer pages: Home and Book are a phone-width column floating in the middle of a desktop screen, with the bottom tab bar still showing. That's a bigger design decision, so tell me if you want it. Narrow pages: about 30 pages already used a centered narrow column, which looks fine. --- src/app/(owner)/owner/page.tsx | 5 +- src/app/(owner)/owner/reports/page.tsx | 5 +- src/app/(owner)/owner/services/page.tsx | 5 +- src/app/(owner)/owner/team/page.tsx | 5 +- src/app/(staff)/staff/current/page.tsx | 9 +- src/app/(staff)/staff/settings/page.tsx | 5 +- src/components/layout/page-container.tsx | 43 ++++ .../settings/settings-page-shell.tsx | 6 +- .../components/business-details-form.tsx | 233 +++++++++--------- .../owner/components/loyalty-config-form.tsx | 64 ++--- 10 files changed, 217 insertions(+), 163 deletions(-) create mode 100644 src/components/layout/page-container.tsx diff --git a/src/app/(owner)/owner/page.tsx b/src/app/(owner)/owner/page.tsx index bfa64ad..1a59f31 100644 --- a/src/app/(owner)/owner/page.tsx +++ b/src/app/(owner)/owner/page.tsx @@ -23,6 +23,7 @@ import { import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { Card } from "@/components/ui/card"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { hasPermission, isSuperAdmin } from "@/lib/auth/permissions"; @@ -135,7 +136,7 @@ export default async function OwnerDashboardPage() { leftAction={} /> -
+

{context.businessName} — today's overview

{summary.warnings.length > 0 && ( @@ -250,7 +251,7 @@ export default async function OwnerDashboardPage() { ))}
- + ); } diff --git a/src/app/(owner)/owner/reports/page.tsx b/src/app/(owner)/owner/reports/page.tsx index d065a18..69455bf 100644 --- a/src/app/(owner)/owner/reports/page.tsx +++ b/src/app/(owner)/owner/reports/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { Card } from "@/components/ui/card"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { hasPermission } from "@/lib/auth/permissions"; @@ -73,7 +74,7 @@ export default async function OwnerReportsPage({ return (
-
+

{fromDate} to {toDate}

@@ -196,7 +197,7 @@ export default async function OwnerReportsPage({ )} -
+
); } diff --git a/src/app/(owner)/owner/services/page.tsx b/src/app/(owner)/owner/services/page.tsx index f17fc7f..640b9a3 100644 --- a/src/app/(owner)/owner/services/page.tsx +++ b/src/app/(owner)/owner/services/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { hasPermission } from "@/lib/auth/permissions"; @@ -27,7 +28,7 @@ export default async function OwnerServicesPage() { return (
-
+ ({ id: c.id, @@ -48,7 +49,7 @@ export default async function OwnerServicesPage() { })), }))} /> -
+
); } diff --git a/src/app/(owner)/owner/team/page.tsx b/src/app/(owner)/owner/team/page.tsx index 0f49298..8de3314 100644 --- a/src/app/(owner)/owner/team/page.tsx +++ b/src/app/(owner)/owner/team/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { Plus, Search, ShieldCheck, ShieldPlus } from "lucide-react"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { hasPermission } from "@/lib/auth/permissions"; @@ -54,7 +55,7 @@ export default async function OwnerTeamPage({ return (
-
+
- + ); } diff --git a/src/app/(staff)/staff/current/page.tsx b/src/app/(staff)/staff/current/page.tsx index 82c4e9c..13afe5b 100644 --- a/src/app/(staff)/staff/current/page.tsx +++ b/src/app/(staff)/staff/current/page.tsx @@ -5,6 +5,7 @@ import { redirect } from "next/navigation"; import { Card } from "@/components/ui/card"; import { StatusPill } from "@/components/ui/status-pill"; import { EmptyState } from "@/components/feedback/empty-state"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { getActingStaff } from "@/lib/staff/get-acting-staff"; @@ -60,13 +61,13 @@ export default async function StaffCurrentPage() { className="bg-stone" leftAction={} /> -
+ -
+ ); } @@ -98,7 +99,7 @@ export default async function StaffCurrentPage() { className="bg-stone" leftAction={} /> -
+
@@ -243,7 +244,7 @@ export default async function StaffCurrentPage() { )}
-
+
); } diff --git a/src/app/(staff)/staff/settings/page.tsx b/src/app/(staff)/staff/settings/page.tsx index 03deffe..e4ac419 100644 --- a/src/app/(staff)/staff/settings/page.tsx +++ b/src/app/(staff)/staff/settings/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { Bell, CalendarClock, CalendarOff, CalendarSync, Clock, type LucideIcon } from "lucide-react"; import { auth } from "@/auth"; import { ProfileCard } from "@/components/ui/profile-card"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; import { SignOutButton } from "@/features/auth/components/sign-out-button"; import { getNavRole } from "@/lib/auth/permissions"; @@ -81,7 +82,7 @@ export default async function StaffSettingsPage() { leftAction={} /> -
+
- + ); } diff --git a/src/components/layout/page-container.tsx b/src/components/layout/page-container.tsx new file mode 100644 index 0000000..53beabc --- /dev/null +++ b/src/components/layout/page-container.tsx @@ -0,0 +1,43 @@ +import type { ElementType, ReactNode } from "react"; +import { cn } from "@/lib/utils/cn"; + +/** + * The one place staff/owner page content gets its desktop width. The shell + * (StaffShell) deliberately sets no content width of its own, so before + * this each page picked its own — some centered, some pinned left, some + * stretched edge to edge — and the same sidebar layout looked different + * from one page to the next. + * + * Always centred in the space beside the sidebar, never pinned left: + * - `narrow` (max-w-2xl) — a single form or detail column. + * - `default` (max-w-4xl) — settings pages and lists; room for a + * two-column form on desktop. + * - `wide` (max-w-6xl) — dashboards and reports with card grids. + * Phones are unaffected: the width cap only bites once the viewport is + * wider than the cap, and the 24px gutter is the same as before. + */ +const SIZE_CLASS = { + narrow: "max-w-2xl", + default: "max-w-4xl", + wide: "max-w-6xl", +} as const; + +export type PageContainerSize = keyof typeof SIZE_CLASS; + +export function PageContainer({ + size = "default", + as: Component = "main", + className, + children, +}: { + size?: PageContainerSize; + as?: ElementType; + className?: string; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/settings/settings-page-shell.tsx b/src/components/settings/settings-page-shell.tsx index 99e92a1..c25a9d5 100644 --- a/src/components/settings/settings-page-shell.tsx +++ b/src/components/settings/settings-page-shell.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; +import { PageContainer } from "@/components/layout/page-container"; import { TopNav } from "@/components/navigation/top-nav"; -import { cn } from "@/lib/utils/cn"; export interface SettingsPageShellProps { title: string; @@ -20,7 +20,9 @@ export function SettingsPageShell({ title, children, mainClassName }: SettingsPa return (
-
{children}
+ + {children} +
); } diff --git a/src/features/owner/components/business-details-form.tsx b/src/features/owner/components/business-details-form.tsx index e9a89b7..d797389 100644 --- a/src/features/owner/components/business-details-form.tsx +++ b/src/features/owner/components/business-details-form.tsx @@ -106,127 +106,130 @@ export function BusinessDetailsForm({ initial }: { initial: BusinessDetailsIniti return (
-
-

Profile

-
- set("name", e.target.value)} /> - set("logoUrl", url)} category="branding" /> -