feat(ui): merge Exchange/Pass into one action; drop dead Tournaments tab
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m0s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m0s
Lobby: remove the inert 🏆 Tournaments tab (it only raised a 'coming soon' toast); the lobby is back to three tabs, matching docs/FUNCTIONAL.md. Game: fold the separate 🥺 Skip (pass) tab into the 🔄 tab, now Exchange/Pass. Its dialog passes when no tile is selected (button 'Pass without exchanging') and exchanges when tiles are ('Exchange N'). The tab is no longer gated on an empty bag (pass must stay reachable in the endgame); inside the dialog tile selection is disabled while the bag is below a full rack (bagLen >= RACK_SIZE). The merge is UI-only. A pass is NOT an exchange of zero tiles: the rules allow an exchange only with a full rack left in the bag and forbid a zero-tile swap, while a pass is always legal; GCG (Poslfit) writes a pass as a bare '-' and an exchange as '-TILES'. Pass and exchange stay distinct end-to-end (wire, engine, history/GCG); the dialog dispatches the existing gateway.pass / gateway.exchange. No backend/wire/history/GCG change. Docs: docs/UI_DESIGN.md, docs/FUNCTIONAL.md (+_ru), PRERELEASE.md. Tests: ui/e2e/game.spec.ts.
This commit is contained in:
@@ -442,3 +442,23 @@ Then Stage 18.
|
|||||||
- **No schema/wire change → no contour DB wipe.** Bake-back: `docs/UI_DESIGN.md`, `docs/FUNCTIONAL.md`
|
- **No schema/wire change → no contour DB wipe.** Bake-back: `docs/UI_DESIGN.md`, `docs/FUNCTIONAL.md`
|
||||||
(+`_ru`). Regression gate: UI `check` + unit (`tapconfirm`) + build + bundle budget + e2e (Chromium &
|
(+`_ru`). Regression gate: UI `check` + unit (`tapconfirm`) + build + bundle budget + e2e (Chromium &
|
||||||
WebKit), all green.
|
WebKit), all green.
|
||||||
|
|
||||||
|
- **UI — Merge Exchange/Pass; drop the dead Tournaments tab** (owner ad-hoc, not on the raw TODO
|
||||||
|
list): the lobby's 🏆 *Tournaments* tab was an inert `lobby.soon` toast — removed (the lobby is back
|
||||||
|
to three tabs, matching `docs/FUNCTIONAL.md`). In-game the separate 🥺 *Skip* (pass) tab folds into
|
||||||
|
the 🔄 tab, now **Exchange/Pass**, whose dialog passes when no tile is selected and exchanges when
|
||||||
|
tiles are.
|
||||||
|
- **Decision — a pass is NOT an exchange of zero (verified against the rules + GCG):** the merge is
|
||||||
|
**UI-only**. Pass and exchange stay distinct game actions end-to-end — wire (`GameActionRequest` vs
|
||||||
|
`ExchangeRequest`), engine (`ActionPass` vs `ActionExchange`), and the GCG Poslfit dialect (a pass is
|
||||||
|
a bare `-`, an exchange is `-TILES`). The engine forbids a zero-tile exchange (`ErrNothingToExchange`)
|
||||||
|
and allows an exchange only with a full rack left in the bag (`ErrNotEnoughTilesToExchange`), while a
|
||||||
|
pass is always legal — collapsing them would lose a real distinction. The dialog dispatches the
|
||||||
|
existing `gateway.pass` / `gateway.exchange`.
|
||||||
|
- **What shipped:** `Lobby.svelte` (tab removed); `Game.svelte` (one 🔄 Exchange/Pass tab no longer
|
||||||
|
gated on an empty bag; the dialog disables tile selection while the bag is below a full rack
|
||||||
|
(`bagLen >= RACK_SIZE`), its confirm button reading **Pass without exchanging** / **Exchange N**);
|
||||||
|
i18n (`game.draw` → Exchange/Pass, new `game.passNoExchange`, dropped `game.skip` /
|
||||||
|
`lobby.tournaments` / `lobby.soon`). No backend/wire/history/GCG change.
|
||||||
|
- **No schema/wire change → no contour DB wipe.** Bake-back: `docs/UI_DESIGN.md`, `docs/FUNCTIONAL.md`
|
||||||
|
(+`_ru`). Regression gate: UI `check` + unit + build + bundle budget + e2e (Chromium & WebKit).
|
||||||
|
|||||||
+4
-2
@@ -102,8 +102,10 @@ settings and the game starts once every invitee has accepted — any decline can
|
|||||||
expires after seven days.
|
expires after seven days.
|
||||||
|
|
||||||
### Playing a game
|
### Playing a game
|
||||||
Place tiles, pass, exchange, or resign. Tiles are laid without choosing a
|
Place tiles, pass, exchange, or resign. Pass and exchange share one control —
|
||||||
direction — the game infers the play's orientation, so a single tile that extends
|
choosing no tiles passes the turn, choosing tiles exchanges them. Tiles are laid
|
||||||
|
without choosing a direction — the game infers the play's orientation, so a single
|
||||||
|
tile that extends
|
||||||
an existing word (down a column or across a row) is accepted. A play is validated
|
an existing word (down a column or across a row) is accepted. A play is validated
|
||||||
against the game's dictionary at submit time and scored; an unlimited preview
|
against the game's dictionary at submit time and scored; an unlimited preview
|
||||||
reports the word(s) a tentative move would form and its score, or that it is not
|
reports the word(s) a tentative move would form and its score, or that it is not
|
||||||
|
|||||||
@@ -106,8 +106,10 @@ nudge) приходят от бота **этой партии** — по язы
|
|||||||
ответа приглашение протухает через семь дней.
|
ответа приглашение протухает через семь дней.
|
||||||
|
|
||||||
### Игровой процесс
|
### Игровой процесс
|
||||||
Выкладывание фишек, пас, обмен или сдача. Фишки кладутся без выбора направления —
|
Выкладывание фишек, пас, обмен или сдача. Пас и обмен — один элемент управления:
|
||||||
игра сама определяет ориентацию хода, поэтому одна фишка, продолжающая уже лежащее
|
без выбранных фишек — пас, с выбранными — обмен. Фишки кладутся без выбора
|
||||||
|
направления — игра сама определяет ориентацию хода, поэтому одна фишка, продолжающая
|
||||||
|
уже лежащее
|
||||||
слово (по столбцу или по строке), принимается. Ход проверяется по словарю партии при
|
слово (по столбцу или по строке), принимается. Ход проверяется по словарю партии при
|
||||||
сдаче и считается; безлимитный предпросмотр показывает слово (или слова), которое
|
сдаче и считается; безлимитный предпросмотр показывает слово (или слова), которое
|
||||||
образует предполагаемый ход, и его очки — либо что ход недопустим, — и ход можно
|
образует предполагаемый ход, и его очки — либо что ход недопустим, — и ход можно
|
||||||
|
|||||||
+7
-4
@@ -142,15 +142,18 @@ Login uses `Screen`.
|
|||||||
- **TapConfirm** (`components/TapConfirm.svelte`, logic in `lib/tapconfirm.ts`): the shared
|
- **TapConfirm** (`components/TapConfirm.svelte`, logic in `lib/tapconfirm.ts`): the shared
|
||||||
tap-to-confirm control. A first tap arms a ~2 s window showing a **fading ✅** (no fade
|
tap-to-confirm control. A first tap arms a ~2 s window showing a **fading ✅** (no fade
|
||||||
under reduce-motion, but the window still holds); a tap on the ✅ within it confirms,
|
under reduce-motion, but the window still holds); a tap on the ✅ within it confirms,
|
||||||
otherwise it reverts. Used by the **Skip** and **Hint** tabs (the icon morphs to ✅, no
|
otherwise it reverts. Used by the **Hint** tab (the icon morphs to ✅, no label — replacing
|
||||||
label — replacing the old press-and-hold popover) and the in-game **add-friend 🤝**. The
|
the old press-and-hold popover) and the in-game **add-friend 🤝**. The
|
||||||
**Drop game** action keeps its `Modal` confirmation (a destructive, less-frequent action).
|
**Drop game** action keeps its `Modal` confirmation (a destructive, less-frequent action).
|
||||||
- **MakeMove / Reset**: when ≥1 tile is pending the rack collapses its used slots
|
- **MakeMove / Reset**: when ≥1 tile is pending the rack collapses its used slots
|
||||||
and shifts left, a **borderless ✅ icon button** (styled like a tab, not a filled accent
|
and shifts left, a **borderless ✅ icon button** (styled like a tab, not a filled accent
|
||||||
button) beside the rack commits the move — no popover, and disabled while the pending word
|
button) beside the rack commits the move — no popover, and disabled while the pending word
|
||||||
is known illegal; the 🔀 Shuffle tab is replaced by a **↩️ Reset** tab.
|
is known illegal; the 🔀 Shuffle tab is replaced by a **↩️ Reset** tab.
|
||||||
- **Game tab bar**: 🔄 Draw (disabled when the bag is empty), 🥺 Skip, 🛟 Hint (with a
|
- **Game tab bar**: 🔄 Exchange/Pass (opens a dialog — pick tiles to **Exchange N**, or pick
|
||||||
remaining-count badge, disabled at zero); 🔀 Shuffle (no label, no confirm), which
|
none to **Pass without exchanging**; pass is always available on your turn, exchange only
|
||||||
|
while the bag still holds a full rack, below which the tiles disable and only the pass
|
||||||
|
remains), 🛟 Hint (with a remaining-count badge, disabled at zero); 🔀 Shuffle (no label,
|
||||||
|
no confirm), which
|
||||||
**animates** — tiles hop along a low parabola to their new slots (duration scaled by the
|
**animates** — tiles hop along a low parabola to their new slots (duration scaled by the
|
||||||
distance, the longest ≤ 0.3 s; off under reduce-motion) with a short haptic shake. The
|
distance, the longest ≤ 0.3 s; off under reduce-motion) with a short haptic shake. The
|
||||||
under-board slot shows the **Scores: N** preview. The screen **title** is the variant's
|
under-board slot shows the **Scores: N** preview. The screen **title** is the variant's
|
||||||
|
|||||||
+19
-11
@@ -17,7 +17,8 @@ async function openGame(page: Page): Promise<void> {
|
|||||||
|
|
||||||
test('offline shows the Connecting indicator and softly disables server actions', async ({ page }) => {
|
test('offline shows the Connecting indicator and softly disables server actions', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
// The exchange/draw tab is a server action; on my turn with tiles in the bag it is live.
|
// The Exchange/Pass tab is a server action; on my turn it is live (pass is always available,
|
||||||
|
// so unlike before it no longer goes inert when the bag empties).
|
||||||
const draw = page.locator('.tab').first();
|
const draw = page.locator('.tab').first();
|
||||||
await expect(draw).toBeEnabled();
|
await expect(draw).toBeEnabled();
|
||||||
await expect(page.getByText('Connecting…')).toHaveCount(0);
|
await expect(page.getByText('Connecting…')).toHaveCount(0);
|
||||||
@@ -179,25 +180,32 @@ test('history: a swipe-up close does not make a follow-up score tap jump', async
|
|||||||
await expect(page.locator('.boardwrap.slid')).toBeVisible();
|
await expect(page.locator('.boardwrap.slid')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Draw opens the exchange dialog and confirms a selection', async ({ page }) => {
|
test('Exchange/Pass dialog: a selection exchanges, no selection offers a pass', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
await page.locator('button:has-text("🔄")').click(); // Draw tab
|
await page.locator('button:has-text("🔄")').click(); // the merged Exchange/Pass tab
|
||||||
await expect(page.locator('.exch')).toBeVisible();
|
await expect(page.locator('.exch')).toBeVisible();
|
||||||
|
|
||||||
|
// With nothing selected the confirm button offers a pass; selecting tiles flips it to an
|
||||||
|
// exchange of that many (the two are distinct game actions behind one dialog).
|
||||||
|
const confirm = page.locator('button.confirm');
|
||||||
|
await expect(confirm).toHaveText('Pass without exchanging');
|
||||||
await page.locator('.etile').first().click();
|
await page.locator('.etile').first().click();
|
||||||
await expect(page.locator('.etile.sel')).toHaveCount(1);
|
await expect(page.locator('.etile.sel')).toHaveCount(1);
|
||||||
await page.locator('button.confirm').click();
|
await expect(confirm).toHaveText('Exchange 1');
|
||||||
|
await confirm.click();
|
||||||
await expect(page.locator('.exch')).toBeHidden();
|
await expect(page.locator('.exch')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pass confirms with a tap on the fading ✅ instead of a popup', async ({ page }) => {
|
test('passing goes through the Exchange/Pass dialog with no tiles selected', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
const pass = page.getByRole('button', { name: 'Skip' }); // the 🥺 tab (aria-label)
|
const draw = page.locator('button:has-text("🔄")'); // the merged Exchange/Pass tab
|
||||||
await expect(pass).toBeEnabled();
|
await expect(draw).toBeEnabled();
|
||||||
await pass.click(); // arm: 🥺 -> a fading ✅
|
await draw.click();
|
||||||
await pass.click(); // tap the ✅ to confirm within the window
|
// No tiles selected -> the confirm button passes the turn (a pass, not an exchange of zero).
|
||||||
// The pass hands the turn over, so the control goes inert.
|
await page.locator('button.confirm').click();
|
||||||
await expect(pass).toBeDisabled();
|
await expect(page.locator('.exch')).toBeHidden();
|
||||||
|
// The pass hands the turn over, so the tab goes inert.
|
||||||
|
await expect(draw).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check-word sanitises input and shows a verdict', async ({ page }) => {
|
test('check-word sanitises input and shows a verdict', async ({ page }) => {
|
||||||
|
|||||||
+21
-8
@@ -104,7 +104,13 @@
|
|||||||
const playable = $derived(!!view && (view.game.status === 'active' || view.game.status === 'open'));
|
const playable = $derived(!!view && (view.game.status === 'active' || view.game.status === 'open'));
|
||||||
const isMyTurn = $derived(!!view && playable && view.game.toMove === view.seat);
|
const isMyTurn = $derived(!!view && playable && view.game.toMove === view.seat);
|
||||||
const gameOver = $derived(!!view && view.game.status === 'finished');
|
const gameOver = $derived(!!view && view.game.status === 'finished');
|
||||||
const bagEmpty = $derived((view?.bagLen ?? 0) === 0);
|
// RACK_SIZE mirrors the engine's rules.RackSize (7 for every current variant). The exchange
|
||||||
|
// gate is only a UX guard: the backend stays the source of truth and rejects an under-supplied
|
||||||
|
// exchange regardless (engine rejects when bag.Len() < rules.RackSize).
|
||||||
|
const RACK_SIZE = 7;
|
||||||
|
// Exchange is legal only while the bag still holds a full rack; below that the player may pass
|
||||||
|
// but not swap, so the merged Exchange/Pass dialog offers pass alone there.
|
||||||
|
const canExchange = $derived((view?.bagLen ?? 0) >= RACK_SIZE);
|
||||||
// The seat whose move the history grid awaits with a "thinking…" placeholder: the player to
|
// The seat whose move the history grid awaits with a "thinking…" placeholder: the player to
|
||||||
// move while the game is active, but never the viewer themselves (their own pending cell
|
// move while the game is active, but never the viewer themselves (their own pending cell
|
||||||
// stays empty) and never on a finished game.
|
// stays empty) and never on a finished game.
|
||||||
@@ -634,6 +640,16 @@
|
|||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// confirmExchangeOrPass routes the merged dialog's confirm button: no tiles selected means a
|
||||||
|
// pass (a distinct game action), any selection means a tile exchange.
|
||||||
|
async function confirmExchangeOrPass() {
|
||||||
|
if (exchangeSel.length === 0) {
|
||||||
|
exchangeOpen = false;
|
||||||
|
await doPass();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await doExchange();
|
||||||
|
}
|
||||||
|
|
||||||
function resultText(): string {
|
function resultText(): string {
|
||||||
if (!view) return '';
|
if (!view) return '';
|
||||||
@@ -917,12 +933,9 @@
|
|||||||
{#snippet tabbar()}
|
{#snippet tabbar()}
|
||||||
{#if view}
|
{#if view}
|
||||||
<TabBar>
|
<TabBar>
|
||||||
<button class="tab" disabled={busy || !isMyTurn || !connection.online || bagEmpty} onclick={openExchange}>
|
<button class="tab" disabled={busy || !isMyTurn || !connection.online} onclick={openExchange}>
|
||||||
<span class="sq">🔄</span><span class="lbl">{t('game.draw')}</span>
|
<span class="sq">🔄</span><span class="lbl">{t('game.draw')}</span>
|
||||||
</button>
|
</button>
|
||||||
<TapConfirm triggerClass="tab" label={t('game.skip')} disabled={busy || !isMyTurn || !connection.online} onconfirm={doPass}>
|
|
||||||
<span class="sq">🥺</span><span class="lbl">{t('game.skip')}</span>
|
|
||||||
</TapConfirm>
|
|
||||||
<TapConfirm
|
<TapConfirm
|
||||||
triggerClass="tab"
|
triggerClass="tab"
|
||||||
label={t('game.hint')}
|
label={t('game.hint')}
|
||||||
@@ -966,13 +979,13 @@
|
|||||||
<Modal title={t('game.exchangeTitle')} onclose={() => (exchangeOpen = false)}>
|
<Modal title={t('game.exchangeTitle')} onclose={() => (exchangeOpen = false)}>
|
||||||
<div class="exch">
|
<div class="exch">
|
||||||
{#each view.rack as letter, i (i)}
|
{#each view.rack as letter, i (i)}
|
||||||
<button class="etile" class:sel={exchangeSel.includes(i)} onclick={() => toggleExch(i)}>
|
<button class="etile" class:sel={exchangeSel.includes(i)} disabled={!canExchange} onclick={() => toggleExch(i)}>
|
||||||
{letter === BLANK ? '?' : letter}
|
{letter === BLANK ? '?' : letter}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="confirm" disabled={exchangeSel.length === 0} onclick={doExchange}>
|
<button class="confirm" onclick={confirmExchangeOrPass}>
|
||||||
{t('game.exchangeConfirm', { n: exchangeSel.length })}
|
{exchangeSel.length === 0 ? t('game.passNoExchange') : t('game.exchangeConfirm', { n: exchangeSel.length })}
|
||||||
</button>
|
</button>
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const en = {
|
|||||||
'lobby.noFinished': 'No finished games yet.',
|
'lobby.noFinished': 'No finished games yet.',
|
||||||
'lobby.new': 'New',
|
'lobby.new': 'New',
|
||||||
'lobby.stats': 'Stats',
|
'lobby.stats': 'Stats',
|
||||||
'lobby.tournaments': 'Tourn.',
|
|
||||||
'lobby.profile': 'Profile',
|
'lobby.profile': 'Profile',
|
||||||
'lobby.settings': 'Settings',
|
'lobby.settings': 'Settings',
|
||||||
'lobby.about': 'About',
|
'lobby.about': 'About',
|
||||||
@@ -38,7 +37,6 @@ export const en = {
|
|||||||
'lobby.theirTurn': 'Their turn',
|
'lobby.theirTurn': 'Their turn',
|
||||||
'lobby.hideGame': 'Remove from list',
|
'lobby.hideGame': 'Remove from list',
|
||||||
'lobby.vs': 'vs {opponents}',
|
'lobby.vs': 'vs {opponents}',
|
||||||
'lobby.soon': 'Coming soon',
|
|
||||||
|
|
||||||
'new.title': 'New game',
|
'new.title': 'New game',
|
||||||
'new.subtitle': 'Auto-match with another player',
|
'new.subtitle': 'Auto-match with another player',
|
||||||
@@ -63,8 +61,7 @@ export const en = {
|
|||||||
'game.waiting': "Waiting for {name}",
|
'game.waiting': "Waiting for {name}",
|
||||||
'game.makeMove': 'Make move',
|
'game.makeMove': 'Make move',
|
||||||
'game.reset': 'Reset',
|
'game.reset': 'Reset',
|
||||||
'game.draw': 'Draw',
|
'game.draw': 'Exchange/Pass',
|
||||||
'game.skip': 'Skip',
|
|
||||||
'game.shuffle': 'Shuffle',
|
'game.shuffle': 'Shuffle',
|
||||||
'game.hint': 'Hint',
|
'game.hint': 'Hint',
|
||||||
'game.chat': 'Chat',
|
'game.chat': 'Chat',
|
||||||
@@ -75,8 +72,9 @@ export const en = {
|
|||||||
'game.previewIllegal': 'Not a legal move',
|
'game.previewIllegal': 'Not a legal move',
|
||||||
'game.oneWordRule': 'One word per turn',
|
'game.oneWordRule': 'One word per turn',
|
||||||
'game.chooseBlank': 'Choose a letter for the blank',
|
'game.chooseBlank': 'Choose a letter for the blank',
|
||||||
'game.exchangeTitle': 'Select tiles to exchange',
|
'game.exchangeTitle': 'Exchange or pass',
|
||||||
'game.exchangeConfirm': 'Exchange {n}',
|
'game.exchangeConfirm': 'Exchange {n}',
|
||||||
|
'game.passNoExchange': 'Pass without exchanging',
|
||||||
'game.confirmResign': 'Resign this game?',
|
'game.confirmResign': 'Resign this game?',
|
||||||
'game.hintShown': 'Best move: {word} for {n}',
|
'game.hintShown': 'Best move: {word} for {n}',
|
||||||
'game.over': 'Game over',
|
'game.over': 'Game over',
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'lobby.noFinished': 'Пока нет завершённых игр.',
|
'lobby.noFinished': 'Пока нет завершённых игр.',
|
||||||
'lobby.new': 'Новая',
|
'lobby.new': 'Новая',
|
||||||
'lobby.stats': 'Статы',
|
'lobby.stats': 'Статы',
|
||||||
'lobby.tournaments': 'Турниры',
|
|
||||||
'lobby.profile': 'Профиль',
|
'lobby.profile': 'Профиль',
|
||||||
'lobby.settings': 'Настройки',
|
'lobby.settings': 'Настройки',
|
||||||
'lobby.about': 'О программе',
|
'lobby.about': 'О программе',
|
||||||
@@ -39,7 +38,6 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'lobby.theirTurn': 'Ход соперника',
|
'lobby.theirTurn': 'Ход соперника',
|
||||||
'lobby.hideGame': 'Убрать из списка',
|
'lobby.hideGame': 'Убрать из списка',
|
||||||
'lobby.vs': 'против {opponents}',
|
'lobby.vs': 'против {opponents}',
|
||||||
'lobby.soon': 'Скоро',
|
|
||||||
|
|
||||||
'new.title': 'Новая игра',
|
'new.title': 'Новая игра',
|
||||||
'new.subtitle': 'Автоподбор соперника',
|
'new.subtitle': 'Автоподбор соперника',
|
||||||
@@ -64,8 +62,7 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'game.waiting': 'Ожидаем {name}',
|
'game.waiting': 'Ожидаем {name}',
|
||||||
'game.makeMove': 'Сделать ход',
|
'game.makeMove': 'Сделать ход',
|
||||||
'game.reset': 'Сброс',
|
'game.reset': 'Сброс',
|
||||||
'game.draw': 'Обмен',
|
'game.draw': 'Обмен/Пас',
|
||||||
'game.skip': 'Пас',
|
|
||||||
'game.shuffle': 'Перемешать',
|
'game.shuffle': 'Перемешать',
|
||||||
'game.hint': 'Подсказка',
|
'game.hint': 'Подсказка',
|
||||||
'game.chat': 'Чат',
|
'game.chat': 'Чат',
|
||||||
@@ -76,8 +73,9 @@ export const ru: Record<MessageKey, string> = {
|
|||||||
'game.previewIllegal': 'Недопустимый ход',
|
'game.previewIllegal': 'Недопустимый ход',
|
||||||
'game.oneWordRule': 'Одно слово за ход',
|
'game.oneWordRule': 'Одно слово за ход',
|
||||||
'game.chooseBlank': 'Выберите букву для бланка',
|
'game.chooseBlank': 'Выберите букву для бланка',
|
||||||
'game.exchangeTitle': 'Выберите фишки для обмена',
|
'game.exchangeTitle': 'Обмен или пас',
|
||||||
'game.exchangeConfirm': 'Обменять {n}',
|
'game.exchangeConfirm': 'Обменять {n}',
|
||||||
|
'game.passNoExchange': 'Пас без обмена',
|
||||||
'game.confirmResign': 'Сдаться в этой игре?',
|
'game.confirmResign': 'Сдаться в этой игре?',
|
||||||
'game.hintShown': 'Лучший ход: {word} на {n}',
|
'game.hintShown': 'Лучший ход: {word} на {n}',
|
||||||
'game.over': 'Игра окончена',
|
'game.over': 'Игра окончена',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Screen from '../components/Screen.svelte';
|
import Screen from '../components/Screen.svelte';
|
||||||
import TabBar from '../components/TabBar.svelte';
|
import TabBar from '../components/TabBar.svelte';
|
||||||
import { app, handleError, showToast } from '../lib/app.svelte';
|
import { app, handleError } from '../lib/app.svelte';
|
||||||
import { connection } from '../lib/connection.svelte';
|
import { connection } from '../lib/connection.svelte';
|
||||||
import { gateway } from '../lib/gateway';
|
import { gateway } from '../lib/gateway';
|
||||||
import { navigate } from '../lib/router.svelte';
|
import { navigate } from '../lib/router.svelte';
|
||||||
@@ -228,9 +228,6 @@
|
|||||||
<button class="tab" onclick={() => navigate('/stats')}>
|
<button class="tab" onclick={() => navigate('/stats')}>
|
||||||
<span class="sq">📊</span><span class="lbl">{t('lobby.stats')}</span>
|
<span class="sq">📊</span><span class="lbl">{t('lobby.stats')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab" onclick={() => showToast(t('lobby.soon'))}>
|
|
||||||
<span class="sq">🏆</span><span class="lbl">{t('lobby.tournaments')}</span>
|
|
||||||
</button>
|
|
||||||
<button class="tab" onclick={() => navigate('/settings')}>
|
<button class="tab" onclick={() => navigate('/settings')}>
|
||||||
<span class="sq">⚙️{#if app.notifications > 0}<span class="badge">{app.notifications}</span>{/if}</span>
|
<span class="sq">⚙️{#if app.notifications > 0}<span class="badge">{app.notifications}</span>{/if}</span>
|
||||||
<span class="lbl">{t('lobby.settings')}</span>
|
<span class="lbl">{t('lobby.settings')}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user