Port the July 7 gate-extender branch onto current master (the You-hub,
touch action bar, glass tab bar, Phase L, and hint-ghost work all landed
since) and finish batch 3. cluster_app now includes the mode, replay,
input, radial, tooltip, cursor, touch-selection, and safe-area plugins on
top of batches 1-2; the 585 ambiguous pairs the expansion exposed are
annotated down to zero and the gate stays assert_eq!(count, 0).
New ordering machinery:
- ReplayPlayback: playback driver chain on the pre-mutation spine
(PointerInput < ReplayPlayback < overlay chain < GameMutation)
- ModeStart: mode-start handlers between the stats abandon-recorder and
GameMutation; ChallengeCompletion < DailyCompletion < WeeklyGoalsEval
pinned inside ProgressUpdate so `.after(ProgressUpdate)` readers see
settled progress
- PointerInput: public set wrapping the input chain; AbandonRecord on the
stats abandon-recorder; PendingHint around the async hint pipeline;
HintGhostFx for the ghost chain (after the full visual spine)
- Clubs: MoveRequestWriters, WarningToastWriters join the existing
request/toast writer clubs; UiTextFx grows the overlay/tooltip/safe-area
chrome painters; SettingsAccess grows the lag-tolerant settings readers
Gate: cargo test --workspace green (993 engine tests), clippy
--all-targets -D warnings clean, rustfmt applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Two unused-for-free upstream card_game/klondike features:
- GameState now exposes the granular KlondikeStats counters
(move_to_foundation_count, move_to_tableau_count,
move_from_foundation_count, flip_up_count) and the win modal shows a
quiet per-move-type recap line built from them (e.g. "21 to
foundation - 14 tableau moves - 9 flips")
- wasm ReplayPlayer gains seek(step): clamped jump to any position,
rewinding via a stored copy of the recorded deal instead of reparsing
the replay JSON; replay.js Prev now uses it
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>
Three items the multi-agent sweep flagged, now removed with user
approval (§8 for the solitaire_sync changes):
- WinCascadePlugin: never registered; handle_win_cascade in
AnimationPlugin is the live win cascade and builds its own targets.
Its now-orphaned helpers (win_scatter_targets, cascade_delay,
WIN_CASCADE_INTERVAL_SECS) had no callers outside their own tests
and go with it.
- SyncCompleteEvent: written by the pull-completion system, zero
readers — UI reads SyncStatusResource instead.
- solitaire_sync::ApiError: unused by client and server; the merge_at
crate-root re-export goes too (merge::merge_at stays for the merge
module's own use).
ARCHITECTURE.md updated to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Double-click/tap auto-move previously tried the run's top card alone
before the full run headed by the clicked card, so a top card with a
foundation move hijacked the intended whole-stack move. Both handlers
now share auto_move_for_run: a lone top card goes foundation-first, a
multi-card run moves whole to a tableau or not at all. The double-click
key is now the clicked card, so two clicks on different cards of the
same stack no longer register as a double-click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compiler-verified via RUSTFLAGS=--force-warn dead_code plus workspace-wide
reference greps; three parallel audit agents covered the engine crate, the
other eight crates, and Copilot commit-message-vs-diff drift.
Removed:
- replay_overlay/input.rs: 19 orphaned twins (~950 lines) of items also
defined in mod.rs — the glob re-export made the mod.rs copies win and
the file-level #![allow(dead_code)] hid the corpses. The live keyboard/
button handlers and ReplayScrubKeyHold stay; the allow is retired.
- retarget_animation (never called; doc examples were its only refs)
- ScanThemesRequestEvent (never registered/written/read; its doc claimed
a handle_scan_themes consumer that does not exist)
- _VEC3_REFERENCED workaround const + now-unneeded Vec3 import
- solitaire_data: load_stats/save_stats/time_attack_session_with_now
default-path wrappers (the _from/_to variants are the live API) and
surplus re-export names (settings MIN/MAX bounds, token loaders)
- solitaire_core: Session re-export (no external consumer)
- solitaire_wasm: ReplayPlayer::is_finished (no JS caller)
- solitaire_app: build_app wrapper (real entry is run())
Doc fixes:
- audio_plugin: WAV count 5→7, add FoundationCompletedEvent table row,
drop bogus 'placeholder' label, bevy_kira_audio→kira
- ToastVariant::Warning: variant is live (5 writer plugins); dropped the
stale allow(dead_code) and its 'currently unused' comment
Deliberately kept: Spider module (staged forward work), WinCascadePlugin
(documented alternative cascade, pending owner decision), SyncCompleteEvent
and solitaire_sync ApiError/merge_at (§8 change-controlled, flagged to owner).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 38e4c03 (shipped in v0.40.0) switched Android's focused_mode to
reactive_low_power(100 ms) on the premise that every animation tick
system writes RequestRedraw while it has active work. The writers were
never actually added — the diff only contained the WinitSettings change,
imports, and add_message registrations. Result: with no touch input,
nothing wakes the winit loop during a card slide except the 100 ms
fallback ceiling, so animations render at ~10 fps on Android. Desktop
and web keep Continuous mode, which is why only Android was affected
(reported by Rhys the day v0.40.0 shipped; confirmed absent on v0.39.1).
Adds the missing MessageWriter<RequestRedraw> to all seven systems the
original commit message named:
- advance_card_animations (CardAnimationPlugin)
- advance_card_anims (AnimationPlugin — deal/win cascade/slides)
- tick_shake_anim, tick_settle_anim, tick_foundation_flourish
(FeedbackAnimPlugin)
- drive_toast_display (AnimationPlugin — toast countdown)
- drive_auto_complete (AutoCompletePlugin — step-interval keepalive)
Each writes one RequestRedraw per frame while active work exists
(including delay phases, which also need per-frame ticks). Regression
test asserts an active CardAnimation emits RequestRedraw and an idle
board does not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase G of docs/ui-redesign-2026-07.md. The win modal now leads with
actions and reads the stats recap quietly below them:
- Play Again: primary, full-row, Enter accelerator unchanged; still
fires NewGameRequestEvent::default() (same mode; deal options from
Settings) so the rematch is one tap.
- Watch Replay / Share Replay: secondary pair reusing the global
stats_plugin markers (WatchReplayButton / CopyShareLinkButton), so
both act on the just-won replay — SelectedReplayIndex snaps to 0 on
every win. This also fixes the old win-modal handler picking
replays.last(), which after the newest-first history refactor was
the OLDEST replay, not the newest.
- Watch closes the celebration overlay (new close_overlay_on_watch_-
replay system); Share keeps it open and relies on the existing
copy-feedback toasts.
- Stats recap (score breakdown reveal, time, XP, achievements) moves
below the actions; the Time line drops from headline/primary to
body/secondary styling.
Tests: 4 new (action presence, Play Again close+request, Watch closes,
Share keeps open) on a manual-clock fixture that steps the 0.5 s
celebration delay deterministically. Workspace suite green, workspace
clippy -D warnings clean, fmt applied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C dismissal audit: Esc / scrim-tap / Done must behave the same
on every modal. Stragglers found and fixed — none of these had any
Esc path (pause's toggle guard swallowed the key while they were
open):
- Settings: Esc clears SettingsScreen, gated on being the topmost
modal so a stacked sync-setup / theme-store dialog owns Esc
- Help: Esc closes alongside F1/Done (the code comment already
claimed an Esc path existed — now it does)
- Leaderboard: Esc closes when topmost; the display-name dialog
stacked above it now Esc-cancels like sync-setup's dialog
- Theme store: Esc closes (always topmost when open)
Scrim-tap opt-ins are unchanged — ui_modal documents which modals
deliberately stay non-dismissible on outside clicks.
Tests: escape_closes_help_screen, escape_closes_settings_screen_flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase C of docs/ui-redesign-2026-07.md. The Modes row is gone — Home
owns mode selection, so the popover's Play section carries a Home row
firing the new ToggleHomeRequestEvent (read by toggle_home_screen
alongside the existing M accelerator). Section headers are quiet
caption-size labels inside the existing panel widget, not a new
widget. The action-bar Modes button and its popover are untouched
(their removal is Phase B territory when Home gains hierarchy).
- MenuOption: Modes variant replaced by Home; rows grouped Play (Home)
· You (Profile, Stats, Achievements) · Community (Leaderboard) ·
System (Settings, Help)
- handle_menu_option_click no longer chains into spawn_modes_popover
- Tests: tooltip sweep updated (7 rows still), new
toggle_home_event_opens_home_screen covers the popover's open path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase E of docs/ui-redesign-2026-07.md. New you_hub_plugin owns the
modal shell (header, shared tab chips, single Done); each tab's content
is a body builder extracted from its original plugin with every marker
component unchanged, so per-row update/scroll/selector systems keep
working. The replay selector gets its own Replays tab (Watch/Copy
buttons move into the tab body).
- Toggle*RequestEvents + P/S/A accelerators open the hub on the right
tab, switch tabs in place, or toggle closed on a same-tab request;
Esc/Done/scrim-click close
- Legacy ProfileScreen/StatsScreen/AchievementsScreen markers ride the
hub scrim for the active tab — external queries and tests keep their
meaning
- Standalone toggle/close systems and per-screen Done buttons removed
(ProfileCloseButton, StatsCloseButton, AchievementsCloseButton)
- Tests: 2 new hub lifecycle tests; profile/stats/achievements modal
tests adapted (fixtures add YouHubPlugin; selector tests target the
Replays tab). Engine suite 916 green, clippy -D warnings, fmt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Settings' tab_chip becomes a thin wrapper; the You hub (Phase E) will
reuse the same widget so tabbed modals stay visually identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the 37-row single-scroll settings modal with five tabs
(Audio, Gameplay, Appearance, Accessibility, Account); only the active
tab's rows spawn, so every tab fits without scroll-hunting.
- SettingsTab + ActiveSettingsTab (session-only) + SettingsTabButton
chips under the modal header; rebuild-on-switch mirrors the
leaderboard despawn/respawn pattern via a shared build_panel helper
- Accessibility tab gathers color-blind, high-contrast, reduce-motion,
touch-input, and tooltip-delay from the old Gameplay/Cosmetic mix;
Account = Sync + Privacy
- SettingsButton enum, input handlers, and persistence untouched
- Tests: per-tab Focusable/Tooltip sweeps, tab-switch rebuild test,
picker/thumbnail tests pinned to the Appearance tab
Plan: docs/ui-redesign-2026-07.md (also added, phases A–M)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New SolutionPlaybackPlugin: the pause modal's 'Show solution' button
resumes the game and requests a solve of the live position via
GameState::winning_line on AsyncComputeTaskPool (never blocks the main
thread; stale results discarded via a move_count snapshot). The line
then plays back one instruction per 0.45 s through the normal
MoveRequestEvent / DrawRequestEvent pipeline — animations, scoring,
undo history and win detection behave as if the player made the moves.
Tableau run counts are decoded against the live state at step time.
Playback cancels on Esc, pause, undo / new-game requests, and any
rejected move (the signature of the player diverging the board).
Toasts cover search start, line found, unwinnable, and budget-
exhausted outcomes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #152 poller fixes resolution + layout on a missed fold/unfold, but
a transient clip could survive if card sprites held stale visuals after
geometry converged. Emit StateChangedEvent alongside the synthetic
WindowResized so card_plugin re-renders every sprite from scratch —
silent self-heal, no player-facing prompt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1+2 of the theme-store roadmap: a free catalog served by
solitaire_server and an in-app browse/install flow, making custom
themes installable on Android for the first time (the manual
drop-a-zip flow can't reach the app-private themes dir there).
- solitaire_sync: ThemeCatalogEntry/ThemeCatalogResponse wire types
(additive module; SyncPayload and SyncProvider untouched)
- solitaire_server: THEME_STORE_DIR scan at startup (meta-only
theme.ron parse, sha256, 20 MiB cap, best-effort per archive);
public GET /api/themes, /api/themes/{id}/download, /{id}/preview;
compose volume + README_SERVER docs
- solitaire_data: ThemeStoreClient — catalog fetch + download with
mandatory size/sha256 verification before bytes are released
- solitaire_engine: ThemeStorePlugin — 'Browse theme store' button in
Settings → Cosmetic, modal catalog (leaderboard-style rebuild),
download on AsyncComputeTaskPool, atomic .tmp+rename write into
user_theme_dir, then the existing hardened import_theme pipeline and
an in-place registry refresh
New deps: sha2 (workspace, server+data); ron/zip reused in server;
serde_json added to solitaire_sync dev-deps for DTO round-trip tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- set_theme resolved every id to themes:// — the bundled dark/classic
themes live at embedded://, so switching to them via the public API
would NotFound and silently keep the old theme. All three load paths
now share one theme_manifest_url resolver.
- load_initial_theme's settings-absent fallback was "classic", a
leftover from v0.33 when classic was the default; it now derives from
Settings::default() so it can't drift from the data crate again, with
a test pinning the default id to a bundled theme.
- settings.rs doc no longer claims "classic" is migrated to "dark";
only the pre-rename "default" id is rewritten.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Random arm was only unreachable because seeds_for(Random) returns
None in a different function — a future catalog change would turn it
into a shipped runtime panic. Returning a system-time seed is the
correct Random behaviour either way. CLAUDE.md §2.3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
user_theme_dir() panicked on desktop when dirs::data_dir() returned
None (broken $HOME / $XDG_*). Return an empty path instead — exactly
what the wasm32 branch already did — so theme discovery reports 'no
user themes' and the bundled default keeps working. Warns once with
the set_user_theme_dir() workaround. CLAUDE.md §2.3: no panic! in
runtime logic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The repo was formatted under an older stable; rustfmt 1.9 (Rust 1.95)
wraps signatures and call sites differently, so every touched file was
picking up unrelated formatting hunks. One mechanical pass, and a
'cargo fmt --check' step in the test workflow (same pinned 1.95.0
toolchain) so drift can't accumulate again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
winit's Android backend does not forward content-rect changes that happen
while backgrounded (open TODOs in winit 0.30 logged on every resume), so a
fold/unfold cycle can leave Bevy rendering and laying out for the previous
screen: tableau clipped off the left edge, bottom third empty (#130).
Add a continuous decor-view size poller (JNI, every 30 frames) that, on
mismatch with the cached Window resolution: writes the real physical size
into window.resolution, emits a synthetic WindowResized in logical pixels,
and re-arms the safe-area inset poller — covering both the fold-size and
inset-staleness cases in one mechanism, without relying on
AppLifecycle::WillResume being delivered (it may never be; a new evidence
log in rearm_on_resumed settles that question on-device).
Closes#130
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>