Clicking a square with the palette's currently-selected piece now
deletes that piece instead of re-placing it. Makes tap-to-delete a
natural single-gesture operation without switching to the Erase
brush. A different brush-piece still replaces (unchanged behaviour).
Previously the FEN textarea held an independent draft that only
synced to the board when the user clicked Reset. Placing a piece
left the FEN showing stale content, which was confusing — the
field looked like a proxy for the board but wasn't.
Now the textarea mirrors the live board FEN by default:
- Placing, erasing, or clearing pieces updates the textarea
immediately.
- Starting to type into the textarea marks it 'dirty' (via a ref
so we don't rerender), pausing the auto-sync until the user
Loads (accept the draft) or Discards (restore the live FEN).
- The 'Unsaved edit' indicator appears while dirty.
- Load / Discard buttons disable when draft equals live FEN —
nothing to apply.
- Library load also resets the dirty flag, as that's a clean
state reset.
- Removed the redundant 'Current: <fen>' helper line below the
textarea; the textarea itself is the live FEN now.
E2E: 2 new tests
- FEN field updates live as pieces are placed.
- Typing into FEN field pauses live sync until Load or Discard.
25/25 Playwright tests passing. 1025 unit tests green.
The Empty layout had no real user need:
- Can't Play Solo (board with no pieces, nothing to click).
- Server rejects it for Create Room (validator requires >=1 king
per side).
- The 'blank canvas' use case is already handled by the
LayoutEditor's Custom... flow, which opens with an empty piece
list and lets the user compose.
It was UI clutter in the picker dropdown — users would see
'Empty Board' alongside real layouts and have no way to actually
use it.
EMPTY_LAYOUT stays exported for unit tests that need a zero-piece
starting state, but it no longer registers in LAYOUT_REGISTRY, is
not imported by the layouts barrel side-effects, and no longer
appears in the picker. Source flipped to 'custom' to signal it's
not a user-selectable premade.
Tests updated:
- premades.test.ts asserts 'empty' is NOT in the registry.
- e2e/layouts.spec.ts removes the Empty-solo test and asserts the
'empty' option is absent from the picker dropdown.
1025 unit tests + 23 e2e tests green.
Covers gaps in the original Phase E spec:
- Solo play reaches the engine: picking Pawns-Only and Classic and
asserting the rendered board matches the layout's piece placement
(not just that the UI dropdown says so).
- Empty layout solo: documents current behavior (solo lets users
poke at any layout, validator only gates multiplayer).
- Chess960 re-randomizes: takes 8 consecutive snapshots of the
back rank and asserts they're not all identical (p(collision)^7
is effectively zero).
- Valid ?fen query pre-selects Custom (inverse of the existing
malformed-FEN test).
- Library operations: full save → star → unstar → delete round
trip via the library drawer's aria-labeled buttons.
- Layout badge hidden for classic solo games.
- Multiplayer with Dunsany: full pipeline proof — picker →
Create Room UI → server resolves + validates → room.created
echo → joiner renders same Dunsany setup via room.joined echo.
Compares rank-1 and rank-8 snapshots between two browser
contexts to verify bit-identical rendering.
- Server-side rejection: sends a kingless custom layout via raw
WebSocket and asserts LAYOUT_INVALID error with a human-readable
king-related message.
New server-spawn block mirrors multiplayer.spec.ts so the layouts
tests can run in isolation or against an existing dev server.
24/24 Playwright tests passing (21 layouts + 2 multiplayer +
1 full-flow) in ~40s.
- LayoutEditor: move Esc handling to a window keydown listener so
the modal closes regardless of where focus currently sits (the
prior onKeyDown on the modal div only fired when the root div
itself held focus).
- LayoutPicker: add data-testid on the description <p> so e2e
assertions can target it unambiguously instead of relying on
free-text matches that also select the hidden <option> labels.
- e2e: use the new testid.
All 12 layouts e2e tests pass; existing multiplayer + full-flow
e2e suites still green.
Adds a full in-browser editor for authoring custom starting
layouts and a persistent library to save them across sessions.
persist/layout-library.ts:
- SavedLayout shape with id/name/pieces/starred/updatedAt.
- saveToLibrary / loadLibrary / deleteFromLibrary / setStarred /
duplicateEntry / makeId helpers.
- Capacity cap at 20 entries; oldest non-starred is evicted on
overflow; all-starred + full returns { ok: false, reason }
so the UI can surface an actionable message.
- Localstorage key is versioned (houserules:layouts:v1) for a
future migration path.
- 14 unit tests cover eviction, shape validation, star toggles,
duplicate flow, and the crypto.randomUUID fallback.
ui/LayoutEditor.tsx:
- Modal overlay with three panels — palette (white/black pieces +
Erase + Clear), interactive 8x8 board with click-to-place
brushes, and an actions panel.
- Live FEN textarea (toFen/fromFen round-trips) with a Load
button that replaces the board and a Reset-to-board sync.
- Live validation panel shows errors (block CTA) and warnings
(inform but don't block). The 'Use This Layout' CTA is disabled
until errors clear.
- Save to Library writes a SavedLayout; the integrated Library
drawer lists saved layouts sorted by starred-first + most-recent,
with Load / Star / Duplicate / Delete actions.
- Copy Share Link writes a \${origin}/?fen=...&name=... URL to the
clipboard — pastes straight into the lobby's existing query-param
pre-select flow from Phase D.
- Esc closes the modal.
ui/Lobby.tsx:
- Custom... entry in the layout picker opens the editor.
- onApply(layout) commits the custom layout as the lobby's
current selection so Create Room ships it to the server.
e2e/layouts.spec.ts:
- Picker renders every premade + Custom entry.
- Selecting Dunsany updates description; ?layoutId=dunsany and
malformed ?fen behave correctly.
- Editor opens on Custom, validates king count, erases pieces,
loads FEN, commits via 'Use This Layout', saves to library
with cross-reload persistence, closes on Esc.
1025 unit tests passing; bun run check clean. Playwright suite
requires dev server — run with \`bun run --filter @paratype/chess e2e\`
when needed.