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

@@ -0,0 +1,45 @@
import { ArrowUp } from "lucide-react";
import { AnimatePresence, m } from "framer-motion";
import { useEffect, useState } from "react";
import { useI18n } from "../i18n";
const SHOW_AFTER = 400;
/**
* Floating "back to top" button. Appears once the page is scrolled past
* SHOW_AFTER px and smoothly returns the window to the top on click. Sits
* above the mobile bottom nav and clears it on larger screens.
*/
export function BackToTop() {
const { t } = useI18n();
const [visible, setVisible] = useState(false);
useEffect(() => {
const update = () => setVisible(window.scrollY > SHOW_AFTER);
update();
window.addEventListener("scroll", update, { passive: true });
return () => window.removeEventListener("scroll", update);
}, []);
return (
<AnimatePresence>
{visible ? (
<m.button
type="button"
initial={{ opacity: 0, scale: 0.8, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.8, y: 8 }}
transition={{ type: "spring", stiffness: 380, damping: 26 }}
onClick={() =>
window.scrollTo({ top: 0, behavior: "smooth" })
}
className="fixed bottom-[94px] right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-ark-gold text-black shadow-lg shadow-black/40 outline-none transition hover:bg-ark-gold2 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:bottom-8 md:right-8"
aria-label={t("backToTop")}
title={t("backToTop")}
>
<ArrowUp className="h-5 w-5" strokeWidth={2.4} />
</m.button>
) : null}
</AnimatePresence>
);
}