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

58
src/motion/variants.ts Normal file
View File

@@ -0,0 +1,58 @@
import type { Transition, Variants } from "framer-motion";
/** Premium ease-out curve shared by all motion. */
export const EASE_OUT = [0.22, 1, 0.36, 1] as const;
/** Base transition for reveal-style animations. */
export const baseTransition: Transition = {
duration: 0.4,
ease: EASE_OUT,
};
/**
* Fade + lift in. `visible` is a function variant so callers can pass a
* per-item delay via `custom` (defaults to 0, e.g. when driven by a
* stagger container).
*/
export const fadeInUp: Variants = {
hidden: { opacity: 0, y: 16 },
visible: (delay: number = 0) => ({
opacity: 1,
y: 0,
transition: { ...baseTransition, delay },
}),
};
/** Fade + subtle scale in (cards, popovers). */
export const scaleIn: Variants = {
hidden: { opacity: 0, scale: 0.96 },
visible: (delay: number = 0) => ({
opacity: 1,
scale: 1,
transition: { ...baseTransition, delay },
}),
};
/** Parent container that staggers its children's `visible` state. */
export const staggerContainer: Variants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.06, delayChildren: 0.02 },
},
};
/** Route enter/exit transition used with AnimatePresence. */
export const pageTransition: Variants = {
initial: { opacity: 0, y: 8 },
enter: { opacity: 1, y: 0, transition: { duration: 0.24, ease: EASE_OUT } },
exit: { opacity: 0, y: -6, transition: { duration: 0.16, ease: EASE_OUT } },
};
/** Springy hover lift for cards. Use rest/hover states on an `m` element. */
export const cardHover: Variants = {
rest: { y: 0 },
hover: {
y: -4,
transition: { type: "spring", stiffness: 380, damping: 26 },
},
};