diff --git a/app/(control)/admin/layout.tsx b/app/(control)/admin/layout.tsx index f0741a7..16242e3 100644 --- a/app/(control)/admin/layout.tsx +++ b/app/(control)/admin/layout.tsx @@ -8,22 +8,42 @@ export default async function AdminLayout({ children }: { children: React.ReactN const locale = await controlLocale(); const t = await getTranslations({ locale, namespace: "control.shell" }); + // Grouped by the question each section answers — who wants to buy, who is + // running, what is being charged, what happened — not by the order the pages + // were built. Ten equal-weight links in a header row was a list to scan + // rather than a structure to navigate. return ( {children} diff --git a/components/control/ControlShell.tsx b/components/control/ControlShell.tsx index 2759cb7..9152da2 100644 --- a/components/control/ControlShell.tsx +++ b/components/control/ControlShell.tsx @@ -1,44 +1,58 @@ import Link from "next/link"; import BrandMark from "@/components/BrandMark"; import { logoutAction } from "@/lib/actions/auth-actions"; +import SidebarNav, { type NavGroup, type NavItem } from "./SidebarNav"; /** Shared chrome for the partner dashboard and founder admin. Labels arrive - * translated from the server layouts (control-plane i18n, sofra #9). */ + * translated from the server layouts (control-plane i18n, sofra #9). + * + * Two nav shapes, because the two surfaces are different sizes: the partner + * dashboard has one to three destinations and keeps them inline in the header + * (`nav`), while admin has ten and gets a grouped sidebar (`groups`) — ten + * equal-weight links in a row is a list to scan, not a structure to navigate. + * The sidebar collapses to a scrollable strip under the header on small + * screens rather than hiding behind a toggle: no JS, nothing to discover. */ export default function ControlShell({ title, nav, + groups, userLabel, signOutLabel, children, }: { title: string; - nav: { href: string; label: string }[]; + nav?: NavItem[]; + groups?: NavGroup[]; userLabel: string; signOutLabel: string; children: React.ReactNode; }) { + const inlineNav = nav ?? []; return ( <>
-
+
- + SofraPiwas {title} - + {inlineNav.length > 0 && ( + + )}
@@ -54,16 +68,28 @@ export default function ControlShell({
- {/* Mobile nav row */} - + {inlineNav.length > 0 && ( + /* Mobile nav row (inline shape only — the sidebar has its own) */ + + )}
-
{children}
+ + {groups ? ( +
+ +
{children}
+
+ ) : ( +
{children}
+ )} ); } diff --git a/components/control/ProvisionForm.tsx b/components/control/ProvisionForm.tsx index b8b63a9..d6eec73 100644 --- a/components/control/ProvisionForm.tsx +++ b/components/control/ProvisionForm.tsx @@ -7,6 +7,7 @@ import { type ProvisionActionState, } from "@/lib/actions/provisioning-actions"; import ActionError from "./ActionError"; +import ProvisionPicker from "./ProvisionPicker"; /** * Admin form that proposes a new tenant (ADR-012): submits to the server action @@ -75,21 +76,16 @@ export default function ProvisionForm({ disabled }: Readonly<{ disabled?: boolea aria-label={t("provision.currency")} className="input-primary" /> - -