diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 36c1c57..f5193e6 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -111,7 +111,9 @@ an existing word (down a column or across a row) is accepted. A play is validate 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 legal, and the move is offered for submission only once it is confirmed legal. The dictionary check tool is -unlimited and offers a complaint on any result. Hints are governed per game — +unlimited and offers a complaint on any result; for a word it finds, it also links out to an +external reference dictionary (gramota.ru for Russian games, scrabblewordfinder.org for +English) to look it up. Hints are governed per game — whether they are allowed and how many each player starts with — and draw on a personal hint wallet once the per-game allowance is spent. The game ends when the bag empties and a player clears their rack, after 6 consecutive scoreless turns, diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index 2a7d38a..a08598a 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -115,7 +115,9 @@ nudge) приходят от бота **этой партии** — по язы сдаче и считается; безлимитный предпросмотр показывает слово (или слова), которое образует предполагаемый ход, и его очки — либо что ход недопустим, — и ход можно отправить только после подтверждения, что он допустим. Инструмент проверки слова безлимитный и -предлагает пожаловаться на любой результат. Подсказки управляются настройками +предлагает пожаловаться на любой результат; для найденного слова он также даёт ссылку на +внешний справочный словарь (gramota.ru для русских игр, scrabblewordfinder.org для английских), +чтобы его посмотреть. Подсказки управляются настройками партии — разрешены ли они и сколько их у каждого игрока на старте — и расходуют личный кошелёк подсказок после исчерпания внутриигрового лимита. Партия завершается, когда мешок пуст и игрок выложил стойку, после 6 подряд бесплодных diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md index a5dca0a..2a6bfd3 100644 --- a/docs/UI_DESIGN.md +++ b/docs/UI_DESIGN.md @@ -73,7 +73,10 @@ Login uses `Screen`. **closing confirmation** is enabled while a game is open **on mobile only** (on desktop closing is deliberate and the "changes may not be saved" dialog is just noise — move drafts auto-save); **vertical swipes** (swipe-to-minimise) - are disabled so they don't fight tile drag or the board scroll; and a live stream dropped + are disabled so they don't fight tile drag or the board scroll; **external links** (the word-check + dictionary lookup, the rules link, operator-reply links) open through `Telegram.WebApp.openLink` + so Telegram shows them in its in-app browser instead of the WebView's "open this link?" + confirmation a plain `target=_blank` triggers; and a live stream dropped by a background suspend reconnects silently on return — the connection banner is suppressed while hidden and for a short grace after resume (visibilitychange + pageshow/pagehide + Telegram `activated`/`deactivated`). diff --git a/ui/e2e/game.spec.ts b/ui/e2e/game.spec.ts index 35c62a1..db5c8a1 100644 --- a/ui/e2e/game.spec.ts +++ b/ui/e2e/game.spec.ts @@ -221,6 +221,13 @@ test('check-word sanitises input and shows a verdict', async ({ page }) => { await page.locator('.check button').click(); // Check (enabled: length 3) await expect(page.locator('.ok, .bad')).toBeVisible(); + + // A found word offers an external dictionary lookup link that opens in a new tab; the + // verdict still carries the complaint button. + const lookup = page.getByRole('link', { name: 'Look it up' }); + await expect(lookup).toBeVisible(); + await expect(lookup).toHaveAttribute('target', '_blank'); + await expect(lookup).toHaveAttribute('href', /qza$/); }); test('dropping the game ends it and shows the result', async ({ page }) => { diff --git a/ui/src/components/AdBanner.svelte b/ui/src/components/AdBanner.svelte index e5d2e04..b9e37fc 100644 --- a/ui/src/components/AdBanner.svelte +++ b/ui/src/components/AdBanner.svelte @@ -9,6 +9,7 @@ type BannerConfig, type BannerItem, } from '../lib/banner'; + import { onExternalLinkClick } from '../lib/telegram'; let { items = mockBanners(), config = defaultBannerConfig }: { items?: BannerItem[]; config?: BannerConfig } = $props(); @@ -39,7 +40,11 @@ onDestroy(() => rotator?.stop()); -
- {c.rulesPrefix}{c.rulesLink}. + {c.rulesPrefix}{c.rulesLink}.
{#each replySegments as seg, i (i)}{#if seg.href}{seg.text}{:else}{seg.text}{/if}{/each}
+{#each replySegments as seg, i (i)}{#if seg.href}{seg.text}{:else}{seg.text}{/if}{/each}