fix(ui): merge nav + sticky filter into a single backdrop-filter band
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7m27s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7m27s
Two adjacent backdrop-filter elements (nav at y=0-80, filter at y=80+) always show a visible seam at their boundary because each filter clips its own blur kernel, so the edge pixels sample slightly different neighborhoods. Same recipe doesn't help — it's a structural issue. Fix: when filter is stuck, render an absolutely-positioned glass child inside the filter that extends from -top-20 to bottom-0 (i.e. covers nav area + filter area as ONE element). Nav reads filterStuck from a tiny shared zustand UI store and disables its own glass layer in that state, so only the shared band is visible. Single element, single backdrop-filter, no seam. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ed222d1c5f
commit
3f5d33c422
@ -11,6 +11,7 @@ import { getActivityEndTime, sortArtists, type SortKey } from "@/lib/mock-data";
|
||||
import { useVoteStore } from "@/lib/store";
|
||||
import { useVoteAction } from "@/hooks/useVoteAction";
|
||||
import { useScrollRestore } from "@/hooks/useScrollRestore";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { tosUrl } from "@/lib/tos";
|
||||
|
||||
@ -23,6 +24,7 @@ export default function Home() {
|
||||
const [sortKey, setSortKey] = useState<SortKey>("votes");
|
||||
const [filterStuck, setFilterStuck] = useState(false);
|
||||
const filterSentinelRef = useRef<HTMLDivElement>(null);
|
||||
const setStoreFilterStuck = useUIStore((s) => s.setFilterStuck);
|
||||
|
||||
const endTime = useMemo(() => getActivityEndTime(), []);
|
||||
|
||||
@ -66,6 +68,13 @@ export default function Home() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 把 filterStuck 同步到全局 UI store —— 让导航栏感知,在吸顶时关掉自己的玻璃,
|
||||
// 让筛选条延伸出的"共享玻璃带"成为唯一的 backdrop-filter,消除接缝
|
||||
useEffect(() => {
|
||||
setStoreFilterStuck(filterStuck);
|
||||
return () => setStoreFilterStuck(false);
|
||||
}, [filterStuck, setStoreFilterStuck]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hero · 全屏沉浸式视频 · 作为第一个 snap 点
|
||||
@ -119,16 +128,25 @@ export default function Home() {
|
||||
{/* 哨兵:用于检测筛选条是否吸顶 */}
|
||||
<div ref={filterSentinelRef} aria-hidden style={{ height: 0 }} />
|
||||
|
||||
{/* 筛选条 · 外层铺满(与导航栏同宽),吸顶后启用毛玻璃;内层版心承载文案 */}
|
||||
{/* 筛选条 · 外层铺满,内层版心承载文案。
|
||||
吸顶时,absolute 子层把玻璃从 -top-20 一路扩到容器底部 ——
|
||||
这是一个单一元素的 backdrop-filter,横跨 nav 区域 + filter 区域,
|
||||
消除两块独立玻璃在 y=80 接缝处的视觉割裂。导航栏同步关掉自己的玻璃。 */}
|
||||
<div
|
||||
className={cn(
|
||||
"sticky z-30 transition-colors duration-200",
|
||||
filterStuck &&
|
||||
"bg-surface/40 backdrop-blur-xl backdrop-saturate-150 border-b border-white/[0.06]",
|
||||
)}
|
||||
className="sticky z-30 transition-colors duration-200"
|
||||
style={{ top: "80px" }}
|
||||
>
|
||||
<div className="max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
{/* 共享玻璃带:absolute,吸顶时延伸到 nav 顶部,opacity 平滑过渡 */}
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"absolute inset-x-0 -top-20 bottom-0 pointer-events-none",
|
||||
"bg-surface/40 backdrop-blur-xl backdrop-saturate-150 border-b border-white/[0.06]",
|
||||
"transition-opacity duration-300",
|
||||
filterStuck ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="relative max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<ArtistFilters
|
||||
tagFilter={tagFilter}
|
||||
onTagChange={setTagFilter}
|
||||
|
||||
@ -8,6 +8,7 @@ import SearchTrigger from "./SearchTrigger";
|
||||
import AuthMenu from "./auth/AuthMenu";
|
||||
import RemainingVotesBadge from "./auth/RemainingVotesBadge";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
|
||||
/**
|
||||
* 导航栏 · 上下文敏感的玻璃态切换
|
||||
@ -23,6 +24,10 @@ export default function Navigation() {
|
||||
const pathname = usePathname();
|
||||
// 初始乐观:任何页面首屏都假设在顶部/Hero 上,透明。effect 挂载后会立刻校正。
|
||||
const [isTransparent, setIsTransparent] = useState(true);
|
||||
// 筛选条吸顶时,nav 也变透明 —— 此时筛选条的"共享玻璃带"已延伸到 nav 顶部,
|
||||
// nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。
|
||||
const filterStuck = useUIStore((s) => s.filterStuck);
|
||||
const glassOff = isTransparent || filterStuck;
|
||||
|
||||
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
|
||||
useEffect(() => {
|
||||
@ -57,12 +62,15 @@ export default function Navigation() {
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 inset-x-0 z-50">
|
||||
{/* 玻璃层 · 通过 opacity 渐显渐隐 · bg-surface 与全站卡片同色 */}
|
||||
{/* 玻璃层 · 通过 opacity 渐显渐隐 · bg-surface 与全站卡片同色
|
||||
glassOff = isTransparent || filterStuck:
|
||||
- isTransparent: Hero / 页顶,本来就要透明
|
||||
- filterStuck: 筛选条已伸出共享玻璃,nav 关掉自己的玻璃避免接缝 */}
|
||||
<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",
|
||||
glassOff ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
/>
|
||||
{/* 顶部 1px 高光 · 仅玻璃态下显示 */}
|
||||
@ -70,7 +78,7 @@ export default function Navigation() {
|
||||
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",
|
||||
glassOff ? "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">
|
||||
@ -91,7 +99,7 @@ export default function Navigation() {
|
||||
<NavLinks
|
||||
className={cn(
|
||||
"relative md:hidden overflow-x-auto transition-colors duration-300",
|
||||
isTransparent
|
||||
glassOff
|
||||
? "border-t border-transparent"
|
||||
: "border-t border-white/[0.05]",
|
||||
)}
|
||||
|
||||
18
src/lib/ui-store.ts
Normal file
18
src/lib/ui-store.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
/**
|
||||
* 跨组件 UI 协调状态。
|
||||
* 主要用途:首页筛选条吸顶时通知导航关掉自己的玻璃,
|
||||
* 让筛选条把玻璃从 y=0 一路延伸到自己底部,形成单一连续 backdrop-filter,
|
||||
* 消除两块独立玻璃在 y=80 处的接缝。
|
||||
*/
|
||||
interface UIStore {
|
||||
/** 当前页面有 sticky 筛选条且已经吸顶 */
|
||||
filterStuck: boolean;
|
||||
setFilterStuck: (stuck: boolean) => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStore>((set) => ({
|
||||
filterStuck: false,
|
||||
setFilterStuck: (stuck) => set({ filterStuck: stuck }),
|
||||
}));
|
||||
Loading…
x
Reference in New Issue
Block a user