Stage 17 (#12): lines-off board variant (gapless checkerboard), Settings toggle
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 30s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s

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.
This commit is contained in:
Ilia Denisov
2026-06-06 14:51:48 +02:00
parent d0c1306d9b
commit 71b054227a
8 changed files with 66 additions and 1 deletions
+27 -1
View File
@@ -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 @@
<div class="viewport" class:zoomed bind:this={viewport}>
<div class="scaler" style="--z: {z};">
<div class="grid">
<div class="grid" class:gridless={!lines}>
{#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
+1
View File
@@ -576,6 +576,7 @@
{zoomed}
{variant}
labelMode={app.boardLabels}
lines={app.boardLines}
locale={app.locale}
{focus}
oncell={onCell}
+10
View File
@@ -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<void> {
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
+1
View File
@@ -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',
+1
View File
@@ -140,6 +140,7 @@ export const ru: Record<MessageKey, string> = {
'settings.labelsBeginner': 'Новичок',
'settings.labelsClassic': 'Классика',
'settings.labelsNone': 'Без текста',
'settings.boardLines': 'Линии сетки',
'settings.reduceMotion': 'Меньше анимаций',
'about.title': 'О программе',
+2
View File
@@ -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<Partial<Prefs>> {
+12
View File
@@ -3,6 +3,7 @@
import {
app,
setBoardLabels,
setBoardLines,
setLocalePref,
setReduceMotion,
setTheme,
@@ -63,6 +64,14 @@
</button>
{/each}
</div>
<label class="row gridlines">
<span>{t('settings.boardLines')}</span>
<input
type="checkbox"
checked={app.boardLines}
onchange={(e) => setBoardLines(e.currentTarget.checked)}
/>
</label>
</section>
<section>
@@ -118,4 +127,7 @@
align-items: center;
justify-content: space-between;
}
.gridlines {
margin-top: 12px;
}
</style>