Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da3e5423dc | |||
| a1864271de | |||
| f63db769ae | |||
| 4437a1aaf9 | |||
| e7345aed6c | |||
| 140251beae | |||
| d6f32d3154 | |||
| 8fdc41f36f | |||
| 2e25476d0a | |||
| d3cb1a51d4 | |||
| c8358f4275 | |||
| a2432dfe7a | |||
| 511550232c | |||
| e5c4f51a6e | |||
| 23902cdc44 | |||
| 3cc8eacafa | |||
| 90e24d9711 | |||
| decbe0bbd9 | |||
| 1873b3f9be | |||
| d11d97e677 | |||
| d322abf67b | |||
| c9e4c0b4cd | |||
| fe68861e10 | |||
| c33b39cf11 | |||
| 23ff62c397 | |||
| 0b2ffca016 | |||
| fbe48acef6 | |||
| cd79877933 | |||
| 52befa6199 | |||
| e63046700c | |||
| ab857bbb6e | |||
| 886e0cf8a1 | |||
| 3d92a91e3b | |||
| 9113cdb483 | |||
| c153363626 | |||
| 93b67f1d0b | |||
| 279e23d0af | |||
| 12fba2157a | |||
| f23df3b805 | |||
| 68d50b5021 | |||
| ec804d54c6 | |||
| d87761d451 | |||
| 2fb2d638bf | |||
| c9af1ead22 | |||
| ed152e2d8f | |||
| 279a834f9d | |||
| daa655a0af | |||
| 4d48cad4e3 | |||
| dd970215cc | |||
| ddb65403c2 | |||
| 62b61cc786 | |||
| 31139ae455 | |||
| 07e035771c | |||
| c5787c6953 | |||
| 716a025352 | |||
| 3eb3a26789 | |||
| 0c1cc40266 | |||
| 04f9bf9be3 | |||
| a292a7ead0 | |||
| d109c32b75 | |||
| dd101b3d54 | |||
| af414b6aed | |||
| ae84dc1504 | |||
| 8719f77ec2 | |||
| a14200ac2f | |||
| e8bf9d79da | |||
| 48b28d29f8 | |||
| babe5cc9c8 | |||
| 3a4bb63a6f | |||
| 56233687b0 | |||
| 73ac67d76b | |||
| a27cf5a020 | |||
| 29136d815d | |||
| ef54cdeb65 | |||
| e080b49914 | |||
| 54005d5494 | |||
| 44f5972edd | |||
| 13ae16051d | |||
| a65e5b8c7b | |||
| 6204db8bb1 | |||
| c84d9f445c | |||
| cacb19c03f | |||
| 39b84965b6 | |||
| 41a009a693 | |||
| fa7f98ac52 | |||
| 9891ae4ba3 | |||
| cdcaddaabe | |||
| d752870007 | |||
| 1d1543e4bc | |||
| 651f4060e6 | |||
| a1376075bd | |||
| ceec4fc486 | |||
| 0d477ac9fd | |||
| 4b51e50203 | |||
| f2d2119db5 | |||
| 59424a370c | |||
| fb8b2ac684 | |||
| 690e1d2ad6 | |||
| 35516d31f6 | |||
| 9b065e5ac6 | |||
| e1b8766e15 | |||
| 67c150bd7b | |||
| aa2a021712 | |||
| 6037596cc0 | |||
| d7ffb16df5 | |||
| b57db017d3 | |||
| 0b3140ad6d | |||
| e41def8c89 | |||
| aad8bb9c83 | |||
| 55c235b55f | |||
| 21ec03b157 | |||
| 17e3112502 | |||
| de4751115f | |||
| 9ff48ace5b | |||
| 91b7605b9f | |||
| 42d90b199c | |||
| 3e11e9e79a | |||
| bfcd05fbb5 | |||
| c497c3193c | |||
| 9aa0dd23b1 | |||
| d065d49fe7 | |||
| c30b04ec72 | |||
| 40d6e0ab17 | |||
| 9fe650fa20 | |||
| b73d246b4c | |||
| ae40a1db7a | |||
| b7c3a4996f | |||
| d48b9489db | |||
| 08b006ff30 | |||
| 17e0737a10 | |||
| dd63261999 | |||
| 93660c2217 | |||
| 56e2e6f151 | |||
| cc635328be | |||
| a4bc063497 | |||
| 540869c851 | |||
| bdac754b26 | |||
| f863d85c35 | |||
| 3c7a0eb4fb | |||
| d489e7a31b | |||
| f2f30c8002 | |||
| a49a340a30 | |||
| 27cdf78ce0 | |||
| faa6c5efc4 | |||
| 487b99bbc9 | |||
| 53e3b816cf | |||
| 87275bf340 | |||
| 56647d7f0d | |||
| cbf2483028 | |||
| a54201e97b | |||
| 48e412177c | |||
| cd54ce1bb0 | |||
| 7a3032b74c | |||
| 89699a8a86 |
@@ -47,11 +47,10 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
|
||||
### Design Principles
|
||||
|
||||
- **Offline first.** The local file is always the source of truth. Sync is additive, never destructive.
|
||||
- **Pure core.** All game logic lives in a dependency-free Rust crate with no Bevy, no network, and no I/O. This keeps it fully unit-testable and portable.
|
||||
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
||||
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
||||
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
||||
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
|
||||
|
||||
Pure-core, no-panics-in-game-logic, and UI-first-interaction constraints are enforced by CLAUDE.md §2.1, §2.3, and §3.3 respectively — those are the canonical statements; this file describes the design that motivates them.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,114 +1,571 @@
|
||||
# Solitaire Quest — Claude Code Instructions
|
||||
# CLAUDE.md
|
||||
|
||||
See @ARCHITECTURE.md for full project design, crate responsibilities, data models, and API reference.
|
||||
version: unified-3.0
|
||||
|
||||
---
|
||||
|
||||
## Project Layout
|
||||
# 0. Role of This File
|
||||
|
||||
```text
|
||||
solitaire_core/ # Pure Rust game logic — NO Bevy, NO network, NO I/O
|
||||
solitaire_sync/ # Shared API types — NO Bevy, serde/uuid/chrono only
|
||||
solitaire_data/ # Persistence + SyncProvider trait + server client
|
||||
solitaire_engine/ # Bevy ECS systems, components, plugins
|
||||
solitaire_server/ # Axum sync server binary
|
||||
solitaire_app/ # Thin binary entry point
|
||||
assets/ # Source assets — embedded at compile time via include_bytes!()
|
||||
This document defines:
|
||||
|
||||
* **Execution rules (what Claude must do)**
|
||||
* **System constraints (what Claude must never violate)**
|
||||
* **Operational architecture (how code is structured)**
|
||||
|
||||
For full system design details:
|
||||
→ `ARCHITECTURE.md` (authoritative source of truth)
|
||||
|
||||
This file overrides all conversational assumptions.
|
||||
|
||||
---
|
||||
|
||||
# 1. System Architecture (Authoritative Mapping)
|
||||
|
||||
## 1.1 Crates
|
||||
|
||||
```text id="crate_map"
|
||||
solitaire_core/ # PURE logic (no IO, no Bevy, deterministic)
|
||||
solitaire_sync/ # Shared API + merge logic
|
||||
solitaire_data/ # Persistence + sync client
|
||||
solitaire_engine/ # Bevy ECS + UI + gameplay orchestration
|
||||
solitaire_server/ # Axum backend (optional sync layer)
|
||||
solitaire_app/ # Entry binary
|
||||
assets/ # Runtime assets (except audio)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build & Test Commands
|
||||
## 1.2 Architecture Source of Truth
|
||||
|
||||
```bash
|
||||
# Dev run (fast compile via dynamic linking)
|
||||
cargo run -p solitaire_app --features bevy/dynamic_linking
|
||||
* Full system design: `ARCHITECTURE.md`
|
||||
* This file NEVER redefines system design
|
||||
* This file ONLY enforces behavior
|
||||
|
||||
# Release build
|
||||
cargo build --workspace --release
|
||||
---
|
||||
|
||||
# All tests — MUST pass before any commit
|
||||
# 2. Hard Global Constraints (NON-NEGOTIABLE)
|
||||
|
||||
These override all other instructions.
|
||||
|
||||
## 2.1 Core Determinism
|
||||
|
||||
* `solitaire_core` MUST:
|
||||
|
||||
* be deterministic
|
||||
* be side-effect free
|
||||
* never depend on Bevy / IO / async
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Sync Isolation
|
||||
|
||||
* `solitaire_sync`:
|
||||
|
||||
* no Bevy
|
||||
* no IO
|
||||
* no engine dependencies
|
||||
* merge logic must be pure functions only
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Error Policy
|
||||
|
||||
* NO `unwrap()`
|
||||
* NO `panic!()` in runtime/game logic
|
||||
* All state transitions:
|
||||
|
||||
```rust id="err_model"
|
||||
Result<T, MoveError>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2.4 Threading Rules
|
||||
|
||||
* Sync must run on `AsyncComputeTaskPool`
|
||||
* NEVER block Bevy main thread
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Persistence Rules
|
||||
|
||||
* atomic writes only:
|
||||
|
||||
* write `.tmp`
|
||||
* rename atomically
|
||||
* no partial state writes allowed
|
||||
|
||||
---
|
||||
|
||||
## 2.6 Security Rules
|
||||
|
||||
* credentials ONLY via `keyring`
|
||||
* NEVER store secrets in:
|
||||
|
||||
* files
|
||||
* logs
|
||||
* source code
|
||||
|
||||
---
|
||||
|
||||
## 2.7 Sync System Rules
|
||||
|
||||
* All sync backends implement:
|
||||
|
||||
```rust id="sync_trait"
|
||||
trait SyncProvider
|
||||
```
|
||||
|
||||
* `SyncPlugin` MUST be backend-agnostic
|
||||
* NEVER match on backend inside ECS systems
|
||||
|
||||
---
|
||||
|
||||
# 3. Engine Rules (Bevy Layer)
|
||||
|
||||
## 3.1 ECS Design
|
||||
|
||||
* systems = single responsibility
|
||||
* communication = Events only
|
||||
* shared state = Resources only
|
||||
* per-entity state = Components only
|
||||
|
||||
---
|
||||
|
||||
## 3.2 Game State Authority
|
||||
|
||||
* ONLY `GameStateResource` can mutate game state
|
||||
* UI systems MUST NOT directly modify core logic
|
||||
|
||||
---
|
||||
|
||||
## 3.3 UI-First Constraint (CRITICAL)
|
||||
|
||||
Every player action MUST:
|
||||
|
||||
* have a visible UI control
|
||||
* NOT rely solely on keyboard shortcuts
|
||||
|
||||
Keyboard shortcuts are:
|
||||
→ optional accelerators only
|
||||
|
||||
---
|
||||
|
||||
## 3.4 Layout System
|
||||
|
||||
* recompute on `WindowResized`
|
||||
* no fixed resolution assumptions
|
||||
|
||||
---
|
||||
|
||||
# 4. Asset System Rules
|
||||
|
||||
## 4.1 Runtime Assets (AssetServer)
|
||||
|
||||
Loaded via:
|
||||
|
||||
* `CardImageSet`
|
||||
* `BackgroundImageSet`
|
||||
* `FontResource`
|
||||
|
||||
Includes:
|
||||
|
||||
* cards
|
||||
* backgrounds
|
||||
* fonts
|
||||
|
||||
---
|
||||
|
||||
## 4.2 Embedded Assets
|
||||
|
||||
Only audio:
|
||||
|
||||
```text id="audio_rule"
|
||||
include_bytes!()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4.3 Test Compatibility Rule
|
||||
|
||||
All asset loaders MUST accept:
|
||||
|
||||
```rust id="asset_fallback"
|
||||
Option<Res<AssetServer>>
|
||||
```
|
||||
|
||||
Must degrade gracefully under `MinimalPlugins`.
|
||||
|
||||
---
|
||||
|
||||
# 5. Code Standards
|
||||
|
||||
## 5.1 Error Handling
|
||||
|
||||
* use `thiserror`
|
||||
* no `Box<dyn Error>` in libraries
|
||||
|
||||
---
|
||||
|
||||
## 5.2 Public API Rules
|
||||
|
||||
* prefer `Into<T>` over concrete types
|
||||
* all public items require doc comments
|
||||
|
||||
---
|
||||
|
||||
## 5.3 Derive Order
|
||||
|
||||
```rust id="derive_order"
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.4 Performance Rules
|
||||
|
||||
* NO `clone()` in hot paths
|
||||
* profile before optimizing
|
||||
|
||||
---
|
||||
|
||||
## 5.5 SQL Rules
|
||||
|
||||
* ONLY `sqlx::query!`
|
||||
* NO raw SQL strings
|
||||
|
||||
---
|
||||
|
||||
# 6. Build & Verification Rules
|
||||
|
||||
These are mandatory before ANY commit.
|
||||
|
||||
```bash id="build_rules"
|
||||
cargo test --workspace
|
||||
|
||||
# Lint — MUST pass clean (zero warnings)
|
||||
cargo clippy --workspace -- -D warnings
|
||||
|
||||
# Run sync server locally
|
||||
cargo run -p solitaire_server
|
||||
|
||||
# Check a single crate
|
||||
cargo test -p solitaire_core
|
||||
cargo clippy -p solitaire_core -- -D warnings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hard Rules
|
||||
# 7. Git Workflow Rules
|
||||
|
||||
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
|
||||
- No `unwrap()` or `panic!()` in game logic. All state transitions return `Result<_, MoveError>`.
|
||||
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`.
|
||||
- Card faces (52 PNGs in `assets/cards/faces/`), card backs (`assets/cards/backs/back_N.png`), board backgrounds (`assets/backgrounds/bg_N.png`), and the UI font (`assets/fonts/main.ttf`) are loaded at runtime via `AssetServer::load()` and stored as `Handle<Image>`/`Handle<Font>` in the `CardImageSet`, `BackgroundImageSet`, and `FontResource` resources. The `assets/` directory must ship alongside the binary.
|
||||
- Asset-loading systems take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` (tests). When `CardImageSet` is absent, `card_plugin` falls back to a `Text2d` rank+suit overlay; when `BackgroundImageSet` is absent, the board falls back to a solid colour.
|
||||
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
||||
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
||||
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
||||
- All sync backends implement the `SyncProvider` trait. The `SyncPlugin` is backend-agnostic — never `match` on `SyncBackend` inside a Bevy system.
|
||||
- `cargo clippy --workspace -- -D warnings` must pass clean after every change.
|
||||
- `cargo test --workspace` must pass after every change.
|
||||
## Commit format
|
||||
|
||||
```text id="commit_fmt"
|
||||
type(scope): description
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
* feat(core): add draw-three rules
|
||||
* fix(engine): correct drag z-order
|
||||
* test(core): undo boundary cases
|
||||
|
||||
---
|
||||
|
||||
## Code Style
|
||||
## Commit conditions
|
||||
|
||||
- Use `thiserror` for error types. Never `Box<dyn Error>` in library crates.
|
||||
- Prefer `Into<T>` over concrete types in public API function parameters.
|
||||
- All public items must have doc comments (`///`). Private items: comment only when non-obvious.
|
||||
- Derive order convention: `#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]`
|
||||
- Bevy systems: one responsibility per system. Use `Events` for cross-system communication, never shared mutable state.
|
||||
- SQL queries: use `sqlx::query!` macros (compile-time checked), not raw string queries.
|
||||
- No `clone()` calls in hot paths (game loop systems). Profile before optimising elsewhere.
|
||||
* tests must pass
|
||||
* clippy must be clean
|
||||
|
||||
NEVER commit otherwise
|
||||
|
||||
---
|
||||
|
||||
## Bevy Conventions
|
||||
# 8. Change Control (ASK BEFORE DOING)
|
||||
|
||||
- One `Plugin` per major feature: `CardPlugin`, `AudioPlugin`, `AchievementPlugin`, `UIPlugin`, `SyncPlugin`.
|
||||
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
||||
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
||||
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
||||
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
|
||||
Claude must request confirmation before:
|
||||
|
||||
* adding dependencies
|
||||
* modifying `solitaire_sync`
|
||||
* changing DB schema
|
||||
* introducing `unsafe`
|
||||
* changing merge strategy
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow
|
||||
# 9. System Mental Model (IMPORTANT)
|
||||
|
||||
- Commit after each passing phase, not after every file change.
|
||||
- Commit message format: `type(scope): description`
|
||||
- `feat(core): add draw-three mode validation`
|
||||
- `fix(engine): card z-order during drag`
|
||||
- `test(core): undo stack boundary conditions`
|
||||
- `chore(server): add sqlx migration 002`
|
||||
- Never commit with failing tests or clippy warnings.
|
||||
- Never commit secrets, `.env` files, or `*.db` files.
|
||||
```text id="mental_model"
|
||||
Core (rules + deterministic logic)
|
||||
↓
|
||||
Engine (Bevy orchestration)
|
||||
↓
|
||||
Data layer (persistence + sync)
|
||||
↓
|
||||
Server (optional external system)
|
||||
```
|
||||
|
||||
Core is always the source of truth.
|
||||
|
||||
---
|
||||
|
||||
## Ask Before Doing
|
||||
# 10. Known Platform Pitfalls
|
||||
|
||||
- Adding a new crate dependency (discuss alternatives first).
|
||||
- Changing a type in `solitaire_sync` (breaking change on both client and server).
|
||||
- Altering the database schema (requires a new sqlx migration).
|
||||
- Introducing `unsafe` code anywhere.
|
||||
- Changing the merge strategy in `solitaire_sync::merge()`.
|
||||
Must always be handled explicitly:
|
||||
|
||||
* Bevy `Time` uses `f32`
|
||||
* `sqlx::migrate!()` path is crate-relative
|
||||
* `dirs::data_dir()` may return `None`
|
||||
* Linux may lack keyring backend
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
# 11. Forbidden Patterns
|
||||
|
||||
> Add entries here when Claude makes a mistake so it isn't repeated.
|
||||
* game logic inside Bevy systems
|
||||
* duplication across crates
|
||||
* blocking async calls in ECS
|
||||
* insecure credential storage
|
||||
* bypassing core logic layer
|
||||
|
||||
- Bevy's `Time` resource uses `f32` seconds; convert to `u64` only when writing to `StatsSnapshot`.
|
||||
- `sqlx::migrate!()` macro path is relative to the crate root, not the workspace root.
|
||||
- `keyring` on Linux requires a running secret service (e.g. GNOME Keyring or KWallet) — handle `Error::NoStorageAccess` gracefully and fall back to prompting the user.
|
||||
- `dirs::data_dir()` returns `None` on some minimal Linux environments — always handle the `None` case explicitly, do not unwrap.
|
||||
---
|
||||
|
||||
# 12. Execution Rules for Claude
|
||||
|
||||
When generating code:
|
||||
|
||||
1. respect crate boundaries
|
||||
2. minimize diff size
|
||||
3. do not expand scope
|
||||
4. follow existing patterns
|
||||
5. preserve invariants
|
||||
|
||||
If unclear:
|
||||
→ ask before acting
|
||||
|
||||
---
|
||||
|
||||
# 13. Relationship to ARCHITECTURE.md
|
||||
|
||||
| File | Role |
|
||||
| --------------- | ------------------------- |
|
||||
| CLAUDE.md | execution + constraints |
|
||||
| ARCHITECTURE.md | system design truth |
|
||||
| Both combined | full system understanding |
|
||||
|
||||
---
|
||||
# 14. Context Injection System (AUTOMATIC SCOPE FILTER)
|
||||
|
||||
## 14.1 Purpose
|
||||
|
||||
Before generating any response, Claude MUST construct a **minimal relevant context set**.
|
||||
|
||||
This prevents:
|
||||
|
||||
* architectural drift
|
||||
* irrelevant spec loading
|
||||
* over-engineering
|
||||
* cross-crate confusion
|
||||
|
||||
---
|
||||
|
||||
## 14.2 Input Classification Step (MANDATORY)
|
||||
|
||||
Every request MUST be classified into exactly one task type:
|
||||
|
||||
```text id="task_types"
|
||||
feature
|
||||
bugfix
|
||||
refactor
|
||||
system_design
|
||||
bevy_system
|
||||
core_logic
|
||||
sync
|
||||
optimization
|
||||
test
|
||||
debug
|
||||
```
|
||||
|
||||
If uncertain → ask clarification.
|
||||
|
||||
---
|
||||
|
||||
## 14.3 Context Selection Engine
|
||||
|
||||
After classification, Claude MUST include ONLY the relevant sections below.
|
||||
|
||||
---
|
||||
|
||||
## 14.4 Context Map (CORE RULESET)
|
||||
|
||||
### feature
|
||||
|
||||
Include:
|
||||
|
||||
* §2 Hard Global Constraints
|
||||
* §3 Engine Rules
|
||||
* ARCHITECTURE.md (crate of target feature only)
|
||||
* relevant data models (GameState, SyncPayload if needed)
|
||||
|
||||
---
|
||||
|
||||
### bugfix
|
||||
|
||||
Include:
|
||||
|
||||
* §2 Hard Global Constraints
|
||||
* §5 Code Standards
|
||||
* affected crate boundaries
|
||||
* relevant system (engine/core/sync only)
|
||||
|
||||
---
|
||||
|
||||
### refactor
|
||||
|
||||
Include:
|
||||
|
||||
* §3 Engine Rules
|
||||
* §5 Code Standards
|
||||
* §11 Forbidden Patterns
|
||||
* target crate boundaries
|
||||
|
||||
---
|
||||
|
||||
### system_design
|
||||
|
||||
Include:
|
||||
|
||||
* ARCHITECTURE.md (FULL)
|
||||
* §9 Mental Model
|
||||
* §1 System Architecture Mapping
|
||||
|
||||
---
|
||||
|
||||
### core_logic
|
||||
|
||||
Include:
|
||||
|
||||
* solitaire_core rules only
|
||||
* GameState model
|
||||
* MoveError model
|
||||
* §2.1–2.3 constraints
|
||||
|
||||
---
|
||||
|
||||
### bevy_system
|
||||
|
||||
Include:
|
||||
|
||||
* §3 Engine Rules
|
||||
* ECS rules (Events/Resources/Components)
|
||||
* UI-first constraint
|
||||
* relevant plugin system only
|
||||
|
||||
---
|
||||
|
||||
### sync
|
||||
|
||||
Include:
|
||||
|
||||
* SyncProvider trait
|
||||
* merge strategy rules
|
||||
* solitaire_sync models
|
||||
* §2.6 Sync Rules
|
||||
|
||||
---
|
||||
|
||||
### optimization
|
||||
|
||||
Include:
|
||||
|
||||
* target crate only
|
||||
* §5.4 Performance Rules
|
||||
* hot path constraints
|
||||
|
||||
---
|
||||
|
||||
### test
|
||||
|
||||
Include:
|
||||
|
||||
* §6 Build Rules
|
||||
* relevant module
|
||||
* expected invariants
|
||||
|
||||
---
|
||||
|
||||
### debug
|
||||
|
||||
Include:
|
||||
|
||||
* target file/module only
|
||||
* §2.3 Error Policy
|
||||
* runtime assumptions relevant to failure
|
||||
|
||||
---
|
||||
|
||||
## 14.5 Context Compression Rules
|
||||
|
||||
Claude MUST obey:
|
||||
|
||||
* never include full ARCHITECTURE.md unless system_design
|
||||
* max 2 crates per response unless explicitly required
|
||||
* prefer function-level context over file-level context
|
||||
* exclude unrelated plugins/systems
|
||||
|
||||
---
|
||||
|
||||
## 14.6 Context Priority Order
|
||||
|
||||
When space is limited:
|
||||
|
||||
1. Hard Constraints (§2)
|
||||
2. Target crate rules
|
||||
3. Data models
|
||||
4. Only then: architecture snippets
|
||||
|
||||
---
|
||||
|
||||
## 14.7 “No Context Pollution” Rule
|
||||
|
||||
Claude must NOT include:
|
||||
|
||||
* unrelated crates
|
||||
* unrelated plugins
|
||||
* unused data models
|
||||
* full architecture dumps
|
||||
* speculative systems
|
||||
|
||||
---
|
||||
|
||||
## 14.8 Self-Check Before Execution
|
||||
|
||||
Before writing code, Claude MUST verify:
|
||||
|
||||
* [ ] Is only relevant context included?
|
||||
* [ ] Is at least one hard constraint present?
|
||||
* [ ] Am I touching more than one crate unnecessarily?
|
||||
* [ ] Am I duplicating ARCHITECTURE.md content?
|
||||
|
||||
If any fail → revise context selection.
|
||||
|
||||
---
|
||||
|
||||
## 14.9 Injection Output Format (Internal Model)
|
||||
|
||||
Claude should behave as if it constructed:
|
||||
|
||||
```text id="ctx_format"
|
||||
[SELECTED TASK TYPE]
|
||||
|
||||
[MINIMAL REQUIRED RULES]
|
||||
|
||||
[MINIMAL ARCHITECTURE SLICES]
|
||||
|
||||
[RELEVANT MODELS]
|
||||
|
||||
[REQUEST]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14.10 Relationship to ARCHITECTURE.md
|
||||
|
||||
* ARCHITECTURE.md = source of truth
|
||||
* CLAUDE.md = execution constraints
|
||||
* THIS SECTION = filtering layer between them
|
||||
|
||||
---
|
||||
|
||||
# END CONTEXT INJECTION SYSTEM
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
# CLAUDE_PROMPT_PACK.md
|
||||
|
||||
version: 1.0
|
||||
|
||||
---
|
||||
|
||||
# 0. GLOBAL INSTRUCTION (prepend to every prompt)
|
||||
|
||||
```
|
||||
You must follow CLAUDE_SPEC.md strictly.
|
||||
|
||||
Rules:
|
||||
- Do not expand scope beyond what is defined
|
||||
- Do not refactor unrelated code
|
||||
- Do not introduce new dependencies
|
||||
- Prefer minimal, surgical changes
|
||||
- Use existing patterns in the codebase
|
||||
- Return minimal diffs or changed functions only
|
||||
|
||||
Before writing code:
|
||||
1. List relevant constraints from CLAUDE_SPEC.md
|
||||
2. Identify risks
|
||||
3. Then implement
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 1. FEATURE IMPLEMENTATION
|
||||
|
||||
```
|
||||
# TASK: Feature Implementation
|
||||
|
||||
feature: "<name>"
|
||||
|
||||
goal:
|
||||
"<clear outcome>"
|
||||
|
||||
scope:
|
||||
crates: []
|
||||
systems: []
|
||||
files: []
|
||||
|
||||
non_goals:
|
||||
- ""
|
||||
|
||||
constraints:
|
||||
- must follow CLAUDE_SPEC.md
|
||||
- event-driven architecture required
|
||||
- no blocking operations
|
||||
- no cross-crate leakage
|
||||
|
||||
acceptance_criteria:
|
||||
- ""
|
||||
- ""
|
||||
|
||||
edge_cases:
|
||||
- ""
|
||||
|
||||
---
|
||||
|
||||
## Required Patterns
|
||||
|
||||
Use this pattern for systems:
|
||||
<PASTE EXISTING SYSTEM SNIPPET HERE>
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
intent:
|
||||
plan:
|
||||
constraints_used:
|
||||
risks:
|
||||
|
||||
code_changes:
|
||||
(minimal diffs only)
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2. BUGFIX
|
||||
|
||||
```
|
||||
# TASK: Bug Fix
|
||||
|
||||
bug_description:
|
||||
"<what is broken>"
|
||||
|
||||
expected_behavior:
|
||||
"<correct behavior>"
|
||||
|
||||
root_cause_hint (optional):
|
||||
""
|
||||
|
||||
scope:
|
||||
crates: []
|
||||
files: []
|
||||
|
||||
constraints:
|
||||
- minimal fix only
|
||||
- no refactors unless required
|
||||
- must add regression protection if applicable
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Identify root cause
|
||||
2. Fix it minimally
|
||||
3. Preserve all invariants
|
||||
4. Do not change unrelated logic
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
analysis:
|
||||
root_cause:
|
||||
fix_strategy:
|
||||
|
||||
code_changes:
|
||||
(minimal diff)
|
||||
|
||||
regression_test (only if high-value):
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. REFACTOR
|
||||
|
||||
```
|
||||
# TASK: Refactor
|
||||
|
||||
target:
|
||||
"<what is being improved>"
|
||||
|
||||
goal:
|
||||
"<what improves>"
|
||||
|
||||
scope:
|
||||
crates: []
|
||||
files: []
|
||||
|
||||
non_goals:
|
||||
- no behavior changes
|
||||
- no new features
|
||||
|
||||
constraints:
|
||||
- must preserve behavior exactly
|
||||
- must respect crate boundaries
|
||||
- must not duplicate logic
|
||||
|
||||
---
|
||||
|
||||
## Refactor Type
|
||||
|
||||
- [ ] simplify logic
|
||||
- [ ] reduce duplication
|
||||
- [ ] improve readability
|
||||
- [ ] performance (non-invasive)
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
analysis:
|
||||
issues_found:
|
||||
|
||||
refactor_plan:
|
||||
|
||||
code_changes:
|
||||
(diff only)
|
||||
|
||||
verification:
|
||||
- behavior unchanged: yes/no
|
||||
- invariants preserved: yes/no
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. SYSTEM DESIGN (NEW FEATURE)
|
||||
|
||||
```
|
||||
# TASK: System Design
|
||||
|
||||
feature:
|
||||
"<name>"
|
||||
|
||||
goal:
|
||||
"<what problem it solves>"
|
||||
|
||||
constraints:
|
||||
- must fit existing architecture
|
||||
- must follow plugin + event model
|
||||
- must not violate crate boundaries
|
||||
|
||||
---
|
||||
|
||||
## Required Output
|
||||
|
||||
design:
|
||||
|
||||
components:
|
||||
- plugins:
|
||||
- systems:
|
||||
- events:
|
||||
- resources:
|
||||
|
||||
data_flow:
|
||||
(step-by-step)
|
||||
|
||||
integration_points:
|
||||
- where it connects to existing systems
|
||||
|
||||
risks:
|
||||
- ""
|
||||
|
||||
tradeoffs:
|
||||
- ""
|
||||
|
||||
---
|
||||
|
||||
## DO NOT
|
||||
|
||||
- write full implementation
|
||||
- modify unrelated systems
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 5. NEW BEVY SYSTEM
|
||||
|
||||
```
|
||||
# TASK: Add Bevy System
|
||||
|
||||
system_name:
|
||||
""
|
||||
|
||||
trigger:
|
||||
(event or condition)
|
||||
|
||||
reads:
|
||||
[Resources]
|
||||
|
||||
writes:
|
||||
[Resources]
|
||||
|
||||
emits:
|
||||
[Events]
|
||||
|
||||
constraints:
|
||||
- must be event-driven
|
||||
- must not directly mutate unrelated state
|
||||
- must be single responsibility
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
system_signature:
|
||||
|
||||
implementation:
|
||||
(code only)
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. CORE LOGIC FUNCTION (solitaire_core)
|
||||
|
||||
```
|
||||
# TASK: Core Logic Implementation
|
||||
|
||||
function:
|
||||
"<name>"
|
||||
|
||||
goal:
|
||||
"<what it does>"
|
||||
|
||||
rules:
|
||||
- no IO
|
||||
- no async
|
||||
- no Bevy
|
||||
- deterministic
|
||||
|
||||
invariants:
|
||||
- ""
|
||||
- ""
|
||||
|
||||
errors:
|
||||
- ""
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
constraints_checked:
|
||||
|
||||
implementation:
|
||||
(code only)
|
||||
|
||||
edge_case_handling:
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 7. SYNC / MERGE LOGIC
|
||||
|
||||
```
|
||||
# TASK: Sync Logic
|
||||
|
||||
goal:
|
||||
"<what is being merged or synced>"
|
||||
|
||||
constraints:
|
||||
- must be deterministic
|
||||
- must be idempotent
|
||||
- must be lossless
|
||||
- must not delete data
|
||||
|
||||
rules:
|
||||
- counters → max
|
||||
- times → min
|
||||
- collections → union
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
analysis:
|
||||
|
||||
merge_logic:
|
||||
|
||||
code_changes:
|
||||
|
||||
invariants_verified:
|
||||
- deterministic
|
||||
- idempotent
|
||||
- lossless
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 8. PERFORMANCE OPTIMIZATION
|
||||
|
||||
```
|
||||
# TASK: Optimization
|
||||
|
||||
target:
|
||||
"<what is slow>"
|
||||
|
||||
constraints:CLAUDE_WORKFLOW.md
|
||||
- no behavior change
|
||||
- no architecture change
|
||||
- minimal code changes
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
analysis:
|
||||
bottleneck:
|
||||
|
||||
optimization_strategy:
|
||||
|
||||
code_changes:
|
||||
|
||||
impact_estimate:
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 9. TEST GENERATION (STRICT MODE)
|
||||
|
||||
```
|
||||
# TASK: Test Generation
|
||||
|
||||
target:
|
||||
"<function/system>"
|
||||
|
||||
reason:
|
||||
- bugfix | complex logic | invariant protection
|
||||
|
||||
constraints:
|
||||
- no redundant tests
|
||||
- must test real behavior
|
||||
- must fail if logic breaks
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
test_cases:
|
||||
- ""
|
||||
|
||||
test_code:
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 10. DEBUGGING / INVESTIGATION
|
||||
|
||||
```
|
||||
# TASK: Debug
|
||||
|
||||
problem:
|
||||
"<symptom>"
|
||||
|
||||
context:
|
||||
"<relevant code or system>"
|
||||
|
||||
---
|
||||
|
||||
## Required Steps
|
||||
|
||||
1. List possible causes
|
||||
2. Narrow down most likely
|
||||
3. Suggest verification steps
|
||||
4. Provide minimal fix
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
hypotheses:
|
||||
|
||||
most_likely:
|
||||
|
||||
verification_steps:
|
||||
|
||||
fix:
|
||||
|
||||
notes:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 11. HARD CONSTRAINT OVERRIDE (RARE)
|
||||
|
||||
```
|
||||
# TASK: Exception Handling
|
||||
|
||||
reason:
|
||||
"<why constraints must be bent>"
|
||||
|
||||
requested_exception:
|
||||
"<rule being broken>"
|
||||
|
||||
justification:
|
||||
"<why unavoidable>"
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
analysis:
|
||||
|
||||
alternatives_considered:
|
||||
|
||||
final_decision:
|
||||
|
||||
risk:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 12. STOP CONDITIONS (always append)
|
||||
|
||||
```
|
||||
Stop when:
|
||||
- acceptance criteria are met
|
||||
- code is minimal and correct
|
||||
|
||||
Do NOT:
|
||||
- expand scope
|
||||
- refactor unrelated code
|
||||
- optimize prematurely
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# END
|
||||
@@ -0,0 +1,292 @@
|
||||
# CLAUDE_SPEC.md
|
||||
|
||||
version: 1.0
|
||||
|
||||
---
|
||||
|
||||
## 0. Global Rules
|
||||
|
||||
(Core determinism, panic policy, and event-driven engine constraints live in CLAUDE.md §2.1, §2.3, §3.1. Listed here only when they add information CLAUDE.md doesn't carry.)
|
||||
|
||||
rules:
|
||||
|
||||
* id: single_source_of_truth
|
||||
description: "GameStateResource is the only mutable game state in runtime"
|
||||
|
||||
* id: sync_is_additive
|
||||
description: "Remote data must never destructively overwrite local data"
|
||||
|
||||
---
|
||||
|
||||
## 1. Crate Graph
|
||||
|
||||
crates:
|
||||
solitaire_core:
|
||||
depends_on: [rand, serde, chrono]
|
||||
forbidden_deps: [bevy, reqwest, tokio, std::fs]
|
||||
|
||||
solitaire_sync:
|
||||
depends_on: [serde, serde_json, uuid, chrono]
|
||||
role: "shared_types"
|
||||
|
||||
solitaire_data:
|
||||
depends_on: [solitaire_core, solitaire_sync, reqwest, tokio, keyring]
|
||||
role: "persistence_and_sync"
|
||||
|
||||
solitaire_engine:
|
||||
depends_on: [bevy, kira, solitaire_core, solitaire_data]
|
||||
role: "runtime_engine"
|
||||
|
||||
solitaire_server:
|
||||
depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken]
|
||||
role: "backend"
|
||||
|
||||
solitaire_app:
|
||||
depends_on: [solitaire_engine]
|
||||
role: "entrypoint"
|
||||
|
||||
---
|
||||
|
||||
## 2. Data Ownership
|
||||
|
||||
ownership:
|
||||
GameState:
|
||||
owner: solitaire_core
|
||||
mutable_in: solitaire_engine
|
||||
access_pattern: "via GameStateResource only"
|
||||
|
||||
StatsSnapshot:
|
||||
owner: solitaire_data
|
||||
|
||||
PlayerProgress:
|
||||
owner: solitaire_data
|
||||
|
||||
AchievementRecord:
|
||||
owner: solitaire_data
|
||||
|
||||
SyncPayload:
|
||||
owner: solitaire_sync
|
||||
|
||||
---
|
||||
|
||||
## 3. State Transitions
|
||||
|
||||
state_machine:
|
||||
GameState:
|
||||
transitions:
|
||||
- action: move_cards
|
||||
returns: Result<GameState, MoveError>
|
||||
|
||||
```
|
||||
- action: draw
|
||||
returns: Result<GameState, MoveError>
|
||||
|
||||
- action: undo
|
||||
returns: Result<GameState, MoveError>
|
||||
|
||||
invariants:
|
||||
- "52 cards always exist"
|
||||
- "no duplicate card IDs"
|
||||
- "all cards belong to exactly one pile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Event System
|
||||
|
||||
events:
|
||||
|
||||
input:
|
||||
- MoveRequestEvent
|
||||
- DrawRequestEvent
|
||||
- UndoRequestEvent
|
||||
- NewGameRequestEvent
|
||||
|
||||
state:
|
||||
- StateChangedEvent
|
||||
- GameWonEvent
|
||||
|
||||
meta:
|
||||
- AchievementUnlockedEvent
|
||||
- SyncCompleteEvent
|
||||
|
||||
rules:
|
||||
|
||||
* "Input events trigger core logic"
|
||||
* "Core logic emits state events"
|
||||
* "UI reacts to state events only"
|
||||
|
||||
---
|
||||
|
||||
## 5. Sync Contract
|
||||
|
||||
sync:
|
||||
|
||||
provider_trait:
|
||||
methods:
|
||||
- pull() -> SyncPayload
|
||||
- push(payload) -> SyncResponse
|
||||
|
||||
guarantees:
|
||||
- "non-blocking during gameplay"
|
||||
- "blocking allowed on exit only"
|
||||
|
||||
merge:
|
||||
rules:
|
||||
counters: "max"
|
||||
best_times: "min"
|
||||
collections: "union"
|
||||
achievements: "never removed"
|
||||
|
||||
```
|
||||
properties:
|
||||
- deterministic
|
||||
- idempotent
|
||||
- lossless
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Persistence
|
||||
|
||||
storage:
|
||||
|
||||
format: json
|
||||
|
||||
files:
|
||||
- stats.json
|
||||
- progress.json
|
||||
- achievements.json
|
||||
- settings.json
|
||||
- game_state.json
|
||||
|
||||
guarantees:
|
||||
- atomic_write: true
|
||||
- crash_safe: true
|
||||
|
||||
---
|
||||
|
||||
## 7. Engine Rules
|
||||
|
||||
engine:
|
||||
|
||||
mutation_rules:
|
||||
- "Only GameLogicSystem mutates GameState"
|
||||
- "UI systems are read-only"
|
||||
|
||||
threading:
|
||||
- "sync runs on AsyncComputeTaskPool"
|
||||
- "main thread must never block"
|
||||
|
||||
plugins:
|
||||
pattern: "feature_isolation"
|
||||
communication: "events"
|
||||
|
||||
---
|
||||
|
||||
## 8. Server Contract
|
||||
|
||||
server:
|
||||
|
||||
auth:
|
||||
method: jwt
|
||||
access_expiry: 24h
|
||||
refresh_expiry: 30d
|
||||
|
||||
endpoints:
|
||||
- POST /api/auth/register
|
||||
- POST /api/auth/login
|
||||
- GET /api/sync/pull
|
||||
- POST /api/sync/push
|
||||
|
||||
limits:
|
||||
payload_max: 1MB
|
||||
rate_limit: "10 req/min auth routes"
|
||||
|
||||
---
|
||||
|
||||
## 9. Achievement System
|
||||
|
||||
achievements:
|
||||
|
||||
definition_location: solitaire_core
|
||||
state_location: solitaire_data
|
||||
|
||||
types:
|
||||
- condition_based
|
||||
- event_driven
|
||||
|
||||
rule:
|
||||
- "achievements cannot be revoked"
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing Rules
|
||||
|
||||
testing:
|
||||
|
||||
philosophy:
|
||||
- "test real failures"
|
||||
- "avoid redundant tests"
|
||||
|
||||
required_coverage:
|
||||
solitaire_core:
|
||||
- move_validation
|
||||
- undo_integrity
|
||||
- win_detection
|
||||
|
||||
```
|
||||
solitaire_sync:
|
||||
- merge_correctness
|
||||
- idempotency
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Prohibited Patterns
|
||||
|
||||
(See CLAUDE.md §11 for the canonical forbidden-patterns list.)
|
||||
|
||||
---
|
||||
|
||||
## 12. Extension Points
|
||||
|
||||
extensibility:
|
||||
|
||||
sync_backends:
|
||||
pattern: "implement SyncProvider"
|
||||
|
||||
game_modes:
|
||||
location: solitaire_core::GameMode
|
||||
|
||||
plugins:
|
||||
rule: "new feature = new plugin"
|
||||
|
||||
---
|
||||
|
||||
## 13. Validation Checklist (for Claude)
|
||||
|
||||
validation:
|
||||
|
||||
* check: "crate dependency rules respected"
|
||||
* check: "no panics in core"
|
||||
* check: "events used for cross-system communication"
|
||||
* check: "GameState mutations centralized"
|
||||
* check: "merge function properties preserved"
|
||||
* check: "no blocking operations in main loop"
|
||||
|
||||
---
|
||||
|
||||
## 14. Mental Model
|
||||
|
||||
model:
|
||||
|
||||
layers:
|
||||
- core
|
||||
- engine
|
||||
- data
|
||||
- server
|
||||
|
||||
flow:
|
||||
- input -> engine -> core -> engine -> ui
|
||||
- data <-> sync <-> server
|
||||
@@ -0,0 +1,335 @@
|
||||
# CLAUDE_WORKFLOW.md
|
||||
|
||||
version: 1.0
|
||||
|
||||
---
|
||||
|
||||
## 0. Overview
|
||||
|
||||
This workflow defines a **two-agent system**:
|
||||
|
||||
* **Builder Agent** → writes and modifies code
|
||||
* **Guardian Agent** → enforces architecture + rejects invalid changes
|
||||
|
||||
No code is considered valid unless it passes Guardian validation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Agent Roles
|
||||
|
||||
### 1.1 Builder Agent
|
||||
|
||||
role: "code_generation"
|
||||
|
||||
responsibilities:
|
||||
|
||||
* implement features
|
||||
* refactor code
|
||||
* generate tests (only when justified)
|
||||
* follow CLAUDE_SPEC.md
|
||||
|
||||
constraints:
|
||||
|
||||
* cannot bypass validation
|
||||
* must declare intent before writing code
|
||||
|
||||
output_contract:
|
||||
must_produce:
|
||||
- change_summary
|
||||
- files_modified
|
||||
- reasoning (short)
|
||||
- code_diff
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Guardian Agent
|
||||
|
||||
role: "architecture_enforcement"
|
||||
|
||||
responsibilities:
|
||||
|
||||
* validate against CLAUDE_SPEC.md
|
||||
* detect violations
|
||||
* reject or approve changes
|
||||
* suggest minimal fixes (not full rewrites)
|
||||
|
||||
constraints:
|
||||
|
||||
* no feature implementation
|
||||
* no large rewrites
|
||||
* must be deterministic
|
||||
|
||||
output_contract:
|
||||
must_produce:
|
||||
- status: APPROVED | REJECTED
|
||||
- violations[]
|
||||
- required_fixes[]
|
||||
- optional_improvements[]
|
||||
|
||||
---
|
||||
|
||||
## 2. Workflow Pipeline
|
||||
|
||||
```text
|
||||
User Request
|
||||
↓
|
||||
Builder Agent (proposal + code)
|
||||
↓
|
||||
Guardian Agent (validation)
|
||||
↓
|
||||
IF approved → commit
|
||||
IF rejected → feedback → Builder retry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Builder Protocol
|
||||
|
||||
### Step 1 — Intent Declaration
|
||||
|
||||
Builder MUST start with:
|
||||
|
||||
```yaml
|
||||
intent:
|
||||
feature: "<name>"
|
||||
crates_touched: []
|
||||
systems_affected: []
|
||||
risk_level: low|medium|high
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Plan
|
||||
|
||||
```yaml
|
||||
plan:
|
||||
- step: "..."
|
||||
- step: "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Implementation
|
||||
|
||||
* Only modify declared crates
|
||||
* Follow ownership rules
|
||||
* Use events for cross-system communication
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Output
|
||||
|
||||
```yaml
|
||||
change_summary: "..."
|
||||
|
||||
files_modified:
|
||||
- path: ...
|
||||
change: "..."
|
||||
|
||||
violations_self_check:
|
||||
- none | list
|
||||
|
||||
notes: "short reasoning"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Guardian Protocol
|
||||
|
||||
### Step 1 — Spec Validation
|
||||
|
||||
Check against:
|
||||
|
||||
* crate boundaries
|
||||
* mutation rules
|
||||
* event system usage
|
||||
* sync guarantees
|
||||
* forbidden patterns
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Invariant Validation
|
||||
|
||||
Must verify:
|
||||
|
||||
* GameState invariants preserved
|
||||
* no new panic paths
|
||||
* no blocking calls in engine
|
||||
* merge properties unchanged
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Output Decision
|
||||
|
||||
#### APPROVED
|
||||
|
||||
```yaml
|
||||
status: APPROVED
|
||||
|
||||
notes:
|
||||
- "no violations"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### REJECTED
|
||||
|
||||
```yaml
|
||||
status: REJECTED
|
||||
|
||||
violations:
|
||||
- id: core_purity_violation
|
||||
file: "solitaire_core/src/..."
|
||||
reason: "uses std::fs"
|
||||
|
||||
required_fixes:
|
||||
- "move IO to solitaire_data"
|
||||
|
||||
optional_improvements:
|
||||
- "simplify event naming"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Enforcement Rules
|
||||
|
||||
### Hard Fail (automatic rejection)
|
||||
|
||||
* core crate uses IO / Bevy / network
|
||||
* GameState mutated outside GameLogicSystem
|
||||
* blocking async on main thread
|
||||
* duplicate logic across crates
|
||||
* merge function altered incorrectly
|
||||
|
||||
---
|
||||
|
||||
### Soft Fail (allowed but flagged)
|
||||
|
||||
* unnecessary complexity
|
||||
* redundant tests
|
||||
* minor architectural drift
|
||||
|
||||
---
|
||||
|
||||
## 6. Iteration Loop
|
||||
|
||||
Max attempts per task: **3**
|
||||
|
||||
```text
|
||||
Attempt 1 → Reject → Fix
|
||||
Attempt 2 → Reject → Fix
|
||||
Attempt 3 → Final decision
|
||||
```
|
||||
|
||||
If still failing:
|
||||
→ escalate to user
|
||||
|
||||
---
|
||||
|
||||
## 7. Diff Strategy
|
||||
|
||||
Builder MUST produce:
|
||||
|
||||
* minimal diffs
|
||||
* no unrelated refactors
|
||||
* no formatting-only changes
|
||||
|
||||
---
|
||||
|
||||
## 8. Test Strategy Integration
|
||||
|
||||
Builder rules:
|
||||
|
||||
* only add tests if:
|
||||
|
||||
* fixing a bug
|
||||
* protecting complex logic
|
||||
* validating invariants
|
||||
|
||||
Guardian rejects:
|
||||
|
||||
* redundant tests
|
||||
* no-op tests
|
||||
|
||||
---
|
||||
|
||||
## 9. Optional Extensions
|
||||
|
||||
### 9.1 Third Agent (Optimizer)
|
||||
|
||||
role: performance + cleanup
|
||||
|
||||
runs AFTER approval:
|
||||
|
||||
* reduce allocations
|
||||
* simplify logic
|
||||
* improve ECS scheduling
|
||||
|
||||
---
|
||||
|
||||
### 9.2 CI Integration
|
||||
|
||||
Pipeline:
|
||||
|
||||
```text
|
||||
Builder → Guardian → cargo check → clippy → tests
|
||||
```
|
||||
|
||||
Guardian runs BEFORE compilation to catch structural issues early.
|
||||
|
||||
---
|
||||
|
||||
## 10. Example Interaction
|
||||
|
||||
### Builder
|
||||
|
||||
```yaml
|
||||
intent:
|
||||
feature: "undo stack limit fix"
|
||||
crates_touched: [solitaire_core]
|
||||
risk_level: low
|
||||
```
|
||||
|
||||
```yaml
|
||||
change_summary: "limit undo stack to 64 entries"
|
||||
|
||||
files_modified:
|
||||
- solitaire_core/src/game_state.rs
|
||||
|
||||
notes: "prevents unbounded memory growth"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Guardian
|
||||
|
||||
```yaml
|
||||
status: APPROVED
|
||||
|
||||
notes:
|
||||
- "respects core constraints"
|
||||
- "no invariant violations"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Mental Model
|
||||
|
||||
* Builder = **creative**
|
||||
* Guardian = **strict**
|
||||
|
||||
Builder explores
|
||||
Guardian enforces
|
||||
|
||||
Neither replaces the other.
|
||||
|
||||
---
|
||||
|
||||
## 12. Success Criteria
|
||||
|
||||
System is working if:
|
||||
|
||||
* architectural violations go to ~0
|
||||
* code stays consistent across features
|
||||
* refactors become safe
|
||||
* complexity grows sub-linearly
|
||||
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"accesskit_consumer",
|
||||
"hashbrown 0.15.5",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
@@ -126,6 +126,19 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -313,6 +326,23 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"log",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.60.2",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.1"
|
||||
@@ -702,7 +732,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"console_error_panic_hook",
|
||||
"ctrlc",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"log",
|
||||
"thiserror 2.0.18",
|
||||
"variadics_please",
|
||||
@@ -736,7 +766,7 @@ dependencies = [
|
||||
"crossbeam-channel",
|
||||
"derive_more",
|
||||
"disqualified",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"either",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
@@ -784,7 +814,7 @@ dependencies = [
|
||||
"bevy_utils",
|
||||
"bevy_window",
|
||||
"derive_more",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
@@ -1183,7 +1213,7 @@ dependencies = [
|
||||
"bevy_utils",
|
||||
"derive_more",
|
||||
"disqualified",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"erased-serde",
|
||||
"foldhash 0.2.0",
|
||||
"glam 0.30.10",
|
||||
@@ -1242,7 +1272,7 @@ dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bytemuck",
|
||||
"derive_more",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"encase",
|
||||
"fixedbitset",
|
||||
"glam 0.30.10",
|
||||
@@ -1837,6 +1867,18 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
dependencies = [
|
||||
"calloop",
|
||||
"rustix 0.38.44",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -1972,6 +2014,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
@@ -2683,6 +2734,12 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "2.0.2"
|
||||
@@ -2882,6 +2939,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
@@ -4968,6 +5031,18 @@ dependencies = [
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
@@ -5065,6 +5140,19 @@ dependencies = [
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"dispatch2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-image"
|
||||
version = "0.2.2"
|
||||
@@ -5121,6 +5209,17 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-link-presentation"
|
||||
version = "0.2.2"
|
||||
@@ -5129,7 +5228,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
|
||||
dependencies = [
|
||||
"block2 0.5.1",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
]
|
||||
|
||||
@@ -5724,6 +5823,15 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
@@ -6097,7 +6205,7 @@ dependencies = [
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.12.0",
|
||||
"usvg",
|
||||
"zune-jpeg",
|
||||
]
|
||||
@@ -6434,6 +6542,19 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sctk-adwaita"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"log",
|
||||
"memmap2",
|
||||
"smithay-client-toolkit",
|
||||
"tiny-skia 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -6778,6 +6899,31 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cursor-icon",
|
||||
"libc",
|
||||
"log",
|
||||
"memmap2",
|
||||
"rustix 0.38.44",
|
||||
"thiserror 1.0.69",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-csd-frame",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
"wayland-scanner",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.2"
|
||||
@@ -6811,6 +6957,8 @@ dependencies = [
|
||||
"keyring",
|
||||
"solitaire_data",
|
||||
"solitaire_engine",
|
||||
"tiny-skia 0.12.0",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6856,6 +7004,7 @@ dependencies = [
|
||||
name = "solitaire_engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"async-trait",
|
||||
"bevy",
|
||||
"chrono",
|
||||
@@ -6869,7 +7018,7 @@ dependencies = [
|
||||
"solitaire_sync",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.12.0",
|
||||
"tokio",
|
||||
"usvg",
|
||||
"uuid",
|
||||
@@ -7456,7 +7605,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-channel",
|
||||
"datasketches",
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"fastdivide",
|
||||
"fnv",
|
||||
"fs4",
|
||||
@@ -7508,7 +7657,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc"
|
||||
dependencies = [
|
||||
"downcast-rs",
|
||||
"downcast-rs 2.0.2",
|
||||
"fastdivide",
|
||||
"itertools 0.14.0",
|
||||
"serde",
|
||||
@@ -7696,6 +7845,20 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tiny-skia-path 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
@@ -7708,7 +7871,18 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.18.1",
|
||||
"tiny-skia-path",
|
||||
"tiny-skia-path 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8479,7 +8653,7 @@ dependencies = [
|
||||
"siphasher",
|
||||
"strict-num",
|
||||
"svgtypes",
|
||||
"tiny-skia-path",
|
||||
"tiny-skia-path 0.12.0",
|
||||
"ttf-parser",
|
||||
"unicode-bidi",
|
||||
"unicode-script",
|
||||
@@ -8685,6 +8859,114 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs 1.2.1",
|
||||
"rustix 1.1.4",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"rustix 1.1.4",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-csd-frame"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"cursor-icon",
|
||||
"wayland-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-cursor"
|
||||
version = "0.31.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d"
|
||||
dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"wayland-client",
|
||||
"xcursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.97"
|
||||
@@ -9500,6 +9782,7 @@ version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.11.1",
|
||||
@@ -9514,9 +9797,10 @@ dependencies = [
|
||||
"dpi",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"memmap2",
|
||||
"ndk",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-app-kit 0.2.2",
|
||||
"objc2-foundation 0.2.2",
|
||||
"objc2-ui-kit",
|
||||
"orbclient",
|
||||
@@ -9525,11 +9809,17 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.44",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"web-sys",
|
||||
"web-time",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -9697,6 +9987,12 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xcursor"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b"
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon-dl"
|
||||
version = "0.4.2"
|
||||
|
||||
@@ -30,6 +30,7 @@ dirs = "6"
|
||||
keyring = "4"
|
||||
keyring-core = "1"
|
||||
reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
|
||||
arboard = { version = "3", default-features = false }
|
||||
|
||||
solitaire_core = { path = "solitaire_core" }
|
||||
solitaire_sync = { path = "solitaire_sync" }
|
||||
@@ -53,12 +54,24 @@ bevy = { version = "0.18", default-features = false, features = [
|
||||
"bevy_window",
|
||||
"custom_cursor",
|
||||
"reflect_auto_register",
|
||||
# default_platform (desktop subset; no android/wayland/webgl/gilrs/sysinfo)
|
||||
# default_platform (desktop subset)
|
||||
"std",
|
||||
"bevy_winit",
|
||||
"default_font",
|
||||
"multi_threaded",
|
||||
# winit prefers Wayland when WAYLAND_DISPLAY is set on the
|
||||
# session and falls through to X11 otherwise. Without `wayland`,
|
||||
# winit-on-Wayland-session falls back to XWayland which renders
|
||||
# the game in an X11 frame inside the Wayland compositor.
|
||||
"wayland",
|
||||
"x11",
|
||||
# Android: NativeActivity glue. The feature is target-gated inside
|
||||
# bevy_internal — desktop builds compile it out, so leaving it on
|
||||
# the always-on list is harmless on Linux/macOS/Windows. Pairs with
|
||||
# cargo-apk's NativeActivity wrapper (cargo-apk 0.10+ uses this by
|
||||
# default). Switch to `android-game-activity` later if we want
|
||||
# AndroidX GameActivity for Google Play Games integration.
|
||||
"android-native-activity",
|
||||
# common_api
|
||||
"bevy_color",
|
||||
"bevy_image",
|
||||
|
||||
@@ -1,140 +1,366 @@
|
||||
# Solitaire Quest — Session Handoff
|
||||
|
||||
**Last updated:** 2026-05-02 (session 9, post-v0.14.0 release prep) — v0.14.0 cut. The Quat bug fixes, the rest of the v0.13.0 candidate list, and the entire replay → upload → web-viewer pipeline are all bundled in this release. Direction now opens for the next round.
|
||||
**Last updated:** 2026-05-08 — **v0.21.6 cut and tagged at
|
||||
`f63db76`**, working tree clean, all post-tag work pushed to
|
||||
origin.
|
||||
|
||||
v0.21.6 is a patch release with through-line:
|
||||
**Move Log panel + scrub-UX polish**. v0.21.5 closed out the
|
||||
keyboard-accelerator surface (Space / Esc / ← / →) and the
|
||||
keybind footer; v0.21.6 builds on that with two parallel
|
||||
threads — accessibility + scrub-on-hold polish for the v0.21.5
|
||||
surfaces, plus a brand-new Move Log panel anchored to the
|
||||
viewport's bottom edge that gives players a 5-row recent-and-
|
||||
upcoming move history alongside the existing top-edge banner.
|
||||
|
||||
The Move Log panel is the first replay-overlay surface that
|
||||
*isn't* attached to the banner — it lives at a separate screen
|
||||
anchor (`bottom: 0`) with its own spawn/despawn lifecycle.
|
||||
Establishes the "multi-anchor replay UI" pattern that the
|
||||
remaining B-2 sub-piece (mini-tableau preview) will inherit.
|
||||
|
||||
Six commits on the B-2 replay screen-takeover redesign arc land
|
||||
here, bringing the post-v0.21.4 total to 12. The remaining B-2
|
||||
piece — mini-tableau preview that dims the gameplay tableau
|
||||
during replay — is the only major sub-piece still open.
|
||||
|
||||
Full v0.21.6 detail lives in `CHANGELOG.md` § [0.21.6]. This
|
||||
file from here on focuses on what's *open* post-cut and how to
|
||||
resume.
|
||||
|
||||
## Status at pause
|
||||
|
||||
- **HEAD on origin:** v0.14.0's tag commit (CHANGELOG + handoff refresh).
|
||||
- **Working tree:** clean apart from untracked `CARD_PLAN.md` (intentional).
|
||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
|
||||
- **Tests:** **1134 passed / 0 failed** across the workspace.
|
||||
- **Tags on origin:** `v0.9.0`, `v0.10.0`, `v0.11.0`, `v0.12.0`, `v0.13.0`, `v0.14.0`.
|
||||
- **HEAD locally:** see `git rev-parse HEAD`. The cut commit is
|
||||
`f63db76`; any post-cut docs edits ride on top of that.
|
||||
- **HEAD on origin:** matches local. v0.21.6 is fully on origin.
|
||||
- **Working tree:** clean. No WIP outstanding.
|
||||
- **`artwork/` directory:** still untracked. Intentional.
|
||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
clean.
|
||||
- **Tests:** **1273 passing / 0 failing** across the workspace.
|
||||
Detail in `CHANGELOG.md` § [0.21.6] § Stats.
|
||||
- **Tags on origin:** `v0.9.0` through `v0.21.6`. v0.21.6 is on
|
||||
`f63db76`; v0.21.5 stays on `a2432df`; v0.21.4 stays on
|
||||
`23ff62c`; v0.21.3 stays on `3d92a91`; v0.21.2 stays on
|
||||
`f23df3b`; v0.21.1 stays on `daa655a`; v0.21.0 stays on
|
||||
`04f9bf9`; v0.20.0 stays on `41a009a`.
|
||||
|
||||
## Where we are
|
||||
## Since the v0.21.6 cut
|
||||
|
||||
v0.14.0 is the largest release since the card-theme system. Three threads land together:
|
||||
No threads in flight. Working tree clean as of 2026-05-08. New
|
||||
work since the cut would land here as commit narratives; for
|
||||
the v0.21.6 contents themselves, see `CHANGELOG.md` § [0.21.6].
|
||||
|
||||
1. **The remaining v0.13.0-era UX candidates** — theme thumbnails, daily-challenge calendar, Time Attack auto-save, per-mode bests, time-bonus multiplier slider.
|
||||
2. **Quat smoke-test bug fixes** — multi-card move validation, softlock detection, deal-tween information leak.
|
||||
3. **The replay pipeline** — record on win, persist to disk, upload to server, view in browser via a new `solitaire_wasm` crate. The biggest single feature since the card-theme system.
|
||||
Open next-step menu (Move Log + scrub-UX + keyboard accelerator
|
||||
coverage + accessibility are all complete):
|
||||
1. **Mini-tableau preview** — the only remaining major B-2
|
||||
sub-piece. Mockup shows a 240 px-tall band at 50 % opacity
|
||||
showing the gameplay tableau peeking through the replay
|
||||
chrome. Implementation needs to add a settings-aware dim
|
||||
overlay or alpha modulation on the tableau cards during
|
||||
replay. Architectural — touches `card_plugin` rendering.
|
||||
Multi-session.
|
||||
2. **Move Log auto-scroll** — only relevant if the panel's
|
||||
row count grows beyond the current 5-row fixed window.
|
||||
Currently the prev-2 / active / next-2 layout fits all
|
||||
visible content, so auto-scroll is unneeded. Becomes
|
||||
relevant if a future commit expands the panel's row
|
||||
capacity (e.g. 10-row scrolling list).
|
||||
3. **Polish: notch label centering.** Bevy 0.18 lacks a
|
||||
clean `translate-x: -50%` primitive so middle three
|
||||
labels sit slightly right-of-notch. Could use a child
|
||||
Text wrapper with computed left-margin compensation.
|
||||
Tiny commit, requires visual review.
|
||||
4. **Polish: WIN MOVE marker HC bump.** Currently uses
|
||||
`STATE_SUCCESS` lime which stays visible under HC, but a
|
||||
contrast bump under HC would make it even more legible
|
||||
alongside the bumped notches. Optional.
|
||||
|
||||
The card-flight web animations and replay E2E test coverage close out the pipeline.
|
||||
|
||||
### Design direction (unchanged)
|
||||
|
||||
- **Tone:** Balatro — chunky readable type, theatrical hierarchy, satisfying micro-interactions.
|
||||
- **Palette:** Midnight Purple base + Balatro yellow primary + warm magenta secondary.
|
||||
- See `~/.claude/projects/-home-manage-Rusty-Solitare/memory/project_ux_overhaul_2026-04.md` (machine-local).
|
||||
|
||||
### Canonical remote
|
||||
|
||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo. Always push there.
|
||||
|
||||
## Session 8 + 9 (shipped 2026-05-02) — v0.14.0
|
||||
|
||||
### v0.13.0-era UX candidates (had landed but missed v0.13.0's tag)
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Theme thumbnails | `ba527de` | Each Settings → Cosmetic theme chip renders an Ace + back preview pair via `rasterize_svg`. Cached per theme. Missing-SVG themes show a transparent placeholder rather than crashing. |
|
||||
| Daily-challenge calendar | `1a10476` | 14-dot horizontal calendar in the Profile modal. Today is ringed, completed days fill `STATE_SUCCESS`, missed days fill `BG_ELEVATED`. Caption: "Current streak: N · Longest: M". `PlayerProgress` gains `daily_challenge_history` (capped at 365) and `daily_challenge_longest_streak`. |
|
||||
| Time Attack auto-save | `0001432` | New sibling `time_attack_session.json` next to `game_state.json`. Atomic .tmp + rename. 30 s auto-save while active + on `AppExit`. Sessions whose 10-min window expired in real time while the app was closed are discarded on load. |
|
||||
| Per-mode bests | `3984231` | StatsSnapshot gains six `#[serde(default)]` fields (Classic / Zen / Challenge × best_score + fastest_win_seconds). Stats screen renders a "Per-mode bests" section. Lifetime totals continue to roll all modes together. |
|
||||
| Time-bonus slider | `89c51ab` | Settings → Gameplay slider 0.0–2.0, default 1.0, "Off" at zero. Multiplies the time-bonus shown in the win modal. Cosmetic only — does NOT affect achievement unlock thresholds. |
|
||||
|
||||
### Quat smoke-test bug fixes
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Move validation (#1) | `f1aeb24` | `solitaire_core::rules::is_valid_tableau_sequence(&[Card]) -> bool` checks every adjacent pair in a moved stack descends one rank with alternating colour. Wired into `move_cards`. Closes the bug where any multi-card lift could be dropped as long as the bottom landed legally. |
|
||||
| Deal-tween leak (#4) | `3eabc14` | New-game snaps every card sprite to the stock pile position before writing `StateChangedEvent`, so all 52 cards animate from a single deck point during the deal. Previously sprites started from previous-game positions, briefly revealing the prior deal. |
|
||||
| Softlock detection (#2) | `2716472` | `has_legal_moves` rewritten: walks every potential move source (every stock card, every waste card, the face-up top of every tableau column) against every foundation and every tableau. Previous heuristic returned `true` whenever stock had cards, hiding genuine softlocks. `GameOverScreen` now actually fires for true softlocks. |
|
||||
| End-game screen (#3) | — | Resolved as downstream of #2. The pre-existing `GameOverScreen` and `WinSummaryOverlay` already cover the close-out paths; the softlock screen just never spawned because the old `has_legal_moves` lied. |
|
||||
|
||||
### Replay pipeline (the major feature)
|
||||
|
||||
| Area | Commit | What landed |
|
||||
|---|---|---|
|
||||
| Replay storage | `42535f5` | `solitaire_data::replay::Replay` (seed + draw_mode + mode + score + time + recorded date + ordered move list) and atomic save/load helpers under `<data_dir>/latest_replay.json`. Schema v1; `load` returns None for any other version. |
|
||||
| Engine recording | `57d1c58` | `RecordingReplay` resource + `ReplayPath` settings. Every successful `MoveRequestEvent` / `DrawRequestEvent` appends to recording; `GameWonEvent` freezes the recording into a `Replay` and persists. Undo intentionally not recorded. New game clears the recording. |
|
||||
| Stats button | `d9f36bf` | Stats overlay surfaces a "Latest win:" caption + "Watch replay" button. Loads from disk via `LatestReplayResource`. (Full in-engine playback deferred — button currently fires an `InfoToastEvent` describing the replay.) |
|
||||
| Server upload + fetch | `93182fa` | `POST /api/replays` accepts a `Replay` JSON; `GET /api/replays/:id` returns it. JWT-gated. SQL migration for the new `replays` table. |
|
||||
| Engine sync | `23c9704` | Engine uploads winning replays automatically when the player has cloud sync configured. Re-uses the existing JWT/refresh-token flow. |
|
||||
| WASM crate | `5bed43e` | New workspace member `solitaire_wasm` compiles replay-relevant `solitaire_core` types to WebAssembly so a browser can re-execute a replay client-side. `wasm-bindgen` glue. |
|
||||
| Web viewer | `07b8ecd` | `GET /replays/:id` returns HTML + CSS + the wasm bundle. Browser fetches the replay JSON, rasterises a deal from the seed, and animates the recorded moves. |
|
||||
| E2E coverage | `3081505` | Server tests covering the full upload → fetch round-trip via `axum::test`. |
|
||||
| Web flight anim | `1fcd032` | Card-flight tweens on the web side so the browser viewer reads as a real game replay rather than a static dump. |
|
||||
Recommended order: option 1 (mini-tableau preview) is the
|
||||
big remaining piece that closes B-2 — best tackled in a
|
||||
fresh session because it crosses into `card_plugin`. Options
|
||||
3 and 4 are visual polish that benefit from user review.
|
||||
|
||||
## Open punch list
|
||||
|
||||
### Release prep
|
||||
1. **Smoke-test on the alex machine** after pulling — confirm Quat's three bug fixes hold up in real gameplay, and try the new replay button + web viewer end-to-end.
|
||||
2. **Desktop packaging** per `ARCHITECTURE.md §17`. The Arch PKGBUILD exists in `/home/manage/solitaire-quest-pkgbuild/` (separate repo). Pending: app icon, macOS `.icns` + notarisation cert, Windows `.ico` + Authenticode cert, AppImage recipe.
|
||||
### Phase Android (build + persistence shipped; runtime gaps remain)
|
||||
|
||||
### UX iteration (next-round candidates)
|
||||
- **APK launch verification on AVD / device.** `adb install` then
|
||||
`adb logcat` against the `bevy_test` AVD or an x86_64 device.
|
||||
The build works and persistence is wired, but no end-to-end
|
||||
device run has been logged. Shakes out runtime bugs the build +
|
||||
unit tests can't catch.
|
||||
- **JNI ClipboardManager bridge.** Replaces the Android stub for
|
||||
the Stats "Copy share link" toast. `arboard` doesn't ship an
|
||||
Android backend; small custom JNI call.
|
||||
- **Android Keystore for credentials.** `keyring` is target-gated
|
||||
to a stub returning `KeychainUnavailable`; replace with Android
|
||||
Keystore via JNI when sync auth ships on mobile.
|
||||
- **Google Play Games (gpgs) integration.** Listed as a
|
||||
Phase-Android target since Phase 1; now unblocked by the build
|
||||
target.
|
||||
- **Cosmetic `cargo apk build --lib` workaround.** Post-sign
|
||||
panic doesn't affect the APK on disk but produces noisy stderr.
|
||||
Either upstream a cargo-apk fix or document `--lib` as
|
||||
canonical in the runbook.
|
||||
|
||||
- **Solver-at-deal toggle** (Quat investigation #1, still deferred): add a Settings → Gameplay toggle "Winnable deals only" rather than baking solver-only into every deal. Lightest middle ground.
|
||||
- **Disable Bevy's default audio feature** (Quat investigation #2, still deferred): one-line `default-features = false` swap on the workspace `bevy =` line, re-enable explicitly the features the engine uses (`render`, `bevy_winit`, `2d`, `bevy_window`, `png`, `bevy_text`, `bevy_ui`, `bevy_log`, `bevy_asset`, `default_font`, `bevy_state`). Drops ~50 transitive crates including the rodio + symphonia stack the project doesn't use (kira handles audio).
|
||||
- **In-engine replay playback** — promote the "Watch replay" button from a stub toast to a real playback overlay that re-runs the recorded moves with `CardAnimation` tweens. The wasm crate already proves the playback math; the in-engine version reuses the same execute logic against the live game state.
|
||||
- **Per-replay history** — currently single-slot at `latest_replay.json`. A "best replay per mode" bucket or a recent-N rolling list would let players revisit notable wins.
|
||||
- **Solver-driven hint system** — extend the existing hint toggle so a deal-time solver provides higher-quality hints (currently a heuristic). Requires the solver from the toggle work above.
|
||||
- **Achievement: "won via replay path"** — track when a player wins a deal whose previously-saved replay also won the same deal. Mostly fun; trivial scope.
|
||||
### Visual-identity follow-ups (post-v0.21.0)
|
||||
|
||||
## Card-theme system (CARD_PLAN.md, fully shipped)
|
||||
The visual-identity arc is effectively complete: token system,
|
||||
chrome migration, splash boot screen, replay-overlay banner,
|
||||
card-face artwork (both rendering paths), and the `ACCENT_PRIMARY`
|
||||
palette refresh all shipped in v0.20.0 + v0.21.0. What stays open:
|
||||
|
||||
Seven phases landed across `b8fb3fb` → `924a1e2` in v0.11.0; v0.13.0's `7ed4f2c` consumes the per-theme `back.svg`; v0.14.0's `ba527de` adds preview thumbnails. End-to-end:
|
||||
- **Replay-overlay screen-takeover redesign.** The full mockup
|
||||
(`docs/ui-mockups/replay-overlay-mobile.html`) calls for a
|
||||
mini-tableau preview, playback controls, move-log scroll, and
|
||||
a WIN MOVE marker on the scrub bar. Banner-local pieces all
|
||||
shipped in v0.21.0 (`c84d9f4` + `6204db8` + `54005d5` +
|
||||
`e080b49`); the floating MOVE chip above the focused card
|
||||
shipped in v0.21.2 (`2fb2d63`). The WIN MOVE scrub-bar marker
|
||||
shipped post-v0.21.3 in `ab857bb` (data field) + `52befa6`
|
||||
(UI). Playback controls (pause / resume / step + Space
|
||||
accelerator) shipped post-v0.21.3 in `fbe48ac`. v0.21.5
|
||||
bundled six more commits under "replay-overlay scrubbing
|
||||
affordances + accessibility" (scrub notches + labels +
|
||||
keybind footer + ESC and ← / → accelerators + HC border).
|
||||
v0.21.6 bundled six more under "Move Log panel + scrub-UX
|
||||
polish" — bottom-edge Move Log panel with prev/active/next
|
||||
rows + active highlight, HC-mode coverage for the scrub
|
||||
track + notches, continuous scrub on key-held arrows. Banner
|
||||
height grew 60 → 76 → 92 px across two layout-changing
|
||||
commits in v0.21.5; Move Log panel grew 56 → 84 → 112 px
|
||||
across the v0.21.6 move-log commits. Per-commit detail in
|
||||
`CHANGELOG.md` § [0.21.5] and § [0.21.6]. The only major
|
||||
B-2 piece left is the mini-tableau preview — the mockup's
|
||||
"Game Peek Band" at 50 % opacity. Architectural; touches
|
||||
`card_plugin` rendering.
|
||||
Multi-session.
|
||||
- *Floating `MOVE N/M` chip above the focused card during
|
||||
playback — closed 2026-05-08 by `2fb2d63`.* World-space
|
||||
`Text2d` entity sibling to the banner overlay; uses the same
|
||||
`LayoutResource` pile coordinates so it survives window
|
||||
resizes without UI/camera math.
|
||||
- *Toast Warning variant wiring — closed 2026-05-08 by `279e23d`.*
|
||||
Daily-challenge-expiry toast fires once per `daily.date` when
|
||||
within 30 min of UTC midnight reset and today is incomplete.
|
||||
`ToastVariant` is now fully load-bearing (every variant has at
|
||||
least one real driver). Future Warning drivers can either reuse
|
||||
the generic `WarningToastEvent(String)` carrier or add their
|
||||
own domain message + `animation_plugin` handler.
|
||||
- *Toast Error variant wiring — closed 2026-05-08 by `68d50b5`.*
|
||||
`MoveRejectedEvent` now fires a 2-second pink-bordered
|
||||
"Invalid move" toast as the third leg of the
|
||||
audio + visual + text rejection-feedback stool.
|
||||
- *High-contrast accessibility mode — closed 2026-05-08 by
|
||||
`c5787c6` + `07e0357` (engine + UI) + v0.21.2's HC chrome
|
||||
rollout (`c9af1ea` + `d87761d` + `ec804d5`) + post-cut
|
||||
dynamic-paint rollout (`c153363`).* Card text rendering plus
|
||||
8 static-border chrome surfaces (modal scaffold, tooltip,
|
||||
onboarding key chips, help panel key chips, stats panel
|
||||
cells, home Level/XP/Score row, home mode buttons, home
|
||||
mode-hotkey chips, 4 settings panel surfaces) all boost
|
||||
borders to `BORDER_SUBTLE_HC` under HC via the
|
||||
`HighContrastBorder` marker. The previously-carved-out
|
||||
dynamic-paint sites are now also covered: HUD action buttons
|
||||
and modal buttons take the same marker (their paint cycles
|
||||
only mutate `BackgroundColor`, so no race); the radial menu
|
||||
rim folds HC into its per-frame spawn via
|
||||
`radial_rim_outline` so the focused rim boosts to
|
||||
`BORDER_SUBTLE_HC` under HC (preserving focused-vs-resting
|
||||
hierarchy that naive marker substitution would invert).
|
||||
- *Reduced-motion mode — closed 2026-05-08 by `c5787c6` +
|
||||
v0.21.2's `ed152e2`.* `effective_slide_secs` forces 0 on
|
||||
card animations; `pulse_splash_cursor` skips the per-frame
|
||||
pulse multiplier; `spawn_splash` skips the scanline overlay
|
||||
entirely. Future scope: gate any future card-lift z-bump
|
||||
animation, warning-chip pulse (when one materialises).
|
||||
|
||||
- **Bundled default theme** ships in the binary via `embedded://` — 52 hayeah/playing-cards-assets SVGs + a midnight-purple `back.svg`.
|
||||
- **User themes** under `themes://`. Drop a directory containing `theme.ron` + 53 SVGs.
|
||||
- **Importer** at `solitaire_engine::theme::import_theme(zip)` validates archives and atomically unpacks.
|
||||
- **Picker UI** in Settings → Cosmetic; thumbnails + the active theme's `back` override the legacy `back_N.png` picker when present.
|
||||
### Carried forward from v0.19.0
|
||||
|
||||
- *App icon round — closed 2026-05-08 by `3eb3a26` + `716a025`.*
|
||||
Runtime `Window::icon` wired (Linux/macOS/Windows); 9-size
|
||||
PNG hierarchy at `assets/icon/icon_<size>.png` covers Linux
|
||||
hicolor + downstream `.icns`/`.ico` packaging needs. The
|
||||
`.ico` and `.icns` bundle-format files themselves are *not*
|
||||
generated — both would need new crate deps (`ico` and
|
||||
`icns` respectively) and only matter at app-bundle time
|
||||
(cargo-bundle / packaging), not at `cargo run`. Open if the
|
||||
project later ships as a packaged macOS / Windows app.
|
||||
|
||||
### Other small candidates
|
||||
|
||||
- **Prev/Next selector chips spawn site.** v0.19.0's `9b065e5`
|
||||
noted Prev/Next markers exist in `stats_plugin` but no spawn
|
||||
site renders them today — the Shareable badge therefore lands
|
||||
on the single-replay caption. If/when Prev/Next is plumbed,
|
||||
the badge will need to follow.
|
||||
- **Toast queue / immediate unification.** The two toast paths
|
||||
(`spawn_queued_toast` for `InfoToastEvent` queue; `spawn_toast`
|
||||
for fire-and-forget) now share visual treatment but remain
|
||||
separate functions because they serve different temporal
|
||||
needs (sequential vs. parallel). If overlap becomes a UX
|
||||
issue, merge into one queue with priority lanes.
|
||||
|
||||
### Process notes
|
||||
|
||||
- **The desktop-adaptation spec is the canonical reference for
|
||||
geometry decisions** when porting any future plugin. Read
|
||||
`docs/ui-mockups/desktop-adaptation.md` first; apply the
|
||||
universal rules to every surface; consult the per-screen
|
||||
table for the priority surfaces. The 9 missing-plugin screens
|
||||
(splash now ported; eight remaining) inherit the universal
|
||||
rules without dedicated guidance.
|
||||
- **Stitch `generate_variants` is unreliable for layout-only
|
||||
adaptation prompts** as of 2026-05-07. The first call timed
|
||||
out and no variant ever landed in `list_screens`. If a future
|
||||
session wants visual desktop mockups, prefer
|
||||
`generate_screen_from_text` with a fresh narrow prompt per
|
||||
screen rather than `generate_variants` against existing
|
||||
mobile screens.
|
||||
- **Token-port pattern.** v0.20.0's chrome-migration commits
|
||||
set a reusable shape for "centralised design system applied
|
||||
across N plugins":
|
||||
1. Constants module (`ui_theme.rs`) is the source of truth.
|
||||
2. Const sites that can't call `Alpha::with_alpha` (not yet
|
||||
`const` on stable) use a literal RGB matching the token,
|
||||
with a unit test pinning the RGB to the token (e.g.
|
||||
`MARKER_VALID`, `HINT_PILE_HIGHLIGHT_COLOUR`,
|
||||
`RIGHT_CLICK_HIGHLIGHT_COLOUR`).
|
||||
3. Cross-plugin duplication (e.g. `MARKER_DEFAULT` ↔
|
||||
`PILE_MARKER_DEFAULT_COLOUR`) collapses to a single
|
||||
promoted const re-exported from one plugin and imported
|
||||
by the other — replaces "kept in sync" doc comments with a
|
||||
compile-time invariant.
|
||||
4. Domain colours (suit pips, card faces, lerp helpers) stay
|
||||
as literals with a comment naming the rationale; only UI
|
||||
chrome routes through tokens.
|
||||
- **`SplashFadable` scaffolding pattern** (introduced in
|
||||
`cacb19c`). Any future overlay that needs to fade `N >> 3`
|
||||
elements together should follow the same shape: one tiny
|
||||
marker carrying the full-alpha base colour, one global query
|
||||
that lerps every marker's alpha each frame, no per-element
|
||||
query plumbing. Cleanly outscales the `Without<X>, Without<Y>`
|
||||
query exclusion pattern that the old splash was hitting at
|
||||
three siblings.
|
||||
|
||||
### Canonical remote
|
||||
|
||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
|
||||
Always push there. As of v0.21.0 origin matches local; the next
|
||||
push happens when post-cut work accumulates and is ready to roll
|
||||
into a v0.21.1 / v0.22.0 cut.
|
||||
|
||||
### Design direction (Terminal — base16-eighties)
|
||||
|
||||
- **Tone:** retro-terminal / synthwave — flat depth (no box-shadows),
|
||||
monospaced-forward typography (JetBrains Mono / FiraMono), tight
|
||||
16 px edge margins, 8 px card radius.
|
||||
- **Palette:** near-black surface ramp (`#151515` / `#202020` /
|
||||
`#2a2a2a` / `#353535`), brick-red primary CTA (`#a54242` —
|
||||
swapped from cyan `#6fc2ef` in v0.21.0 commit `a292a7e`), lime
|
||||
success (`#acc267`), gold warning (`#ddb26f`), pink error /
|
||||
suit-red (`#fb9fb1`), lavender celebration (`#e1a3ee`), teal
|
||||
info (`#12cfc0`).
|
||||
- **Two-color suits.** Red = `#fb9fb1`, black = `#d0d0d0`.
|
||||
Outlined glyphs for diamonds & clubs are *always on*; the
|
||||
Settings "color-blind mode" toggle swaps red → lime `#acc267`
|
||||
(was red → cyan pre-v0.21.0; lime is the next-best non-red
|
||||
base16-eighties accent now that the primary itself is red).
|
||||
- **Card glyphs render upright in both corners** — no 180°
|
||||
inverted-corner-indicator rotation. Single-orientation
|
||||
digital play doesn't benefit from the traditional flip-
|
||||
readback convention. `design-system.md` § Game Cards
|
||||
documents this deliberate deviation.
|
||||
|
||||
## Resume prompt
|
||||
|
||||
```
|
||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||
Working directory: <Rusty_Solitaire clone path on this machine — local
|
||||
directory may still be named Rusty_Solitare from earlier; that's fine>.
|
||||
Branch: master. Direction is OPEN — v0.14.0 just shipped covering the
|
||||
Quat bug fixes, the v0.13.0 candidate tail, and the entire
|
||||
replay-pipeline feature.
|
||||
Working directory: <Rusty_Solitaire clone path on this machine>.
|
||||
Branch: master. v0.21.6 is tagged at f63db76 (cut 2026-05-08, a
|
||||
patch release rolling up Move Log panel + scrub-UX polish:
|
||||
brand-new bottom-edge Move Log panel with prev / active / next
|
||||
row context + active-row highlight, plus HC-mode coverage for
|
||||
scrub track + notches and continuous scrub on key-held arrow
|
||||
keys). v0.21.5 stays at a2432df, v0.21.4 at 23ff62c, v0.21.3
|
||||
at 3d92a91, v0.21.2 at f23df3b, v0.21.1 at daa655a, v0.21.0 at
|
||||
04f9bf9. Working tree clean. See CHANGELOG.md § [0.21.6] for
|
||||
full detail.
|
||||
|
||||
State: HEAD at v0.14.0. Working tree clean apart from untracked
|
||||
CARD_PLAN.md (intentional).
|
||||
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
|
||||
Tests: 1134 passed / 0 failed.
|
||||
State: HEAD locally — see `git rev-parse HEAD`. The cut commit
|
||||
is f63db76; any post-cut docs edits ride on top of that.
|
||||
Workspace tests: 1273 passing / 0 failing. Clippy clean.
|
||||
|
||||
READ FIRST (in order, before doing anything):
|
||||
1. SESSION_HANDOFF.md — v0.14.0 changelog + open punch list
|
||||
2. CHANGELOG.md — release-by-release record
|
||||
3. CLAUDE.md — hard rules (UI-first, no panics, etc.)
|
||||
4. ARCHITECTURE.md — crate responsibilities + data flow
|
||||
5. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||
— saved feedback / project context (machine-local;
|
||||
may be missing on a fresh machine)
|
||||
1. SESSION_HANDOFF.md — this file
|
||||
2. CHANGELOG.md — [0.21.6] section is the most recent cut
|
||||
3. CLAUDE.md — unified-3.0 rule set
|
||||
4. CLAUDE_SPEC.md — formal architecture spec
|
||||
5. ARCHITECTURE.md — crate responsibilities + data flow
|
||||
6. docs/ui-mockups/ — design system + 24-mockup library +
|
||||
desktop-adaptation.md (the rules-based
|
||||
companion to the mockups; read this
|
||||
before any plugin port)
|
||||
7. docs/android/* — Android setup + build runbook
|
||||
8. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||
— saved feedback / project context
|
||||
(machine-local; may be missing on a
|
||||
fresh machine)
|
||||
|
||||
DECISION TO ASK THE PLAYER FIRST:
|
||||
A. Smoke-test v0.14.0 on the alex machine first to confirm the
|
||||
three Quat bug fixes hold up in real gameplay and the replay
|
||||
pipeline works end-to-end (record → upload → web viewer).
|
||||
B. Take the deferred Bevy-audio-feature trim (Quat investigation
|
||||
#2) — one-line workspace edit, ~50 fewer transitive crates.
|
||||
C. Take the deferred solver toggle (Quat investigation #1): add
|
||||
"Winnable deals only" Settings toggle. Larger.
|
||||
D. Promote the in-engine "Watch replay" button to real playback.
|
||||
E. Pick from the remaining "next-round candidates" in this doc.
|
||||
F. Take the deferred desktop-packaging item (needs artwork +
|
||||
signing certs from the user).
|
||||
A. APK launch verification on AVD / device — `adb install` +
|
||||
`adb logcat` to shake out runtime bugs the build / unit
|
||||
tests can't catch. Likely surfaces JNI ClipboardManager
|
||||
and Android Keystore stubs that need real bridges. Larger
|
||||
scope; needs an Android device or emulator running.
|
||||
B. Replay-overlay screen-takeover redesign — nearly complete
|
||||
after 12 commits across v0.21.4-6. Scrub bar with notches
|
||||
+ labels + WIN MOVE marker, pause / resume / step / stop
|
||||
buttons, Space + Esc + ← / → keyboard accelerators with
|
||||
continuous scrub on hold, keybind-hint footer, full HC-mode
|
||||
coverage on the banner pieces, and a brand-new bottom-edge
|
||||
Move Log panel with a 5-row prev/active/next window all
|
||||
ship. The only remaining major B-2 sub-piece is the
|
||||
**mini-tableau preview** — the mockup's "Game Peek Band"
|
||||
at 50 % opacity showing the tableau through the replay
|
||||
chrome. Implementation needs a settings-aware dim overlay
|
||||
or alpha modulation on the tableau cards during replay.
|
||||
Architectural — touches `card_plugin` rendering. Best
|
||||
tackled in a fresh session because it crosses into a
|
||||
plugin the recent B-2 work hasn't touched. Mockup at
|
||||
`docs/ui-mockups/replay-overlay-mobile.html`.
|
||||
C. Phase 8 (sync) — local storage scaffolding, self-hosted
|
||||
Axum server, `SolitaireServerClient` impl, GPGS stub
|
||||
wired into Settings. The biggest open arc by scope; rolls
|
||||
up several Phase Android dependencies (Keystore,
|
||||
ClipboardManager).
|
||||
|
||||
WORKFLOW NOTES:
|
||||
- Commits use:
|
||||
git -c user.name=funman300 -c user.email=root@vscode.infinity \
|
||||
commit -m "..."
|
||||
- When attributing playtester feedback in commits/docs, use "Quat"
|
||||
not "Rhys" (saved feedback memory).
|
||||
- Use the system git config (already correct).
|
||||
- When attributing playtester feedback in commits/docs, use
|
||||
"Quat" not "Rhys" (saved feedback memory).
|
||||
- Sub-agents stage + verify only; orchestrator commits.
|
||||
- Every commit must pass build / clippy / test before pushing.
|
||||
- Push to GitHub (origin) — that is the canonical remote.
|
||||
- Push to GitHub (origin) — gh auth setup-git wired on
|
||||
primary dev box; verify on laptop before first push.
|
||||
- Token-port pattern: when migrating tokens, walk every
|
||||
concrete artifact downstream of the token (PNG textures,
|
||||
embedded SVGs, hardcoded literals, comment color names),
|
||||
not just the token name. v0.21.0 surfaced three "the
|
||||
migration walked past this" follow-ups that all matched
|
||||
this shape — codified here so future similar work can
|
||||
pattern-match instead of rediscovering.
|
||||
- Doc-vs-implementation drift pattern: v0.21.1's pile-marker
|
||||
visibility fix (`4d48cad`) implemented an invariant that
|
||||
had been declared in a module doc comment but was never
|
||||
enforced in code. When future work touches a module with
|
||||
a "this does X" doc comment, verify the code actually does
|
||||
X and add a test if not. Two layers, two checks.
|
||||
|
||||
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
||||
OPEN AT THE START: ask which of A–C. Don't pick unilaterally.
|
||||
Note: every remaining option is multi-session by nature (A is
|
||||
gated on Android tooling, B and C are explicitly multi-session
|
||||
arcs). A fresh session is a better fit for any of them than the
|
||||
tail of a long working stretch.
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 365 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 263 B |
|
After Width: | Height: | Size: 369 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 927 B |
@@ -0,0 +1,228 @@
|
||||
# Android build — developer setup
|
||||
|
||||
This doc captures the toolchain install + build invocation for the
|
||||
Android target. Steps are runnable on a fresh Debian 13 (trixie) box;
|
||||
later sections document what's known to compile, what's stubbed, and
|
||||
the next milestones.
|
||||
|
||||
> **Status (2026-05-07):** First working APK at `fb8b2ac`. 54 MB
|
||||
> debug-signed `solitaire-quest.apk` for `x86_64-linux-android`. Has
|
||||
> NOT yet been verified to launch on a device or emulator — that's
|
||||
> the next milestone.
|
||||
|
||||
---
|
||||
|
||||
## 1. Toolchain install (Debian 13 / trixie)
|
||||
|
||||
Run as one block. Will pull ~15-20 GB of disk between APT, the SDK,
|
||||
the NDK, the system image, and Rust target sysroots. Requires sudo.
|
||||
|
||||
```bash
|
||||
# 1. JDK 21 (Android tooling needs JDK 17+; Debian 13 default is 21).
|
||||
sudo apt update && sudo apt install -y openjdk-21-jdk-headless unzip wget
|
||||
|
||||
# 2. SDK directory + Google's cmdline-tools bootstrap.
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
wget -O /tmp/cmdline-tools.zip \
|
||||
https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
|
||||
unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
|
||||
mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
|
||||
rm /tmp/cmdline-tools.zip
|
||||
|
||||
# 3. Persist env vars.
|
||||
{
|
||||
echo ''
|
||||
echo '# Android dev'
|
||||
echo 'export ANDROID_HOME="$HOME/Android/Sdk"'
|
||||
echo 'export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264"'
|
||||
echo 'export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))"'
|
||||
echo 'export PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator"'
|
||||
} >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# 4. Accept SDK licences (interactive prompts answered by `yes |`).
|
||||
yes | sdkmanager --licenses
|
||||
|
||||
# 5. Platform packages — ~5 GB.
|
||||
sdkmanager \
|
||||
"platform-tools" \
|
||||
"platforms;android-34" \
|
||||
"build-tools;34.0.0" \
|
||||
"ndk;26.3.11579264" \
|
||||
"emulator" \
|
||||
"system-images;android-34;google_apis;x86_64"
|
||||
|
||||
# 6. AVD for testing (one-time).
|
||||
echo no | avdmanager create avd \
|
||||
-n bevy_test \
|
||||
-k "system-images;android-34;google_apis;x86_64" \
|
||||
-d pixel_7
|
||||
|
||||
# 7. Rust cross-compile targets.
|
||||
rustup target add \
|
||||
aarch64-linux-android \
|
||||
armv7-linux-androideabi \
|
||||
x86_64-linux-android \
|
||||
i686-linux-android
|
||||
|
||||
# 8. cargo-apk.
|
||||
cargo install cargo-apk
|
||||
```
|
||||
|
||||
Sanity:
|
||||
|
||||
```bash
|
||||
java --version | head -1 # openjdk 21.0.x
|
||||
adb --version | head -1 # 35.x or higher
|
||||
sdkmanager --list_installed | head # build-tools, emulator, ndk, platforms, system-images
|
||||
avdmanager list avd | head # bevy_test
|
||||
rustup target list --installed | grep android # 4 targets
|
||||
cargo apk --help | head -5
|
||||
```
|
||||
|
||||
If `sdkmanager --version` errors with `JAVA_HOME is not set`, the env
|
||||
section in step 3 didn't apply to your shell — `source ~/.bashrc`
|
||||
again or open a new terminal.
|
||||
|
||||
### Optional: emulator runtime libs
|
||||
|
||||
The Android emulator is dynamically linked against X11/GL/audio. If
|
||||
`emulator -list-avds` works but `emulator -avd bevy_test` complains
|
||||
about `libX11.so.6`, install:
|
||||
|
||||
```bash
|
||||
sudo apt install -y \
|
||||
libx11-6 libxcursor1 libxrandr2 libxi6 libxinerama1 libxxf86vm1 \
|
||||
libgl1 libnss3 libpulse0 libxcomposite1
|
||||
```
|
||||
|
||||
Headless emulator launch:
|
||||
|
||||
```bash
|
||||
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
||||
adb wait-for-device && adb devices
|
||||
# Stop later:
|
||||
# adb -s emulator-5554 emu kill
|
||||
```
|
||||
|
||||
Headless + software rendering is fine for "does it boot" smoke tests
|
||||
but useless for perf measurement — use a physical Pixel-class device
|
||||
over USB for real numbers.
|
||||
|
||||
---
|
||||
|
||||
## 2. Build the APK
|
||||
|
||||
```bash
|
||||
cargo apk build -p solitaire_app --target x86_64-linux-android
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
target/debug/apk/solitaire-quest.apk
|
||||
```
|
||||
|
||||
Targets shipped via `[package.metadata.android].build_targets` in
|
||||
`solitaire_app/Cargo.toml`:
|
||||
|
||||
| Target | Use |
|
||||
|--------|-----|
|
||||
| `aarch64-linux-android` | Real phones (modern 64-bit ARM) |
|
||||
| `armv7-linux-androideabi` | Older 32-bit ARM phones |
|
||||
| `x86_64-linux-android` | The `bevy_test` AVD on this dev box |
|
||||
|
||||
Build any of them with `--target <triple>`.
|
||||
|
||||
### Known cosmetic warning
|
||||
|
||||
After the APK is signed cargo-apk panics with:
|
||||
|
||||
```
|
||||
thread 'main' panicked: Bin is not compatible with Cdylib
|
||||
```
|
||||
|
||||
This happens AFTER the APK is on disk and signed. cargo-apk is
|
||||
trying to also wrap the desktop `[[bin]]` target. The APK is still
|
||||
valid. Work around with `--lib`:
|
||||
|
||||
```bash
|
||||
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
|
||||
```
|
||||
|
||||
(Permanent fix to come — likely a `[[bin]] required-features = ["desktop"]`
|
||||
gate so cargo-apk skips the bin target on Android.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Install + run
|
||||
|
||||
Physical device:
|
||||
|
||||
```bash
|
||||
adb devices # confirm connection
|
||||
adb install target/debug/apk/solitaire-quest.apk
|
||||
adb shell am start -n com.solitairequest.app/android.app.NativeActivity
|
||||
adb logcat | grep -iE "RustStdoutStderr|solitaire|panic"
|
||||
```
|
||||
|
||||
Emulator:
|
||||
|
||||
```bash
|
||||
emulator -avd bevy_test -no-window -gpu swiftshader_indirect &
|
||||
adb wait-for-device
|
||||
adb install target/debug/apk/solitaire-quest.apk
|
||||
# ... same start + logcat steps as above.
|
||||
```
|
||||
|
||||
If `adb install` errors with `INSTALL_FAILED_NO_MATCHING_ABIS`, the
|
||||
emulator is x86_64 but the APK was built for arm — rebuild with the
|
||||
`x86_64-linux-android` target, or add an x86_64 system image to the
|
||||
AVD.
|
||||
|
||||
---
|
||||
|
||||
## 4. What's wired vs. what's stubbed
|
||||
|
||||
The first build pass (commit `fb8b2ac`) gates four desktop-only
|
||||
crates / call sites so the workspace cross-compiles. Each gate is
|
||||
documented at its call site.
|
||||
|
||||
| Surface | Desktop | Android |
|
||||
|---------|---------|---------|
|
||||
| Bevy windowing | x11 + wayland | `android-native-activity` (NativeActivity glue) |
|
||||
| Clipboard ("Copy share link") | `arboard` writes URL | Toast surfaces the URL inline |
|
||||
| OS keychain (JWT tokens) | `keyring` v4 → Secret Service / Keychain / Credential Store | Stub returning `KeychainUnavailable`; sync requires fresh login each launch |
|
||||
| App entry point | `bin` target → `solitaire_app::run()` | `cdylib` target loaded by NativeActivity |
|
||||
|
||||
What's NOT yet ported / not yet measured:
|
||||
|
||||
- `dirs::data_dir()` returns `None` on Android. Callers in
|
||||
`solitaire_data/src/storage.rs`, `progress.rs`, `replay.rs`,
|
||||
`achievements.rs`, `settings.rs` all need an Android-aware
|
||||
helper (likely `/data/data/com.solitairequest.app/files`).
|
||||
- Touch UX pass — hit-target sizes, modal scaling on small screens,
|
||||
app lifecycle (suspend / resume), font scaling.
|
||||
- Android Keystore via JNI for `auth_tokens`.
|
||||
- JNI ClipboardManager for share links.
|
||||
- Google Play Games sign-in (the `solitaire_gpgs` crate referenced
|
||||
in older docs doesn't yet exist).
|
||||
|
||||
---
|
||||
|
||||
## 5. Iteration loop
|
||||
|
||||
```bash
|
||||
# Edit code…
|
||||
cargo build -p solitaire_app # desktop sanity
|
||||
cargo clippy --workspace --all-targets -- -D warnings # gate
|
||||
cargo test --workspace # gate
|
||||
cargo apk build -p solitaire_app --target x86_64-linux-android --lib
|
||||
adb install -r target/debug/apk/solitaire-quest.apk # `-r` reinstalls
|
||||
adb logcat -c && adb shell am start -n com.solitairequest.app/android.app.NativeActivity
|
||||
adb logcat | grep -iE "RustStdoutStderr|solitaire"
|
||||
```
|
||||
|
||||
`adb logcat` is the canonical way to see Bevy / Rust panic output —
|
||||
they end up in the `RustStdoutStderr` tag.
|
||||
@@ -0,0 +1,293 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Rusty Solitaire - Achievements</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500;700&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"on-tertiary-container": "#683476",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"error": "#fb9fb1",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"background": "#101417",
|
||||
"error-container": "#93000a",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"inverse-primary": "#00668a",
|
||||
"highlight-valid": "#acc267",
|
||||
"suit-red": "#fb9fb1",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"on-secondary": "#293500",
|
||||
"on-primary-container": "#004f6c",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"on-surface": "#e0e3e6",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-background": "#e0e3e6",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"info": "#12cfc0",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"warning": "#ddb26f",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"surface": "#151515",
|
||||
"surface-container-highest": "#313538",
|
||||
"outline": "#505050",
|
||||
"on-primary": "#003549",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-variant": "#313538",
|
||||
"on-error": "#690005",
|
||||
"suit-black": "#d0d0d0",
|
||||
"primary": "#a1dcff",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"surface-bright": "#363a3d",
|
||||
"on-tertiary": "#4c195b",
|
||||
"surface-dim": "#101417",
|
||||
"primary-container": "#6fc2ef",
|
||||
"tertiary": "#f7c3ff",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"surface-container-high": "#272a2d",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"surface-container": "#1c2023",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"secondary-container": "#435401",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"secondary": "#bad073",
|
||||
"surface-container-low": "#181c1f"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-overlap": "2rem",
|
||||
"gutter-card": "0.375rem",
|
||||
"touch-target-min": "48dp",
|
||||
"margin-edge": "1rem",
|
||||
"action-bar-height": "64px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
background-color: #151515;
|
||||
color: #e0e3e6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.scanline {
|
||||
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.1) 50%);
|
||||
background-size: 100% 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="font-body-md text-body-md overflow-x-hidden pb-[action-bar-height]">
|
||||
<!-- Status Bar -->
|
||||
<header class="fixed top-0 w-full h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge z-[60] border-b border-outline-variant">
|
||||
<div class="flex items-center gap-2 font-label-caps text-on-surface">
|
||||
<span class="text-primary">▌</span>achievements.json
|
||||
</div>
|
||||
<div class="font-label-caps text-[#a0a0a0]">
|
||||
8/19 UNLOCKED
|
||||
</div>
|
||||
</header>
|
||||
<!-- Top App Bar (Shared Component Reference) -->
|
||||
<nav class="fixed top-[32px] w-full h-[64px] bg-surface flex items-center justify-between px-margin-edge z-50 border-b border-outline-variant">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
|
||||
<h1 class="font-headline text-[20px] text-primary uppercase tracking-widest">Rusty Solitaire</h1>
|
||||
</div>
|
||||
<button class="w-10 h-10 flex items-center justify-center hover:bg-surface-container-highest transition-colors">
|
||||
<span class="material-symbols-outlined text-on-surface-variant" data-icon="settings">settings</span>
|
||||
</button>
|
||||
</nav>
|
||||
<main class="mt-[112px] px-margin-edge">
|
||||
<!-- Hero Progress Card -->
|
||||
<section class="w-full h-[100px] bg-[#202020] border border-[#353535] rounded-lg p-4 mb-6">
|
||||
<div class="flex flex-col justify-between h-full">
|
||||
<span class="font-label-caps text-[10px] text-[#a0a0a0]">PROGRESS</span>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="font-headline text-[28px] font-bold text-[#d0d0d0]">8/19</span>
|
||||
<span class="font-label-caps text-[14px] text-highlight-celebration">(42%)</span>
|
||||
</div>
|
||||
<div class="w-full h-[4px] bg-[#353535] rounded-full overflow-hidden mt-1">
|
||||
<div class="h-full bg-highlight-celebration" style="width: 42%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Filter Chip Row -->
|
||||
<section class="flex gap-2 mb-6 overflow-x-auto no-scrollbar">
|
||||
<button class="h-[32px] px-3 flex items-center justify-center border border-[#6fc2ef] text-[#6fc2ef] rounded-[4px] font-label-caps text-[11px]">
|
||||
[ ALL ]
|
||||
</button>
|
||||
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||
UNLOCKED
|
||||
</button>
|
||||
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||
LOCKED
|
||||
</button>
|
||||
<button class="h-[32px] px-3 flex items-center justify-center border border-outline text-[#a0a0a0] rounded-[4px] font-label-caps text-[11px] hover:border-primary hover:text-primary transition-colors">
|
||||
SECRET
|
||||
</button>
|
||||
</section>
|
||||
<!-- Achievements Grid -->
|
||||
<section class="grid grid-cols-2 gap-3 mb-10">
|
||||
<!-- FIRST WIN -->
|
||||
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="emoji_events" style="font-variation-settings: 'FILL' 1;">emoji_events</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">FIRST WIN</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win your first game</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SPEED DEMON -->
|
||||
<div class="h-[100px] bg-[#202020] border border-highlight-celebration p-3 flex flex-col justify-between rounded-sm relative">
|
||||
<div class="absolute inset-0 border border-highlight-celebration opacity-20 pointer-events-none"></div>
|
||||
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="speed" style="font-variation-settings: 'FILL' 1;">speed</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">SPEED DEMON</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Win in under 3:00</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- STREAK 10 -->
|
||||
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="bolt" style="font-variation-settings: 'FILL' 1;">bolt</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">STREAK 10</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">10 wins in a row</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- DAILY DEFENDER -->
|
||||
<div class="h-[100px] bg-[#202020] border border-[#353535] p-3 flex flex-col justify-between rounded-sm">
|
||||
<div class="w-8 h-8 rounded-full bg-[#6fc2ef] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#151515] text-[20px]" data-icon="calendar_today" style="font-variation-settings: 'FILL' 1;">calendar_today</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#d0d0d0] leading-none mb-1">DAILY DEFENDER</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#a0a0a0] leading-tight">Complete 7 daily seeds</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PERFECTIONIST (LOCKED) -->
|
||||
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="undo">undo</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PERFECTIONIST</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Win without using undo</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CHALLENGE BEATEN (LOCKED) -->
|
||||
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="military_tech">military_tech</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">CHALLENGE BEATEN</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Complete CHALLENGE mode</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SECRET (LOCKED) -->
|
||||
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="help_outline">help_outline</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">????</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">SECRET · Hidden until unlocked</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAR HUNTER (LOCKED) -->
|
||||
<div class="h-[100px] bg-[#0d0d0d] border border-[#353535] p-3 flex flex-col justify-between rounded-sm opacity-80">
|
||||
<div class="w-8 h-8 rounded-full border border-[#505050] flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[#505050] text-[20px]" data-icon="golf_course">golf_course</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-label-caps text-[13px] font-bold text-[#505050] leading-none mb-1">PAR HUNTER</h3>
|
||||
<p class="font-label-caps text-[10px] text-[#353535] leading-tight">Beat par on 50 games</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- Footer Status -->
|
||||
<footer class="fixed bottom-[action-bar-height] w-full h-[24px] bg-background border-t border-outline-variant flex items-center justify-between px-margin-edge z-40 text-[10px] font-label-caps">
|
||||
<div class="flex items-center">
|
||||
<span class="text-primary mr-1">▌</span>
|
||||
<span class="text-on-surface-variant">NORMAL</span>
|
||||
<span class="mx-2 text-outline">│</span>
|
||||
<span class="text-on-surface-variant">achievements</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div><span class="text-[#a0a0a0]">[F]</span> <span class="text-[#505050]">filter</span></div>
|
||||
<div><span class="text-[#a0a0a0]">[/]</span> <span class="text-[#505050]">search</span></div>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Bottom Navigation Bar (Shared Component Reference) -->
|
||||
<nav class="fixed bottom-0 w-full h-action-bar-height bg-surface-container flex justify-around items-center px-margin-edge z-50 border-t border-outline-variant">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[Q] QUIT</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined" data-icon="undo">undo</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary hover:bg-surface-container-highest transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
|
||||
</button>
|
||||
</nav>
|
||||
<!-- CRT Overlay Effect (Visual Decoration) -->
|
||||
<div class="fixed inset-0 pointer-events-none z-[100] opacity-[0.03] scanline"></div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,251 @@
|
||||
# Card-face artwork migration plan
|
||||
|
||||
**Status:** planning artifact (no code changed by this document).
|
||||
**Tracks:** the "Card-face / suit / card-back artwork regeneration"
|
||||
item in `SESSION_HANDOFF.md` → "Visual-identity follow-ups"
|
||||
(SESSION_HANDOFF Resume prompt option D).
|
||||
**Companion to:** `docs/ui-mockups/design-system.md` (Game Cards
|
||||
spec, lines 214–233) and `docs/ui-mockups/desktop-adaptation.md`
|
||||
(rules-based companion to the mockups).
|
||||
|
||||
## Why this is a multi-session arc
|
||||
|
||||
Every post-v0.20.0 visual-identity port to date (modal scaffold,
|
||||
toasts, table chrome, splash boot screen, replay overlay) was a
|
||||
**single rendering path** — change tokens, change comments, ship.
|
||||
Cards have **two** rendering paths that are visually identical
|
||||
today and would visually disagree the moment one moves:
|
||||
|
||||
1. **PNG path (production).** `assets/cards/faces/<rank><suit>.png`
|
||||
loaded into `CardImageSet.faces[suit][rank]` at startup; card
|
||||
sprites blit the texture. 52 face PNGs + 5 back PNGs already
|
||||
in `assets/`, all the legacy white-card aesthetic from the
|
||||
pre-Terminal design system.
|
||||
2. **Constant fallback (tests + asset-missing edge).** When
|
||||
`CardImageSet` isn't a registered resource (the case under
|
||||
`MinimalPlugins` test fixtures, and the bare-bones path the
|
||||
first-frame of production hits before assets resolve), the
|
||||
renderer falls back to solid-colour sprites driven by the
|
||||
`card_plugin` constants:
|
||||
- `CARD_FACE_COLOUR` — `(0.98, 0.98, 0.95)` cream-ish white.
|
||||
- `RED_SUIT_COLOUR` — `(0.78, 0.12, 0.15)` warm red.
|
||||
- `BLACK_SUIT_COLOUR` — `(0.08, 0.08, 0.08)` near-black.
|
||||
- `CARD_FACE_COLOUR_RED_CBM` — `(0.85, 0.92, 1.0, 1.0)` light
|
||||
blue (the legacy color-blind tint).
|
||||
- `card_back_colour(idx)` — five legacy back themes.
|
||||
|
||||
A single-path migration leaves a known-broken state where tests
|
||||
pass against Terminal constants while a human sees legacy artwork
|
||||
on screen — the exact bisection-hostile drift the handoff's
|
||||
"in lockstep" warning preempts.
|
||||
|
||||
## Target state — Terminal aesthetic
|
||||
|
||||
Per `design-system.md` § Game Cards (lines 214–233):
|
||||
|
||||
### Card face
|
||||
|
||||
| Element | Spec |
|
||||
|---|---|
|
||||
| Background | `#1a1a1a` |
|
||||
| Border | 1 px solid in **suit colour** (pink for ♥/♦, foreground gray for ♠/♣) |
|
||||
| Corner radius | 8 px |
|
||||
| Top-left | rank in JetBrains Mono **Bold 18 px** + small suit glyph (10 px) |
|
||||
| Bottom-right | large suit glyph (32 px), rotated 180° |
|
||||
| Glyph fill rule | ♥ ♠ filled; ♦ ♣ outlined (1.5 px stroke). Always on, not a toggle. |
|
||||
|
||||
### Suit colours (always-on glyph differentiation is the *primary*
|
||||
distinguishing mechanism; colour is supplementary):
|
||||
|
||||
| Suit | Default | Color-blind mode |
|
||||
|---|---|---|
|
||||
| Hearts | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
|
||||
| Diamonds | `#fb9fb1` (pink) | `#6fc2ef` (cyan) |
|
||||
| Spades | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
|
||||
| Clubs | `#d0d0d0` (gray) | `#d0d0d0` (unchanged) |
|
||||
|
||||
### Card back ("Terminal" theme)
|
||||
|
||||
| Element | Spec |
|
||||
|---|---|
|
||||
| Background | `#151515` |
|
||||
| Pattern | horizontal scanlines at 2 px pitch in `#1a1a1a` (1 px line, 1 px gap), full bleed |
|
||||
| Border | 1 px solid `#353535` |
|
||||
| Top-left badge | 12×16 px solid `#6fc2ef` block, 6 px from corner |
|
||||
| Bottom-right monogram | `▌RS` in JetBrains Mono 12 px `#505050`, 6 px from corner |
|
||||
| Corner radius | 8 px |
|
||||
| Theme name / author | `"Terminal"` / `"Rusty Solitaire"` |
|
||||
|
||||
## Generation pipeline — programmatic SVG via the existing
|
||||
`resvg` stack
|
||||
|
||||
### Why this path (vs. external tooling or direct `tiny_skia`)
|
||||
|
||||
The codebase already ships an SVG-to-PNG rasteriser at
|
||||
`solitaire_engine/src/assets/svg_loader.rs`:
|
||||
|
||||
- Public `rasterize_svg(svg_bytes: &[u8], target: UVec2) -> Result<Image, _>`
|
||||
- Backed by `usvg` (parser) + `resvg` (renderer) + `tiny_skia`
|
||||
(CPU pixmap)
|
||||
- Bundled font db includes JetBrains-style mono (FiraMono — same
|
||||
face the splash uses; close enough to JetBrains Mono for
|
||||
rasterisation purposes, and identical to what the Bevy UI
|
||||
consumes in the rest of the app)
|
||||
- `RenderAssetUsages::default()` is the call-site convention here
|
||||
|
||||
This means: **generating new card PNGs is one new file
|
||||
(`solitaire_engine/examples/card_face_generator.rs`) calling an
|
||||
existing public function.** No new dependencies, no asset-pipeline
|
||||
changes, no build-script machinery. Anyone who runs the example
|
||||
gets bit-identical artwork.
|
||||
|
||||
The two alternatives are weaker:
|
||||
|
||||
- **External tool (Inkscape / Figma / hand-design)** — produces
|
||||
one-off PNGs that can't be re-generated reproducibly without
|
||||
re-opening the source files in a specific tool. Iteration cost
|
||||
is high; design tweaks (e.g. "make the suit glyph 2 px larger")
|
||||
require a designer-in-the-loop.
|
||||
- **Direct `tiny_skia` painting calls** — bypasses SVG entirely,
|
||||
but loses the readability of "open the SVG to see exactly what
|
||||
the card looks like." Also reinvents primitives (rounded
|
||||
rectangles, text layout) that `usvg` already handles.
|
||||
|
||||
### Output format
|
||||
|
||||
PNG, RGBA8 sRGB, **dimensions 256 × 384** (2:3 aspect, half the
|
||||
default `SvgLoaderSettings` of 512 × 768).
|
||||
|
||||
Rationale: cards never exceed ~250 px wide on desktop windows
|
||||
today, and 256 × 384 PNGs are ~6 KB each at this content density
|
||||
(13.4 KB total for a full deck of 52 + 5 backs). The default 512 ×
|
||||
768 is 2× what's needed and quadruples the on-disk asset weight.
|
||||
The existing legacy PNGs are 512 × 768 — reducing the new ones
|
||||
halves the runtime asset size.
|
||||
|
||||
## Lockstep migration — recommended order
|
||||
|
||||
Each step is a separate commit; the constraint is that **steps 4
|
||||
and 5 must land in the same commit** (or at most adjacent commits
|
||||
on the same branch) so the rendered output never diverges between
|
||||
the two paths.
|
||||
|
||||
1. **(Done — this commit)** Land the migration plan doc.
|
||||
2. **Land the SVG generator example.** New
|
||||
`solitaire_engine/examples/card_face_generator.rs`. Output
|
||||
goes to `assets/cards/faces/` and `assets/cards/backs/`. Run
|
||||
once locally to seed the new artwork. The example file stays
|
||||
in-tree as a regenerator for future tweaks.
|
||||
3. **(Optional — can land separately)** Add a one-shot regression
|
||||
test that re-runs the generator into a `tempdir` and compares
|
||||
the resulting bytes against the on-disk artwork; pinning the
|
||||
generator output prevents silent drift if `usvg`/`resvg` ever
|
||||
tweak rendering. Skip if the test runtime cost is unacceptable.
|
||||
4. **Land the new artwork** (PNG bytes from step 2 committed to
|
||||
`assets/cards/`) **and** the constant migration in the *same
|
||||
commit*:
|
||||
- `CARD_FACE_COLOUR` → `Color::srgb(0.102, 0.102, 0.102)` (`#1a1a1a`)
|
||||
- `RED_SUIT_COLOUR` → `Color::srgb(0.984, 0.624, 0.694)` (`#fb9fb1`)
|
||||
- `BLACK_SUIT_COLOUR` → `Color::srgb(0.816, 0.816, 0.816)` (`#d0d0d0`)
|
||||
- `CARD_FACE_COLOUR_RED_CBM` → `Color::srgb(0.435, 0.761, 0.937)` (`#6fc2ef`) — note this is now the colour-blind *suit* colour, not a face tint; semantics shift slightly.
|
||||
- `card_back_colour(idx)` — re-author for the Terminal palette;
|
||||
index 0 stays the canonical "Terminal" back from `design-system.md`.
|
||||
5. **Test updates land in step 4's commit.** The pinning tests at
|
||||
`card_plugin.rs` lines 1749, 1750, 1767, 1768, 2057, 2063,
|
||||
2071, 2081 all assert against the old constants. New
|
||||
assertions update in lockstep with the constant changes.
|
||||
|
||||
## CBM (color-blind mode) semantics shift — flag
|
||||
|
||||
The **legacy** `CARD_FACE_COLOUR_RED_CBM` was a *face tint* — red
|
||||
suits got a light-blue background wash. The **Terminal** spec
|
||||
moves CBM into the *suit colour* itself (red glyphs swap to cyan).
|
||||
Step 4 will rename / repurpose this constant; it's not a 1:1
|
||||
replacement.
|
||||
|
||||
Two options:
|
||||
|
||||
- **Rename + repurpose:** `CARD_FACE_COLOUR_RED_CBM` →
|
||||
`RED_SUIT_COLOUR_CBM`. Communicates the semantic shift in the
|
||||
symbol name. Requires touching every callsite.
|
||||
- **Keep the name, change the meaning:** less code churn but
|
||||
worse for greppability — a future reader hitting the legacy
|
||||
name will assume face-tint behaviour.
|
||||
|
||||
Recommendation: **rename**. The CBM swap is a one-frame operation
|
||||
even if it touches every existing callsite (currently lines 642,
|
||||
2071, 2081 per `grep -n CARD_FACE_COLOUR_RED_CBM`).
|
||||
|
||||
## Theme system — out of scope here
|
||||
|
||||
The card-theme system (`docs/CARD_PLAN.md`, `theme/plugin.rs`)
|
||||
already supports user-supplied themes via `assets/themes/<theme>/`
|
||||
SVG files rasterised by `svg_loader.rs`. The new Terminal artwork
|
||||
is the **default theme**, not a new entry in the theme picker —
|
||||
the theme system continues to overlay user themes on top of the
|
||||
default at runtime.
|
||||
|
||||
If the next session wants to also ship Terminal as a *named theme
|
||||
slot* (so a user can switch back to the legacy artwork via the
|
||||
theme picker), that's an additive change after step 4 and lives
|
||||
in `theme::plugin::apply_theme_to_card_image_set`.
|
||||
|
||||
## Test impact summary
|
||||
|
||||
`grep -n CARD_FACE_COLOUR\\b\|RED_SUIT_COLOUR\\b\|BLACK_SUIT_COLOUR\\b` in
|
||||
`card_plugin.rs`:
|
||||
|
||||
- Line 1749–1750: red-suit text colour assertions (♥ + ♦).
|
||||
- Line 1767–1768: black-suit text colour assertions (♠ + ♣).
|
||||
- Line 2057, 2063: face-colour assertion in default mode.
|
||||
- Line 2071, 2081: face-colour assertion in CBM.
|
||||
|
||||
The four suit-colour and two face-colour tests are **invariant
|
||||
guards** — they exist precisely so a constant tweak surfaces here
|
||||
rather than in a visual review. Step 4 updates each in lockstep
|
||||
with the constant value change. No new test infrastructure
|
||||
needed.
|
||||
|
||||
## Open questions to resolve before step 4
|
||||
|
||||
1. **Border colour conflict.** The spec (line 218) says "Border:
|
||||
1 px solid in suit colour." The fallback path doesn't draw a
|
||||
border today — it draws solid-colour sprites. Step 4 either:
|
||||
(a) leaves the fallback as solid-colour squares (the test
|
||||
environment doesn't visually validate borders anyway), or
|
||||
(b) extends the fallback renderer to paint a 1 px outline.
|
||||
Recommend (a) — fallback fidelity isn't load-bearing.
|
||||
2. **Glyph rendering in the constant fallback.** The fallback
|
||||
today doesn't render suit glyphs at all — it's a coloured
|
||||
square. The spec's filled-vs-outlined glyph differentiation
|
||||
only matters in the PNG path. No change to the constant
|
||||
fallback for glyphs.
|
||||
3. **High-contrast mode.** `design-system.md` line 274 mentions
|
||||
a high-contrast accessibility mode (boosts foreground from
|
||||
`#d0d0d0` to `#f5f5f5`, suit-red from `#fb9fb1` to `#ff8aa0`).
|
||||
Not currently implemented anywhere; out of scope for this
|
||||
migration but worth flagging for a future accessibility pass.
|
||||
|
||||
## Post-migration — what's still open
|
||||
|
||||
- **High-contrast mode** (above).
|
||||
- **Reduced-motion mode** for card lift / drop transitions
|
||||
(also a `design-system.md` accessibility item, separate from
|
||||
artwork).
|
||||
- **The 9 missing-plugin screens** (splash, challenge,
|
||||
time-attack, weekly-goals, leaderboard, sync, level-up,
|
||||
replay, radial-menu) per `project_ui_overhaul` memory still
|
||||
need their plugin ports — separate from the cards arc.
|
||||
|
||||
## Sign-off criteria for "D closed"
|
||||
|
||||
D from the SESSION_HANDOFF Resume prompt is closed when **all of
|
||||
the following hold simultaneously**:
|
||||
|
||||
- The 52 face PNGs + 5 back PNGs in `assets/cards/` are the
|
||||
Terminal-aesthetic artwork (regeneratable via the example).
|
||||
- The five `card_plugin` constants reflect the Terminal palette.
|
||||
- All pinning tests pass against the new values.
|
||||
- A human boots the game and sees Terminal cards (not white
|
||||
cards). This sign-off needs a real `cargo run`, not just
|
||||
`cargo test`.
|
||||
@@ -0,0 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
|
||||
<title>Challenge Mode Menu</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"error-container": "#93000a",
|
||||
"tertiary": "#f7c3ff",
|
||||
"on-primary-container": "#004f6c",
|
||||
"on-surface": "#e0e3e6",
|
||||
"surface-dim": "#101417",
|
||||
"surface-container-high": "#272a2d",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"secondary-container": "#435401",
|
||||
"suit-red": "#fb9fb1",
|
||||
"on-error": "#690005",
|
||||
"surface-container-low": "#181c1f",
|
||||
"surface-variant": "#313538",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"primary-container": "#6fc2ef",
|
||||
"background": "#101417",
|
||||
"primary": "#a1dcff",
|
||||
"outline": "#505050",
|
||||
"suit-black": "#d0d0d0",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"surface-container": "#202020",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"error": "#fb9fb1",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"highlight-valid": "#acc267",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"on-error-container": "#ffdad6",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"on-secondary": "#293500",
|
||||
"on-tertiary": "#4c195b",
|
||||
"on-background": "#e0e3e6",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"secondary": "#bad073",
|
||||
"inverse-primary": "#00668a",
|
||||
"surface-bright": "#363a3d",
|
||||
"surface": "#151515",
|
||||
"on-tertiary-container": "#683476",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"warning": "#ddb26f",
|
||||
"info": "#12cfc0",
|
||||
"surface-container-highest": "#313538",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"on-primary": "#003549"
|
||||
},
|
||||
"fontFamily": {
|
||||
"mono": ["JetBrains Mono", "monospace"],
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
background-color: #101417;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.retro-scanline {
|
||||
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.03));
|
||||
background-size: 100% 2px, 3px 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen text-on-background overflow-hidden">
|
||||
<!-- Mobile Container (390x844) -->
|
||||
<div class="relative w-[390px] h-[844px] bg-background flex flex-col overflow-hidden border border-outline-variant">
|
||||
<!-- Status Bar -->
|
||||
<div class="h-[32px] bg-surface-container flex items-center justify-between px-4 text-[11px] font-mono border-b border-outline-variant shrink-0">
|
||||
<span class="text-suit-black">▌challenge.tsx</span>
|
||||
<span class="text-[#a0a0a0]">LV 12 · UNLOCKED</span>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<header class="h-[80px] px-margin-edge flex flex-col justify-center border-b border-outline-variant shrink-0">
|
||||
<h1 class="text-[24px] font-bold leading-tight text-suit-black">CHALLENGE MODE</h1>
|
||||
<p class="text-[12px] text-[#a0a0a0] mt-1">Curated puzzles · Beat par for bonus XP</p>
|
||||
</header>
|
||||
<!-- Stats Row -->
|
||||
<div class="mx-margin-edge mt-4 bg-surface-container rounded-[4px] p-3 flex items-center justify-between border border-outline-variant shrink-0">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-[14px] font-bold text-suit-black">DONE 8/24</span>
|
||||
<span class="text-[14px] font-bold text-highlight-celebration">(33%)</span>
|
||||
</div>
|
||||
<span class="text-outline-variant text-[14px]">│</span>
|
||||
<div class="text-[14px] font-bold text-suit-black">BEST AVG 03:42</div>
|
||||
<span class="text-outline-variant text-[14px]">│</span>
|
||||
<div class="text-[14px] font-bold text-highlight-valid">+1240 XP</div>
|
||||
</div>
|
||||
<!-- Scrollable List Area -->
|
||||
<div class="flex-1 overflow-y-auto px-margin-edge pt-4 space-y-3 pb-6">
|
||||
<!-- Card 1 -->
|
||||
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||
<div class="w-[6px] h-full bg-warning"></div>
|
||||
<div class="flex-1 flex items-center justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[14px] font-bold text-suit-black">DEEP STACK</span>
|
||||
<span class="text-[12px] text-on-surface-variant">Win with 0 stock · ★★★☆☆</span>
|
||||
</div>
|
||||
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||
✓ DONE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 2 -->
|
||||
<div class="h-[80px] bg-surface-container border border-primary rounded-[4px] flex relative overflow-hidden">
|
||||
<div class="w-[6px] h-full bg-highlight-valid"></div>
|
||||
<div class="flex-1 flex items-center justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[14px] font-bold text-suit-black">SPEED RUN</span>
|
||||
<span class="text-[12px] text-on-surface-variant">Win under 2:30 · ★★☆☆☆</span>
|
||||
</div>
|
||||
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||
▶ ACTIVE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 3 -->
|
||||
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||
<div class="w-[6px] h-full bg-suit-red"></div>
|
||||
<div class="flex-1 flex items-center justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[14px] font-bold text-suit-black">NO UNDO</span>
|
||||
<span class="text-[12px] text-on-surface-variant">Win without undo · ★★★★☆</span>
|
||||
</div>
|
||||
<div class="bg-primary px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||
▶ ACTIVE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 4 -->
|
||||
<div class="h-[80px] bg-surface-container border border-outline rounded-[4px] flex relative overflow-hidden">
|
||||
<div class="w-[6px] h-full bg-info"></div>
|
||||
<div class="flex-1 flex items-center justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[14px] font-bold text-suit-black">FOUR SUITS</span>
|
||||
<span class="text-[12px] text-on-surface-variant">1 card per suit · ★☆☆☆☆</span>
|
||||
</div>
|
||||
<div class="bg-highlight-valid px-2 py-1 rounded-[2px] text-background text-[11px] font-bold">
|
||||
✓ DONE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 5 (Locked) -->
|
||||
<div class="h-[80px] bg-surface-container border border-outline-variant rounded-[4px] flex relative overflow-hidden opacity-60">
|
||||
<div class="w-[6px] h-full bg-highlight-celebration"></div>
|
||||
<div class="flex-1 flex items-center justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-[14px] font-bold text-suit-black">PERFECT RUN</span>
|
||||
<span class="text-[12px] text-on-surface-variant">Below par moves · ★★★★★</span>
|
||||
</div>
|
||||
<div class="bg-outline px-2 py-1 rounded-[2px] text-on-surface text-[11px] font-bold">
|
||||
🔒 LOCKED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filler Graphic for retro feel -->
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<div class="h-[1px] flex-1 bg-outline-variant"></div>
|
||||
<span class="px-4 text-[10px] text-outline text-label-caps">END OF LIST</span>
|
||||
<div class="h-[1px] flex-1 bg-outline-variant"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Shared Component: Terminal Context (Used as Footer) -->
|
||||
<div class="h-[24px] bg-surface px-4 flex items-center justify-between text-[10px] font-mono border-t border-outline-variant shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-primary">▌ NORMAL</span>
|
||||
<span class="text-outline">│</span>
|
||||
<span class="text-on-surface-variant uppercase tracking-widest">challenge</span>
|
||||
</div>
|
||||
<div class="text-[#a0a0a0] flex items-center gap-3">
|
||||
<span>[ENTER] select</span>
|
||||
<span>[F] filter</span>
|
||||
<span class="text-suit-red">[ESC] back</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Retro Scanline Overlay -->
|
||||
<div class="absolute inset-0 retro-scanline z-50"></div>
|
||||
</div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=390, height=844, initial-scale=1.0" name="viewport"/>
|
||||
<title>Rusty Solitaire - Daily Challenge</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
body {
|
||||
background-color: #101417;
|
||||
color: #e0e3e6;
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.scanline-bg {
|
||||
background: linear-gradient(to bottom, transparent 50%, rgba(26, 26, 26, 0.5) 50%);
|
||||
background-size: 100% 4px;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"on-error": "#690005",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"tertiary": "#f7c3ff",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"primary-container": "#6fc2ef",
|
||||
"surface-dim": "#101417",
|
||||
"surface-variant": "#313538",
|
||||
"on-error-container": "#ffdad6",
|
||||
"warning": "#ddb26f",
|
||||
"on-surface": "#e0e3e6",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"error-container": "#93000a",
|
||||
"on-tertiary": "#4c195b",
|
||||
"info": "#12cfc0",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"primary": "#a1dcff",
|
||||
"on-primary": "#003549",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"highlight-valid": "#acc267",
|
||||
"surface-container-low": "#181c1f",
|
||||
"surface-container": "#1c2023",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"secondary-container": "#435401",
|
||||
"error": "#fb9fb1",
|
||||
"surface": "#151515",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"outline": "#505050",
|
||||
"surface-container-highest": "#313538",
|
||||
"on-secondary": "#293500",
|
||||
"on-primary-container": "#004f6c",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"background": "#101417",
|
||||
"surface-container-high": "#272a2d",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"suit-red": "#fb9fb1",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"inverse-primary": "#00668a",
|
||||
"surface-bright": "#363a3d",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"on-background": "#e0e3e6",
|
||||
"on-tertiary-container": "#683476",
|
||||
"suit-black": "#d0d0d0",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"secondary": "#bad073",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"highlight-celebration": "#e1a3ee"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"gutter-card": "0.375rem",
|
||||
"stack-overlap": "2rem",
|
||||
"margin-edge": "1rem",
|
||||
"action-bar-height": "64px",
|
||||
"touch-target-min": "48dp"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"],
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"headline": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen max-w-[390px] mx-auto overflow-hidden shadow-2xl border-x border-outline">
|
||||
<!-- 1. Status Bar -->
|
||||
<div class="h-[32px] bg-[#202020] flex items-center justify-between px-margin-edge border-b border-outline">
|
||||
<span class="font-hud-timer text-[12px] text-on-surface-variant">▌daily/2024-127.json</span>
|
||||
<div class="bg-warning/10 border border-warning px-2 py-0.5 rounded-sm">
|
||||
<span class="font-hud-timer text-[11px] text-warning font-bold tracking-tighter">EXPIRES 11:42:30</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="flex-1 p-margin-edge space-y-4 overflow-y-auto pb-8">
|
||||
<!-- 2. Header Card -->
|
||||
<section class="h-[130px] bg-[#1a1a1a] border border-[#353535] rounded-lg p-4 flex flex-col justify-between">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-headline font-bold text-[24px] text-suit-black leading-none">MAY 07 · 2026</span>
|
||||
<span class="font-headline font-extrabold text-[32px] text-highlight-valid -tracking-[0.01em] leading-tight">#2024-127</span>
|
||||
</div>
|
||||
<span class="font-label-caps text-[11px] text-on-surface-variant/70">DRAW-3 · DIFFICULTY ★★★☆☆ · PAR 04:30</span>
|
||||
</section>
|
||||
<!-- 3. Primary CTA -->
|
||||
<button class="w-full h-[64px] bg-primary-container text-surface font-headline font-bold text-[14px] uppercase tracking-wider rounded-lg active:scale-95 transition-transform duration-80 flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-[18px]">play_arrow</span>
|
||||
ATTEMPT TODAY'S SEED
|
||||
</button>
|
||||
<!-- 4. Your Attempts Card -->
|
||||
<section class="h-[96px] bg-[#202020] rounded-lg p-4 flex flex-col justify-between">
|
||||
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase">YOUR ATTEMPTS</span>
|
||||
<div class="flex justify-between items-end">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-hud-score text-[16px] text-suit-black">BEST 04:12</span>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="bg-warning text-surface text-[10px] font-bold px-1.5 py-0.5 rounded-sm">WIN</span>
|
||||
<span class="font-label-caps text-[11px] text-warning">RANK 17/2843</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[13px] text-error mb-1">LAST: FAILED at move 47</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 5. Leaderboard Card -->
|
||||
<section class="bg-[#202020] rounded-lg p-4 flex flex-col flex-grow">
|
||||
<span class="font-label-caps text-[11px] text-on-surface-variant/60 uppercase mb-4">TOP TODAY · 2,843 PLAYERS</span>
|
||||
<div class="space-y-0 divide-y divide-[#353535]">
|
||||
<!-- Row 1 -->
|
||||
<div class="h-[32px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-warning text-surface text-[10px] font-bold rounded-full">01</span>
|
||||
<span class="font-hud-timer text-[14px]">swift_jaguar</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-on-surface-variant">02:47</span>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
<div class="h-[32px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-[#a0a0a0] text-surface text-[10px] font-bold rounded-full">02</span>
|
||||
<span class="font-hud-timer text-[14px]">base16_fan</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:12</span>
|
||||
</div>
|
||||
<!-- Row 3 -->
|
||||
<div class="h-[32px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-[#7a5d3b] text-surface text-[10px] font-bold rounded-full">03</span>
|
||||
<span class="font-hud-timer text-[14px]">cli_player</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-on-surface-variant">03:54</span>
|
||||
</div>
|
||||
<!-- Row 4 -->
|
||||
<div class="h-[32px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">04</span>
|
||||
<span class="font-hud-timer text-[14px]">tablejockey</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:01</span>
|
||||
</div>
|
||||
<!-- Row 5 -->
|
||||
<div class="h-[32px] flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-[#353535] text-on-surface-variant text-[10px] font-bold rounded-full">05</span>
|
||||
<span class="font-hud-timer text-[14px]">vim_motions</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-on-surface-variant">04:05</span>
|
||||
</div>
|
||||
<!-- Row 17 (YOU) -->
|
||||
<div class="h-[36px] flex items-center justify-between bg-primary-container/10 -mx-4 px-4 border-y border-primary-container/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-5 h-5 flex items-center justify-center bg-primary-container text-surface text-[10px] font-bold rounded-full">17</span>
|
||||
<span class="font-hud-timer text-[14px] text-primary-container font-bold">(YOU) anonymous</span>
|
||||
</div>
|
||||
<span class="font-hud-timer text-[14px] text-primary-container font-bold">04:12</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex-1 border-t border-[#353535] pt-4 flex flex-col items-center justify-center opacity-30 select-none">
|
||||
<span class="material-symbols-outlined text-[48px]">terminal</span>
|
||||
<span class="font-label-caps text-[10px] mt-2">END OF VISIBLE LOG</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- 6. Footer Navigation -->
|
||||
<footer class="h-[24px] bg-background border-t border-outline flex items-center justify-between px-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-label-caps text-[10px] text-on-surface-variant">▌ NORMAL │ daily</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ENTER]</span> attempt</span>
|
||||
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[L]</span> full leaderboard</span>
|
||||
<span class="font-label-caps text-[9px] text-on-surface-variant/70"><span class="text-primary-container">[ESC]</span> back</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Shared Component Shell Rendering Logic -->
|
||||
<header class="w-full top-0 sticky bg-background border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||
<h1 class="font-headline text-headline text-primary uppercase tracking-widest">RUSTY SOLITAIRE</h1>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors duration-120 cursor-pointer">settings</span>
|
||||
</header>
|
||||
<nav class="fixed bottom-0 w-full h-action-bar-height z-50 bg-surface-container border-t border-outline flex justify-around items-center px-2 hidden">
|
||||
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
<span class="font-label-caps text-label-caps">DEAL</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
<span class="font-label-caps text-label-caps">UNDO</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||
<span class="material-symbols-outlined">lightbulb</span>
|
||||
<span class="font-label-caps text-label-caps">HINT</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center text-primary dark:text-primary-fixed-dim hover:bg-surface-container-highest transition-colors duration-120 cursor-pointer w-full h-full">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
<span class="font-label-caps text-label-caps">MENU</span>
|
||||
</div>
|
||||
</nav>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: Terminal
|
||||
colors:
|
||||
surface: '#151515'
|
||||
surface-dim: '#0d0d0d'
|
||||
surface-bright: '#2a2a2a'
|
||||
surface-container-lowest: '#0a0a0a'
|
||||
surface-container-low: '#1a1a1a'
|
||||
surface-container: '#202020'
|
||||
surface-container-high: '#2a2a2a'
|
||||
surface-container-highest: '#353535'
|
||||
on-surface: '#d0d0d0'
|
||||
on-surface-variant: '#a0a0a0'
|
||||
inverse-surface: '#d0d0d0'
|
||||
inverse-on-surface: '#151515'
|
||||
outline: '#505050'
|
||||
outline-variant: '#353535'
|
||||
surface-tint: '#a54242'
|
||||
primary: '#a54242'
|
||||
on-primary: '#151515'
|
||||
primary-container: '#3a1f1f'
|
||||
on-primary-container: '#d5a8a8'
|
||||
inverse-primary: '#993e3e'
|
||||
secondary: '#acc267'
|
||||
on-secondary: '#151515'
|
||||
secondary-container: '#2a3320'
|
||||
on-secondary-container: '#c5d585'
|
||||
tertiary: '#e1a3ee'
|
||||
on-tertiary: '#151515'
|
||||
tertiary-container: '#3a2a40'
|
||||
on-tertiary-container: '#eec3f5'
|
||||
error: '#fb9fb1'
|
||||
on-error: '#151515'
|
||||
error-container: '#4a2530'
|
||||
on-error-container: '#fdc3ce'
|
||||
background: '#151515'
|
||||
on-background: '#d0d0d0'
|
||||
surface-variant: '#353535'
|
||||
suit-red: '#fb9fb1'
|
||||
suit-black: '#d0d0d0'
|
||||
suit-red-cb: '#acc267'
|
||||
highlight-valid: '#acc267'
|
||||
highlight-celebration: '#e1a3ee'
|
||||
highlight-warning: '#ddb26f'
|
||||
highlight-info: '#12cfc0'
|
||||
typography:
|
||||
hud-score:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 24px
|
||||
fontWeight: '700'
|
||||
lineHeight: 32px
|
||||
letterSpacing: '-0.02em'
|
||||
hud-timer:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: 24px
|
||||
card-rank:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 18px
|
||||
fontWeight: '700'
|
||||
lineHeight: 18px
|
||||
body-md:
|
||||
fontFamily: Inter
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: 24px
|
||||
label-caps:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 12px
|
||||
fontWeight: '500'
|
||||
lineHeight: 16px
|
||||
letterSpacing: '0.08em'
|
||||
headline:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 28px
|
||||
fontWeight: '700'
|
||||
lineHeight: 32px
|
||||
letterSpacing: '-0.01em'
|
||||
rounded:
|
||||
sm: 0.125rem
|
||||
DEFAULT: 0.25rem
|
||||
md: 0.5rem
|
||||
lg: 0.75rem
|
||||
xl: 1rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
margin-edge: 1rem
|
||||
gutter-card: 0.375rem
|
||||
stack-overlap: 2rem
|
||||
touch-target-min: 48dp
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
|
||||
The "Terminal" design system replaces the previous "Premium Solitaire" calm-indie aesthetic with a **retro-terminal / synthwave** identity. The intent is the visual confidence of a well-tuned terminal emulator (think Berkeley Mono dotfiles, base16-eighties, CRT phosphor): monospaced, dense, legible, snappy. It is *not* casino-glitz, *not* skeuomorphic felt, and *not* whimsical.
|
||||
|
||||
The personality is **technical, deliberate, slightly playful**. Cards are flat with thin colored strokes; the HUD reads like a status bar; modals look like terminal panes. Motion is short and snap-easing — no bouncy springs. Long-session calm is preserved by keeping the chroma low and reserving saturated accents for *meaning* (CTAs, feedback, celebrations) rather than decoration.
|
||||
|
||||
Influences: base16-eighties (Chris Kempson), Berkeley Mono, Vim/Neovim status lines, the iA Writer aesthetic, classic CRT phosphor with no chromatic aberration.
|
||||
|
||||
## Palette
|
||||
|
||||
The palette is base16-eighties — a 16-slot terminal palette where indices 00–07 form a monochrome ramp and 08–0F provide saturated accents. We map base16 slots to Material Design 3 token roles below.
|
||||
|
||||
### Source palette (base16-eighties)
|
||||
|
||||
| Slot | Hex | Role |
|
||||
|---|---|---|
|
||||
| base00 | `#151515` | background |
|
||||
| base01 | `#202020` | surface-container |
|
||||
| base02 | `#303030` | line-highlight (subtle) |
|
||||
| base03 | `#505050` | outline / muted text |
|
||||
| base04 | `#b0b0b0` | secondary text |
|
||||
| base05 | `#d0d0d0` | foreground / on-surface |
|
||||
| base06 | `#e0e0e0` | bright text |
|
||||
| base07 | `#f5f5f5` | brightest highlight |
|
||||
| base08 | `#fb9fb1` | red — used for `error`, `suit-red` |
|
||||
| base09 | `#ddb26f` | orange — used for warning chips |
|
||||
| base0A | `#acc267` | yellow/lime — used for `highlight-valid` (drag targets, valid moves) |
|
||||
| base0B | `#12cfc0` | green/teal — used for `highlight-info` (toasts, neutral status) |
|
||||
| base0C | `#6fc2ef` | cyan/sky — historically the primary CTA; now reserved for ad-hoc accents only |
|
||||
| base0D | `#6fc2ef` | (alias) |
|
||||
| base08 (project) | `#a54242` | brick red — primary CTA, focus ring, `selection` (project-specific extension; the base16-eighties `base08` slot is `#fb9fb1` pink which we keep as `error`/`suit-red`) |
|
||||
| `suit-red-cb` slot | `#acc267` | lime — color-blind-mode swap for red suits (was `#6fc2ef` cyan before the 2026-05-08 primary-accent swap; lime is the next-best non-red base16-eighties accent) |
|
||||
| base0E | `#e1a3ee` | violet — used for celebration (level-up, achievement unlock) |
|
||||
| base0F | `#fb9fb1` | (alias) |
|
||||
|
||||
### Semantic assignments
|
||||
|
||||
- **CTA / Primary action**: brick red `#a54242`. Reserved for "Play," "New Game," "Save," "Resume," and the focus ring on selected cards. Never used decoratively. (Was cyan `#6fc2ef` before the 2026-05-08 swap.)
|
||||
- **Valid-move / drag-target highlight**: lime `#acc267`. Reserved for in-game feedback only. Never appears in chrome.
|
||||
- **Celebration**: lavender `#e1a3ee`. Used for level-up flashes, achievement unlock cards, and the daily-streak chip when the streak is active. Quiet otherwise.
|
||||
- **Warning / soft alert**: gold `#ddb26f`. Used for "challenge expires in N minutes" chips, sync-pending status, and the daily-seed countdown.
|
||||
- **Info**: teal `#12cfc0`. Used for neutral system toasts and the sync-connected indicator.
|
||||
- **Error**: pink `#fb9fb1`. Used for sync conflict, server unreachable, invalid move shake.
|
||||
|
||||
## Suit Colors
|
||||
|
||||
**Two-color traditional pairing**, with mandatory color-blind
|
||||
support. Saturated red for hearts + diamonds, near-white for clubs
|
||||
+ spades — the "Microsoft Solitaire on dark mode" feel of a real
|
||||
playing-card deck. (A brief 4-color-deck experiment shipped between
|
||||
v0.21.0 and the next post-cut commit; reverted to traditional
|
||||
2-color at the player's request.)
|
||||
|
||||
| Suit | Default | Color-blind mode | Glyph differentiation |
|
||||
|---|---|---|---|
|
||||
| Hearts | `#e35353` (saturated red) | `#acc267` (lime) | Solid filled glyph |
|
||||
| Diamonds | `#e35353` (saturated red) | `#acc267` (lime) | **Outlined glyph (1.5px stroke)** |
|
||||
| Spades | `#e8e8e8` (near-white) | `#e8e8e8` (unchanged) | Solid filled glyph |
|
||||
| Clubs | `#e8e8e8` (near-white) | `#e8e8e8` (unchanged) | **Outlined glyph (1.5px stroke)** |
|
||||
|
||||
The outlined-glyph treatment is the **primary** differentiation mechanism. Color is supplementary. This means a player viewing the game on a monochrome display, or with severe red-green deficiency, can still distinguish all four suits without context. This is a hard requirement, not an optional setting.
|
||||
|
||||
The "color-blind mode" toggle in Settings swaps both red suits (hearts + diamonds) from `#e35353` to `#acc267` (lime); clubs + spades stay at the near-white. The toggle does not turn the outlined glyphs on or off, because outlined glyphs are always on. (Was red→cyan before the 2026-05-08 primary-accent swap; CBM moved to lime to stay hue-distinct from the new red-family primary.)
|
||||
|
||||
## Typography
|
||||
|
||||
**Monospace-forward, dual-font system.**
|
||||
|
||||
- **JetBrains Mono** is used for: HUD (score, timer, moves), card rank/value text, all labels, all headlines, all numerals anywhere in the app, and any chip-style component. This is the dominant face.
|
||||
- **Inter** is used only for: long-form body copy (Help screen, Settings descriptions, achievement tooltips, onboarding copy). It is the *exception*, not the default.
|
||||
|
||||
Weights: 400 regular, 500 medium for labels, 700 bold for HUD numbers and headlines. No 600 / no italics anywhere — the terminal aesthetic doesn't have them.
|
||||
|
||||
Letter spacing: tight (`-0.02em`) on HUD score for visual mass; wide (`+0.08em`) on uppercase labels for readability at 12px. Body uses default (0).
|
||||
|
||||
HUD numbers must use **tabular figures** (`font-feature-settings: 'tnum'`) so the timer and score don't reflow as digits change.
|
||||
|
||||
## Layout & Spacing
|
||||
|
||||
Optimized for **Android portrait, 390×844 (Pixel 6 baseline), API 34**.
|
||||
|
||||
- **Margins**: 16px (1rem) edge safety margin. *Tighter than the previous system's 24px.* Eighties palettes are dense by nature; over-padding fights the aesthetic.
|
||||
- **Tableau**: 7-column layout, 32px (2rem) vertical card overlap. Tighter than before to fit a longer cascade on phone screens.
|
||||
- **HUD position**: top of screen, in the system safe area. Bottom 64px holds the action bar (Undo / Hint / New Game / Auto-complete). Action bar is **always visible** in-game — no hover-fade — because there is no hover on touch.
|
||||
- **Touch target minimum**: 48dp on all interactive elements. Cards in the tableau may be smaller visually but use a 48dp invisible hit area centered on the visible glyph.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Depth is created through **tonal layering and 1px outlines**, not blur shadows. (Synthwave-flat, not Material-soft.)
|
||||
|
||||
- **Level 0 (Background)**: the `#151515` base canvas.
|
||||
- **Level 1 (Tableau slots, empty piles)**: 1px dashed outline in `#353535`. Empty foundations show a faint suit glyph at 12% opacity inside the outline.
|
||||
- **Level 2 (Cards at rest)**: solid `#1a1a1a` fill, 1px solid border in the suit color (so the suit is detectable at a glance even if the card is partially obscured).
|
||||
- **Level 3 (Active / dragged card)**: same border, but glow effect: 0 0 12px of `#a54242` at 40% opacity. **No scale transform** — flatness preserved. Z-index lifts above siblings.
|
||||
- **Modals**: full-screen with backdrop `#151515` at 95% opacity (just enough to dim the table without blurring it). Modal panel is `#202020` with a 1px `#505050` border — like a terminal pane.
|
||||
- **Toasts**: bottom of screen, `#202020` fill, 1px border in the toast's accent color (info=teal, warning=gold, error=pink, celebration=lavender). 16px monospaced caption.
|
||||
|
||||
No `box-shadow` is used anywhere. **All depth is achieved with borders and tonal value.** This is a hard constraint.
|
||||
|
||||
## Shapes
|
||||
|
||||
The shape language is **soft-rounded but tight**:
|
||||
|
||||
- **Cards**: `rounded-md` (8px) — slightly less rounded than the previous system's 16px to read more "technical."
|
||||
- **Buttons / chips / inputs**: `rounded` (4px) default, `rounded-sm` (2px) for the smallest chips.
|
||||
- **Modals / sheets**: `rounded-lg` (12px).
|
||||
- **Avatars / circular indicators**: `rounded-full`.
|
||||
- **Card-back pattern corners**: matches the card's `rounded-md`.
|
||||
|
||||
Selection highlights use a **2px inset stroke** in `#a54242` following the host shape's corner radius. Never an outer stroke — the outer stroke is reserved for the suit-color hairline.
|
||||
|
||||
## Motion
|
||||
|
||||
**Snappy, no spring.** All transitions use `ease-out` with a 120ms duration unless specified.
|
||||
|
||||
- Card lift (start drag): 80ms.
|
||||
- Card place (drop): 120ms with a 16ms holdframe (no bounce).
|
||||
- Modal enter: 200ms ease-out, fade + 8px translate-up.
|
||||
- Modal exit: 120ms ease-in, fade only.
|
||||
- Selection ring appear: 80ms.
|
||||
- Win-summary stat reveal: 60ms each, staggered 40ms.
|
||||
- HUD number tick: instant (no transition) — terminal counters don't ease.
|
||||
|
||||
**Optional CRT effect**: a 1-frame scanline sweep across the screen on game-state transitions (start, win, restart). User-toggleable in Settings. Off by default.
|
||||
|
||||
## Components
|
||||
|
||||
### Game Cards
|
||||
|
||||
Flat face design.
|
||||
- Background: `#1a1a1a`
|
||||
- Border: none. The card shape is defined by the body fill alone against the play surface. The earlier 1px suit-coloured border was removed because it produced visible anti-aliasing artifacts at the rounded corners (a "gray sliver" where the colored stroke faded through gray pixels into the dark play surface). The 5-unit brightness gap between `#1a1a1a` body and `#151515` surface is enough to read as a card edge without an explicit stroke.
|
||||
- Top-left: rank in JetBrains Mono Bold 18px + small suit glyph (10px)
|
||||
- Bottom-right: large suit glyph (32px), upright (same orientation as the top-left small glyph — single-orientation digital play does not benefit from the traditional 180° inverted-corner indicator)
|
||||
- Corner radius: 8px
|
||||
- Suit differentiation: hearts and spades have **filled** glyphs; diamonds and clubs have **outlined** glyphs (1.5px stroke)
|
||||
|
||||
### Card Back ("Terminal" theme)
|
||||
|
||||
- Theme name: `"Terminal"`
|
||||
- Author: `"Rusty Solitaire"`
|
||||
- Background: `#151515`
|
||||
- Pattern: horizontal scanlines at 2px pitch in `#1a1a1a` (1px line, 1px gap), full bleed
|
||||
- Border: 1px solid `#353535`
|
||||
- Top-left badge: a 12×16px solid `#a54242` block (the "terminal cursor"), 6px from the corner
|
||||
- Bottom-right monogram: the characters `▌RS` in JetBrains Mono 12px, color `#505050`, 6px from the corner
|
||||
- Corner radius: 8px (matches face)
|
||||
|
||||
### Primary Buttons
|
||||
|
||||
Solid `#a54242` fill, `#151515` text, JetBrains Mono Medium 14px uppercase with `+0.08em` tracking. 4px corner radius. Pressed state: darken to `#7a3030`. Disabled: `#353535` fill, `#505050` text.
|
||||
|
||||
### Secondary Buttons
|
||||
|
||||
Transparent fill, 1px `#505050` border, `#d0d0d0` text. Hover/press: border becomes `#a54242`, text becomes `#a54242`.
|
||||
|
||||
### HUD Chips
|
||||
|
||||
`#202020` fill, no border, 4px radius. Monospaced 16px text. Score chip pulses to `#acc267` for 200ms when score increases.
|
||||
|
||||
### Drag Targets
|
||||
|
||||
When a card is being dragged over a valid pile, the pile's empty-slot dashed outline becomes:
|
||||
- Solid 1px in `#acc267`
|
||||
- Plus a 0 0 8px outer glow in `#acc267` at 30% opacity
|
||||
|
||||
This is the *only* place glow effects appear in the system.
|
||||
|
||||
### Modals
|
||||
|
||||
Full-screen backdrop at 95% opacity. Centered panel: `#202020` fill, 1px `#505050` border, 12px corner radius. Title bar shows the screen name in monospaced 14px, color `#a0a0a0`, with a single `▌` cursor character prefix to reinforce the terminal pane motif.
|
||||
|
||||
### Navigation Bar
|
||||
|
||||
Fixed at the bottom of in-game screens. Height: 64px. `#202020` fill, 1px top border in `#353535`. Four icon buttons: Undo / Hint / New / Auto-complete. Icons: 24px, 1.5px stroke weight, color `#d0d0d0`. Active/pressed: icon color `#a54242`.
|
||||
|
||||
### Status / Sync Indicator
|
||||
|
||||
Top-right corner of the HUD: a 6px circular dot.
|
||||
- Connected & synced: `#12cfc0`
|
||||
- Pending: `#ddb26f` (pulsing 1.5s)
|
||||
- Error: `#fb9fb1` (steady)
|
||||
- Offline: `#505050`
|
||||
|
||||
## Accessibility
|
||||
|
||||
1. **Color-blind mode** (Settings → Gameplay): swaps the red suits' default `#e35353` for `#acc267` (lime). Outlined-glyph differentiation remains active in *all* modes.
|
||||
2. **High-contrast mode** (Settings → Gameplay): boosts on-surface from `#d0d0d0` to `#f5f5f5`, outline from `#505050` to `#a0a0a0`, suit-red from `#fb9fb1` to `#ff8aa0`.
|
||||
3. **Reduce-motion mode** (Settings → Gameplay): disables card-lift transition (instant z-lift), disables CRT scanline effect, disables the warning-chip pulse animation.
|
||||
4. **Tabular figures** are mandatory for any number that updates live (timer, score, moves) so they don't reflow.
|
||||
5. **Touch targets** are 48dp minimum even when the visual element is smaller.
|
||||
6. **Text contrast**: all body text on background passes WCAG AA at minimum (`#d0d0d0` on `#151515` = 9.5:1; `#a0a0a0` on `#151515` = 5.7:1).
|
||||
@@ -0,0 +1,283 @@
|
||||
# Terminal — Desktop Adaptation Spec
|
||||
|
||||
> **Why this exists.** The 24 mockups in this directory are mobile
|
||||
> (390 × 844 logical, iPhone 14 Pro frame) with one exception
|
||||
> (`home-menu-desktop.html`). The Stitch project that produced them
|
||||
> is named "Solitaire Quest *Mobile* Redesign" — the mobile-first
|
||||
> framing was deliberate when the new Android target opened, but
|
||||
> desktop is still the primary delivery surface. Porting the mobile
|
||||
> mockups 1:1 would land a 390-px-wide column floating in the middle
|
||||
> of an 1800 × 1100 window. This file is the rules-based desktop
|
||||
> companion — apply these adaptations whenever you port a Bevy
|
||||
> plugin against a mobile mockup in this directory.
|
||||
|
||||
## Status
|
||||
|
||||
* **Token system.** All tokens (palette, type scale, spacing,
|
||||
radii, motion) in `design-system.md` are layout-agnostic and
|
||||
apply unchanged on both targets. Do **not** introduce desktop-
|
||||
specific token variants — adapt geometry, not tokens.
|
||||
* **Already adapted in code.** v0.20.0's port is layout-agnostic
|
||||
(modal scaffold, toasts, table chrome, card chrome, gameplay-
|
||||
feedback, splash cursor). Those surfaces already adapt
|
||||
correctly because their Bevy UI nodes use flex / percent /
|
||||
stretch sizing rather than fixed pixel widths from the
|
||||
mockups.
|
||||
* **Not yet adapted in code.** Any future plugin port that
|
||||
copies layout from a mobile mockup must apply the rules below.
|
||||
|
||||
## Viewport assumptions
|
||||
|
||||
| Range | Width × height | Source |
|
||||
|---|---|---|
|
||||
| Mobile target | 390 × 844 | iPhone 14 Pro logical, Stitch mockup canvas |
|
||||
| Desktop minimum | 1024 × 600 | Smaller windows degrade to mobile rules |
|
||||
| Desktop default | ~70 % of monitor | `apply_smart_default_window_size` (since v0.19.0) |
|
||||
| Desktop typical | 1600 × 900 to 2560 × 1440 | The range we tune for |
|
||||
| Desktop max | 3840 × 2160 | 4K, with HiDPI scaling already applied |
|
||||
|
||||
The "smart default" sizer means a 1080p monitor opens a ~1344 × 756
|
||||
window, a 1440p monitor opens ~1792 × 1008, a 4K monitor opens
|
||||
~2688 × 1512. Tune for the 1600–2400 width band as the centre of
|
||||
the distribution; below 1024 width, fall back to the mobile rules
|
||||
verbatim.
|
||||
|
||||
## Universal adaptation rules
|
||||
|
||||
Apply these to every screen unless the per-screen section
|
||||
overrides them.
|
||||
|
||||
### 1. Edge margins
|
||||
|
||||
| Mobile | Desktop |
|
||||
|---|---|
|
||||
| `margin-edge: 16px` (`SPACE_4`) | `SPACE_5` (24 px) for windows < 1440 wide; `SPACE_6` (32 px) for 1440–2400; `SPACE_7` (48 px) for ≥ 2400 |
|
||||
|
||||
Engine: drive from `LayoutResource` based on `Window` size, not a
|
||||
constant.
|
||||
|
||||
### 2. Modal max-width
|
||||
|
||||
| Mobile | Desktop |
|
||||
|---|---|
|
||||
| `100% - 2 × edge-margin` | `min(720 px, 50 % of viewport)` |
|
||||
|
||||
The 720 px cap is already in `ui_modal::spawn_modal`. No code
|
||||
change needed; this rule documents *why* it's there.
|
||||
|
||||
### 3. Vertical content stacks
|
||||
|
||||
A mobile screen often stacks `Header → Body → Footer` vertically
|
||||
to fit a tall narrow column. On desktop, prefer horizontal
|
||||
distribution where the content allows:
|
||||
|
||||
* **Header rows that stack vertically on mobile** (title above
|
||||
count above timer) → keep them in one horizontal row on
|
||||
desktop.
|
||||
* **Two-column flex layouts** (e.g. Settings rows: label left,
|
||||
control right) — already work on both targets; no change.
|
||||
* **Cards stacking with `mt-48`-style fixed gaps** — replace with
|
||||
flex / percent gaps so the layout breathes.
|
||||
|
||||
### 4. Touch-target minimums
|
||||
|
||||
Mobile spec mandates 48 dp minimum touch targets. Desktop has no
|
||||
such floor (mouse precision is finer), but **don't shrink below
|
||||
mobile's 48 px** for primary actions — keyboard / gamepad focus
|
||||
rings still need a visible target.
|
||||
|
||||
Secondary controls (chip-style toggles, hotkey hints, etc.) can
|
||||
shrink to `TYPE_BODY` (14 px) text + `SPACE_3` (12 px) padding on
|
||||
desktop where they were larger on mobile.
|
||||
|
||||
### 5. Bottom-anchored elements
|
||||
|
||||
Mobile mockups often anchor key controls (action bar, primary CTA,
|
||||
toast position) to the bottom of the viewport for thumb reach.
|
||||
Desktop has no thumb-reach concern:
|
||||
|
||||
* **Toasts** — keep bottom-anchored (already done in `a137607`),
|
||||
the design language is consistent across targets and the
|
||||
bottom is still the least-disruptive overlay zone.
|
||||
* **Action bars** — top of viewport on desktop unless the
|
||||
per-screen section says otherwise. The HUD already sits on
|
||||
top.
|
||||
* **Single primary CTA** — modals already right-align in the
|
||||
actions row; no change.
|
||||
|
||||
### 6. Typography rungs unchanged
|
||||
|
||||
Do **not** shift `TYPE_*` tokens up a rung for desktop. The
|
||||
spec's 14 / 18 / 26 / 40 progression is already calibrated for
|
||||
the desktop reading distance (60–90 cm). Mobile uses the same
|
||||
rungs at a closer reading distance (30–40 cm); same physical
|
||||
angular size on the eye.
|
||||
|
||||
### 7. Hotkey hints become full strings
|
||||
|
||||
Mobile cells like `▌Esc` — the cursor block plus key letter — can
|
||||
expand to `[Esc] cancel` style on desktop where horizontal
|
||||
real-estate is cheap. Drives discoverability of keyboard-only
|
||||
flows. Optional; only apply where horizontal space exists.
|
||||
|
||||
## Per-screen adaptation rules
|
||||
|
||||
### Game Table
|
||||
|
||||
Mockup: `game-table-mobile.html` (390 × 844).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| HUD band | full width, 56 px tall | full width, 48 px tall |
|
||||
| Foundation row | 4 piles centred, fan-tight | 4 piles centred, **gutter doubled** so the row fills ~50 % of viewport width |
|
||||
| Stock + waste | left of foundations, stacked | left of foundations, **horizontal pair**: stock on the left, waste to its immediate right (the mobile vertical pair feels cramped on a wide canvas) |
|
||||
| Tableau row | 7 columns, 4 % gutter | 7 columns, **6 % gutter**, total tableau block ≤ 70 % viewport width |
|
||||
| Card aspect | 2 : 3 (already in `Layout::card_size`) | unchanged — card aspect is domain |
|
||||
| Tableau fan | `TABLEAU_FAN_FRAC = 0.25` | unchanged — fan is in card-height units, not viewport units |
|
||||
| Drag-shadow offset | small | unchanged — pinned to 0 alpha under Terminal anyway |
|
||||
|
||||
**Engine impact:** `solitaire_engine/src/layout.rs::compute_layout`
|
||||
already drives most of this from `Window::size()`. The mobile vs.
|
||||
desktop difference is the gutter percentages — bake desktop
|
||||
gutters when window width ≥ 1024.
|
||||
|
||||
### Win Summary
|
||||
|
||||
Mockup: `win-summary-mobile.html` (390 × 858).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| Modal width | 100 % − 2 × edge | **`min(720 px, 50 % viewport)`** (already done by `ui_modal`) |
|
||||
| Score row | stacked vertically (line per metric) | **3-column grid**: Score / Time / Moves in one row, breakdown rows below in single-line per row |
|
||||
| Action buttons | full-width stacked (Play Again, Continue, Stats) | **right-aligned action row** — the existing `spawn_modal_actions` already does this on both targets |
|
||||
|
||||
**Engine impact:** `solitaire_engine/src/win_summary_plugin.rs`. The
|
||||
score-breakdown-stagger animation (`MOTION_SCORE_BREAKDOWN_*`) is
|
||||
unchanged across targets.
|
||||
|
||||
### Settings
|
||||
|
||||
Mockup: `settings-mobile.html` (390 × 4330 — long scroll).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||
| Sections | full-width labels above stacked controls | **section labels left, control widget right** — already the engine's pattern; no change |
|
||||
| Long page | scroll the whole modal | **two-column layout**: nav (sections list) on left ~30 %, current section on right ~70 %. Reduces scroll distance on desktop |
|
||||
| Sliders | full-width on mobile | cap at 320 px on desktop |
|
||||
|
||||
**Engine impact:** if a desktop port wants the two-column nav, it's
|
||||
a `settings_plugin` rewrite. Keep the existing single-column
|
||||
stacked-modal layout for now — it works on both targets and the
|
||||
two-column variant is a polish item, not a blocker.
|
||||
|
||||
### Help & Controls
|
||||
|
||||
Mockup: `help-mobile.html` (390 × 2544).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||
| Section list | one column of `Heading → 2-col rows` | **two columns of section blocks** for windows ≥ 1280 wide; halves vertical scroll distance |
|
||||
| Hotkey rows | `key | description` 2-col flex | unchanged; 2-col already adapts |
|
||||
|
||||
**Engine impact:** `help_plugin`. Single-column on mobile, 2-col
|
||||
on desktop windows ≥ 1280 wide is a flex-wrap option.
|
||||
|
||||
### Pause Menu
|
||||
|
||||
Mockup: `pause-menu-mobile.html` (390 × 1768).
|
||||
|
||||
Already a small modal; no significant geometry change. Modal
|
||||
already uses `ui_modal::spawn_modal` which caps width and centres.
|
||||
No desktop-specific rule.
|
||||
|
||||
### Home Menu
|
||||
|
||||
Mockup: `home-menu-mobile.html` and `home-menu-desktop.html`
|
||||
(both already in this directory — desktop variant is the
|
||||
authoritative reference).
|
||||
|
||||
The desktop mockup already specifies the layout. Cross-check it
|
||||
against the mobile version when porting; differences are
|
||||
deliberate (more horizontal real-estate, larger primary CTA, the
|
||||
secondary actions row).
|
||||
|
||||
### Splash
|
||||
|
||||
Mockup: `splash-mobile.html` (390 × 844).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| Full-screen overlay | `inset-0` | unchanged — splash always covers the viewport |
|
||||
| Cursor block (`▌`) | 96 px JetBrains Mono | unchanged — already done in `cdcadda`. The 96 px size scales fine on desktop because the splash is a brand beat, not a layout-driven element |
|
||||
| Title `RUSTY SOLITAIRE` | 32 px | scale to 40 px (`TYPE_DISPLAY`) on desktop |
|
||||
| Subtitle `TERMINAL EDITION` | 12 px | unchanged |
|
||||
| Boot log lines | 70 % width column | cap at 480 px so the column doesn't stretch on a wide window |
|
||||
| Progress bar | 100 % − 2 × edge | cap at 720 px |
|
||||
| Palette swatch row + version footer | bottom-anchored | unchanged; bottom-anchor still reads correctly on desktop |
|
||||
|
||||
**Engine impact:** `splash_plugin` already has the cursor block
|
||||
(`cdcadda`). The boot log / progress bar / palette swatch rows
|
||||
are the next polish increment when option D is picked up.
|
||||
|
||||
### Stats
|
||||
|
||||
Mockup: `stats-mobile.html` (390 × 2624).
|
||||
|
||||
| Element | Mobile | Desktop |
|
||||
|---|---|---|
|
||||
| Modal width | 100 % − 2 × edge | `min(720 px, 50 % viewport)` |
|
||||
| Big-number cards | 2 × 2 grid | **4 × 1 row** for windows ≥ 1024 wide (the four headline metrics fit in a single horizontal row at desktop scale) |
|
||||
| Latest-win caption | full-width line | unchanged |
|
||||
| Replay clip / share row | full-width row | unchanged |
|
||||
|
||||
### Profile / Achievements / Theme Picker / Daily Challenge
|
||||
|
||||
These follow the **standard modal pattern** (`spawn_modal` with
|
||||
header / body / actions). They already work on desktop because
|
||||
`ui_modal` handles modal-width capping. Per-screen tweaks are
|
||||
small and listed below; no structural changes:
|
||||
|
||||
* **Profile** — avatar + level / streak chips can flow into a
|
||||
single horizontal row on desktop instead of stacking.
|
||||
* **Achievements** — 3 × N grid on mobile becomes 4 × N or 5 × N
|
||||
on desktop where windows ≥ 1280 wide.
|
||||
* **Theme Picker** — 2-col grid of theme cards on mobile becomes
|
||||
3- or 4-col on desktop.
|
||||
* **Daily Challenge** — single-column scroll on both; no change.
|
||||
|
||||
## Mockup parity gap
|
||||
|
||||
The 9 missing-plugin screens (`splash`, `challenge`, `time-attack`,
|
||||
`weekly-goals`, `leaderboard`, `sync`, `level-up`, `replay-overlay`,
|
||||
`radial-menu`) have only mobile mockups. When porting any of these
|
||||
plugins:
|
||||
|
||||
1. Read the mobile mockup for content + visual hierarchy.
|
||||
2. Apply the universal adaptation rules above.
|
||||
3. Apply the closest matching per-screen rule (e.g. an info modal
|
||||
uses the same shape as Win Summary or Stats).
|
||||
4. **No new layout pattern without explicit user approval.**
|
||||
Adapting an existing pattern is in scope; inventing a desktop-
|
||||
specific component is design work and should be flagged as such.
|
||||
|
||||
## Process notes
|
||||
|
||||
* **Smart-default sizer is the layout's source of truth.** Before
|
||||
reading the mockup, always re-read `Window::size()` —
|
||||
`apply_smart_default_window_size` runs at startup and the
|
||||
player can resize freely. Hardcoded breakpoints in plugin code
|
||||
should reference the *current* `Window` width via a
|
||||
`LayoutResource` lookup, not the launch size.
|
||||
* **`WindowResized` already drives layout recomputes** (CLAUDE.md
|
||||
§3.4). Any per-window-width adaptation in this file should hook
|
||||
into the existing recompute path, not a new system.
|
||||
* **Mobile rules win at narrow desktop windows.** A user dragging
|
||||
their desktop window down to 600 px width is closer to the
|
||||
mobile use-case than the desktop one. Below 1024 px width,
|
||||
apply the mobile rules verbatim.
|
||||
* **Run on a 4K monitor before declaring a port done.** HiDPI
|
||||
scaling routes through Bevy's logical sizing, but visual
|
||||
polish (border thickness, motion budgets at high refresh rate)
|
||||
is worth eyeballing.
|
||||
@@ -0,0 +1,253 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.outlined-glyph {
|
||||
-webkit-text-stroke: 1.5px currentColor;
|
||||
color: transparent;
|
||||
}
|
||||
.scanline-pattern {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
#1a1a1a,
|
||||
#1a1a1a 2px,
|
||||
#151515 2px,
|
||||
#151515 4px
|
||||
);
|
||||
}
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface": "#151515",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"warning": "#ddb26f",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"on-background": "#e0e3e6",
|
||||
"on-primary-container": "#004f6c",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"on-surface": "#e0e3e6",
|
||||
"error": "#fb9fb1",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"inverse-primary": "#00668a",
|
||||
"surface-container-high": "#272a2d",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"surface-bright": "#363a3d",
|
||||
"on-primary": "#003549",
|
||||
"on-tertiary": "#4c195b",
|
||||
"error-container": "#93000a",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"surface-container": "#202020",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"surface-container-highest": "#313538",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"highlight-valid": "#acc267",
|
||||
"primary": "#a1dcff",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"on-error-container": "#ffdad6",
|
||||
"secondary": "#bad073",
|
||||
"on-tertiary": "#293500",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"on-error": "#690005",
|
||||
"info": "#12cfc0",
|
||||
"suit-red": "#fb9fb1",
|
||||
"surface-dim": "#101417",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"background": "#101417",
|
||||
"secondary-container": "#435401",
|
||||
"surface-variant": "#313538",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"suit-black": "#d0d0d0"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"touch-target-min": "48px",
|
||||
"margin-edge": "1rem",
|
||||
"action-bar-height": "64px",
|
||||
"stack-overlap": "2rem",
|
||||
"gutter-card": "0.375rem"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface font-body-md overflow-hidden selection:bg-primary selection:text-surface">
|
||||
<!-- TopAppBar -->
|
||||
<header class="fixed top-0 w-full flex justify-between items-center px-margin-edge h-[56px] bg-surface-container border-b border-outline dark:border-outline z-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||
<h1 class="font-hud-score text-[18px] text-primary">solitaire.sh</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-[6px] h-[6px] rounded-full bg-info"></div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant">settings</span>
|
||||
</div>
|
||||
</header>
|
||||
<!-- HUD Band -->
|
||||
<div class="fixed top-[56px] left-0 w-full h-[56px] bg-surface-container border-b border-outline-variant flex items-center justify-around px-margin-edge z-40">
|
||||
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
|
||||
<span class="font-label-caps text-[10px] text-on-surface-variant">SCORE</span>
|
||||
<span class="font-hud-score text-primary tabular-nums">247</span>
|
||||
</div>
|
||||
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center border border-outline">
|
||||
<span class="font-label-caps text-[10px] text-on-surface-variant">TIME</span>
|
||||
<span class="font-hud-timer text-on-surface tabular-nums">12:34</span>
|
||||
</div>
|
||||
<div class="bg-surface p-1 px-3 rounded flex flex-col items-center">
|
||||
<span class="font-label-caps text-[10px] text-on-surface-variant">MOVES</span>
|
||||
<span class="font-hud-score text-secondary tabular-nums">87</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Game Table -->
|
||||
<main class="pt-[124px] px-margin-edge h-screen w-full relative">
|
||||
<!-- Top Row: Stock, Waste, Foundations -->
|
||||
<div class="grid grid-cols-7 gap-gutter-card h-[110px]">
|
||||
<!-- Stock -->
|
||||
<div class="relative w-full h-full rounded-xl border border-outline-variant bg-surface overflow-hidden scanline-pattern">
|
||||
<div class="absolute top-1 left-1 w-3 h-4 bg-suit-red-cb"></div>
|
||||
<div class="absolute bottom-1 right-1 font-label-caps text-[8px] text-suit-black">▌RS</div>
|
||||
<div class="absolute bottom-[-16px] left-0 w-full text-center font-label-caps text-[10px] text-on-surface-variant">STOCK · 18</div>
|
||||
</div>
|
||||
<!-- Waste -->
|
||||
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
|
||||
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal">♥</span></div>
|
||||
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180">♥</div>
|
||||
</div>
|
||||
<!-- Empty Gap -->
|
||||
<div></div>
|
||||
<!-- Foundation S -->
|
||||
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank">♠</span>
|
||||
</div>
|
||||
<!-- Foundation H -->
|
||||
<div class="relative w-full h-full rounded-xl border border-suit-red bg-[#1a1a1a] flex flex-col justify-between p-1.5">
|
||||
<div class="font-card-rank text-suit-red leading-none">2<br/><span class="font-normal">♥</span></div>
|
||||
<div class="self-end text-[32px] font-card-rank text-suit-red rotate-180">♥</div>
|
||||
</div>
|
||||
<!-- Foundation C -->
|
||||
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph">♣</span>
|
||||
</div>
|
||||
<!-- Foundation D -->
|
||||
<div class="relative w-full h-full rounded-xl border border-dashed border-outline-variant flex items-center justify-center">
|
||||
<span class="text-on-surface-variant opacity-20 text-[32px] font-card-rank outlined-glyph">♦</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tableau -->
|
||||
<div class="mt-8 grid grid-cols-7 gap-gutter-card items-start relative h-[400px]">
|
||||
<!-- Col 1 -->
|
||||
<div class="relative w-full h-full">
|
||||
<div class="w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5">
|
||||
<div class="font-card-rank text-suit-black leading-none">K<br/><span class="font-normal">♠</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Col 2 -->
|
||||
<div class="relative w-full h-full">
|
||||
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
|
||||
<div class="font-card-rank text-suit-red leading-none">Q<br/><span class="font-normal">♥</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Col 3 -->
|
||||
<div class="relative w-full h-full">
|
||||
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||
<div class="absolute top-[32px] w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5">
|
||||
<div class="font-card-rank text-suit-red leading-none">10<br/><span class="font-normal outlined-glyph">♦</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Col 4 -->
|
||||
<div class="relative w-full h-full">
|
||||
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern"></div>
|
||||
<div class="w-full h-[96px] rounded-xl border border-outline bg-[#1a1a1a] scanline-pattern absolute top-[32px]"></div>
|
||||
<!-- Valid Drop Target Glow -->
|
||||
<div class="absolute top-[64px] w-full h-[96px] rounded-xl border border-suit-black bg-[#1a1a1a] p-1.5 ring-4 ring-highlight-valid/30">
|
||||
<div class="font-card-rank text-suit-black leading-none">9<br/><span class="font-normal">♠</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Col 5, 6 (Empty/Filler) -->
|
||||
<div class="relative w-full"></div>
|
||||
<div class="relative w-full"></div>
|
||||
<!-- Col 7 -->
|
||||
<div class="relative w-full">
|
||||
<!-- Original Position Placeholder -->
|
||||
<div class="w-full h-[96px] rounded-xl border border-dashed border-outline"></div>
|
||||
<!-- Being Dragged Card -->
|
||||
<div class="absolute top-[-20px] left-[30px] w-full h-[96px] rounded-xl border border-suit-red bg-[#1a1a1a] p-1.5 shadow-[0_0_20px_rgba(111,194,239,0.4)] z-50 ring-1 ring-primary/40">
|
||||
<div class="font-card-rank text-suit-red leading-none">4<br/><span class="font-normal outlined-glyph">♦</span></div>
|
||||
<div class="absolute bottom-1 right-1 text-[24px] font-card-rank text-suit-red rotate-180 outlined-glyph">♦</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- BottomNavBar / Action Bar -->
|
||||
<nav class="fixed bottom-0 left-0 w-full h-action-bar-height bg-surface-container border-t border-outline-variant flex justify-around items-center px-margin-edge z-50">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||
<span class="material-symbols-outlined" data-icon="menu">menu</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[ESC] MENU</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-info font-bold active:opacity-80">
|
||||
<span class="material-symbols-outlined" data-icon="undo">undo</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[U] UNDO</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||
<span class="material-symbols-outlined" data-icon="lightbulb">lightbulb</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[H] HINT</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-info transition-colors duration-120 active:opacity-80">
|
||||
<span class="material-symbols-outlined" data-icon="add_box">add_box</span>
|
||||
<span class="font-label-caps text-[10px] mt-1">[N] NEW</span>
|
||||
</button>
|
||||
</nav>
|
||||
<!-- Drag & CRT Overlay (Visual Decoration) -->
|
||||
<div class="pointer-events-none fixed inset-0 z-[100] opacity-[0.03] scanline-pattern mix-blend-overlay"></div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"surface-container-low": "#181c1f",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"secondary": "#bad073",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"secondary-container": "#435401",
|
||||
"background": "#101417",
|
||||
"surface-variant": "#313538",
|
||||
"on-primary-container": "#004f6c",
|
||||
"highlight-valid": "#acc267",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-background": "#e0e3e6",
|
||||
"surface-bright": "#363a3d",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"surface-dim": "#101417",
|
||||
"on-surface": "#e0e3e6",
|
||||
"info": "#12cfc0",
|
||||
"on-secondary": "#293500",
|
||||
"suit-red": "#fb9fb1",
|
||||
"error": "#fb9fb1",
|
||||
"error-container": "#93000a",
|
||||
"surface-container": "#202020",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"warning": "#ddb26f",
|
||||
"tertiary": "#f7c3ff",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"primary-container": "#6fc2ef",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"on-tertiary": "#4c195b",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"surface-container-highest": "#313538",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"surface-container-high": "#272a2d",
|
||||
"primary": "#a1dcff",
|
||||
"suit-black": "#d0d0d0",
|
||||
"on-tertiary-container": "#683476",
|
||||
"on-error": "#690005",
|
||||
"inverse-primary": "#00668a",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"outline": "#505050",
|
||||
"on-primary": "#003549",
|
||||
"surface": "#151515"
|
||||
},
|
||||
"fontFamily": {
|
||||
"jetbrains": ["JetBrains Mono", "monospace"],
|
||||
"inter": ["Inter", "sans-serif"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
font-size: 16px;
|
||||
}
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
body { background-color: #151515; }
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex items-center justify-center min-h-screen p-4">
|
||||
<!-- Mobile Container (390x844) -->
|
||||
<div class="w-[390px] h-[844px] bg-surface flex flex-col overflow-hidden relative border border-outline/20">
|
||||
<!-- 1. Status Bar -->
|
||||
<header class="h-[32px] bg-surface-container flex items-center justify-between px-4 shrink-0">
|
||||
<span class="font-jetbrains text-[12px] font-bold text-suit-black tracking-tight">▌rusty-solitaire(1) · MAN PAGE</span>
|
||||
<button class="font-jetbrains text-[12px] font-bold text-suit-black/60 hover:text-primary transition-colors">× CLOSE</button>
|
||||
</header>
|
||||
<!-- 2. Heading Band -->
|
||||
<div class="h-[120px] px-4 pt-10 pb-4 shrink-0">
|
||||
<h1 class="font-jetbrains font-bold text-[24px] text-suit-black leading-none mb-1">GESTURES & SHORTCUTS</h1>
|
||||
<p class="font-inter text-[13px] text-on-surface-variant/80">Touch gestures and keyboard equivalents.</p>
|
||||
</div>
|
||||
<!-- Scrollable Content Section -->
|
||||
<main class="flex-1 overflow-y-auto px-4 pb-8 space-y-6">
|
||||
<!-- 3a. TOUCH GESTURES -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">TOUCH GESTURES</h2>
|
||||
<div class="space-y-1">
|
||||
<!-- Row 1 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-suit-black" data-icon="square">square</span>
|
||||
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">TAP card</span>
|
||||
</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Select / unselect for move</div>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-suit-black" data-icon="east">east</span>
|
||||
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DRAG stack</span>
|
||||
</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Move with translucent ghost preview</div>
|
||||
</div>
|
||||
<!-- Row 3 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-suit-black" data-icon="double_arrow">double_arrow</span>
|
||||
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">DOUBLE-TAP</span>
|
||||
</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-send to best foundation</div>
|
||||
</div>
|
||||
<!-- Row 4 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-suit-black" data-icon="touch_app">touch_app</span>
|
||||
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">LONG-PRESS</span>
|
||||
</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Highlight all legal moves for card</div>
|
||||
</div>
|
||||
<!-- Row 5 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-suit-black" data-icon="south">south</span>
|
||||
<span class="font-jetbrains text-[13px] font-medium text-suit-black uppercase">SWIPE DOWN</span>
|
||||
</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Reveal hidden action bar</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- 3b. KEYBOARD SHORTCUTS -->
|
||||
<section class="space-y-3">
|
||||
<h2 class="font-jetbrains text-[11px] font-medium tracking-widest text-on-surface-variant/60 uppercase">KEYBOARD SHORTCUTS</h2>
|
||||
<div class="space-y-1">
|
||||
<!-- Row 1 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[U]</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Undo last move</div>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[H]</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Show hint</div>
|
||||
</div>
|
||||
<!-- Row 3 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[N]</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">New game</div>
|
||||
</div>
|
||||
<!-- Row 4 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[A]</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Auto-complete (when possible)</div>
|
||||
</div>
|
||||
<!-- Row 5 -->
|
||||
<div class="h-[56px] bg-surface-container rounded-lg px-3 flex items-center border border-outline/10">
|
||||
<div class="w-[40%] font-jetbrains text-[13px] font-medium text-suit-black uppercase">[ESC]</div>
|
||||
<div class="w-[60%] font-jetbrains text-[12px] text-on-surface-variant leading-tight">Pause / back</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- 4. Footer -->
|
||||
<footer class="h-[24px] bg-surface-container border-t border-outline/20 flex items-center justify-between px-2 shrink-0">
|
||||
<div class="font-jetbrains text-[10px] text-suit-black">
|
||||
<span class="opacity-80">▌ NORMAL │ help</span>
|
||||
</div>
|
||||
<div class="font-jetbrains text-[10px] uppercase tracking-wider flex items-center gap-1">
|
||||
<span class="text-outline">PRESS</span>
|
||||
<span class="text-on-surface-variant">[ESC]</span>
|
||||
<span class="text-outline">OR TAP</span>
|
||||
<span class="text-on-surface-variant">×</span>
|
||||
<span class="text-outline">TO RETURN</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,343 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>RS_TERMINAL_OS - Rusty Solitaire</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
font-size: 18px;
|
||||
}
|
||||
body {
|
||||
background-color: #151515;
|
||||
color: #d0d0d0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
.scanline {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: rgba(26, 26, 26, 0.5);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #151515;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #353535;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-tertiary-container": "#683476",
|
||||
"surface-dim": "#101417",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"on-error": "#690005",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"on-tertiary": "#4c195b",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"outline-variant": "#3f484e",
|
||||
"tertiary": "#f7c3ff",
|
||||
"surface": "#151515",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"background": "#101417",
|
||||
"surface-container": "#202020",
|
||||
"primary-container": "#6fc2ef",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"on-surface": "#d0d0d0",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-container-low": "#181c1f",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"on-background": "#e0e3e6",
|
||||
"secondary-container": "#435401",
|
||||
"error": "#fb9fb1",
|
||||
"info": "#12cfc0",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"warning": "#ddb26f",
|
||||
"inverse-primary": "#00668a",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"suit-black": "#d0d0d0",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"on-secondary": "#293500",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"surface-container-highest": "#313538",
|
||||
"error-container": "#93000a",
|
||||
"surface-container-high": "#272a2d",
|
||||
"on-primary-container": "#004f6c",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"on-primary": "#003549",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"highlight-valid": "#acc267",
|
||||
"surface-variant": "#313538",
|
||||
"secondary": "#bad073",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"outline": "#505050",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"primary": "#a1dcff",
|
||||
"surface-bright": "#363a3d",
|
||||
"suit-red": "#fb9fb1"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0px",
|
||||
"lg": "0px",
|
||||
"xl": "0px",
|
||||
"full": "0px"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-overlap": "2rem",
|
||||
"touch-target-min": "48px",
|
||||
"margin-edge": "1rem",
|
||||
"gutter-card": "0.375rem",
|
||||
"action-bar-height": "64px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"label-caps": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface h-screen flex flex-col antialiased">
|
||||
<!-- TOP BAR (32px) -->
|
||||
<header class="h-8 bg-surface-container border-b border-outline flex items-center justify-between px-4 z-50">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-primary-container font-bold">▌</span>
|
||||
<h1 class="font-headline text-[14px] font-bold tracking-tight text-on-surface">RS_TERMINAL_OS</h1>
|
||||
</div>
|
||||
<nav class="flex gap-4 font-label-caps text-[12px] uppercase tracking-widest">
|
||||
<span class="text-primary-container">[ HOME ]</span>
|
||||
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· PLAY</span>
|
||||
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· STATS</span>
|
||||
<span class="text-on-surface-variant hover:text-primary transition-colors cursor-pointer">· SETTINGS</span>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3 font-label-caps text-[11px] text-on-surface-variant">
|
||||
<div class="flex items-center gap-1">
|
||||
<span>LV 12</span>
|
||||
<span class="text-outline">|</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>XP 320/500</span>
|
||||
<div class="w-[60px] h-1 bg-surface-container-highest">
|
||||
<div class="h-full bg-primary-container w-[64%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-outline">|</span>
|
||||
<div class="flex items-center gap-1 text-info">
|
||||
<span class="w-2 h-2 rounded-full bg-info"></span>
|
||||
<span class="uppercase">Synced</span>
|
||||
</div>
|
||||
<span class="text-outline">|</span>
|
||||
<span class="text-outline">v0.20.0</span>
|
||||
</div>
|
||||
</header>
|
||||
<!-- MAIN CONTENT AREA -->
|
||||
<main class="flex-1 flex overflow-hidden">
|
||||
<!-- LEFT PANE (40%) -->
|
||||
<section class="w-[40%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||
<div class="space-y-1">
|
||||
<p class="text-outline font-label-caps text-xs">▌play.tsx</p>
|
||||
<h2 class="font-headline text-[32px] font-bold text-on-surface leading-none uppercase">Ready to play?</h2>
|
||||
<p class="text-on-surface-variant font-label-caps text-sm tracking-wide">RESUME · 12:34 ELAPSED · DRAW-3</p>
|
||||
</div>
|
||||
<button class="w-full h-24 bg-primary-container text-surface font-headline text-[24px] font-bold flex items-center justify-center gap-4 hover:brightness-110 active:scale-[0.98] transition-all">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
|
||||
CONTINUE GAME
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
NEW GAME
|
||||
</button>
|
||||
<button class="h-12 border border-outline bg-transparent text-on-surface font-label-caps text-sm hover:border-primary-container hover:text-primary-container transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
RESTART RUN
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Game Modes</p>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<!-- Zen -->
|
||||
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">spa</span>
|
||||
<span class="font-label-caps text-[10px] uppercase">Zen</span>
|
||||
</div>
|
||||
<!-- Time Attack -->
|
||||
<div class="aspect-square border border-outline flex flex-col items-center justify-center gap-2 hover:bg-surface-container transition-colors cursor-pointer group">
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary-container">timer</span>
|
||||
<span class="font-label-caps text-[10px] uppercase text-center">Time<br/>Attack</span>
|
||||
</div>
|
||||
<!-- Locked Challenge -->
|
||||
<div class="aspect-square bg-[#0d0d0d] border border-outline/30 flex flex-col items-center justify-center gap-2 relative opacity-60">
|
||||
<span class="material-symbols-outlined text-outline">lock</span>
|
||||
<span class="font-label-caps text-[10px] uppercase">Challenge</span>
|
||||
<div class="absolute -top-2 -right-2 bg-warning text-surface px-1 py-0.5 text-[8px] font-bold">LV 5</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- VISUAL DECORATION (IMAGE PLACEHOLDER) -->
|
||||
<div class="mt-auto pt-8">
|
||||
<div class="w-full h-40 border border-outline overflow-hidden">
|
||||
<img class="w-full h-full object-cover opacity-40 grayscale hover:grayscale-0 transition-all duration-700" data-alt="A dark, high-contrast digital art piece showing an abstract terminal interface with glowing cyan scanlines and retro-futuristic grid patterns. The composition is geometric and minimalist, following a synthwave aesthetic with deep black backgrounds and crisp cyan light elements. The lighting is moody and artificial, suggesting a high-performance computer screen in a dimly lit server room. Professional, sharp-edged UI design style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAet8SrRWSacZfwd8ISRQdDC7CDGixBwRnPAVMmMcjbifq1jnHSzCGWgSSL6YPSRfCkLNWr91BxTzV4zigGjMBLlk7rCLo5I7X7F6ydinDrKJVqZkRbvHJeSo90BPANoQwZtzPvhKXVEA9C2DbBaj8KPR4ObCo24Mj25NXPvGNThOE-3BSpuU6MPC-hrUMPVCPJpZnJdI_OmSz8mT021vjTxFERN12S1PFOzXKmNUDleoTDIat-8UifyKmKg4eKilecrBW6sFqaBw"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- CENTER PANE (30%) -->
|
||||
<section class="w-[30%] border-r border-outline flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||
<div class="space-y-1">
|
||||
<p class="text-outline font-label-caps text-xs">▌daily.json</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-headline text-[18px] font-bold text-on-surface">MAY 07 · 2026</h3>
|
||||
<span class="bg-warning/20 text-warning px-2 py-1 text-[10px] font-bold border border-warning/40">EXPIRES 11:42:30</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container p-6 border border-outline space-y-4">
|
||||
<div class="space-y-1">
|
||||
<p class="text-on-surface-variant font-label-caps text-[10px] uppercase tracking-tighter">Current Seed</p>
|
||||
<p class="font-headline text-[24px] font-extrabold text-highlight-valid">#2024-127</p>
|
||||
</div>
|
||||
<button class="w-full py-3 bg-primary-container text-surface font-label-caps text-xs font-bold uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all">
|
||||
▶ Attempt Today
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Global Standings</p>
|
||||
<div class="space-y-1 text-xs font-label-caps">
|
||||
<div class="flex justify-between py-2 border-b border-outline/30 text-highlight-valid">
|
||||
<span>01 │ swift_jaguar</span>
|
||||
<span>02:47</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||
<span>02 │ pixel_drifter</span>
|
||||
<span>03:12</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||
<span>03 │ null_ptr</span>
|
||||
<span>03:15</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 border-b border-outline/30 text-on-surface-variant">
|
||||
<span>04 │ core_dump_88</span>
|
||||
<span>03:44</span>
|
||||
</div>
|
||||
<div class="flex justify-between py-2 text-primary-container bg-primary-container/10 px-2 -mx-2">
|
||||
<span>12 │ YOU (anon)</span>
|
||||
<span>--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- RIGHT PANE (30%) -->
|
||||
<section class="w-[30%] flex flex-col p-8 gap-8 overflow-y-auto custom-scrollbar">
|
||||
<div class="space-y-1">
|
||||
<p class="text-outline font-label-caps text-xs">▌stats.log</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="border border-outline p-4 space-y-1">
|
||||
<p class="text-on-surface-variant text-[10px] uppercase">Games</p>
|
||||
<p class="font-hud-score text-[28px] text-on-surface">247</p>
|
||||
</div>
|
||||
<div class="border border-outline p-4 space-y-1 text-highlight-valid">
|
||||
<p class="text-on-surface-variant text-[10px] uppercase">Win Rate</p>
|
||||
<p class="font-hud-score text-[28px]">61%</p>
|
||||
</div>
|
||||
<div class="border border-outline p-4 space-y-1">
|
||||
<p class="text-on-surface-variant text-[10px] uppercase">Best Time</p>
|
||||
<p class="font-hud-score text-[28px]">01:54</p>
|
||||
</div>
|
||||
<div class="border border-outline p-4 space-y-1 text-primary-container">
|
||||
<p class="text-on-surface-variant text-[10px] uppercase">Streak</p>
|
||||
<p class="font-hud-score text-[28px]">7</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<p class="text-outline font-label-caps text-xs uppercase tracking-widest">Achievements (8/19)</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Filled Cyan Dots -->
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<div class="w-3 h-3 bg-primary-container"></div>
|
||||
<!-- Empty Dots -->
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
<div class="w-3 h-3 border border-outline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto border border-outline bg-surface-container p-4 flex items-center justify-between hover:border-primary-container transition-colors cursor-pointer group">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary-container text-surface flex items-center justify-center font-bold text-lg">RS</div>
|
||||
<div class="space-y-0.5">
|
||||
<p class="text-on-surface font-bold text-xs">anonymous@local</p>
|
||||
<p class="text-on-surface-variant text-[10px]">Session: Active</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-primary-container group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- BOTTOM BAR (24px) -->
|
||||
<footer class="h-6 bg-surface-container border-t border-outline flex items-center justify-between px-4 text-[10px] font-label-caps">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-primary-container">▌ NORMAL</span>
|
||||
<span class="text-outline">│</span>
|
||||
<span class="text-on-surface-variant">~/rusty-solitaire/home</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-on-surface-variant">
|
||||
<div class="flex items-center gap-1"><span class="text-primary-container">[SPACE]</span> play</div>
|
||||
<div class="flex items-center gap-1"><span class="text-primary-container">[D]</span> daily</div>
|
||||
<div class="flex items-center gap-1"><span class="text-primary-container">[S]</span> settings</div>
|
||||
<div class="flex items-center gap-1"><span class="text-primary-container">[?]</span> help</div>
|
||||
</div>
|
||||
<div class="text-outline">
|
||||
2026-05-07 17:42 EDT
|
||||
</div>
|
||||
</footer>
|
||||
<!-- GLOBAL SCANLINE EFFECT -->
|
||||
<div class="fixed inset-0 pointer-events-none z-[100] overflow-hidden opacity-10">
|
||||
<div class="absolute inset-0" style="background: repeating-linear-gradient(0deg, #151515, #151515 2px, #202020 4px);"></div>
|
||||
</div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Rusty Solitaire - Main Menu</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"outline": "#505050",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"suit-black": "#d0d0d0",
|
||||
"surface-container-high": "#272a2d",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"on-tertiary-container": "#683476",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"background": "#101417",
|
||||
"primary-container": "#6fc2ef",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"surface-container-low": "#181c1f",
|
||||
"on-surface": "#d0d0d0",
|
||||
"primary": "#a1dcff",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"secondary-container": "#435401",
|
||||
"inverse-primary": "#00668a",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"surface-bright": "#363a3d",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"warning": "#ddb26f",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"suit-red": "#fb9fb1",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"info": "#12cfc0",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"error": "#fb9fb1",
|
||||
"surface-variant": "#313538",
|
||||
"on-error": "#690005",
|
||||
"surface": "#151515",
|
||||
"surface-container": "#202020",
|
||||
"on-primary-container": "#004f6c",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"on-primary-fixed-variant": "#004c69",
|
||||
"on-secondary": "#293500",
|
||||
"error-container": "#93000a",
|
||||
"secondary": "#bad073",
|
||||
"tertiary": "#f7c3ff",
|
||||
"outline-variant": "#3f484e",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"surface-container-highest": "#313538",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-primary": "#003549",
|
||||
"on-background": "#e0e3e6",
|
||||
"surface-dim": "#101417",
|
||||
"on-tertiary": "#4c195b",
|
||||
"highlight-valid": "#acc267"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"margin-edge": "1rem",
|
||||
"touch-target-min": "48px",
|
||||
"stack-overlap": "2rem",
|
||||
"action-bar-height": "64px",
|
||||
"gutter-card": "0.375rem"
|
||||
},
|
||||
"fontFamily": {
|
||||
"hud-timer": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"card-rank": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"hud-timer": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
|
||||
"headline": ["28px", { "lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700" }],
|
||||
"label-caps": ["12px", { "lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500" }],
|
||||
"hud-score": ["24px", { "lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700" }],
|
||||
"body-md": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
|
||||
"card-rank": ["18px", { "lineHeight": "18px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.scanline {
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1));
|
||||
background-size: 100% 4px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface font-hud-timer min-h-screen flex flex-col relative overflow-hidden">
|
||||
<!-- Subtle CRT scanline overlay -->
|
||||
<div class="absolute inset-0 pointer-events-none scanline opacity-20 z-0"></div>
|
||||
<!-- Status Bar Zone -->
|
||||
<div class="h-6 w-full flex justify-end items-center px-margin-edge pt-2 z-10 relative">
|
||||
<div class="w-2 h-2 rounded-full bg-info"></div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<header class="px-margin-edge pt-4 pb-6 flex justify-between items-center z-10 relative">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="font-headline text-headline text-on-surface">▌RUSTY SOLITAIRE</span>
|
||||
<div class="w-2 h-6 bg-primary-container inline-block ml-1 animate-pulse"></div>
|
||||
</div>
|
||||
<div class="bg-surface-container px-3 py-1 flex items-center gap-2 border border-outline">
|
||||
<span class="font-label-caps text-label-caps text-on-surface">LV 12</span>
|
||||
<div class="w-2 h-2 rounded-full bg-highlight-celebration"></div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="flex-1 px-margin-edge flex flex-col gap-8 z-10 relative pb-24 overflow-y-auto">
|
||||
<!-- XP Section -->
|
||||
<section class="flex flex-col gap-2">
|
||||
<div class="w-full h-1 bg-surface-container border border-outline relative">
|
||||
<div class="absolute top-0 left-0 h-full bg-primary-container w-[64%]"></div>
|
||||
</div>
|
||||
<div class="font-label-caps text-label-caps text-on-surface-variant text-right">
|
||||
320 / 500 XP
|
||||
</div>
|
||||
</section>
|
||||
<!-- Primary Action -->
|
||||
<section class="flex flex-col gap-2">
|
||||
<button class="w-full h-[56px] bg-primary-container text-surface flex items-center justify-center gap-2 hover:bg-surface-tint transition-colors duration-120">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
|
||||
<span class="font-label-caps text-[14px] uppercase tracking-widest font-bold">PLAY</span>
|
||||
</button>
|
||||
<div class="font-label-caps text-label-caps text-on-surface-variant text-center">
|
||||
RESUME LAST GAME · 12:34 ELAPSED
|
||||
</div>
|
||||
</section>
|
||||
<!-- Daily Challenge Tile -->
|
||||
<section>
|
||||
<div class="bg-surface-container border border-outline p-4 flex justify-between items-center hover:bg-surface-container-high transition-colors cursor-pointer group">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-label-caps text-label-caps text-primary">DAILY CHALLENGE</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">DRAW-3 · SEED #2024-127</span>
|
||||
<div class="inline-flex">
|
||||
<span class="bg-surface px-2 py-0.5 border border-warning text-warning font-label-caps text-[10px]">EXPIRES 11:42:30</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-primary group-hover:translate-x-1 transition-transform">chevron_right</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Special Modes Grid -->
|
||||
<section class="flex flex-col gap-4">
|
||||
<h2 class="font-label-caps text-label-caps text-on-surface-variant">SPECIAL MODES</h2>
|
||||
<div class="grid grid-cols-3 gap-gutter-card">
|
||||
<!-- ZEN -->
|
||||
<button class="aspect-square bg-surface border border-outline flex flex-col items-center justify-center gap-2 hover:border-primary hover:text-primary transition-colors text-on-surface">
|
||||
<span class="material-symbols-outlined text-[32px]">self_improvement</span>
|
||||
<span class="font-label-caps text-label-caps">ZEN</span>
|
||||
</button>
|
||||
<!-- TIME ATTACK -->
|
||||
<button class="aspect-square bg-surface border border-outline flex flex-col items-center justify-center gap-2 hover:border-primary hover:text-primary transition-colors text-on-surface">
|
||||
<span class="material-symbols-outlined text-[32px]">timer</span>
|
||||
<span class="font-label-caps text-label-caps">TIME ATTACK</span>
|
||||
</button>
|
||||
<!-- CHALLENGE (Locked) -->
|
||||
<button class="aspect-square bg-[#0d0d0d] border border-surface-container-high flex flex-col items-center justify-center gap-2 text-on-surface-variant opacity-75 cursor-not-allowed relative">
|
||||
<span class="material-symbols-outlined text-[32px]">lock</span>
|
||||
<span class="font-label-caps text-label-caps">CHALLENGE</span>
|
||||
<div class="absolute top-2 right-2 bg-surface px-1 py-0.5 border border-warning text-warning font-label-caps text-[10px]">
|
||||
LV 5
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Secondary Nav Grid -->
|
||||
<section class="grid grid-cols-2 gap-y-4 gap-x-6 pb-6">
|
||||
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
|
||||
<span class="material-symbols-outlined">bar_chart</span>
|
||||
<span class="font-label-caps text-label-caps">STATS</span>
|
||||
</button>
|
||||
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start relative">
|
||||
<span class="material-symbols-outlined">emoji_events</span>
|
||||
<span class="font-label-caps text-label-caps">ACHIEVEMENTS</span>
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-highlight-celebration"></div>
|
||||
</button>
|
||||
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
|
||||
<span class="material-symbols-outlined">format_list_numbered</span>
|
||||
<span class="font-label-caps text-label-caps">LEADERBOARD</span>
|
||||
</button>
|
||||
<button class="flex items-center gap-3 h-[56px] border-l-2 border-outline pl-3 hover:border-primary hover:text-primary transition-colors text-on-surface justify-start">
|
||||
<span class="material-symbols-outlined">account_circle</span>
|
||||
<span class="font-label-caps text-label-caps">PROFILE</span>
|
||||
</button>
|
||||
</section>
|
||||
<!-- Footer Links -->
|
||||
<footer class="flex flex-col items-center gap-4 mt-auto">
|
||||
<div class="flex items-center gap-4 font-label-caps text-label-caps text-primary cursor-pointer hover:text-surface-tint">
|
||||
<span>SETTINGS</span>
|
||||
<span class="text-on-surface-variant">·</span>
|
||||
<span>HELP</span>
|
||||
</div>
|
||||
<div class="font-label-caps text-[10px] text-on-surface-variant text-center opacity-60">
|
||||
v0.20.0 — TERMINAL THEME · BUILD 2026.05
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,315 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Rusty Solitaire - Leaderboard</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&family=Inter:wght@400;500&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
background-color: #151515;
|
||||
color: #e0e3e6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.scanline-overlay {
|
||||
background: linear-gradient(to bottom, rgba(21, 21, 21, 0) 50%, rgba(26, 26, 26, 0.2) 50%);
|
||||
background-size: 100% 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.terminal-glow {
|
||||
box-shadow: 0 0 10px rgba(111, 194, 239, 0.1);
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"outline": "#505050",
|
||||
"on-surface-variant": "#bfc8cf",
|
||||
"secondary-container": "#435401",
|
||||
"surface-container-lowest": "#0b0f11",
|
||||
"primary": "#a1dcff",
|
||||
"secondary-fixed": "#d5ec8c",
|
||||
"on-secondary-fixed": "#161e00",
|
||||
"on-error": "#690005",
|
||||
"inverse-primary": "#00668a",
|
||||
"surface-container": "#202020",
|
||||
"highlight-valid": "#acc267",
|
||||
"suit-black": "#d0d0d0",
|
||||
"on-secondary-fixed-variant": "#3c4d00",
|
||||
"on-primary-fixed": "#001e2c",
|
||||
"on-tertiary-fixed-variant": "#653173",
|
||||
"primary-fixed": "#c4e7ff",
|
||||
"inverse-on-surface": "#2d3134",
|
||||
"secondary-fixed-dim": "#bad073",
|
||||
"on-secondary": "#293500",
|
||||
"on-surface": "#e0e3e6",
|
||||
"on-tertiary-container": "#683476",
|
||||
"secondary": "#bad073",
|
||||
"surface-bright": "#363a3d",
|
||||
"tertiary-container": "#e1a3ee",
|
||||
"surface-variant": "#313538",
|
||||
"suit-red": "#fb9fb1",
|
||||
"primary-fixed-dim": "#7ed0fe",
|
||||
"surface-container-low": "#181c1f",
|
||||
"surface": "#151515",
|
||||
"suit-red-cb": "#6fc2ef",
|
||||
"on-primary": "#003549",
|
||||
"primary-container": "#6fc2ef",
|
||||
"on-background": "#e0e3e6",
|
||||
"tertiary": "#f7c3ff",
|
||||
"surface-container-highest": "#313538",
|
||||
"tertiary-fixed-dim": "#f0b0fc",
|
||||
"tertiary-fixed": "#fbd7ff",
|
||||
"info": "#12cfc0",
|
||||
"error": "#fb9fb1",
|
||||
"warning": "#ddb26f",
|
||||
"on-primary-container": "#004f6c",
|
||||
"surface-container-high": "#272a2d",
|
||||
"inverse-surface": "#e0e3e6",
|
||||
"error-container": "#93000a",
|
||||
"on-tertiary-fixed": "#340043",
|
||||
"surface-tint": "#7ed0fe",
|
||||
"on-tertiary": "#4c195b",
|
||||
"background": "#101417",
|
||||
"on-error-container": "#ffdad6",
|
||||
"on-secondary-container": "#b2c86d",
|
||||
"outline-variant": "#3f484e",
|
||||
"highlight-celebration": "#e1a3ee",
|
||||
"surface-dim": "#101417",
|
||||
"on-primary-fixed-variant": "#004c69"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"touch-target-min": "48px",
|
||||
"action-bar-height": "64px",
|
||||
"stack-overlap": "2rem",
|
||||
"gutter-card": "0.375rem",
|
||||
"margin-edge": "1rem"
|
||||
},
|
||||
"fontFamily": {
|
||||
"card-rank": ["JetBrains Mono"],
|
||||
"body-md": ["Inter"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"label-caps": ["JetBrains Mono"],
|
||||
"hud-timer": ["JetBrains Mono"],
|
||||
"hud-score": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="font-body-md overflow-hidden h-[844px] w-[390px] mx-auto relative border-x border-outline/20">
|
||||
<div class="scanline-overlay absolute inset-0 z-0"></div>
|
||||
<!-- Top AppBar (Identity Anchor) -->
|
||||
<header class="fixed top-0 w-full h-action-bar-height z-50 flex items-center px-margin-edge justify-between bg-surface dark:bg-surface text-primary dark:text-primary border-b border-outline dark:border-outline">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||
<h1 class="font-headline text-headline text-primary dark:text-primary uppercase tracking-tighter">Rusty Solitaire</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-on-surface-variant hover:bg-surface-variant transition-colors duration-120 p-2 rounded-lg cursor-pointer">sync</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-[64px] h-[calc(100%-64px)] flex flex-col z-10 relative">
|
||||
<!-- Pseudo Status Bar -->
|
||||
<div class="h-[32px] bg-surface-container flex items-center justify-between px-4 font-label-caps text-[10px] tracking-tight">
|
||||
<div class="text-[#a0a0a0]">▌leaderboard.tsx</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-info"></span>
|
||||
<span class="text-on-surface-variant">SYNCED</span>
|
||||
</span>
|
||||
<span class="text-outline">v0.20.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab Strip -->
|
||||
<nav class="h-[40px] bg-[#1a1a1a] border-b border-[#353535] flex items-center">
|
||||
<div class="flex-1 flex flex-col items-center justify-center relative">
|
||||
<span class="font-label-caps text-[11px] text-[#6fc2ef]">[ TODAY ]</span>
|
||||
<div class="absolute bottom-0 w-full h-[2px] bg-[#6fc2ef]"></div>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<span class="font-label-caps text-[11px] text-[#a0a0a0]">WEEK</span>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<span class="font-label-caps text-[11px] text-[#a0a0a0]">ALL-TIME</span>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<span class="font-label-caps text-[11px] text-[#a0a0a0]">FRIENDS</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex-1 overflow-y-auto px-margin-edge pt-4 space-y-4 pb-[88px]">
|
||||
<!-- Hero Podium Card -->
|
||||
<section class="h-[120px] bg-surface-container border border-[#353535] rounded-lg p-2 flex flex-col justify-between">
|
||||
<div class="font-label-caps text-[10px] text-[#a0a0a0]">TOP 3 · TODAY</div>
|
||||
<div class="flex gap-2 items-end justify-between flex-1 mt-1">
|
||||
<!-- 2nd -->
|
||||
<div class="flex-1 border border-[#a0a0a0] h-full rounded flex flex-col items-center justify-center relative py-1">
|
||||
<span class="font-card-rank text-[16px] text-[#a0a0a0]">02</span>
|
||||
<span class="text-[9px] font-mono text-[#d0d0d0] truncate w-full text-center px-1">base16_fan</span>
|
||||
<span class="text-[10px] font-mono text-[#a0a0a0]">03:12</span>
|
||||
</div>
|
||||
<!-- 1st -->
|
||||
<div class="flex-[1.2] border border-warning h-[110%] mb-[-2px] rounded-lg bg-surface flex flex-col items-center justify-center relative py-1 terminal-glow">
|
||||
<span class="absolute top-1 right-1 text-warning material-symbols-outlined text-[14px]">star</span>
|
||||
<span class="font-card-rank text-[24px] text-warning leading-none">01</span>
|
||||
<span class="text-[11px] font-mono text-[#d0d0d0] font-bold truncate w-full text-center px-1">swift_jaguar</span>
|
||||
<span class="text-[12px] font-mono text-[#d0d0d0]">02:47</span>
|
||||
</div>
|
||||
<!-- 3rd -->
|
||||
<div class="flex-1 border border-[#7a5d3b] h-full rounded flex flex-col items-center justify-center relative py-1">
|
||||
<span class="font-card-rank text-[16px] text-[#7a5d3b]">03</span>
|
||||
<span class="text-[9px] font-mono text-[#d0d0d0] truncate w-full text-center px-1">cli_player</span>
|
||||
<span class="text-[10px] font-mono text-[#a0a0a0]">03:54</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Search/Filter Row -->
|
||||
<div class="flex items-center gap-2 h-[40px]">
|
||||
<div class="px-3 h-8 border border-outline rounded flex items-center justify-center bg-surface-container-low">
|
||||
<span class="font-label-caps text-[10px] text-[#6fc2ef]">[ ALL TIMES ]</span>
|
||||
</div>
|
||||
<div class="flex-1 h-8 border border-outline rounded flex items-center px-2 bg-surface gap-2">
|
||||
<span class="font-mono text-[12px] text-outline">/ search players</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Leaderboard List -->
|
||||
<div class="space-y-0.5 font-mono text-[12px]">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between px-2 pb-1 border-b border-outline/20 text-outline text-[10px] uppercase font-bold tracking-widest">
|
||||
<span>Rank & User</span>
|
||||
<span>Time</span>
|
||||
</div>
|
||||
<!-- Rank 04 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">004</span>
|
||||
<span class="text-on-surface">tablejockey</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">04:01</span>
|
||||
</div>
|
||||
<!-- Rank 05 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">005</span>
|
||||
<span class="text-on-surface">vim_motions</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">04:05</span>
|
||||
</div>
|
||||
<!-- Rank 06 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">006</span>
|
||||
<span class="text-on-surface">tmux_lover</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">04:18</span>
|
||||
</div>
|
||||
<!-- Rank 07 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">007</span>
|
||||
<span class="text-on-surface">nvim_dotfiles</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">04:23</span>
|
||||
</div>
|
||||
<!-- Rank 08 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">008</span>
|
||||
<span class="text-on-surface">dark_theme</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">04:31</span>
|
||||
</div>
|
||||
<!-- Spacer for truncated view -->
|
||||
<div class="flex justify-center py-2 text-outline/30 tracking-[1em]">...</div>
|
||||
<!-- YOU (Rank 17) -->
|
||||
<div class="flex items-center justify-between px-2 py-2 bg-[#1f3a4a]/30 border border-[#6fc2ef]/40 rounded-sm">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#6fc2ef] w-8 font-bold">▶ 017</span>
|
||||
<span class="text-[#6fc2ef] font-bold">anonymous (YOU)</span>
|
||||
</div>
|
||||
<span class="text-[#6fc2ef] font-bold">04:12</span>
|
||||
</div>
|
||||
<!-- Rank 18 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">018</span>
|
||||
<span class="text-on-surface">bash_brawler</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">05:01</span>
|
||||
</div>
|
||||
<!-- Rank 19 -->
|
||||
<div class="flex items-center justify-between px-2 py-2 border-b border-[#353535]">
|
||||
<div class="flex gap-4">
|
||||
<span class="text-[#a0a0a0] w-8">019</span>
|
||||
<span class="text-on-surface">curl_master</span>
|
||||
</div>
|
||||
<span class="text-[#a0a0a0]">05:14</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CLI Style Footer -->
|
||||
<footer class="fixed bottom-0 w-full h-[24px] bg-[#202020] border-t border-[#353535] px-2 flex items-center justify-between font-mono text-[9px] z-50">
|
||||
<div class="text-[#a0a0a0]">
|
||||
<span class="text-info font-bold">▌</span> NORMAL │ leaderboard
|
||||
</div>
|
||||
<div class="text-[#a0a0a0] flex gap-3">
|
||||
<span>[1-4] tab</span>
|
||||
<span>[/] search</span>
|
||||
<span>[ESC] back</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Shared Component: BottomNavBar -->
|
||||
<nav class="fixed bottom-[24px] w-full h-action-bar-height z-50 flex justify-around items-center bg-surface-container dark:bg-surface-container border-t border-outline dark:border-outline">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
|
||||
<span class="material-symbols-outlined">playing_cards</span>
|
||||
<span class="font-label-caps text-label-caps">DEAL [F1]</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
|
||||
<span class="material-symbols-outlined">undo</span>
|
||||
<span class="font-label-caps text-label-caps">UNDO [Z]</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
|
||||
<span class="material-symbols-outlined">lightbulb</span>
|
||||
<span class="font-label-caps text-label-caps">HINT [H]</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center bg-primary-container dark:bg-primary-container text-on-primary-container dark:text-on-primary-container rounded-none p-2 transition-all duration-120 ease-linear active:bg-surface-container-highest">
|
||||
<span class="material-symbols-outlined">analytics</span>
|
||||
<span class="font-label-caps text-label-caps">STATS [S]</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant dark:text-on-surface-variant p-2 hover:text-primary dark:hover:text-primary transition-all duration-120 ease-linear active:bg-surface-container-highest">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
<span class="font-label-caps text-label-caps">MENU [ESC]</span>
|
||||
</button>
|
||||
</nav>
|
||||
</main>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 43 KiB |