import { Link } from "@tanstack/react-router"; import { Activity, GaugeCircle, KeyRound, LibraryBig, ScrollText, Server, Settings, } from "lucide-react"; import { motion, stagger } from "motion/react"; import type { ReactNode } 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: "/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; 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 tab bar (< sm): the primary navigation on phones. */}
); } 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) => ( ))}
); }