import { Link, useRouterState } from "@tanstack/react-router"; import { Activity, GaugeCircle, KeyRound, LibraryBig, MonitorPlay, MoreHorizontal, ScrollText, Server, Settings, } from "lucide-react"; import { motion, stagger } from "motion/react"; import { type ReactNode, useState } from "react"; import { BrandMark } from "@/components/brand-mark"; import { Wordmark } from "@/components/wordmark"; import { changeLocale, type Locale, locales, useLocale } from "@/lib/i18n"; import { cn } from "@/lib/utils"; import { m } from "@/paraglide/messages"; const MLink = motion(Link); const NAV = [ { to: "/", icon: Activity, label: () => m.nav_dashboard() }, { to: "/host", icon: Server, label: () => m.nav_host() }, { to: "/displays", icon: MonitorPlay, label: () => m.nav_displays() }, { to: "/library", icon: LibraryBig, label: () => m.nav_library() }, { to: "/stats", icon: GaugeCircle, label: () => m.nav_stats() }, { to: "/logs", icon: ScrollText, label: () => m.nav_logs() }, { to: "/pairing", icon: KeyRound, label: () => m.nav_pairing() }, { to: "/settings", icon: Settings, label: () => m.nav_settings() }, ] as const; // On phones a flat 8-tab bar is too cramped, so the first four are pinned and the rest live behind a // "More" tab that opens a sheet above the bar. Keep it ≤ 5 slots including "More". const MOBILE_PRIMARY = NAV.slice(0, 4); const MOBILE_OVERFLOW = NAV.slice(4); export function AppShell({ children }: { children: ReactNode }) { // Read the locale so the whole shell re-renders on a language switch. useLocale(); return (
{/* Desktop sidebar (≥ sm). Sticky at viewport height: the page (body) scrolls with long content, but the sidebar stays pinned — the explicit h-dvh stops the flex stretch that would otherwise grow it (and push the language switcher) below the fold. overflow-y-auto lets the nav itself scroll on very short viewports. */}
{/* Mobile top bar (< sm): brand + language. The sidebar is hidden here. */}
{/* pb-24 leaves room for the fixed bottom nav on mobile. */}
{children}
); } /** Mobile bottom navigation (< sm): four pinned tabs + a "More" tab whose sheet holds the rest. */ function MobileNav() { const [moreOpen, setMoreOpen] = useState(false); const pathname = useRouterState({ select: (s) => s.location.pathname }); // Highlight "More" when the current route lives in the overflow. const overflowActive = MOBILE_OVERFLOW.some( (n) => pathname === n.to || pathname.startsWith(`${n.to}/`), ); // Fixed two-line-tall label box so a 1- or 2-line label (labels vary by locale) keeps every icon // at the same height. const tab = "flex flex-1 flex-col items-center justify-center gap-1 px-0.5 py-2 text-muted-foreground transition-colors"; const lbl = "flex h-7 w-full items-center justify-center text-center text-[10px] leading-tight"; return ( <> {/* Tap-outside backdrop, under the bar (z-50) but over the page. */} {moreOpen && ( ); } function LanguageSwitcher() { const current = useLocale(); return ( // biome-ignore lint/a11y/useSemanticElements: an aria-labelled role="group" is the right pattern for this small control cluster — no single semantic element fits.
{locales.map((l: Locale) => ( ))}
); }