2026-03-31 18:52:37 +08:00
|
|
|
/* ============================================================
|
|
|
|
|
BOTTOM NAV — Luxsin X9 Controller (Mobile)
|
|
|
|
|
Design: iOS/Android App-style bottom tab bar
|
2026-05-11 17:42:18 +08:00
|
|
|
5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页)
|
2026-03-31 18:52:37 +08:00
|
|
|
- Active tab: cyan icon + top indicator bar
|
|
|
|
|
- Inactive: muted icon + label
|
|
|
|
|
============================================================ */
|
2026-05-11 17:42:18 +08:00
|
|
|
import { useDevice } from "@/contexts/DeviceContext";
|
2026-03-31 18:52:37 +08:00
|
|
|
import { cn } from "@/lib/utils";
|
2026-05-08 18:15:52 +08:00
|
|
|
import { useAIDrawer } from "@/contexts/AIDrawerContext";
|
|
|
|
|
import { useIsLgUp } from "@/hooks/useIsLgUp";
|
2026-05-11 17:42:18 +08:00
|
|
|
import localeZh from "@/locales/data-zh.json";
|
|
|
|
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
|
|
|
|
import localeEn from "@/locales/data-en.json";
|
2026-03-31 18:52:37 +08:00
|
|
|
import {
|
|
|
|
|
Activity,
|
|
|
|
|
Bot,
|
|
|
|
|
Home,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
Sliders,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import { Link, useLocation } from "wouter";
|
2026-05-11 17:42:18 +08:00
|
|
|
import { useMemo } from "react";
|
|
|
|
|
|
|
|
|
|
type BottomNavLocale = NonNullable<(typeof localeZh)["bottomNav"]>;
|
2026-03-31 18:52:37 +08:00
|
|
|
|
|
|
|
|
interface NavItem {
|
|
|
|
|
path: string;
|
|
|
|
|
icon: React.ComponentType<{ size?: number; className?: string }>;
|
2026-05-11 17:42:18 +08:00
|
|
|
labelKey: keyof BottomNavLocale;
|
2026-03-31 18:52:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NAV_ITEMS: NavItem[] = [
|
2026-05-11 17:42:18 +08:00
|
|
|
{ path: "/community", icon: MessageSquare, labelKey: "community" },
|
|
|
|
|
{ path: "/eq", icon: Sliders, labelKey: "hpEq" },
|
|
|
|
|
{ path: "/", icon: Home, labelKey: "home" },
|
|
|
|
|
{ path: "/effects", icon: Activity, labelKey: "effects" },
|
|
|
|
|
{ path: "/ai", icon: Bot, labelKey: "myAi" },
|
2026-03-31 18:52:37 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export default function BottomNav() {
|
|
|
|
|
const [location] = useLocation();
|
2026-05-08 18:15:52 +08:00
|
|
|
const isLgUp = useIsLgUp();
|
|
|
|
|
const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer();
|
2026-05-11 17:42:18 +08:00
|
|
|
const { deviceState } = useDevice();
|
|
|
|
|
|
|
|
|
|
const navText = useMemo((): BottomNavLocale => {
|
|
|
|
|
const lang = deviceState?.language;
|
|
|
|
|
const pack =
|
|
|
|
|
lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
|
|
|
|
return (pack.bottomNav ?? localeZh.bottomNav) as BottomNavLocale;
|
|
|
|
|
}, [deviceState?.language]);
|
2026-03-31 18:52:37 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<nav
|
2026-04-29 10:14:25 +08:00
|
|
|
className="fixed bottom-0 left-0 right-0 z-50 lg:left-1/2 lg:right-auto lg:w-[min(1180px,calc(100vw-3rem))] lg:-translate-x-1/2"
|
2026-03-31 18:52:37 +08:00
|
|
|
style={{
|
|
|
|
|
background: "rgba(10, 10, 12, 0.96)",
|
|
|
|
|
backdropFilter: "blur(40px) saturate(200%)",
|
|
|
|
|
WebkitBackdropFilter: "blur(40px) saturate(200%)",
|
|
|
|
|
borderTop: "0.5px solid rgba(255,255,255,0.1)",
|
|
|
|
|
boxShadow: "0 -1px 0 rgba(0,255,246,0.05)",
|
|
|
|
|
paddingBottom: "env(safe-area-inset-bottom, 8px)",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-stretch justify-around">
|
|
|
|
|
{NAV_ITEMS.map((item) => {
|
2026-05-08 18:15:52 +08:00
|
|
|
const isActive =
|
|
|
|
|
item.path === "/ai"
|
|
|
|
|
? location === "/ai" || (isLgUp && aiDrawerOpen)
|
|
|
|
|
: location === item.path ||
|
|
|
|
|
(item.path === "/" && location === "/") ||
|
|
|
|
|
(item.path !== "/" && location.startsWith(item.path));
|
2026-03-31 18:52:37 +08:00
|
|
|
const Icon = item.icon;
|
|
|
|
|
|
2026-05-08 18:15:52 +08:00
|
|
|
const inner = (
|
|
|
|
|
<div className="flex flex-col items-center justify-center relative pt-2.5 pb-1.5 px-3">
|
2026-03-31 18:52:37 +08:00
|
|
|
{/* Active indicator bar */}
|
|
|
|
|
<div
|
|
|
|
|
className="absolute top-0 left-1/2 -translate-x-1/2 rounded-b-full transition-all duration-300"
|
|
|
|
|
style={{
|
|
|
|
|
width: isActive ? 24 : 0,
|
|
|
|
|
height: 2.5,
|
|
|
|
|
background: "#00FFF6",
|
|
|
|
|
boxShadow: isActive ? "0 0 8px rgba(0,255,246,0.9)" : "none",
|
|
|
|
|
opacity: isActive ? 1 : 0,
|
|
|
|
|
transition: "width 0.25s cubic-bezier(0.34,1.56,0.64,1), opacity 0.2s ease",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Icon */}
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center justify-center rounded-[12px] transition-all duration-200"
|
|
|
|
|
style={{
|
|
|
|
|
width: 38,
|
|
|
|
|
height: 32,
|
|
|
|
|
background: isActive ? "rgba(0,255,246,0.1)" : "transparent",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon
|
|
|
|
|
size={isActive ? 20 : 19}
|
|
|
|
|
className={cn(
|
|
|
|
|
"transition-all duration-200",
|
|
|
|
|
isActive ? "text-[#00FFF6]" : "text-white/30"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Label */}
|
|
|
|
|
<span
|
|
|
|
|
className="mt-0.5 transition-all duration-200 leading-none"
|
|
|
|
|
style={{
|
|
|
|
|
fontFamily: "-apple-system, 'SF Pro Text', sans-serif",
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
fontWeight: isActive ? 600 : 400,
|
|
|
|
|
color: isActive ? "#00FFF6" : "rgba(255,255,255,0.28)",
|
|
|
|
|
textShadow: isActive ? "0 0 8px rgba(0,255,246,0.5)" : "none",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-11 17:42:18 +08:00
|
|
|
{navText[item.labelKey]}
|
2026-03-31 18:52:37 +08:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-05-08 18:15:52 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (item.path === "/ai") {
|
|
|
|
|
if (!isLgUp) {
|
|
|
|
|
return (
|
|
|
|
|
<Link key={item.path} href={item.path}>
|
|
|
|
|
{inner}
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={item.path}
|
|
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
className="cursor-pointer select-none"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (location === "/ai") return;
|
|
|
|
|
setAiDrawerOpen((o) => !o);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key !== "Enter" && e.key !== " ") return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (location === "/ai") return;
|
|
|
|
|
setAiDrawerOpen((o) => !o);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{inner}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Link key={item.path} href={item.path}>
|
|
|
|
|
{inner}
|
2026-03-31 18:52:37 +08:00
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
|
|
|
|
}
|