8.6 KiB
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 aretarget="_blank"). No change. - #2 Landing:
howToPlay/howToPlayExampleline added between the tagline and the nickname field (routes/+page.svelte).ChainHistory.svelte's opening row gets anopeningCaptionline styled like.corrected. - #3
GameBoard.svelteheader:ConnectionBadgegained acompactprop (dot-only,sr-onlylabel, whilestatus === 'open'); "Luật chơi" is now a 44px.icon-buttonanchor witharia-label; the room-code/difficulty.modeline moved from the header into.prompt, under the syllable. Chat pill kept itsdata-testid="chat-pill"/chatOpenaria-label; visible text dropped to an emoji +sr-onlytitle + bare unread number (aria-label unchanged, still spells it out).room-code,player-count,scoreboard,chat-toggletestids untouched (they live inRoomCodePanel/Lobby/ScoreBoard/ChatPanel, not this file). - #4
WordInput.svelte: suggestion/report moved onto their own.fixesrow at a 36px floor; suggestion is accent-filled ("primary"), report stays outlined. Classes kept assuggestion/report(plusfix) soword-input.test.js's selectors still match.useSuggestion/reportlogic untouched — still fill-only, field still uncontrolled. - #5 "Bí từ" and "Đầu hàng" now share one
.secondaryrow, bothArmedButton, both always mounted whilephase === 'playing' && !iAmOut, disabled off-turn viaArmedButton's own disarm-on-disable behaviour rather than being mounted/unmounted. - #6 Spectating box (
.spectating,data-testid="eliminated", moved here fromPlayerStatus.svelte) now showsyouAreOutplus the elimination suggestions ornoSuggestions; the duplicate banner inPlayerStatus.svelte(and its.banner.gonestyle) is removed.game-shape.js'sEliminationtypedef gainedsyllable;game-apply.jscapturesstate.currentSyllableinto it at knockout.GameOverPanel.sveltenow readselimination.syllableinstead 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_turnrewritten for the resign/claim race and routed (game-apply.js) intostate.claimErroralongsidenot_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 therole="status"paragraph into a siblingaria-hiddenone;quickMatchWaitingcopy dropped its embedded{n}.quickMatchNudge/quickMatchNudgeLinkreworded to say the nudge leaves the queue. The nudge's/playlink now readssettings.state.lastDifficulty(new store field, persisted tolocalStorage, set from the landingDifficultyPicker'sonselect), falling back to plain/playwhen unset. AddedquickMatchHint/createRoomHintsub-labels under the two entry buttons. - #9
ChainHistory.svelte:.partsis now a<ul>of pill<li>s (aria-label={t.pointsBreakdown}), rendered only whenindex === 0 || open.pointKindLabels[SYLLABLES]renameddài→từ dài. - #10
app.css: ramp collapsed 9→7 with the review's exact values;--accent-hover/--accent-pressedadded (light + dark) and wired into every accent-filled button (WordInput,ChatPanel,Lobby,GameOverPanel, bothroutes/+page.svelteandroutes/online/+page.svelteprimaries); light--bglifted to#eef3ef. No webfont added — system stack kept; the syllable's old1.6remliteral and the countdown ring's1.5remliteral now readvar(--text-7)/var(--text-5)per the review's table. Every existingvar(--text-N)reference across all components was remapped in one pass (old 1/2→new 1, old 3/4/5→new 2, old 6→3, 7→4, 8→5, 9→6) so nothing changed size except the two literals above. The 34 counted6px/10px/14pxspacing literals became--space-2/--space-3/--space-4. Left un-tokenized: the countdown ring's1.75remurgent-value literal andonline/+page.svelte's1.3remh1(no ramp step given for either in the review's table). - Copy (
vi.js+app.html):howToPlay/howToPlayExample,openingCaption,difficultyHints(new table, tested),nicknameHint(collapsed to one sentence),onlineIntro+quickMatchHint/createRoomHint,quickMatchNudge/quickMatchNudgeLink,not_a_dead_end,winsLabel/newwinsCompact(split lobby vs. board),need_more_players,opponentTurn,chatAuthorLeft, meta description,too_fast,pointKindLabels.SYLLABLES,pointsBreakdown.noBestScoreremoved (superseded bydifficultyHints).spectatingkey removed (superseded by the mergedyouAreOut).
Files touched
web/src/app.css, web/src/app.html, web/src/lib/i18n/vi.js, web/src/lib/stores/{game-apply,game-shape,settings.svelte}.js, web/src/lib/components/{AttributionFooter,ChainHistory,ChatPanel,ConnectionBadge,CountdownRing,DifficultyPicker,GameBoard,GameOverPanel,Lobby,NicknameInput,PlayerStatus,RoomCodePanel,ScoreBoard,ThemeToggle,WordInput}.svelte, web/src/routes/{+layout,+page,online/+page,rules/+page}.svelte, web/tests/{game-board,game-store,i18n,settings-store}.test.js, web/e2e/pvp-game.spec.js.
Not touched: server/, proto/, web/src/lib/proto/, word-input.test.js, chat-panel.test.js (no markup change needed in either — verified their selectors/assertions still hold against the new markup).
Tests added
game-board.test.js: "the persistent claim/resign row" — both buttons mounted and enabled on-turn; both stay mounted and becomedisabled(not removed) once the turn moves on.game-store.test.js:not_your_turnroutes toclaimError, not the top banner (mirrors the existingnot_a_dead_endtest); elimination captures the syllable the player was stuck on and keeps it aftercurrentSyllablemoves on.i18n.test.js:difficultyHintswalked againstDifficultySchemathe same waydifficultyLabelsalready is.settings-store.test.js:lastDifficultystartsnull, persists across a reload, ignores a non-numeric stored value, survives hostile storage.
e2e (unverified — no browser on this host)
Two pvp-game.spec.js text assertions changed to match the new copy and are unverified:
wins-p1/wins-p2(Lobby):'Tỉ số 1'→'Ván thắng 1'(and0).series-p2(ScoreBoard):'Tỉ số 1'→'Thắng 1'.lobby-errortoo-fast check:'Bạn thao tác quá nhanh'→'Thao tác quá nhanh'.
No other e2e-referenced testid was renamed (room-code, player-count, scoreboard, chat-toggle, chat-pill, eliminated, player-out, turn-indicator, current-syllable, word-submit, standings, ready/wins-*/series-* all kept). eliminated moved from PlayerStatus.svelte's banner onto GameBoard.svelte's merged spectating box — same attribute, same "is it visible" assertion in pvp-game.spec.js:705, not expected to need a spec change, but unverified since it cannot be run here.
Verification tail
npm run lint → 0 errors, 1 pre-existing warning (tests/room-code.test.js, unrelated)
npm run check → 384 files, 0 errors, 0 warnings
npm test → vite build succeeds; 16 test files, 266 tests passed
Deferred (out of the requested scope)
Everything in the review not named in the deliverable list was left alone by design (KISS — the review's other findings are recorded there for a future pass): the online entry's two-primary-button emphasis, /online's missing rules link, rules-page .toc/.back sizing, wide-screen board width cap, urgent-clock board glow, game-over finalScore/🏆 role="img", lobby seat-colour/kick-disabled-reason, chat timestamp/contrast tweaks, reconnect-stalled ring dash, and the two motion micro-interactions.
Unresolved questions
.ring.urgent .value's1.75remandonline/+page.svelte'sh11.3remare the two font-size literals the review flagged that have no explicit new-ramp mapping in its table (unlike the syllable's1.6remand the ring's base1.5rem, which do). Left as literals rather than guessing a step.eliminated's e2e assertion (pvp-game.spec.js:705,toBeVisible()only, no text check) should still pass since the testid moved intact to the new merged box, but this is unverified without a browser.