name: responsive-ui description: Build mobile-first responsive UI in Allstars Galaxy without regressing desktop. Use when a screen "feels too big / too cramped on mobile", when adding cards/rails/headers that must reflow on phones, or when touching typography sizes. Encodes the type scale, the layout patterns we use, the Tailwind-v4 gotchas that have bitten us, and how to verify layout without auth.
Responsive UI Workflow
Mobile-first conventions for this app. The portal switches to multi-column
layouts at the sm breakpoint (640px), so sm: is the "desktop" step for
most portal work; lg: (1024px) is the wide step.
Golden rule
Pin the desktop value, step the mobile (base) value down from it. Write the
size you want at ≥640px as sm:… and a smaller base class for phones. This
guarantees desktop is unchanged while phones get denser. Example:
text-xl sm:text-2xl (20 → 24). Never leave a single fixed large class on a
heading/stat — it will look oversized on phones.
Typography
Use the primitives in @/components/ui/typography
(Heading, Text, …). The Heading levels are already responsive
(mobile → desktop) — see docs/TYPOGRAPHY.md for
the full table. Don't re-add fixed sizes on top.
- Headings →
<Heading level="h3">(h3 = 20→24, etc.). Override only weight (className="font-bold"), never the size. - Hero numbers / KPI stats are not headings — apply the same pattern to a
<p>/<Mono>:text-lg sm:text-xl lg:text-2xlfor a card's main figure,text-base sm:text-lgfor a secondary stat. - Body/
muted/smallare already phone-appropriate (≤14px) — leave them.
Layout patterns we use
- Compact mobile variant of a hero widget (e.g. the financial-health donut):
render two instances — a small one inline (
sm:hidden) and the full one in its desktop slot (hidden sm:block). Give the component asize/showFooterprop instead of forking markup. - Pin an element top-right next to a title on mobile: put title + element in
one row with
flex items-start justify-between gap-3(NOflex-wrap), give the title blockmin-w-0so it shrinks, and the elementshrink-0. Hide the element's widest sub-line on phones (hidden sm:flex) so it stays narrow. - Horizontal-scroll card rail with a peek (signals "more to scroll"): on
mobile
flex gap-3 overflow-x-auto snap-xwith each cardw-[44%] shrink-0 snap-start; fromsmupsm:grid sm:grid-cols-2 lg:grid-cols-4. ~44% width shows two cards plus a quarter of the third.
Gotchas (these have actually bitten us)
overflow-x-autoclips the cross axis too. A horizontal rail will shave the top/bottom ring + shadow of its cards. Fix with vertical padding (py-1) for breathing room.- Never use a negative vertical margin to offset that padding. Tailwind v4
space-y-*spaces siblings viamargin-bottomon non-last children, and a-m-1/-my-1overrides that margin-bottom → the next section collapses flush against the rail. Use horizontal-only negatives:-mx-1 px-1(cancels, no shift) pluspy-1(no negative). Verified: this keeps the 24pxspace-y-6gap (measured 28px = 24 + 4px padding) vs ~0 with-m-1. - Dev-tool helpers need a stable identity.
useRegisterDevToolre-registers on identity change; an inline object loops. Build it once withuseState(() => ({...}))(notuseMemo(() => ({...}), []), which the React Compiler lint rejects when deps are empty).
Verifying layout without auth
Portal pages are behind Supabase auth and you must not enter passwords to log in. To measure spacing/sizes:
- Create a throwaway public route (anything outside
/portal/, e.g.app/spacing-check/page.tsx) that reproduces the exact markup/classes. Wrap a sub-tree instyle={{ maxWidth: 390 }}to simulate a phone if the preview viewport is wider. - The user's
pnpm devalready runs on:3010(the dev wrench is visible in their screenshots) and hot-reloads the new route. Next 16 refuses a second dev server in the same dir, so drive the user's existing one via the Chrome MCP (navigatetohttp://localhost:3010/spacing-check, thenjavascript_toolto readgetComputedStyle(...).fontSize/getBoundingClientRect()). - Note the page viewport may be pinned (~502px) by a side panel — that's still
< 640, so mobile classes apply; use amaxWidthwrapper for narrower checks. - Delete the throwaway page before committing.
Checklist
- Desktop (≥640px) sizes/spacing unchanged — diff only adds
sm:/base steps. - Headings via
<Heading>; hero numbers use the mobile-first stat pattern. - Any
overflow-xrail haspy-*room and no negative vertical margin. - Measured the result in the browser; threw away the test route.
- If the type scale itself changed, update
docs/TYPOGRAPHY.mdin the same commit.