UI-UX/src/components/Navigation.tsx
iye ed222d1c5f
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m17s
feat(ui): scroll-aware nav glass + floating back button + hero polish
- Navigation: fixed + transparent over Hero (home) / at page top (other routes);
  fades to glass-on-scroll. Glass uses surface tone matching site cards.
- Filter bar sticky glass synced to nav recipe (no seam between layers).
- HeroBanner: full-viewport video, center title removed, bottom dim overlay
  removed, eyebrow/countdown repositioned below the nav.
- ArtistDetail: removed portrait shadow; added FloatingBackButton that uses
  router.back() with internal-history fallback to /.
- Floating buttons (back + vote) translateY upward to avoid footer rather
  than disappearing, via useFooterPush.
- Home: useScrollRestore preserves scroll position on return from detail
  pages; temporarily disables scroll-snap during restore.
- PerformanceVideo: max-w capped by 85svh*16/9 so small viewports never crop.
- ArtistFilters: hide horizontal scrollbar thumb in tag container.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 12:25:54 +08:00

103 lines
3.8 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import Logo from "./Logo";
import NavLinks from "./NavLinks";
import SearchTrigger from "./SearchTrigger";
import AuthMenu from "./auth/AuthMenu";
import RemainingVotesBadge from "./auth/RemainingVotesBadge";
import { cn } from "@/lib/cn";
/**
* 导航栏 · 上下文敏感的玻璃态切换
* - 顶部/盖在 Hero 之上时:完全透明,只有内容(无玻璃层、无分割线)
* - 内容滚到 nav 下方时:渐显为毛玻璃(blur + saturate + surface 微着色),与下方筛选条无缝连接
* - 玻璃层用独立 absolute 子层 + opacity 过渡,避免直接切 backdrop-filter class 的硬切
*
* 检测策略:
* - 路由存在 [data-hero-banner] → IntersectionObserver 观察 Hero(首页)
* - 否则 → 滚动监听,scrollY <= 0 时透明,任意向下滚动即玻璃
*/
export default function Navigation() {
const pathname = usePathname();
// 初始乐观:任何页面首屏都假设在顶部/Hero 上,透明。effect 挂载后会立刻校正。
const [isTransparent, setIsTransparent] = useState(true);
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
useEffect(() => {
const prev = parseInt(sessionStorage.getItem("nav:count") || "0", 10);
sessionStorage.setItem("nav:count", String(prev + 1));
}, [pathname]);
useEffect(() => {
const heroEl = document.querySelector("[data-hero-banner]");
// 1) 有 Hero 的页面:观察 Hero 是否还在视口顶部下方
if (heroEl) {
setIsTransparent(true);
const observer = new IntersectionObserver(
([entry]) => setIsTransparent(entry.isIntersecting),
{
// 视口顶部下移 80px(导航高度),让 Hero 完全滚出 nav 下方时触发
rootMargin: "-80px 0px 0px 0px",
threshold: 0,
},
);
observer.observe(heroEl);
return () => observer.disconnect();
}
// 2) 无 Hero 的页面:scrollY === 0 时透明,任何向下滚动即玻璃
const onScroll = () => setIsTransparent(window.scrollY <= 0);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, [pathname]);
return (
<header className="fixed top-0 inset-x-0 z-50">
{/* 玻璃层 · 通过 opacity 渐显渐隐 · bg-surface 与全站卡片同色 */}
<div
aria-hidden
className={cn(
"absolute inset-0 bg-surface/40 backdrop-blur-xl backdrop-saturate-150 transition-opacity duration-300",
isTransparent ? "opacity-0" : "opacity-100",
)}
/>
{/* 顶部 1px 高光 · 仅玻璃态下显示 */}
<div
aria-hidden
className={cn(
"absolute inset-x-0 top-0 h-px bg-white/[0.06] transition-opacity duration-300",
isTransparent ? "opacity-0" : "opacity-100",
)}
/>
<nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-8">
<Logo size="md" />
{/* 中部:首页 / 排行榜 / 我的 */}
<NavLinks className="hidden md:flex" />
{/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */}
<div className="ml-auto flex items-center gap-3">
<SearchTrigger />
<RemainingVotesBadge />
<AuthMenu />
</div>
</nav>
{/* 移动端:单独一行 nav links · 顶部分割线只在玻璃态显示 */}
<NavLinks
className={cn(
"relative md:hidden overflow-x-auto transition-colors duration-300",
isTransparent
? "border-t border-transparent"
: "border-t border-white/[0.05]",
)}
mobile
/>
</header>
);
}