feat: 资料流标题+筛选吸顶,滚动时保持可见
把「全部资料」标题与筛选标签合并为一个吸顶块,钉在全局顶栏下方 (top-[64px]/md:top-[70px]),向下滚动时标题和筛选都不再消失,仅内容流滚动。 移除 FilterChips 原先吸到 top-0(藏到顶栏背后)的行为。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import type { PostScope } from "../types/post";
|
import type { PostScope } from "../types/post";
|
||||||
import { MessageStream } from "./messageStream/MessageStream";
|
import { MessageStream } from "./messageStream/MessageStream";
|
||||||
import { SectionHeader } from "./SectionHeader";
|
|
||||||
|
|
||||||
type AssetStreamPageProps = {
|
type AssetStreamPageProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -9,50 +7,9 @@ type AssetStreamPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AssetStreamPage({ title, scope }: AssetStreamPageProps) {
|
export function AssetStreamPage({ title, scope }: AssetStreamPageProps) {
|
||||||
// Telegram-style sticky page title: once the main heading scrolls up behind
|
|
||||||
// the global header, a floating pill slides in so users always know which
|
|
||||||
// page they're on.
|
|
||||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [pinned, setPinned] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = sentinelRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const io = new IntersectionObserver(
|
|
||||||
([entry]) => setPinned(!entry.isIntersecting),
|
|
||||||
// Inset the top by the sticky header height so the pill appears exactly
|
|
||||||
// when the heading disappears behind it.
|
|
||||||
{ rootMargin: "-64px 0px 0px 0px", threshold: 0 },
|
|
||||||
);
|
|
||||||
io.observe(el);
|
|
||||||
return () => io.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className="mx-auto max-w-full px-4 md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
<MessageStream scope={scope} title={title} />
|
||||||
<SectionHeader title={title} />
|
|
||||||
</div>
|
|
||||||
<div ref={sentinelRef} aria-hidden className="h-0" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`pointer-events-none fixed inset-x-0 top-[64px] z-30 flex justify-center px-4 transition-all duration-300 md:top-[70px] ${
|
|
||||||
pinned ? "translate-y-0 opacity-100" : "-translate-y-3 opacity-0"
|
|
||||||
}`}
|
|
||||||
aria-hidden={!pinned}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 rounded-full border border-ark-line/80 bg-[#1c1c21]/90 px-4 py-1.5 shadow-lg shadow-black/40 backdrop-blur-md">
|
|
||||||
<span
|
|
||||||
className="h-3.5 w-[3px] shrink-0 rounded-full bg-ark-gold"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span className="truncate text-sm font-semibold text-white">
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MessageStream scope={scope} />
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 z-10 bg-ark-bg/95 backdrop-blur md:rounded-t-xl md:border-b md:border-ark-line">
|
<div className="bg-ark-bg/95 backdrop-blur md:rounded-t-xl">
|
||||||
<div
|
<div
|
||||||
className="flex items-end gap-2 overflow-x-auto overflow-y-hidden px-4 pr-10 [-ms-overflow-style:none] [scrollbar-width:none] md:gap-5 md:px-1 md:pr-1 [&::-webkit-scrollbar]:hidden"
|
className="flex items-end gap-2 overflow-x-auto overflow-y-hidden px-4 pr-10 [-ms-overflow-style:none] [scrollbar-width:none] md:gap-5 md:px-1 md:pr-1 [&::-webkit-scrollbar]:hidden"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import type { PostScope } from "../../types/post";
|
|||||||
import { Reveal } from "../../motion";
|
import { Reveal } from "../../motion";
|
||||||
import { Skeleton } from "../Skeleton";
|
import { Skeleton } from "../Skeleton";
|
||||||
import { FilterChips } from "./FilterChips";
|
import { FilterChips } from "./FilterChips";
|
||||||
|
import { SectionHeader } from "../SectionHeader";
|
||||||
import { MessageBubble } from "./MessageBubble";
|
import { MessageBubble } from "./MessageBubble";
|
||||||
import { useGroupedByDay } from "./hooks/useGroupedByDay";
|
import { useGroupedByDay } from "./hooks/useGroupedByDay";
|
||||||
import { usePostStream } from "./hooks/usePostStream";
|
import { usePostStream } from "./hooks/usePostStream";
|
||||||
|
|
||||||
export type MessageStreamProps = {
|
export type MessageStreamProps = {
|
||||||
scope: PostScope;
|
scope: PostScope;
|
||||||
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MessageStream({ scope }: MessageStreamProps) {
|
export function MessageStream({ scope, title }: MessageStreamProps) {
|
||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
const [sp, setSp] = useSearchParams();
|
const [sp, setSp] = useSearchParams();
|
||||||
const { hash } = useLocation();
|
const { hash } = useLocation();
|
||||||
@@ -114,7 +116,16 @@ export function MessageStream({ scope }: MessageStreamProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-full md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
<div className="mx-auto max-w-full md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
||||||
<FilterChips type={type} onTypeChange={(v) => updateParam("type", v)} />
|
{/* Title + filters stay pinned below the global header so users always
|
||||||
|
see which page they're on and can switch filters while scrolling. */}
|
||||||
|
<div className="sticky top-[64px] z-30 bg-ark-bg md:top-[70px]">
|
||||||
|
{title ? (
|
||||||
|
<div className="px-4 pb-1 pt-2 md:px-0">
|
||||||
|
<SectionHeader title={title} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<FilterChips type={type} onTypeChange={(v) => updateParam("type", v)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 px-4 pt-4 md:px-0 md:pt-2">
|
<div className="flex flex-col gap-3 px-4 pt-4 md:px-0 md:pt-2">
|
||||||
{isInitialLoad ? (
|
{isInitialLoad ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user