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 { useVoteStore } from "@/lib/store";
|
||||||
import { useVoteAction } from "@/hooks/useVoteAction";
|
import { useVoteAction } from "@/hooks/useVoteAction";
|
||||||
import { useScrollRestore } from "@/hooks/useScrollRestore";
|
import { useScrollRestore } from "@/hooks/useScrollRestore";
|
||||||
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { tosUrl } from "@/lib/tos";
|
import { tosUrl } from "@/lib/tos";
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ export default function Home() {
|
|||||||
const [sortKey, setSortKey] = useState<SortKey>("votes");
|
const [sortKey, setSortKey] = useState<SortKey>("votes");
|
||||||
const [filterStuck, setFilterStuck] = useState(false);
|
const [filterStuck, setFilterStuck] = useState(false);
|
||||||
const filterSentinelRef = useRef<HTMLDivElement>(null);
|
const filterSentinelRef = useRef<HTMLDivElement>(null);
|
||||||
|
const setStoreFilterStuck = useUIStore((s) => s.setFilterStuck);
|
||||||
|
|
||||||
const endTime = useMemo(() => getActivityEndTime(), []);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Hero · 全屏沉浸式视频 · 作为第一个 snap 点
|
{/* Hero · 全屏沉浸式视频 · 作为第一个 snap 点
|
||||||
@ -119,16 +128,25 @@ export default function Home() {
|
|||||||
{/* 哨兵:用于检测筛选条是否吸顶 */}
|
{/* 哨兵:用于检测筛选条是否吸顶 */}
|
||||||
<div ref={filterSentinelRef} aria-hidden style={{ height: 0 }} />
|
<div ref={filterSentinelRef} aria-hidden style={{ height: 0 }} />
|
||||||
|
|
||||||
{/* 筛选条 · 外层铺满(与导航栏同宽),吸顶后启用毛玻璃;内层版心承载文案 */}
|
{/* 筛选条 · 外层铺满,内层版心承载文案。
|
||||||
|
吸顶时,absolute 子层把玻璃从 -top-20 一路扩到容器底部 ——
|
||||||
|
这是一个单一元素的 backdrop-filter,横跨 nav 区域 + filter 区域,
|
||||||
|
消除两块独立玻璃在 y=80 接缝处的视觉割裂。导航栏同步关掉自己的玻璃。 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="sticky z-30 transition-colors duration-200"
|
||||||
"sticky z-30 transition-colors duration-200",
|
|
||||||
filterStuck &&
|
|
||||||
"bg-surface/40 backdrop-blur-xl backdrop-saturate-150 border-b border-white/[0.06]",
|
|
||||||
)}
|
|
||||||
style={{ top: "80px" }}
|
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
|
<ArtistFilters
|
||||||
tagFilter={tagFilter}
|
tagFilter={tagFilter}
|
||||||
onTagChange={setTagFilter}
|
onTagChange={setTagFilter}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import SearchTrigger from "./SearchTrigger";
|
|||||||
import AuthMenu from "./auth/AuthMenu";
|
import AuthMenu from "./auth/AuthMenu";
|
||||||
import RemainingVotesBadge from "./auth/RemainingVotesBadge";
|
import RemainingVotesBadge from "./auth/RemainingVotesBadge";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导航栏 · 上下文敏感的玻璃态切换
|
* 导航栏 · 上下文敏感的玻璃态切换
|
||||||
@ -23,6 +24,10 @@ export default function Navigation() {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// 初始乐观:任何页面首屏都假设在顶部/Hero 上,透明。effect 挂载后会立刻校正。
|
// 初始乐观:任何页面首屏都假设在顶部/Hero 上,透明。effect 挂载后会立刻校正。
|
||||||
const [isTransparent, setIsTransparent] = useState(true);
|
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() 是否安全
|
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -57,12 +62,15 @@ export default function Navigation() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 inset-x-0 z-50">
|
<header className="fixed top-0 inset-x-0 z-50">
|
||||||
{/* 玻璃层 · 通过 opacity 渐显渐隐 · bg-surface 与全站卡片同色 */}
|
{/* 玻璃层 · 通过 opacity 渐显渐隐 · bg-surface 与全站卡片同色
|
||||||
|
glassOff = isTransparent || filterStuck:
|
||||||
|
- isTransparent: Hero / 页顶,本来就要透明
|
||||||
|
- filterStuck: 筛选条已伸出共享玻璃,nav 关掉自己的玻璃避免接缝 */}
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 bg-surface/40 backdrop-blur-xl backdrop-saturate-150 transition-opacity duration-300",
|
"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 高光 · 仅玻璃态下显示 */}
|
{/* 顶部 1px 高光 · 仅玻璃态下显示 */}
|
||||||
@ -70,7 +78,7 @@ export default function Navigation() {
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-x-0 top-0 h-px bg-white/[0.06] transition-opacity duration-300",
|
"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">
|
<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
|
<NavLinks
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative md:hidden overflow-x-auto transition-colors duration-300",
|
"relative md:hidden overflow-x-auto transition-colors duration-300",
|
||||||
isTransparent
|
glassOff
|
||||||
? "border-t border-transparent"
|
? "border-t border-transparent"
|
||||||
: "border-t border-white/[0.05]",
|
: "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