feat: move theme toggle and language selector to top nav bar

Theme toggle (sun/moon) and language selector (globe + dropdown) now
live in the top nav's right section alongside help and avatar.
Removed the floating Footer component from the home page since its
contents are now in the nav bar.
This commit is contained in:
SnapOtter
2026-06-14 23:13:38 +08:00
parent 6bcee1e073
commit aff9159ae0
2 changed files with 103 additions and 7 deletions
+102 -2
View File
@@ -2,14 +2,19 @@ import {
ChevronLeft,
ChevronRight,
FolderOpen,
Globe,
HelpCircle,
LayoutGrid,
Moon,
Sun,
Workflow,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { useTranslation } from "@/contexts/i18n-context";
import { useAuth } from "@/hooks/use-auth";
import { useMobile } from "@/hooks/use-mobile";
import { useTheme } from "@/hooks/use-theme";
import { cn } from "@/lib/utils";
import { ImageEditIcon } from "../common/image-edit-icon";
import { OtterLogo } from "../common/otter-logo";
@@ -195,8 +200,11 @@ export function TopNav({
{/* Spacer */}
<div className="flex-1" />
{/* Right: help + avatar */}
<div className="flex items-center gap-1">
{/* Right: theme + language + help + avatar */}
<div className="flex items-center gap-0.5">
{!isMobile && <ThemeToggle isDark={isDark} />}
{!isMobile && <LanguageSelector isDark={isDark} />}
<button
type="button"
onClick={onHelpClick}
@@ -218,3 +226,95 @@ export function TopNav({
</header>
);
}
function ThemeToggle({ isDark }: { isDark: boolean }) {
const { resolvedTheme, toggleTheme } = useTheme();
return (
<button
type="button"
onClick={toggleTheme}
className={cn(
"p-1.5 rounded-md transition-colors",
isDark
? "text-[#aaa] hover:text-[#e0e0e0] hover:bg-[#333]"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
)}
title="Toggle theme"
>
{resolvedTheme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button>
);
}
function LanguageSelector({ isDark }: { isDark: boolean }) {
const { locale, setLocale, supportedLocales } = useTranslation();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
const current = supportedLocales.find((l) => l.code === locale);
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={cn(
"flex items-center gap-1 px-2 py-1.5 rounded-md text-xs transition-colors",
isDark
? "text-[#aaa] hover:text-[#e0e0e0] hover:bg-[#333]"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
)}
title="Language"
>
<Globe className="h-3.5 w-3.5" />
{current?.nativeName ?? "English"}
</button>
{open && (
<div
className={cn(
"absolute top-full mt-1 end-0 w-48 max-h-72 overflow-y-auto rounded-lg border shadow-lg z-50",
isDark ? "bg-[#2a2a2a] border-[#444]" : "bg-card border-border",
)}
>
{supportedLocales.map((l) => (
<button
key={l.code}
type="button"
onClick={() => {
setLocale(l.code);
setOpen(false);
}}
className={cn(
"w-full text-start px-3 py-1.5 text-sm transition-colors flex items-center justify-between",
isDark ? "hover:bg-[#333] text-[#e0e0e0]" : "hover:bg-muted text-foreground",
)}
>
<span className={l.code === locale ? "font-medium" : ""}>{l.nativeName}</span>
{l.code === locale && (
<svg
className="h-3.5 w-3.5 text-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</button>
))}
</div>
)}
</div>
);
}
+1 -5
View File
@@ -5,7 +5,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { ToolCard } from "@/components/common/tool-card.js";
import { AppLayout } from "@/components/layout/app-layout.js";
import { Footer } from "@/components/layout/footer.js";
import { useTranslation } from "@/contexts/i18n-context";
import { useFuseSearch } from "@/hooks/use-fuse-search.js";
import { usePageTitle } from "@/hooks/use-page-title.js";
@@ -24,8 +23,7 @@ export function HomePage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<string>("all");
const [search, setSearch] = useState("");
const { fetch: fetchSettings, disabledTools, experimentalEnabled, loaded } =
useSettingsStore();
const { fetch: fetchSettings, disabledTools, experimentalEnabled, loaded } = useSettingsStore();
const recentToolIds = useRecentTools();
usePageTitle();
@@ -119,8 +117,6 @@ export function HomePage() {
<ToolGrid recentTools={recentTools} groupedTools={groupedTools} />
)}
</div>
<Footer />
</div>
</AppLayout>
);