style(web): fit a finished game and its lobby on one screen

After a game the buttons that start the next one were under the fold on
a 1080p screen: the result panel needed 206px more than the column had,
and 300 with four players and a list of words the loser missed. Every
game ended with a scroll to find "Sẵn sàng".

The duplication went first. The board's scoreboard and the result's
standings are the same names and the same scores, so the board's stops
at the final whistle and the standings are shown for two players as well
as for four. Under a result the lobby lists the seats that exist rather
than three "Còn trống" rows the count line above already accounts for,
the two stat tiles are one line of small type — the score is in the
table now — and the words the loser could have played sit on the line of
their own label rather than under it.

The rest is chrome giving back pixels: a shorter header and footer, a
40px theme button, tighter seat rows and card padding, and the panel's
"Về trang chủ" and "Tải chuỗi từ" side by side instead of stacked, which
wrap back to one each on a phone. Readying, starting and playing a word
all keep their 44px.

Four seats, a loser's suggestions and a full lobby now come to exactly
the height of a 1080p column, with the chain scrolling inside what is
left. A phone still scrolls, which is what a phone is for.
This commit is contained in:
tiennm99 committed 2026-09-10 15:16:25 +07:00
1 parent a0029ba5aa
commit 644379fa06
8 files changed
+95 -43

No files matched your search

