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
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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -576,6 +576,7 @@
|
||||
{zoomed}
|
||||
{variant}
|
||||
labelMode={app.boardLabels}
|
||||
lines={app.boardLines}
|
||||
locale={app.locale}
|
||||
{focus}
|
||||
oncell={onCell}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -140,6 +140,7 @@ export const ru: Record<MessageKey, string> = {
|
||||
'settings.labelsBeginner': 'Новичок',
|
||||
'settings.labelsClassic': 'Классика',
|
||||
'settings.labelsNone': 'Без текста',
|
||||
'settings.boardLines': 'Линии сетки',
|
||||
'settings.reduceMotion': 'Меньше анимаций',
|
||||
|
||||
'about.title': 'О программе',
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user