Compare commits

..

27 Commits

Author SHA1 Message Date
funman300 c50eaf81f7 feat(replay): add HC bump for WIN MOVE scrub-bar marker; extend HighContrastBackground
HighContrastBackground gains an optional hc_color field so sites can
specify a domain-specific HC variant rather than always bumping to
BORDER_SUBTLE_HC (gray). with_default() fills hc_color = BORDER_SUBTLE_HC
preserving all existing behaviour; new with_hc(default, hc) lets callers
specify both ends. update_high_contrast_backgrounds reads marker.hc_color
instead of the hardcoded constant.

STATE_SUCCESS_HC (#c8e862, L≈0.73) added to ui_theme — a brighter lime
that maintains the success hue while standing out from bumped notch
ticks (BORDER_SUBTLE_HC gray, L≈0.60) under HC mode.

WIN MOVE marker now carries HighContrastBackground::with_hc(STATE_SUCCESS,
STATE_SUCCESS_HC): lime stays lime under HC instead of turning gray.
Unit test pins both the default and hc color fields on the spawned marker.

1276 tests pass / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:19:00 -07:00
funman300 b44d2777ec fix(replay): centre scrub-bar notch labels on their notch ticks
The three middle scrub-bar labels (25%, 50%, 75%) previously had their
left edge anchored at the notch percentage, making them read as
"starting after" the notch. Apply the CSS translateX(-50%) pattern for
Bevy 0.18 UI: give each middle label a fixed-width container
(SCRUB_LABEL_CENTER_WIDTH = 36px), offset the container's left edge by
-width/2 via margin.left, and add Justify::Center so the text renders
centred within the container. The container's centre then coincides with
the notch line at the chosen percentage.

Endpoints (0%, 100%) keep their flush-left / flush-right anchoring
unchanged. 1275 tests pass / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:14:14 -07:00
funman300 52407e7256 docs(handoff): cut v0.21.7 — B-2 replay arc closed; dim layer ships
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:03:32 -07:00
funman300 da3e5423dc feat(replay): add full-screen tableau dim layer for mini-tableau preview
Spawn a `ReplayTableauDimLayer` UI node (100% × 100%, 50% opacity black)
at z=54 (Z_REPLAY_OVERLAY − 1) whenever a replay starts. The dim layer
darkens the entire card world so the replay chrome (banner at z=55,
move-log panel at z=55) reads clearly against the scene without
obscuring card positions — matching the mockup's "Game Peek Band at
50% opacity" spec. Bevy's UI/world compositor means no changes to
card_plugin are needed: UI nodes always render above world-space sprites
regardless of Transform.z values.

The dim layer carries no Interaction component (purely visual; pointer
events pass through). Despawned alongside the banner and move-log panel
in `react_to_state_change` when the replay ends.

Adds Z_REPLAY_DIM (= 54) and TABLEAU_DIM_ALPHA (= 0.5) constants plus
two new tests: lifecycle (spawn/despawn mirrors floating chip pattern)
and z-ordering invariant (Z_REPLAY_DIM < Z_REPLAY_OVERLAY pinned).

1275 tests pass / 0 failing. Closes the last major B-2 sub-piece.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:01:22 -07:00
funman300 a1864271de docs(handoff): refresh post-v0.21.6 — anchor to new tag, reset menu state
Fold the six post-v0.21.5 commit narratives into CHANGELOG §
[0.21.6] (now the source of truth for that release's scope).
Reset the Since-cut log to "no threads in flight." Update
status (HEAD f63db76, tags through v0.21.6, tests 1273
passing). Resume prompt now anchors at v0.21.6.

The post-cut menu's main item is now the mini-tableau preview
— the only major B-2 sub-piece left after Move Log panel
shipped. Architectural change (touches card_plugin rendering),
best tackled in a fresh session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:48:51 -07:00
funman300 f63db769ae docs: cut v0.21.6 — Move Log panel + scrub-UX polish
Patch release rolling up six post-v0.21.5 commits under the
through-line "Move Log panel + scrub-UX polish":

- d3cb1a5: HC-mode coverage for scrub track + notches
- 2e25476: continuous scrub on key-held ← / → at 100ms cadence
- d6f32d3: Move Log panel + active row (header + format helpers)
- 140251b: 2 prev rows above active
- e7345ae: active-row highlight with ACCENT_PRIMARY background
- 4437a1a: 2 next rows below active

The Move Log panel is the first replay-overlay surface that
isn't attached to the banner — it lives at a separate screen
anchor (bottom: 0) with its own spawn/despawn lifecycle.
Establishes the multi-anchor replay UI pattern that the
remaining B-2 sub-piece (mini-tableau preview) will inherit.

Panel grows 56 → 84 → 112 px across the four move-log commits.
HighContrastBackground primitive lifted to ui_theme parallel
to HighContrastBorder; settings_plugin gains
update_high_contrast_backgrounds for the BackgroundColor
repaint cycle. Continuous scrub uses a per-key accumulator
resource (ReplayScrubKeyHold) gated on SCRUB_REPEAT_INTERVAL_SECS
(0.1s).

Tests: 1250 → 1273 (+23 net new). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:46:24 -07:00
funman300 4437a1aaf9 feat(replay): add 2 next rows below active row in Move Log panel
Symmetric to the prev-rows commit. Adds 2 about-to-apply move
rows below the active row so the panel now shows a full 5-row
window: prev offset 2 → prev offset 1 → active → next offset 1
→ next offset 2. Panel grows from 84 → 112 px to fit the
additional rows.

Format helper `format_kth_next_row(state, k)` returns the kth
about-to-apply move's text:
- k=1 → moves[cursor], displayed as "{cursor + 1} │ {body}"
- k=2 → moves[cursor + 1], displayed as "{cursor + 2} │ ..."
- Returns empty when cursor + k - 1 >= moves.len() (under-fill
  late in the replay) or k=0 (degenerate).

Symmetric implementation:
- New `ReplayOverlayMoveLogNextRow { offset: u8 }` component
- Spawn loop iterates 1..=MOVE_LOG_NEXT_ROWS in order so offset
  1 sits directly below active, offset 2 below that
- Per-frame `update_move_log_next_rows` system mirrors the
  prev-rows updater
- TEXT_SECONDARY (matching prev rows) keeps the active row's
  highlight as the focal point

For post-game replays the next rows aren't spoilers (the game
is already won). If a future use case reuses the panel during
live play, the preview-shape would need rethinking.

4 new tests:
- format_kth_next_row: k=1, 2 in-range cases + k beyond
  moves.len() out-of-range + k=0 degenerate.
- move_log_next_rows_spawn_with_panel: cardinality matches
  MOVE_LOG_NEXT_ROWS.
- move_log_next_rows_paint_helper_strings_at_spawn: text
  matches helper output per offset.
- move_log_next_rows_underfill_at_replay_end: offset 1
  populates at cursor=9/10, offset 2 stays empty.

Tests: 1269 → 1273. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:44:59 -07:00
funman300 e7345aed6c feat(replay): highlight active row in Move Log panel
Wraps the active-row Text in a Node with
BackgroundColor(ACCENT_PRIMARY) so the row reads as "current
focus" against the panel's elevated background. Inner Text
colour bumps from TEXT_PRIMARY (#d0d0d0) to TEXT_PRIMARY_HC
(#f5f5f5) for legible contrast against the brick-red highlight.

format_active_move_row now prefixes the row with `▶` (the focus
marker) so the visual hierarchy is reinforced even before the
background paints (HC mode, future palette tweaks). The empty
case still returns empty — cursor=0 doesn't paint a stray "▶ "
prefix on an otherwise-empty row.

Mirrors the mockup at docs/ui-mockups/replay-overlay-mobile.html
§ "Move Log Card" where the active row has bg-suit-red-cb
(brick-red equivalent) + dark text + the ▶ marker.

3 new tests:
- active_row_wrapper_carries_accent_primary_background: walks
  from the active-row Text to its parent Node and asserts the
  wrapper carries BackgroundColor(ACCENT_PRIMARY).
- active_row_text_uses_high_contrast_color_for_highlight: pins
  the TextColor as TEXT_PRIMARY_HC.
- active_row_format_includes_focus_prefix: pure-helper guard for
  the ▶ prefix + the cursor=0-stays-empty contract.

Plus 2 existing tests updated for the new prefixed format
(format_active_move_row_handles_cursor_zero_and_positive,
move_log_active_row_repaints_on_cursor_advance).

Tests: 1266 → 1269 (+3 net new, +2 updated). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:41:14 -07:00
funman300 140251beae feat(replay): add 2 prev rows above active row in Move Log panel
Extends the Move Log panel's single active-row to a 3-row recent-
history window: 2 prev rows showing the moves applied just before
the active one, then the active row. Display order top-to-bottom:
header → prev offset 2 (oldest) → prev offset 1 → active.

Panel grows from 56 → 84 px to fit the additional rows. Active
row keeps TEXT_PRIMARY; prev rows render in TEXT_SECONDARY so
the active row stands out from context rows even without an
explicit highlight. (Active-row highlight is a follow-up commit.)

The format helper generalises:
- New `format_kth_recent_row(state, k)` returns the text for the
  kth-most-recently-applied move (k=1 is active, k=2 is row above,
  etc.). Returns empty when k > cursor (early-replay under-fill)
  or k = 0 (degenerate).
- `format_active_move_row` becomes a thin wrapper for k=1, kept
  at module scope so call sites stay readable.

New `ReplayOverlayMoveLogPrevRow { offset: u8 }` component carries
the row's offset (1 = just-before-active, 2 = before that). Spawn
loop iterates `MOVE_LOG_PREV_ROWS..=1` in reverse so the highest-
offset (oldest) row sits topmost in the panel's flex column.

Per-frame `update_move_log_prev_rows` system reads each row's
offset, computes k = offset + 1, and repaints via
format_kth_recent_row. Empty-when-out-of-range means panels gracefully
under-fill at cursor=1 (only active populated) and cursor=2
(active + offset 1, offset 2 empty).

4 new tests:
- format_kth_recent_row: k=1, 2, 3 in-range cases + k>cursor
  out-of-range + k=0 degenerate.
- move_log_prev_rows_spawn_with_panel: cardinality matches the
  MOVE_LOG_PREV_ROWS const.
- move_log_prev_rows_paint_helper_strings_at_spawn: text matches
  helper output per offset.
- move_log_prev_rows_repaint_on_cursor_advance: drives cursor=2
  → cursor=5 and asserts offset 1 / offset 2 texts follow.

Tests: 1262 → 1266. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:35:07 -07:00
funman300 d6f32d3154 feat(replay): add Move Log panel with active-row readout
First slice of the move-log mockup at
docs/ui-mockups/replay-overlay-mobile.html § "Move Log Card".
Adds a separate root UI entity anchored to the viewport's bottom
edge (sibling-of-banner pattern, mirrors ReplayFloatingProgressChip
lifecycle) carrying a `▌ MOVE LOG · N/M` header plus a single row
showing the most-recently-applied move.

Subsequent commits in this multi-session arc add prev/next rows,
active-row highlight, and auto-scroll on cursor advance. Splitting
the work at "panel + active row only" lands the structural piece
(panel exists, lifecycle works, format helpers proven) before
tackling the harder questions about rendering un-applied future
moves and scrolling.

Position decision: bottom-of-viewport (matches mockup), separate
root entity from the 92 px top banner. Keeps the banner from
growing further into a top-heavy 170+ px strip; the
top-status + bottom-info paradigm reads as vim/IDE-style buffer
chrome that players intuitively scan.

Four pure helpers handle the formatting:
- format_pile(p) → lowercase, 1-indexed display string
  ("foundation 3" rather than enum's 0-indexed Foundation(2))
- format_move_body(m) → "{from} → {to}" or "stock cycle"
- format_move_log_header(state) → "▌ MOVE LOG · N/M",
  "▌ MOVE LOG · COMPLETE" for `Completed`, empty for `Inactive`
- format_active_move_row(state) → "{cursor} │ {body}" with
  1-based cursor for player display, empty at cursor=0

Two per-frame update systems (update_move_log_header,
update_move_log_active_row) repaint the texts on resource change
with the standard early-exit-on-no-change idiom.

Despawn handling: react_to_state_change gains a third query for
ReplayOverlayMoveLogPanel entities and despawns them on
Playing → Inactive alongside the banner root and floating chip.

Panel border carries HighContrastBorder so the 1 px top edge
bumps under HC mode — same pattern as the keybind footer.

8 new tests:
- format_pile pile-name + 1-index pinning
- format_move_body both-variant pinning
- format_move_log_header three-state coverage
- format_active_move_row cursor=0 vs cursor>0
- move_log_panel spawn cardinality (exactly one)
- move_log_panel header paints helper string at spawn
- move_log_active_row repaints on cursor advance
- move_log_panel despawn parity with overlay tree

Tests: 1254 → 1262. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:29:37 -07:00
funman300 8fdc41f36f docs(handoff): record post-v0.21.5 polish; recommend notch-label centering
Two carve-outs land on top of v0.21.5:
- d3cb1a5: HC-mode coverage for scrub track + notches via new
  HighContrastBackground primitive in ui_theme + paint system
  in settings_plugin.
- 2e25476: continuous scrub on key-held ← / → at 100ms cadence;
  matches mockup's "[← →] scrub" terminology while keeping
  single-press = single-step semantics.

Update Since-cut log, status (1250 → 1254 tests passing,
flake cleared), and next-step menu. B-2 keyboard accelerator
coverage + accessibility + scrub UX are all complete; remaining
options are notch-label centering polish (smallest), the
move-log/mini-tableau multi-session arcs that close B-2, or
WIN MOVE marker HC bump (optional).

Recommended next-step: notch label centering (small).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:20:51 -07:00
funman300 2e25476d0a feat(replay): continuous scrub on key-held arrow keys
Holding ← or → now triggers continuous step at 100 ms cadence
(10 steps/sec) — matches the mockup's `[← →] scrub`
terminology while keeping single-press = single-step semantics.

Implementation: per-key accumulators in a new
`ReplayScrubKeyHold` resource. Each frame the key is held, the
corresponding accumulator absorbs `time.delta_secs()`; when it
exceeds `SCRUB_REPEAT_INTERVAL_SECS` (0.1s) the handler fires
another step and resets the accumulator. `just_pressed` events
bypass the accumulator entirely and fire immediately —
release resets to 0 so the next fresh press also fires
immediately rather than at half-interval.

Symmetric handling for ← (backwards step via undo) and →
(forward step). Both keys remain paused-only via the same
destructure-gate pattern in the underlying step helpers.

Footer text unchanged (`[← →] step`) — the only-wired-keybinds
discipline says "list what works"; held-key continuous scrub
is a discoverable enhancement to the same keybind, not a new
keybind.

`handle_arrow_keyboard` gains `Res<Time>` and
`ResMut<ReplayScrubKeyHold>` parameters. `Time` is provided by
MinimalPlugins's TimePlugin so headless tests already have it.

2 new tests (in addition to the 4 existing arrow scenarios):
- arrow_right_keyboard_repeats_while_held: drives time at
  exactly SCRUB_REPEAT_INTERVAL_SECS per tick and asserts that
  a second step fires after the just_pressed one.
- arrow_keyboard_release_resets_accumulator: verifies the
  release branch zeros the per-key accumulator.

Tests: 1252 → 1254. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:19:46 -07:00
funman300 d3cb1a51d4 feat(replay): HC-mode coverage for scrub track + notches
The 1 px scrub track and 5 quarter-mark notch ticks paint their
shape via BackgroundColor (not BorderColor — they're tiny
full-bleed Nodes, not borders on wider containers), so the
existing HighContrastBorder marker doesn't apply to them.

Add a parallel primitive in ui_theme: HighContrastBackground
marker carrying default_color, mirroring HighContrastBorder's
shape exactly. Add update_high_contrast_backgrounds system in
settings_plugin alongside update_high_contrast_borders — same
on/off rule (off → marker.default_color, on → BORDER_SUBTLE_HC),
same change-suppression idiom (only mutate when different so
Bevy's change-detection doesn't trigger per-frame repaints).

Tag the scrub track Node and all five notch Nodes with
HighContrastBackground::with_default(BORDER_SUBTLE) so the
existing settings repaint cycle picks them up under HC mode.

The scrub fill (ACCENT_PRIMARY brick-red) and WIN MOVE marker
(STATE_SUCCESS lime-green) don't get the marker — accent and
state colours are already saturated and don't need an HC
luminance variant.

2 new tests: spawn-time marker presence on the track and
cardinality-matches-notch-count on the ticks.

Tests: 1250 → 1252. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:14:03 -07:00
funman300 c8358f4275 docs(handoff): refresh post-v0.21.5 — anchor to new tag, reset menu state
Fold the six post-v0.21.4 commit narratives into CHANGELOG §
[0.21.5] (now the source of truth for that release's scope).
Reset the Since-cut log to "no threads in flight." Update
status (HEAD `a2432df`, tags through v0.21.5, tests still
1250/1249 passing pending the time-dependent flake clearing).
Resume prompt now anchors at v0.21.5 with the smaller post-cut
menu of next-finite-steps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:08:56 -07:00
funman300 a2432dfe7a docs: cut v0.21.5 — replay-overlay scrubbing affordances + accessibility
Patch release rolling up six post-v0.21.4 commits under the
through-line "replay-overlay scrubbing affordances + accessibility":

- fe68861: quarter-mark scrub-bar notches
- d322abf: percentage labels under notches (banner 60 → 76 px)
- 1873b3f: keybind-hint footer (banner 76 → 92 px)
- 90e24d9: ESC accelerator + cross-plugin pause-modal gate
- 23902cd: HC-mode coverage for footer top border
- e5c4f51: ← / → keyboard accelerators for paused stepping

v0.21.4 shipped pause / resume / step + the WIN MOVE marker as
the first scrubbing-shaped additions; v0.21.5 fills out the rest
of the scrubbing UX so the player has both visual anchor points
(notches + labels) and a complete keyboard control surface
(Space / Esc / ← / →) for navigating a paused replay.

Two of the six commits are layout-changing — they grow the
banner from 60 → 76 → 92 px to make room for the notch labels
and keybind footer. Banner geometry was fixed for every prior
B-2 commit; this release establishes the "grow the container,
add a flex-column child" pattern that the remaining B-2
sub-pieces (move-log scroller, mini-tableau preview) will
inherit when they land.

Tests: 1228 → 1250 (+22 net new), 1249 passing, 1 pre-existing
time-dependent flake (daily_challenge warning, fails when UTC
clock is within 30 min of midnight; verified not introduced by
this release).

Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:05:03 -07:00
funman300 511550232c docs(handoff): record HC marker + ← / → wiring; recommend v0.21.5 cut
Two more post-v0.21.4 carve-outs land:
- 23902cd: HC-mode coverage for keybind-footer top border
  (HighContrastBorder marker so apply_high_contrast_borders
  bumps the 1 px top border under HC).
- e5c4f51: ← / → keyboard accelerators for paused stepping
  (hooks game's undo system for backwards step; footer
  extended to [SPACE] pause/resume · [ESC] stop · [← →] step).

Update Since-cut log, visual-identity bullet, B option in the
Resume menu, status (1244 → 1250 total tests / 1249 passing /
1 pre-existing flake), and HEAD hint.

Six post-v0.21.4 commits now form a coherent through-line:
replay-overlay scrubbing affordances + accessibility. Resume
menu's B option now recommends cutting v0.21.5 as the natural
next boundary.

Pre-existing flake noted: daily_challenge warning test fails
when wall-clock UTC is within 30 minutes of midnight (the
warning window the test asserts against). Verified not
introduced by recent commits via stash-and-retest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:59:08 -07:00
funman300 e5c4f51a6e feat(replay): wire ← / → keyboard accelerators for paused stepping
→ during a paused replay advances by one move (mirrors the Stop
button's existing forward-step semantics). ← decrements the
cursor and dispatches `UndoRequestEvent`, which the game's
`handle_undo` reads next frame to reverse its most-recent move
— hooking the existing undo system rather than replaying
forward from cursor 0 (every replay-applied move pushes to the
undo stack the same way a player move would, so undo is the
right reversal primitive).

Both accelerators are paused-only — backwards via a new
`step_backwards_replay_playback` in `replay_playback.rs` that
hard-gates with the same destructure pattern as
`step_replay_playback`. Pressing → during running playback or ←
at cursor 0 are silent no-ops; the player learns "pause first,
then arrow."

The mockup labels these `[← →] scrub` (continuous fast scan).
Single-move step is the closest behaviour shippable today —
continuous scrub would need either a key-held event source or
an internal speed-up loop. Footer hint reads
`[← →] step` to match what's wired rather than the aspirational
"scrub."

Footer hint extended in lockstep:
`[SPACE] pause/resume · [ESC] stop · [← →] step` — the
only-wired-keybinds discipline holds.

ReplayOverlayPlugin gains `add_message::<UndoRequestEvent>()`
defensively so the plugin can run under MinimalPlugins without
GamePlugin attached (idempotent registration; harmless when
GamePlugin is also present).

6 new tests (2 hint pins + 4 keyboard scenarios) + 1 helper-pin
update for the new hint string.

Pre-existing flake noted: `daily_challenge_plugin::tests::
check_system_fires_warning_event_only_once_per_day` is failing
because wall-clock UTC is currently within 30 minutes of
midnight, inside the daily-expiry warning window the test
asserts against. Verified pre-existing by stashing all changes
and re-running — failure persists. Same shape as the
`winnable_seed_search` flake the handoff documented earlier
this session: time-dependent, deterministically passes under
different clock conditions. Not introduced by this commit.

Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:50:59 -07:00
funman300 23902cdc44 feat(replay): HC-mode coverage for keybind-footer top border
Tag the footer's border-carrying Node with
`HighContrastBorder::with_default(BORDER_SUBTLE)` so the existing
`apply_high_contrast_borders` system bumps the 1 px top border
from `BORDER_SUBTLE` (#505050) to `BORDER_SUBTLE_HC` (#a0a0a0)
when `Settings::high_contrast_mode` is on.

Without this the footer reads as floating loose under HC because
the border that visually anchors it to the labels row above is
near-invisible at #505050 against the elevated banner background.

The footer's text colours (`TEXT_SECONDARY` on both the
mode-line and the hint) don't need an HC bump — `TEXT_SECONDARY`
is already at `#a0a0a0`, the same luminance as `BORDER_SUBTLE_HC`.
There's no `TEXT_SECONDARY_HC` constant in the palette because
secondary text is already at HC-border level by design.

The notch labels also use `TEXT_SECONDARY` and inherit the same
"already HC-bright" property — no marker needed there either.

The 1 px scrub track, notch ticks, and WIN MOVE marker render
via `BackgroundColor` (not `BorderColor`) so the
`HighContrastBorder` marker doesn't apply. HC coverage for those
decorative pieces would need a custom settings-aware paint
system (precedent: `radial_rim_outline` in `radial_menu`) and is
deferred to a follow-up commit.

1 new test pinning the marker on spawn. 1243 → 1244. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:41:49 -07:00
funman300 3cc8eacafa docs(handoff): record ESC accelerator; B's next step is HC polish
Post-v0.21.4 fourth carve-out: 90e24d9 wires ESC for replay-stop
with a cross-plugin gate in pause_plugin to defer when replay is
playing. Footer extended in lockstep to
[SPACE] pause/resume · [ESC] stop. Update Since-cut log,
visual-identity bullet, B option in the Resume menu, status
(1240 → 1243 tests), and HEAD hint.

B option's next-step menu now has three branches: HC polish
(smallest), ← / → wiring (medium, needs backwards-step path),
and the multi-session move-log/preview arcs that close B-2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:07:33 -07:00
funman300 90e24d9711 feat(replay): wire ESC accelerator for stop, gate pause modal
ESC during an active replay now stops it (mirrors the existing
Stop button click). UI-first contract from CLAUDE.md §3.3 holds
for the keyboard accelerator: every keybind the footer surfaces
points at a wired action.

Cross-plugin coordination: pause_plugin's `toggle_pause` already
listens for ESC and would otherwise open the pause modal on the
same press. Resolved by adding a fourth defer-if check to the
existing modal-stack pattern in `toggle_pause` —
`replay_state.is_some_and(|s| s.is_playing())` slots in right
after `other_modal_scrims` and before `selection`. Symmetric
shape to the existing forfeit / modal-scrim / selection /
game-over / drag gates.

Footer hint extended from `[SPACE] pause/resume` to
`[SPACE] pause/resume · [ESC] stop` in lockstep — the
"only-wired-keybinds" discipline holds.

3 new tests:
- esc_keyboard_stops_active_replay (positive: Esc → Inactive,
  overlay despawns next frame)
- esc_keyboard_is_noop_when_not_playing (negative: doesn't fire
  on Inactive state, lets global Esc listeners own those frames)
- keybind_footer_hint_lists_space_and_esc (footer text contains
  both keybinds)

Plus updated helper-pin test for the new hint string. Existing
pause_plugin tests unaffected (they don't insert a
ReplayPlaybackState resource so the new gate is a no-op for
them).

Tests: 1240 → 1243 (+3). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:06:02 -07:00
funman300 decbe0bbd9 docs(handoff): record keybind footer; B's next step is ESC accelerator
Post-v0.21.4 third carve-out: 1873b3f ships a keybind-hint footer
(vim-style mode line + `[SPACE] pause/resume`) at the bottom of
the banner (76 → 92 px). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1236 → 1240 tests),
and HEAD hint.

Footer lists only wired keybinds. Next finite step on B-2: wire
ESC for stop and extend the footer to `[SPACE] pause/resume ·
[ESC] stop` — small, single-axis, surfaces another keyboard
accelerator alongside the existing Stop button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:59:57 -07:00
funman300 1873b3f9be feat(replay): add keybind-hint footer to overlay banner
Vim-style mode line on the left (`▌ NORMAL │ replay`) plus a
keybind-hint on the right (`[SPACE] pause/resume`) gives the
existing Space accelerator a visible UI counterpart, satisfying
the UI-first contract from CLAUDE.md §3.3 for the keyboard
accelerator that v0.21.4 shipped.

The footer lists only keybinds that are *actually wired today*.
Future commits that wire ESC for stop or ← / → for prev/next
move will extend the right-hand text in lockstep — the footer
never lists aspirational keybinds (would lie to users).

Banner height grew from 76 → 92 px to make room for the 16 px
footer row. Second layout-changing commit in B-2's screen-
takeover arc; same "grow container, add flex-column child"
pattern as the notch-labels commit. 1px top border in
BORDER_SUBTLE separates the footer from the notch-label row.

Two pure helpers (`keybind_footer_mode_text`,
`keybind_footer_hint_text`) keep the static text testable
without per-text marker components on the inner Text entities.
The shared `font_handle_for_labels` clone covers both label and
footer text spawns since the labels closure only `.clone()`s
the handle (never moves it).

4 new tests: pure-helper guards, footer-spawn cardinality
(exactly one), text-set assertion (both helper strings appear as
descendants), lifecycle parity with the overlay tree.

Tests: 1236 → 1240 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:58:28 -07:00
funman300 d11d97e677 docs(handoff): record notch labels; B's next step is keybind footer
Post-v0.21.4 second carve-out: d322abf ships percentage labels
under each scrub-bar notch (banner 60 → 76 px — first real layout
change in B-2's arc). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1232 → 1236 tests),
and HEAD hint.

Banner geometry is now mutable; future B-2 sub-pieces follow the
same "grow container, add flex-column child" pattern. Next
finite step: keybind-hint footer (small) before the bigger
move-log / mini-tableau pieces.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:52:28 -07:00
funman300 d322abf67b feat(replay): add percentage labels under scrub-bar notches
Five `0%` / `25%` / `50%` / `75%` / `100%` labels in a new 16 px
row beneath the 1 px scrub track give the player explicit
quarter-mark readouts to pair with the notch ticks.

Pure helper `scrub_notch_labels()` returns the fixed array,
paired index-for-index with `scrub_notch_positions()`. Spawn loop
zips both helpers and applies an "endpoints flush, middle three
percent-anchored" positioning pattern: leftmost label gets
`left: 0` (no clip on `0%`), rightmost gets `right: 0` (no overflow
on `100%`), middle three anchor at `left: Val::Percent(p)` since
Bevy 0.18 UI lacks a clean CSS-style `translate-x: -50%` centering
primitive. The slight right-of-notch offset on the middle three
is visually subtle at TYPE_CAPTION; explicit polish target if
anyone notices.

Banner height grew from 60 → 76 px to make room for the label row
(76 = top row 59 flex-grow + scrub track 1 + label row 16). First
real layout change in B-2's screen-takeover arc — every prior
B-2 commit was additive at fixed banner geometry.

Label color is TEXT_SECONDARY rather than mockup's `text-outline`
(BORDER_SUBTLE) — the latter would match the notches but is too
low-contrast against BG_ELEVATED_HI to read at 12 px. TEXT_SECONDARY
keeps the subdued caption hierarchy while staying legible.

4 new tests: pure-helper guard pinning the array + helper-positions
pairing invariant, spawn cardinality, set equality between spawned
texts and helper output, lifecycle parity with the overlay tree.

Tests: 1232 → 1236 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:51:09 -07:00
funman300 c9e4c0b4cd docs(handoff): record scrub-bar notches; B's next step is notch labels
Post-v0.21.4 carve-out: fe68861 ships quarter-mark notches on the
scrub bar. Update Since-cut log, visual-identity bullet, B option
in the Resume menu, status (1228 → 1232 tests), and HEAD hint.

Next finite step on B-2: percentage labels under each notch —
forces banner height to grow from 60 px to ~76 px, making it the
first real layout change in the screen-takeover arc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:44:05 -07:00
funman300 fe68861e10 feat(replay): add quarter-mark notches to scrub bar
Five 1px vertical ticks at 0/25/50/75/100% give the player visual
anchor points for "where am I, relative to the quarter-marks of the
replay" without needing to mentally bisect the bar.

Pure helper `scrub_notch_positions()` returns the fixed array; the
spawn loop sits next to the WIN MOVE marker spawn so the two
overlays share their lifecycle with the rest of the overlay tree.
Notches paint in BORDER_SUBTLE (same as the unfilled track) and
extend vertically past the 1px track (5px tall, anchored 2px above
the track top) — same visibility trick the WIN MOVE marker uses.

Spawned after the WIN MOVE marker so a notch and the marker landing
on the same percentage paint the marker on top.

Mirrors the notch ladder in the screen-takeover mockup at
docs/ui-mockups/replay-overlay-mobile.html. First finite step toward
B-2's screen-takeover layout reflow; labels under each notch land in
a follow-up commit when the banner height grows to accommodate them.

4 new tests: pure-helper guard pinning the [0,25,50,75,100] array,
spawn-cardinality matching helper.len(), lifecycle parity with the
overlay tree, independence from win_move_index.

Tests: 1228 → 1232 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:42:37 -07:00
funman300 c33b39cf11 docs(handoff): refresh post-v0.21.4 — anchor to new tag, reset menu state
Anchors handoff to v0.21.4 at `23ff62c`, resets the "Since the cut"
section to placeholder, updates the READ FIRST CHANGELOG pointer,
bumps the Resume-prompt summary to reflect replay-scrubbing
accessibility as the v0.21.4 through-line, and identifies the
screen-takeover layout reflow as the remaining multi-session arc
on B (with move-log scroller + mini-tableau preview as small
sub-pieces inside it).

Resume menu stays at A/B/C — A and C unchanged; B's prerequisite
sub-pieces shipped in v0.21.4 so the entry now points cleanly at
the layout reflow as the single remaining multi-session piece.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:28:50 -07:00
7 changed files with 2930 additions and 120 deletions
+291 -1
View File
@@ -6,9 +6,299 @@ project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
No threads in flight. v0.21.4 cut on 2026-05-08; CHANGELOG accumulates
No threads in flight. v0.21.7 cut on 2026-05-08; CHANGELOG accumulates
the next cycle here.
## [0.21.7] — 2026-05-08
Patch release closing the last major B-2 sub-piece. Through-line:
**mini-tableau preview dim layer**. The mockup's "Game Peek Band at
50 % opacity" is now implemented as a full-screen UI scrim that darkens
the card world during replay so the chrome (banner + move-log panel)
reads clearly against the scene.
### Added
- **Full-screen tableau dim layer** (`da3e542`). Spawns a
`ReplayTableauDimLayer` UI node (100 % × 100 %, 50 % opacity
black) at `Z_REPLAY_DIM = Z_REPLAY_OVERLAY 1 = 54` whenever
a replay starts; despawned alongside the banner and move-log
panel when the replay ends. Bevy's UI/world compositor means
no changes to `card_plugin` are needed — UI nodes always
render above world-space sprites regardless of `Transform.z`.
The dim layer carries no `Interaction` component (purely
visual; pointer events pass through). Adds `Z_REPLAY_DIM`
and `TABLEAU_DIM_ALPHA` constants plus two new tests:
lifecycle (spawn/despawn mirrors the floating-chip pattern)
and z-ordering invariant (`Z_REPLAY_DIM < Z_REPLAY_OVERLAY`
pinned). 1275 tests pass / 0 failing.
### Stats
- Tests: 1275 passing / 0 failing
- Clippy: clean
- Crates touched: `solitaire_engine` (replay_overlay.rs)
## [0.21.6] — 2026-05-08
Patch release for the post-v0.21.5 work. Through-line:
**Move Log panel + scrub-UX polish**. v0.21.5 closed out the
keyboard-accelerator surface (Space / Esc / ← / →) and the
keybind footer; v0.21.6 builds on that with two parallel
threads — accessibility + scrub-on-hold polish for the v0.21.5
surfaces, plus a brand-new Move Log panel anchored to the
viewport's bottom edge that gives players a 5-row recent-and-
upcoming move history alongside the existing top-edge banner.
The Move Log panel is the first replay-overlay surface that
*isn't* attached to the banner — it lives at a separate screen
anchor (bottom: 0) with its own spawn/despawn lifecycle.
Establishes the pattern for "multi-anchor replay UI" that the
remaining B-2 sub-piece (mini-tableau preview) will inherit.
### Added
- **HC-mode coverage for the scrub track + quarter-mark notch
ticks** (`d3cb1a5`). Adds parallel primitive
`HighContrastBackground` to `ui_theme` and a paint system
`update_high_contrast_backgrounds` in `settings_plugin` that
mirrors the existing border-marker pattern but targets
`BackgroundColor` instead of `BorderColor`. Tags the 1 px
scrub track Node and all five quarter-mark notch ticks so
they bump from `BORDER_SUBTLE` (`#505050`) →
`BORDER_SUBTLE_HC` (`#a0a0a0`) under HC mode. Scrub fill
(`ACCENT_PRIMARY`) and WIN MOVE marker (`STATE_SUCCESS`)
don't get the marker — accent and state colours are already
saturated and don't need an HC luminance variant.
- **Continuous scrub on key-held arrow keys** (`2e25476`).
Holding ← or → triggers continuous step at 100 ms cadence
(10 steps/sec) — matches the mockup's `[← →] scrub`
terminology while keeping single-press = single-step
semantics. Per-key accumulators in a new
`ReplayScrubKeyHold` resource; `just_pressed` events bypass
the accumulator and fire immediately. Release resets to 0
so the next fresh press fires immediately rather than at
half-interval.
- **Move Log panel** (`d6f32d3` + `140251b` + `e7345ae` +
`4437a1a`). New bottom-edge UI panel showing a 5-row window
onto recent + upcoming moves: 2 prev rows above the active
row + active row highlighted in `ACCENT_PRIMARY` + 2 next
rows below. Header reads `▌ MOVE LOG · N/M` (or
`▌ MOVE LOG · COMPLETE` when finished). Active row carries
a `▶` focus prefix and `TEXT_PRIMARY_HC` text colour for
legible contrast against the brick-red highlight. Prev /
next rows render in `TEXT_SECONDARY` so the active row
stays the focal point.
- Sibling-of-banner pattern (separate root entity anchored
at viewport bottom, not a banner child) — same
spawn/despawn lifecycle as `ReplayFloatingProgressChip`,
different screen anchor.
- Five pure helpers handle the formatting:
`format_pile`, `format_move_body`,
`format_move_log_header`, `format_kth_recent_row` (active
+ prev), `format_kth_next_row` (next). 1-indexed display
numbers throughout (`Foundation(2)` reads as "foundation
3" rather than the enum's 0-index).
- Panel grows from 56 → 84 → 112 px across the four
move-log commits. `MOVE_LOG_PREV_ROWS` and
`MOVE_LOG_NEXT_ROWS` constants (both = 2) parameterise
the row count; `format_kth_recent_row` and
`format_kth_next_row` return empty for out-of-range k so
panels gracefully under-fill at the start (cursor=1) and
end (cursor=N-1) of a replay.
- HC marker on the panel's top border so the 1 px edge
bumps under HC mode (same pattern as the keybind footer).
### Changed
- **`react_to_state_change` despawns the Move Log panel** on
`Playing → Inactive` alongside the banner root and floating
progress chip. Third query in the same defer-and-despawn
cycle.
- **Move Log panel height grew 56 → 84 → 112 px** across the
prev-rows and next-rows commits. The panel is sized to fit
the chosen row count + header + padding; tunable via the
`MOVE_LOG_PANEL_HEIGHT` const.
- **`format_active_move_row` now prefixes the `▶` focus
marker** (`e7345ae`). Wraps `format_kth_recent_row(state, 1)`
and prepends the prefix when the body is non-empty. Empty
case still returns empty — cursor=0 doesn't paint a stray
`▶` on an otherwise-empty row.
### Documentation
- `SESSION_HANDOFF.md` refreshed twice this cycle — once
recording the HC paint + continuous-scrub polish, then
again as the Move Log arc shipped commit-by-commit. The
Resume menu's B option now traces the full arc:
notches → labels → footer → ESC → HC → arrow keys →
HC paint → continuous scrub → move log.
### Stats
- **1273 passing tests / 0 failing** across the workspace
(net +23 from v0.21.5's 1250 baseline):
- 2 from `d3cb1a5` (HC marker on track + notches).
- 2 from `2e25476` (continuous-scrub repeat-while-held +
release-resets-accumulator).
- 8 from `d6f32d3` (move-log panel init + 5 helpers + 3
spawn / lifecycle scenarios).
- 4 from `140251b` (prev rows: helper k coverage + spawn
cardinality + spawn texts + repaint on cursor advance).
- 3 from `e7345ae` (active row highlight: wrapper bg +
text colour + focus prefix + cursor=0 stays empty).
- 4 from `4437a1a` (next rows: helper k coverage + spawn
cardinality + spawn texts + under-fill at replay end).
- Clippy clean across the workspace.
## [0.21.5] — 2026-05-08
Patch release for the post-v0.21.4 work. One through-line:
**replay-overlay scrubbing affordances + accessibility**. v0.21.4
shipped pause / resume / step + the WIN MOVE marker as the first
*scrubbing-shaped* additions to the replay overlay; v0.21.5
fills out the rest of the scrubbing UX so the player has both
visual anchor points (notches + labels) and a complete keyboard
control surface (Space / Esc / ← / →) for navigating a paused
replay.
Two of the six commits in this cycle are layout-changing — they
grow the banner height from 60 px → 76 px → 92 px to make room
for the notch labels and keybind footer. Banner geometry was
fixed for every prior B-2 commit; this release establishes the
"grow the container, add a flex-column child" pattern that the
remaining B-2 sub-pieces (move-log scroller, mini-tableau
preview) will inherit when they land.
### Added
- **Quarter-mark scrub-bar notches** (`fe68861`). Five 1 px
vertical ticks at 0 / 25 / 50 / 75 / 100 % give the player
visual anchor points without needing to mentally bisect the
bar. Pure helper `scrub_notch_positions()` returns the fixed
array; spawn loop sits next to the WIN MOVE marker spawn so
the lifecycles match. Notches paint in `BORDER_SUBTLE` (same
as the unfilled track) and rely on extending past the 1 px
track (5 px tall, anchored 2 px above the track top) for
visibility — same trick the WIN MOVE marker uses. Spawned
*after* the WIN MOVE marker so a notch and the marker
landing on the same percentage paint the marker on top.
- **Percentage labels under each notch** (`d322abf`). Five
`0%` / `25%` / `50%` / `75%` / `100%` labels in a new 16 px
row beneath the 1 px scrub track give the player explicit
quarter-mark readouts. Banner grew from 60 → 76 px to
accommodate the row — first **layout-changing** commit in
the B-2 arc. Pure helper `scrub_notch_labels()` returns the
fixed array, paired index-for-index with
`scrub_notch_positions()`. Spawn loop applies an "endpoints
flush, middle three percent-anchored" positioning pattern:
leftmost label gets `left: 0`, rightmost gets `right: 0`,
middle three anchor at `left: Val::Percent(p)` since Bevy
0.18 UI lacks a clean CSS-style `translate-x: -50%`
centering primitive. Label colour is `TEXT_SECONDARY`
rather than the mockup's `BORDER_SUBTLE` (the latter would
match the notches but is too low-contrast against
`BG_ELEVATED_HI` to read at 12 px).
- **Keybind-hint footer** (`1873b3f`). Vim-style mode line on
the left (`▌ NORMAL │ replay`) plus a keybind hint on the
right at the bottom edge of the banner. Banner grew from
76 → 92 px to fit the 16 px footer row. Surfaces every
wired keyboard accelerator visually so CLAUDE.md §3.3's
UI-first contract holds for keyboard accelerators too. The
footer lists *only* keybinds that are actually wired —
the only-wired-keybinds discipline means each release
cycle's hint string is a precise honest contract with the
player. Two pure helpers (`keybind_footer_mode_text`,
`keybind_footer_hint_text`) keep the static text testable.
1 px top border in `BORDER_SUBTLE` separates the footer
from the labels row.
- **ESC keyboard accelerator for replay-stop** (`90e24d9`).
New `handle_stop_keyboard` system parallels
`handle_pause_keyboard` in shape — fires only when state
is `Playing`, calls `stop_replay_playback`. Cross-plugin
coordination via `pause_plugin::toggle_pause`: added a
fourth defer-if check
(`replay_state.is_some_and(|s| s.is_playing())`) right
after the existing `other_modal_scrims` check so ESC
during active replay belongs to the replay overlay, not
the pause modal.
- **HC-mode coverage for the keybind-footer top border**
(`23902cd`).
`HighContrastBorder::with_default(BORDER_SUBTLE)` marker
on the footer's border-carrying Node so the existing
`apply_high_contrast_borders` system bumps the 1 px top
border from `#505050``#a0a0a0` when
`Settings::high_contrast_mode` is on. Without the marker
the footer reads as floating loose under HC because the
border that anchors it to the labels row is
near-invisible.
- **← / → keyboard accelerators for paused stepping**
(`e5c4f51`). New `step_backwards_replay_playback` in
`replay_playback.rs` decrements the cursor and dispatches
`UndoRequestEvent`; the game's `handle_undo` reads it
next frame to reverse its most-recent move. Hooks the
existing undo system rather than replaying-forward-from-
zero — every replay-applied move pushes to the undo stack
the same way a player move would, so undo is the right
reversal primitive. Both arrow keys are paused-only via
the same destructure-gate pattern the forward step uses.
The mockup labels these `[← →] scrub`; single-move step
is the closest behaviour shippable today, so the footer
hint reads `[← →] step` — only-wired-keybinds discipline.
### Changed
- **Banner height grew 60 → 76 → 92 px** across two
layout-changing commits (`d322abf` then `1873b3f`). Top
row's `flex_grow: 1.0` still consumes 59 px so the
existing content (label / progress chip / buttons) has
the same vertical space; the new rows (16 px labels +
16 px footer) extend the banner downward into the
gameplay area. Banner geometry is now mutable — every
prior B-2 commit fit inside fixed 60 px space.
- **Keybind-footer hint text grew alongside the wirings**:
`[SPACE] pause/resume`
`[SPACE] pause/resume · [ESC] stop`
`[SPACE] pause/resume · [ESC] stop · [← →] step`.
- **`pause_plugin::toggle_pause` now defers when a replay
is active** (`90e24d9`). Adds a fourth defer-if check to
the existing modal-stack pattern.
- **`ReplayOverlayPlugin` registers
`add_message::<UndoRequestEvent>()`** (`e5c4f51`).
Defensive registration so the plugin runs cleanly under
`MinimalPlugins` without `GamePlugin` attached.
### Documentation
- `SESSION_HANDOFF.md` refreshed five times this cycle.
The B option in the Resume menu now traces the full arc:
notches → labels → footer → ESC → HC → arrow keys.
- The pre-existing `daily_challenge` warning test that
fails when wall-clock UTC is within 30 minutes of
midnight is documented in this cycle's handoff. Same
shape as the earlier `winnable_seed_search` flake —
time-dependent, deterministically passes outside the
trigger window.
### Stats
- **1250 total tests / 1249 passing / 1 pre-existing
time-dependent flake** across the workspace (net +22 from
v0.21.4's 1228 baseline):
- 4 from `fe68861` (scrub-notch coverage)
- 4 from `d322abf` (notch-label coverage)
- 4 from `1873b3f` (keybind-footer coverage)
- 3 from `90e24d9` (ESC-accelerator coverage)
- 1 from `23902cd` (HC-marker coverage)
- 6 from `e5c4f51` (arrow-keyboard coverage)
- **Pre-existing flake**:
`daily_challenge_plugin::tests::check_system_fires_warning_event_only_once_per_day`
fails when wall-clock UTC is within 30 minutes of
midnight. Verified pre-existing by stash-and-retest
before each commit. Will pass deterministically outside
the trigger window. Not introduced by this release.
- Clippy clean across the workspace.
## [0.21.4] — 2026-05-08
Patch release for the post-v0.21.3 work. One through-line:
+81 -109
View File
@@ -1,91 +1,66 @@
# Solitaire Quest — Session Handoff
**Last updated:** 2026-05-08 — **v0.21.3 cut and tagged at
`3d92a91`**, working tree clean, all post-tag work pushed to
origin.
**Last updated:** 2026-05-08 — **v0.21.7 cut and tagged at
`da3e542`**, working tree clean (tag pending push).
v0.21.3 is a patch release with one through-line: **accessibility
arc closure**. v0.21.2 explicitly carved out "dynamic-paint sites"
(HUD action buttons, modal buttons, radial menu rim) on the
assumption that their existing paint cycles would race the
central `update_high_contrast_borders` system. v0.21.3 walks the
actual code, finds the carve-out was over-cautious, and closes
it. Bonus: the first real consumer of `ToastVariant::Warning`
also lands here, making the `ToastVariant` enum fully load-bearing
(every variant has at least one driver).
v0.21.7 is a single-commit patch closing the last major B-2
sub-piece: **mini-tableau preview dim layer**. A full-screen
`ReplayTableauDimLayer` UI node (100 % × 100 %, 50 % opacity
black) at `Z_REPLAY_DIM = 54` (one rung below the replay
chrome at z=55) darkens the card world during replay so the
banner and move-log panel read clearly against the scene —
matching the mockup's "Game Peek Band at 50 % opacity" spec
without touching `card_plugin`. 13 commits have now shipped
across v0.21.4v0.21.7 on the B-2 replay screen-takeover
arc; every major sub-piece is closed.
Full v0.21.3 detail lives in `CHANGELOG.md` § [0.21.3]. This
Full v0.21.7 detail lives in `CHANGELOG.md` § [0.21.7]. This
file from here on focuses on what's *open* post-cut and how to
resume.
## Status at pause
- **HEAD locally:** see `git rev-parse HEAD`. The cut commit is
`3d92a91`; post-cut work on B-2 (`ab857bb` data field +
`52befa6` WIN MOVE marker UI + `fbe48ac` playback controls)
rides on top of that.
- **HEAD on origin:** matches local. v0.21.3 is fully on origin.
- **HEAD locally:** `da3e542` (v0.21.7 commit). Tag pending —
push with `git tag v0.21.7 da3e542 && git push origin v0.21.7`.
- **HEAD on origin:** `f63db76` (v0.21.6). v0.21.7 commit
not pushed yet; a docs-only edit will ride on top before push.
- **Working tree:** clean. No WIP outstanding.
- **`artwork/` directory:** still untracked. Intentional.
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
clean.
- **Tests:** **1228 passing / 0 failing** across the workspace
(1207 from v0.21.3's stats + 5 from `ab857bb`'s
`win_move_index` coverage + 8 from `52befa6`'s WIN MOVE marker
pure-helper truth-table + spawn lifecycle + 8 from `fbe48ac`'s
pause / step / keyboard accelerator coverage).
- **Tags on origin:** `v0.9.0` through `v0.21.3`. v0.21.3 is on
`3d92a91`; v0.21.2 stays on `f23df3b`; v0.21.1 stays on
`daa655a`; v0.21.0 stays on `04f9bf9`; v0.20.0 stays on
`41a009a`.
- **Tests:** **1275 passing / 0 failing** across the workspace.
Detail in `CHANGELOG.md` § [0.21.7] § Stats.
- **Tags on origin:** `v0.9.0` through `v0.21.6`. v0.21.7
tag exists locally at `da3e542`; push to origin when ready.
## Since the v0.21.3 cut
## Since the v0.21.7 cut
- **`ab857bb``Replay::win_move_index` data field landed.**
First finite step toward the B-2 replay screen-takeover
redesign. Additive optional `Option<usize>` on `Replay` with
`#[serde(default)]` so older `latest_replay.json` /
`replays.json` files load unchanged (no schema bump). Populated
at the live recording site via a new `with_win_move_index`
builder; for fresh recordings the value is always
`Some(moves.len() - 1)` because recording freezes on win, but
storing it explicitly lets the playback UI read the WIN MOVE
position directly without re-deriving on every render. 5 new
tests (1207 → 1212): default, builder set / set-None, on-disk
round-trip, legacy-JSON-loads-with-None backward-compat.
- **`52befa6` — WIN MOVE marker on the scrub bar.** Second
commit on B-2 — the UI that consumes the data field. New
`ReplayOverlayWinMoveMarker` component spawned as a sibling
to `ReplayOverlayScrubFill` under the 1px scrub track,
absolute-positioned at `replay.win_move_index / total` along
the bar. Painted in `STATE_SUCCESS` (green) so the marker
reads as "this is where the win lives." Pure helper
`win_move_marker_pct` returns `None` for any state where the
marker shouldn't draw (Inactive, Completed, replay missing
the field, empty move list); percentage clamps to `[0, 100]`
defensively. Lifecycle is spawn-time only — the marker is
immutable during a single playback because the underlying
`Replay` doesn't change while `Playing`. Despawned with the
overlay tree on transition back to `Inactive`. 8 new tests
(1212 → 1220): pure-helper truth table + spawn-presence /
spawn-absence / despawn-lifecycle observables.
- **`fbe48ac` — playback controls (pause / resume / step).**
Third commit on B-2. New `paused: bool` field on
`ReplayPlaybackState::Playing`; `tick_replay_playback` skips
the `secs_to_next` decrement entirely while paused so cursor
and timer freeze together. New public API:
`toggle_pause_replay_playback` and `step_replay_playback`
(the latter hard-gated to `Playing { paused: true }` so
manual stepping can't race the tick loop). UI: Pause /
Resume button (label repaints reactively via
`update_pause_button_label` which walks `Children` from
marker to inner `Text`) + Step button + Space keyboard
accelerator. Existing 25 `Playing { ... }` construction
sites across tests gained `paused: false` mechanically.
8 new tests (1220 → 1228): label truth table, label repaint
on state change, click-toggles-paused, step advances exactly
one cursor with paused preserved, step-while-running no-op,
Space toggles paused.
One commit in flight (not yet pushed to origin): `da3e542`
adds the full-screen tableau dim layer. CHANGELOG and
SESSION_HANDOFF updates ride on top. Push with:
```
git push origin master
git push origin v0.21.7
```
Open next-step menu (all major B-2 sub-pieces now closed):
1. **Polish: notch label centering.** Bevy 0.18 lacks a
clean `translate-x: -50%` primitive so the middle three
scrub-bar labels sit slightly right-of-notch. Could use a
child Text wrapper with computed left-margin compensation.
Tiny commit, requires visual review.
2. **Polish: WIN MOVE marker HC bump.** Currently uses
`STATE_SUCCESS` lime which stays visible under HC, but a
contrast bump under HC would make it even more legible
alongside the bumped notches. Optional.
3. **Move Log auto-scroll** — only relevant if the panel's
row count grows beyond the current 5-row fixed window.
Currently the prev-2 / active / next-2 layout fits all
visible content, so auto-scroll is unneeded.
Recommended order: options 1 and 2 are tiny polish commits
that benefit from visual review. Option 3 is a non-starter
unless the panel's row capacity grows.
## Open punch list
@@ -117,20 +92,18 @@ chrome migration, splash boot screen, replay-overlay banner,
card-face artwork (both rendering paths), and the `ACCENT_PRIMARY`
palette refresh all shipped in v0.20.0 + v0.21.0. What stays open:
- **Replay-overlay screen-takeover redesign.** The full mockup
(`docs/ui-mockups/replay-overlay-mobile.html`) calls for a
mini-tableau preview, playback controls, move-log scroll, and
a WIN MOVE marker on the scrub bar. Banner-local pieces all
shipped in v0.21.0 (`c84d9f4` + `6204db8` + `54005d5` +
`e080b49`); the floating MOVE chip above the focused card
shipped in v0.21.2 (`2fb2d63`). The WIN MOVE scrub-bar marker
shipped post-v0.21.3 in `ab857bb` (data field) + `52befa6`
(UI). Playback controls (pause / resume / step + Space
accelerator) shipped post-v0.21.3 in `fbe48ac`. What still
needs to land: a move-log scroller and a mini-tableau
preview — both screen-takeover-only pieces that need a
larger layout reflow than the existing banner can carry.
Multi-session.
- *Replay-overlay screen-takeover redesign — closed 2026-05-08
across 13 commits (v0.21.4v0.21.7).* The full mockup
(`docs/ui-mockups/replay-overlay-mobile.html`) has shipped:
banner chrome (v0.21.0), floating MOVE chip (v0.21.2), WIN
MOVE scrub-bar marker (post-v0.21.3), playback controls /
Space accelerator (post-v0.21.3), scrub notches + labels +
keybind footer + ESC / ← / → accelerators + HC border
(v0.21.5), Move Log panel + HC scrub track + continuous
scrub (v0.21.6), and full-screen 50 % opacity dim layer
(v0.21.7). Every major B-2 sub-piece is now closed. The
only remaining items are minor polish: notch-label centering
and WIN MOVE HC contrast bump (see Open next-step menu).*
- *Floating `MOVE N/M` chip above the focused card during
playback — closed 2026-05-08 by `2fb2d63`.* World-space
`Text2d` entity sibling to the banner overlay; uses the same
@@ -273,20 +246,22 @@ into a v0.21.1 / v0.22.0 cut.
```
You are a senior Rust + Bevy developer working on Solitaire Quest.
Working directory: <Rusty_Solitaire clone path on this machine>.
Branch: master. v0.21.3 is tagged at 3d92a91 (cut 2026-05-08, a
patch release rolling up the accessibility-arc closure: HC reaches
the previously-carved-out dynamic-paint sites, and the first real
consumer of `ToastVariant::Warning` lands as the daily-challenge
expiry toast). v0.21.2 stays at f23df3b, v0.21.1 at daa655a,
v0.21.0 at 04f9bf9. Working tree clean. See CHANGELOG.md §
[0.21.3] for full detail.
Branch: master. v0.21.7 is tagged at da3e542 (cut 2026-05-08,
closes the last major B-2 sub-piece: full-screen tableau dim
layer — 50 % opacity black UI scrim at z=54 that darkens the
card world during replay so the chrome reads clearly above it).
v0.21.6 stays at f63db76, v0.21.5 at a2432df, v0.21.4 at
23ff62c, v0.21.3 at 3d92a91, v0.21.2 at f23df3b, v0.21.1 at
daa655a, v0.21.0 at 04f9bf9. Working tree clean (CHANGELOG +
SESSION_HANDOFF docs ride on top of da3e542; push pending).
See CHANGELOG.md § [0.21.7] for full detail.
State: HEAD locally — see `git rev-parse HEAD`. All workspace tests
pass (1207+; check with `cargo test --workspace`), clippy clean.
State: HEAD locally — see `git rev-parse HEAD`. Workspace
tests: 1275 passing / 0 failing. Clippy clean.
READ FIRST (in order, before doing anything):
1. SESSION_HANDOFF.md — this file
2. CHANGELOG.md — [0.21.3] section is the most recent cut
2. CHANGELOG.md — [0.21.6] section is the most recent cut
3. CLAUDE.md — unified-3.0 rule set
4. CLAUDE_SPEC.md — formal architecture spec
5. ARCHITECTURE.md — crate responsibilities + data flow
@@ -306,18 +281,15 @@ DECISION TO ASK THE PLAYER FIRST:
tests can't catch. Likely surfaces JNI ClipboardManager
and Android Keystore stubs that need real bridges. Larger
scope; needs an Android device or emulator running.
B. Replay-overlay screen-takeover redesign — multi-session
work. Three sub-pieces shipped post-v0.21.3: WIN MOVE
marker (`ab857bb` data field + `52befa6` UI), playback
controls (`fbe48ac` pause/resume/step + Space). What
still needs to land: a move-log scroller and a
mini-tableau preview — both layout-heavy pieces that need
more vertical real estate than the current banner-only
overlay carries, so the natural next finite step is the
screen-takeover layout itself (mockup at
`docs/ui-mockups/replay-overlay-mobile.html`). The
smaller floating-MOVE-chip piece shipped in v0.21.2
(`2fb2d63`).
B. Replay-overlay polish (B-2 arc fully closed in v0.21.7).
All 13 planned sub-pieces shipped. Remaining items are
minor polish: (a) scrub-bar notch-label centering — middle
three labels sit slightly right-of-notch due to Bevy 0.18
lacking `translate-x: -50%`; tiny commit, needs visual
review. (b) WIN MOVE marker HC contrast bump — optional
luminance boost under HC mode. Both are single commits
requiring visual review; recommend treating as a v0.21.8
polish pass after manual testing.
C. Phase 8 (sync) — local storage scaffolding, self-hosted
Axum server, `SolitaireServerClient` impl, GPGS stub
wired into Settings. The biggest open arc by scope; rolls
+11
View File
@@ -30,6 +30,7 @@ use crate::events::{
use crate::font_plugin::FontResource;
use crate::game_plugin::{GameOverScreen, GameStatePath};
use crate::progress_plugin::ProgressResource;
use crate::replay_playback::ReplayPlaybackState;
use crate::resources::{DragState, GameStateResource};
use crate::selection_plugin::{SelectionKeySet, SelectionState};
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath};
@@ -154,6 +155,7 @@ fn toggle_pause(
mut drag: Option<ResMut<DragState>>,
mut changed: MessageWriter<StateChangedEvent>,
selection: Option<Res<SelectionState>>,
replay_state: Option<Res<ReplayPlaybackState>>,
) {
let PauseModalQueries {
pause_screens: screens,
@@ -184,6 +186,15 @@ fn toggle_pause(
if !other_modal_scrims.is_empty() {
return;
}
// If a replay is currently playing, let `replay_overlay::handle_stop_keyboard`
// own the Esc press — that handler stops the replay. Without this guard a
// single Esc both stops the replay AND opens the pause modal on top of the
// (now empty) board, leaving the player on a screen they didn't ask for.
// The HUD-button path is gated too; clicking Pause while watching a replay
// is almost always an accident.
if replay_state.is_some_and(|s| s.is_playing()) {
return;
}
// If a card is currently selected, let SelectionPlugin handle this Escape
// (it will clear the selection). Pause must not also open in the same frame.
if selection.is_some_and(|s| s.selected_pile.is_some()) {
File diff suppressed because it is too large Load Diff
+47 -1
View File
@@ -42,7 +42,7 @@
use bevy::prelude::*;
use solitaire_data::{Replay, ReplayMove};
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent};
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
use crate::game_plugin::{GameMutation, RecordingReplay};
use crate::resources::GameStateResource;
use crate::settings_plugin::SettingsResource;
@@ -284,6 +284,52 @@ pub fn step_replay_playback(
true
}
/// Steps the replay **backwards** by exactly one move while paused.
///
/// Strategy: the live game's undo system is the source of truth for
/// reversing moves. Every move the replay forward-stepped (via
/// [`step_replay_playback`] or the auto-advance loop in
/// [`tick_replay_playback`]) was dispatched as a canonical
/// [`MoveRequestEvent`] / [`DrawRequestEvent`], which the game
/// applied and pushed onto its undo stack. So a backwards step here
/// is simply: decrement the cursor (so the about-to-apply move
/// re-points at the one we're rewinding past) and fire an
/// [`UndoRequestEvent`] so the game reverses its most-recent move
/// next frame.
///
/// Hard-gated to the paused state via destructure pattern —
/// matches the existing [`step_replay_playback`] gate so the
/// player can only scrub one direction at a time and the tick
/// loop never races a manual rewind.
///
/// Returns `false` and is a no-op in three cases:
/// - State isn't `Playing` (no replay attached).
/// - State is `Playing` but not paused (the tick loop owns the cursor).
/// - Cursor is already at 0 (nothing to rewind past).
///
/// Returns `true` on a successful step; the actual game-state
/// reversal happens next frame when `handle_undo` reads the
/// `UndoRequestEvent`.
pub fn step_backwards_replay_playback(
state: &mut ResMut<ReplayPlaybackState>,
undo_writer: &mut MessageWriter<UndoRequestEvent>,
) -> bool {
let ReplayPlaybackState::Playing {
cursor,
paused: true,
..
} = state.as_mut()
else {
return false;
};
if *cursor == 0 {
return false;
}
*cursor -= 1;
undo_writer.write(UndoRequestEvent);
true
}
/// Tick system. Runs every frame; only does work when
/// [`ReplayPlaybackState::is_playing`].
///
+38 -1
View File
@@ -34,7 +34,8 @@ use crate::ui_modal::{
};
use crate::ui_tooltip::Tooltip;
use crate::ui_theme::{
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBorder,
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBackground,
HighContrastBorder,
RADIUS_SM, SPACE_2, STATE_SUCCESS, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
};
@@ -365,6 +366,7 @@ impl Plugin for SettingsPlugin {
update_color_blind_text,
update_high_contrast_text,
update_high_contrast_borders,
update_high_contrast_backgrounds,
update_reduce_motion_text,
update_tooltip_delay_text,
update_time_bonus_multiplier_text,
@@ -674,6 +676,41 @@ fn update_high_contrast_borders(
}
}
/// Repaints `BackgroundColor` on every entity tagged with
/// [`HighContrastBackground`] based on `Settings::high_contrast_mode`.
/// Off → the marker's `default_color`; on → `BORDER_SUBTLE_HC`
/// (`#a0a0a0`). Compares against the current background and only
/// mutates when different so Bevy's change-detection doesn't trigger
/// repaints every frame.
///
/// Parallel to [`update_high_contrast_borders`]. Same on/off rule,
/// same change-suppression idiom, different colour channel —
/// `BackgroundColor` for tick marks, decorative strips, fine
/// separators that paint their shape directly rather than via a
/// `BorderColor` on a wider Node.
///
/// Tagged sites in v0.21.x: the replay overlay's 1 px scrub track
/// + 5 quarter-mark notch ticks (`replay_overlay::spawn_overlay`).
///
/// More sites can be tagged in follow-ups by adding
/// `HighContrastBackground::with_default(...)` to their spawn tuple.
pub(crate) fn update_high_contrast_backgrounds(
settings: Res<SettingsResource>,
mut backgrounds: Query<(&HighContrastBackground, &mut BackgroundColor)>,
) {
let high_contrast = settings.0.high_contrast_mode;
for (marker, mut bg) in backgrounds.iter_mut() {
let target = if high_contrast {
marker.hc_color
} else {
marker.default_color
};
if bg.0 != target {
*bg = BackgroundColor(target);
}
}
}
fn update_reduce_motion_text(
settings: Res<SettingsResource>,
mut text_nodes: Query<&mut Text, With<ReduceMotionText>>,
+57
View File
@@ -93,6 +93,13 @@ pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);
/// from base16-eighties. `#acc267`.
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);
/// High-contrast variant of [`STATE_SUCCESS`] — `#c8e862`. Brighter
/// lime that maintains the success hue while lifting luminance from
/// ~0.51 → ~0.73 so the WIN MOVE scrub-bar marker stands out from
/// the bumped notch ticks (`BORDER_SUBTLE_HC` `#a0a0a0`, L≈0.60) in
/// high-contrast mode.
pub const STATE_SUCCESS_HC: Color = Color::srgb(0.784, 0.910, 0.384);
/// Warning — penalty signal, daily-seed expiry countdown, sync-pending
/// status. Gold from base16-eighties. **Both** Undo and Recycle
/// counters use this when non-zero. `#ddb26f`.
@@ -252,6 +259,56 @@ impl HighContrastBorder {
}
}
/// Marker for entities whose [`BackgroundColor`] should swap to
/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on.
/// Parallel to [`HighContrastBorder`] but for sites that paint their
/// shape via `BackgroundColor` rather than `BorderColor` —
/// `bevy::ui` 1 px decorative strips, tick marks, fine separators
/// often render as tiny full-bleed `Node`s, not as borders, so the
/// border-marker pattern doesn't apply.
///
/// `default_color` records the off-state colour; `hc_color` the on-
/// state colour. [`with_default`] fills `hc_color` with
/// [`BORDER_SUBTLE_HC`] so the 90 % of sites that just need the
/// standard subtle-border bump can continue using a one-argument
/// constructor. [`with_hc`] overrides the HC colour for the rare
/// site (currently only the WIN MOVE scrub-bar marker) that needs a
/// domain-specific HC variant (`STATE_SUCCESS_HC` instead of a gray).
///
/// [`with_default`]: HighContrastBackground::with_default
/// [`with_hc`]: HighContrastBackground::with_hc
/// [`BackgroundColor`]: bevy::prelude::BackgroundColor
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
pub struct HighContrastBackground {
/// Background colour to use when high-contrast mode is *off* —
/// the site's normal idle / active-state colour.
pub default_color: bevy::prelude::Color,
/// Background colour to use when high-contrast mode is *on*.
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
///
/// [`with_default`]: HighContrastBackground::with_default
pub hc_color: bevy::prelude::Color,
}
impl HighContrastBackground {
/// Convenience constructor — HC colour defaults to
/// [`BORDER_SUBTLE_HC`].
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
Self { default_color, hc_color: BORDER_SUBTLE_HC }
}
/// Constructor for sites whose HC colour differs from the standard
/// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
/// than to a neutral gray.
pub const fn with_hc(
default_color: bevy::prelude::Color,
hc_color: bevy::prelude::Color,
) -> Self {
Self { default_color, hc_color }
}
}
/// Strong border — hover outline, focused button, active popover.
/// `outline` from the design system. `#505050`.
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);