@@ -31,10 +31,10 @@
* the credit is an obligation, not a decoration.
*/
.credit {
padding-block: var(--space-3);
padding-block: var(--space-2);
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
padding-bottom: max(var(--space-2), env(safe-area-inset-bottom));
text-align: center;
color: var(--text-muted);
font-size: var(--text-2);
+6 -1
View File
@@ -84,7 +84,12 @@
{#if modeLabel}<span class="mode">{modeLabel}</span>{/if}
</div>
<!-- Not once the game is over: the result panel below carries the same
names and scores as a ranked table, and two of them is one screenful
of duplication between the result and the lobby's own buttons. -->
{#if game.state.phase !== 'over'}
<ScoreBoard />
{/if}
{#if banner}{@render banner()}{/if}
@@ -176,7 +181,7 @@
display: flex;
flex-direction: column;
flex: 1;
gap: 14px;
gap: var(--space-3);
min-height: 0;
padding-bottom: var(--space-2);
}
+54 -27
View File
@@ -62,9 +62,11 @@
<p class="reason">{endReasonMessages[result.reason]}</p>
{/if}
{#if standings.length > 2}
{#if standings.length > 1}
<!-- Ranked by who outlasted whom, which is what the game is decided
on. The score sits beside the place rather than setting it. -->
on. The score sits beside the place rather than setting it.
Shown for two players as well now that it is the only table on
the screen: the board's own stops at the final whistle. -->
<ol class="standings" aria-label={t.standingsTitle} data-testid="standings">
{#each standings as player (player.playerId)}
<li class:me={player.isMe} class:winner={player.rank === 1}>
@@ -97,7 +99,7 @@
nothing, which is worth hearing too. -->
{#if elimination.suggestions.length > 0}
<div class="suggestions">
<h3>{t.suggestionsTitle}</h3>
<h3>{t.suggestionsTitle}:</h3>
<ul>
{#each elimination.suggestions as word}
<li>{word}</li>
@@ -115,23 +117,26 @@
<p class="record">{t.newRecord}</p>
{/if}
<!-- One row, wrapping when the words no longer fit it. Stacked, the
three of them were 150px of buttons, which on a 1080p screen was
what pushed the lobby's own — the ones that start the next game —
off the bottom. -->
<div class="actions">
{#if onrematch}
<button type="button" class="primary" onclick={onrematch}>{t.rematch}</button>
{/if}
<button type="button" onclick={onhome}>{t.home}</button>
</div>
<button type="button" class="export" onclick={exportHistory}>{t.exportHistory}</button>
</div>
</div>
{/if}
<style>
.panel {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-5);
gap: var(--space-2);
padding: var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
@@ -142,7 +147,7 @@
h2 {
margin: 0;
color: var(--danger);
font-size: var(--text-8);
font-size: var(--text-7);
}
h2.won {
@@ -168,7 +173,8 @@
display: flex;
align-items: baseline;
gap: 10px;
padding: var(--space-2) var(--space-3);
padding: var(--space-1) var(--space-3);
font-size: var(--text-5);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-alt);
@@ -201,27 +207,48 @@
font-variant-numeric: tabular-nums;
}
/* One line rather than two stacked tiles. The score is in the table above
this, so the tiles were 40px of repeating it — and 40px is the
difference between the lobby's buttons being on a 1080p screen and
under it. */
.stats {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--space-8);
gap: var(--space-2) var(--space-5);
margin: 0;
}
.stats div {
display: flex;
align-items: baseline;
gap: var(--space-2);
}
dt {
color: var(--text-muted);
font-size: var(--text-3);
font-size: var(--text-4);
}
dd {
margin: 0;
font-size: var(--text-8);
font-size: var(--text-5);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* Label and words on one line, wrapping when they run out of it: a heading
of its own cost a whole row of a screen the lobby's buttons are also on. */
.suggestions {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
gap: 6px var(--space-2);
}
.suggestions h3 {
margin: 0 0 6px;
margin: 0;
color: var(--text-muted);
font-size: var(--text-3);
font-weight: 600;
@@ -242,7 +269,7 @@
}
.suggestions li {
padding: 6px var(--space-3);
padding: var(--space-1) var(--space-3);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: var(--surface-alt);
@@ -263,26 +290,19 @@
font-weight: 700;
}
.export {
min-height: 44px;
padding: 10px var(--space-3);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
font-size: var(--text-5);
font-weight: 600;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* Wrapping rather than shrinking: three of these across a phone would
break "Tải chuỗi từ" over two lines, so the row gives way instead. */
.actions button {
flex: 1;
flex: 1 1 auto;
min-width: 120px;
min-height: 44px;
padding: var(--space-3);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
@@ -294,4 +314,11 @@
background: var(--accent);
color: var(--accent-text);
}
/* Keeping the chain is worth offering and not worth pressing first. */
.actions .export {
background: transparent;
color: var(--text-muted);
font-size: var(--text-5);
}
</style>
+24 -7
View File
@@ -36,7 +36,12 @@
const s = $derived(game.state);
// The seats nobody is in yet, drawn so a room that is waiting on people
// looks like one rather than like a room that is simply small.
const empties = $derived(Array.from({ length: game.freeSeats }, (_, i) => i));
// Not under a finished game: there the count line above says 3/4 already,
// and three empty rows between the result and the button that starts the
// next game is what pushed that button off a 1080p screen.
const empties = $derived(
compact ? [] : Array.from({ length: game.freeSeats }, (_, i) => i)
);
const shortHanded = $derived(s.roomPlayers.length < s.minPlayers);
const offline = $derived(connection.status !== Status.OPEN);
// The owner is who everybody else is waiting on, so the hint has to stop
@@ -77,7 +82,7 @@
$effect(() => () => clearTimeout(armTimer));
</script>
<section class="lobby" aria-label={t.lobbyTitle}>
<section class="lobby" class:compact aria-label={t.lobbyTitle}>
<!-- The board carries these in the compact case, and two connection badges
on one screen say nothing the first one did not. -->
{#if !compact}
@@ -239,7 +244,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
gap: 14px;
gap: var(--space-3);
}
.top {
@@ -268,12 +273,21 @@
flex-wrap: wrap;
align-items: baseline;
gap: 10px;
padding: var(--space-3) 14px;
/* A seat is a row to read, not a target to hit, so it keeps the
type and gives up the padding: four of them decide whether the
buttons under the list are on screen. */
padding: var(--space-2) 14px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
}
/* Tighter still under a finished game, where the list is competing with a
result panel for the same screen. */
.compact .seat {
padding: var(--space-1) 14px;
}
/* Readiness is a tint. Being the owner is a marker down the edge: a fact
about the seat, not a state it has entered. */
.seat.ready {
@@ -401,7 +415,10 @@
.actions button {
flex: 1;
padding: 14px;
/* Still a full 44px: readying and starting are what a player came
here to press. */
min-height: 44px;
padding: 10px var(--space-3);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
@@ -427,8 +444,8 @@
.leave {
align-self: center;
min-height: 44px;
padding: 10px var(--space-4);
min-height: 36px;
padding: var(--space-1) var(--space-4);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: transparent;
+2 -2
View File
@@ -68,7 +68,7 @@
align-items: center;
flex: 1 1 0;
min-width: 72px;
padding: 6px var(--space-2);
padding: var(--space-1) var(--space-2);
border: 1px solid transparent;
border-radius: var(--radius-sm);
}
@@ -115,7 +115,7 @@
}
.score {
font-size: var(--text-8);
font-size: var(--text-7);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
+2 -2
View File
@@ -21,8 +21,8 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
width: 40px;
height: 40px;
border: 1px solid var(--border-strong);
border-radius: 50%;
background: var(--surface);
+4 -1
View File
@@ -61,7 +61,10 @@
align-items: center;
justify-content: space-between;
gap: 12px;
padding-block: 14px;
/* The header is chrome: every pixel of it is one the board and the
lobby below do not get, and on a 1080p screen the post-game panel
was the one that had to scroll for them. */
padding-block: var(--space-2);
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
}
+1 -1
View File
@@ -470,7 +470,7 @@
*/
.pane.game {
flex: 1;
gap: var(--space-4);
gap: var(--space-3);
min-height: 0;
}