"use client"; /** * `` — sidebar + main column shell for the v1.4 settings split. * * Each section lives at its own route (`/settings/[section]/page.tsx`) and * mounts inside this shell. The shell owns nothing but navigation: the active * section is read from the route, the section heading + content are rendered * by the page itself. * * Mobile-first per `docs/ui-guidelines.md`: * - =md: sticky 220px sidebar + content column with `max-w-screen-xl`. */ import * as React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Bell, Info, KeyRound, LayoutDashboard, Link2, Settings2, Sparkles, User, type LucideIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; import { SETTINGS_SECTION_SLUGS, isSettingsSectionSlug, type SettingsSectionSlug, } from "./section-slugs"; // Re-export so existing call-sites importing from this module keep working. // New server-side call-sites (e.g. `generateStaticParams()`) should import // from `./section-slugs` directly, since this file is `"use client"` and // values exported from a client module become unusable proxies on the // server. (Discovered the hard way: `SETTINGS_SECTION_SLUGS.map(...)` in // `generateStaticParams()` blows up at build time when imported through a // client boundary.) export { SETTINGS_SECTION_SLUGS, isSettingsSectionSlug, type SettingsSectionSlug, }; interface SettingsSection { slug: SettingsSectionSlug; /** i18n key under `settings.sections..title`. */ titleKey: string; icon: LucideIcon; } /** * Source of truth for the section list. Order matches the in-app navigation * order so the sidebar, mobile strip, and `generateStaticParams()` all line * up. Don't reorder without updating tests. */ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ { slug: "account", titleKey: "settings.sections.account.title", icon: User }, { slug: "integrations", titleKey: "settings.sections.integrations.title", icon: Link2, }, { slug: "notifications", titleKey: "settings.sections.notifications.title", icon: Bell, }, { slug: "dashboard", titleKey: "settings.sections.dashboard.title", icon: LayoutDashboard, }, { slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles }, { slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound }, { slug: "advanced", titleKey: "settings.sections.advanced.title", icon: Settings2, }, { slug: "about", titleKey: "settings.sections.about.title", icon: Info }, ] as const; export interface SettingsShellProps { /** * Optional override for the active section. When omitted the shell reads * the active slug from `usePathname()` (the production behaviour). Tests * pass an explicit value so they don't need to mock the router. */ active?: SettingsSectionSlug; children: React.ReactNode; } function deriveActiveSlug( pathname: string | null, override?: SettingsSectionSlug, ): SettingsSectionSlug { if (override) return override; if (!pathname) return "account"; // Match `/settings/` and ignore any trailing segments (none today, // but cheap insurance for future nested routes). const match = pathname.match(/^\/settings\/([^/]+)/); const candidate = match?.[1] ?? ""; return isSettingsSectionSlug(candidate) ? candidate : "account"; } export function SettingsShell({ active, children }: SettingsShellProps) { const pathname = usePathname(); const { t } = useTranslations(); const activeSlug = deriveActiveSlug(pathname, active); return (
{/* Mobile section strip — horizontal scroll, hidden on md+. */}
{/* Desktop sticky sidebar */} {/* Main column — page renders its own h1 + subtitle */}
{children}
); }