While a drag is committed the floating bar slides below the screen
edge (UiTransform translation - no layout dirtying, no fight with the
safe-area anchor over Node::bottom) so it never occludes a drop target
on the bottom tableau rows; it slides back on release. Same tween rate
as the label reveal, snaps under reduce-motion. Desktop docked bar is
untouched (no TabBarDuck component).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New Settings::reduce_transparency_mode (serde-default, Accessibility
toggle row) swaps every GlassSurface's gradients for a flat opaque
BG_ELEVATED fill; high-contrast mode now also boosts the glass fill
opacity and rim luminance to BORDER_SUBTLE_HC levels. Both applied by
settings_plugin::update_glass_surfaces, which retargets the gradients
in place on toggle or on newly spawned glass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Touch layout only (desktop bar unchanged): a floating glass pill
(Undo / Draw / Hint / Pause) plus a detached circular Menu button,
margin above the bottom edge on top of the safe-area inset. Draw is
the persistently expanded accent pill (icon + label); other buttons
are icon-only and slide their label out while pressed, snapping under
reduce-motion. Buttons keep the existing marker components so click
handlers, tooltips, focus ring, and chrome toggle work unchanged.
Icons: None degrades to text-fallback labels (MinimalPlugins tests /
SVG regression), keeping the bar fully usable.
Toast stack clearance now derives from TAB_BAR_CLEARANCE_PX; the
legacy touch bar spawn path and primary-button metrics are removed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five white 24-box stroke icons (undo/draw/hint/pause/menu) rasterised
through the existing usvg/resvg pipeline; tinted at spawn time via
ImageNode::color. Avoids FiraMono's missing icon glyphs on Android.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layered bevy_ui treatment (sheen gradient, specular BorderGradient rim,
BoxShadow) approximating frosted glass without shaders or backdrop
sampling - WebGL2-safe by construction. Layout callers depend only on
glass_surface() + GLASS_BORDER_PX so the material can be swapped later.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
U (undo), N (new game), Z (zen), D/Space (draw) and G (forfeit) were
not gated on an active drag, so pressing one mid-drag fired a
StateChangedEvent whose card re-sync inserted a `CardAnim` on the
dragged cards — fighting `follow_drag`'s per-frame Transform writes
(jittering XY, animated Z) and leaving `DragState` origin indices
stale against the mutated state.
`handle_keyboard_core` and `handle_keyboard_forfeit` now reuse the
mutual-exclusion guard `handle_selection_keys` already documents: skip
while `DragState` is non-idle, unless it is the keyboard-lift sentinel
(`KEYBOARD_DRAG_TOUCH_ID`), whose lift is already dropped cleanly by
`clear_selection_on_state_change`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New ui_modal::spawn_empty_state — glyph + headline + optional detail,
centred, with next-step actions composed alongside rather than
configured in. Glyphs restricted to FiraMono-covered ranges (suits,
arrows, ASCII) per the section-10/11 Android constraint.
Swept every improvised surface onto it:
- Theme store: loading / error / empty-catalog branches
- Leaderboard: fetching / error / be-the-first branches
- You-hub Replays tab: dedicated no-replays state (selector and
actions no longer spawn dead controls when history is empty)
- Stats: first-launch nudge above the em-dash grid
The Account tab's sync row was deliberately NOT swept: it is a live
status + action row, not an empty state, and its deeper treatment
belongs to Phase M's sync-transparency work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase J audit findings: the visible focus ring the doc asked for
already exists (FocusOverlay singleton, 2px accent ring, breathing
pulse, reduce-motion aware — landed after the doc was written), and
post-Phase-C every modal already opens and dismisses keyboard-only.
The real gaps were two silently diverging static hotkey tables
(onboarding's slide vs Help's list — onboarding still said 'Mode
Launcher (then 1-5)') and no way to see the bindings mid-game.
- New crate::hotkeys module owns THE binding table (21 rows, verified
against a grep inventory of every just_pressed(KeyCode::..) handler);
rows carry an `essential` flag — the onboarding slide teaches that
subset, the cheat sheet shows everything.
- New cheat_sheet_plugin: hold `/` for a right-anchored reference
overlay of every binding; release hides it. Deliberately not a
spawn_modal modal (momentary reference, closer to a tooltip), never
spawns while a modal owns the screen (also keeps it out of the
seed-entry field), inert on touch builds.
- Onboarding's stale local table deleted in favour of the shared one
(copy updated: Home naming, hold-to-repeat undo, added H).
5 new tests (table integrity, essential-subset size, uniqueness,
show/hide driver, modal suppression). Workspace + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings -> Accessibility gains a UI Scale row cycling four steps,
applied live through bevy::ui::UiScale: every menu, modal, and HUD
element scales while the table itself stays window-fit via
compute_layout. New Settings::ui_scale (serde default 1.0, sanitized
clamp to [0.9, 1.3]).
Safe-area anchors and modal scrim padding pre-divide physical insets
by the UI scale so post-multiplication lands exactly on the system
bars — without this, 90% would sink the bottom action bar into the
Android gesture zone. The anchor systems also re-run on UiScale
changes, not just inset changes.
Deferred from Phase K (noted in the doc): seeding the setting from the
Android system font scale on first run (needs JNI), and the 44px
touch-target audit (on-device).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two situation-fired teaches, each shown exactly once and recorded in
Settings like shown_achievement_onboarding:
- Stall tip: 45s with no board change in an active, started game
(clock frozen while paused / a modal is open) points at Hint with
platform-correct copy (H on desktop, bottom-bar Hint on touch).
- Radial teach: a player 15 moves into a game who has never opened the
radial menu learns the long-press / right-click gesture. Organic
radial use marks the tip done silently — nobody is taught what they
already know.
Tips ride the queued InfoToastEvent path, so they render in the unified
toast stack and never interrupt play. This completes Phase I.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ObtainX updates install silently, so shipped features went unnoticed.
On the first launch where the running release differs from the new
Settings::last_seen_whats_new, a dismissible card summarises the
latest CHANGELOG.md section (embedded; its top version doubles as the
app's release identity — no build-time version plumbing). Internal
sections are dropped and bullets reduce to their bold lead sentence.
Launch beat: splash -> onboarding (first run) -> what's-new -> Home;
spawn_home_on_launch waits on the new WhatsNewPending resource. Fresh
installs never see the card — onboarding completion stamps the current
version silently. The seen-stamp persists on spawn, not dismissal, so
the card can never nag twice.
8 new tests (changelog parsing incl. the real embedded file, upgrade/
seen/fresh-install gating, dismissal). Workspace + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A hint now spawns a translucent copy of the hinted card that glides to
the suggested destination twice (0.7s per pass, SmoothSnap easing, tail
fade so the loop reads as a repeat), alongside the existing static
source/destination highlights. Auto-disabled under reduce-motion — the
static highlights remain the whole story. The ghost despawns on timer,
on a fresh hint, or the moment the board changes (a preview of a stale
board is worse than none).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live catalog has advertised preview_url per theme since the store
shipped, but the client never fetched it — rows were text-only. The
modal now downloads each advertised preview PNG off the main thread
(same AsyncComputeTaskPool + Tokio pattern as the catalog fetch and
avatar image), decodes it into an Image asset, and renders a thumbnail
sized by the theme's own card_aspect at the head of the row. Previews
pop in as they arrive and are cached for the session, so reopening the
store is instant. Failures log and leave the row text-only; the decode
path is skipped entirely under MinimalPlugins (no Assets<Image>).
solitaire_data grows ThemeStoreClient::fetch_preview with a 512 KB cap
(previews carry no checksum — decorative only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The how-to-play slide told touch players to left/right-click; Android
now gets tap/double-tap copy pointing at the bottom-bar Hint button.
- On a fresh profile the Home auto-show spawned underneath the
onboarding modal, stacking two scrims (v0.44.0 emulator smoke
finding). spawn_home_on_launch now waits until first_run_complete
and the onboarding modal is gone, so the launch beat is onboarding,
then Home, then the table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queued and immediate toasts used two subtly different geometries and
staggered anchors purely to dodge each other when simultaneous. Both
paths now spawn geometry-free ToastNodes that adopt_toasts_into_stack
slots into a single persistent ToastStackRoot flex column — one anchor,
one style, simultaneous toasts stack upward. The root rides
SafeAreaAnchoredBottom and clears the Phase F touch action bar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Touch layout (USE_TOUCH_UI_LAYOUT) restructures the bottom action bar
to five buttons: an enlarged Undo / Draw / Hint trio (96x64px targets,
1.35x labels) between compact Menu and Pause. Draw is new — it fires
the same DrawRequestEvent as tapping the stock, so the most frequent
action no longer needs a reach to the top of a tall folded screen.
Help, Modes, and New Game leave the touch bar (they live in Menu ->
System, the Home grid, and Home's hero respectively). Desktop keeps
the seven-button bar unchanged (decision 5: touch-only).
Holding Undo now steps back repeatedly after a 0.45s delay (5.5/s),
each step through the normal request path so the scoring penalty
applies. New self-ambiguous DrawRequestWriters set keeps the ambiguity
gate at zero with the fourth DrawRequestEvent writer.
6 new hud_plugin tests; workspace suite + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Gitea actions cache on this instance stores caches (1.85 GB saves
confirmed in run 597) but never restores them — every run back through
run 584+ logs 'No cache found' even for exact keys saved an hour
earlier, including master-to-master. Every CI run has therefore been a
full cold build (~37 min), plus ~4 min tarring a cache nobody reads.
- Point CARGO_TARGET_DIR at a persistent path on the rust-host runner
(host executor — filesystem carries over between runs), with a 40 GiB
prune guard. Warm runs drop to minutes without touching the broken
cache API.
- Drop Swatinem/rust-cache (pure overhead until the server is fixed).
- Split cargo fmt --check into a seconds-long fmt job gating the heavy
test job, so a formatting slip can't burn a 35-minute build again
(run 600).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructures the Home overlay per docs/ui-redesign-2026-07.md Phase B:
- Continue card (mode / elapsed / score) when a game is in progress;
clicking returns to the table
- Hero New Game replays the persisted Settings::last_mode (new field,
serde-default Classic) with one tap; locked modes fall back to Classic
- Deal options (draw 1/3, winnable-only, difficulty tiers) move into a
disclosure under the hero — off the Home top level (decision 2)
- Compact symmetric 2x3 mode grid; descriptions on wide viewports only
- Stats strip moves to the bottom (right pane on wide)
- Two-pane body on wide viewports (>= 1000 logical px) via new
ui_modal::spawn_modal_sized (default card stays 720 px)
- Cancel becomes "Back to table" and only renders while a live
(un-won) game exists
10 new home_plugin tests; all 936 engine tests + clippy green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cycle regression gate still read payload.moves, which schema v4
removed in favour of recording.instructions, so every game reported
replay_history_mismatch:0/N and games_with_issues tripped the
--require-zero-issues gate on master (runs 587/594/596).
Verified locally: 12-game gate run reports 0 issues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #170 changed the web replay payload (moves list -> embedded session
recording) but missed these Playwright specs, which only run on master
pushes and so failed post-merge. Assertions now match the v4 shape:
schema_version 4, recording.initial_state present, moves at
recording.instructions.
Verified locally against a real server with freshly built wasm bundles:
full suite 18/18 green, including the five play_canvas specs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GameState's save serde had the same latent flaw PR #170 removed from
replays: v5 persisted seed + saved_moves and re-dealt the board from the
seed on load, so any RNG or upstream upgrade that shifts the seed->deal
mapping would invalidate every in-progress save (graceful error, but the
player loses their game). v6 persists the existing SessionRecording
payload (upstream card_game session serde: config + dealt board +
instructions) instead; seed stays as presentation metadata only.
- v4/v5 files still load through the legacy seed path and are rewritten
as v6 on the next save; v6 files load from the recording and replay
with per-instruction validation, so tampered or corrupt files surface
an error rather than a silently wrong board
- saved_moves() removed (dead once serialisation reads recording())
- tests: v6 round-trip via storage, seed-corruption immunity, v5 legacy
load, missing-recording rejection, invalid-history rejection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replays previously persisted only seed + moves and re-dealt the board
from the seed at playback time, so any change to the seed->deal mapping
(RNG bumps, upstream upgrades) silently invalidated every existing
replay. Schema v4 instead embeds a SessionRecording - the upstream
card_game Session serde ({config, initial_state, instructions}) - so
playback rebuilds the exact recorded board; seed/draw_mode/mode remain
caption metadata only.
- core: SessionRecording newtype delegating to Session<Klondike> serde;
GameState::recording() / from_recording(); from_instructions_unchecked
fixture helper; serde_json added to dev-deps (tests only)
- data: Replay v4 (recording replaces moves); v1-v3 files rejected by
the existing version gate
- engine: win-recording and sync upload freeze game.recording();
playback rebuilds from the recording; Playing carries the extracted
move list (+ Box<Replay> for clippy large_enum_variant)
- wasm: replay_export() builds the full v4 upload payload so JS never
hand-assembles it (the old game.js path hardcoded schema_version: 2
and corrupted u64 seeds via Math.round); ReplayPlayer::from_json
enforces schema_version == 4 with a descriptive error
- web: game.js/play.html use replay_export; replay.js surfaces player
construction errors in the caption instead of dying silently
- server: mode validation accepts data-carrying GameMode variants
(Difficulty uploads previously 400'd against the String field)
Both replays on prod are May-era v1 rows with empty move lists - every
shared replay was already unplayable; the viewer now says why.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>