diff --git a/plans/reports/fullstack-developer-260921-1632-ux-pass.md b/plans/reports/fullstack-developer-260921-1632-ux-pass.md new file mode 100644 index 0000000..00fa4ac --- /dev/null +++ b/plans/reports/fullstack-developer-260921-1632-ux-pass.md @@ -0,0 +1,59 @@ +# Whole-game UX pass — implementation report + +Branch: `worktree-agent-a4990d1e576b04b80` +Worktree: `/workspace/tiennm99/noitu/.claude/worktrees/agent-a4990d1e576b04b80` +Base: fast-forwarded to `dev` `8c5bb8b` before starting (had `room-session.svelte.js`, `ArmedButton.svelte`, `chat-panel.test.js`). +Source: `plans/reports/ui-ux-designer-260921-1529-whole-game-ux-review.md`. + +## Per-item summary + +- **#1** Already done on `dev` (in-room rules links are `target="_blank"`). No change. +- **#2** Landing: `howToPlay`/`howToPlayExample` line added between the tagline and the nickname field (`routes/+page.svelte`). `ChainHistory.svelte`'s opening row gets an `openingCaption` line styled like `.corrected`. +- **#3** `GameBoard.svelte` header: `ConnectionBadge` gained a `compact` prop (dot-only, `sr-only` label, while `status === 'open'`); "Luật chơi" is now a 44px `.icon-button` anchor with `aria-label`; the room-code/difficulty `.mode` line moved from the header into `.prompt`, under the syllable. Chat pill kept its `data-testid="chat-pill"`/`chatOpen` `aria-label`; visible text dropped to an emoji + `sr-only` title + bare unread number (aria-label unchanged, still spells it out). `room-code`, `player-count`, `scoreboard`, `chat-toggle` testids untouched (they live in `RoomCodePanel`/`Lobby`/`ScoreBoard`/`ChatPanel`, not this file). +- **#4** `WordInput.svelte`: suggestion/report moved onto their own `.fixes` row at a 36px floor; suggestion is accent-filled ("primary"), report stays outlined. Classes kept as `suggestion`/`report` (plus `fix`) so `word-input.test.js`'s selectors still match. `useSuggestion`/`report` logic untouched — still fill-only, field still uncontrolled. +- **#5** "Bí từ" and "Đầu hàng" now share one `.secondary` row, both `ArmedButton`, both always mounted while `phase === 'playing' && !iAmOut`, disabled off-turn via `ArmedButton`'s own disarm-on-disable behaviour rather than being mounted/unmounted. +- **#6** Spectating box (`.spectating`, `data-testid="eliminated"`, moved here from `PlayerStatus.svelte`) now shows `youAreOut` plus the elimination suggestions or `noSuggestions`; the duplicate banner in `PlayerStatus.svelte` (and its `.banner.gone` style) is removed. `game-shape.js`'s `Elimination` typedef gained `syllable`; `game-apply.js` captures `state.currentSyllable` into it at knockout. `GameOverPanel.svelte` now reads `elimination.syllable` instead of the (possibly-moved-on) `game.state.currentSyllable`. +- **#7** `rejectMessages[NOT_YOUR_TURN]` extended to name the race for a submitted word. `errorMessages.not_your_turn` rewritten for the resign/claim race and routed (`game-apply.js`) into `state.claimError` alongside `not_a_dead_end`, so it renders beside the secondary row rather than the top banner. +- **#8** `online/+page.svelte`: the seconds counter moved out of the `role="status"` paragraph into a sibling `aria-hidden` one; `quickMatchWaiting` copy dropped its embedded `{n}`. `quickMatchNudge`/`quickMatchNudgeLink` reworded to say the nudge leaves the queue. The nudge's `/play` link now reads `settings.state.lastDifficulty` (new store field, persisted to `localStorage`, set from the landing `DifficultyPicker`'s `onselect`), falling back to plain `/play` when unset. Added `quickMatchHint`/`createRoomHint` sub-labels under the two entry buttons. +- **#9** `ChainHistory.svelte`: `.parts` is now a `
- {entry.parts - .map((/** @type {import('$lib/stores/game.svelte.js').PointPart} */ p) => - `+${p.value} ${pointKindLabels[p.kind] ?? ''}` - ) - .join(' · ')} -
+ travel with the total. Shown only on the newest row and on + one whose meaning panel is open — every row, in a 9rem list, + used to turn three rows of history into three rows of + arithmetic. --> +- {label} + {label}
diff --git a/web/src/lib/components/GameBoard.svelte b/web/src/lib/components/GameBoard.svelte index a7ffa83..03361c5 100644 --- a/web/src/lib/components/GameBoard.svelte +++ b/web/src/lib/components/GameBoard.svelte @@ -65,12 +65,22 @@{t.spectating}
+{t.youAreOut}
+ {#if game.state.elimination?.suggestions.length} ++ {t.suggestionsTitle}: {game.state.elimination.suggestions.join(' · ')} +
+ {:else if game.state.elimination} ++ {fill(t.noSuggestions, { syllable: game.state.elimination.syllable })} +
+ {/if} +- {game.state.claimError} - -
- {/if} {/if} {/if} - + {#if game.state.phase === 'playing' && !game.iAmOut} -+ {game.state.claimError} + +
+ {/if} {/if}- {fill(t.noSuggestions, { syllable: game.state.currentSyllable })} + {fill(t.noSuggestions, { syllable: elimination.syllable })}
{/if} {/if} @@ -146,7 +150,7 @@ h2 { margin: 0; color: var(--danger); - font-size: var(--text-7); + font-size: var(--text-4); } h2.won { @@ -171,9 +175,9 @@ .standings li { display: flex; align-items: baseline; - gap: 10px; + gap: var(--space-3); padding: var(--space-1) var(--space-3); - font-size: var(--text-5); + font-size: var(--text-2); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-alt); @@ -202,7 +206,7 @@ .points { margin-left: auto; color: var(--text-muted); - font-size: var(--text-4); + font-size: var(--text-2); font-variant-numeric: tabular-nums; } @@ -226,12 +230,12 @@ dt { color: var(--text-muted); - font-size: var(--text-4); + font-size: var(--text-2); } dd { margin: 0; - font-size: var(--text-5); + font-size: var(--text-2); font-weight: 700; font-variant-numeric: tabular-nums; } @@ -243,13 +247,13 @@ flex-wrap: wrap; align-items: baseline; justify-content: center; - gap: 6px var(--space-2); + gap: var(--space-2) var(--space-2); } .suggestions h3 { margin: 0; color: var(--text-muted); - font-size: var(--text-3); + font-size: var(--text-2); font-weight: 600; /* Uppercase Vietnamese stacks a tone mark above a capital. */ line-height: 1.6; @@ -261,7 +265,7 @@ display: flex; flex-wrap: wrap; justify-content: center; - gap: 6px; + gap: var(--space-2); margin: 0; padding: 0; list-style: none; @@ -282,7 +286,7 @@ .record { margin: 0; - padding: 6px var(--space-3); + padding: var(--space-2) var(--space-3); border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--accent); @@ -312,12 +316,21 @@ border-color: transparent; background: var(--accent); color: var(--accent-text); + transition: background-color 150ms ease-out; + } + + .actions .primary:hover { + background: var(--accent-hover); + } + + .actions .primary:active { + background: var(--accent-pressed); } /* Keeping the chain is worth offering and not worth pressing first. */ .actions .export { background: transparent; color: var(--text-muted); - font-size: var(--text-5); + font-size: var(--text-2); } diff --git a/web/src/lib/components/Lobby.svelte b/web/src/lib/components/Lobby.svelte index ab84413..13365f6 100644 --- a/web/src/lib/components/Lobby.svelte +++ b/web/src/lib/components/Lobby.svelte @@ -231,7 +231,7 @@ .count { margin: 0; color: var(--text-muted); - font-size: var(--text-4); + font-size: var(--text-2); font-weight: 600; } @@ -248,11 +248,11 @@ display: flex; flex-wrap: wrap; align-items: baseline; - gap: 10px; + gap: var(--space-3); /* 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; + padding: var(--space-2) var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); @@ -261,7 +261,7 @@ /* 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; + padding: var(--space-1) var(--space-4); } /* Readiness is a tint. Being the owner is a marker down the edge: a fact @@ -294,7 +294,7 @@ border-radius: var(--radius-pill); background: var(--surface-alt); color: var(--text-muted); - font-size: var(--text-2); + font-size: var(--text-1); } /* One right-hand group, so a row keeps its shape whether or not it has a @@ -308,19 +308,19 @@ .wins { color: var(--text-muted); - font-size: var(--text-2); + font-size: var(--text-1); white-space: nowrap; } .wins strong { color: var(--text); - font-size: var(--text-5); + font-size: var(--text-2); font-variant-numeric: tabular-nums; } .state { color: var(--text-muted); - font-size: var(--text-3); + font-size: var(--text-2); } .state.offline { @@ -346,7 +346,7 @@ border-radius: var(--radius-pill); background: transparent; color: var(--text-muted); - font-size: var(--text-6); + font-size: var(--text-3); line-height: 1; } @@ -370,7 +370,7 @@ .hint { margin: 0; color: var(--text-muted); - font-size: var(--text-5); + font-size: var(--text-2); text-align: center; } @@ -380,11 +380,11 @@ justify-content: space-between; gap: var(--space-2); margin: 0; - padding: 10px var(--space-3); + padding: var(--space-3) var(--space-3); border-radius: var(--radius-sm); background: var(--danger-soft); color: var(--danger); - font-size: var(--text-5); + font-size: var(--text-2); } .actions { @@ -397,7 +397,7 @@ /* Still a full 44px: readying and starting are what a player came here to press. */ min-height: 44px; - padding: 10px var(--space-3); + padding: var(--space-3) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); @@ -408,6 +408,15 @@ border-color: transparent; background: var(--accent); color: var(--accent-text); + transition: background-color 150ms ease-out; + } + + .actions .primary:hover:not(:disabled) { + background: var(--accent-hover); + } + + .actions .primary:active:not(:disabled) { + background: var(--accent-pressed); } .actions .primary.on { @@ -429,7 +438,7 @@ border-radius: var(--radius-sm); background: transparent; color: var(--text-muted); - font-size: var(--text-4); + font-size: var(--text-2); } .leave:disabled { @@ -439,7 +448,7 @@ .note { margin: 0; color: var(--text-muted); - font-size: var(--text-3); + font-size: var(--text-2); text-align: center; } diff --git a/web/src/lib/components/NicknameInput.svelte b/web/src/lib/components/NicknameInput.svelte index 01d06ce..aa7ceeb 100644 --- a/web/src/lib/components/NicknameInput.svelte +++ b/web/src/lib/components/NicknameInput.svelte @@ -29,25 +29,25 @@ .field { display: flex; flex-direction: column; - gap: 6px; + gap: var(--space-2); } label { font-weight: 600; - font-size: var(--text-5); + font-size: var(--text-2); } input { - padding: var(--space-3) 14px; + padding: var(--space-3) var(--space-4); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); - font-size: var(--text-6); + font-size: var(--text-3); } .hint { margin: 0; color: var(--text-muted); - font-size: var(--text-3); + font-size: var(--text-2); } diff --git a/web/src/lib/components/PlayerStatus.svelte b/web/src/lib/components/PlayerStatus.svelte index a36c59e..326ba2a 100644 --- a/web/src/lib/components/PlayerStatus.svelte +++ b/web/src/lib/components/PlayerStatus.svelte @@ -59,11 +59,7 @@ } -{#if game.iAmOut} - - -{:else if game.state.lastOut && !game.state.lastOut.isMe && game.state.phase === 'playing'} +{#if game.state.lastOut && !game.state.lastOut.isMe && game.state.phase === 'playing'} {/if} @@ -81,16 +77,11 @@ diff --git a/web/src/lib/components/RoomCodePanel.svelte b/web/src/lib/components/RoomCodePanel.svelte index b5fe29f..5659708 100644 --- a/web/src/lib/components/RoomCodePanel.svelte +++ b/web/src/lib/components/RoomCodePanel.svelte @@ -110,7 +110,7 @@ display: flex; flex-direction: column; align-items: center; - gap: 10px; + gap: var(--space-3); padding: var(--space-5); border: 1px solid var(--border); border-radius: var(--radius); @@ -135,7 +135,7 @@ .label { margin: 0; color: var(--text-muted); - font-size: var(--text-4); + font-size: var(--text-2); } /* @@ -146,13 +146,13 @@ .code { margin: 0; font-family: ui-monospace, 'SFMono-Regular', 'Cascadia Mono', Menlo, monospace; - font-size: var(--text-9); + font-size: var(--text-6); font-weight: 700; letter-spacing: 0.12em; } .panel.compact .code { - font-size: var(--text-7); + font-size: var(--text-4); } .actions { @@ -164,18 +164,18 @@ .actions button { min-height: 44px; - padding: 10px var(--space-4); + padding: var(--space-3) var(--space-4); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-alt); - font-size: var(--text-4); + font-size: var(--text-2); font-weight: 600; } .failed { margin: 0; color: var(--danger); - font-size: var(--text-3); + font-size: var(--text-2); text-align: center; } @@ -187,7 +187,7 @@ margin: 0; max-width: 100%; color: var(--text-muted); - font-size: var(--text-3); + font-size: var(--text-2); } .link-label { diff --git a/web/src/lib/components/ScoreBoard.svelte b/web/src/lib/components/ScoreBoard.svelte index db70a5a..4b10840 100644 --- a/web/src/lib/components/ScoreBoard.svelte +++ b/web/src/lib/components/ScoreBoard.svelte @@ -1,5 +1,5 @@