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:
+216
-12
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user