Merge pull request 'feat(ui): merge Exchange/Pass into one action; drop dead Tournaments tab' (#52) from feature/ui-merge-exchange-pass into development
This commit was merged in pull request #52.
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