Project-wide palette shift at user request. Replaces the cyan
primary accent everywhere it surfaces — splash boot screen,
home menu glyphs, action chevrons, replay overlay banner +
scrub fill + chip border, achievement checkmarks, leaderboard
#1 indicator, radial menu fill, focus ring, card-back canonical
badge, etc. — with `#a54242` from the same base16-eighties
family as the existing pink suit colour.
Knock-on changes that all land in this commit per the
lockstep rule:
- ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER
(#c25e5e brightened companion), FOCUS_RING (same hue, 0.85
alpha). Module-level palette comment + STOCK_BADGE_FG +
CARD_SHADOW_ALPHA_DRAG doc strings updated to match.
- card_plugin.rs: card_back_colour(0) now returns the brick-red
ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from
cyan to lime #acc267 — the CBM alternative needs to stay
hue-distinct from the new red-family primary, lime is the
next-best non-red base16-eighties accent. text_colour doc
+ CBM tests renamed cyan→lime in lockstep
(text_colour_color_blind_mode_swaps_red_suits_to_lime).
- card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical
Terminal back).
- splash_plugin.rs / ui_modal.rs / replay_overlay.rs /
selection_plugin.rs: descriptive "cyan" comments swapped to
"accent" / "primary-accent" wording so the doc strings stay
decoupled from any specific hue. Future palette tweaks won't
require comment churn.
- design-system.md: YAML token frontmatter updated (primary,
surface-tint, suit-red-cb, primary-container,
on-primary-container, inverse-primary). Palette table gains
a project-specific `base08` slot for the new red. CTA /
Selection / Card-back badge / Primary button / Bottom-bar
active-icon / glow / CBM swap text all retuned. Historical
references preserved (e.g. "Was cyan #6fc2ef before the
2026-05-08 swap") so the audit trail stays in the spec.
- card_face_svg_pin.rs: rebaselined. Exactly one hash drift
(back_0 — the canonical Terminal back's badge changed
colour). Other 56 hashes identical (face SVGs don't
reference the accent; back_1..4 use unchanged accents). The
one-hash-drift signal confirms the change scope was
surgical.
Workspace clippy + cargo test --workspace clean, 1184 passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates SESSION_HANDOFF.md to reflect the post-2026-05-08 state:
- "Last updated" + status header rewritten — origin caught up
to local through dd101b3, 1184 tests passing (net +4 from
the 1180 baseline: splash polish +2, card-face pin +1, CBM
test consolidation -2 then +1).
- New narrative entry under "Since the v0.20.0 cut" walks
through the 9-commit Option D arc: plan + tooling
(5623368/3a4bb63/babe5cc/48b28d2), lockstep step 4+5
(e8bf9d7), the three sign-off follow-ups (a14200a default-
theme SVG override, 8719f77 backgrounds flattened, ae84dc1
top-bar overlap), the path-glyph fix (af414b6), and the
glyph-orientation tweak (dd101b3).
- "Visual-identity follow-ups" punch-list: card-face item
marked closed with the same commit chain referenced from
the narrative.
- Resume prompt header rewritten — Options B/C/D all closed,
the post-tag work is fully on origin. Option D's bullet
expanded to record the closure rather than describe pending
work.
- The "fallback path the migration walked past" pattern is
documented explicitly so a future session can pattern-match
on it (token migrations need a checklist of every concrete
artifact downstream of the tokens, not just the tokens
themselves).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The user noticed the bottom-right large suit glyphs were
rendering upside-down — point-up hearts, stem-up spades — because
the SVG transform pipeline applied a `rotate(180)` to match the
traditional playing-card inverted-corner convention.
That convention exists so a card reads correctly when flipped or
read from the opposite side of the table. Single-orientation
digital play doesn't benefit from it; most modern digital decks
have abandoned it. User preference is upright.
Drops the rotate from face_svg's bottom-right `<g transform>`
and adjusts the translate so the visible glyph still lands at
(178, 286)–(242, 350) — same screen footprint, same scale, just
no flip.
design-system.md § Game Cards updated in lockstep — line 220
no longer says "rotated 180°", instead documents the deliberate
deviation from the traditional convention.
Knock-on lockstep changes in this commit:
- EXPECTED in tests/card_face_svg_pin.rs rebaselined: 52 face
hashes shift, 5 back hashes unchanged.
- assets/cards/faces/*.png regenerated (52 face PNGs).
- solitaire_engine/assets/themes/default/*_*.svg regenerated
(52 theme face SVGs that production rasterises at startup).
Workspace clippy + cargo test --workspace clean. Pin test
passes against the new hashes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The user's first post-migration screenshot showed near-invisible
suit glyphs on every card — the rank rendered at correct size but
the ♠ ♥ ♦ ♣ marks were tiny dots regardless of the requested
20px / 64px font-size.
Root cause: the bundled FiraMono in svg_loader::shared_fontdb
doesn't carry usable Unicode suit glyphs (U+2660-2666). usvg
silently fell back to a substitute rendering at default size,
producing the "tofu" effect.
Fixes by replacing the `<text>` glyph rendering with inline SVG
paths. `suit_path_d(suit)` returns a single closed-perimeter path
authored in a 32 × 32 logical box, then face_svg wraps it in two
`<g transform>` blocks (top-left small + bottom-right rotated
large). Path-based rendering bypasses the font system entirely
— same bytes on every machine, no fontdb dependency, no
substitution risk.
Same path data renders correctly whether filled (♥ ♠) or
outlined (♦ ♣ — the always-on color-blind glyph differentiation
from the design system).
Knock-on changes that must land in this commit per the migration
plan's lockstep rule:
- `EXPECTED` in tests/card_face_svg_pin.rs rebaselined: 52 face
hashes change (text → path), 5 back hashes unchanged
(back_svg untouched). The bootstrap pattern in the test
handled the rebaseline cleanly — empty EXPECTED, re-run,
paste, re-run.
- assets/cards/faces/*.png regenerated (the 52 face PNGs).
- solitaire_engine/assets/themes/default/*_*.svg regenerated
(the 52 theme face SVGs that production rasterises at
startup). Both rendering paths must agree.
Workspace clippy + cargo test --workspace clean. Pin test
passes against the new hashes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The post-Option-D screenshot showed the left-anchored HUD column
("Score: 0 Moves: 0 0:00") and the right-anchored action button
row colliding mid-screen at portrait/narrow window widths. Both
were absolute-positioned siblings without a shared flex parent,
so Bevy 0.15's UI couldn't auto-arrange them when their natural
widths exceeded the available horizontal space.
The action button text was a hardcoded `font_size: 16.0` literal
— a miss from the typography migration audit, since every other
text element in `hud_plugin.rs` already routes through the
`TYPE_*` tokens. Switching to `TYPE_BODY` (14.0) brings the
button row in line with the design system *and* trims roughly
12% off label widths.
Pairs with a horizontal-padding cut from VAL_SPACE_3 to
VAL_SPACE_2: 8px less on each side, six buttons, ~96px total
reclaimed across the row. Vertical padding stays at VAL_SPACE_2
so button height tracks the rest of the chrome band.
Combined effect: the action button row narrows by ~150-200px,
which is enough margin to clear typical portrait window widths
without requiring a structural refactor (a shared SpaceBetween
flex parent for HUD+actions would be more robust but touches
many query sites and was out of scope for the visual-polish
pass).
cargo clippy + cargo test --workspace clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The post-Option-D screenshot showed Terminal cards correctly but
a green felt play surface — the chrome migration only retuned
in-engine constants, leaving the on-disk PNGs at
assets/backgrounds/bg_*.png as the legacy felt textures.
Adds solitaire_engine/examples/background_generator.rs following
the same regeneratable pattern as card_face_generator. Five solid
near-black variants from the base16-eighties palette:
- bg_0: #151515 (Terminal canonical, BG_PRIMARY)
- bg_1: #0a0a0a (BG_DEEPEST)
- bg_2: #1a1a1a (BG_ELEVATED — same as card face)
- bg_3: #121820 (slight cool tint)
- bg_4: #201812 (slight warm tint)
Per design-system.md the Terminal play surface is *flat* — no
felt, no gradient — so all 5 slots are pure solid colours. Each
PNG is 120 × 168 (matches the legacy tile size; spawn_background
stretches to window_size * 2.0 at runtime so source resolution
is immaterial). On-disk weight drops from ~16KB average to ~100
bytes per tile.
Run with: cargo run --example background_generator --release
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 4's PNG regeneration left the cards looking unchanged at
runtime because the PNGs at assets/cards/ are only the *fallback*
art — production renders the bundled-default theme's SVGs, which
get include_bytes!()-embedded into the binary by
solitaire_engine::assets::sources and applied to CardImageSet at
startup by theme::plugin::apply_theme_to_card_image_set. Those
SVGs were still the legacy vector-playing-cards art.
Extends card_face_generator to write SVGs into both runtime
paths in lockstep:
1. assets/cards/{faces,backs}/*.png — fallback art (unchanged
from step 4).
2. solitaire_engine/assets/themes/default/*.svg — what production
actually renders. 52 face SVGs + 1 back SVG, generated from
the same face_svg / back_svg builders as the PNGs so the two
paths can never visually diverge.
Adds two helper functions to card_face_svg:
- theme_suit_token (clubs/diamonds/hearts/spades — lowercase
full word, matching CardKey::manifest_name)
- theme_rank_token (ace/2..10/jack/queen/king — same)
The theme back uses BACK_ACCENTS[0] (canonical Terminal cyan).
The other four accents only live as PNG fallbacks because the
theme system carries one back per theme.
Net SVG diff: -14884 / +940 lines — the legacy vector-playing-
cards SVGs were ~300 lines each of Inkscape-authored paths;
the Terminal SVGs are ~10 lines of programmatic output.
Workspace clippy + cargo test --workspace clean. Pin test
unaffected (the SVG builders themselves did not change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 4+5 lockstep commit closing Option D from SESSION_HANDOFF.
The 52 face PNGs + 5 back PNGs in assets/cards/ are regenerated
to the Terminal-aesthetic artwork emitted by the
card_face_generator example (#1a1a1a face, #fb9fb1 / #d0d0d0
suit glyphs, scanline-pattern backs with palette-rotated badge
accents). Resolution drops from 512×768 to 256×384 — sufficient
for ~250 px-wide desktop sprites and ~⅓ the on-disk weight.
Constant fallback path migrated in lockstep so the
constant-fallback tests (under MinimalPlugins) and the PNG path
(production) agree at every commit boundary:
- CARD_FACE_COLOUR → #1a1a1a (was off-white #fafaf2)
- RED_SUIT_COLOUR → #fb9fb1 (was #c71f26)
- BLACK_SUIT_COLOUR → #d0d0d0 (was #141414)
- CARD_FACE_COLOUR_RED_CBM → renamed to RED_SUIT_COLOUR_CBM,
value #6fc2ef (was #d9ebff). Semantic shift: pre-Terminal
this was a face-background tint, now it's a suit-glyph
colour swap. The Terminal face is uniformly CARD_FACE_COLOUR
regardless of CBM; CBM only swaps red suits to cyan in the
glyph itself.
- card_back_colour() → returns the 5 base16-eighties accents
matching card_face_svg::BACK_ACCENTS in lockstep, so the
test-fallback back is the same hue family as the on-disk
PNG art for that index.
Function signatures shift to follow the semantic move:
- text_colour gains a color_blind: bool parameter (returns
RED_SUIT_COLOUR_CBM for red+CBM).
- face_colour deleted entirely. The face is uniform
CARD_FACE_COLOUR; card_sprite inlines the constant. CBM
parameter dropped from card_sprite as a knock-on.
Test updates land in this commit per the migration plan:
- text_colour_is_red_for_hearts_and_diamonds + sibling: pass
`, false` to text_colour calls now that the signature has
the CBM bool.
- 4 face_colour CBM tests replaced with 2 text_colour CBM
tests asserting (a) red-suit cards swap to cyan in CBM and
(b) black-suit cards do not change.
Engine test count: 747 → 745 (net -2 from the test
consolidation — 4 face_colour tests collapsed into 2
text_colour CBM tests).
Sign-off criteria: a human still needs to `cargo run -p
solitaire_app` and confirm Terminal cards render. clippy +
cargo test --workspace clean as of this commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 3 of the migration plan in docs/ui-mockups/card-face-migration.md.
Extracts face_svg / back_svg + palette constants from the
card_face_generator example into a new
solitaire_engine::assets::card_face_svg module so an integration
test can call them. The example becomes a thin wrapper.
The new tests/card_face_svg_pin.rs hashes the raw RGBA8 pixel
bytes from rasterising every face × suit + every back accent and
compares each FNV-1a fingerprint against an embedded constant.
Catches silent rendering drift if usvg / resvg / tiny_skia / the
bundled FiraMono ever change in a way that perturbs pixels.
Hashing is FNV-1a inline (~5 lines) rather than adding sha2 or
blake3 — cryptographic strength isn't load-bearing here, just
stable byte fingerprints.
When the SVG builders intentionally change, empty EXPECTED to
`&[]` and re-run the test once; it panics with the new hashes
formatted as Rust source ready to paste back in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Generates 52 face PNGs (4 suits × 13 ranks) + 5 back PNGs
into assets/cards/. Implements step 2 of the migration plan
in docs/ui-mockups/card-face-migration.md — the bytes this
emits are what step 4 commits alongside the card_plugin
constant migration.
Filled vs outlined glyphs (♥♠ filled; ♦♣ outlined) implement
the always-on color-blind glyph differentiation from the
design system. The 5 back themes share the canonical
Terminal scanline pattern but rotate the badge accent
through the base16-eighties palette so all 5 slots stay
distinguishable without leaving the palette.
Run with: cargo run --example card_face_generator --release
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rasterises one Ace of Spades to /tmp/ace_spades_terminal.png via
the existing usvg + resvg + tiny_skia stack already used by
svg_loader. Proves the per-card grain works before looping over
all 52 faces + 5 backs in step 2 of the migration plan.
Run with: cargo run --example card_face_poc --release
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lays out the lockstep migration from legacy white-card PNGs +
constants to the Terminal aesthetic. Steps 4 + 5 (artwork +
constant + test updates) must land in one commit so the PNG
path and the constant-fallback path don't visually diverge.
Tracks Option D from the SESSION_HANDOFF Resume prompt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bookkeeping pass after 29136d8 (cursor pulse) and a27cf5a (scanline
overlay) shipped — both halves of the splash polish arc deferred in
cacb19c are now done.
Changes:
- "Since the v0.20.0 cut": added per-commit narratives for 29136d8
and a27cf5a with the implementation notes worth preserving past
the commit log:
- The "multiply, don't override" pattern that resolved the
cursor-pulse / fade-timeline conflict (and generalises to any
two ECS systems writing the same per-frame component).
- The texture-α × tint-α GPU-composite trick that integrates the
scanline with the fade without a new "multiplicative fadable"
abstraction.
- Two Bevy 0.18 API surprises (RenderAssetUsages module move;
pixel_size() returning Result) — pinned for next time we touch
runtime-generated images.
- The defensive period <= 0.0 guard on cursor_pulse_factor — a
cheap NaN-prevention pattern worth mirroring on every trig
helper.
- "Open punch list" → "Visual-identity follow-ups": collapsed the
two splash-polish bullets into closed pointers.
- Resume prompt → Option B: marked closed with "no further splash
work pending unless a new mockup detail surfaces" so a future
session knows it's a finished arc, not an in-flight one.
Three options now closed (A, B, C); D / E / F remain — all three
have a real blocker (D = multi-session, E = artwork PNGs missing,
F = Android hardware/AVD) so the next session starts with a
genuine commitment-vs-blocker decision rather than picking the
smallest piece.
Closes the second half of the splash polish arc deferred in cacb19c.
A fullscreen ImageNode tiles a runtime-generated 2×2 RGBA8 texture
over the splash content — top row transparent, bottom row #1a1a1a
at ~30 % alpha — producing the 1 px-pitch horizontal scanline
pattern called for in docs/ui-mockups/splash-mobile.html.
Implementation:
- New build_scanline_image() pure helper returns the 2×2 source
texture. Pixels hard-coded as RGBA bytes (0,0,0,0 / 26,26,26,76)
so the visible appearance is locked into source rather than
reconstructed from constants.
- spawn_splash gains an `Option<ResMut<Assets<Image>>>` parameter;
when present (always in production), the image is added and an
ImageNode child of the splash root tiles it via
NodeImageMode::Tiled { tile_x: true, tile_y: true, stretch_value: 1.0 }.
When absent (legacy bare-MinimalPlugins tests), the overlay is
silently skipped — the rest of the splash still spawns.
- New SplashFadableImage marker + extension to advance_splash that
writes (1, 1, 1, global_alpha) into the ImageNode tint each tick.
Multiplying (rather than overwriting like SplashFadableBg does)
preserves the per-pixel 30 % alpha in the texture so the GPU
composite is `0.3 × global_alpha` — fades cleanly with the
splash without drifting to 100 % alpha during the hold.
- New SplashScanlineOverlay marker for tests. Distinct from
SplashFadableImage so the test query intent stays explicit
(there's only one fadable image today, but adding more later
shouldn't break the scanline-locator).
Bevy 0.18 API quirks worth pinning for next time: RenderAssetUsages
is re-exported under `bevy::asset::` (not `bevy::render::render_asset`),
and TextureFormat::pixel_size() returns Result<usize, _> rather
than usize. Both fixed in the imports / debug_assert.
Headless test fixture now also init_resource::<Assets<Image>>()
since MinimalPlugins doesn't pull AssetPlugin — same pattern
settings_plugin's tests already use.
Two new tests (1183 → 1185): build_scanline_image_has_expected_2x2_rgba_bytes
locks the texture pixels literally, scanline_overlay_spawns_and_fades_with_splash
asserts spawn placement under SplashRoot and the new fade-images
branch's correctness end-to-end.
This closes Option B from the SESSION_HANDOFF Resume prompt — both
splash polish pieces (cursor pulse + scanline overlay) shipped.
Closes the cursor-pulse half of the splash polish arc deferred in
cacb19c. The "▌ ready_" boot-log line now ends with a 6×12 px cyan
Node that pulses on a 1 s sine cadence — matching the mockup at
docs/ui-mockups/splash-mobile.html. The pulse alpha is multiplied
with the global splash fade timeline rather than fighting it: the
cursor can't reach full alpha while the rest of the splash is still
fading in, and it fades out cleanly with everything else.
Implementation:
- New SplashCursorPulse marker on the trailing Node. Carries
SplashFadableBg too so it picks up the global fade for free; the
pulse system overwrites the per-tick BackgroundColor afterward
(last writer wins, both values are commensurate so the override
is correct, not a fight).
- New pulse_splash_cursor system, scheduled .chain()'d AFTER
advance_splash so the pulse multiplication is the final write.
No-op when no SplashRoot exists (post-despawn or under a test
fixture without one).
- New pure helper cursor_pulse_factor(age, period, min) returns a
sine-driven multiplier in [min..1.0]. Defensive zero/negative
period guard returns 1.0 so a misconfiguration produces a
steady cursor instead of a divide-by-zero NaN.
- Two splash-local consts: MOTION_PULSE_PERIOD_SECS = 1.0 (terminal-
blink cadence) and PULSE_ALPHA_MIN = 0.4 (the cursor never fully
extinguishes — matches a real terminal's blink that dips but
stays visible).
Used Node-with-explicit-dimensions rather than a `█` text glyph so
the 6×12 px size doesn't drift with line font; the leading `▌`
glyph stays a character (textual) while the trailing pulse is a
Node (geometric) — different primitives for different intents.
One new test (1182 → 1183): cursor_pulse_factor_corners pins the
peak (factor = 1 at age = period/4), trough (factor = min at age =
period * 3/4), and the defensive zero/negative-period guard.
Scanline overlay (the other half of cacb19c's skipped polish)
remains open — separate commit.
Bookkeeping pass after 54005d5 (GAME #YYYY-DDD caption) and e080b49
(MOVE N/M chip restyle) shipped — both pieces of the Option C
banner-local enrichments arc are now done.
Changes:
- "Since the v0.20.0 cut": added per-commit narratives for 54005d5
and e080b49 with the implementation notes worth preserving past
the commit log (the BANNER_HEIGHT 48→60 bump rationale, the Bevy
0.18 BorderColor::all() correction, the "marker on the leaf, not
the wrapper" ECS-design choice).
- "Open punch list" → "Replay-overlay enrichments beyond the scrub
bar": pivoted from "tractable banner additions still open" to
"all banner-local pieces shipped; remaining are cross-plugin or
multi-session". Reflects current state without erasing the
forward-looking work.
- Resume prompt → Option C: marked closed with a forward pointer to
the cross-plugin/multi-session items that should get their own
decision tree next time.
- Resume prompt → test count: dropped the hardcoded "1180 tests
pass" (already stale at 1182) for "~1180+; check with
`cargo test --workspace`" — same dynamic-reference pattern as
44f5972's commit-count fix, applied to the next aggregate that
was vulnerable to it.
Closes the centre-text half of the replay-overlay enrichments arc.
The plain "Move N of M" text becomes a 1px ACCENT_PRIMARY-bordered
chip containing "MOVE N/M" — uppercase + slash separator reads as a
Terminal output line and matches the floating-chip motif in
docs/ui-mockups/replay-overlay-mobile.html. The chip lives in-banner
rather than floating above the focused card; the screen-takeover
treatment that requires plumbing cursor → card identity remains
deferred per SESSION_HANDOFF.
Implementation: the centre Text spawn is now wrapped in a Node with
1px border + axes(VAL_SPACE_2, VAL_SPACE_1) padding and no background
fill (Terminal aesthetic gets depth from borders + tonal layering,
not shadows). The ReplayOverlayProgressText marker stays on the
inner Text so update_progress_text continues to repaint contents
unchanged. format_progress now returns "MOVE N/M" for Playing and
"REPLAY COMPLETE" for Completed (uppercase to match the chip's
typographic treatment); Inactive still returns "" since the overlay
shouldn't be spawned in that state.
Used BorderColor::all(ACCENT_PRIMARY) — Bevy's BorderColor is per-side
in 0.18, no longer the tuple struct it was earlier.
Module-level docstring + ReplayOverlayScrubFill doc comment both
updated to quote the new "MOVE N/M" string. Test
overlay_progress_text_reflects_cursor swapped its assertion to match.
1182 tests still pass; clippy clean.
This closes Option C from the SESSION_HANDOFF Resume prompt's banner-
local enrichments. The full screen-takeover redesign (mini-tableau,
playback controls, move-log scroll, WIN MOVE marker requiring a
win_move_index field on Replay) remains the multi-session item.
Adds the right-anchored game-identifier piece of the replay-overlay
mockup (docs/ui-mockups/replay-overlay-mobile.html), adapted to live
under the existing "▌ replay" headline rather than as a separate
top-bar surface — the screen-takeover redesign is intentionally
deferred per the SESSION_HANDOFF punch list.
The caption reads `GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122`
for a replay recorded 2026-05-02), matching the mockup's
`GAME #2024-127` motif. Year + chrono ordinal gives a compact,
monotonically-increasing identifier that's grep-friendly across
replay files. TYPE_CAPTION (11 px) / TEXT_SECONDARY paint so the
caption reads as subordinate metadata, not a callout.
Implementation: new ReplayOverlayGameCaption marker, new pure
helper `format_game_caption(state) -> Option<String>` (None for
Inactive / Completed since the replay is consumed in those branches),
left-side label spawn restructured into a column container holding
the headline + caption with a 2 px row gap. BANNER_HEIGHT bumped
48 → 60 px so the column fits without overflow (16 px vertical
padding + 1 px scrub + ~39 px content; +12 px banner mass is the
deliberate cost of the new content).
Two new tests (1180 → 1182): format_game_caption_covers_state_corners
pins the three branches (Inactive / Completed / Playing) plus the
zero-pad-to-3-digits invariant for early-January ordinals; and
overlay_game_caption_shows_replay_date drives ReplayPlaybackState
end-to-end and asserts the caption text on spawn and that the
overlay stays spawned through Playing → Completed.
MOVE chip restyle from the same mockup is the next commit.
The "4 commits ahead" / explicit-HEAD-SHA lines in SESSION_HANDOFF.md
were stale the moment 13ae160 (the prior touch-up commit) landed —
docs that count themselves are a recursion trap. Replaced four sites
with pointers to `git log --oneline origin/master..HEAD` and
`git rev-parse HEAD` so future docs-only edits don't immediately
stale the handoff.
Sites updated:
- "Last updated:" preamble.
- "Status at pause" → HEAD locally + ahead-count bullets.
- "Canonical remote" → push reminder.
- Resume prompt → branch state.
The narrative entries under "Since the v0.20.0 cut" still name SHAs
explicitly because those *are* the per-commit anchors readers grep
against; only the rolling totals were brittle.
Pure docs; no code changes, no test impact.
Three small clarity touch-ups to SESSION_HANDOFF.md so a future-session
reader doesn't have to reconstruct intent from git log alone:
- The c84d9f4 narrative listed "header text treatment" as still open;
6204db8 closed it the same session. Added a parenthetical pointer.
- The cacb19c "Skipped" sub-section now cross-links each item to its
follow-up status in the punch list (scanline + cursor pulse → still
open; "RUSTY SOLITAIRE" wordmark → closed, the in-engine wordmark
stays "Solitaire Quest").
- 6204db8 adopted "▌ replay" instead of the mockup's literal
"▌replay.tsx" — the .tsx was a Stitch/React prototyping leak.
Documented the deviation alongside the existing RUSTY SOLITAIRE
precedent so the in-engine string isn't second-guessed later.
Pure docs; no code changes, no test impact.
The prior handoff (f2d2119) was written when [Unreleased] was
accumulating v0.20 candidates. v0.20.0 is now cut at 41a009a and
tagged; four post-cut commits sit on top locally — 39b8496
desktop-adaptation spec, cacb19c splash boot-screen port, c84d9f4
replay-overlay scrub bar finish, 6204db8 replay banner ▌ cursor-
block label — none yet pushed. Working tree is clean.
Rewrites the handoff to:
- Distinguish local-master (6204db8) from origin-master (41a009a)
so the next session doesn't assume git push has happened.
- Document each of the four post-cut commits in its own subsection
under "Since the v0.20.0 cut" — the cycle is closed; these are
early entries in whatever cuts next.
- Name docs/ui-mockups/desktop-adaptation.md as the canonical
geometry reference for future plugin ports — applies to every
screen including the 8 still-unported missing-plugin surfaces.
- Note the Stitch generate_variants reliability issue
(timed out on layout-only adaptation prompts) so a future
session reaches for generate_screen_from_text instead.
- Refresh the SplashFadable scaffolding pattern to the process
notes (introduced in cacb19c) — the reusable shape for any
future overlay that fades N >> 3 elements together.
- Refresh the Resume Prompt's A–F options: push / v0.20.1 cut
decision (A), splash polish (B), replay-overlay enrichments
beyond the scrub bar (C), card artwork regeneration (D), app
icon round (E), APK launch verification + JNI bridges (F).
Tests: 1180 passing / 0 failing. Build clippy-clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aligns the replay overlay's headline with the splash boot-screen idiom
landed in cacb19c — the cursor block (`▌`, U+258C) prefixed to a
lowercased label reads as a Terminal output line rather than a
generic UI title. "Replay" → "▌ replay" and "Replay complete" →
"▌ replay complete" in both the spawn-time path and the per-frame
update_banner_label updater. Doc comments that quote the literal
strings updated in lockstep so the next reader doesn't grep for an
absent literal.
Tests adjusted to match (banner_text assertions in
overlay_spawns_when_playback_starts and overlay_text_changes_on_completed).
The existing 1178 unit tests still pass; clippy clean.
Move-log scroll, MOVE chip, and WIN MOVE callout from the same mockup
remain open — separate commits.
Closes the spawn-time half of the replay-overlay redesign open in
SESSION_HANDOFF.md by adding the 1px cyan scrub bar called for in
docs/ui-mockups/replay-overlay-mobile.html. A track in BORDER_SUBTLE
spans the bottom edge of the banner and the cyan ACCENT_PRIMARY fill
mirrors cursor / total via a new ReplayOverlayScrubFill component +
update_scrub_fill system. The pure scrub_pct helper is shared between
the spawn path (initial fill width) and the per-frame updater so the
first paint already reflects state instead of popping 0 → cursor on
the first tick — same shape as the existing format_progress /
update_progress_text split.
Two new tests (1176 → 1178): scrub_pct_covers_state_corners pins the
helper's four corners (Inactive / cursor=0 / midpoint / Completed) and
overlay_scrub_fill_tracks_cursor drives ReplayPlaybackState end-to-end
and asserts Node.width on the unique scrub-fill entity. Same change-
detection guard as the text updaters, so an idle replay leaves the
node untouched.
Header text treatment, move-log scroll, MOVE chip, and WIN MOVE callout
from the same mockup are still open — separate commits.
Implements the full mockup-spec splash from
docs/ui-mockups/splash-mobile.html plus the desktop adaptation rules
from docs/ui-mockups/desktop-adaptation.md. The header (cursor block,
wordmark, divider, "TERMINAL EDITION" subtitle), boot log (three
✓ check rows + "▌ ready_"), progress bar (1px track with full-width
cyan fill + "DONE · 247 ASSETS" caption), and footer
(BASE16-EIGHTIES label, eight palette swatches, version) all land
together. Rules-driven sizing: boot-log column capped at 480 px on
desktop (otherwise 70 % viewport), progress bar capped at 720 px
(otherwise 80 %), per the desktop-adaptation spec.
Refactored the alpha-fade scaffold from per-marker queries
(SplashTitle / SplashSubtitle / SplashCursor) to a single
SplashFadable { base_color: Color } + SplashFadableBg variant.
~15 fadable elements now share one global query each; adding more
elements is one component-attach, not three new query types.
Skipped (each its own potential follow-up):
- Scanline overlay — needs a tiled-pattern asset or a custom
shader; both are out of scope for a UI-Node port.
- Pulsing cursor on the "ready_" line — would fight the global
fade timeline; stays static.
- "RUSTY SOLITAIRE" wordmark from the mockup — actual product is
"Solitaire Quest"; the mockup leaked the repo name.
Tests: 8 carried + 2 new (Terminal boot-screen content present;
fadables start transparent and reach full alpha).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the spec gap flagged after v0.20.0: the 24 mockups in
docs/ui-mockups/ are 23 mobile + 1 desktop, but desktop is still
the primary delivery surface. Stitch's variant generation kept
timing out on layout-only adaptation prompts, so the deterministic
fix is rules-based: a markdown spec that captures (a) the desktop
viewport assumptions, (b) seven universal adaptation rules that
apply to every screen, and (c) per-screen geometry rules for the
priority surfaces (Game Table, Win Summary, Settings, Help, Pause,
Home, Splash, Stats, Profile / Achievements / Theme Picker / Daily
Challenge).
Why rules > visual mockups for this gap:
- Apply uniformly to every screen — including the 9 missing-plugin
surfaces (splash, challenge, time-attack, weekly-goals, leader-
board, sync, level-up, replay-overlay, radial-menu) that have
only mobile mockups today.
- Reference-able from code comments and commit messages without
loading an image.
- Layout-agnostic by construction: tells the engine "use percent /
flex / min(720, 50%) widths" instead of pinning a specific
desktop pixel layout.
- Cheaper than re-running Stitch generation per screen, which is
flaky for layout-only adaptation work.
Cross-check confirms that v0.20.0's port (modal scaffold, toasts,
table chrome, card chrome, gameplay-feedback, splash cursor) is
already layout-agnostic — the spec gap mattered for *next* ports,
not the work that just shipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promotes the [Unreleased] section to [0.20.0] dated 2026-05-07
and opens a fresh empty [Unreleased]. The cycle's two through-
lines:
- **Terminal visual-identity port.** ui_theme token system
(0d477ac) is load-bearing; downstream chrome migrations cover
the modal scaffold, gameplay-feedback layer (ceec4fc), toasts
with a new ToastVariant enum (a137607), table chrome (651f406),
card chrome (d752870), splash cursor (cdcadda), and final
hint-source / dest pairing (9891ae4). Card-face / suit / card-
back palette intentionally NOT migrated — those track PNG
artwork that hasn't been regenerated yet. The 24 Stitch-rendered
mockups and design-system.md spec landed in fa7f98a.
- **Android persistence shim.** solitaire_data::data_dir
routes through a per-platform shim (4b51e50) closing the
CLAUDE.md §10 dirs::data_dir() = None pitfall on Android.
Settings, stats, achievements, replays, game-state, time-attack
sessions, and user themes now persist on a real APK.
Also closes three v0.19.0 punch-list candidates that landed
earlier in the cycle (pull_failure flake at 67c150b, smart-window-
size opt-out at e1b8766, Shareable badge at 9b065e5).
Tests: 1176 passing / 0 failing (six new this cycle: ui_theme
invariant guards, toast-variant-border-mapping, palette-tracking
guards on MARKER_VALID / HINT_PILE_HIGHLIGHT_COLOUR /
RIGHT_CLICK_HIGHLIGHT_COLOUR / toast-border distinctness).
SESSION_HANDOFF.md refreshed: HEAD pointer, test count, the
v0.20.0 changelog summary, the open punch list (Phase Android
runtime gaps, visual-identity follow-ups including the artwork
regeneration item), the updated design-direction box (was
Midnight Purple + Balatro yellow; now base16-eighties Terminal),
and a refreshed Resume Prompt offering A–F next-step options.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the spec the recent visual-identity port pass referenced:
- design-system.md — base16-eighties palette, type scale, spacing
scale, motion budget, component library, accessibility notes
(color-blind toggle, high-contrast mode, glyph differentiation),
and the canonical "Terminal" card-back theme.
- 24 Stitch-rendered mockups (HTML + PNG): 12 redesigned existing
screens, 1 desktop home variant, 2 onboarding steps, and 9
missing-plugin screens (splash, challenge, time-attack,
weekly-goals, leaderboard, sync, level-up, replay, radial-menu).
These mockups are the source the engine plugins were ported
against in commits 0d477ac through 9891ae4 (token system,
modal scaffold, gameplay-feedback layer, toasts, table chrome,
card chrome, splash cursor, hint highlight). Future plugin work
should diff against the matching mockup before touching pixels.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- input_plugin's hint-source card tint moves from raw bright-yellow
`srgba(1.0, 1.0, 0.4, 1.0)` to the design-system STATE_WARNING
token, so the source card and the destination pile (which already
uses STATE_WARNING via HINT_PILE_HIGHLIGHT_COLOUR) wear the same
attention colour as a coherent pair.
- replay_overlay had two stale doc comments referencing the old
"loud yellow accent" — Primary is now cyan (ACCENT_PRIMARY).
Comments updated; no behaviour change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Splash now renders the design system's signature `▌` cyan terminal-
cursor glyph (96px) above the wordmark, matching docs/ui-mockups/
splash-mobile.html. The cursor uses ACCENT_PRIMARY and fades on the
same per-frame alpha schedule as the title and subtitle so the
brand beat still dissolves as a single layer.
Did NOT pull in the mockup's full boot-loader treatment (scanline
overlay, ✓ check log lines, progress bar, ROOT@SOLITAIRE prompt) —
those are aesthetic features that warrant their own commit, not
this token-port pass. The splash already consumed every relevant
ui_theme token; the cursor glyph is the single highest-signal
visual element the spec called for.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drag-elevation shadow now sources its colour from CARD_SHADOW_COLOR
+ CARD_SHADOW_ALPHA_DRAG, so the Terminal "no box-shadow" policy
disables the stack shadow in lockstep with the per-card shadows.
Re-enabling shadows for a future palette swap is now a one-line
edit in ui_theme, not a hunt across plugins.
- RIGHT_CLICK_HIGHLIGHT_COLOUR retuned from raw `srgba(0.2, 0.8, 0.2, 0.6)`
to STATE_SUCCESS's RGB at 60% alpha. Spelled as a literal because
Alpha::with_alpha isn't const on stable; a new test pins the RGB
to STATE_SUCCESS so a palette swap can't drift the two apart.
- Drop the duplicated PILE_MARKER_DEFAULT_COLOUR const — import the
promoted const from table_plugin instead. STOCK_NORMAL_COLOUR is
now an alias of that const so all idle pile-marker tints track a
single source of truth.
- Stock recycle "↺" text changed from raw `srgba(1.0, 1.0, 1.0, 0.7)`
to TEXT_PRIMARY at 0.7 alpha, picking up the off-white foreground
used elsewhere in the Terminal UI.
Card-face / suit / card-back palette constants are intentionally
NOT migrated: the runtime path renders PNG artwork that's still on
the previous "white card" palette, so swapping the fallback
constants ahead of artwork regeneration would mix two visual
systems for any code path where image loading fails.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Commit 0d477ac (the Terminal token system) pinned both
CARD_SHADOW_ALPHA_IDLE and CARD_SHADOW_ALPHA_DRAG to 0.0 because the
Terminal design system explicitly disallows box-shadow ("depth via
1px borders and tonal layering"). The existing invariant
\`drag_alpha > idle_alpha\` then fails — \`0.0 > 0.0\` is false.
Loosen the assertion to \`drag_alpha >= idle_alpha\` and document the
intent: under Terminal both are 0; under any future palette that
re-enables shadows, drag still must not be weaker than idle. The
useful regression-guard (catching an accidental swap of the two
constants) is preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Promote `marker_colour` to module-level const PILE_MARKER_DEFAULT_COLOUR
and re-export it. cursor_plugin::MARKER_DEFAULT now imports the const
directly, replacing the prior duplicated literal kept in sync only by
doc comment. Drift becomes a compile error instead of a stale claim.
- Empty-tableau "K" placeholder text now uses TEXT_PRIMARY at 0.35 alpha
(was raw `Color::srgba(1.0, 1.0, 1.0, 0.35)`) so it picks up the
Terminal off-white foreground.
- HINT_PILE_HIGHLIGHT_COLOUR retuned from bright `srgb(1.0, 0.85, 0.1)`
to the design-system STATE_WARNING token (`#ddb26f`). Spelled as a
literal because Alpha::with_alpha is not yet const on stable; a new
test pins the RGB to STATE_WARNING so a palette swap can't drift the
two apart silently.
- The existing "is gold" character test was hardcoded to the old bright
palette (red ≥ 0.9). Loosened to "warmer than cool" + ranges that the
Terminal muted gold satisfies, with exact-RGB tracking handled by the
new STATE_WARNING test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Toasts now follow `docs/ui-mockups/design-system.md`:
- Bottom-anchored absolute position (was top / mid-screen)
- Opaque BG_ELEVATED fill (was translucent black-at-alpha)
- 1px accent border keyed off a new ToastVariant enum
- TYPE_BODY_LG caption (was 22 / 32 px literals)
- RADIUS_MD corners
ToastVariant exposes Info / Warning / Error / Celebration, each
mapped to its design-system token via border_color(). Variants are
threaded through every spawn_toast call site:
- Achievement / Level-up / XP / Daily / Weekly / Challenge → Celebration
- Goal-announcement / Time-attack / Settings volume / Auto-complete → Info
Queued banner and fire-and-forget toasts use slightly different
bottom anchors (6% vs. 14%) so a celebration toast spawned in the
same frame as a queued info banner layers above it instead of
overlapping. Two new tests pin variant→border mapping to the
design tokens and require all four borders to be visually distinct.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Selection-highlight tints in selection_plugin and the valid-drop
marker tint in cursor_plugin were hand-tuned RGB literals from the
prior Premium-Solitaire palette. Migrate them to the semantic
state tokens introduced in ui_theme:
- keyboard-drag source highlight (picking) → ACCENT_PRIMARY
- keyboard-drag source highlight (lifted) → STATE_WARNING
- keyboard-drag destination highlight → STATE_SUCCESS
- cursor_plugin::MARKER_VALID → STATE_SUCCESS @ 0.55α
`MARKER_VALID` stays a Color literal (Alpha::with_alpha is not yet
const on stable); a new tracking test pins its RGB to STATE_SUCCESS
so a future palette swap can't drift the two apart silently.
Also fix three stale doc comments in ui_modal that still described
the previous yellow / magenta palette ("Loud yellow CTA",
"Primary swaps to the magenta secondary accent"). Cyan and lavender
now, matching the actual token values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the prior Premium-Solitaire palette and ad-hoc constants
with the full Terminal (base16-eighties) token set: near-black
surface ramp, cyan primary CTA, lime/lavender/gold/teal/pink
semantic accents, 5-rung type scale, 7-rung 4-multiple spacing
scale, 3-step radius, 14-rung z-index hierarchy, and a complete
motion budget. Card drop-shadow alphas pinned to 0 — Terminal
depth is 1px borders + tonal layering, not box-shadow.
Tokens stay as `pub const` so static contexts (default Sprite
colours etc.) keep compiling; a future UiTheme resource can layer
runtime switching on top without breaking the constant API. Four
unit tests pin the spacing/type/z-index invariants so a careless
edit can't silently break the scale. Plugin-by-plugin migration
to consume these tokens follows in subsequent commits.
Spec: docs/ui-mockups/design-system.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dirs::data_dir() returns None on Android, which silently disabled
every persistence path (settings, stats, achievements, replays,
game-state, time-attack sessions, user themes). New
solitaire_data::platform::data_dir() shim falls through to
dirs::data_dir() on desktop and returns the per-app sandbox at
/data/data/com.solitairequest.app/files on Android — no JNI needed,
since the package id is pinned in
[package.metadata.android].
CLAUDE.md §10 already flagged this as a known pitfall; the shim
pays it down at the one chokepoint instead of per feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The v0.19.0 handoff had drifted material across seven commits:
HEAD pointer was wrong (still claimed 6037596; actually 59424a3),
"Tags on origin" still claimed v0.19.0 wasn't pushed, the
known-flake list still mentioned `pull_failure_sets_error_status`
(fixed in 67c150b), and three of four v0.19.0 punch-list
"candidates" had silently shipped without the doc tracking it.
The Android build target landing in fb8b2ac wasn't mentioned at
all despite being the largest single change in the cycle.
CHANGELOG [Unreleased] populated with all seven commits grouped
into Added / Fixed:
Added:
- Android build target — first working APK (fb8b2ac)
- Android developer setup + build runbook (59424a3)
- F3 FPS / frame-time overlay (690e1d2)
- "Smart window size" Settings toggle (e1b8766)
- "Shareable" badge on Latest-win caption (9b065e5)
- Help: M / P / Win-Summary-Enter rows (35516d3)
Fixed:
- pull_failure_sets_error_status flake (67c150b)
SESSION_HANDOFF.md fully rewritten:
- Status section reflects HEAD 59424a3, clean working tree (apart
from this commit's docs), 1170 passing tests, no known flakes
- "Where we are" tracks v0.19.0 candidates' close status (3 of 4
shipped, App icon still open and now blocked on a re-export)
- New v0.20 candidates table covers all seven commits
- New "Phase Android" punch-list section captures the unblocked-
by-fb8b2ac work: APK launch verification, dirs::data_dir port,
JNI ClipboardManager, Android Keystore, gpgs integration, the
cosmetic cargo-apk panic workaround
- Process notes call out the async-test starvation pattern
(seen twice now), the bin→lib+shim refactor as a reusable
pattern, and target-gating-by-default for cross-platform deps
- Resume prompt's A–D menu refreshed to reflect actually-open
work: APK verification, Phase-Android persistence, app icon,
and a v0.20 cut
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
Tests: 1170 passing / 0 failing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the toolchain install for Debian 13 (the path Quat ran on
this dev box, including the JDK 21 / unzip / SDK-licence prompts),
the `cargo apk build` invocation, the cosmetic post-pass panic
workaround, and the table of what's wired vs. stubbed for the
android target. Runnable on a fresh box from a clone — no
machine-local context required.
Pairs with the workspace cfg-gating in fb8b2ac. Future Phase-Android
work (dirs::data_dir port, JNI ClipboardManager, Android Keystore,
gpgs) is listed as the not-yet-done section so a contributor can
pick it up without re-deriving the punch list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the workspace through `cargo apk build`. After this commit
`cargo apk build -p solitaire_app --target x86_64-linux-android`
produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk`
containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable
on the AVD or a physical x86_64 device.
The five gating points discovered by iterating compile cycles:
1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib`
to bundle as `libmain.so`; pure-bin crates panic with
"Bin is not compatible with Cdylib". `src/lib.rs` carries the
ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim
that delegates for the desktop `cargo run` path.
2. `[package.metadata.android]` pins target SDK 34 / min SDK 26
so cargo-apk doesn't probe for whatever default it ships
(which on this machine was an uninstalled API 30). `assets =
"../assets"` lets the same asset directory feed both desktop
and APK.
3. Workspace `bevy` features add `android-native-activity` (the
Bevy-side glue that pairs with cargo-apk's NativeActivity
wrapper). The feature is target-gated inside bevy_internal so
desktop builds compile it out.
4. `arboard` (clipboard, used by Stats's "Copy share link") has
no Android backend — `cargo apk build` fails with E0433 on
`platform::Clipboard` if unconditional. Target-gated to
`cfg(not(target_os = "android"))`; the system surfaces an
informational toast on Android until JNI ClipboardManager is
wired in the Phase-Android round.
5. `keyring` + `keyring-core` cannot compile for android — the
transitive `rpassword` uses `libc::__errno_location` which
bionic doesn't expose. Both crates target-gated; `auth_tokens`
ships a stub on Android that returns `KeychainUnavailable` for
every call, matching how callers already handle a Linux box
without Secret Service.
Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed
when it tries to also wrap the bin target. The APK on disk is
unaffected. Working around this with `cargo apk build --lib` is
the next small step.
What's verified:
- Desktop `cargo build`, `cargo clippy --workspace --all-targets`,
and `cargo test --workspace` all clean.
- `cargo apk build -p solitaire_app --target x86_64-linux-android`
produces 54 MB debug APK with libsolitaire_app.so + assets.
What's NOT yet verified:
- Whether the APK actually launches on the AVD / a phone (next
step: `adb install` + `adb logcat` against the bevy_test AVD).
- Whether `dirs::data_dir()` on Android returns a usable path
(sync / persistence will surface this if not).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Performance work for the upcoming Android port needs a numeric
baseline we can quote across desktop and mobile, instead of "feels
slow". DiagnosticsHudPlugin wraps Bevy's FrameTimeDiagnosticsPlugin
and renders a tiny corner readout the developer can toggle with F3.
- Hidden by default — production builds ship the plugin but the
overlay starts invisible.
- F3 reads ButtonInput<KeyCode> directly (not gated by pause /
modal state); diagnostics should always be reachable.
- Reads `smoothed()` FPS + frame_time so the cell isn't a jittery
per-frame scoreboard. Format: "FPS NN \u{2022} M.MM ms".
- Anchored top-right at z = Z_SPLASH + 100 so the readout sits
above every modal / toast / splash layer.
- Update system bails when hidden so we don't pay the
diagnostic-store lookup or text mutation when nobody's looking.
Next up on the perf track: get the Android build target wired so we
can put real numbers in this readout from a phone or emulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Help (F1) modal's Overlays section listed S/A/L/O but skipped
two post-v0.18 entries — M (Home / Mode launcher) and P (Profile) —
and never mentioned the recently-shipped Enter accelerator that
dismisses the Win Summary.
Help is the canonical keyboard-discovery surface. Three new rows
cover the gap so a player who opens F1 sees every overlay-toggle
key, plus the contextual Enter shortcut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Copy share link button on the Stats overlay only produces a URL
when the displayed replay has a `share_url` populated; otherwise it
surfaces a toast explaining the upload prerequisite. Players had no
way to know the button would work without clicking it.
Adds a "\u{2022} Shareable" suffix to the Latest-win caption when
the displayed replay carries a share_url, matching the format the
v0.19.0 handoff sketched ("Replay 3 / 8 \u{2022} Shareable") for
the future Prev/Next selector. The Prev/Next markers exist in
stats_plugin but no spawn site renders them today, so the live
fix is on the existing single-replay caption.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
launch sizing
Players who specifically prefer the literal 1280×800 baseline on
every fresh-install launch had no way to opt out of the v0.19.0
smart-default sizer. Adds a Gameplay-section toggle (mirrors the
"Winnable deals only" pattern) so they can flip it off.
- New `Settings::disable_smart_default_size: bool` field with
`#[serde(default)]` so legacy `settings.json` files load to the
shipped behaviour (smart sizer enabled).
- Settings panel gains a "Smart window size" row with ON/OFF label
inverting the negative flag, and a tooltip clarifying that saved
window geometry always wins over both branches.
- `solitaire_app::main` reads the flag once at startup and skips
the `apply_smart_default_window_size` registration when it's set.
Mid-session changes apply on next launch (documented on the
field).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fixed 5-update budget in `pull_failure_sets_error_status` was
the last test still subject to the AsyncComputeTaskPool starvation
mode that v0.19.0's auto-save fix already cleared. Under heavy
parallel cargo-test load, 5 updates wasn't always enough for the
failing pull task to surface its Err and flip
SyncStatusResource to Error.
Pumps updates in a loop bounded by a 5-second deadline (with
std::thread::yield_now between iterations to give the task pool a
chance to run), exiting as soon as the status flips. Mirrors the
auto-save flake fix shape — a healthy run hits the assertion in a
handful of frames, while a starved run gets the budget it needs
without hanging the suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promotes [Unreleased] to [0.19.0]. The release closes v0.18.0's
punch list (async H-key hint, persistent replay share URLs),
expands desktop platform fit (Wayland session support +
monitor-aware default window size), polishes the win-celebration
and double-click animation paths, and clears two test-flake
contributors. The Rusty Pixel pixel-art card theme arc was
prototyped and reverted in the same window — the engine plumbing
(pixel_art ThemeMeta field, PNG manifest face support, second
embedded:// theme channel) was fully reverted and is not part of
this release.
SESSION_HANDOFF.md refreshed to reflect the v0.19.0 ship:
v0.18.0 punch-list items B and D marked shipped; new Open punch
list documents the Rusty Pixel arc as historical, calls out the
desktop-packaging follow-through (app icon next), the
pull_failure_sets_error_status flake (next-round candidate),
and a settings-UI item for the smart-default-size opt-out.
Resume prompt refreshed with the post-v0.19.0 A-D decision menu.
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
Tests: 1170 passing / 0 failing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A successful double-click was rendering the slide-to-destination
animation twice — once from the first press's MoveRequestEvent
landing, and again from the release's StateChangedEvent racing the
in-flight CardAnim and replacing it from the mid-animation
position.
The frame trace:
Frame N (second press):
handle_double_click → MoveRequestEvent (queued)
start_drag → DragState set, drag.committed = false
(start_drag never mutates Transform; the
card is still visually in place)
handle_move → applies the move, fires StateChangedEvent
sync_cards_on_change → cur ≠ target, inserts CardAnim slide
(animation #1 starts)
Frames N+1, N+2, …:
follow_drag idles (drag uncommitted, cursor not moving)
CardAnim animates the card from old to new pile
Frame N+K (release):
end_drag → drag.committed = false branch:
drag.clear() + StateChangedEvent ← CULPRIT
sync_cards_on_change → sees the card mid-CardAnim
(cur ≠ target), replaces CardAnim
with a fresh one starting at the
current mid-position (animation #2
visibly restarts the slide)
The fix is one line: drop the StateChangedEvent write in the
uncommitted-drag branch of end_drag. The defensive resync was
never needed there — start_drag only mutates the DragState
resource on press, never card transforms, so an uncommitted drag
has no visual side effect to undo. The committed-drag branch (line
762) keeps its StateChangedEvent write since snap-back from a
real drag does need a resync.
Existing tests pass unchanged. The bug only manifested in the
specific timing of double-click → quick-release before
animation-complete; an integration test would require driving
mouse press/release across several frames with a dispatched
GameMutation pass between, which is heavier than the fix
warrants.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
handle_double_click had a coverage gap. The flow was:
- Priority 1: try moving the single top card to its best
destination (foundation, then tableau).
- Priority 2: if Priority 1 failed AND the player clicked the
base of a multi-card stack, try moving the whole stack.
`MoveRejectedEvent` was only fired inside the Priority 2 else-branch
— so a double-click on a single card with no legal destination
fell through both priorities silently: no card_invalid.wav, no
shake animation on the source pile, the player got zero feedback
that the click was acknowledged.
The fix collapses both priorities' failure paths into one
unconditional `MoveRejectedEvent` write at the end of the
double-click branch. Single-card miss now plays the same feedback
as multi-card-stack miss. The early `return` on each successful
move keeps the rejection branch from firing on the success path.
Pre-fix, a player double-clicking the 7♠ buried under a 6♥ on
column 5 (no foundation slot for 7s; no tableau column accepting
black 7) saw nothing happen. Post-fix, the source pile shakes
and the invalid-move sound plays, exactly like a drag-and-drop
rejection.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related platform-fit fixes for desktop launch:
1. Wayland session compatibility. The workspace Cargo.toml's
Bevy feature list previously enabled only `x11`, leaving
winit-on-Wayland to fall through to XWayland — the game
rendered inside an X11 frame stitched into the Wayland
compositor instead of as a native Wayland client. Adding
the `wayland` feature lets winit prefer Wayland when
WAYLAND_DISPLAY is set on the session, falling back to X11
when it isn't. Costs a few hundred KB of binary for the
libwayland-client bindings; comment in Cargo.toml explains
the trade.
2. Smart default window sizing. The fallback window size for
first launches (no saved geometry) was a fixed 1280x800. On
a 4K monitor that's a comparatively tiny window in one
corner; the game's cards then occupy a small physical area
even though the screen has plenty of room. New
`apply_smart_default_window_size` Update system queries
`Monitor` (with the `PrimaryMonitor` marker) and resizes the
primary window to ~70% of the monitor's *logical* size on
the first frame. Logical size already factors in the OS's
HiDPI scale factor, so:
- 1920x1080 / 1.0 scale → 1344x756 target
- 2560x1440 / 1.0 scale → 1792x1008 target
- 3840x2160 / 1.0 scale → 2688x1512 target
- 2880x1800 / 2.0 scale (Retina) → 1008x630 target
(same physical size as 1080p)
Clamped to the existing 800x600 minimum so old systems
don't get sub-minimum windows. Skipped entirely when saved
geometry was applied — the player's chosen size always
wins. Uses `Local<bool>` for one-shot semantics; the early-
exit per tick costs nothing once `*applied` is true.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>