Loading panel/index.html +12 −1 Changes for panel/index.html: 12 added lines, 1 removed line. Original line number Diff line number Diff line <!doctype html> <html lang="en" class="dark"> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Felis · Control Panel</title> <script> try { const theme = localStorage.getItem("felis-theme") || "system"; const dark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); if (dark) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } } catch (_) {} </script> </head> <body> <div id="root"></div> Loading panel/src/App.tsx +3 −0 Changes for panel/src/App.tsx: 3 added lines, 0 removed lines. Original line number Diff line number Diff line import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { ThemeProvider } from "@/lib/theme"; import { TierProvider } from "@/lib/tier"; import { AppShell } from "@/components/AppShell"; import { RequireAdmin } from "@/components/RequireAdmin"; Loading @@ -23,6 +24,7 @@ import { FleetTable } from "@/pages/ops/FleetTable"; // the split is by concern, not by tier. TierProvider fetches /me once at boot. export default function App() { return ( <ThemeProvider storageKey="felis-theme"> <TierProvider> <BrowserRouter> <Routes> Loading Loading @@ -62,5 +64,6 @@ export default function App() { </Routes> </BrowserRouter> </TierProvider> </ThemeProvider> ); } panel/src/components/AppShell.tsx +34 −4 Changes for panel/src/components/AppShell.tsx: 34 added lines, 4 removed lines. Original line number Diff line number Diff line import { useEffect } from "react"; import { NavLink, Outlet } from "react-router-dom"; import { Cat, Globe } from "lucide-react"; import { Cat, Globe, Sun, Moon } from "lucide-react"; import { useTranslation } from "react-i18next"; import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; import { useTier } from "@/lib/tier"; import { visibleSections } from "@/lib/nav"; import { Select, SelectContent, SelectItem } from "@/components/ui/select"; import { useTheme } from "@/lib/theme"; function LangToggle() { const { i18n } = useTranslation(); return ( <Select value={i18n.language} onValueChange={(v) => i18n.changeLanguage(v)}> <SelectPrimitive.Trigger className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-zinc-900/50 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-colors shrink-0"> <SelectPrimitive.Trigger className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-colors shrink-0"> <Globe className="h-4 w-4" /> </SelectPrimitive.Trigger> <SelectContent align="start" className="min-w-[6rem]"> Loading @@ -24,6 +25,26 @@ function LangToggle() { ); } function ThemeToggle() { const { theme, toggleTheme } = useTheme(); const { t } = useTranslation("common"); return ( <button onClick={toggleTheme} className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-all duration-200 active:scale-95 shrink-0" aria-label={t("toggle_theme")} title={t("toggle_theme")} > {theme === "light" ? ( <Sun className="h-4 w-4" /> ) : ( <Moon className="h-4 w-4" /> )} </button> ); } export function AppShell() { const { isAdmin } = useTier(); const { t, i18n } = useTranslation("navigation"); Loading Loading @@ -77,8 +98,11 @@ export function AppShell() { </nav> <div className="mt-auto px-3 pt-4 border-t border-border/60"> <div className="flex items-start gap-2.5"> <div className="flex items-start gap-2"> <div className="flex items-center gap-1.5 shrink-0"> <LangToggle /> <ThemeToggle /> </div> <span className="text-[10px] text-muted-foreground/60 leading-tight pt-1"> {t("common:brand_tagline")} </span> Loading @@ -87,9 +111,15 @@ export function AppShell() { </aside> <div className="flex min-w-0 flex-1 flex-col"> <header className="flex h-14 items-center gap-2 border-b border-border px-4 md:hidden"> <header className="flex h-14 items-center justify-between border-b border-border px-4 md:hidden"> <div className="flex items-center gap-2"> <Cat className="h-5 w-5 text-primary" /> <span className="font-semibold">{t("common:brand_name")}</span> </div> <div className="flex items-center gap-2"> <LangToggle /> <ThemeToggle /> </div> </header> <main className="flex-1 p-6"> <Outlet /> Loading panel/src/i18n/resources/en-US/common.json +2 −1 Changes for panel/src/i18n/resources/en-US/common.json: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -18,5 +18,6 @@ "page_title": "Felis · Control Panel", "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "Switch to {{lang}}" "lang_toggle_hint": "Switch to {{lang}}", "toggle_theme": "Toggle theme" } panel/src/i18n/resources/zh-CN/common.json +2 −1 Changes for panel/src/i18n/resources/zh-CN/common.json: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -18,5 +18,6 @@ "page_title": "Felis · 控制面板", "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "切换至 {{lang}}" "lang_toggle_hint": "切换至 {{lang}}", "toggle_theme": "切换主题" } Loading
panel/index.html +12 −1 Changes for panel/index.html: 12 added lines, 1 removed line. Original line number Diff line number Diff line <!doctype html> <html lang="en" class="dark"> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Felis · Control Panel</title> <script> try { const theme = localStorage.getItem("felis-theme") || "system"; const dark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); if (dark) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } } catch (_) {} </script> </head> <body> <div id="root"></div> Loading
panel/src/App.tsx +3 −0 Changes for panel/src/App.tsx: 3 added lines, 0 removed lines. Original line number Diff line number Diff line import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { ThemeProvider } from "@/lib/theme"; import { TierProvider } from "@/lib/tier"; import { AppShell } from "@/components/AppShell"; import { RequireAdmin } from "@/components/RequireAdmin"; Loading @@ -23,6 +24,7 @@ import { FleetTable } from "@/pages/ops/FleetTable"; // the split is by concern, not by tier. TierProvider fetches /me once at boot. export default function App() { return ( <ThemeProvider storageKey="felis-theme"> <TierProvider> <BrowserRouter> <Routes> Loading Loading @@ -62,5 +64,6 @@ export default function App() { </Routes> </BrowserRouter> </TierProvider> </ThemeProvider> ); }
panel/src/components/AppShell.tsx +34 −4 Changes for panel/src/components/AppShell.tsx: 34 added lines, 4 removed lines. Original line number Diff line number Diff line import { useEffect } from "react"; import { NavLink, Outlet } from "react-router-dom"; import { Cat, Globe } from "lucide-react"; import { Cat, Globe, Sun, Moon } from "lucide-react"; import { useTranslation } from "react-i18next"; import * as SelectPrimitive from "@radix-ui/react-select"; import { cn } from "@/lib/utils"; import { useTier } from "@/lib/tier"; import { visibleSections } from "@/lib/nav"; import { Select, SelectContent, SelectItem } from "@/components/ui/select"; import { useTheme } from "@/lib/theme"; function LangToggle() { const { i18n } = useTranslation(); return ( <Select value={i18n.language} onValueChange={(v) => i18n.changeLanguage(v)}> <SelectPrimitive.Trigger className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-zinc-900/50 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-colors shrink-0"> <SelectPrimitive.Trigger className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-colors shrink-0"> <Globe className="h-4 w-4" /> </SelectPrimitive.Trigger> <SelectContent align="start" className="min-w-[6rem]"> Loading @@ -24,6 +25,26 @@ function LangToggle() { ); } function ThemeToggle() { const { theme, toggleTheme } = useTheme(); const { t } = useTranslation("common"); return ( <button onClick={toggleTheme} className="h-7 w-7 p-0 flex items-center justify-center border border-border rounded-md bg-muted/40 hover:bg-accent text-muted-foreground hover:text-foreground focus:outline-none transition-all duration-200 active:scale-95 shrink-0" aria-label={t("toggle_theme")} title={t("toggle_theme")} > {theme === "light" ? ( <Sun className="h-4 w-4" /> ) : ( <Moon className="h-4 w-4" /> )} </button> ); } export function AppShell() { const { isAdmin } = useTier(); const { t, i18n } = useTranslation("navigation"); Loading Loading @@ -77,8 +98,11 @@ export function AppShell() { </nav> <div className="mt-auto px-3 pt-4 border-t border-border/60"> <div className="flex items-start gap-2.5"> <div className="flex items-start gap-2"> <div className="flex items-center gap-1.5 shrink-0"> <LangToggle /> <ThemeToggle /> </div> <span className="text-[10px] text-muted-foreground/60 leading-tight pt-1"> {t("common:brand_tagline")} </span> Loading @@ -87,9 +111,15 @@ export function AppShell() { </aside> <div className="flex min-w-0 flex-1 flex-col"> <header className="flex h-14 items-center gap-2 border-b border-border px-4 md:hidden"> <header className="flex h-14 items-center justify-between border-b border-border px-4 md:hidden"> <div className="flex items-center gap-2"> <Cat className="h-5 w-5 text-primary" /> <span className="font-semibold">{t("common:brand_name")}</span> </div> <div className="flex items-center gap-2"> <LangToggle /> <ThemeToggle /> </div> </header> <main className="flex-1 p-6"> <Outlet /> Loading
panel/src/i18n/resources/en-US/common.json +2 −1 Changes for panel/src/i18n/resources/en-US/common.json: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -18,5 +18,6 @@ "page_title": "Felis · Control Panel", "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "Switch to {{lang}}" "lang_toggle_hint": "Switch to {{lang}}", "toggle_theme": "Toggle theme" }
panel/src/i18n/resources/zh-CN/common.json +2 −1 Changes for panel/src/i18n/resources/zh-CN/common.json: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -18,5 +18,6 @@ "page_title": "Felis · 控制面板", "lang_en": "EN", "lang_zh": "中文", "lang_toggle_hint": "切换至 {{lang}}" "lang_toggle_hint": "切换至 {{lang}}", "toggle_theme": "切换主题" }