feat: scroll to post bubble from recommended card + back-to-top button
Some checks failed
Deploy to Frontend Servers / deploy (push) Failing after 14s

Recommended cards already routed to /browse#post-<id>, but the stream had
no logic to scroll to the target bubble — and the post might not be paged
in yet. MessageStream now resolves the #post-<id> hash, auto-loads more
pages until the bubble renders, scrolls to it, and gives it a brief gold
highlight. Bubbles get scroll-mt so they clear the sticky header.

Also adds a global floating back-to-top button (BackToTop) mounted in
PublicLayout, shown after scrolling past 400px.

Bundles related staging UI work already present in the working tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TerryM
2026-05-29 11:50:27 +08:00
parent 8e36894851
commit 88a25b6ad4
27 changed files with 748 additions and 139 deletions

View File

@@ -1,7 +1,15 @@
import { ChevronDown, Menu, Search as SearchIcon, X } from "lucide-react";
import { AnimatePresence, m } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import {
Link,
useLocation,
useNavigate,
useOutlet,
} from "react-router-dom";
import { pageTransition } from "../motion";
import { ArkLogoMark } from "../components/ArkLogoMark";
import { BackToTop } from "../components/BackToTop";
import { DocumentMeta } from "../components/DocumentMeta";
import { SearchPanel } from "../components/SearchPanel";
import { useI18n, type Lang } from "../i18n";
@@ -53,7 +61,9 @@ function navIsActive(
function navClassName(active: boolean) {
return [
"shrink-0 rounded-sm px-2 py-2 text-[13px] font-medium leading-none whitespace-nowrap no-underline outline-none transition-colors",
"relative shrink-0 rounded-sm px-2 py-2 text-[13px] font-medium leading-none whitespace-nowrap no-underline outline-none transition-colors",
// Hover-only gold underline that slides in (resting/active look unchanged).
"after:pointer-events-none after:absolute after:inset-x-2 after:bottom-1 after:h-[2px] after:origin-left after:scale-x-0 after:rounded-full after:bg-ark-gold after:transition-transform after:duration-300 hover:after:scale-x-100 motion-reduce:after:transition-none",
"focus-visible:ring-2 focus-visible:ring-ark-gold/90 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg",
active
? "text-ark-gold visited:text-ark-gold"
@@ -274,6 +284,7 @@ function MobileLanguageButton({
export function PublicLayout() {
const { t, lang, setLang } = useI18n();
const { pathname, search, hash } = useLocation();
const outlet = useOutlet();
const [open, setOpen] = useState(false);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [q, setQ] = useState("");
@@ -606,9 +617,17 @@ export function PublicLayout() {
: "flex-1 px-4 pb-6 pt-6 min-[440px]:px-5 sm:px-6 md:px-9 md:pb-10 md:pt-10 xl:px-0"
}`}
>
<div key={`${pathname}${search}`} className="ark-page-fade-in">
<Outlet />
</div>
<AnimatePresence mode="wait" initial={false}>
<m.div
key={`${pathname}${search}`}
variants={pageTransition}
initial="initial"
animate="enter"
exit="exit"
>
{outlet}
</m.div>
</AnimatePresence>
</main>
<footer className="mt-auto bg-transparent md:border-t md:border-ark-line md:bg-ark-nav/90">
@@ -654,6 +673,8 @@ export function PublicLayout() {
/>
</div>
</nav>
<BackToTop />
</div>
);
}