From 71b054227a834a2cf28b32c38de9cc1605c1f9e2 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sat, 6 Jun 2026 14:51:48 +0200 Subject: [PATCH] Stage 17 (#12): lines-off board variant (gapless checkerboard), Settings toggle Add a 'Grid lines' preference (default off): when off the board drops the 1px grid gaps for a gapless checkerboard (plain cells alternate shades; tiles get rounded corners and a soft right-side shadow so adjacent gapless tiles still read apart), saving ~14px of width. When on, the classic lined grid returns. Persisted with the other board-style prefs; wired through Board's new lines prop. e2e locks the default and the toggle. --- ui/e2e/game.spec.ts | 12 ++++++++++++ ui/src/game/Board.svelte | 28 +++++++++++++++++++++++++++- ui/src/game/Game.svelte | 1 + ui/src/lib/app.svelte.ts | 10 ++++++++++ ui/src/lib/i18n/en.ts | 1 + ui/src/lib/i18n/ru.ts | 1 + ui/src/lib/session.ts | 2 ++ ui/src/screens/Settings.svelte | 12 ++++++++++++ 8 files changed, 66 insertions(+), 1 deletion(-) diff --git a/ui/e2e/game.spec.ts b/ui/e2e/game.spec.ts index 7095035..0c96883 100644 --- a/ui/e2e/game.spec.ts +++ b/ui/e2e/game.spec.ts @@ -48,6 +48,18 @@ test('a pending tile recalls on double-tap, not on a single tap', async ({ page await expect(page.locator('[data-cell].pending')).toHaveCount(0); }); +test('the board is a gapless checkerboard by default; grid lines toggle in Settings', async ({ page }) => { + await openGame(page); + await expect(page.locator('.grid.gridless')).toBeVisible(); // lines off by default + + await page.evaluate(() => (location.hash = '/settings')); + await page.locator('.gridlines input').check(); // turn grid lines on + await page.evaluate(() => (location.hash = '/game/g1')); + + await expect(page.locator('.grid')).toBeVisible(); + await expect(page.locator('.grid.gridless')).toHaveCount(0); +}); + test('shuffle reorders the rack but keeps the same tiles', async ({ page }) => { await openGame(page); const before = await page.locator('.rack .tile').allTextContents(); diff --git a/ui/src/game/Board.svelte b/ui/src/game/Board.svelte index 958accd..1a6df98 100644 --- a/ui/src/game/Board.svelte +++ b/ui/src/game/Board.svelte @@ -16,6 +16,7 @@ zoomed, variant, labelMode, + lines, locale, focus, oncell, @@ -32,6 +33,8 @@ zoomed: boolean; variant: Variant; labelMode: BoardLabelMode; + /** Draw 1px grid lines between cells; when false the board is a gapless checkerboard. */ + lines: boolean; locale: Locale; focus: { row: number; col: number } | null; oncell: (row: number, col: number) => void; @@ -94,7 +97,7 @@
-
+
{#each board as rowCells, r (r)} {#each rowCells as cell, c (c)} {@const p = pending.get(key(r, c))} @@ -107,6 +110,7 @@ class:pending={!!p && !cell} class:hl={!!cell && highlight.has(key(r, c)) && !flash} class:flash={!!cell && flash && highlight.has(key(r, c))} + class:dark={premium[r][c] === '' && !cell && !p && (r + c) % 2 === 1} data-cell data-row={r} data-col={c} @@ -187,6 +191,28 @@ .cell.pending { background: var(--tile-pending); } + /* Lines-off variant: a gapless checkerboard. The 1px grid gaps (and the cell-line they + reveal) collapse, saving ~14px of board width; plain cells alternate shades, and tiles + get rounded corners and a soft right-side shadow so adjacent gapless tiles still read + as separate pieces. */ + .grid.gridless { + gap: 0; + padding: 0; + background: var(--board-bg); + } + .grid.gridless .cell { + border-radius: 0; + } + .grid.gridless .cell.dark { + background: color-mix(in srgb, var(--cell-bg) 88%, #000); + } + .grid.gridless .cell.filled, + .grid.gridless .cell.pending { + border-radius: 4px; + box-shadow: + inset 0 -2px 0 var(--tile-edge), + 2px 0 3px -1px rgba(0, 0, 0, 0.4); + } .cell.hl { background: var(--tile-recent); /* The bottom edge goes darker than the highlighted fill (not lighter, as the plain diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index 6c2b1a7..6b438cc 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -576,6 +576,7 @@ {zoomed} {variant} labelMode={app.boardLabels} + lines={app.boardLines} locale={app.locale} {focus} oncell={onCell} diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 0af18fa..6957f46 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -40,6 +40,8 @@ export const app = $state<{ locale: Locale; reduceMotion: boolean; boardLabels: BoardLabelMode; + /** Draw grid lines between board cells; off (default) is a gapless checkerboard. */ + boardLines: boolean; localeLocked: boolean; /** Pending incoming friend requests + invitations, for the lobby badge. */ notifications: number; @@ -53,6 +55,7 @@ export const app = $state<{ locale: 'en', reduceMotion: false, boardLabels: 'beginner', + boardLines: false, localeLocked: false, notifications: 0, }); @@ -229,6 +232,7 @@ export async function bootstrap(): Promise { app.theme = prefs.theme ?? 'auto'; app.reduceMotion = prefs.reduceMotion ?? false; app.boardLabels = prefs.boardLabels ?? 'beginner'; + app.boardLines = prefs.boardLines ?? false; applyTheme(app.theme); applyReduceMotion(app.reduceMotion); if (prefs.locale) { @@ -354,6 +358,7 @@ function persistPrefs(): void { locale: app.locale, reduceMotion: app.reduceMotion, boardLabels: app.boardLabels, + boardLines: app.boardLines, }); } @@ -406,6 +411,11 @@ export function setBoardLabels(mode: BoardLabelMode): void { persistPrefs(); } +export function setBoardLines(on: boolean): void { + app.boardLines = on; + persistPrefs(); +} + // Background/foreground lifecycle: silence the reconnect banner during a suspend and // reconnect quietly on return (and refresh the lobby badge for any push missed while // hidden, §10). Several signals cover the platforms: the page Visibility API, the diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 2940e1e..dfa075f 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -139,6 +139,7 @@ export const en = { 'settings.labelsBeginner': 'Beginner', 'settings.labelsClassic': 'Classic', 'settings.labelsNone': 'None', + 'settings.boardLines': 'Grid lines', 'settings.reduceMotion': 'Reduce motion', 'about.title': 'About', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index 9fa486f..ba15548 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -140,6 +140,7 @@ export const ru: Record = { 'settings.labelsBeginner': 'Новичок', 'settings.labelsClassic': 'Классика', 'settings.labelsNone': 'Без текста', + 'settings.boardLines': 'Линии сетки', 'settings.reduceMotion': 'Меньше анимаций', 'about.title': 'О программе', diff --git a/ui/src/lib/session.ts b/ui/src/lib/session.ts index f4f703d..5c5a8dd 100644 --- a/ui/src/lib/session.ts +++ b/ui/src/lib/session.ts @@ -124,6 +124,8 @@ export interface Prefs { locale: Locale; reduceMotion: boolean; boardLabels: BoardLabelMode; + /** Draw the 1px grid lines between cells; off (default) shows a gapless checkerboard. */ + boardLines: boolean; } export async function loadPrefs(): Promise> { diff --git a/ui/src/screens/Settings.svelte b/ui/src/screens/Settings.svelte index b2cfc53..978c52c 100644 --- a/ui/src/screens/Settings.svelte +++ b/ui/src/screens/Settings.svelte @@ -3,6 +3,7 @@ import { app, setBoardLabels, + setBoardLines, setLocalePref, setReduceMotion, setTheme, @@ -63,6 +64,14 @@ {/each}
+
@@ -118,4 +127,7 @@ align-items: center; justify-content: space-between; } + .gridlines { + margin-top: 12px; + }