UI: fix last-move highlight, localize move history, clamp zoom overscroll
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 41s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s

- Highlight tracks the last move overall (not the last word): a trailing
  pass/exchange now highlights nothing, so the board no longer lights up the
  opponent's old word after our own empty move.
- Make the highlight event-driven: refreshed only on a real game event
  (open/refresh, opponent move, our own committed move) and dismissed the moment
  composing starts, so recalling a just-placed tile never re-triggers it.
- Localize non-play move-history labels via new move.* catalog keys
  (pass/exchange/resign/timeout); the label printed the raw English action.
- Clamp the zoomed board's pan at its edge (overscroll-behavior: none), removing
  the native rubber-band past the content.

Tests: lastMoveCells unit coverage (trailing pass/exchange -> empty), i18n RU
label assertions, an e2e overscroll-contract check on the zoomed viewport.
This commit is contained in:
Ilia Denisov
2026-06-11 18:50:10 +02:00
parent 5c8b8bf658
commit ac29dca865
8 changed files with 130 additions and 43 deletions
+30 -16
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { lastPlayTiles, replay } from './board';
import { lastMoveCells, replay } from './board';
import type { MoveRecord } from './model';
function play(tiles: { row: number; col: number; letter: string; blank: boolean }[]): MoveRecord {
@@ -17,18 +17,11 @@ function play(tiles: { row: number; col: number; letter: string; blank: boolean
};
}
const pass: MoveRecord = {
player: 1,
action: 'pass',
dir: '',
mainRow: 0,
mainCol: 0,
tiles: [],
words: [],
count: 0,
score: 0,
total: 0,
};
function simple(action: 'pass' | 'exchange'): MoveRecord {
return { player: 1, action, dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
}
const pass = simple('pass');
describe('board replay', () => {
it('places play tiles and ignores non-play moves', () => {
@@ -48,10 +41,31 @@ describe('board replay', () => {
expect(b.length).toBe(15);
expect(b[0].length).toBe(15);
});
});
it('lastPlayTiles returns the most recent play, skipping passes', () => {
describe('lastMoveCells', () => {
it('returns the last move cells when the last move is a play', () => {
const moves = [
pass,
play([
{ row: 7, col: 7, letter: 'A', blank: false },
{ row: 7, col: 8, letter: 'B', blank: false },
]),
];
expect(lastMoveCells(moves)).toEqual(new Set(['7,7', '7,8']));
});
it('highlights nothing when the last move is a pass after a play', () => {
const moves = [play([{ row: 7, col: 7, letter: 'A', blank: false }]), pass];
expect(lastPlayTiles(moves)).toHaveLength(1);
expect(lastPlayTiles([pass])).toHaveLength(0);
expect(lastMoveCells(moves).size).toBe(0);
});
it('highlights nothing when the last move is an exchange', () => {
const moves = [play([{ row: 7, col: 7, letter: 'A', blank: false }]), simple('exchange')];
expect(lastMoveCells(moves).size).toBe(0);
});
it('highlights nothing for an empty journal', () => {
expect(lastMoveCells([]).size).toBe(0);
});
});