From 644379fa060c7dbeded72f98b014449b6a7c63cc Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 10 Sep 2026 15:16:25 +0700 Subject: [PATCH] style(web): fit a finished game and its lobby on one screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../lib/components/AttributionFooter.svelte | 4 +- web/src/lib/components/GameBoard.svelte | 9 ++- web/src/lib/components/GameOverPanel.svelte | 81 ++++++++++++------- web/src/lib/components/Lobby.svelte | 31 +++++-- web/src/lib/components/ScoreBoard.svelte | 4 +- web/src/lib/components/ThemeToggle.svelte | 4 +- web/src/routes/+layout.svelte | 5 +- web/src/routes/online/+page.svelte | 2 +- 8 files changed, 96 insertions(+), 44 deletions(-) diff --git a/web/src/lib/components/AttributionFooter.svelte b/web/src/lib/components/AttributionFooter.svelte index 86ea2f0..de77c5c 100644 --- a/web/src/lib/components/AttributionFooter.svelte +++ b/web/src/lib/components/AttributionFooter.svelte @@ -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); diff --git a/web/src/lib/components/GameBoard.svelte b/web/src/lib/components/GameBoard.svelte index 5ef631c..a4090c2 100644 --- a/web/src/lib/components/GameBoard.svelte +++ b/web/src/lib/components/GameBoard.svelte @@ -84,7 +84,12 @@ {#if modeLabel}{modeLabel}{/if} - + + {#if game.state.phase !== 'over'} + + {/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); } diff --git a/web/src/lib/components/GameOverPanel.svelte b/web/src/lib/components/GameOverPanel.svelte index 7516ee2..ba68b65 100644 --- a/web/src/lib/components/GameOverPanel.svelte +++ b/web/src/lib/components/GameOverPanel.svelte @@ -62,9 +62,11 @@

{endReasonMessages[result.reason]}

{/if} - {#if standings.length > 2} + {#if standings.length > 1} + 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. -->
    {#each standings as player (player.playerId)}
  1. @@ -97,7 +99,7 @@ nothing, which is worth hearing too. --> {#if elimination.suggestions.length > 0}
    -

    {t.suggestionsTitle}

    +

    {t.suggestionsTitle}:

      {#each elimination.suggestions as word}
    • {word}
    • @@ -115,14 +117,17 @@

      {t.newRecord}

      {/if} +
      {#if onrematch} {/if} +
      - -
    {/if} @@ -130,8 +135,8 @@ .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); + } diff --git a/web/src/lib/components/Lobby.svelte b/web/src/lib/components/Lobby.svelte index 215ae69..fa66ed9 100644 --- a/web/src/lib/components/Lobby.svelte +++ b/web/src/lib/components/Lobby.svelte @@ -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)); -
    +
    {#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; diff --git a/web/src/lib/components/ScoreBoard.svelte b/web/src/lib/components/ScoreBoard.svelte index 67e1703..db70a5a 100644 --- a/web/src/lib/components/ScoreBoard.svelte +++ b/web/src/lib/components/ScoreBoard.svelte @@ -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; } diff --git a/web/src/lib/components/ThemeToggle.svelte b/web/src/lib/components/ThemeToggle.svelte index e004128..38642e3 100644 --- a/web/src/lib/components/ThemeToggle.svelte +++ b/web/src/lib/components/ThemeToggle.svelte @@ -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); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 383dabd..2aec6c6 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -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)); } diff --git a/web/src/routes/online/+page.svelte b/web/src/routes/online/+page.svelte index d8b955b..7fe9c24 100644 --- a/web/src/routes/online/+page.svelte +++ b/web/src/routes/online/+page.svelte @@ -470,7 +470,7 @@ */ .pane.game { flex: 1; - gap: var(--space-4); + gap: var(--space-3); min-height: 0; }