Feature: public rooms, mobile UX, reconnection, and gameplay fixes

Rooms & lobby
- Rename docker-compose.yml → compose.yml
- Public/Private toggle on room creation; public rooms assign random seats
  to prevent team collusion
- GET /api/rooms API — lists open public rooms; Join tab shows live list
  with one-tap join
- Room creator: swap any two seats by tapping (select-to-swap UI); ▶ Start
  Game button force-starts with bots filling empty seats

Reconnection
- Session moved from sessionStorage → localStorage (survives browser close)
- Socket handlers split: socket.once for one-shot callbacks, persistent
  socket.on('connect') for auto-rejoin on network drops
- Server rejoin accepts userId match as fallback (cross-device rejoin for
  authenticated users); re-issues token on success
- Server emits hasActiveGame on connect so auth'd users on a new device are
  pulled back into their game automatically
- Explicit leave nulls seat/token/userIds so hasActiveGame never re-drags a
  player back in after they chose to leave

Mobile UX
- Remove all opponent/partner card backs; replace with compact card-count
  badge — frees ~120px of vertical space on small phones
- Screen height: 100dvh (dynamic viewport) instead of 100vh — fixes the
  "only top 1/5 visible" issue on phones with browser chrome
- Table grid side columns shrunk to 36px on touch devices; player names
  rotated vertically
- Bidding overlay: transparent non-blocking top panel on touch; hand stays
  visible and interactive; auto fan-mode during bidding
- touch-action: pan-x on hand scroll, none in fan/drag mode — suppresses
  Android back-gesture and Google Gemini conflicts
- user-select: none on game screen prevents long-press selection menus

Gameplay & notifications
- Center trick area now shows whose turn it is instead of trump (trump is
  already in the info bar); flashes gold when it's the player's turn
- Turn reminder after 5 s of inaction: gold glow pulse on hand area
  + Android vibration OR two-note Web Audio chime on iOS (vibrate API not
  supported by Apple)
- Fix: turn reminder was never triggered after winning a trick — justWon
  branch blocked myTurnNow from being set even when currentTurn === mySeat
- Waiting room ☰ menu: Reload and Exit accessible without entering the game
- Prevent duplicate room joins (same socket, same userId, or same name)

Service worker
- Bump to shelem-v2; pre-cache all 55 card SVGs at install time so cards
  are available instantly from the very first hand, including offline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-05-19 20:42:00 +00:00
