- Update version from 2.0.1 to 3.1.1 in pyproject.toml and server/main.py - Add V3_17_MOBILE_PORTRAIT_LAYOUT.md documenting all mobile improvements: responsive layout, animation sizing fixes, compact header, bottom drawers - Add V3_17 entry to V3 master plan Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.3 KiB
5.3 KiB
V3.17: Mobile Portrait Layout
Version: 3.1.1
Commits: 4fcdf13, fb3bd53
Overview
Full mobile portrait layout for phones, triggered by JS matchMedia on narrow portrait screens (max-width: 500px, orientation: portrait). The desktop layout is completely untouched — all mobile rules are scoped under body.mobile-portrait.
Key Features
Responsive Game Layout
- Viewport fills 100dvh with no scroll;
overscroll-behavior: containprevents pull-to-refresh - Game screen uses flexbox column: compact header → opponents row → player row → bottom bar
- Safe-area insets respected for notched devices (
env(safe-area-inset-top/bottom))
Compact Header
- Single-row header with reduced font sizes (0.75rem) and tight gaps
- Non-essential items hidden on mobile: username display, logout button, active rules bar
- Status message, round info, final turn badge, and leave button all use
white-space: nowrapwith ellipsis overflow
Opponent Cards
- Flat horizontal strip (no arch rotation) with horizontal scroll for 4+ opponents
- Cards scaled to 32x45px with 0.6rem font (26x36px on short screens)
- Dealer chip scaled from 38px to 20px diameter to fit compact opponent areas
- Showing score badge sized proportionally
Deck/Discard Area
- Deck and discard cards match player card size (72x101px) for visual consistency
- Held card floating matches player card size with proportional font scaling
Player Cards
- Fixed 72x101px cards with 1.5rem font in 3-column grid
- 60x84px with 1.3rem font on short screens (max-height: 600px)
- Font size set inline by
card-manager.jsproportional to card width (0.35x ratio on mobile)
Side Panels as Bottom Drawers
- Standings and scoreboard panels slide up as bottom drawers from a mobile bottom bar
- Drawer backdrop overlay with tap-to-dismiss
- Drag handle visual indicator on each drawer
- Drawers auto-close on screen change or layout change back to desktop
Short Screen Fallback
@media (max-height: 600px)reduces all card sizes, gaps, and padding- Opponent cards: 26x36px, deck/discard: 60x84px, player cards: 60x84px
Animation Fixes
Deal Animation Guard
renderGame()returns early whendealAnimationInProgressis true- Prevents WebSocket state updates from destroying card slot DOM elements mid-deal animation
- Cards were piling up at (0,0) because
getCardSlotRect()read stale/null positions afterinnerHTML = ''
Animation Overlay Card Sizing
- Root cause: Base
.cardCSS (width: clamp(65px, 5.5vw, 100px)) was leaking into animation overlay elements (.draw-anim-front.card), overriding the intendedwidth: 100%inherited from the overlay container - Effect: Opponent flip overlays appeared at 65px instead of 32px (too big); deck/discard draw overlays appeared at 65px instead of 72px (too small)
- Fix: Added
!importantto.draw-anim-front/.draw-anim-backwidthandheightrules to ensure animation overlays always match their parent container's inline dimensions from JavaScript
Opponent Swap Held Card Sizing
fireSwapAnimation()now passes aheldRectsized to match the opponent card (32px) positioned at the holding location, instead of defaulting to deck dimensions (72px)- The traveling held card no longer appears oversized relative to opponent cards during the swap arc
Font Size Consistency
cardFontSize()helper inCardAnimationsuses 0.35x width ratio on mobile (vs 0.5x desktop)- Applied consistently across all animation paths:
createAnimCard,createCardFromData, and arc swap font transitions - Held card floating gets inline font-size scaled to card width on mobile
CSS Architecture
All mobile rules use the body.mobile-portrait scope:
/* Applied by JS matchMedia, not CSS media query */
body.mobile-portrait .selector { ... }
/* Short screen fallback uses both */
@media (max-height: 600px) {
body.mobile-portrait .selector { ... }
}
Card sizing uses !important to override base .card clamp values:
body.mobile-portrait .opponent-area .card {
width: 32px !important;
height: 45px !important;
}
Animation overlays use !important to override base .card leaking:
.draw-anim-front,
.draw-anim-back {
width: 100% !important;
height: 100% !important;
}
Files Modified
| File | Changes |
|---|---|
client/style.css |
~470 lines of mobile portrait CSS added at end of file |
client/app.js |
Mobile detection, drawer management, renderGame() guard, swap heldRect sizing, held card font scaling |
client/card-animations.js |
cardFontSize() helper, consistent font scaling across all animation paths |
client/card-manager.js |
Inline font-size on mobile for updateCardElement() |
client/index.html |
Mobile bottom bar, drawer backdrop, viewport-fit=cover |
Testing
- Desktop: No visual changes — all rules scoped under
body.mobile-portrait - Mobile portrait: Verify game fits 100dvh, no scroll, cards properly sized
- Deal animation: Cards fly to correct grid positions (not piling up)
- Draw/discard: Animation overlay matches source card size
- Opponent swap: Flip and arc animations use opponent card dimensions
- Short screens (iPhone SE): All elements fit with reduced sizes
- Orientation change: Layout switches cleanly between mobile and desktop