Stage 17: UI defect fixes (russian variant, Telegram theme/nav/banner, reconnect, hint zoom, plaque, history, transitions, per-game cache)
- #6 align the UI variant id to the backend canonical 'russian_scrabble' (type, variants, Lobby, mock, tests) — fixes the New->Russian 400 - #11/#12 inside Telegram force the colour scheme from WebApp.colorScheme (over OS prefers-color-scheme, fixing the Telegram Desktop breakage) and hide the theme switcher - #14/#15 nav bar takes Telegram's bg; announcement banner gets a dedicated subtle --ad-bg accent token - #16 suppress the reconnect banner while backgrounded and silently reconnect the live stream on return to the foreground - #17 hint zoom scrolls to the placement's bounding box, not the top-left - #19/#20 players plaque: active seat raised with side shadows, others sunk; tap toggles history - #21/#23 history: scrollbar-gutter:stable (no word jitter); fixed-height drawer pins the bottom shadow to the board - #3 (UI) disable nudge on the player's own turn - #18a directional screen slide transitions (forward in from the right, back reveals the lobby) - #13 per-game in-memory cache: instant render on re-entry + background refresh - e2e: openGame waits for the slide transition to settle
This commit is contained in:
+52
-9
@@ -18,6 +18,7 @@
|
||||
import { alphabetLetters, hasAlphabet } from '../lib/alphabet';
|
||||
import { canCheckWord, sanitizeCheckWord } from '../lib/checkword';
|
||||
import { shareOrDownloadGcg } from '../lib/share';
|
||||
import { getCachedGame, setCachedGame } from '../lib/gamecache';
|
||||
import {
|
||||
BLANK,
|
||||
newPlacement,
|
||||
@@ -94,6 +95,7 @@
|
||||
]);
|
||||
view = st;
|
||||
moves = hist.moves;
|
||||
setCachedGame(id, st, hist.moves);
|
||||
placement = newPlacement(st.rack);
|
||||
preview = null;
|
||||
selected = null;
|
||||
@@ -109,7 +111,17 @@
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
onMount(load);
|
||||
onMount(() => {
|
||||
// Render instantly from the cache (a game opened before), then refresh in the
|
||||
// background. A cold open shows the loading state until load() resolves.
|
||||
const cached = getCachedGame(id);
|
||||
if (cached) {
|
||||
view = cached.view;
|
||||
moves = cached.moves;
|
||||
placement = newPlacement(cached.view.rack);
|
||||
}
|
||||
void load();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const e = app.lastEvent;
|
||||
@@ -269,6 +281,17 @@
|
||||
const h = await gateway.hint(id);
|
||||
if (h.move.tiles.length && view) {
|
||||
placement = placementFromHint(h.move.tiles, view.rack);
|
||||
// Scroll the (zoomed) board to the hint's placement rather than the top-left:
|
||||
// focus the centre of the laid tiles' bounding box.
|
||||
const p = placement.pending;
|
||||
if (p.length) {
|
||||
const rows = p.map((tt) => tt.row);
|
||||
const cols = p.map((tt) => tt.col);
|
||||
focus = {
|
||||
row: Math.round((Math.min(...rows) + Math.max(...rows)) / 2),
|
||||
col: Math.round((Math.min(...cols) + Math.max(...cols)) / 2),
|
||||
};
|
||||
}
|
||||
if (isCoarse()) zoomed = true;
|
||||
view = { ...view, hintsRemaining: h.hintsRemaining };
|
||||
recompute();
|
||||
@@ -428,7 +451,9 @@
|
||||
{/snippet}
|
||||
|
||||
{#if view}
|
||||
<div class="scoreboard">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="scoreboard" onclick={() => (historyOpen = !historyOpen)}>
|
||||
{#each view.game.seats as s (s.seat)}
|
||||
<div class="seat" class:turn={view.game.toMove === s.seat && !gameOver} class:win={s.isWinner}>
|
||||
<div class="nm">{s.accountId === app.session?.userId ? t('common.you') : s.displayName}</div>
|
||||
@@ -599,26 +624,39 @@
|
||||
|
||||
{#if panel === 'chat'}
|
||||
<Modal title={t('game.chat')} onclose={() => (panel = 'none')}>
|
||||
<Chat {messages} myId={app.session?.userId ?? ''} {busy} onsend={sendChat} onnudge={nudge} />
|
||||
<Chat {messages} myId={app.session?.userId ?? ''} {busy} canNudge={!isMyTurn} onsend={sendChat} onnudge={nudge} />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.scoreboard {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 6px var(--pad);
|
||||
gap: 6px;
|
||||
padding: 8px var(--pad);
|
||||
background: var(--bg-elev);
|
||||
cursor: pointer;
|
||||
}
|
||||
.seat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
padding: 5px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface-2);
|
||||
/* inactive seats read as "sunk in" */
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
.seat.turn {
|
||||
background: var(--surface-2);
|
||||
outline: 1px solid var(--accent);
|
||||
/* the active seat is "raised": lifted clear of the others with side shadows */
|
||||
background: var(--bg-elev);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.16),
|
||||
-3px 0 6px -2px rgba(0, 0, 0, 0.26),
|
||||
3px 0 6px -2px rgba(0, 0, 0, 0.26);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.seat.turn .nm {
|
||||
color: var(--accent);
|
||||
}
|
||||
.seat.win .sc {
|
||||
color: var(--ok);
|
||||
@@ -642,8 +680,13 @@
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
z-index: 2;
|
||||
max-height: 60%;
|
||||
/* A fixed-height drawer matching the board's slid offset, so the bottom border
|
||||
and its shadow pin to the board immediately instead of tracking the table as
|
||||
moves accumulate. scrollbar-gutter reserves the scrollbar so the centred word
|
||||
column does not jump left/right when the list overflows. */
|
||||
height: 62%;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 -6px 10px -8px rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
Reference in New Issue
Block a user