parent 8e8478e45b
commit a4fefd92f1
7 changed files with 636 additions and 78 deletions
+216 -12
View File
@@ -28,7 +28,11 @@ html, body {
/* ── Screens ──────────────────────────────────── */
.screen { display: none; }
.screen.active { display: flex; flex-direction: column; height: 100vh; }
.screen.active {
display: flex; flex-direction: column;
height: 100vh; /* fallback */
height: 100dvh; /* modern: excludes browser chrome / address bar */
}
/* ── Lobby ────────────────────────────────────── */
#screen-lobby {
@@ -119,6 +123,26 @@ input[type=text], input[type=password], input[type=email] {
}
input:focus { border-color: var(--gold); }
/* ── Visibility toggle (Private / Public) ─────── */
.visibility-row { display: flex; gap: 6px; }
.visibility-btn {
flex: 1;
padding: 8px 6px;
background: rgba(255,255,255,.07);
border: 1px solid rgba(255,255,255,.14);
border-radius: 8px;
color: var(--muted);
cursor: pointer;
font-size: .82rem;
text-align: center;
transition: background .15s;
}
.visibility-btn.active, .visibility-btn:hover {
background: rgba(245,197,24,.2);
border-color: var(--gold);
color: var(--gold);
}
/* ── Mode / score buttons ─────────────────────── */
.mode-row, .score-row { display: flex; gap: 6px; }
.mode-btn, .score-btn {
@@ -239,17 +263,60 @@ input:focus { border-color: var(--gold); }
border: 1px solid rgba(245,197,24,.25);
border-radius: 12px;
}
/* ── Public rooms list ────────────────────────── */
.public-rooms-section { display: flex; flex-direction: column; gap: 6px; width: 100%; }
.rooms-header {
display: flex; align-items: center; justify-content: space-between;
font-size: .78rem; color: var(--muted);
}
.rooms-label { font-weight: 600; letter-spacing: .5px; }
.public-room-item {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 8px 10px;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.1);
border-radius: 8px;
}
.public-room-info { display: flex; flex-direction: column; gap: 2px; text-align: left; }
.public-room-host { font-size: .85rem; font-weight: 600; color: #fff; }
.public-room-meta { font-size: .72rem; color: var(--muted); }
.btn-join-pub { padding: 5px 14px; font-size: .82rem; flex-shrink: 0; }
/* ── Creator controls in waiting room ────────── */
.waiting-creator-controls {
display: flex; gap: 8px; width: 100%;
}
.btn-fill-bots {
padding: 8px 16px;
flex: 1;
padding: 8px 10px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.16);
border-radius: 10px;
color: var(--muted);
cursor: pointer;
font-size: .85rem;
width: 100%;
font-size: .82rem;
}
.btn-fill-bots:hover { background: rgba(255,255,255,.16); color: #fff; }
.btn-start-game {
flex: 1;
padding: 8px 10px;
background: rgba(100,200,100,.15);
border: 1px solid rgba(100,200,100,.35);
border-radius: 10px;
color: #a5d6a7;
cursor: pointer;
font-size: .82rem;
font-weight: 600;
}
.btn-start-game:hover { background: rgba(100,200,100,.28); color: #c8e6c9; }
/* ── Seat swap (creator taps to reorder) ─────── */
.seat-slot.seat-swappable { cursor: pointer; transition: background .12s, border-color .12s; }
.seat-slot.seat-swappable:hover { background: rgba(255,255,255,.1); }
.seat-slot.seat-swap-selected {
border-color: var(--gold);
background: rgba(245,197,24,.12);
}
/* ── Game screen ──────────────────────────────── */
#screen-game { background: var(--felt); overflow: hidden; position: relative; }
@@ -523,11 +590,25 @@ input:focus { border-color: var(--gold); }
display: flex; align-items: center; justify-content: center; gap: 4px;
}
.trick-center-info {
width: 60px; text-align: center; flex-shrink: 0;
flex: 0 0 80px;
text-align: center;
}
.phase-msg {
font-size: .72rem; color: rgba(255,255,255,.6);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.phase-msg.your-turn {
color: var(--gold);
font-weight: 700;
font-size: .78rem;
animation: your-turn-flash .8s ease-in-out infinite alternate;
}
@keyframes your-turn-flash {
from { opacity: .7; }
to { opacity: 1; }
}
/* Drop-zone pulse when dragging */
@@ -914,6 +995,37 @@ input:focus { border-color: var(--gold); }
.lb-table td { border-bottom: 1px solid rgba(255,255,255,.05); }
.lb-table tr:hover td { background: rgba(255,255,255,.04); }
/* ── Waiting room top row (Leave + ☰ menu) ───── */
.waiting-top-row {
display: flex;
width: 100%;
align-items: center;
}
/* ── Turn urgent reminder ──────────────────────── */
@keyframes turn-urgent-glow {
0%, 100% { border-top-color: rgba(0,0,0,.3); box-shadow: none; }
50% {
border-top-color: rgba(245,197,24,.7);
box-shadow: 0 -2px 16px rgba(245,197,24,.3);
}
}
#my-area.turn-urgent {
animation: turn-urgent-glow .65s ease-in-out 6;
}
/* ── Card count badge (replaces card backs) ──── */
.card-count-badge {
font-size: .65rem;
color: rgba(255,255,255,.55);
padding: 1px 5px;
background: rgba(255,255,255,.08);
border-radius: 8px;
min-width: 20px;
text-align: center;
flex-shrink: 0;
}
/* ── Util ─────────────────────────────────────── */
.hidden { display: none !important; }
@@ -928,8 +1040,89 @@ input:focus { border-color: var(--gold); }
bottom: calc(100% + 6px);
}
/* ── Mobile bidding: transparent panel at top so hand stays visible ── */
/* ═══════════════════════════════════════════════
MOBILE OVERHAUL (touch devices / small screens)
═══════════════════════════════════════════════ */
@media (pointer: coarse) {
/* Prevent text selection and system long-press menus on game elements */
#screen-game {
user-select: none;
-webkit-user-select: none;
}
/* Allow horizontal scroll in scroll mode, block vertical pull-to-refresh */
#my-hand {
touch-action: pan-x;
overscroll-behavior-x: contain;
}
/* In fan mode cards need full touch control for drag */
#my-hand.fan-mode {
touch-action: none;
}
#my-hand.fan-mode .card {
touch-action: none;
}
/* ── Info bar: compact ─────────────────────── */
#info-bar { padding: 3px 8px; gap: 5px; }
.team-score { font-size: .7rem; padding: 2px 5px; }
.trump-display, .bid-display { font-size: .68rem; padding: 2px 6px; }
.btn-leave-screen { font-size: .75rem; padding: 3px 7px; }
/* ── Table grid: narrow side columns ──────── */
#table-grid {
grid-template-columns: 36px 1fr 36px;
gap: 2px;
padding: 3px;
}
/* ── Top player label: minimal height ──────── */
.area-top .player-label {
font-size: .7rem;
padding: 2px 6px;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
}
.area-top .player-score-badge { display: none; }
/* ── Side player labels: vertical, very narrow */
.player-label.vertical {
font-size: .6rem;
padding: 3px 2px;
gap: 3px;
writing-mode: vertical-rl;
transform: rotate(180deg);
overflow: hidden;
max-height: 140px;
text-align: center;
}
/* Rotate back the children that shouldn't be rotated */
.player-label.vertical .turn-dot {
writing-mode: horizontal-tb;
transform: none;
font-size: .55rem;
}
.player-label.vertical .player-score-badge { display: none; }
.player-label.vertical .card-count-badge {
writing-mode: horizontal-tb;
transform: none;
font-size: .55rem;
min-width: 16px;
}
/* ── My area: ensure it's always fully visible */
#my-area {
padding: 3px 6px calc(6px + env(safe-area-inset-bottom));
gap: 3px;
flex-shrink: 0;
}
#my-label { font-size: .72rem; gap: 4px; }
.btn-hand-mode, .btn-play-mode { font-size: .65rem; padding: 2px 6px; }
/* ── Bidding overlay: compact panel at top so hand stays visible ── */
#overlay-bid {
align-items: flex-start;
background: transparent;
@@ -942,17 +1135,28 @@ input:focus { border-color: var(--gold); }
max-width: 100%;
border-radius: 0 0 20px 20px;
border-top: none;
background: rgba(8, 22, 8, 0.97);
box-shadow: 0 6px 28px rgba(0,0,0,.75);
max-height: 62vh;
background: rgba(6, 20, 6, 0.97);
box-shadow: 0 6px 28px rgba(0,0,0,.8);
max-height: 55vh;
}
/* Shrink bid history on mobile so it doesn't swallow the screen */
.bid-history { max-height: 110px; }
.bid-box h3 { font-size: .95rem; margin-bottom: 0; }
}
/* ── Mobile tweaks ────────────────────────────── */
@media (max-width: 480px) {
/* ── Small phones (≤ 390px wide) ─────────────── */
@media (pointer: coarse) and (max-width: 390px) {
:root { --card-w: 48px; --card-h: 70px; }
#table-grid { grid-template-columns: 32px 1fr 32px; }
.overlay-box { padding: 14px 12px; }
.bid-amount-display { font-size: 1.3rem; }
.trump-suit-btn { font-size: .82rem; padding: 10px 4px; }
}
/* ── Desktop / mouse-pointer fallback tweaks ─── */
@media (max-width: 480px) and (pointer: fine) {
:root { --card-w: 54px; --card-h: 78px; }
#table-grid { grid-template-columns: 70px 1fr 70px; }
.overlay-box { padding: 18px 16px; }
.bid-amount-display { font-size: 1.4rem; }
.trump-suit-btn { font-size: .88rem; padding: 11px 6px; }
}