https://checkmate.social
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add design spec: mobile-responsive design pass

Captures the brainstormed design for making all pages render well on
phones down to iPhone SE 2/3 (375x600 effective viewport). Single
breakpoint (md), one new component (MoveDrawer), tap-target bumps to
44px floor. Header keeps a single treatment for maintenance simplicity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

jcalabro 68948789 3f596c2a

+148
+148
docs/superpowers/specs/2026-04-15-mobile-responsive-design.md
··· 1 + # Mobile-responsive design pass 2 + 3 + **Status:** approved 4 + **Date:** 2026-04-15 5 + **Author:** brainstormed with claude 6 + **Scope target:** modern phones in **portrait orientation** down to iPhone SE 2/3 (375×667 device, ~375×600 effective viewport in browser). Landscape on phones and tablets explicitly out of scope. 7 + 8 + ## Background 9 + 10 + The app's three primary screens (`LoginScreen`, `LobbyScreen`, `GameScreen`) were initially designed at desktop widths. Empirical measurement at the lower bound of our target range (effective ~375×600 viewport) shows that the `GameScreen` overflows by roughly 90px because all four major elements — header, two player bars, board, and move list — are forced to share a single column at full width. 11 + 12 + Tap targets on the persistent buttons are also undersized (Sign out 62×24, Resign 71×26) — well below Apple HIG (44×44) and Material (48×48) thresholds. 13 + 14 + The other screens are constrained tightly enough by `max-w-sm` / `max-w-xs` that they should already fit, but we will verify each one in the browser before declaring done. 15 + 16 + ## Goals 17 + 18 + 1. `GameScreen` fits on iPhone SE 2/3 portrait without scrolling, with the chess board taking as much of the available area as possible. 19 + 2. All persistent touch targets meet at least 44×44 px. 20 + 3. No layout regressions on desktop (≥ `lg` breakpoint, 1024px). 21 + 4. The Header has a single treatment across viewports — no in-game variant — to minimize maintenance surface. 22 + 5. Each modified page is verified visually at mobile sizes before the work is considered done. 23 + 24 + ## Non-goals 25 + 26 + - Landscape orientation on phones. 27 + - Tablet-specific layouts (iPad portrait will get the desktop treatment at the `md` breakpoint and that is fine). 28 + - Visual redesign — the classic-chess-club aesthetic stays. 29 + - Changes to game logic, OAuth, or SpacetimeDB integration. 30 + 31 + ## Design 32 + 33 + ### Strategy 34 + 35 + A single Tailwind breakpoint divides treatments: **below `md` (768px)** is the mobile treatment, **at or above `md`** keeps current desktop behavior. We chose `md` rather than `sm` because tablets in portrait benefit from the sidebar move list, and our smallest tablet target (iPad portrait at 810px) sits comfortably above `md`. 36 + 37 + The only structurally new piece is a **bottom-sheet drawer** for the move list on mobile. Everything else is responsive class additions, padding adjustments, and tap-target bumps. 38 + 39 + ### Components 40 + 41 + #### `client/src/components/game/MoveDrawer.tsx` (new) 42 + 43 + Bottom-sheet drawer that wraps the existing `MoveList` component on mobile. Two exports keep the trigger button decoupled from the panel so the trigger can be slotted into the bottom `PlayerBar`'s `action` slot: 44 + 45 + - `MoveDrawerTrigger`: small button labeled "Moves" with a `▴` arrow. Touchable to ≥44×44. 46 + - `MoveDrawerPanel`: the sheet itself, mounted at the GameScreen level. Renders nothing when closed; renders backdrop + sheet when open. 47 + 48 + State (open/closed) lives in `GameScreen` as a single `useState<boolean>`. Trigger calls `setOpen(true)`; panel's dismiss handlers (backdrop click, X button, Escape key) call `setOpen(false)`. 49 + 50 + Sheet structure: 51 + - Backdrop: `fixed inset-0 z-20 bg-black/60`, click closes 52 + - Sheet container: `fixed inset-x-0 bottom-0 z-30 max-h-[70vh] rounded-t-xl border-t border-wood-600 bg-wood-800 shadow-2xl` 53 + - Inside: a tiny grab-handle (`h-1 w-10 rounded-full bg-wood-500 mx-auto mt-2`), a header row ("Moves" + close X button at ≥44×44), then `<MoveList moves={moves} />` filling the remaining height 54 + - Auto-scrolls to latest move on open via `MoveList`'s existing scroll-on-`moves.length`-change effect (no changes needed there) 55 + 56 + The drawer is **mobile-only** — its container uses `md:hidden` so it's never mounted on desktop. The desktop sidebar `MoveList` continues to be the desktop treatment. 57 + 58 + Swipe-down-to-dismiss is **not in this scope**. Backdrop tap, X button, and Escape are sufficient. We can add swipe later if it proves missed. 59 + 60 + #### `client/src/components/game/GameScreen.tsx` 61 + 62 + - Outer container padding: `p-4` → `p-2 md:p-4`. Saves 16px vertical and 16px horizontal at mobile sizes (board grows ~16px wider on SE). 63 + - Move list sidebar: wrap the `<div>` containing it in `hidden md:flex` so it disappears on mobile. 64 + - Mount `<MoveDrawerPanel open={...} onClose={...} moves={moves} />` as a sibling of the board column. Renders to a portal-equivalent (`fixed`) positioning so it's not affected by parent flex layout. 65 + - Bottom `PlayerBar` action slot now contains either `[Resign]` (desktop, current behavior) or `[MoveDrawerTrigger, Resign]` (mobile). The trigger uses `md:hidden` so it auto-disappears on desktop. 66 + - Outer column gap: `gap-4` → `gap-0 md:gap-4`. The sidebar gap is irrelevant on mobile since the sidebar is hidden. 67 + 68 + #### `client/src/components/layout/Header.tsx` 69 + 70 + - Add `truncate` and `min-w-0` to the display-name `<span>` and its parent flex item so long handles ellipsize instead of pushing the sign-out button off the page. 71 + - Horizontal padding: `px-5` → `px-4 md:px-5`. 72 + - Sign-out button: add `min-h-[44px]` (with corresponding `px-3` for horizontal hit area) so the hit box meets the 44px floor without changing font size. The visual chrome is unchanged; only the click/tap area grows. 73 + 74 + #### `client/src/components/game/PlayerBar.tsx` 75 + 76 + - Avatar: `h-8 w-8` → `h-9 w-9`. Slightly more identifiable. 77 + - Outer gap: `gap-3` → `gap-2 md:gap-3` so a two-button action group at the right doesn't shove the clock against the name on narrow widths. 78 + - The optional `action` slot remains generic — `PlayerBar` does not learn about Resign or Moves specifically. `GameScreen` continues to compose the action contents. 79 + 80 + #### `client/src/components/game/GameScreen.tsx` (Resign confirmation) 81 + 82 + - Resign button: add `min-h-[44px]` so the hit box meets the 44px floor. Same for Yes/No confirmation buttons. Visual chrome (`py-1`, `text-xs`) preserved so the button doesn't grow comically tall in the player bar. 83 + 84 + #### `client/src/components/game/GameStatus.tsx` 85 + 86 + - "Back to Lobby" button: add `min-h-[44px]` (current `py-2.5` + `text-sm` puts it at ~40px). Visual chrome unchanged. 87 + 88 + #### `client/src/components/lobby/LobbyScreen.tsx` and `client/src/components/auth/LoginScreen.tsx` 89 + 90 + Verified-but-likely-no-changes. The Lobby's time control rows are `py-3.5` (≈48px tap target with text), the Login form input and submit button are both `py-3`. Both screens are constrained by `max-w-{xs,sm}` and centered. We will verify each in the browser at 375×600 and 320×~460 to be sure, and only edit if there's a real issue. 91 + 92 + #### `client/src/app.css` 93 + 94 + - No structural changes anticipated. If we observe sticky `:hover` states on touch devices (where `hover:` styles "stick" after tap), we will add `@media (hover: hover)` overrides — but Tailwind v4 already gates `hover:` on `(hover: hover)` by default, so this should be a no-op. 95 + 96 + ### Breakpoint policy 97 + 98 + - `md:` (768px) is the **mobile/desktop divider** for layout treatments. 99 + - `sm:` is **not** used as a layout divider — it's already used in the codebase for content tweaks (e.g., showing/hiding text labels) and we keep that convention. 100 + - Avoid hand-rolled `@media` queries; everything responsive uses Tailwind variant classes for greppability. 101 + 102 + ### Testing strategy 103 + 104 + Per the user's directive, **each modified page is verified in the browser before moving on**. 105 + 106 + For each page, after edits: 107 + 108 + 1. Reload at the current Chrome mobile-emulation viewport. 109 + 2. Verify it renders without scrolling at our target floor (375×~600). 110 + 3. Verify all interactive elements have ≥44px tap targets via DevTools / measurement script. 111 + 4. Verify it still renders correctly at desktop widths (≥1024px). 112 + 113 + We also write **vitest unit tests for `MoveDrawer`**, since it's the only new component with state: 114 + - Trigger button renders nothing when no moves prop available (still renders trigger; the open panel shows "No moves yet" via the underlying `MoveList`). 115 + - Clicking trigger sets state; clicking backdrop, X, or pressing Escape closes. 116 + - Backdrop is not mounted when closed (no DOM noise / pointer-event traps). 117 + 118 + We do **not** add screenshot-diff testing or device-farm testing — overkill for a change of this scope. 119 + 120 + ### Documentation 121 + 122 + - **CLAUDE.md** gets a new section reminding future contributors that "the app must work well on phones down to iPhone SE 2/3 (375×600 effective viewport) in portrait orientation; verify mobile rendering when changing layout-bearing components." This is the user's most important requirement: institutional memory so this doesn't regress. 123 + 124 + ## Risk analysis 125 + 126 + - **`fixed`-positioned drawer + iOS Safari bottom toolbar.** iOS Safari's dynamic toolbar can occlude `bottom: 0` content. Mitigation: use `padding-bottom: env(safe-area-inset-bottom)` on the sheet. Low risk; if missed, only the close button is partially obscured and the user can tap above it to dismiss. 127 + - **Drawer state lost on game change.** The drawer should auto-close when the active game changes. We add a `useEffect` keyed on `activeGame.id` in `GameScreen` to reset `open` to `false` on game transitions. 128 + - **Tap-target bumps push narrow-viewport widths past available space.** Mitigated by also reducing horizontal `gap-3` → `gap-2 md:gap-3` on the player bar so the row doesn't grow horizontally as a side effect. 129 + 130 + ## Out-of-scope follow-ups 131 + 132 + Captured here so we don't lose them, but **not** part of this change: 133 + 134 + - Swipe-down-to-dismiss for the drawer (only matters if backdrop-tap proves insufficient in practice) 135 + - A "spectator" view for completed games on small phones (currently the GameStatus modal handles this fine) 136 + - A persistent disconnection toast on mobile in case the connection-status dot in the header is too subtle (separate UX concern, not scope-bound to mobile) 137 + - Landscape orientation support 138 + - iPad-specific layout (currently picks up the desktop treatment at `md`, which is good enough) 139 + 140 + ## Acceptance criteria 141 + 142 + 1. `GameScreen` renders without scrolling at 375×600 with all elements visible and interactive. 143 + 2. All persistent touch targets on every screen measure ≥44×44 in DevTools. 144 + 3. Header display name truncates with ellipsis at 320px viewport width with a long handle. 145 + 4. Move drawer opens, displays moves, and dismisses via backdrop / X / Escape. 146 + 5. Desktop layout (≥1024px) is visually identical to the pre-change version. 147 + 6. `bun run test` passes; `npx tsc --noEmit` passes. 148 + 7. CLAUDE.md mentions the mobile-support requirement.