From fc19b9215816e7e8834a8eeaab527d8457933115 Mon Sep 17 00:00:00 2001 From: TerryM Date: Wed, 3 Jun 2026 14:30:20 +0800 Subject: [PATCH] fix(layout): align /category main padding with /browse Both routes render the same MessageStream; the layout wrapper used to inset /category by px-4 / sm:px-6 on mobile while /browse stayed edge-to-edge, shrinking bubble width and making the category waterfall feel narrower than the all-resources page. --- .../2026-06-03-category-page-layout-fix.md | 35 +++++++++++++++++++ src/layouts/PublicLayout.tsx | 9 ++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .unipi/docs/fix/2026-06-03-category-page-layout-fix.md diff --git a/.unipi/docs/fix/2026-06-03-category-page-layout-fix.md b/.unipi/docs/fix/2026-06-03-category-page-layout-fix.md new file mode 100644 index 0000000..d2a912e --- /dev/null +++ b/.unipi/docs/fix/2026-06-03-category-page-layout-fix.md @@ -0,0 +1,35 @@ +--- +title: "Category page stream layout mismatch — Quick Fix" +type: quick-fix +date: 2026-06-03 +--- + +# Category page stream layout mismatch — Quick Fix + +## Bug +After clicking a card on 资料分类 (`/categories`) and landing on `/category/:slug`, the resource bubbles render with narrower bubbles / different waterfall spacing than the 全部资料 page (`/browse`). Both pages use the same `MessageStream` component, but the page-level wrapper applies extra horizontal padding only to the category route. + +## Root Cause +`src/layouts/PublicLayout.tsx` chooses the `
` padding using a flag named `footerInContentFlow`, defined as: + +```ts +const footerInContentFlow = stripLangPrefix(pathname) === "/browse"; +``` + +That flag selects the `px-0 ... md:px-9 xl:px-0` zero-mobile-padding branch — which is the layout `MessageStream` is designed for (it manages its own inner `max-w` and centers bubbles). All other routes fall through to the default `px-4 min-[440px]:px-5 sm:px-6 md:px-9 ...`, which on mobile inset the stream by 16–24 px and shrunk each bubble's `max-w` proportionally. Because `/category/:slug` rendered the same `MessageStream`, that extra inset is exactly what made the category waterfall look "off" vs `/browse`. + +## Fix +Extend the same zero-padding branch to also match `/category/`, so both routes share the wrapper that `MessageStream` was designed to live in. + +### Files Modified +- `src/layouts/PublicLayout.tsx` — renamed the flag's derivation to also include `/category/*`. Kept the existing `BackToTop` and footer-in-content checks (`stripLangPrefix(pathname) === "/browse"`) untouched, since those are separate features the user did not ask to share with category pages. + +## Verification +- `npx tsc --noEmit` — clean. +- `npm run format:check` — clean. +- `npm test` — 49/49 passing. +- Visual: opening `/categories` → tapping a category card now lands on a `/category/:slug` view whose `
` matches `/browse` (no extra mobile horizontal inset), so bubbles render with the same `max-w-[358px]` width. + +## Notes +- The flag is still called `footerInContentFlow` for now even though it only controls padding, matching prior code; renaming would expand the change footprint beyond this fix. +- BackToTop and the `footerInContentFlow` footer slot remain `/browse`-only — those are independent of layout width and the user didn't ask to enable them on category pages. diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 180d51e..f1c62bc 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -329,7 +329,14 @@ export function PublicLayout() { }); } }; - const footerInContentFlow = stripLangPrefix(pathname) === "/browse"; + // Routes that render a full-bleed asset stream and manage their own inner + // width / padding via `MessageStream`. Both 全部资料 (/browse) and the + // per-category view (/category/) reuse the same component, so they + // need the same zero outer padding here — otherwise the category page's + // bubbles render narrower than the all-resources page. + const strippedPath = stripLangPrefix(pathname); + const footerInContentFlow = + strippedPath === "/browse" || strippedPath.startsWith("/category/"); // Current page name shown in the header brand slot (falls back to the brand). const pageTitle = usePageTitle();