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,4 +1,5 @@
import { Download, LoaderCircle } from "lucide-react";
import { m } from "framer-motion";
import { Link } from "react-router-dom";
import type { Resource } from "../api";
import { assetUrl } from "../api";
@@ -11,13 +12,20 @@ import {
downloadAttachment,
downloadFile,
} from "./messageStream/utils/downloadFile";
import { useToast } from "./Toast";
function isPlaceholderAsset(path: string | undefined | null) {
return !path || path.includes("placeholder-cover");
}
const CARD_BASE_CLASS =
"group flex shrink-0 flex-col overflow-hidden rounded-xl border bg-[#1D1E23] transition hover:border-ark-gold/55";
"group flex shrink-0 flex-col overflow-hidden rounded-xl border bg-[#1D1E23] transition hover:border-ark-gold/55 hover:shadow-lg hover:shadow-black/30";
const CARD_HOVER_SPRING = {
type: "spring",
stiffness: 380,
damping: 26,
} as const;
const CARD_CAROUSEL_SIZE_CLASS =
"w-[208px] md:w-[240px] lg:w-[246.4px] min-[1100px]:max-xl:w-[273px] xl:w-[246.4px]";
@@ -41,6 +49,7 @@ export function RecommendedCard({
layout?: "carousel" | "grid";
}) {
const { t } = useI18n();
const { showToast } = useToast();
const [isDownloading, setIsDownloading] = useState(false);
const figmaCover =
officialRecommendationCoverFallbacks[
@@ -73,18 +82,21 @@ export function RecommendedCard({
r.downloadAttachmentId,
displayTitle,
);
return;
} else {
await downloadFile(dl, displayTitle);
}
await downloadFile(dl, displayTitle);
showToast(t("downloadOk"));
} catch {
/* ignore */
showToast(t("downloadFail"), "error");
} finally {
setIsDownloading(false);
}
};
return (
<article
<m.article
whileHover={{ y: -4 }}
transition={CARD_HOVER_SPRING}
className={`${CARD_BASE_CLASS} ${
layout === "grid" ? CARD_GRID_SIZE_CLASS : CARD_CAROUSEL_SIZE_CLASS
} ${
@@ -101,12 +113,9 @@ export function RecommendedCard({
<img
src={cover}
alt=""
className={`h-full w-full object-cover transition duration-300 ${
useFigmaDesign
? "group-hover:scale-[1.02]"
: "group-hover:scale-[1.02]"
}`}
className="ark-img-fade h-full w-full object-cover transition duration-300 group-hover:scale-[1.02]"
loading="lazy"
onLoad={(e) => e.currentTarget.classList.add("is-loaded")}
/>
) : (
<div className="h-full w-full bg-gradient-to-br from-neutral-900 to-neutral-950" />
@@ -164,8 +173,8 @@ export function RecommendedCard({
type="button"
className={
useFigmaDesign
? "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#191921] text-ark-gold outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
: "shrink-0 rounded-lg p-1 text-ark-gold outline-none hover:bg-ark-gold/10 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
? "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#191921] text-ark-gold outline-none transition hover:bg-[#22232D] active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
: "shrink-0 rounded-lg p-1 text-ark-gold outline-none transition hover:bg-ark-gold/10 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
}
title={isDownloading ? t("downloading") : t("download")}
aria-label={isDownloading ? t("downloading") : t("download")}
@@ -179,11 +188,7 @@ export function RecommendedCard({
>
{isDownloading ? (
<LoaderCircle
className={
useFigmaDesign
? "h-5 w-5 animate-spin"
: "h-5 w-5 animate-spin"
}
className="h-5 w-5 animate-spin"
strokeWidth={2.2}
/>
) : useFigmaDesign ? (
@@ -195,7 +200,7 @@ export function RecommendedCard({
) : null}
</div>
</div>
</article>
</m.article>
);
}