6.5 KiB
phase, title, status, priority, effort, dependencies
| phase | title | status | priority | effort | dependencies | |
|---|---|---|---|---|---|---|
| 5 | Phase 5: Meanings in the client | completed | P1 | 5h |
|
Phase 5: Meanings in the client
Overview
Show a word's meanings under it in the chain, open for the newest word, closed for the rest, with a click on any word toggling its own; keep the open/closed set in the store so the rules are testable without a browser, and cover the behaviour end to end.
Requirements
- Functional:
ChainEntrygainsmeanings: {pos: string, gloss: string}[], filled fromplayed.meaningsandvalue.openingMeanings. - Functional: the store keeps
expanded: Set<string>of words whose meaning is open, andtoggleMeaning(word). Any number of words may be open at once. Rules (validation session 1: they apply to every word, with or without definitions):gameStarted:expanded = {openingWord}.turnUpdatewith a word: remove the previous newest word (chain[chain.length-1].wordbefore the push), add the new word. AturnUpdatewithout a word (an elimination) changes nothing.toggleMeaning(word): flip membership.reset()clears the set. A resumed session getsgameStartedthen oneturnUpdateand ends up with only the last word open, which is the same as a fresh one.
- Functional:
ChainHistory.svelterenders every word as a<button type="button">witharia-expandedandaria-controlspointing at its panel. The panel is shown when the word is inexpanded: an<ol class="meanings">with one<li>per sense rendered as(pos) gloss, orglossalone whenposis empty; when the word has no senses the panel is a single<p class="meanings none">witht.meaningNone. Everything else in the row — who played it, badges, points, the correction note — is unchanged. - Functional: strings in
web/src/lib/i18n/vi.js:meaningShow/meaningHidefor the button'saria-label(Xem nghĩa của {word}/Ẩn nghĩa của {word}) andmeaningNone(Chưa có nghĩa). - Functional: the auto-scroll effect keeps working — the newest row is at the top and its open list is what the scroll lands on.
- Functional:
history-export.jsis unchanged (non-goal). - Non-functional: the meanings are rendered as text (
{sense}), never with{@html}. - Non-functional: the row stays legible on a phone: the list wraps under the word at full
row width (
flex-basis: 100%), muted colour, ~0.85rem, numbered by the<ol>. - Non-functional:
prefers-reduced-motionrespected if any open/close transition is added; the simplest is none.
Architecture
game.svelte.js
state.chain[i].meanings from the wire
state.expanded Set<string>, client-only
toggleMeaning(word)
ChainHistory.svelte
<li>
<button class="word" type="button" aria-expanded={open} aria-controls={id}
aria-label={fill(open ? t.meaningHide : t.meaningShow, { word: entry.word })}
onclick={() => game.toggleMeaning(entry.word)}>{entry.word}</button>
… by / meta / corrected as today …
{#if open}
{#if entry.meanings.length}
<ol class="meanings" {id}>
{#each entry.meanings as sense}<li>{sense.pos ? `(${sense.pos}) ` : ''}{sense.gloss}</li>{/each}
</ol>
{:else}
<p class="meanings none" {id}>{t.meaningNone}</p>
{/if}
{/if}
</li>
id is derived from the row's index in the chain, not from the word, so two rows never
share one (a word is never played twice, but the opening word could in principle be typed
as a later variant).
The e2e helper chainWords selects ol li .word; the class stays on the button so the
helper and every existing spec keep working, and the nested <ol class="meanings"> has no
.word inside it.
Related Code Files
- Modify:
web/src/lib/stores/game.svelte.js—ChainEntrytypedef,expanded,toggleMeaning, thegameStarted/turnUpdate/resetbranches - Modify:
web/src/lib/components/ChainHistory.svelte— markup and styles - Modify:
web/src/lib/i18n/vi.js— three strings - Modify:
web/tests/game-store.test.js— the expansion rules above, each as a case - Modify:
web/tests/i18n.test.js— only if it enumerates keys - Modify:
web/e2e/helpers.js—openMeanings(page)returning the words whose list is visible;web/e2e/bot-game.spec.js— newest open, previous closed after a move, click toggles;web/e2e/pvp-game.spec.js— the other browser sees the same word open - Modify:
testdata/fixture-words.txt— meanings for the words the specs play (phase 3)
Implementation Steps
- Store first, with tests: opening open; new word closes previous and opens itself; a word with no meanings is opened the same way; elimination leaves the set alone; toggle flips and two words can be open together; reset clears; the resume sequence ends with one open.
- Component markup and styles; check both themes and a 360px viewport.
- i18n strings; run the copy test.
- e2e: fixture meanings, helper, three assertions. CI runs the browser; locally, run what Playwright allows.
npm run check && npm test.
Success Criteria
npm run check && npm testgreen; store tests cover all seven rules.- In a bot game the opening word's meaning is open; after the bot's first move only the bot's word is open; clicking the opening word opens it again while the bot's stays open, and clicking the bot's word closes it.
- Senses render as
(danh từ) …; a sense with an empty label renders the gloss alone; a word without meanings opens toChưa có nghĩa. - e2e specs assert the three behaviours and pass in CI.
- Nothing in the transcript export changed.
Risk Assessment
The button steals focus from the word input. The README says focus is left alone while
a player types, and a chain row that grabs focus on render would break that. Signal: typing
interrupted after a move. Response: never call focus() in the component; the button is
only focusable by the user's own click or Tab.
Long meanings push the chain off screen on a phone. Five senses of 200 characters is a
tall row. Signal: the newest row fills the viewport. Response: the cap is server-side and
already chosen; if it proves too tall, max-height with overflow-y: auto on the list is
a style change. Do not shorten text client-side — it would differ from what the server sent.
A resumed session opens two words. gameStarted opens the opening word; the replayed
turnUpdate must close it. Covered by the rule ordering and a store test using the resume
sequence.