Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41a009a693 | |||
| fa7f98ac52 | |||
| 9891ae4ba3 | |||
| cdcaddaabe | |||
| d752870007 | |||
| 1d1543e4bc | |||
| 651f4060e6 | |||
| a1376075bd | |||
| ceec4fc486 | |||
| 0d477ac9fd | |||
| 4b51e50203 | |||
| f2d2119db5 | |||
| 59424a370c | |||
| fb8b2ac684 | |||
| 690e1d2ad6 | |||
| 35516d31f6 | |||
| 9b065e5ac6 | |||
| e1b8766e15 | |||
| 67c150bd7b |
@@ -6,7 +6,242 @@ project follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
_Nothing yet._
|
No threads in flight. v0.20.0 cut on 2026-05-07; CHANGELOG accumulates
|
||||||
|
the next cycle here.
|
||||||
|
|
||||||
|
## [0.20.0] — 2026-05-07
|
||||||
|
|
||||||
|
Two through-lines closed: a full **Android port** (build target,
|
||||||
|
first 54 MB APK, JNI-free per-app persistence shim) and the
|
||||||
|
**Terminal visual-identity port** that replaces the prior
|
||||||
|
Premium-Solitaire palette across every UI surface. The Android
|
||||||
|
arc opened in `fb8b2ac` (compile + APK), continued in `4b51e50`
|
||||||
|
(`solitaire_data::data_dir` shim closing the CLAUDE.md §10
|
||||||
|
`dirs::data_dir() = None` pitfall), and is functional end-to-end
|
||||||
|
on a real device — though the runtime artwork is still the legacy
|
||||||
|
white-card palette, and JNI ClipboardManager / keyring bridges
|
||||||
|
remain stubbed (matching v0.19.0's documented fallback behaviour).
|
||||||
|
The Terminal port lands as a top-down stack: the `ui_theme` token
|
||||||
|
API in `0d477ac` is load-bearing, and the rest of the cycle is
|
||||||
|
downstream applications (modal scaffold, gameplay-feedback,
|
||||||
|
toasts, table / card chrome, splash cursor, hint-highlight
|
||||||
|
pairing). The card faces and suit-pip palette are deliberately
|
||||||
|
NOT migrated — those track PNG artwork that hasn't been
|
||||||
|
regenerated yet, and swapping the fallback constants ahead of the
|
||||||
|
artwork would mix two visual systems on any code path where
|
||||||
|
image loading fails.
|
||||||
|
|
||||||
|
The 24 Stitch-rendered mockups in `docs/ui-mockups/` are now
|
||||||
|
in-tree (`fa7f98a`); future plugin work should diff against the
|
||||||
|
matching mockup before touching pixels.
|
||||||
|
|
||||||
|
Two threads from v0.19.0's punch list also closed in this cycle:
|
||||||
|
the pull-failure test flake (`67c150b`), the Settings opt-out for
|
||||||
|
the smart-default window sizer (`e1b8766`), and the share-link
|
||||||
|
discoverability surfacing (`9b065e5`). The remaining v0.19.0
|
||||||
|
candidate — the app-icon round — stays open.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`ui_theme` Terminal design-token system** (`0d477ac`). Single
|
||||||
|
source of truth for the engine's visual identity:
|
||||||
|
base16-eighties palette (cyan primary CTA, lime/lavender/gold/
|
||||||
|
teal/pink semantic accents), 5-rung type scale, 7-rung 4-multiple
|
||||||
|
spacing scale, 3-step radius, 14-rung z-index hierarchy, full
|
||||||
|
motion budget, and four invariant-pinning unit tests. Every
|
||||||
|
downstream port commit in this cycle reads from this module —
|
||||||
|
swapping the palette is now a one-file edit, not a hunt across
|
||||||
|
~50 plugin files. Card-shadow alphas pinned to 0 (Terminal
|
||||||
|
achieves depth via 1px borders + tonal layering, no
|
||||||
|
`box-shadow`); the rendering path is left intact so a future
|
||||||
|
palette can re-enable shadows without touching consumers.
|
||||||
|
- **`ToastVariant` enum + Terminal toast styling** (`a137607`).
|
||||||
|
Toasts now follow `docs/ui-mockups/design-system.md`: opaque
|
||||||
|
`BG_ELEVATED` fill, 1px accent border keyed off
|
||||||
|
`Info` / `Warning` / `Error` / `Celebration` variants, 18px
|
||||||
|
monospaced caption (`TYPE_BODY_LG`), bottom-anchored. All ten
|
||||||
|
call sites pass their semantic variant: achievement / level-up
|
||||||
|
/ XP / daily / weekly / challenge → Celebration (lavender);
|
||||||
|
goal-announcement / time-attack / settings volume / auto-complete
|
||||||
|
→ Info (teal). Two regression tests pin variant→border mapping
|
||||||
|
to the design tokens and require all four borders to be visually
|
||||||
|
distinct. Queued and immediate toasts use slightly different
|
||||||
|
bottom anchors (6 % vs. 14 %) so a celebration toast spawned
|
||||||
|
alongside a queued info banner layers above it.
|
||||||
|
- **Terminal cursor block on the splash overlay** (`cdcadda`).
|
||||||
|
The launch splash now renders the design system's signature
|
||||||
|
`▌` cyan (`ACCENT_PRIMARY`) glyph (96 px, hand-tuned literal)
|
||||||
|
above the wordmark, matching `docs/ui-mockups/splash-mobile.html`.
|
||||||
|
Cursor fades on the same per-frame alpha schedule as the title
|
||||||
|
and subtitle so the brand beat still dissolves as a single
|
||||||
|
layer. Did *not* pull in the mockup's full boot-loader treatment
|
||||||
|
(scanline overlay, ✓ check log, progress bar, ROOT@SOLITAIRE
|
||||||
|
prompt) — those are aesthetic features warranting their own
|
||||||
|
commit.
|
||||||
|
- **Terminal design-system spec + 24-mockup library** (`fa7f98a`).
|
||||||
|
`docs/ui-mockups/design-system.md` (palette, type scale, spacing
|
||||||
|
scale, motion budget, component library, accessibility notes —
|
||||||
|
color-blind toggle, high-contrast mode, glyph differentiation,
|
||||||
|
canonical `"Terminal"` card-back theme) and 24 Stitch-rendered
|
||||||
|
mockups (HTML + PNG): 12 redesigned existing screens, 1 desktop
|
||||||
|
home variant, 2 onboarding steps, and 9 missing-plugin screens
|
||||||
|
(splash, challenge, time-attack, weekly-goals, leaderboard,
|
||||||
|
sync, level-up, replay, radial-menu). The spec the rest of this
|
||||||
|
cycle ports against; future plugin work diffs here before
|
||||||
|
touching pixels.
|
||||||
|
- **Android build target — first working APK** (`fb8b2ac`).
|
||||||
|
`cargo apk build -p solitaire_app --target x86_64-linux-android`
|
||||||
|
now produces a 54 MB debug-signed APK at
|
||||||
|
`target/debug/apk/solitaire-quest.apk`. Five gating points
|
||||||
|
resolved end-to-end:
|
||||||
|
- **`solitaire_app` split into bin + lib.** cargo-apk needs a
|
||||||
|
`cdylib` to bundle as `libmain.so`; pure-bin crates panic
|
||||||
|
with "Bin is not compatible with Cdylib". `src/lib.rs`
|
||||||
|
carries the ECS bootstrap as `pub fn run`; `src/main.rs` is
|
||||||
|
a 3-line shim that delegates for the desktop path.
|
||||||
|
- **`[package.metadata.android]`** pins target SDK 34 / min
|
||||||
|
SDK 26 and points `assets = "../assets"` at the workspace
|
||||||
|
asset directory so desktop and APK share one set.
|
||||||
|
- **Workspace `bevy` features** add `android-native-activity`
|
||||||
|
(target-gated inside bevy_internal — desktop builds compile
|
||||||
|
it out). Pairs with cargo-apk's NativeActivity wrapper.
|
||||||
|
- **`arboard` target-gated** to `cfg(not(target_os =
|
||||||
|
"android"))`. The crate has no Android backend; cargo apk
|
||||||
|
fails with E0433 on `platform::Clipboard` if left
|
||||||
|
unconditional. Stats's "Copy share link" surfaces an
|
||||||
|
informational toast on Android until JNI ClipboardManager
|
||||||
|
lands in the Phase-Android round.
|
||||||
|
- **`keyring` + `keyring-core` target-gated.** Bionic doesn't
|
||||||
|
expose `libc::__errno_location` so the transitive
|
||||||
|
`rpassword` won't compile. `auth_tokens` ships an Android
|
||||||
|
stub returning `KeychainUnavailable` for every call —
|
||||||
|
matches the existing fallback for a Linux box without
|
||||||
|
Secret Service.
|
||||||
|
- Cosmetic: cargo-apk panics post-sign when it tries to also
|
||||||
|
wrap the bin target. The APK on disk is unaffected;
|
||||||
|
`cargo apk build --lib` is the small workaround.
|
||||||
|
- **Android developer setup + build runbook** (`59424a3`).
|
||||||
|
Captures Debian 13 toolchain install (JDK 21, unzip, SDK
|
||||||
|
licence prompts), the `cargo apk build` invocation, the
|
||||||
|
cosmetic post-sign panic workaround, and a what-is-wired-vs-
|
||||||
|
stubbed table for the android target. Runnable on a fresh
|
||||||
|
clone — no machine-local context required.
|
||||||
|
- **F3-toggleable FPS / frame-time overlay** (`690e1d2`).
|
||||||
|
`DiagnosticsHudPlugin` wraps Bevy's `FrameTimeDiagnosticsPlugin`
|
||||||
|
and renders a corner readout the developer toggles with F3.
|
||||||
|
Hidden by default; F3 is not gated by pause / modal state.
|
||||||
|
Reads `smoothed()` so the cell isn't a per-frame jittery
|
||||||
|
scoreboard. Format: `FPS NN \u{2022} M.MM ms`. Anchored
|
||||||
|
top-right at `z = Z_SPLASH + 100` above every modal / toast /
|
||||||
|
splash. Update system bails when hidden so the
|
||||||
|
diagnostic-store lookup is free when nobody's looking.
|
||||||
|
- **"Smart window size" Settings toggle** (`e1b8766`). Gameplay
|
||||||
|
section gains an opt-out toggle for v0.19.0's
|
||||||
|
`apply_smart_default_window_size` system. New
|
||||||
|
`Settings::disable_smart_default_size: bool` with
|
||||||
|
`#[serde(default)]` so legacy `settings.json` files load to
|
||||||
|
the shipped behaviour (smart sizer enabled). `solitaire_app::main`
|
||||||
|
reads the flag once at startup and skips the system's
|
||||||
|
registration when set. Saved window geometry still wins over
|
||||||
|
both branches; tooltip on the row makes that explicit.
|
||||||
|
- **"Shareable" badge on the Latest-win caption** (`9b065e5`).
|
||||||
|
The Stats overlay's Latest-win caption now appends
|
||||||
|
`\u{2022} Shareable` when the displayed replay carries a
|
||||||
|
populated `share_url`. Players can see at a glance whether the
|
||||||
|
Copy share link button will produce a URL or surface the
|
||||||
|
upload-prerequisite toast.
|
||||||
|
- **Help overlay covers M / P / Win-Summary-Enter** (`35516d3`).
|
||||||
|
Three new rows in the Overlays section: M (Home / Mode
|
||||||
|
launcher), P (Profile), and the Enter accelerator that
|
||||||
|
dismisses the Win Summary modal. Three post-v0.18 entries
|
||||||
|
that had drifted out of the cheat sheet are now listed.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Gameplay-feedback colours route through Terminal state
|
||||||
|
tokens** (`ceec4fc`). Selection-highlight tints in
|
||||||
|
`selection_plugin` and the valid-drop marker tint in
|
||||||
|
`cursor_plugin` were hand-tuned RGB literals. Migrated to
|
||||||
|
semantic state tokens: keyboard-drag picking source →
|
||||||
|
`ACCENT_PRIMARY` (cyan focus); keyboard-drag lifted source →
|
||||||
|
`STATE_WARNING` (gold attention); destination → `STATE_SUCCESS`
|
||||||
|
(lime valid-move); `cursor_plugin::MARKER_VALID` →
|
||||||
|
`STATE_SUCCESS` at 0.55 α with a tracking test pinning its RGB
|
||||||
|
to the token. Three stale doc comments in `ui_modal` corrected
|
||||||
|
("loud yellow CTA" / "magenta secondary accent" → cyan /
|
||||||
|
lavender to match the actual token values).
|
||||||
|
- **`table_plugin` chrome migration to Terminal tokens** (`651f406`).
|
||||||
|
`marker_colour` promoted to module-level `pub const
|
||||||
|
PILE_MARKER_DEFAULT_COLOUR` so `cursor_plugin::MARKER_DEFAULT`
|
||||||
|
imports the const directly — replaces the prior
|
||||||
|
duplicated literal kept in sync only by doc comment with a
|
||||||
|
compile-enforced invariant. The empty-tableau "K" placeholder
|
||||||
|
text now uses `TEXT_PRIMARY` at 0.35 α; `HINT_PILE_HIGHLIGHT_COLOUR`
|
||||||
|
retuned from bright `srgb(1.0, 0.85, 0.1)` to the `STATE_WARNING`
|
||||||
|
token (`#ddb26f`) with a tracking test, and the existing "is
|
||||||
|
gold" character test loosened to fit the muted Terminal gold
|
||||||
|
while still rejecting non-warm colours.
|
||||||
|
- **`card_plugin` chrome migration to Terminal tokens** (`d752870`).
|
||||||
|
Drag-elevation shadow now sources its colour from
|
||||||
|
`CARD_SHADOW_COLOR` + `CARD_SHADOW_ALPHA_DRAG` so the Terminal
|
||||||
|
"no box-shadow" policy disables the stack shadow in lockstep
|
||||||
|
with the per-card shadows. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
|
||||||
|
retuned from raw green to `STATE_SUCCESS` at 0.6 α with a
|
||||||
|
tracking test. The duplicated `PILE_MARKER_DEFAULT_COLOUR`
|
||||||
|
const dropped — this plugin now imports the promoted const
|
||||||
|
from `table_plugin`. Stock recycle "↺" text moved from raw
|
||||||
|
white-at-0.7-α to `TEXT_PRIMARY.with_alpha(0.7)`. Card-face /
|
||||||
|
suit / card-back palette constants were intentionally NOT
|
||||||
|
migrated (the runtime path renders PNG artwork that's still on
|
||||||
|
the previous "white card" palette).
|
||||||
|
- **Hint-source card tint matches the destination pile**
|
||||||
|
(`9891ae4`). `input_plugin`'s hint-source card tint moved from
|
||||||
|
raw bright-yellow `srgba(1.0, 1.0, 0.4, 1.0)` to `STATE_WARNING`,
|
||||||
|
so the source card and the destination pile (which already uses
|
||||||
|
`STATE_WARNING` via `HINT_PILE_HIGHLIGHT_COLOUR`) wear the same
|
||||||
|
attention colour as a coherent pair.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`solitaire_data::data_dir` shim closes the Android persistence
|
||||||
|
gap** (`4b51e50`). `dirs::data_dir()` returns `None` on Android,
|
||||||
|
which silently disabled every persistence path (settings, stats,
|
||||||
|
achievements, replays, game-state, time-attack sessions, user
|
||||||
|
themes). New `solitaire_data::platform::data_dir()` shim falls
|
||||||
|
through to `dirs::data_dir()` on desktop and returns the per-app
|
||||||
|
sandbox at `/data/data/com.solitairequest.app/files` on Android
|
||||||
|
— no JNI needed, since the package id is pinned in
|
||||||
|
`[package.metadata.android]`. Six call sites across
|
||||||
|
`solitaire_data` plus `solitaire_engine/assets/user_dir.rs`
|
||||||
|
migrated. CLAUDE.md §10 already flagged this as a known
|
||||||
|
pitfall; the shim pays it down at the one chokepoint instead
|
||||||
|
of per feature.
|
||||||
|
- **`card_shadow_params` test aligned with Terminal "no shadow"
|
||||||
|
intent** (`1d1543e`). The Terminal token system pinned both
|
||||||
|
`CARD_SHADOW_ALPHA_IDLE` and `CARD_SHADOW_ALPHA_DRAG` to 0.0,
|
||||||
|
which made the prior `drag_alpha > idle_alpha` assertion fail
|
||||||
|
(`0 > 0` is false). Loosened to `drag_alpha >= idle_alpha`
|
||||||
|
with a comment naming the new invariant: under Terminal both
|
||||||
|
are 0; under any future palette that re-enables shadows, drag
|
||||||
|
still must not be weaker than idle. The useful regression-guard
|
||||||
|
(catching an accidental swap of the two constants) is preserved.
|
||||||
|
- **`pull_failure_sets_error_status` test flake** (`67c150b`).
|
||||||
|
The fixed 5-update budget was the last test still subject to
|
||||||
|
the AsyncComputeTaskPool starvation mode that v0.19.0's
|
||||||
|
auto-save fix already cleared. Replaced with a wall-clock-
|
||||||
|
bounded loop (5-second deadline, `std::thread::yield_now`
|
||||||
|
between iterations) that exits as soon as the status flips.
|
||||||
|
Mirrors the auto-save flake fix shape.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- **1176 passing tests / 0 failing** across the workspace
|
||||||
|
(six new tests this cycle: four `ui_theme` invariant guards
|
||||||
|
for the type / spacing / z-index scales + `scaled_duration`,
|
||||||
|
one toast-variant-border-mapping pair, and four palette-
|
||||||
|
tracking guards on `MARKER_VALID` / `HINT_PILE_HIGHLIGHT_COLOUR`
|
||||||
|
/ `RIGHT_CLICK_HIGHLIGHT_COLOUR` / toast-border distinctness).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
## [0.19.0] — 2026-05-06
|
## [0.19.0] — 2026-05-06
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ bevy = { version = "0.18", default-features = false, features = [
|
|||||||
"bevy_window",
|
"bevy_window",
|
||||||
"custom_cursor",
|
"custom_cursor",
|
||||||
"reflect_auto_register",
|
"reflect_auto_register",
|
||||||
# default_platform (desktop subset; no android/webgl/gilrs/sysinfo)
|
# default_platform (desktop subset)
|
||||||
"std",
|
"std",
|
||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
"default_font",
|
"default_font",
|
||||||
@@ -65,6 +65,13 @@ bevy = { version = "0.18", default-features = false, features = [
|
|||||||
# the game in an X11 frame inside the Wayland compositor.
|
# the game in an X11 frame inside the Wayland compositor.
|
||||||
"wayland",
|
"wayland",
|
||||||
"x11",
|
"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
|
# common_api
|
||||||
"bevy_color",
|
"bevy_color",
|
||||||
"bevy_image",
|
"bevy_image",
|
||||||
|
|||||||
@@ -1,171 +1,273 @@
|
|||||||
# Solitaire Quest — Session Handoff
|
# Solitaire Quest — Session Handoff
|
||||||
|
|
||||||
**Last updated:** 2026-05-06 (post-v0.19.0) — Tagged + pushed at
|
**Last updated:** 2026-05-07 — v0.20.0 cut. Two through-lines closed
|
||||||
`6037596`. v0.19.0 closes the v0.18.0 punch list (async H-key hint,
|
in this cycle: a full **Terminal visual-identity port** (token system
|
||||||
persistent replay share URLs), expands desktop platform fit (Wayland
|
in `ui_theme` plus downstream chrome migrations across modal scaffold,
|
||||||
session support + monitor-aware default window size), polishes the
|
gameplay-feedback, toasts, and the table / card / splash surfaces)
|
||||||
win-celebration and double-click animation paths, and clears two
|
and the **Android persistence shim** that closes the
|
||||||
test-flake contributors. A short-lived "Rusty Pixel" pixel-art card
|
`dirs::data_dir() = None` pitfall flagged in CLAUDE.md §10. The
|
||||||
theme was prototyped and reverted in the same window.
|
Android *build* target landed earlier in the cycle (`fb8b2ac`); this
|
||||||
|
session paid down the persistence half so a real APK can survive a
|
||||||
|
cold start. The 24 Stitch-rendered mockups are now in-tree under
|
||||||
|
`docs/ui-mockups/`; future plugin work diffs against the matching
|
||||||
|
mockup before touching pixels.
|
||||||
|
|
||||||
## Status at pause
|
## Status at pause
|
||||||
|
|
||||||
- **HEAD on origin:** `6037596` (post-tag commit; the tag itself
|
- **HEAD on origin:** the v0.20.0 docs commit (the one that lands
|
||||||
points at this commit).
|
this file + CHANGELOG cut). Tag not yet pushed; cut whenever
|
||||||
- **Working tree:** modified — `CHANGELOG.md` and
|
feels right.
|
||||||
`SESSION_HANDOFF.md` carry the v0.19.0 promotion + this refresh,
|
- **Working tree:** clean apart from the still-untracked `artwork/`
|
||||||
ready to commit.
|
directory (intentional — the card PNGs there are mid-flight for
|
||||||
|
the Terminal aesthetic and committing now would freeze a
|
||||||
|
transitional state).
|
||||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
||||||
clean (verified this session).
|
clean.
|
||||||
- **Tests:** **1170 passing / 0 failing** across the workspace
|
- **Tests:** **1176 passing / 0 failing** across the workspace.
|
||||||
(verified this session). One known flake remains:
|
Six new tests this cycle: four `ui_theme` invariant guards
|
||||||
`solitaire_engine::sync_plugin::tests::pull_failure_sets_error_status`
|
(type / spacing / z-index scales + `scaled_duration`), one
|
||||||
occasionally fails when cargo-test parallelism starves the
|
toast-variant-border-mapping pair, and four palette-tracking
|
||||||
`AsyncComputeTaskPool` within the test's 5-update budget. Same
|
guards on `MARKER_VALID` / `HINT_PILE_HIGHLIGHT_COLOUR` /
|
||||||
shape as the auto-save flake before v0.19.0's hardening; could be
|
`RIGHT_CLICK_HIGHLIGHT_COLOUR` / toast-border distinctness. No
|
||||||
fixed similarly with a wall-clock-bounded loop.
|
known flakes.
|
||||||
- **Tags on origin:** `v0.9.0` through `v0.18.0` (v0.19.0 ready to
|
- **Tags on origin:** `v0.9.0` through `v0.19.0`. v0.20.0 not yet
|
||||||
push once committed).
|
tagged.
|
||||||
|
|
||||||
## Where we are
|
## What shipped in v0.20.0
|
||||||
|
|
||||||
v0.18.0's resume-prompt menu (A–D) is closed:
|
### Terminal visual-identity port
|
||||||
|
|
||||||
- ~~**A — Tag v0.18.0:**~~ shipped at `bfcd05f`.
|
Top-down stack — every commit downstream of the token system
|
||||||
- ~~**B — Solver-on-`AsyncComputeTaskPool` for the H-key hint:**~~
|
reads from it, so swapping the palette is now a one-file edit:
|
||||||
shipped at `3e11e9e`.
|
|
||||||
- **C — Desktop packaging:** still gated on artwork + signing
|
|
||||||
certs. Icon export PNGs (11 sizes, 16–1024 px) sit in
|
|
||||||
`artwork/` from the v0.18-era export; not yet wired into the
|
|
||||||
Bevy window or assembled into `.icns` / `.ico`. App icon is
|
|
||||||
the first natural step.
|
|
||||||
- ~~**D — Persistent share link:**~~ shipped at `42d90b1`.
|
|
||||||
|
|
||||||
The Rusty Pixel theme arc is documented as a sub-history but
|
- **`ui_theme` token system** (`0d477ac`). base16-eighties
|
||||||
not part of v0.19.0's content:
|
palette, 5-rung type scale, 7-rung 4-multiple spacing scale,
|
||||||
|
3-step radius, 14-rung z-index hierarchy, full motion budget,
|
||||||
|
4 invariant-pinning unit tests. Card-shadow alphas pinned to 0
|
||||||
|
(Terminal achieves depth via 1px borders + tonal layering).
|
||||||
|
- **Modal scaffold already on tokens** — `ui_modal` was ported
|
||||||
|
in the same commit's wake; three stale "loud yellow" /
|
||||||
|
"magenta secondary" doc comments fixed.
|
||||||
|
- **Gameplay feedback → semantic state tokens** (`ceec4fc`).
|
||||||
|
Selection / valid-drop tints route through `ACCENT_PRIMARY` /
|
||||||
|
`STATE_WARNING` / `STATE_SUCCESS`.
|
||||||
|
- **Toasts** (`a137607`). New `ToastVariant` enum
|
||||||
|
(Info / Warning / Error / Celebration); opaque `BG_ELEVATED`
|
||||||
|
+ 1px accent border + bottom-anchor. All ten call sites pass
|
||||||
|
their semantic variant.
|
||||||
|
- **`table_plugin` chrome** (`651f406`).
|
||||||
|
`PILE_MARKER_DEFAULT_COLOUR` promoted; `cursor_plugin` imports
|
||||||
|
it, replacing a "kept in sync" doc comment with a compile-
|
||||||
|
enforced invariant. `HINT_PILE_HIGHLIGHT_COLOUR` →
|
||||||
|
`STATE_WARNING`.
|
||||||
|
- **`card_plugin` chrome** (`d752870`). Drag-elevation shadow
|
||||||
|
routes through `CARD_SHADOW_*` tokens. `RIGHT_CLICK_HIGHLIGHT_COLOUR`
|
||||||
|
→ `STATE_SUCCESS`. Stock recycle "↺" text → `TEXT_PRIMARY @ 0.7α`.
|
||||||
|
Card-face / suit / card-back palette intentionally NOT migrated
|
||||||
|
(artwork dependency — see open-list item below).
|
||||||
|
- **Splash cursor** (`cdcadda`). The signature `▌` cyan glyph
|
||||||
|
(96 px) added above the wordmark, matching the spec.
|
||||||
|
- **Hint-source / dest pairing** (`9891ae4`). `input_plugin`'s
|
||||||
|
source-card tint now matches the destination pile's
|
||||||
|
`STATE_WARNING`.
|
||||||
|
- **Design system + 24-mockup library** (`fa7f98a`).
|
||||||
|
`docs/ui-mockups/design-system.md` + 24 Stitch mockups (HTML +
|
||||||
|
PNG) covering every screen plus 9 missing-plugin surfaces.
|
||||||
|
- **`card_shadow_params` test aligned** (`1d1543e`). Drag-vs-
|
||||||
|
idle shadow assertion loosened to `>=` to accept the Terminal
|
||||||
|
"no shadow" intent without losing the regression-guard.
|
||||||
|
|
||||||
| Commit | Status |
|
### Android persistence
|
||||||
|---|---|
|
|
||||||
| `de47511` PNG-format thumbnail support | reverted |
|
|
||||||
| `17e3112` `pixel_art: bool` field + nearest-sampling opt-in | reverted |
|
|
||||||
| `21ec03b` bundle Rusty Pixel as `embedded://` theme | reverted |
|
|
||||||
| `aad8bb9` / `e41def8` / `0b3140a` reverts | landed |
|
|
||||||
|
|
||||||
The arc remains in commit history for archaeology but the
|
- **`solitaire_data::data_dir` shim** (`4b51e50`). New
|
||||||
codebase reaches v0.19.0's HEAD identical to where it would be if
|
`solitaire_data::platform::data_dir()` falls through to
|
||||||
the arc had never landed.
|
`dirs::data_dir()` on desktop and returns the per-app sandbox
|
||||||
|
at `/data/data/com.solitairequest.app/files` on Android — no
|
||||||
|
JNI needed (package id pinned in `[package.metadata.android]`).
|
||||||
|
Six `solitaire_data` callsites + `solitaire_engine/assets/user_dir.rs`
|
||||||
|
migrated. Settings, stats, achievements, replays, game-state,
|
||||||
|
time-attack sessions, and user themes now persist on Android.
|
||||||
|
|
||||||
### Design direction (unchanged)
|
### Inherited from earlier in the cycle (pre-session)
|
||||||
|
|
||||||
- **Tone:** Balatro — chunky readable type, theatrical hierarchy,
|
- Android build target + APK (`fb8b2ac`), runbook (`59424a3`),
|
||||||
satisfying micro-interactions.
|
F3 FPS overlay (`690e1d2`), Smart Window Size opt-out
|
||||||
- **Palette:** Midnight Purple base + Balatro yellow primary + warm
|
(`e1b8766`), Shareable badge (`9b065e5`), Help cheat-sheet
|
||||||
magenta secondary.
|
M/P/Enter rows (`35516d3`), `pull_failure_sets_error_status`
|
||||||
|
flake fix (`67c150b`).
|
||||||
|
|
||||||
|
## Open punch list
|
||||||
|
|
||||||
|
### Phase Android (build + persistence shipped; runtime gaps remain)
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
### Visual-identity follow-ups (opened by v0.20.0's port)
|
||||||
|
|
||||||
|
- **Card-face / suit / card-back artwork regeneration.** The
|
||||||
|
Terminal spec calls for dark `#1a1a1a` cards with light suit
|
||||||
|
pips (pink for hearts/diamonds, foreground gray for spades/
|
||||||
|
clubs); the runtime path still renders the legacy white-card
|
||||||
|
PNG artwork. The fallback constants in `card_plugin`
|
||||||
|
(`CARD_FACE_COLOUR`, `RED_SUIT_COLOUR`, `BLACK_SUIT_COLOUR`,
|
||||||
|
`CARD_FACE_COLOUR_RED_CBM`, `card_back_colour` palette) are
|
||||||
|
intentionally unmigrated and should swap in lockstep with the
|
||||||
|
artwork. Largest visible payoff remaining in the visual-
|
||||||
|
identity arc.
|
||||||
|
- **Splash boot-loader richness.** The mockup
|
||||||
|
(`docs/ui-mockups/splash-mobile.html`) calls for a scanline
|
||||||
|
overlay, ✓ lime check log lines, pulsing cursor, ROOT@SOLITAIRE
|
||||||
|
prompt, and a loading bar — none of which v0.20.0's
|
||||||
|
cursor-glyph-only port pulled in. Aesthetic feature, its own
|
||||||
|
commit.
|
||||||
|
- **Replay-overlay redesign.** The mockup
|
||||||
|
(`docs/ui-mockups/replay-overlay-mobile.html`) envisions a
|
||||||
|
much richer surface (terminal `▌replay.tsx` header, move log
|
||||||
|
scroll, MOVE 47/87 chip, WIN MOVE callout, status bar) versus
|
||||||
|
the current top banner. Aesthetic feature.
|
||||||
|
- **Toast Warning / Error variants.** The new `ToastVariant`
|
||||||
|
enum has slots for `Warning` (gold) and `Error` (pink) but no
|
||||||
|
in-engine event uses them yet (the four current toast events
|
||||||
|
all map to Info or Celebration). Wire when a warning- or
|
||||||
|
error-flavoured toast event materialises.
|
||||||
|
|
||||||
|
### Carried forward from v0.19.0
|
||||||
|
|
||||||
|
- **App icon round.** `Window::icon` not yet wired; no
|
||||||
|
`.icns` / `.ico` / Linux hicolor PNG hierarchy. The 11-size
|
||||||
|
icon export the v0.19 handoff referenced is *not* currently
|
||||||
|
in `artwork/` (current `artwork/` holds the reverted Rusty
|
||||||
|
Pixel card PNGs and is intentionally untracked); icon-export
|
||||||
|
needs to be re-run before this item can be picked up.
|
||||||
|
Half-day task once the PNGs are back in place. No cert
|
||||||
|
dependency.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
- **Token-port pattern.** v0.20.0's chrome-migration commits
|
||||||
|
set a reusable shape for "centralized 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.
|
||||||
|
- **Audit before migrating wide.** Before touching any plugin,
|
||||||
|
grep for the literal pattern (`Color::srgb\(|Color::srgba\(|
|
||||||
|
Color::WHITE|Color::BLACK`) and classify each hit as domain
|
||||||
|
vs. chrome. Most plugins after the modal scaffold port turned
|
||||||
|
out to be 100 % token-correct already; the audit prevents
|
||||||
|
wasted churn.
|
||||||
|
|
||||||
### Canonical remote
|
### Canonical remote
|
||||||
|
|
||||||
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
|
`github.com/funman300/Rusty_Solitaire` is the canonical repo.
|
||||||
Always push there.
|
Always push there.
|
||||||
|
|
||||||
## v0.19.0 (2026-05-06)
|
### Design direction (now Terminal — base16-eighties)
|
||||||
|
|
||||||
| Area | Commits | What landed |
|
- **Tone:** retro-terminal / synthwave — flat depth (no box-shadows),
|
||||||
|---|---|---|
|
monospaced-forward typography (JetBrains Mono / FiraMono), tight
|
||||||
| Async H-key hint | `3e11e9e` | New `pending_hint.rs` module: `PendingHintTask` resource, `poll_pending_hint_task` + `drop_pending_hint_on_state_change` systems, cancel-on-replace, stale-state guard via `move_count_at_spawn`. Removes the last synchronous solver hot path. |
|
16 px edge margins, 8 px card radius.
|
||||||
| Persistent share URLs | `42d90b1` | `Replay.share_url: Option<String>` with `#[serde(default)]`. `poll_replay_upload_result` writes into `replays[0].share_url` + persists. Stats Copy button reads from selected replay. `LastSharedReplayUrl` deleted. |
|
- **Palette:** near-black surface ramp (`#151515` / `#202020` / `#2a2a2a`
|
||||||
| Auto-save flake fix | `91b7605` | `test_app` clears `PendingRestoredGame(None)` after plugin build; test re-arms the timer in a bounded loop. No production-code change. |
|
/ `#353535`), cyan primary CTA (`#6fc2ef`), lime success
|
||||||
| Wayland support | `b57db01` | Adds `wayland` to Bevy features. winit prefers Wayland when `WAYLAND_DISPLAY` is set, falls back to X11. Native Wayland surface instead of XWayland frame. |
|
(`#acc267`), gold warning (`#ddb26f`), pink error / suit-red
|
||||||
| Smart default window size | `b57db01` | New `apply_smart_default_window_size` Update system queries `PrimaryMonitor` and resizes the window to ~70 % of monitor's logical size on the first frame. Skipped when saved geometry was applied. |
|
(`#fb9fb1`), lavender celebration (`#e1a3ee`), teal info
|
||||||
| Win-celebration cleanup | `55c235b` | Drops the duplicate "You Win" toast that rendered behind the WinSummary modal. Cards-fly-off cascade kept; toast removed. |
|
(`#12cfc0`).
|
||||||
| Double-click reject animation | `d7ffb16` | Single-card double-clicks with no destination now play the same shake + sound as multi-card stack misses. Both priorities' failure paths converge on one `MoveRejectedEvent` write. |
|
- **Two-color suits.** Red = `#fb9fb1`, black = `#d0d0d0`. Outlined
|
||||||
| Double-click animation dedup | `6037596` | Drops the redundant `StateChangedEvent` write in `end_drag`'s uncommitted-drag branch; previously raced an in-flight CardAnim and restarted the slide visibly. |
|
glyphs for diamonds & clubs are *always on*; the Settings
|
||||||
|
"color-blind mode" toggle only swaps red → cyan.
|
||||||
|
|
||||||
## Open punch list
|
(Was: Midnight Purple base + Balatro yellow primary + warm magenta.
|
||||||
|
Replaced this cycle.)
|
||||||
### Carried forward
|
|
||||||
|
|
||||||
- **Desktop packaging** per `ARCHITECTURE.md §17`. Eleven icon
|
|
||||||
PNG sizes (16, 24, 32, 48, 64, 96, 128, 192, 256, 512, 1024)
|
|
||||||
exported via `artwork/Icon Export.html` sit in `artwork/`
|
|
||||||
pending wiring. Pending: actual Bevy window-icon hookup,
|
|
||||||
macOS `.icns` assembly via `iconutil`, Windows `.ico` via
|
|
||||||
`magick convert`, Linux hicolor PNG hierarchy install,
|
|
||||||
AppImage recipe, macOS notarisation cert, Windows
|
|
||||||
Authenticode cert.
|
|
||||||
|
|
||||||
### Possible next-round candidates
|
|
||||||
|
|
||||||
- **App icon round** — wire the icon into the Bevy window via
|
|
||||||
`Window::icon`, generate `.icns` and `.ico` from the existing
|
|
||||||
PNGs. Half-day task; doesn't depend on signing certs.
|
|
||||||
- **`pull_failure_sets_error_status` flake fix** — same pattern
|
|
||||||
as the auto-save flake. Wall-clock-bounded loop instead of
|
|
||||||
fixed 5-update budget. ~10 lines.
|
|
||||||
- **Settings UI for "open at this size on launch"** — once the
|
|
||||||
smart-default-size system is shipping, expose a checkbox to
|
|
||||||
*disable* it (player who specifically wants 1280×800 every
|
|
||||||
time). Trivial.
|
|
||||||
- **Persistent share link URL on selector caption** — surface
|
|
||||||
whether the currently-selected replay has a `share_url`
|
|
||||||
populated (e.g. "Replay 3 / 8 \u{2022} Shareable") so players
|
|
||||||
know which entries the Copy button can copy.
|
|
||||||
|
|
||||||
### Process notes (from this round)
|
|
||||||
|
|
||||||
- **Async port template (worked again):** the H-key port
|
|
||||||
followed `d489e7a`'s `PendingNewGameSeed` shape one-to-one
|
|
||||||
and the second async port required no new infrastructure.
|
|
||||||
Future async ports (e.g. moving `try_solve_with_first_move`'s
|
|
||||||
full-search variant, if it ever surfaces in the picker UI)
|
|
||||||
should follow the same shape.
|
|
||||||
- **Rusty Pixel reverted cleanly:** `git revert` of three
|
|
||||||
contiguous feature commits produced a clean three-revert
|
|
||||||
sequence with no manual conflict resolution. Bisect remains
|
|
||||||
fast over the full v0.19.0 history because the reverts are
|
|
||||||
individual commits, not a squash.
|
|
||||||
- **Defensive event writes pattern:** the
|
|
||||||
`auto_save_writes_after_30_seconds` flake AND the
|
|
||||||
`end_drag` double-animation bug shared a root cause:
|
|
||||||
defensive `MessageWriter` writes that originally covered an
|
|
||||||
edge case which no longer holds, but became load-bearing
|
|
||||||
once another system started paying attention to the event.
|
|
||||||
Worth a periodic pass: any event write that doesn't
|
|
||||||
correspond to a real state change is a candidate for
|
|
||||||
removal.
|
|
||||||
|
|
||||||
## Resume prompt
|
## Resume prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||||
Working directory: <Rusty_Solitaire clone path on this machine>.
|
Working directory: <Rusty_Solitaire clone path on this machine>.
|
||||||
Branch: master. v0.19.0 just shipped. The next natural item is
|
Branch: master. v0.20.0 just cut on 2026-05-07; CHANGELOG's new
|
||||||
desktop-packaging follow-through, starting with the app icon.
|
[Unreleased] section is empty pending the next cycle's threads.
|
||||||
|
|
||||||
State: HEAD at 6037596 + the v0.19.0 docs commit on top (this
|
State: HEAD on the v0.20.0 docs commit. Tag not pushed yet — last
|
||||||
session). Tag v0.19.0 points at the docs commit.
|
pushed tag is v0.19.0. Working tree clean apart from the
|
||||||
|
intentionally-untracked `artwork/`.
|
||||||
|
|
||||||
READ FIRST (in order, before doing anything):
|
READ FIRST (in order, before doing anything):
|
||||||
1. SESSION_HANDOFF.md — this file
|
1. SESSION_HANDOFF.md — this file
|
||||||
2. CHANGELOG.md — [Unreleased] is empty; [0.19.0] just landed
|
2. CHANGELOG.md — [0.20.0] section is the most recent cut
|
||||||
3. CLAUDE.md — unified-3.0 rule set
|
3. CLAUDE.md — unified-3.0 rule set
|
||||||
4. CLAUDE_SPEC.md — formal architecture spec
|
4. CLAUDE_SPEC.md — formal architecture spec
|
||||||
5. ARCHITECTURE.md — crate responsibilities + data flow
|
5. ARCHITECTURE.md — crate responsibilities + data flow
|
||||||
6. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
6. docs/ui-mockups/ — design system + 24-mockup library
|
||||||
|
(Terminal aesthetic — landed in fa7f98a)
|
||||||
|
7. docs/android/* — Android setup + build runbook
|
||||||
|
8. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||||
— saved feedback / project context
|
— saved feedback / project context
|
||||||
(machine-local; may be missing on a
|
(machine-local; may be missing on a
|
||||||
fresh machine)
|
fresh machine)
|
||||||
|
|
||||||
DECISION TO ASK THE PLAYER FIRST:
|
DECISION TO ASK THE PLAYER FIRST:
|
||||||
A. App icon — wire artwork/icon-{size}.png into Bevy's
|
A. Push v0.20.0 tag — `git tag v0.20.0 && git push --tags`. If
|
||||||
Window::icon, generate .icns + .ico, drop into Linux
|
the player wants the cut formalised before any new work.
|
||||||
hicolor hierarchy. Half-day task. No cert dependency.
|
B. APK launch verification — `adb install` + `adb logcat` on
|
||||||
B. Desktop packaging continued — AppImage recipe, .desktop
|
bevy_test AVD or an x86_64 device. Now that persistence is
|
||||||
file, install scripts. Larger task; unlocks distro
|
wired (4b51e50), shake out remaining runtime bugs.
|
||||||
packaging. No cert dependency.
|
C. Card-face artwork regeneration — generate Terminal-aesthetic
|
||||||
C. macOS / Windows signing cert acquisition — needs user
|
card PNGs (dark face, light suit pips), then migrate
|
||||||
action; agent can't drive.
|
CARD_FACE_COLOUR / RED_SUIT_COLOUR / BLACK_SUIT_COLOUR /
|
||||||
D. `pull_failure_sets_error_status` flake fix — small, well-
|
CARD_FACE_COLOUR_RED_CBM in lockstep. Largest visible
|
||||||
scoped. Same pattern as the v0.19.0 auto-save flake fix.
|
payoff remaining in the visual-identity arc.
|
||||||
|
D. Splash boot-loader richness — port the scanline overlay,
|
||||||
|
✓ check log, pulsing cursor, ROOT@SOLITAIRE prompt, and
|
||||||
|
loading bar from docs/ui-mockups/splash-mobile.html. Pure
|
||||||
|
polish; no behavioural change.
|
||||||
|
E. App icon round — re-run artwork/Icon Export.html (the
|
||||||
|
export PNGs are not currently in `artwork/`), then wire
|
||||||
|
Window::icon + generate .icns / .ico. Half-day task. No
|
||||||
|
cert dependency.
|
||||||
|
F. JNI ClipboardManager / Keystore bridge — replaces the
|
||||||
|
Android stubs for Stats clipboard share + sync auth.
|
||||||
|
|
||||||
WORKFLOW NOTES:
|
WORKFLOW NOTES:
|
||||||
- Use the system git config (already correct).
|
- Use the system git config (already correct).
|
||||||
@@ -173,8 +275,8 @@ WORKFLOW NOTES:
|
|||||||
"Quat" not "Rhys" (saved feedback memory).
|
"Quat" not "Rhys" (saved feedback memory).
|
||||||
- Sub-agents stage + verify only; orchestrator commits.
|
- Sub-agents stage + verify only; orchestrator commits.
|
||||||
- Every commit must pass build / clippy / test before pushing.
|
- Every commit must pass build / clippy / test before pushing.
|
||||||
- Push to GitHub (origin) — gh auth setup-git is already
|
- Push to GitHub (origin) — gh auth setup-git wired on
|
||||||
wired on this machine.
|
primary dev box; verify on laptop before first push.
|
||||||
|
|
||||||
OPEN AT THE START: ask which of A–D. Don't pick unilaterally.
|
OPEN AT THE START: ask which of A–F. Don't pick unilaterally.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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,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,278 @@
|
|||||||
|
---
|
||||||
|
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: '#6fc2ef'
|
||||||
|
primary: '#6fc2ef'
|
||||||
|
on-primary: '#151515'
|
||||||
|
primary-container: '#1f3a4a'
|
||||||
|
on-primary-container: '#a8dcf5'
|
||||||
|
inverse-primary: '#0e6e99'
|
||||||
|
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: '#6fc2ef'
|
||||||
|
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 — primary CTA, focus ring, `selection`, `suit-red-cb` (color-blind tinted red) |
|
||||||
|
| base0D | `#6fc2ef` | (alias) |
|
||||||
|
| base0E | `#e1a3ee` | violet — used for celebration (level-up, achievement unlock) |
|
||||||
|
| base0F | `#fb9fb1` | (alias) |
|
||||||
|
|
||||||
|
### Semantic assignments
|
||||||
|
|
||||||
|
- **CTA / Primary action**: cyan `#6fc2ef`. Reserved for "Play," "New Game," "Save," "Resume," and the focus ring on selected cards. Never used decoratively.
|
||||||
|
- **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 mapping**, with mandatory color-blind support:
|
||||||
|
|
||||||
|
| Suit | Default | Color-blind mode | Glyph differentiation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hearts | `#fb9fb1` (pink) | `#6fc2ef` (cyan) | Solid filled glyph |
|
||||||
|
| Diamonds | `#fb9fb1` (pink) | `#6fc2ef` (cyan) | **Outlined glyph (1.5px stroke)** |
|
||||||
|
| Spades | `#d0d0d0` (foreground) | `#d0d0d0` | Solid filled glyph |
|
||||||
|
| Clubs | `#d0d0d0` (foreground) | `#d0d0d0` | **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 only swaps red→cyan; it does not turn the outlined glyphs on or off, because outlined glyphs are always on.
|
||||||
|
|
||||||
|
## 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 `#6fc2ef` 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 `#6fc2ef` 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: 1px solid in suit color (pink for hearts/diamonds, foreground gray for spades/clubs)
|
||||||
|
- Top-left: rank in JetBrains Mono Bold 18px + small suit glyph (10px)
|
||||||
|
- Bottom-right: large suit glyph (32px), rotated 180°
|
||||||
|
- 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 `#6fc2ef` 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 `#6fc2ef` fill, `#151515` text, JetBrains Mono Medium 14px uppercase with `+0.08em` tracking. 4px corner radius. Pressed state: darken to `#5aa9d4`. Disabled: `#353535` fill, `#505050` text.
|
||||||
|
|
||||||
|
### Secondary Buttons
|
||||||
|
|
||||||
|
Transparent fill, 1px `#505050` border, `#d0d0d0` text. Hover/press: border becomes `#6fc2ef`, text becomes `#6fc2ef`.
|
||||||
|
|
||||||
|
### 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 `#6fc2ef`.
|
||||||
|
|
||||||
|
### 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 red suits' default `#fb9fb1` for `#6fc2ef`. 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,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 |
@@ -0,0 +1,259 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>ROOT@SOLITAIRE:~ | LEVEL UP</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&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;
|
||||||
|
}
|
||||||
|
.scanline-overlay {
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.card-glow {
|
||||||
|
box-shadow: 0 0 24px rgba(225, 163, 238, 0.25);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"background": "#101417",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"surface": "#151515",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"outline": "#505050",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"suit-red": "#fb9fb1"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"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"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-background font-body-md overflow-hidden h-screen select-none">
|
||||||
|
<!-- Top App Bar -->
|
||||||
|
<header class="fixed top-0 w-full z-50 flex justify-between items-center px-margin-edge h-action-bar-height bg-background dark:bg-background border-b border-outline-variant dark:border-outline-variant">
|
||||||
|
<div class="font-headline text-headline text-primary dark:text-primary uppercase tracking-tighter">ROOT@SOLITAIRE:~</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="memory">memory</span>
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="settings_ethernet">settings_ethernet</span>
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="wifi_tethering">wifi_tethering</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Main Tableau (Dimmed Background) -->
|
||||||
|
<main class="pt-24 px-4 flex flex-col gap-8 opacity-20 filter grayscale">
|
||||||
|
<!-- HUD Chips -->
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="bg-surface-container p-3 flex flex-col">
|
||||||
|
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">SCORE</span>
|
||||||
|
<span class="font-hud-score text-hud-score text-primary">04,820</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-container p-3 flex flex-col items-end">
|
||||||
|
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">TIMER</span>
|
||||||
|
<span class="font-hud-timer text-hud-timer text-on-surface">04:12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Foundation & Stock -->
|
||||||
|
<div class="flex gap-gutter-card justify-between">
|
||||||
|
<div class="flex gap-gutter-card">
|
||||||
|
<div class="w-[64px] h-[88px] border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="w-[64px] h-[88px] bg-surface border border-outline rounded-DEFAULT relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 scanline-overlay"></div>
|
||||||
|
<div class="absolute top-2 left-2 w-3 h-4 bg-suit-red-cb"></div>
|
||||||
|
<div class="absolute bottom-2 right-2 font-card-rank text-[12px] text-on-surface">▌RS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-gutter-card">
|
||||||
|
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
|
||||||
|
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
|
||||||
|
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
|
||||||
|
<div class="w-[64px] h-[88px] border border-outline rounded-DEFAULT bg-surface"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Cascades -->
|
||||||
|
<div class="grid grid-cols-7 gap-gutter-card">
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
<div class="h-24 border border-dashed border-outline rounded-DEFAULT"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- CELEBRATION OVERLAY SCREEM -->
|
||||||
|
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-surface/95 backdrop-blur-sm">
|
||||||
|
<!-- Celebration Card -->
|
||||||
|
<div class="w-[340px] h-[480px] bg-[#202020] border border-highlight-celebration rounded-xl card-glow flex flex-col overflow-hidden relative">
|
||||||
|
<!-- Title Bar -->
|
||||||
|
<div class="h-[28px] bg-[#1a1a1a] border-b border-outline flex items-center px-4 shrink-0">
|
||||||
|
<span class="text-primary mr-1">▌</span>
|
||||||
|
<span class="font-headline text-[12px] text-[#a0a0a0]">level-up.tsx</span>
|
||||||
|
</div>
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-grow p-6 flex flex-col">
|
||||||
|
<!-- Hero Band -->
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div class="font-headline text-[14px] text-highlight-celebration uppercase tracking-[0.08em] mb-2">▲ LEVEL UP</div>
|
||||||
|
<div class="flex items-baseline justify-center gap-2">
|
||||||
|
<span class="font-headline font-bold text-[96px] text-suit-black leading-none tracking-tighter">13</span>
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span class="font-label-caps text-[11px] text-outline uppercase">FROM 12</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 font-headline font-medium text-[13px] text-highlight-celebration tracking-[0.08em]">█ NEW PERKS UNLOCKED</div>
|
||||||
|
</div>
|
||||||
|
<!-- Perks List -->
|
||||||
|
<div class="space-y-2 mb-6">
|
||||||
|
<!-- Item 1 -->
|
||||||
|
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<span class="font-headline text-[13px] text-suit-black">▢ +1 daily challenge slot</span>
|
||||||
|
<span class="bg-highlight-celebration/10 text-highlight-celebration px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-celebration/30">NEW</span>
|
||||||
|
</div>
|
||||||
|
<!-- Item 2 -->
|
||||||
|
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<span class="font-headline text-[13px] text-suit-black">▢ Background: Forest</span>
|
||||||
|
<span class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-valid/30">UNLOCKED</span>
|
||||||
|
</div>
|
||||||
|
<!-- Item 3 -->
|
||||||
|
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<span class="font-headline text-[13px] text-suit-black">▢ Card-back: Stripes</span>
|
||||||
|
<span class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-full font-label-caps text-[10px] border border-highlight-valid/30">UNLOCKED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- XP Recap -->
|
||||||
|
<div class="h-[48px] bg-[#1a1a1a] rounded-[4px] px-4 flex items-center justify-between mt-auto">
|
||||||
|
<span class="font-headline text-[12px] text-[#a0a0a0]">XP</span>
|
||||||
|
<span class="font-headline font-bold text-[14px] text-highlight-valid uppercase">+200 XP THIS LEVEL</span>
|
||||||
|
<div class="w-[60px] h-[4px] bg-outline-variant rounded-full overflow-hidden">
|
||||||
|
<div class="w-[0%] h-full bg-suit-red-cb"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Action Button -->
|
||||||
|
<button class="h-[56px] w-full bg-suit-red-cb flex items-center justify-center gap-2 hover:opacity-90 active:opacity-75 transition-opacity">
|
||||||
|
<span class="font-headline font-bold text-[14px] text-background tracking-wider">▶ CONTINUE</span>
|
||||||
|
</button>
|
||||||
|
<!-- Scanline layer inside card -->
|
||||||
|
<div class="absolute inset-0 scanline-overlay opacity-20 pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Caption -->
|
||||||
|
<div class="absolute bottom-8 w-full text-center">
|
||||||
|
<span class="font-body-md text-[11px] text-[#a0a0a0] uppercase tracking-widest opacity-60">Tap anywhere to dismiss</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Bottom Nav Bar -->
|
||||||
|
<nav class="fixed bottom-0 w-full z-50 flex justify-between items-center h-action-bar-height bg-surface-container dark:bg-surface-container border-t border-outline dark:border-outline">
|
||||||
|
<div class="flex flex-col items-center justify-center bg-primary text-background p-2 w-1/5 h-full transition-colors font-label-caps text-label-caps uppercase tracking-widest">
|
||||||
|
<span class="material-symbols-outlined" data-icon="videogame_asset">videogame_asset</span>
|
||||||
|
<span>NORMAL</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
|
||||||
|
<span class="material-symbols-outlined" data-icon="edit">edit</span>
|
||||||
|
<span>INSERT</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
|
||||||
|
<span class="material-symbols-outlined" data-icon="visibility">visibility</span>
|
||||||
|
<span>VISUAL</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
|
||||||
|
<span class="material-symbols-outlined" data-icon="auto_fix_high">auto_fix_high</span>
|
||||||
|
<span>SOLVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 w-1/5 h-full hover:text-primary transition-colors font-label-caps text-label-caps uppercase tracking-widest">
|
||||||
|
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
|
||||||
|
<span>QUIT</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Persistent Background Overlay (CRT Effect) -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none z-[200] scanline-overlay opacity-30"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,206 @@
|
|||||||
|
<!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=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;800&family=Inter:wght@400;500&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": {
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface": "#151515",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"background": "#101417",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"outline": "#505050",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"primary-fixed-dim": "#7ed0fe"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"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-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #151515;
|
||||||
|
color: #d0d0d0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.card-scanline {
|
||||||
|
background: linear-gradient(rgba(21, 21, 21, 0) 50%, rgba(26, 26, 26, 1) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen">
|
||||||
|
<!-- Mobile Canvas (390x844 simulated) -->
|
||||||
|
<main class="w-[390px] h-[844px] bg-surface relative overflow-hidden flex flex-col">
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<header class="h-8 bg-surface-container flex items-center justify-between px-margin-edge border-b border-outline-variant">
|
||||||
|
<div class="font-hud-timer text-[11px] text-primary tracking-tight">▌onboard/01-draw.tsx</div>
|
||||||
|
<div class="font-hud-timer text-[11px] text-on-surface-variant font-bold">STEP 1 OF 3</div>
|
||||||
|
</header>
|
||||||
|
<!-- Content Canvas -->
|
||||||
|
<div class="flex-1 overflow-y-auto pb-action-bar-height">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="h-[140px] flex flex-col items-center justify-center mt-8">
|
||||||
|
<div class="w-8 h-12 bg-primary animate-pulse mb-2"></div>
|
||||||
|
<h1 class="font-headline text-headline text-on-surface uppercase tracking-tighter">
|
||||||
|
WELCOME <span class="text-primary">▌_</span>
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
<!-- Headline -->
|
||||||
|
<section class="px-margin-edge mt-4 text-center">
|
||||||
|
<h2 class="font-headline text-[22px] leading-tight text-on-surface mb-1">CHOOSE A DRAW MODE</h2>
|
||||||
|
<p class="font-body-md text-[12px] text-on-surface-variant">You can change this any time in Settings.</p>
|
||||||
|
</section>
|
||||||
|
<!-- Choice Cards -->
|
||||||
|
<section class="px-margin-edge mt-8 space-y-4">
|
||||||
|
<!-- DRAW-3 Card -->
|
||||||
|
<div class="h-[120px] bg-surface-container border border-primary p-4 relative flex items-start gap-4">
|
||||||
|
<div class="absolute top-0 right-0 bg-primary px-2 py-0.5">
|
||||||
|
<span class="font-label-caps text-[10px] text-surface font-bold">RECOMMENDED</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-16 flex items-center justify-center border border-outline-variant bg-surface-dim">
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="filter_3">filter_3</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="font-headline text-[16px] text-primary mb-1">DRAW-3 (CLASSIC)</h3>
|
||||||
|
<p class="font-hud-timer text-[12px] leading-snug text-on-surface-variant">
|
||||||
|
Cycle 3 cards at a time. Standard solitaire rules for a tactical challenge.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- DRAW-1 Card -->
|
||||||
|
<div class="h-[120px] bg-surface-container border border-outline-variant p-4 flex items-start gap-4">
|
||||||
|
<div class="w-12 h-16 flex items-center justify-center border border-outline-variant bg-surface-dim">
|
||||||
|
<span class="material-symbols-outlined text-on-surface-variant" data-icon="filter_1">filter_1</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="font-headline text-[16px] text-on-surface mb-1">DRAW-1 (EASY)</h3>
|
||||||
|
<p class="font-hud-timer text-[12px] leading-snug text-on-surface-variant">
|
||||||
|
Cycle one card at a time. More winnable, faster pace, perfect for quick sessions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Step Indicator -->
|
||||||
|
<section class="mt-12 flex flex-col items-center">
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<div class="w-8 h-1.5 bg-primary"></div>
|
||||||
|
<div class="w-8 h-1.5 bg-outline-variant"></div>
|
||||||
|
<div class="w-8 h-1.5 bg-outline-variant"></div>
|
||||||
|
</div>
|
||||||
|
<div class="font-hud-timer text-[12px] flex gap-4">
|
||||||
|
<span class="text-primary font-bold">[1]</span>
|
||||||
|
<span class="text-outline-variant">[2]</span>
|
||||||
|
<span class="text-outline-variant">[3]</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<!-- Bottom Action Bar -->
|
||||||
|
<footer class="h-action-bar-height bg-surface-container border-t border-outline flex items-center justify-between px-margin-edge fixed bottom-0 w-[390px] z-50">
|
||||||
|
<!-- Back Button (Disabled/Muted) -->
|
||||||
|
<button class="w-[48%] h-12 border border-outline-variant flex items-center justify-center gap-2 opacity-40 cursor-not-allowed">
|
||||||
|
<span class="material-symbols-outlined text-outline-variant text-[18px]" data-icon="arrow_back">arrow_back</span>
|
||||||
|
<span class="font-label-caps text-outline-variant">BACK</span>
|
||||||
|
</button>
|
||||||
|
<!-- Next Button -->
|
||||||
|
<button class="w-[48%] h-12 bg-primary flex items-center justify-center gap-2 active:opacity-80 transition-opacity">
|
||||||
|
<span class="font-headline text-[14px] text-surface font-bold uppercase tracking-widest">NEXT</span>
|
||||||
|
<span class="material-symbols-outlined text-surface text-[18px]" data-icon="arrow_forward">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
<!-- Terminal Overlay (Faint scanlines for atmosphere) -->
|
||||||
|
<div class="pointer-events-none absolute inset-0 opacity-[0.03] card-scanline z-[100]"></div>
|
||||||
|
</main>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,211 @@
|
|||||||
|
<!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&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": {
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"background": "#101417",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface": "#151515",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"tertiary-container": "#e1a3ee"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"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"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
.scanline-bg {
|
||||||
|
background: linear-gradient(to bottom, #1a1a1a 1px, transparent 1px);
|
||||||
|
background-size: 100% 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-surface font-body-md select-none overflow-hidden h-screen flex flex-col">
|
||||||
|
<!-- Top Navigation Bar -->
|
||||||
|
<header class="fixed top-0 w-full bg-background z-50 border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
|
||||||
|
<span class="font-headline text-headline text-primary uppercase tracking-tighter text-sm md:text-base">▌onboard/03-demo.tsx</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-label-caps text-label-caps text-on-surface-variant">STEP 3 OF 3</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-1 mt-[64px] mb-[64px] flex flex-col items-center px-margin-edge pt-6 space-y-6 overflow-y-auto">
|
||||||
|
<!-- Header Text -->
|
||||||
|
<div class="w-full text-center space-y-2">
|
||||||
|
<h1 class="font-headline text-headline text-on-surface">TRY IT OUT</h1>
|
||||||
|
<p class="font-body-md text-on-surface-variant max-w-xs mx-auto">Tap a face-up card to auto-move it to the best legal pile.</p>
|
||||||
|
</div>
|
||||||
|
<!-- Demo Panel -->
|
||||||
|
<div class="w-full max-w-sm bg-surface border border-outline p-6 rounded-lg relative overflow-hidden">
|
||||||
|
<!-- Subtle scanline background effect for "terminal" pane feel -->
|
||||||
|
<div class="absolute inset-0 scanline-bg opacity-10 pointer-events-none"></div>
|
||||||
|
<div class="relative z-10 flex flex-col items-center">
|
||||||
|
<!-- Foundation Slot -->
|
||||||
|
<div class="w-20 h-28 border border-dashed border-outline-variant flex items-center justify-center mb-12">
|
||||||
|
<span class="material-symbols-outlined text-outline-variant opacity-40 text-4xl" data-icon="spades">playing_cards</span>
|
||||||
|
</div>
|
||||||
|
<!-- Path Indicator (The Arrow) -->
|
||||||
|
<div class="absolute top-[84px] left-1/2 -translate-x-1/2 flex flex-col items-center">
|
||||||
|
<div class="font-label-caps text-secondary text-[10px] mb-1">MOVES HERE</div>
|
||||||
|
<span class="material-symbols-outlined text-secondary text-3xl font-bold" data-icon="arrow_upward">arrow_upward</span>
|
||||||
|
</div>
|
||||||
|
<!-- Mini-Cards Row -->
|
||||||
|
<div class="flex gap-gutter-card">
|
||||||
|
<!-- A-Spades (Target) -->
|
||||||
|
<div class="w-20 h-28 bg-surface border-2 border-primary rounded flex flex-col justify-between p-2 relative ring-1 ring-primary ring-offset-2 ring-offset-surface">
|
||||||
|
<div class="font-card-rank text-card-rank text-suit-black">A</div>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl text-suit-black" data-icon="spades" style="font-variation-settings: 'FILL' 1;">playing_cards</span>
|
||||||
|
<!-- Pulse Icon -->
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-primary text-4xl opacity-80" data-icon="touch_app">touch_app</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- K-Hearts -->
|
||||||
|
<div class="w-20 h-28 bg-surface border border-suit-red rounded flex flex-col justify-between p-2 opacity-50">
|
||||||
|
<div class="font-card-rank text-card-rank text-suit-red">K</div>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl text-suit-red" data-icon="favorite" style="font-variation-settings: 'FILL' 1;">favorite</span>
|
||||||
|
</div>
|
||||||
|
<!-- Q-Clubs -->
|
||||||
|
<div class="w-20 h-28 bg-surface border border-outline rounded flex flex-col justify-between p-2 opacity-50">
|
||||||
|
<div class="font-card-rank text-card-rank text-on-surface">Q</div>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl text-on-surface" data-icon="clubs">groups</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- CLI Prompt -->
|
||||||
|
<div class="w-full max-w-sm flex items-center gap-2 font-label-caps text-on-surface py-2">
|
||||||
|
<span class="text-primary">▌</span>
|
||||||
|
<span class="tracking-widest">TAP THE A♠ TO CONTINUE</span>
|
||||||
|
<span class="w-3 h-5 bg-primary animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Feature List -->
|
||||||
|
<div class="w-full max-w-sm space-y-3 pt-2">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||||
|
<span class="font-label-caps text-label-caps">TAP TO AUTO-MOVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||||
|
<span class="font-label-caps text-label-caps">DRAG TO TARGET PILE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-secondary text-sm" data-icon="check_circle" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||||
|
<span class="font-label-caps text-label-caps">DOUBLE-TAP TO FOUNDATION</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Step Indicators -->
|
||||||
|
<div class="flex gap-2 py-4">
|
||||||
|
<div class="w-8 h-1 bg-primary"></div>
|
||||||
|
<div class="w-8 h-1 bg-primary"></div>
|
||||||
|
<div class="w-8 h-1 bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Bottom Action Bar -->
|
||||||
|
<footer class="fixed bottom-0 w-full h-[64px] bg-surface-container border-t border-outline flex items-center px-margin-edge justify-between z-50">
|
||||||
|
<button class="px-6 py-2 border border-outline text-on-surface-variant font-label-caps text-label-caps transition-colors duration-120 active:bg-surface-bright flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-sm" data-icon="arrow_back">arrow_back</span>
|
||||||
|
BACK
|
||||||
|
</button>
|
||||||
|
<button class="px-6 py-2 bg-primary text-on-primary font-label-caps text-label-caps transition-colors duration-120 active:bg-primary-container flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-sm" data-icon="play_arrow" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
|
||||||
|
START PLAYING
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,218 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=390, height=844, 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;800&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": {
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"outline": "#505050",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"surface": "#151515",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"background": "#101417",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"cyan-terminal": "#6fc2ef"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.scanline-pattern {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
#151515,
|
||||||
|
#151515 2px,
|
||||||
|
#1a1a1a 2px,
|
||||||
|
#1a1a1a 4px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.checker-pattern {
|
||||||
|
background-color: #ffffff;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #004c69 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #004c69 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #004c69 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #004c69 75%);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
|
||||||
|
}
|
||||||
|
.stripe-pattern {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
#fb9fb1,
|
||||||
|
#fb9fb1 4px,
|
||||||
|
#151515 4px,
|
||||||
|
#151515 8px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface min-h-screen flex flex-col items-center overflow-hidden selection:bg-cyan-terminal selection:text-surface">
|
||||||
|
<!-- 1. Status Bar -->
|
||||||
|
<header class="w-full h-8 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant">
|
||||||
|
<span class="font-label-caps text-[12px] text-on-surface uppercase tracking-tight">▌onboard/02-theme.tsx</span>
|
||||||
|
<span class="font-label-caps text-[12px] text-[#a0a0a0] uppercase tracking-widest">STEP 2 OF 3</span>
|
||||||
|
</header>
|
||||||
|
<!-- 2. Hero Illustration Band -->
|
||||||
|
<section class="w-full flex flex-col items-center pt-8 pb-4">
|
||||||
|
<div class="h-[100px] flex items-center justify-center relative">
|
||||||
|
<span class="text-cyan-terminal font-headline text-[48px] mr-4 select-none">▌</span>
|
||||||
|
<div class="flex -space-x-4">
|
||||||
|
<div class="w-[24px] h-[34px] border border-outline bg-surface scanline-pattern transform -rotate-12 translate-y-2"></div>
|
||||||
|
<div class="w-[24px] h-[34px] border border-outline bg-surface checker-pattern transform rotate-0 z-10"></div>
|
||||||
|
<div class="w-[24px] h-[34px] border border-outline bg-surface stripe-pattern transform rotate-12 translate-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="font-headline text-[28px] font-bold text-suit-black tracking-tight leading-none">PICK YOUR DECK</h2>
|
||||||
|
</section>
|
||||||
|
<!-- 3. Headline & Description -->
|
||||||
|
<section class="w-full px-margin-edge text-center mb-6">
|
||||||
|
<h3 class="font-headline text-[22px] font-bold text-suit-black mb-1">CHOOSE A CARD-BACK</h3>
|
||||||
|
<p class="font-body-md text-[12px] text-[#a0a0a0] leading-tight">
|
||||||
|
You can swap or import more themes from Settings later.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<!-- 4. Theme Selection Grid -->
|
||||||
|
<main class="w-full px-margin-edge grid grid-cols-3 gap-2 flex-grow max-h-[220px]">
|
||||||
|
<!-- Tile 1: Terminal (Active) -->
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[110/150] bg-surface-container border-2 border-cyan-terminal rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
|
||||||
|
<div class="w-full h-full scanline-pattern border border-outline-variant relative">
|
||||||
|
<div class="absolute top-1 left-1 w-2 h-3 bg-cyan-terminal"></div>
|
||||||
|
<div class="absolute bottom-1 right-1 font-headline text-[10px] text-on-surface opacity-50">▌RS</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-1 right-1 bg-cyan-terminal text-surface w-4 h-4 flex items-center justify-center rounded-full">
|
||||||
|
<span class="material-symbols-outlined text-[12px] font-bold">check</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">TERMINAL</span>
|
||||||
|
</div>
|
||||||
|
<!-- Tile 2: Classic -->
|
||||||
|
<div class="flex flex-col items-center opacity-70">
|
||||||
|
<div class="w-full aspect-[110/150] bg-surface-container border border-outline rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
|
||||||
|
<div class="w-full h-full checker-pattern border border-outline-variant"></div>
|
||||||
|
</div>
|
||||||
|
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">CLASSIC</span>
|
||||||
|
</div>
|
||||||
|
<!-- Tile 3: Stripes -->
|
||||||
|
<div class="flex flex-col items-center opacity-70">
|
||||||
|
<div class="w-full aspect-[110/150] bg-surface-container border border-outline rounded-lg p-3 relative flex items-center justify-center overflow-hidden">
|
||||||
|
<div class="w-full h-full stripe-pattern border border-outline-variant"></div>
|
||||||
|
</div>
|
||||||
|
<span class="mt-2 font-label-caps text-[12px] font-bold text-suit-black tracking-widest uppercase">STRIPES</span>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- 5. More Info -->
|
||||||
|
<div class="w-full text-center mt-4">
|
||||||
|
<span class="font-label-caps text-[11px] font-medium text-[#a0a0a0] tracking-widest">
|
||||||
|
<span class="text-cyan-terminal">+</span> MORE IN SETTINGS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 6. Step Indicator -->
|
||||||
|
<section class="w-full flex flex-col items-center mt-6">
|
||||||
|
<div class="flex gap-1 h-2 mb-2">
|
||||||
|
<div class="w-8 h-1 bg-cyan-terminal rounded-full"></div>
|
||||||
|
<div class="w-8 h-1 bg-cyan-terminal rounded-full"></div>
|
||||||
|
<div class="w-8 h-1 bg-outline rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="font-headline text-[12px] font-medium tracking-[0.2em]">
|
||||||
|
<span class="text-cyan-terminal">[1]</span>
|
||||||
|
<span class="text-cyan-terminal">[2]</span>
|
||||||
|
<span class="text-outline">[3]</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- 7. Bottom Action Bar -->
|
||||||
|
<footer class="fixed bottom-0 w-full h-[64px] bg-surface-container flex items-center justify-between px-margin-edge z-50">
|
||||||
|
<button class="w-[48%] h-12 border border-outline bg-transparent text-suit-black font-label-caps text-[13px] font-medium uppercase rounded-lg active:bg-surface-variant transition-colors">
|
||||||
|
← BACK
|
||||||
|
</button>
|
||||||
|
<button class="w-[48%] h-12 bg-cyan-terminal text-surface font-label-caps text-[14px] font-bold uppercase rounded-lg active:opacity-80 transition-opacity">
|
||||||
|
NEXT →
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
<!-- Image descriptive data for the model (hidden visually) -->
|
||||||
|
<div class="hidden" data-alt="A detailed user interface screen for a retro-terminal themed solitaire game called Rusty Solitaire. The background is a deep black with cyan and gray accents. In the center, a card theme selection grid displays three different card back designs: a scanline pattern, a checker pattern, and a striped pattern. The visual style is crisp, technical, and uses monospaced typography to evoke a command-line interface or professional developer environment. The mood is minimalist, efficient, and technologically nostalgic."></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,212 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Rouge Solitaire - Pause</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700;800&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": {
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"outline": "#505050",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"surface": "#151515",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"tertiary-container": "#e1a3ee"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"gutter-card": "0.375rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.scanline {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(0, 0, 0, 0.05) 50%
|
||||||
|
);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-surface font-body-md overflow-hidden antialiased">
|
||||||
|
<!-- Background Tableau (Simulated by Dimmed Image Overlay) -->
|
||||||
|
<div class="fixed inset-0 z-0">
|
||||||
|
<img alt="Game Tableau Background" class="w-full h-full object-cover" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDJSHHDQ5Y5qul5C_xabnOSM9aS3uxcWSTk47AOHrS_KIlQi0Ur7YhtL0BomjEWTDc8vRLpytWeG4kf5xgxBzpORahTtsWyXOUPsVRg6_H_qp0QjM6DDo57rOPwjU6TFdfK3Pi7cO9rg-xnUSSu1wu29WyKVwSWDDaA5cZ4QN_9L81YMTCTMKAwDTGsY3eGsj1b1i1X2CdF211aepkhmX8xf4bnV35WSB3QuYxUwlPct0Met7iLFf-AGBeizhK6IAboW5u-Wpg8Ag"/>
|
||||||
|
<!-- 95% Opacity Scrim -->
|
||||||
|
<div class="absolute inset-0 bg-surface opacity-95"></div>
|
||||||
|
<!-- Scanline Overlay for Texture -->
|
||||||
|
<div class="absolute inset-0 scanline"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Modal Container -->
|
||||||
|
<div class="fixed inset-0 z-10 flex items-center justify-center p-margin-edge">
|
||||||
|
<!-- Modal Panel -->
|
||||||
|
<div class="w-[330px] h-[480px] bg-[#202020] border border-outline rounded-xl flex flex-col overflow-hidden">
|
||||||
|
<!-- Title Bar -->
|
||||||
|
<div class="h-[28px] bg-[#1a1a1a] border-b border-[#353535] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-primary-container font-headline text-[12px] leading-none mt-px">▌</span>
|
||||||
|
<span class="font-headline text-[12px] text-[#a0a0a0] leading-none">pause.tsx</span>
|
||||||
|
</div>
|
||||||
|
<button class="flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-[16px] text-[#505050]">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Content Canvas -->
|
||||||
|
<div class="flex-1 flex flex-col items-center pt-8 px-4">
|
||||||
|
<!-- Headline -->
|
||||||
|
<h1 class="font-headline text-[24px] font-bold text-[#d0d0d0] tracking-tight text-center">
|
||||||
|
GAME PAUSED
|
||||||
|
</h1>
|
||||||
|
<!-- Subline -->
|
||||||
|
<p class="font-headline text-[12px] text-[#a0a0a0] mt-1 text-center">
|
||||||
|
12:34 ELAPSED · 87 MOVES · DRAW-3
|
||||||
|
</p>
|
||||||
|
<!-- Mini-Stat Chips -->
|
||||||
|
<div class="flex gap-2 mt-4 justify-center">
|
||||||
|
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
|
||||||
|
<span class="font-headline text-[11px] text-[#d0d0d0]">SCORE 247</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
|
||||||
|
<span class="font-headline text-[11px] text-[#d0d0d0]">STOCK 18</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-[#1a1a1a] border border-[#353535] rounded-sm px-2 py-1 flex flex-col items-center">
|
||||||
|
<span class="font-headline text-[11px] text-[#d0d0d0]">MOVES 87</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Action Buttons Cluster -->
|
||||||
|
<div class="w-full mt-6 space-y-3">
|
||||||
|
<!-- Primary CTA -->
|
||||||
|
<button class="w-full h-[48px] bg-primary-container text-surface flex items-center justify-center rounded-lg active:scale-95 transition-transform duration-75">
|
||||||
|
<span class="font-headline text-[14px] font-bold tracking-[0.08em] uppercase">▶ RESUME GAME</span>
|
||||||
|
</button>
|
||||||
|
<!-- Secondary Buttons -->
|
||||||
|
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
|
||||||
|
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">↻ RESTART</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
|
||||||
|
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">✕ FORFEIT</span>
|
||||||
|
</button>
|
||||||
|
<button class="w-full h-[48px] bg-transparent border border-outline text-[#d0d0d0] flex items-center justify-center rounded-lg hover:border-primary-container hover:text-primary-container transition-colors duration-120">
|
||||||
|
<span class="font-headline text-[13px] font-medium tracking-[0.08em] uppercase">⌂ QUIT TO MENU</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer Status Line -->
|
||||||
|
<div class="h-[24px] border-t border-[#353535] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-primary-container font-headline text-[11px]">▌</span>
|
||||||
|
<span class="font-headline text-[11px] text-[#a0a0a0]">NORMAL</span>
|
||||||
|
<span class="text-[#505050] text-[11px]">│</span>
|
||||||
|
<span class="font-headline text-[11px] text-[#a0a0a0]">pause</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 font-headline text-[11px]">
|
||||||
|
<span class="text-[#a0a0a0]">[ESC]</span>
|
||||||
|
<span class="text-[#505050]">resume</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Hidden Navigation Shell (Suppressed due to Task-Focused Modal Context) -->
|
||||||
|
<!-- But included visually as per the brand anchor hierarchy for TopAppBar identity if it were visible -->
|
||||||
|
<header class="hidden fixed top-0 w-full h-action-bar-height flex items-center justify-between px-margin-edge w-full bg-background border-b border-outline-variant">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||||
|
<span class="font-headline text-headline text-primary uppercase tracking-tighter">▌ROUGE_SOLITAIRE</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Bottom Nav Suppression Logic: Not rendered to prioritize the focus canvas -->
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,274 @@
|
|||||||
|
<!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;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"/>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
/* CRT Scanline Overlay Effect */
|
||||||
|
.crt-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
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% 3px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #505050;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"surface": "#151515",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"background": "#101417",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"surface-container": "#202020"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"card-rank": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"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"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-background font-body-md selection:bg-primary selection:text-background overflow-x-hidden">
|
||||||
|
<div class="crt-overlay"></div>
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<header class="bg-surface-container h-[32px] w-full flex items-center justify-between px-margin-edge z-50">
|
||||||
|
<div class="font-headline text-[12px] text-on-surface-variant tracking-wider">
|
||||||
|
▌profile.tsx
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-info"></span>
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface">● SYNCED</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Main Canvas -->
|
||||||
|
<main class="flex-1 overflow-y-auto pb-24">
|
||||||
|
<!-- Profile Header -->
|
||||||
|
<section class="h-[120px] bg-surface-container border-b border-outline-variant flex items-center px-margin-edge gap-4">
|
||||||
|
<div class="w-[64px] h-[64px] bg-[#1a1a1a] border border-outline flex items-center justify-center shrink-0">
|
||||||
|
<span class="font-headline text-[28px] text-primary-container">RS</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1 overflow-hidden">
|
||||||
|
<h1 class="font-headline text-[18px] text-on-surface truncate">anonymous@local</h1>
|
||||||
|
<p class="font-label-caps text-on-surface-variant text-[10px]">MEMBER SINCE 2026-04-22</p>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">247 GAMES</span>
|
||||||
|
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">61% WR</span>
|
||||||
|
<span class="px-2 py-0.5 bg-[#1a1a1a] font-label-caps text-[10px] text-suit-black">12 STREAK</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Level/XP Section -->
|
||||||
|
<section class="p-margin-edge bg-surface-container border-b border-outline-variant">
|
||||||
|
<div class="flex justify-between items-baseline mb-2">
|
||||||
|
<span class="font-headline text-[24px] text-on-surface">LEVEL 12</span>
|
||||||
|
<span class="font-hud-timer text-on-surface-variant">320/500 XP</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-3 w-full bg-[#353535] relative overflow-hidden">
|
||||||
|
<div class="h-full bg-primary-container" style="width: 64%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-3">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-highlight-celebration"></span>
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">180 XP TO LEVEL 13</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Unlocked Cards -->
|
||||||
|
<section class="mt-6">
|
||||||
|
<h2 class="px-margin-edge font-headline text-[14px] text-on-surface mb-4">▌ unlocked.cards</h2>
|
||||||
|
<div class="flex overflow-x-auto gap-4 px-margin-edge pb-4 custom-scrollbar">
|
||||||
|
<!-- Terminal (Active) -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[60px] h-[84px] bg-surface-container-low border-2 border-primary-container relative flex items-center justify-center p-1">
|
||||||
|
<div class="w-full h-full bg-[#151515] overflow-hidden flex flex-col p-1">
|
||||||
|
<div class="w-2 h-2.5 bg-primary-container mb-auto"></div>
|
||||||
|
<div class="self-end text-[8px] font-headline text-on-surface-variant opacity-50">▌RS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[9px] text-primary-container text-center">ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<!-- Classic -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[60px] h-[84px] bg-white border border-outline relative p-1">
|
||||||
|
<div class="w-full h-full border border-red-200 bg-red-50 opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
|
||||||
|
</div>
|
||||||
|
<!-- Stripes -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[60px] h-[84px] bg-surface-container border border-outline p-1">
|
||||||
|
<div class="w-full h-full bg-gradient-to-br from-secondary-container via-surface to-secondary-container opacity-40"></div>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
|
||||||
|
</div>
|
||||||
|
<!-- Polka -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[60px] h-[84px] bg-surface-container border border-outline p-1 overflow-hidden relative">
|
||||||
|
<div class="w-full h-full opacity-30" style="background-image: radial-gradient(#505050 1px, transparent 0); background-size: 6px 6px;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">TAP TO USE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Unlocked Backgrounds -->
|
||||||
|
<section class="mt-6">
|
||||||
|
<h2 class="px-margin-edge font-headline text-[14px] text-on-surface mb-4">▌ unlocked.backgrounds</h2>
|
||||||
|
<div class="flex overflow-x-auto gap-4 px-margin-edge pb-4 custom-scrollbar">
|
||||||
|
<!-- Default (Active) -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[80px] h-[56px] bg-[#151515] border-2 border-primary-container"></div>
|
||||||
|
<span class="font-label-caps text-[9px] text-primary-container text-center">ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<!-- Forest -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[80px] h-[56px] bg-[#0d160d] border border-outline"></div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">FOREST</span>
|
||||||
|
</div>
|
||||||
|
<!-- Slate -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[80px] h-[56px] bg-[#1c2128] border border-outline"></div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">SLATE</span>
|
||||||
|
</div>
|
||||||
|
<!-- Midnight -->
|
||||||
|
<div class="shrink-0 flex flex-col gap-2">
|
||||||
|
<div class="w-[80px] h-[56px] bg-[#09090b] border border-outline"></div>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant text-center opacity-50">MIDNIGHT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Sign-in Card -->
|
||||||
|
<section class="mt-8 px-margin-edge">
|
||||||
|
<button class="w-full h-[64px] bg-surface-container border border-dashed border-outline flex items-center justify-between px-6 hover:bg-surface-variant transition-colors group">
|
||||||
|
<span class="font-label-caps text-on-surface-variant tracking-widest">+ SIGN IN TO SYNC PROGRESS</span>
|
||||||
|
<span class="material-symbols-outlined text-primary-container group-hover:translate-x-1 transition-transform">arrow_forward</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- TopAppBar (from Shared Components - as Terminal Header) -->
|
||||||
|
<div class="fixed top-[32px] left-0 w-full z-40 bg-background border-b border-outline-variant flex items-center justify-between px-margin-edge h-action-bar-height">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-primary">terminal</span>
|
||||||
|
<span class="font-headline text-headline text-primary tracking-tighter">~/root/usr/settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button class="p-2 hover:bg-surface-variant text-primary transition-colors">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- BottomNavBar (from Shared Components - as Terminal Footer) -->
|
||||||
|
<nav class="fixed bottom-0 left-0 w-full z-50 h-[24px] bg-surface-container-lowest border-t border-outline-variant flex justify-between items-center px-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">▌ NORMAL │ profile</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="flex items-center gap-1 group">
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant group-hover:text-primary">[ESC] back</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Decorative CRT Scanline overlay line -->
|
||||||
|
<div class="fixed top-0 left-0 w-full h-[1px] bg-primary opacity-20 pointer-events-none animate-pulse"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 82 KiB |
@@ -0,0 +1,271 @@
|
|||||||
|
<!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;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": {
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"background": "#101417",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"surface": "#151515",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"inverse-surface": "#e0e3e6"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"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"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.radial-segment {
|
||||||
|
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
|
||||||
|
}
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.scanline-bg {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(26, 26, 26, 1) 0px,
|
||||||
|
rgba(26, 26, 26, 1) 2px,
|
||||||
|
rgba(21, 21, 21, 1) 2px,
|
||||||
|
rgba(21, 21, 21, 1) 4px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface font-body-md text-on-surface select-none overflow-hidden h-screen w-screen flex flex-col">
|
||||||
|
<!-- Underlying Game Tableau (Dimmed Background) -->
|
||||||
|
<main class="relative flex-grow opacity-30 grid grid-cols-7 gap-2 p-margin-edge pointer-events-none">
|
||||||
|
<!-- Top row: Foundation/Stock -->
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-dashed border-outline-variant bg-surface-container flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-outline-variant">terminal</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-dashed border-outline-variant"></div>
|
||||||
|
<div class="col-span-1"></div>
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-red-cb flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-suit-red-cb">favorite</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-black flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-suit-black">backspace</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-red-cb flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-suit-red-cb">diamond</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 aspect-[2/3] border border-outline border-suit-black flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-suit-black">spa</span>
|
||||||
|
</div>
|
||||||
|
<!-- Tableau piles -->
|
||||||
|
<div class="col-span-7 grid grid-cols-7 gap-2 mt-4">
|
||||||
|
<div class="space-y-[-120%]">
|
||||||
|
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
|
||||||
|
<span class="font-card-rank text-card-rank text-suit-red-cb">K</span>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-red-cb">diamond</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-[-120%]">
|
||||||
|
<div class="aspect-[2/3] scanline-bg border border-outline relative">
|
||||||
|
<div class="absolute top-1 left-1 w-3 h-4 bg-primary"></div>
|
||||||
|
<span class="absolute bottom-1 right-1 font-label-caps text-[10px] text-primary">▌RS</span>
|
||||||
|
</div>
|
||||||
|
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
|
||||||
|
<span class="font-card-rank text-card-rank text-suit-black">Q</span>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-black" style="font-variation-settings: 'FILL' 1;">spa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-[-120%]">
|
||||||
|
<div class="aspect-[2/3] scanline-bg border border-outline"></div>
|
||||||
|
<div class="aspect-[2/3] scanline-bg border border-outline"></div>
|
||||||
|
<div class="aspect-[2/3] bg-surface-container-high border border-outline p-1 flex flex-col justify-between">
|
||||||
|
<span class="font-card-rank text-card-rank text-suit-red-cb">J</span>
|
||||||
|
<span class="material-symbols-outlined self-end text-3xl rotate-180 text-suit-red-cb" style="font-variation-settings: 'FILL' 1;">favorite</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- More stacks... omitted for brevity as background -->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Radial Menu Overlay -->
|
||||||
|
<div class="fixed inset-0 z-50 bg-[#151515]/70 flex items-center justify-center overflow-hidden">
|
||||||
|
<div class="relative w-[280px] h-[280px] flex items-center justify-center">
|
||||||
|
<!-- Outer Circular Ring Shell -->
|
||||||
|
<div class="absolute inset-0 rounded-full border border-outline bg-surface-container overflow-hidden">
|
||||||
|
<!-- SVG Segments Construction -->
|
||||||
|
<svg class="w-full h-full transform -rotate-22.5" viewbox="0 0 100 100">
|
||||||
|
<!-- Slice 1 (UNDO) - Top / 12:00 -->
|
||||||
|
<!-- Active state: bg-primary-container/15, stroke-primary -->
|
||||||
|
<path d="M 50 50 L 50 0 A 50 50 0 0 1 85.35 14.65 Z" fill="#6fc2ef26" stroke="#6fc2ef" stroke-width="0.5" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 2 (REDO) -->
|
||||||
|
<path d="M 50 50 L 85.35 14.65 A 50 50 0 0 1 100 50 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 3 (HINT) -->
|
||||||
|
<path d="M 50 50 L 100 50 A 50 50 0 0 1 85.35 85.35 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 4 (AUTO) -->
|
||||||
|
<path d="M 50 50 L 85.35 85.35 A 50 50 0 0 1 50 100 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 5 (NEW) -->
|
||||||
|
<path d="M 50 50 L 50 100 A 50 50 0 0 1 14.65 85.35 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 6 (PAUSE) -->
|
||||||
|
<path d="M 50 50 L 14.65 85.35 A 50 50 0 0 1 0 50 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 7 (STATS) -->
|
||||||
|
<path d="M 50 50 L 0 50 A 50 50 0 0 1 14.65 14.65 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
<!-- Slice 8 (SETTINGS) -->
|
||||||
|
<path d="M 50 50 L 14.65 14.65 A 50 50 0 0 1 50 0 Z" fill="transparent" stroke="#353535" stroke-width="0.25" transform="rotate(-22.5, 50, 50)"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- Labels and Icons Overlay -->
|
||||||
|
<div class="absolute inset-0 pointer-events-none">
|
||||||
|
<!-- 12:00 UNDO (ACTIVE) -->
|
||||||
|
<div class="absolute top-[12%] left-1/2 -translate-x-1/2 flex flex-col items-center text-primary">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="undo">undo</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">UNDO</span>
|
||||||
|
</div>
|
||||||
|
<!-- 1:30 REDO -->
|
||||||
|
<div class="absolute top-[22%] right-[12%] flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="redo">redo</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">REDO</span>
|
||||||
|
</div>
|
||||||
|
<!-- 3:00 HINT -->
|
||||||
|
<div class="absolute top-1/2 right-[8%] -translate-y-1/2 flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="lightbulb">lightbulb</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">HINT</span>
|
||||||
|
</div>
|
||||||
|
<!-- 4:30 AUTO -->
|
||||||
|
<div class="absolute bottom-[22%] right-[12%] flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="double_arrow">double_arrow</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">AUTO</span>
|
||||||
|
</div>
|
||||||
|
<!-- 6:00 NEW -->
|
||||||
|
<div class="absolute bottom-[12%] left-1/2 -translate-x-1/2 flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="add">add</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">NEW</span>
|
||||||
|
</div>
|
||||||
|
<!-- 7:30 PAUSE -->
|
||||||
|
<div class="absolute bottom-[22%] left-[12%] flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="pause">pause</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">PAUSE</span>
|
||||||
|
</div>
|
||||||
|
<!-- 9:00 STATS -->
|
||||||
|
<div class="absolute top-1/2 left-[8%] -translate-y-1/2 flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="bar_chart">bar_chart</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">STATS</span>
|
||||||
|
</div>
|
||||||
|
<!-- 10:30 SETTINGS -->
|
||||||
|
<div class="absolute top-[22%] left-[12%] flex flex-col items-center text-on-surface">
|
||||||
|
<span class="material-symbols-outlined text-[24px]" data-icon="settings">settings</span>
|
||||||
|
<span class="font-label-caps text-[11px] mt-1">SETTINGS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Inner Hole -->
|
||||||
|
<div class="absolute w-20 h-20 rounded-full bg-surface-container border border-outline-variant flex flex-col items-center justify-center z-10">
|
||||||
|
<div class="font-headline text-[32px] text-primary leading-none">▌</div>
|
||||||
|
<div class="font-label-caps text-[10px] text-on-surface-variant tracking-widest mt-1">RADIAL</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Instructions (Bottom Floating) -->
|
||||||
|
<div class="absolute bottom-12 left-0 w-full flex flex-col items-center gap-4">
|
||||||
|
<div class="font-label-caps text-[12px] text-on-surface-variant tracking-wider">
|
||||||
|
DRAG TO SELECT · RELEASE TO ACTIVATE
|
||||||
|
</div>
|
||||||
|
<!-- Status Line (Vim style) -->
|
||||||
|
<div class="w-full h-8 bg-surface-container border-t border-outline-variant flex items-center justify-center">
|
||||||
|
<span class="font-label-caps text-[11px] text-on-surface-variant">
|
||||||
|
<span class="text-primary">▌</span> NORMAL │ radial · 1/8 selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Hidden image for standard requirement compliance, though not visually used in this specific overlay task -->
|
||||||
|
<div class="hidden">
|
||||||
|
<img data-alt="A macro shot of a vintage terminal screen displaying green computer code and technical data. The lighting is low-key, with a soft glow emanating from the screen, highlighting the CRT scanlines and subtle reflections. The aesthetic is purely technical and retro-futuristic, focusing on precision and high-contrast digital artifacts. Deep blacks and vibrant green neon tones dominate the color palette, evoking a high-performance system environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAQuJUCOQev_BN72KyX0c-ylmW3DMZD-gOUlylYo3w1SrSpGnvorMvSUwe5oGPAgBgc050cCowC8f1QaxHEDN-DUkyCynOLhzrZHXyCJh2ebCWd6x1quLQwp0ffwbHsZW1-J2zAMuUydMNpEVmpHFQDij0yjVg6lxc6JdsC0etMoAWMhb61S3HUoDffSl-Q23N8Oc77r3dSf6kLFKAMAJCbXFz4nTaJKCKAwtMs62pLr6fd1jzMZrItH43RaO28uzMzvnGGZj3Miw"/>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,284 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Solitaire Replay Overlay</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&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": {
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"outline": "#505050",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface": "#151515",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"on-secondary-container": "#b2c86d"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"gutter-card": "0.375rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"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"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
.card-back-pattern {
|
||||||
|
background-color: #151515;
|
||||||
|
background-image: repeating-linear-gradient(0deg, transparent, transparent 2px, #1a1a1a 2px, #1a1a1a 4px);
|
||||||
|
}
|
||||||
|
/* Custom mechanical transition style */
|
||||||
|
.mechanical-transition {
|
||||||
|
transition: all 120ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface font-body-md select-none overflow-hidden flex flex-col h-[844px] w-[390px] mx-auto border-x border-outline-variant">
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<header class="h-8 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant flex-shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-primary font-headline text-[14px]">▌replay.tsx</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center text-on-surface-variant font-label-caps text-[10px] tracking-wider uppercase">
|
||||||
|
GAME #2024-127 · 87 MOVES
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Game Peek Band (Tableau) -->
|
||||||
|
<main class="h-[240px] relative bg-background overflow-hidden border-b border-outline-variant">
|
||||||
|
<!-- 7-Column Tableau (Dimmed 50%) -->
|
||||||
|
<div class="absolute inset-0 opacity-50 flex justify-around p-2 gap-1">
|
||||||
|
<!-- Tableau Columns 1-7 -->
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] border border-dashed border-outline-variant mb-1"></div>
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col items-center">
|
||||||
|
<div class="w-full aspect-[2/3] bg-surface-container-low border border-outline relative">
|
||||||
|
<!-- Central Focused Card (Move 47) -->
|
||||||
|
<div class="absolute inset-0 z-20 opacity-100">
|
||||||
|
<!-- Shadow-less highlight using glow outline -->
|
||||||
|
<div class="w-full h-full bg-[#1a1a1a] border border-suit-red-cb ring-2 ring-suit-red-cb/40 flex flex-col justify-between p-1">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-card-rank text-card-rank text-suit-red-cb">4</span>
|
||||||
|
<span class="material-symbols-outlined text-[14px] text-suit-red-cb" data-icon="diamond">diamond</span>
|
||||||
|
</div>
|
||||||
|
<div class="self-end rotate-180 flex flex-col">
|
||||||
|
<span class="font-card-rank text-card-rank text-suit-red-cb">4</span>
|
||||||
|
<span class="material-symbols-outlined text-[14px] text-suit-red-cb" data-icon="diamond">diamond</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Move Chip -->
|
||||||
|
<div class="absolute -top-6 left-1/2 -translate-x-1/2 bg-suit-red-cb px-2 py-0.5 rounded-sm">
|
||||||
|
<span class="text-surface font-label-caps text-[9px] font-bold">MOVE 47/87</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Playback Toolbar -->
|
||||||
|
<div class="h-16 bg-surface-container flex items-center justify-between px-4 border-b border-outline-variant">
|
||||||
|
<!-- Left: Timer -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-on-surface font-hud-timer text-[18px] font-bold leading-none">00:42</span>
|
||||||
|
<span class="text-[11px] text-[#a0a0a0] font-label-caps tracking-tighter">/ 02:18</span>
|
||||||
|
</div>
|
||||||
|
<!-- Center: Controls -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="skip_previous">skip_previous</button>
|
||||||
|
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="arrow_left">arrow_left</button>
|
||||||
|
<button class="material-symbols-outlined text-suit-red-cb text-[32px] mechanical-transition" data-icon="play_arrow">play_arrow</button>
|
||||||
|
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="arrow_right">arrow_right</button>
|
||||||
|
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary mechanical-transition" data-icon="skip_next">skip_next</button>
|
||||||
|
</div>
|
||||||
|
<!-- Right: Speed -->
|
||||||
|
<div class="flex items-center bg-surface-variant border border-outline px-2 py-1 gap-1">
|
||||||
|
<span class="font-label-caps text-[14px] font-bold text-on-surface">1.0x</span>
|
||||||
|
<span class="material-symbols-outlined text-[16px] text-on-surface-variant" data-icon="unfold_more">unfold_more</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Scrub Bar Area -->
|
||||||
|
<div class="px-margin-edge pt-6 pb-8 bg-surface-container-low border-b border-outline-variant">
|
||||||
|
<div class="relative w-full h-1 bg-outline rounded-full">
|
||||||
|
<!-- Cyan Progress Track -->
|
||||||
|
<div class="absolute left-0 top-0 h-full bg-suit-red-cb" style="width: 54%;"></div>
|
||||||
|
<!-- Notches & Labels -->
|
||||||
|
<div class="absolute inset-0 flex justify-between">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
|
||||||
|
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
|
||||||
|
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">25%</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
|
||||||
|
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">50%</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
|
||||||
|
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">75%</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-[1px] h-3 bg-outline -mt-1"></div>
|
||||||
|
<span class="absolute -bottom-5 left-1/2 -translate-x-1/2 text-[9px] font-label-caps text-outline">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Current Marker (54%) -->
|
||||||
|
<div class="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-suit-red-cb border border-surface" style="left: 54%;"></div>
|
||||||
|
<div class="absolute -top-4 left-[54%] -translate-x-1/2 text-[10px] text-suit-red-cb font-label-caps font-bold">47/87</div>
|
||||||
|
<!-- Win Marker (72%) -->
|
||||||
|
<div class="absolute top-0 w-[2px] h-3 bg-highlight-valid -translate-y-1" style="left: 72%;"></div>
|
||||||
|
<div class="absolute -bottom-5 left-[72%] -translate-x-1/2 text-[8px] text-highlight-valid font-label-caps font-bold whitespace-nowrap">WIN MOVE</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Move Log Card -->
|
||||||
|
<section class="flex-1 bg-surface-container p-4 overflow-y-auto">
|
||||||
|
<h3 class="font-label-caps text-label-caps text-on-surface-variant mb-4 flex items-center gap-2">
|
||||||
|
<span class="w-1.5 h-3 bg-primary block"></span>
|
||||||
|
MOVE LOG · 47/87
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-col font-label-caps text-[12px]">
|
||||||
|
<!-- Log Rows -->
|
||||||
|
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
|
||||||
|
<span class="w-8">44 |</span>
|
||||||
|
<span>5♥ → tableau col 3</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
|
||||||
|
<span class="w-8">45 |</span>
|
||||||
|
<span>8♣ → tableau col 1</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center h-6 px-2 text-[#a0a0a0] border-b border-outline-variant/30">
|
||||||
|
<span class="w-8">46 |</span>
|
||||||
|
<span>stock cycle</span>
|
||||||
|
</div>
|
||||||
|
<!-- Highlighted Active Move -->
|
||||||
|
<div class="flex items-center h-6 px-2 bg-suit-red-cb text-surface-container font-bold">
|
||||||
|
<span class="w-8">▶ 47 |</span>
|
||||||
|
<span>4♦ → 5♣ on col 7</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center h-6 px-2 text-on-surface border-b border-outline-variant/30">
|
||||||
|
<span class="w-8">48 |</span>
|
||||||
|
<span class="material-symbols-outlined text-[14px] align-middle mr-1" data-icon="foundation">foundation</span> A♠ → foundation
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center h-6 px-2 text-on-surface border-b border-outline-variant/30">
|
||||||
|
<span class="w-8">49 |</span>
|
||||||
|
<span class="material-symbols-outlined text-[14px] align-middle mr-1" data-icon="foundation">foundation</span> 2♠ → foundation
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="h-6 bg-surface-container flex items-center justify-between px-4 border-t border-outline-variant flex-shrink-0 text-[10px] font-label-caps tracking-wider">
|
||||||
|
<div class="text-on-surface-variant">
|
||||||
|
<span class="text-primary">▌</span> NORMAL │ replay
|
||||||
|
</div>
|
||||||
|
<div class="text-on-surface-variant opacity-70">
|
||||||
|
[SPACE] play · [← →] scrub · [ESC]
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Overlay Background (For visualization of depth) -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none border-[16px] border-surface-dim/40 z-50"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,258 @@
|
|||||||
|
<!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;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": {
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"background": "#101417",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"surface": "#151515",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"outline": "#505050",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"error-container": "#93000a"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"top-action-bar-height": "32px",
|
||||||
|
"bottom-action-bar-height": "24px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
.card-back-pattern {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
#151515,
|
||||||
|
#151515 2px,
|
||||||
|
#1a1a1a 2px,
|
||||||
|
#1a1a1a 4px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-background font-body-md selection:bg-primary selection:text-on-primary-container overflow-hidden flex flex-col h-[844px] w-[390px] mx-auto border-x border-outline-variant">
|
||||||
|
<!-- Status Bar / TopAppBar Logic -->
|
||||||
|
<header class="bg-[#202020] h-8 flex items-center justify-between px-4 w-full z-50">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-primary font-headline text-[12px]">▌</span>
|
||||||
|
<span class="text-on-surface font-label-caps text-[12px]">settings.toml</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-outline font-label-caps text-[12px]">v0.20.0</div>
|
||||||
|
</header>
|
||||||
|
<!-- NavigationDrawer (Tab Strip Mapping) -->
|
||||||
|
<nav class="bg-[#1a1a1a] h-10 border-b border-[#353535] flex items-center w-full">
|
||||||
|
<!-- COSMETIC Tab (Active) -->
|
||||||
|
<div class="h-full flex-1 flex flex-col items-center justify-center relative">
|
||||||
|
<span class="text-primary font-label-caps text-[12px] px-2">[ COSMETIC ]</span>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 h-[2px] bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
<!-- GAMEPLAY Tab -->
|
||||||
|
<div class="h-full flex-1 flex items-center justify-center">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[12px]">GAMEPLAY</span>
|
||||||
|
</div>
|
||||||
|
<!-- SYNC Tab -->
|
||||||
|
<div class="h-full flex-1 flex items-center justify-center">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[12px]">SYNC</span>
|
||||||
|
</div>
|
||||||
|
<!-- AUDIO Tab -->
|
||||||
|
<div class="h-full flex-1 flex items-center justify-center">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[12px]">AUDIO</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Content Area (Canvas) -->
|
||||||
|
<main class="flex-1 bg-[#151515] p-margin-edge overflow-y-auto space-y-2">
|
||||||
|
<!-- row: card_theme -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">card_theme</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">Terminal</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-primary material-symbols-outlined" data-icon="arrow_forward">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
<!-- row: background -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">background</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">Solid #151515</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-primary material-symbols-outlined" data-icon="arrow_forward">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
<!-- row: card_back -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">card_back</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">Terminal</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-4 h-6 border border-outline card-back-pattern relative overflow-hidden">
|
||||||
|
<div class="absolute top-0.5 left-0.5 w-1.5 h-2 bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-primary material-symbols-outlined" data-icon="arrow_forward">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- row: color_blind_mode -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">color_blind_mode</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">false</span>
|
||||||
|
</div>
|
||||||
|
<!-- Toggle OFF -->
|
||||||
|
<div class="w-10 h-5 bg-[#202020] border border-outline rounded-full relative">
|
||||||
|
<div class="absolute left-0.5 top-0.5 w-3.5 h-3.5 bg-outline rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- row: high_contrast -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">high_contrast</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">false</span>
|
||||||
|
</div>
|
||||||
|
<!-- Toggle OFF -->
|
||||||
|
<div class="w-10 h-5 bg-[#202020] border border-outline rounded-full relative">
|
||||||
|
<div class="absolute left-0.5 top-0.5 w-3.5 h-3.5 bg-outline rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- row: reduce_motion -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">reduce_motion</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">true</span>
|
||||||
|
</div>
|
||||||
|
<!-- Toggle ON -->
|
||||||
|
<div class="w-10 h-5 bg-[#1f3a4a] border border-primary/50 rounded-full relative">
|
||||||
|
<div class="absolute right-0.5 top-0.5 w-3.5 h-3.5 bg-primary-container rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- row: crt_scanline_effect -->
|
||||||
|
<div class="bg-[#202020] h-14 rounded-[4px] px-3 flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-[#a0a0a0] font-label-caps text-[11px]">crt_scanline_effect</span>
|
||||||
|
<span class="text-suit-black font-label-caps text-[14px]">false</span>
|
||||||
|
</div>
|
||||||
|
<!-- Toggle OFF -->
|
||||||
|
<div class="w-10 h-5 bg-[#202020] border border-outline rounded-full relative">
|
||||||
|
<div class="absolute left-0.5 top-0.5 w-3.5 h-3.5 bg-outline rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- BottomNavBar (Footer Logic) -->
|
||||||
|
<footer class="h-6 bg-[#202020] border-t border-[#353535] flex items-center justify-between px-4 w-full z-50">
|
||||||
|
<div class="flex items-center gap-1 font-label-caps text-[11px]">
|
||||||
|
<span class="text-primary">▌</span>
|
||||||
|
<span class="text-suit-black">NORMAL</span>
|
||||||
|
<span class="text-outline">│</span>
|
||||||
|
<span class="text-on-surface">settings</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 font-label-caps text-[11px]">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-[#a0a0a0]">[1-4]</span>
|
||||||
|
<span class="text-outline ml-1">tab</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-[#a0a0a0]">[ESC]</span>
|
||||||
|
<span class="text-outline ml-1">back</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,213 @@
|
|||||||
|
<!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 - Terminal Edition</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&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": {
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"background": "#101417",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"surface": "#151515",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"boot-cyan": "#a1dcff",
|
||||||
|
"boot-lime": "#acc267"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"gutter-card": "0.375rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #151515;
|
||||||
|
color: #d0d0d0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
.cyan-halo {
|
||||||
|
box-shadow: 0 0 40px 4px rgba(161, 220, 255, 0.15);
|
||||||
|
}
|
||||||
|
.scanlines {
|
||||||
|
background: linear-gradient(to bottom, rgba(21, 21, 21, 0) 50%, rgba(0, 0, 0, 0.2) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex flex-col items-center justify-between font-body-md w-[390px] h-[844px] mx-auto relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 scanlines opacity-30 z-10"></div>
|
||||||
|
<!-- Top Safe Area (Blank) -->
|
||||||
|
<div class="h-10 w-full"></div>
|
||||||
|
<!-- Main Content Stack -->
|
||||||
|
<main class="flex-1 w-full flex flex-col items-center z-20">
|
||||||
|
<!-- Header Section (~30% Top) -->
|
||||||
|
<div class="mt-[10%] flex flex-col items-center">
|
||||||
|
<div class="w-24 h-24 flex items-center justify-center relative mb-4">
|
||||||
|
<div class="text-[96px] text-boot-cyan font-headline cyan-halo leading-none">▌</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<h1 class="font-headline text-[32px] font-bold text-on-surface tracking-tight mb-1">RUSTY SOLITAIRE</h1>
|
||||||
|
<div class="w-48 h-[1px] bg-outline-variant"></div>
|
||||||
|
<span class="font-label-caps text-[12px] text-outline mt-2 tracking-[0.1em] uppercase">TERMINAL EDITION</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Terminal Boot Log (~60% Top Position) -->
|
||||||
|
<div class="mt-48 w-[70%] flex flex-col items-start font-hud-timer text-[11px] space-y-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-boot-lime">✓</span>
|
||||||
|
<span class="text-outline">assets loaded</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-boot-lime">✓</span>
|
||||||
|
<span class="text-outline">theme: terminal</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-boot-lime">✓</span>
|
||||||
|
<span class="text-outline">progress restored</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 mt-1">
|
||||||
|
<span class="text-on-surface-variant">▌ ready_</span>
|
||||||
|
<span class="w-[6px] h-3 bg-boot-cyan animate-pulse"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Progress Bar (~75% Top Position) -->
|
||||||
|
<div class="mt-20 w-full px-8">
|
||||||
|
<div class="w-full h-[1px] bg-surface-container-highest relative">
|
||||||
|
<div class="absolute top-0 left-0 h-full bg-boot-cyan w-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full text-right mt-2">
|
||||||
|
<span class="font-label-caps text-[10px] text-outline tracking-wider uppercase">DONE · 247 ASSETS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Bottom Strip & Footer -->
|
||||||
|
<footer class="w-full flex flex-col items-center z-20 pb-10">
|
||||||
|
<div class="flex flex-col items-center mb-8">
|
||||||
|
<span class="font-label-caps text-[9px] text-outline mb-3 tracking-widest uppercase">BASE16-EIGHTIES</span>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<div class="w-3 h-3 bg-[#fb9fb1]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#ddb26f]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#acc267]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#12cfc0]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#6fc2ef]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#e1a3ee]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#d0d0d0]"></div>
|
||||||
|
<div class="w-3 h-3 bg-[#505050]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="font-hud-timer text-[11px] text-outline">
|
||||||
|
v0.20.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Top App Bar Content (Hidden as per requirement, but following Shared Components Logic) -->
|
||||||
|
<div class="hidden">
|
||||||
|
<header class="fixed top-0 w-full z-50 flex justify-between items-center px-margin-edge h-action-bar-height border-b border-outline-variant bg-background">
|
||||||
|
<div class="font-headline text-headline text-primary uppercase tracking-tighter">ROOT@SOLITAIRE:~</div>
|
||||||
|
<div class="flex gap-4 text-primary">
|
||||||
|
<span class="material-symbols-outlined">memory</span>
|
||||||
|
<span class="material-symbols-outlined">settings_ethernet</span>
|
||||||
|
<span class="material-symbols-outlined">wifi_tethering</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<!-- Placeholder for data-alt requirements even if visual images are minimal -->
|
||||||
|
<div class="hidden">
|
||||||
|
<img data-alt="A cinematic, low-angle digital render of a retro-terminal interface appearing on a high-end mobile display in a dark room. The screen glows with a soft cyan light, illuminating a minimalist layout with sharp, 1px white lines and a large block cursor symbol. The atmosphere is quiet, technical, and high-performance, reflecting a dark-mode synthwave aesthetic with muted grays and vibrant cyan accents. The focus is on the crisp, high-density typography and the mechanical precision of the digital environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuBeZymuyGd_-VJr-zgC8p08qBLD4pk0WyRtxuIhVT5kY6cc3y_qSkZ-P_EYYwKIliGysN5rDgqbCsXLxksfslVnB4nj4BYktu4d5EAKi1zEQ8t8MId17UzIgKujbGqebDo0FWO51Snqxt9AvrjX_afEsvACaaeAyIfTKgoAB8MBOUnanIre26Y1tNTftn1y9jxKfrXgi9eCYiJn6zoiaRmNmdLwo_s7RenmSlloPdIURVb3KKHKaBZHldPaStbWcyMYNR877R6O_A"/>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,279 @@
|
|||||||
|
<!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;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;
|
||||||
|
}
|
||||||
|
/* CRT Scanline effect overlay */
|
||||||
|
.crt-overlay::before {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.02), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.02));
|
||||||
|
z-index: 100;
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface": "#151515",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"background": "#101417",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"outline": "#505050",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"gutter-card": "0.375rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"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"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface text-on-surface font-body-md min-h-screen selection:bg-primary-container selection:text-on-primary-container">
|
||||||
|
<!-- Top Navigation Shell -->
|
||||||
|
<header class="fixed top-0 w-full bg-background z-50 border-b border-outline flex justify-between items-center px-margin-edge h-action-bar-height">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-primary" data-icon="terminal">terminal</span>
|
||||||
|
<h1 class="font-headline text-headline text-primary uppercase tracking-tighter">STATISTICS.LOG</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container-highest transition-colors duration-120 p-2 rounded" data-icon="settings_input_component">settings_input_component</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="pt-action-bar-height pb-[88px] crt-overlay">
|
||||||
|
<!-- Status Bar (Emulated Retro Style) -->
|
||||||
|
<div class="h-[32px] bg-surface-container flex items-center justify-between px-margin-edge text-[11px] font-hud-timer">
|
||||||
|
<span class="text-on-surface">▌stats.log</span>
|
||||||
|
<span class="text-on-surface-variant">247 GAMES TRACKED</span>
|
||||||
|
</div>
|
||||||
|
<!-- Sub-tab Strip -->
|
||||||
|
<nav class="h-[40px] bg-[#1a1a1a] border-b border-outline flex items-center px-margin-edge overflow-x-auto whitespace-nowrap scrollbar-hide">
|
||||||
|
<div class="flex items-center h-full gap-4">
|
||||||
|
<button class="h-full px-1 flex flex-col justify-center font-label-caps text-[11px] text-primary-container border-b-2 border-primary-container">
|
||||||
|
[ OVERVIEW ]
|
||||||
|
</button>
|
||||||
|
<button class="h-full px-1 flex flex-col justify-center font-label-caps text-[11px] text-on-surface-variant hover:text-primary transition-colors">
|
||||||
|
DRAW-1
|
||||||
|
</button>
|
||||||
|
<button class="h-full px-1 flex flex-col justify-center font-label-caps text-[11px] text-on-surface-variant hover:text-primary transition-colors">
|
||||||
|
DRAW-3
|
||||||
|
</button>
|
||||||
|
<button class="h-full px-1 flex flex-col justify-center font-label-caps text-[11px] text-on-surface-variant hover:text-primary transition-colors">
|
||||||
|
DAILY
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Main Content Grid -->
|
||||||
|
<section class="p-4 flex flex-col gap-4">
|
||||||
|
<!-- Hero Stat Card -->
|
||||||
|
<div class="h-24 bg-surface-container border border-outline rounded-lg p-4 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-headline text-[48px] text-on-surface">61%</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">WIN RATE</p>
|
||||||
|
<p class="font-hud-timer text-[13px] text-highlight-valid">▲ +3% vs last 30</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 2x2 Grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline rounded-lg p-3 flex flex-col justify-between">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">GAMES</p>
|
||||||
|
<p class="font-hud-score text-hud-score">247</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline rounded-lg p-3 flex flex-col justify-between">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">WINS</p>
|
||||||
|
<p class="font-hud-score text-hud-score">151</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline rounded-lg p-3 flex flex-col justify-between">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">BEST TIME</p>
|
||||||
|
<p class="font-hud-score text-hud-score text-primary">01:54</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline rounded-lg p-3 flex flex-col justify-between">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">STREAK</p>
|
||||||
|
<p class="font-hud-score text-hud-score">12</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Sparkline Card -->
|
||||||
|
<div class="bg-surface-container border border-outline rounded-lg p-4 h-[200px] flex flex-col">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant mb-4">WIN RATE · LAST 30 DAYS</p>
|
||||||
|
<div class="relative flex-1 flex items-end justify-between border-b border-outline pb-1">
|
||||||
|
<!-- Y-Axis Labels -->
|
||||||
|
<span class="absolute top-0 left-0 text-[9px] font-hud-timer text-outline">100%</span>
|
||||||
|
<span class="absolute bottom-1 left-0 text-[9px] font-hud-timer text-outline">0%</span>
|
||||||
|
<!-- Pixel Bar Chart -->
|
||||||
|
<div class="flex items-end justify-between w-full h-[80%] gap-[2px]">
|
||||||
|
<!-- Generated bars with upward trend -->
|
||||||
|
<div class="bg-primary-container w-full" style="height: 35%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 30%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 40%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 38%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 45%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 42%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 50%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 48%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 55%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 52%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 58%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 60%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 55%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 62%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 65%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 63%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 68%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 70%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 65%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 72%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 75%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 78%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 74%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 80%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 82%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 85%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 88%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 86%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 90%"></div>
|
||||||
|
<div class="bg-primary-container w-full" style="height: 95%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-1">
|
||||||
|
<span class="text-[9px] font-hud-timer text-outline uppercase">30d ago</span>
|
||||||
|
<span class="text-[9px] font-hud-timer text-outline uppercase">today</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Draw-Mode Split Card -->
|
||||||
|
<div class="h-20 bg-surface-container border border-outline rounded-lg p-4 flex flex-col justify-between">
|
||||||
|
<p class="font-label-caps text-[11px] text-on-surface-variant">DRAW MODE SPLIT</p>
|
||||||
|
<div class="h-3 w-full flex rounded-sm overflow-hidden bg-outline">
|
||||||
|
<div class="h-full bg-highlight-valid" style="width: 60%"></div>
|
||||||
|
<div class="h-full bg-primary-container" style="width: 40%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="font-hud-timer text-[11px] text-highlight-valid">DRAW-1 · 60%</span>
|
||||||
|
<span class="font-hud-timer text-[11px] text-primary-container">DRAW-3 · 40%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Retro Footer Strip -->
|
||||||
|
<footer class="h-[24px] px-margin-edge flex items-center justify-between font-hud-timer text-[10px] text-outline mt-2">
|
||||||
|
<div>▌ NORMAL │ stats</div>
|
||||||
|
<div>[1-4] view</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
<!-- Bottom Navigation Shell -->
|
||||||
|
<nav class="fixed bottom-0 w-full z-50 h-[64px] bg-surface-container border-t border-outline flex justify-around items-center">
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant pt-1 hover:text-primary transition-colors duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="playing_cards">playing_cards</span>
|
||||||
|
<span class="font-label-caps text-label-caps">DECK</span>
|
||||||
|
</div>
|
||||||
|
<!-- ACTIVE TAB: STATS -->
|
||||||
|
<div class="flex flex-col items-center justify-center text-primary border-t-2 border-primary pt-1 hover:text-primary transition-colors duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="query_stats" style="font-variation-settings: 'FILL' 1;">query_stats</span>
|
||||||
|
<span class="font-label-caps text-label-caps">STATS</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant pt-1 hover:text-primary transition-colors duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||||
|
<span class="font-label-caps text-label-caps">LOGS</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant pt-1 hover:text-primary transition-colors duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="terminal">terminal</span>
|
||||||
|
<span class="font-label-caps text-label-caps">SYS</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,262 @@
|
|||||||
|
<!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 - Sync Progress</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&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": {
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"outline": "#505050",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"surface": "#151515",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"background": "#101417",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"surface-bright": "#363a3d"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "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"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.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="bg-surface text-on-surface font-body-md min-h-screen flex flex-col overflow-x-hidden selection:bg-primary-container selection:text-on-primary-container">
|
||||||
|
<!-- 1. Status Bar (Top) -->
|
||||||
|
<nav class="h-8 bg-[#202020] flex items-center justify-between px-4 border-b border-outline-variant z-50">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-headline text-[13px] tracking-tight text-on-surface-variant">▌sync.config</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-warning animate-pulse"></span>
|
||||||
|
<span class="font-label-caps text-[11px] text-warning">PENDING</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[11px] text-outline">v0.20.0</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Shared TopAppBar Component -->
|
||||||
|
<header class="bg-surface flex justify-between items-center w-full px-margin-edge h-action-bar-height max-w-full border-b border-outline-variant">
|
||||||
|
<div class="font-headline text-headline text-primary tracking-tighter uppercase">▌RS_TERMINAL_OS</div>
|
||||||
|
<div class="hidden md:flex gap-6 items-center">
|
||||||
|
<a class="font-label-caps text-label-caps uppercase tracking-widest text-on-surface-variant hover:text-primary transition-colors duration-120" href="#">PLAY</a>
|
||||||
|
<a class="font-label-caps text-label-caps uppercase tracking-widest text-on-surface-variant hover:text-primary transition-colors duration-120" href="#">DAILY</a>
|
||||||
|
<a class="font-label-caps text-label-caps uppercase tracking-widest text-primary border-b-2 border-primary pb-1" href="#">STATS</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<span class="material-symbols-outlined text-primary cursor-pointer">account_circle</span>
|
||||||
|
<span class="material-symbols-outlined text-primary cursor-pointer">sync</span>
|
||||||
|
<span class="material-symbols-outlined text-primary cursor-pointer">settings</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex-grow p-4 md:p-8 max-w-3xl mx-auto w-full space-y-6">
|
||||||
|
<!-- 2. Header Area -->
|
||||||
|
<div class="h-20 flex flex-col justify-center border-l-2 border-outline-variant pl-4">
|
||||||
|
<h1 class="font-headline text-[24px] font-bold text-suit-black leading-tight">SYNC PROGRESS</h1>
|
||||||
|
<p class="font-body-md text-[12px] text-[#a0a0a0]">Connect to a server to sync games across devices.</p>
|
||||||
|
</div>
|
||||||
|
<!-- 3. Status Card -->
|
||||||
|
<section class="bg-[#202020] rounded-[4px] p-4 border-l-[4px] border-warning flex flex-col gap-1">
|
||||||
|
<h2 class="font-label-caps text-[10px] text-[#a0a0a0] uppercase tracking-widest">STATUS</h2>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-headline text-[16px] font-bold text-[#a0a0a0]">○ NOT SIGNED IN</span>
|
||||||
|
<span class="text-[11px] text-outline font-medium">Local progress only · Last attempt: never</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- 4. Login Form Card -->
|
||||||
|
<section class="bg-[#202020] border border-outline-variant rounded-[4px] p-4 flex flex-col gap-5">
|
||||||
|
<div class="flex items-center gap-2 border-b border-outline-variant pb-2 mb-1">
|
||||||
|
<span class="font-headline text-[12px] text-[#a0a0a0] flex items-center">
|
||||||
|
▌ AUTH.toml <span class="ml-1 w-1.5 h-3 bg-primary-container animate-pulse"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Server URL -->
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<label class="font-label-caps text-[10px] text-[#a0a0a0] uppercase tracking-widest">server_url</label>
|
||||||
|
<div class="h-12 bg-[#1a1a1a] border border-outline-variant rounded-[2px] flex items-center px-4">
|
||||||
|
<span class="font-headline text-[13px] text-suit-black">https://sync.rusty-solitaire.app</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<label class="font-label-caps text-[10px] text-[#a0a0a0] uppercase tracking-widest">email</label>
|
||||||
|
<div class="h-12 bg-[#1a1a1a] border border-outline-variant rounded-[2px] flex items-center px-4">
|
||||||
|
<span class="font-headline text-[13px] text-outline">/ user@example.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Passcode -->
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<label class="font-label-caps text-[10px] text-[#a0a0a0] uppercase tracking-widest">passcode</label>
|
||||||
|
<div class="h-12 bg-[#1a1a1a] border border-outline-variant rounded-[2px] flex items-center justify-between px-4">
|
||||||
|
<span class="font-headline text-[13px] text-outline">•••••••• (12 chars)</span>
|
||||||
|
<span class="material-symbols-outlined text-outline cursor-pointer text-[18px]">visibility</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- 5. Form Actions -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button class="h-12 bg-primary-container text-surface flex items-center justify-center rounded-[2px] font-headline text-[14px] font-bold active:scale-95 transition-transform">
|
||||||
|
▶ SIGN IN
|
||||||
|
</button>
|
||||||
|
<button class="h-12 border border-outline text-suit-black flex items-center justify-center rounded-[2px] font-headline text-[13px] font-medium hover:border-primary-container hover:text-primary-container transition-all">
|
||||||
|
+ CREATE ACCOUNT
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 6. Recent History Panel -->
|
||||||
|
<section class="bg-[#202020] rounded-[4px] p-4 flex flex-col gap-3">
|
||||||
|
<h2 class="font-label-caps text-[12px] text-[#a0a0a0] uppercase tracking-widest">RECENT</h2>
|
||||||
|
<div class="space-y-2 border-l border-outline-variant pl-4">
|
||||||
|
<div class="font-headline text-[11px] text-[#a0a0a0] flex items-center gap-2">
|
||||||
|
<span class="text-outline">2026-05-07 17:38</span>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<span>○ no auth</span>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<span>skip</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-headline text-[11px] text-[#a0a0a0] flex items-center gap-2">
|
||||||
|
<span class="text-outline">2026-05-07 14:12</span>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<span>○ no auth</span>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<span>skip</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-headline text-[11px] text-[#a0a0a0] flex items-center gap-2">
|
||||||
|
<span class="text-outline">2026-05-06 09:01</span>
|
||||||
|
<span class="text-outline">·</span>
|
||||||
|
<span class="text-highlight-valid">✓ synced 12 games</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- 7. Footer -->
|
||||||
|
<footer class="h-10 bg-surface border-t border-outline-variant flex items-center justify-between px-4 fixed bottom-0 w-full z-50">
|
||||||
|
<div class="flex items-center gap-2 font-headline text-[11px]">
|
||||||
|
<span class="text-primary-container">▌ NORMAL</span>
|
||||||
|
<span class="text-outline">│</span>
|
||||||
|
<span class="text-on-surface-variant">sync</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 font-headline text-[11px] text-outline uppercase tracking-wider">
|
||||||
|
<span>[ENTER] <span class="text-on-surface-variant">sign in</span></span>
|
||||||
|
<span>[ESC] <span class="text-on-surface-variant">cancel</span></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Shared BottomNavBar Component (Mobile Only) -->
|
||||||
|
<nav class="md:hidden fixed bottom-10 w-full z-50 flex justify-around items-center h-action-bar-height px-margin-edge bg-surface-container border-t border-outline-variant">
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="videogame_asset">videogame_asset</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase tracking-widest mt-1">F1_NEW_GAME</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="event_upcoming">event_upcoming</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase tracking-widest mt-1">F2_CHALLENGE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-primary dark:text-primary-fixed-dim bg-surface-container-highest rounded-none p-2 transition-all duration-120 scale-95">
|
||||||
|
<span class="material-symbols-outlined" data-icon="query_stats">query_stats</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase tracking-widest mt-1">F5_STATS</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined" data-icon="power_settings_new">power_settings_new</span>
|
||||||
|
<span class="font-label-caps text-[10px] uppercase tracking-widest mt-1">ESC_EXIT</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Decorative Screen Texture -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none opacity-[0.03] scanline z-[100]"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,250 @@
|
|||||||
|
<!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;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
|
||||||
|
}
|
||||||
|
.scanline-pattern {
|
||||||
|
background: repeating-linear-gradient(0deg, #1a1a1a, #1a1a1a 2px, #151515 2px, #151515 4px)
|
||||||
|
}
|
||||||
|
.checker-pattern {
|
||||||
|
background-color: #001e2c;
|
||||||
|
background-image: radial-gradient(#a1dcff 10%, transparent 10%), radial-gradient(#a1dcff 10%, transparent 10%);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
background-position: 0 0, 4px 4px
|
||||||
|
}
|
||||||
|
.stripe-pattern {
|
||||||
|
background: repeating-linear-gradient(0deg, #fb9fb1, #fb9fb1 4px, #202020 4px, #202020 8px)
|
||||||
|
}
|
||||||
|
.polka-pattern {
|
||||||
|
background-color: #001e2c;
|
||||||
|
background-image: radial-gradient(#e0e3e6 15%, transparent 15%);
|
||||||
|
background-size: 12px 12px
|
||||||
|
}
|
||||||
|
.vintage-pattern {
|
||||||
|
background-color: #d5ec8c;
|
||||||
|
background-image: url(https://lh3.googleusercontent.com/aida-public/AB6AXuD8S9vQTpEh-DjYtjB5CUHqi2CO326ZEjVVLJoOqG1AA6b92NZ6ctGoD4yZHKV7oHJnSFdvp3z3Wei9zfTI2EGAdrQfHxFYJ-h1DaeiZQY3vTa5khIQ83Sf-bjz2xiudHsjs3RyhSKC5bHv2c8_9t6YjepJdQnJa4GelCetFEs_agpN6u2IfMS1M9RrGxGKLl4K18fj0Pg3BW8IptX_ladhVFR5Hk8F0Reu5WHY8eQt1Nr-p9NNXl-w3C9Jz0uGSxi_Wb7R771lgQ);
|
||||||
|
opacity: 0.8
|
||||||
|
}</style>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"surface-container": "#1c2023",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"surface": "#151515",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"tertiary-container": "#e1a3ee"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"margin-edge": "1rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-surface selection:bg-primary-container selection:text-on-primary-container min-h-screen flex flex-col font-body-md overflow-x-hidden">
|
||||||
|
<!-- TopAppBar Semantic Shell -->
|
||||||
|
<header class="fixed top-0 w-full z-50 flex justify-between items-center h-[32px] px-margin-edge bg-surface-container border-b border-outline-variant">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined text-[16px] text-primary" data-icon="terminal">terminal</span>
|
||||||
|
<span class="font-headline text-[14px] font-bold text-on-surface">▌theme.picker</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity">
|
||||||
|
<span class="font-label-caps text-[12px] text-on-surface-variant uppercase">× CLOSE</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="mt-[32px] mb-[64px] flex-grow flex flex-col px-margin-edge py-6">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="flex justify-between items-start mb-8">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="font-headline text-[24px] font-bold text-on-surface mb-1">CARD THEMES</h1>
|
||||||
|
<p class="font-body-md text-[13px] text-on-surface-variant max-w-[280px]">Choose a card-face theme. Imported themes appear at the bottom.</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-surface-container-high px-2 py-1 border border-outline-variant">
|
||||||
|
<span class="font-label-caps text-[11px] text-primary">5 INSTALLED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme Grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Active Theme: Terminal -->
|
||||||
|
<div class="relative flex flex-col bg-surface border-2 border-primary-container">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full p-2 overflow-hidden flex items-center justify-center bg-background">
|
||||||
|
<!-- Card Preview -->
|
||||||
|
<div class="w-24 h-36 bg-surface border border-primary-container scanline-pattern relative">
|
||||||
|
<div class="absolute top-1 left-1 w-3 h-4 bg-primary-container"></div>
|
||||||
|
<div class="absolute bottom-1 right-1 font-headline text-[10px] text-on-surface">▌RS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-high border-t border-primary-container">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span class="font-headline text-[14px] font-bold truncate">Terminal</span>
|
||||||
|
<span class="font-label-caps text-[10px] text-primary-container">✓ ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-body-md text-[11px] text-on-surface-variant">by Rusty Solitaire</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme: Classic -->
|
||||||
|
<div class="relative flex flex-col bg-surface border border-outline">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full p-2 overflow-hidden flex items-center justify-center bg-background">
|
||||||
|
<div class="w-24 h-36 bg-surface border border-outline checker-pattern"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-low border-t border-outline">
|
||||||
|
<span class="font-headline text-[14px] font-bold block truncate">Classic</span>
|
||||||
|
<span class="font-body-md text-[11px] text-on-surface-variant">by Rusty Solitaire</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme: Stripes -->
|
||||||
|
<div class="relative flex flex-col bg-surface border border-outline">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full p-2 overflow-hidden flex items-center justify-center bg-background">
|
||||||
|
<div class="w-24 h-36 bg-surface border border-outline stripe-pattern"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-low border-t border-outline">
|
||||||
|
<span class="font-headline text-[14px] font-bold block truncate">Stripes</span>
|
||||||
|
<span class="font-body-md text-[11px] text-on-surface-variant">by hayeah</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme: Polka -->
|
||||||
|
<div class="relative flex flex-col bg-surface border border-outline">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full p-2 overflow-hidden flex items-center justify-center bg-background">
|
||||||
|
<div class="w-24 h-36 bg-surface border border-outline polka-pattern"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-low border-t border-outline">
|
||||||
|
<span class="font-headline text-[14px] font-bold block truncate">Polka</span>
|
||||||
|
<span class="font-body-md text-[11px] text-on-surface-variant">by hayeah</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Theme: Vintage -->
|
||||||
|
<div class="relative flex flex-col bg-surface border border-outline">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full p-2 overflow-hidden flex items-center justify-center bg-background">
|
||||||
|
<div class="w-24 h-36 bg-surface border border-outline vintage-pattern"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-low border-t border-outline">
|
||||||
|
<span class="font-headline text-[14px] font-bold block truncate">Vintage</span>
|
||||||
|
<span class="font-body-md text-[11px] text-on-surface-variant">by hayeah</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Import Theme -->
|
||||||
|
<div class="relative flex flex-col bg-surface border-2 border-dashed border-outline-variant hover:border-primary-container transition-colors cursor-pointer">
|
||||||
|
<div class="aspect-[2.5/3.5] w-full flex flex-col items-center justify-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-[32px] text-primary-container" data-icon="add">add</span>
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant tracking-widest text-center px-4">IMPORT FROM .ZIP</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-surface-container-low border-t border-outline-variant">
|
||||||
|
<span class="font-headline text-[14px] font-bold block truncate">+ IMPORT THEME</span>
|
||||||
|
<span class="font-body-md text-[11px] opacity-0">spacer</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- BottomNavBar Semantic Shell -->
|
||||||
|
<footer class="fixed bottom-0 left-0 w-full h-[64px] z-50 bg-surface-container border-t border-outline-variant flex justify-between items-center px-margin-edge pb-safe">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-headline text-[14px] font-bold text-on-surface">▌ NORMAL</span>
|
||||||
|
<span class="text-on-surface-variant">│</span>
|
||||||
|
<span class="font-label-caps text-[12px] text-on-surface-variant">theme</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-on-surface-variant font-label-caps text-[11px]">[ENTER]</span>
|
||||||
|
<span class="text-on-surface-variant font-body-md text-[11px] ml-1">activate</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-on-surface-variant font-label-caps text-[11px]">[I]</span>
|
||||||
|
<span class="text-on-surface-variant font-body-md text-[11px] ml-1">import</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-on-surface-variant font-label-caps text-[11px]">[ESC]</span>
|
||||||
|
<span class="text-on-surface-variant font-body-md text-[11px] ml-1">back</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,215 @@
|
|||||||
|
<!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 - Time Attack Configuration</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&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": {
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"surface": "#151515",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-surface": "#d0d0d0",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"outline": "#505050",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"primary-fixed": "#c4e7ff"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"touch-target-min": "48px",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"hud-timer": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"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"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body { background-color: #151515; color: #d0d0d0; }
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
.scanline {
|
||||||
|
background: linear-gradient(to bottom, rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen">
|
||||||
|
<!-- Mobile Container (390x844) -->
|
||||||
|
<div class="w-[390px] h-[844px] bg-surface relative flex flex-col overflow-hidden border border-outline-variant">
|
||||||
|
<!-- STATUS BAR -->
|
||||||
|
<header class="h-8 bg-surface-container flex items-center justify-between px-4 z-10">
|
||||||
|
<span class="font-headline text-[12px] tracking-tight text-primary">▌time-attack.tsx</span>
|
||||||
|
<span class="font-label-caps text-[10px] text-on-surface-variant">MODE · TIMED</span>
|
||||||
|
</header>
|
||||||
|
<!-- TOP APP BAR (from JSON) -->
|
||||||
|
<nav class="flex justify-between items-center w-full px-margin-edge h-action-bar-height max-w-full bg-surface text-primary font-headline text-headline font-bold border-b border-outline-variant">
|
||||||
|
<div class="font-headline text-headline text-primary tracking-tighter uppercase">▌RS_TERMINAL_OS</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<span class="material-symbols-outlined cursor-pointer hover:text-primary-fixed transition-colors duration-120">account_circle</span>
|
||||||
|
<span class="material-symbols-outlined cursor-pointer hover:text-primary-fixed transition-colors duration-120">sync</span>
|
||||||
|
<span class="material-symbols-outlined cursor-pointer hover:text-primary-fixed transition-colors duration-120">settings</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- MAIN CONTENT -->
|
||||||
|
<main class="flex-1 px-4 py-4 flex flex-col gap-4 overflow-y-auto">
|
||||||
|
<!-- HERO BAND -->
|
||||||
|
<section class="flex flex-col items-center justify-center h-[100px] text-center">
|
||||||
|
<h1 class="font-headline text-[32px] font-bold tracking-tighter text-on-surface uppercase">TIME ATTACK</h1>
|
||||||
|
<p class="font-body-md text-[12px] text-on-surface-variant max-w-[280px]">Race the clock. The faster you finish, the higher your score.</p>
|
||||||
|
</section>
|
||||||
|
<!-- TIMER DISPLAY -->
|
||||||
|
<section class="w-full h-[120px] bg-surface-dim border border-outline-variant flex flex-col items-center justify-center relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 scanline opacity-10 pointer-events-none"></div>
|
||||||
|
<div class="font-headline text-[64px] font-bold tracking-tight text-primary tabular-nums leading-none">05:00</div>
|
||||||
|
<div class="font-label-caps text-[11px] uppercase tracking-[0.2em] text-on-surface-variant mt-2">MINUTES</div>
|
||||||
|
</section>
|
||||||
|
<!-- DURATION PICKER -->
|
||||||
|
<section class="grid grid-cols-4 gap-px bg-outline-variant border border-outline-variant">
|
||||||
|
<button class="h-12 bg-surface-container font-label-caps text-[12px] text-on-surface hover:bg-surface-bright transition-all">1 MIN</button>
|
||||||
|
<button class="h-12 bg-surface-container font-label-caps text-[12px] text-on-surface hover:bg-surface-bright transition-all">3 MIN</button>
|
||||||
|
<button class="h-12 bg-primary text-on-primary font-bold font-label-caps text-[12px]">5 MIN</button>
|
||||||
|
<button class="h-12 bg-surface-container font-label-caps text-[12px] text-on-surface hover:bg-surface-bright transition-all">10 MIN</button>
|
||||||
|
</section>
|
||||||
|
<!-- RULES CARD -->
|
||||||
|
<section class="bg-surface-container h-20 p-3 border border-outline-variant flex flex-col justify-between">
|
||||||
|
<div class="font-label-caps text-[10px] text-on-surface-variant uppercase tracking-widest">RULES</div>
|
||||||
|
<div class="font-headline text-[12px] text-on-surface flex items-center gap-2">
|
||||||
|
<span class="w-1 h-1 bg-primary"></span> DRAW-3
|
||||||
|
<span class="w-1 h-1 bg-primary ml-2"></span> NO HINT PENALTY
|
||||||
|
<span class="w-1 h-1 bg-primary ml-2"></span> +50 XP / WIN
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- BEST RUN CARD -->
|
||||||
|
<section class="bg-surface-container h-16 p-3 border border-outline-variant flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="font-label-caps text-[10px] text-on-surface-variant uppercase tracking-widest">PERSONAL BEST · 5 MIN</div>
|
||||||
|
<div class="font-headline text-[24px] font-bold text-on-surface leading-tight">02:47 <span class="text-[12px] font-normal text-on-surface-variant">WIN</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end">
|
||||||
|
<div class="font-label-caps text-[10px] text-on-surface-variant">GLOBAL RANK 142</div>
|
||||||
|
<div class="bg-warning text-on-surface text-[9px] px-1.5 py-0.5 font-bold mt-1">TOP 5%</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- PRIMARY CTA -->
|
||||||
|
<section class="mt-auto pt-4">
|
||||||
|
<button class="w-full h-20 bg-primary text-on-primary flex flex-col items-center justify-center transition-all active:scale-[0.98] duration-80">
|
||||||
|
<div class="font-headline text-[18px] font-extrabold uppercase tracking-[0.2em] flex items-center gap-2">
|
||||||
|
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">play_arrow</span> BEGIN COUNTDOWN
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<p class="font-body-md text-[11px] text-on-surface-variant text-center mt-3">Game starts after a 3-second countdown.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<footer class="h-6 bg-surface-container-lowest flex items-center justify-between px-4 border-t border-outline-variant">
|
||||||
|
<span class="font-headline text-[10px] text-on-surface-variant">▌ NORMAL │ time-attack</span>
|
||||||
|
<span class="font-label-caps text-[9px] text-on-surface-variant uppercase">[ENTER] begin · [ESC] back</span>
|
||||||
|
</footer>
|
||||||
|
<!-- BOTTOM NAV BAR (from JSON) -->
|
||||||
|
<nav class="fixed bottom-0 w-[390px] z-50 flex justify-around items-center h-action-bar-height px-margin-edge bg-surface-container border-t border-outline-variant">
|
||||||
|
<div class="flex flex-col items-center justify-center text-primary bg-surface-container-highest rounded-none p-2 transition-transform duration-80 active:scale-95">
|
||||||
|
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">videogame_asset</span>
|
||||||
|
<span class="font-label-caps text-label-caps uppercase tracking-widest">F1_NEW_GAME</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined">event_upcoming</span>
|
||||||
|
<span class="font-label-caps text-label-caps uppercase tracking-widest">F2_CHALLENGE</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined">query_stats</span>
|
||||||
|
<span class="font-label-caps text-label-caps uppercase tracking-widest">F5_STATS</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:bg-surface-bright transition-all duration-120">
|
||||||
|
<span class="material-symbols-outlined">power_settings_new</span>
|
||||||
|
<span class="font-label-caps text-label-caps uppercase tracking-widest">ESC_EXIT</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- PADDING FOR FIXED NAV -->
|
||||||
|
<div class="h-action-bar-height"></div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html class="dark" lang="en"><head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Weekly Goals - 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&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"/>
|
||||||
|
<script id="tailwind-config">
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
"colors": {
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"on-primary": "#003549",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"primary-container": "#6fc2ef",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"outline": "#505050",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"error": "#fb9fb1",
|
||||||
|
"suit-black": "#d0d0d0",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"primary": "#a1dcff",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"suit-red": "#fb9fb1",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"on-surface-variant": "#bfc8cf",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"tertiary-fixed": "#fbd7ff",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"surface-container-low": "#181c1f",
|
||||||
|
"background": "#101417",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"on-primary-fixed": "#001e2c",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"tertiary": "#f7c3ff",
|
||||||
|
"outline-variant": "#3f484e",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"surface": "#151515",
|
||||||
|
"on-error": "#690005"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"stack-overlap": "2rem",
|
||||||
|
"action-bar-height": "64px"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||||
|
::-webkit-scrollbar { width: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: #151515; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #353535; border-radius: 2px; }
|
||||||
|
|
||||||
|
/* Scanline Overlay Effect */
|
||||||
|
.crt-overlay {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; bottom: 0; right: 0;
|
||||||
|
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% 3px, 3px 100%;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-surface font-body-md selection:bg-primary-container selection:text-on-primary-container overflow-hidden h-screen flex flex-col">
|
||||||
|
<!-- Top Status Bar -->
|
||||||
|
<div class="h-[32px] bg-surface flex items-center justify-between px-margin-edge z-50 shrink-0">
|
||||||
|
<div class="font-label-caps text-[12px] text-[#a0a0a0] flex items-center">
|
||||||
|
<span class="mr-1">▌</span>weekly-goals.json
|
||||||
|
</div>
|
||||||
|
<div class="bg-warning/10 text-warning px-2 py-0.5 rounded-sm flex items-center gap-1.5">
|
||||||
|
<span class="material-symbols-outlined text-[14px]">timer</span>
|
||||||
|
<span class="font-headline text-[10px] font-bold tabular-nums tracking-wider uppercase">RESETS IN 2D 14H</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Main Header Band -->
|
||||||
|
<header class="h-[80px] px-margin-edge flex flex-col justify-center border-b border-outline/20 shrink-0">
|
||||||
|
<h1 class="font-headline text-[24px] font-bold text-suit-black tracking-tight leading-none">WEEKLY GOALS</h1>
|
||||||
|
<p class="font-body-md text-[12px] text-[#a0a0a0] mt-1">Complete goals before reset to claim XP and rewards.</p>
|
||||||
|
</header>
|
||||||
|
<!-- Main Content Canvas -->
|
||||||
|
<main class="flex-1 overflow-y-auto px-margin-edge py-4 space-y-4">
|
||||||
|
<!-- Overall Progress Card -->
|
||||||
|
<section class="h-[80px] bg-surface-container border border-outline/30 rounded-[4px] p-4 flex flex-col justify-between">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-label-caps text-[10px] text-[#a0a0a0] tracking-widest uppercase">OVERALL · 3/5</span>
|
||||||
|
<span class="font-headline text-[12px] text-highlight-celebration tabular-nums">(60%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[6px] bg-[#353535] rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-highlight-celebration w-[60%]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="font-label-caps text-[11px] text-highlight-valid flex items-center gap-1">
|
||||||
|
<span class="material-symbols-outlined text-[14px]">stars</span>
|
||||||
|
+220 XP CLAIMED
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Goal List -->
|
||||||
|
<div class="space-y-2 pb-16">
|
||||||
|
<!-- Goal 1: COMPLETED -->
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline/30 rounded-[4px] p-3 flex flex-col justify-between">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="font-headline text-[14px] font-bold text-suit-black">PLAY 10 GAMES</h3>
|
||||||
|
<div class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-sm font-label-caps text-[11px]">+50 XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full">
|
||||||
|
<div class="h-full bg-highlight-valid w-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center font-label-caps text-[10px]">
|
||||||
|
<span class="text-[#a0a0a0]">10/10 GAMES</span>
|
||||||
|
<span class="text-highlight-valid flex items-center gap-1">✓ CLAIMED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Goal 2: COMPLETED -->
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline/30 rounded-[4px] p-3 flex flex-col justify-between">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="font-headline text-[14px] font-bold text-suit-black">WIN 5 DAILY SEEDS</h3>
|
||||||
|
<div class="bg-highlight-celebration/10 text-highlight-celebration px-2 py-0.5 rounded-sm font-label-caps text-[11px]">+100 XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full">
|
||||||
|
<div class="h-full bg-highlight-valid w-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center font-label-caps text-[10px]">
|
||||||
|
<span class="text-[#a0a0a0]">5/5 DONE</span>
|
||||||
|
<span class="text-highlight-valid flex items-center gap-1">✓ CLAIMED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Goal 3: IN PROGRESS -->
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline/30 rounded-[4px] p-3 flex flex-col justify-between border-l-2 border-l-primary-container">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="font-headline text-[14px] font-bold text-suit-black">WIN UNDER 4:00 (3 TIMES)</h3>
|
||||||
|
<div class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-sm font-label-caps text-[11px]">+75 XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full">
|
||||||
|
<div class="h-full bg-primary-container w-[66%]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center font-label-caps text-[10px]">
|
||||||
|
<span class="text-[#a0a0a0]">2/3</span>
|
||||||
|
<span class="text-primary-container flex items-center gap-1">▶ IN PROGRESS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Goal 4: NOT STARTED -->
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline/30 rounded-[4px] p-3 flex flex-col justify-between opacity-70">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="font-headline text-[14px] font-bold text-suit-black">PERFECT GAME (NO UNDO)</h3>
|
||||||
|
<div class="bg-warning/10 text-warning px-2 py-0.5 rounded-sm font-label-caps text-[11px]">+150 XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full">
|
||||||
|
<div class="h-full bg-outline w-0"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center font-label-caps text-[10px]">
|
||||||
|
<span class="text-[#a0a0a0]">0/1</span>
|
||||||
|
<span class="text-outline flex items-center gap-1">○ NOT STARTED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Goal 5: IN PROGRESS -->
|
||||||
|
<div class="h-[88px] bg-surface-container border border-outline/30 rounded-[4px] p-3 flex flex-col justify-between border-l-2 border-l-primary-container">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="font-headline text-[14px] font-bold text-suit-black">STREAK OF 5 WINS</h3>
|
||||||
|
<div class="bg-highlight-valid/10 text-highlight-valid px-2 py-0.5 rounded-sm font-label-caps text-[11px]">+50 XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-[4px] bg-[#353535] rounded-full">
|
||||||
|
<div class="h-full bg-primary-container w-[60%]"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center font-label-caps text-[10px]">
|
||||||
|
<span class="text-[#a0a0a0]">3/5</span>
|
||||||
|
<span class="text-primary-container flex items-center gap-1">▶ IN PROGRESS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Navigation Shell from JSON -->
|
||||||
|
<nav class="fixed bottom-0 left-0 w-full z-50 flex justify-around items-center px-margin-edge bg-surface-container border-t border-outline-variant h-action-bar-height">
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors duration-120 active:scale-95 transition-transform duration-80">
|
||||||
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors duration-120 active:scale-95 transition-transform duration-80">
|
||||||
|
<span class="material-symbols-outlined">undo</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-primary active:scale-95 transition-transform duration-80">
|
||||||
|
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">style</span>
|
||||||
|
</button>
|
||||||
|
<button class="flex flex-col items-center justify-center text-on-surface-variant hover:text-primary transition-colors duration-120 active:scale-95 transition-transform duration-80">
|
||||||
|
<span class="material-symbols-outlined">help_outline</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<!-- Bottom Terminal Status Footer -->
|
||||||
|
<footer class="fixed bottom-[64px] left-0 w-full h-[24px] bg-surface flex items-center justify-between px-margin-edge z-50 border-t border-outline/10">
|
||||||
|
<div class="font-label-caps text-[10px] text-[#a0a0a0]">
|
||||||
|
<span class="text-primary">▌</span> NORMAL │ weekly
|
||||||
|
</div>
|
||||||
|
<div class="font-label-caps text-[10px] flex gap-3">
|
||||||
|
<span class="text-[#505050]"><span class="text-[#a0a0a0]">[C]</span> claim all</span>
|
||||||
|
<span class="text-[#505050]"><span class="text-[#a0a0a0]">[ESC]</span> back</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- CRT Overlay -->
|
||||||
|
<div class="crt-overlay"></div>
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 84 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"/>
|
||||||
|
<title>ROOT@SOLITAIRE:~ | Win Summary</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": {
|
||||||
|
"primary-fixed": "#c4e7ff",
|
||||||
|
"inverse-surface": "#e0e3e6",
|
||||||
|
"on-tertiary": "#4c195b",
|
||||||
|
"suit-red-cb": "#6fc2ef",
|
||||||
|
"secondary-fixed-dim": "#bad073",
|
||||||
|
"on-secondary-fixed-variant": "#3c4d00",
|
||||||
|
"on-secondary-fixed": "#161e00",
|
||||||
|
"surface-container-highest": "#313538",
|
||||||
|
"primary-fixed-dim": "#7ed0fe",
|
||||||
|
"on-secondary": "#293500",
|
||||||
|
"on-background": "#e0e3e6",
|
||||||
|
"highlight-celebration": "#e1a3ee",
|
||||||
|
"warning": "#ddb26f",
|
||||||
|
"on-tertiary-fixed-variant": "#653173",
|
||||||
|
"background": "#101417",
|
||||||
|
"surface-container-lowest": "#0b0f11",
|
||||||
|
"info": "#12cfc0",
|
||||||
|
"tertiary-container": "#e1a3ee",
|
||||||
|
"surface-container": "#202020",
|
||||||
|
"secondary": "#bad073",
|
||||||
|
"outline": "#505050",
|
||||||
|
"tertiary-fixed-dim": "#f0b0fc",
|
||||||
|
"secondary-container": "#435401",
|
||||||
|
"inverse-primary": "#00668a",
|
||||||
|
"surface": "#151515",
|
||||||
|
"on-error-container": "#ffdad6",
|
||||||
|
"error-container": "#93000a",
|
||||||
|
"surface-bright": "#363a3d",
|
||||||
|
"surface-dim": "#101417",
|
||||||
|
"on-primary-fixed-variant": "#004c69",
|
||||||
|
"on-tertiary-fixed": "#340043",
|
||||||
|
"inverse-on-surface": "#2d3134",
|
||||||
|
"surface-container-high": "#272a2d",
|
||||||
|
"secondary-fixed": "#d5ec8c",
|
||||||
|
"on-tertiary-container": "#683476",
|
||||||
|
"on-secondary-container": "#b2c86d",
|
||||||
|
"surface-tint": "#7ed0fe",
|
||||||
|
"on-primary-container": "#004f6c",
|
||||||
|
"on-error": "#690005",
|
||||||
|
"on-surface": "#e0e3e6",
|
||||||
|
"surface-variant": "#313538",
|
||||||
|
"highlight-valid": "#acc267",
|
||||||
|
"primary": "#a1dcff"
|
||||||
|
},
|
||||||
|
"borderRadius": {
|
||||||
|
"DEFAULT": "0.125rem",
|
||||||
|
"lg": "0.25rem",
|
||||||
|
"xl": "0.5rem",
|
||||||
|
"full": "0.75rem"
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"margin-edge": "1rem",
|
||||||
|
"action-bar-height": "64px",
|
||||||
|
"gutter-card": "0.375rem",
|
||||||
|
"touch-target-min": "48dp",
|
||||||
|
"stack-overlap": "2rem"
|
||||||
|
},
|
||||||
|
"fontFamily": {
|
||||||
|
"body-md": ["Inter"],
|
||||||
|
"label-caps": ["JetBrains Mono"],
|
||||||
|
"card-rank": ["JetBrains Mono"],
|
||||||
|
"hud-timer": ["JetBrains Mono"],
|
||||||
|
"headline": ["JetBrains Mono"],
|
||||||
|
"hud-score": ["JetBrains Mono"]
|
||||||
|
},
|
||||||
|
"fontSize": {
|
||||||
|
"body-md": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.08em", "fontWeight": "500"}],
|
||||||
|
"card-rank": ["18px", {"lineHeight": "18px", "fontWeight": "700"}],
|
||||||
|
"hud-timer": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
|
||||||
|
"headline": ["28px", {"lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
||||||
|
"hud-score": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.crt-scanlines {
|
||||||
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.02), rgba(0, 255, 0, 0.01), rgba(0, 0, 255, 0.02));
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.lavender-glow {
|
||||||
|
text-shadow: 0 0 16px rgba(225, 163, 238, 0.3);
|
||||||
|
}
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
min-height: max(884px, 100dvh);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-background text-on-background font-body-md selection:bg-primary-container selection:text-on-primary-container min-h-screen flex flex-col relative overflow-hidden">
|
||||||
|
<!-- CRT Overlay -->
|
||||||
|
<div class="fixed inset-0 crt-scanlines z-[100] opacity-30"></div>
|
||||||
|
<!-- Status Bar (Emulating TopAppBar context) -->
|
||||||
|
<header class="h-[32px] bg-surface-container flex items-center justify-between px-margin-edge z-50 border-b border-outline-variant">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-label-caps text-[11px] text-on-surface">▌win.tsx</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="w-2 h-2 rounded-full bg-info"></span>
|
||||||
|
<span class="font-label-caps text-[11px] text-info uppercase">Synced</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-label-caps text-[11px] text-outline uppercase tracking-tight">v0.20.0</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Content Canvas -->
|
||||||
|
<main class="flex-1 flex flex-col px-margin-edge pt-12 pb-8 gap-8 relative z-10">
|
||||||
|
<!-- Hero Band -->
|
||||||
|
<section class="flex flex-col items-center text-center space-y-2">
|
||||||
|
<h1 class="font-headline text-[48px] leading-tight text-highlight-celebration uppercase lavender-glow tracking-tighter">
|
||||||
|
█ COMPLETE
|
||||||
|
</h1>
|
||||||
|
<p class="font-label-caps text-[12px] text-outline-variant tracking-[0.2em]">
|
||||||
|
GAME #2024-127 · DRAW-3
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<!-- Stats Card -->
|
||||||
|
<section class="bg-surface-container border border-outline-variant rounded-lg overflow-hidden p-6 grid grid-cols-2 gap-y-8 gap-x-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="block font-label-caps text-outline uppercase text-[10px]">Final Score</span>
|
||||||
|
<span class="block font-hud-score text-on-background">1,024</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="block font-label-caps text-outline uppercase text-[10px]">Time</span>
|
||||||
|
<span class="block font-hud-timer text-on-background">12:34</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="block font-label-caps text-outline uppercase text-[10px]">Moves</span>
|
||||||
|
<span class="block font-hud-timer text-on-background">87</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="block font-label-caps text-outline uppercase text-[10px]">Par Delta</span>
|
||||||
|
<span class="block font-hud-timer text-highlight-valid">−13</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Achievement Card -->
|
||||||
|
<section class="bg-surface-container border-l-2 border-highlight-celebration rounded-r-lg p-4 flex items-center justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<span class="font-label-caps text-highlight-celebration text-[10px] flex items-center gap-1">
|
||||||
|
<span class="text-[8px]">▲</span> ACHIEVEMENT UNLOCKED
|
||||||
|
</span>
|
||||||
|
<p class="font-label-caps text-suit-black uppercase tracking-wider text-[14px]">FIRST DAILY WIN</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 rounded-full border border-outline-variant flex items-center justify-center bg-surface-container-low">
|
||||||
|
<span class="material-symbols-outlined text-highlight-celebration text-2xl" data-icon="military_tech">military_tech</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mt-auto space-y-3">
|
||||||
|
<button class="w-full h-[56px] bg-info text-surface font-label-caps text-[14px] font-bold tracking-widest flex items-center justify-center gap-2 hover:opacity-90 active:opacity-80 transition-all uppercase">
|
||||||
|
<span class="text-lg">▶</span> New Game
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button class="h-[48px] border border-outline text-on-surface-variant font-label-caps text-[12px] flex items-center justify-center gap-2 hover:border-info hover:text-info transition-colors uppercase">
|
||||||
|
<span class="text-sm">↺</span> Replay Seed
|
||||||
|
</button>
|
||||||
|
<button class="h-[48px] border border-outline text-on-surface-variant font-label-caps text-[12px] flex items-center justify-center gap-2 hover:border-info hover:text-info transition-colors uppercase">
|
||||||
|
<span class="text-sm">⌂</span> Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<!-- Footer Keys -->
|
||||||
|
<footer class="h-action-bar-height flex flex-col items-center justify-center px-margin-edge border-t border-outline-variant bg-surface-container-low">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="font-label-caps text-outline-variant">[ S ]</span>
|
||||||
|
<span class="font-label-caps text-outline uppercase text-[10px]">share screenshot</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="font-label-caps text-outline-variant">[ X ]</span>
|
||||||
|
<span class="font-label-caps text-outline uppercase text-[10px]">copy seed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Bottom Nav Suppression Logic: This is a task-focused confirmation screen, so the global BottomNavBar from JSON is suppressed to focus on primary actions. -->
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -8,8 +8,68 @@ edition.workspace = true
|
|||||||
name = "solitaire_app"
|
name = "solitaire_app"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
# `cdylib` is what cargo-apk packages into `libsolitaire_app.so` for
|
||||||
|
# Android — the activity dlopens the shared object and calls into it.
|
||||||
|
# `rlib` lets the bin target above link the library normally on
|
||||||
|
# desktop. Both produce the same code; only the linkage form differs.
|
||||||
|
[lib]
|
||||||
|
name = "solitaire_app"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
solitaire_engine = { workspace = true }
|
solitaire_engine = { workspace = true }
|
||||||
solitaire_data = { workspace = true }
|
solitaire_data = { workspace = true }
|
||||||
|
|
||||||
|
# `keyring`'s default-store init only matters on platforms with a
|
||||||
|
# real keychain backend (Linux Secret Service, macOS Keychain,
|
||||||
|
# Windows Credential Store). The crate also pulls `rpassword`
|
||||||
|
# transitively, which uses `libc::__errno_location` — a symbol
|
||||||
|
# Android's bionic doesn't expose. Target-gating keeps
|
||||||
|
# `cargo apk build` viable; the call site in `lib.rs` has its own
|
||||||
|
# `cfg(not(target_os = "android"))` guard so the desktop init path
|
||||||
|
# is unchanged.
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
keyring = { workspace = true }
|
keyring = { workspace = true }
|
||||||
|
|
||||||
|
# --- Android packaging metadata (read by `cargo-apk`) -------------------
|
||||||
|
#
|
||||||
|
# Pinning these values inside the repo means a contributor running
|
||||||
|
# `cargo apk build -p solitaire_app --target x86_64-linux-android`
|
||||||
|
# does not need to install whatever SDK version cargo-apk happens to
|
||||||
|
# default to today. The numbers track the SDK we install in the dev
|
||||||
|
# setup script: target SDK 34 (Android 14, current Play Store target),
|
||||||
|
# min SDK 26 (Android 8, the lowest Bevy 0.18 supports cleanly with
|
||||||
|
# the wgpu / GLES path).
|
||||||
|
#
|
||||||
|
# Asset path is `../assets` so the same directory the desktop build
|
||||||
|
# already uses ships into the APK without copy-tree gymnastics.
|
||||||
|
# `apk_name` keeps the output filename predictable across machines.
|
||||||
|
[package.metadata.android]
|
||||||
|
package = "com.solitairequest.app"
|
||||||
|
apk_name = "solitaire-quest"
|
||||||
|
build_targets = ["aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
|
||||||
|
assets = "../assets"
|
||||||
|
# No `runtime_libs` — we don't ship any precompiled .so files,
|
||||||
|
# the entire app is pure Rust + Bevy. cargo-apk would try to
|
||||||
|
# resolve `runtime_libs/<arch>/` if set, and fail on a non-existent
|
||||||
|
# arch directory under our package.
|
||||||
|
strip = "strip"
|
||||||
|
|
||||||
|
[package.metadata.android.sdk]
|
||||||
|
target_sdk_version = 34
|
||||||
|
min_sdk_version = 26
|
||||||
|
|
||||||
|
[[package.metadata.android.uses_feature]]
|
||||||
|
name = "android.hardware.touchscreen"
|
||||||
|
required = true
|
||||||
|
|
||||||
|
[[package.metadata.android.uses_permission]]
|
||||||
|
name = "android.permission.INTERNET"
|
||||||
|
|
||||||
|
[package.metadata.android.application]
|
||||||
|
label = "Solitaire Quest"
|
||||||
|
# `debuggable` defaults to false on release builds; cargo-apk flips it
|
||||||
|
# automatically for debug profiles. Leaving the field unset keeps the
|
||||||
|
# default behaviour.
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
//! Library entry point for `solitaire_app`.
|
||||||
|
//!
|
||||||
|
//! The app is a `cdylib + bin` hybrid: desktop builds run through the
|
||||||
|
//! `bin` target's [`main`](crate::main_desktop) shim; Android builds
|
||||||
|
//! load this `cdylib` via NativeActivity / GameActivity, which calls
|
||||||
|
//! into the platform's own `main` glue. Both paths converge on
|
||||||
|
//! [`run`], so the ECS bootstrap is single-sourced.
|
||||||
|
//!
|
||||||
|
//! Why split this out: cargo-apk requires the package to expose a
|
||||||
|
//! `cdylib` library target — the Android activity dlopens
|
||||||
|
//! `libsolitaire_app.so` and calls into it. A bin-only crate panics
|
||||||
|
//! at build time with `Bin is not compatible with Cdylib`. The split
|
||||||
|
//! keeps the desktop `cargo run -p solitaire_app` flow unchanged
|
||||||
|
//! while making `cargo apk build -p solitaire_app` viable.
|
||||||
|
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::window::{
|
||||||
|
Monitor, MonitorSelection, PresentMode, PrimaryMonitor, PrimaryWindow, WindowPosition,
|
||||||
|
};
|
||||||
|
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
||||||
|
use solitaire_engine::{
|
||||||
|
register_theme_asset_sources, AchievementPlugin, AnimationPlugin, AssetSourcesPlugin,
|
||||||
|
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||||
|
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, FeedbackAnimPlugin, FontPlugin,
|
||||||
|
GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||||||
|
OnboardingPlugin, PausePlugin, ProfilePlugin, ProgressPlugin, RadialMenuPlugin,
|
||||||
|
ReplayOverlayPlugin, ReplayPlaybackPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin,
|
||||||
|
StatsPlugin, SyncPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
||||||
|
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// App entry point — builds and runs the Bevy app.
|
||||||
|
///
|
||||||
|
/// Called from both the desktop `bin` target's `main` shim and (on
|
||||||
|
/// Android) the platform's NativeActivity / GameActivity glue.
|
||||||
|
pub fn run() {
|
||||||
|
// Install a panic hook that writes a crash log next to the save files
|
||||||
|
// before re-running the default hook (so stderr still gets the message
|
||||||
|
// and any debugger attached still sees the panic).
|
||||||
|
install_crash_log_hook();
|
||||||
|
|
||||||
|
// Initialise the platform keyring store before any token operations.
|
||||||
|
// On Linux this uses the Secret Service (GNOME Keyring / KWallet); on
|
||||||
|
// macOS it uses the Keychain; on Windows it uses the Credential store.
|
||||||
|
// If the platform has no OS keyring (e.g. a headless CI box), keyring
|
||||||
|
// operations will fail gracefully with TokenError::KeychainUnavailable.
|
||||||
|
//
|
||||||
|
// Android: `keyring` isn't compiled in (its `rpassword` transitive
|
||||||
|
// pulls a libc symbol Android's bionic doesn't expose). `auth_tokens`
|
||||||
|
// ships an Android stub that returns KeychainUnavailable for every
|
||||||
|
// call — the runtime behaviour is "session login required each launch"
|
||||||
|
// until we wire Android Keystore via JNI in the Phase-Android round.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
if let Err(e) = keyring::use_native_store(true) {
|
||||||
|
eprintln!(
|
||||||
|
"warn: could not initialise OS keyring ({e}); \
|
||||||
|
server sync login will be unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings before building the app so we can construct the right
|
||||||
|
// sync provider. Falls back to defaults if no settings file exists yet.
|
||||||
|
let settings: Settings = settings_file_path()
|
||||||
|
.map(|p| load_settings_from(&p))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let sync_provider = provider_for_backend(&settings.sync_backend);
|
||||||
|
|
||||||
|
// Restore the previous window geometry if the player has one saved.
|
||||||
|
// Otherwise open at the platform default (1280×800, centred on the
|
||||||
|
// primary monitor) — `apply_smart_default_window_size` will resize
|
||||||
|
// up to a monitor-relative target on the first frame so HiDPI / 4K
|
||||||
|
// sessions don't end up with a comparatively tiny window.
|
||||||
|
let had_saved_geometry = settings.window_geometry.is_some();
|
||||||
|
let (window_resolution, window_position) = match settings.window_geometry {
|
||||||
|
Some(geom) => (
|
||||||
|
(geom.width, geom.height).into(),
|
||||||
|
WindowPosition::At(IVec2::new(geom.x, geom.y)),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
(1280u32, 800u32).into(),
|
||||||
|
WindowPosition::Centered(MonitorSelection::Primary),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app = App::new();
|
||||||
|
|
||||||
|
// The card-theme system's `themes://` asset source must be
|
||||||
|
// registered *before* `DefaultPlugins` builds `AssetPlugin`,
|
||||||
|
// because that plugin freezes the asset-source list at build
|
||||||
|
// time. The matching `AssetSourcesPlugin` (added below) finishes
|
||||||
|
// the wiring after `DefaultPlugins` by populating the embedded
|
||||||
|
// default theme into Bevy's `EmbeddedAssetRegistry`.
|
||||||
|
register_theme_asset_sources(&mut app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.add_plugins(
|
||||||
|
DefaultPlugins
|
||||||
|
.set(WindowPlugin {
|
||||||
|
primary_window: Some(Window {
|
||||||
|
title: "Solitaire Quest".into(),
|
||||||
|
// X11/Wayland WM_CLASS so taskbar managers group
|
||||||
|
// multiple windows of this app correctly.
|
||||||
|
name: Some("solitaire-quest".into()),
|
||||||
|
resolution: window_resolution,
|
||||||
|
position: window_position,
|
||||||
|
// AutoNoVsync prefers Mailbox (triple-buffered) and
|
||||||
|
// falls back to Immediate, eliminating the vsync stall
|
||||||
|
// that AutoVsync produces during continuous window
|
||||||
|
// resize on X11 / Wayland. The game's frame budget is
|
||||||
|
// small enough that a few stray dropped frames from
|
||||||
|
// disabling vsync are imperceptible.
|
||||||
|
present_mode: PresentMode::AutoNoVsync,
|
||||||
|
resize_constraints: bevy::window::WindowResizeConstraints {
|
||||||
|
min_width: 800.0,
|
||||||
|
min_height: 600.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
// The `assets/` directory lives at the workspace root, but
|
||||||
|
// Bevy resolves `AssetPlugin::file_path` relative to the
|
||||||
|
// binary package's `CARGO_MANIFEST_DIR` (`solitaire_app/`).
|
||||||
|
// Point one level up so `cargo run -p solitaire_app` finds
|
||||||
|
// card faces, backs, backgrounds, and the UI font.
|
||||||
|
.set(bevy::asset::AssetPlugin {
|
||||||
|
file_path: "../assets".to_string(),
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.add_plugins(AssetSourcesPlugin)
|
||||||
|
.add_plugins(ThemePlugin)
|
||||||
|
.add_plugins(ThemeRegistryPlugin)
|
||||||
|
.add_plugins(FontPlugin)
|
||||||
|
.add_plugins(GamePlugin)
|
||||||
|
.add_plugins(TablePlugin)
|
||||||
|
.add_plugins(CardPlugin)
|
||||||
|
.add_plugins(CursorPlugin)
|
||||||
|
.add_plugins(InputPlugin)
|
||||||
|
.add_plugins(RadialMenuPlugin)
|
||||||
|
.add_plugins(SelectionPlugin)
|
||||||
|
.add_plugins(AnimationPlugin)
|
||||||
|
.add_plugins(FeedbackAnimPlugin)
|
||||||
|
.add_plugins(CardAnimationPlugin)
|
||||||
|
.add_plugins(AutoCompletePlugin)
|
||||||
|
.add_plugins(ReplayPlaybackPlugin)
|
||||||
|
.add_plugins(ReplayOverlayPlugin)
|
||||||
|
.add_plugins(StatsPlugin::default())
|
||||||
|
.add_plugins(ProgressPlugin::default())
|
||||||
|
.add_plugins(AchievementPlugin::default())
|
||||||
|
.add_plugins(DailyChallengePlugin)
|
||||||
|
.add_plugins(WeeklyGoalsPlugin)
|
||||||
|
.add_plugins(ChallengePlugin)
|
||||||
|
.add_plugins(TimeAttackPlugin)
|
||||||
|
.add_plugins(HudPlugin)
|
||||||
|
.add_plugins(HelpPlugin)
|
||||||
|
.add_plugins(HomePlugin::default())
|
||||||
|
.add_plugins(ProfilePlugin)
|
||||||
|
.add_plugins(PausePlugin)
|
||||||
|
.add_plugins(SettingsPlugin::default())
|
||||||
|
.add_plugins(AudioPlugin)
|
||||||
|
.add_plugins(OnboardingPlugin)
|
||||||
|
.add_plugins(SyncPlugin::new(sync_provider))
|
||||||
|
.add_plugins(LeaderboardPlugin)
|
||||||
|
.add_plugins(WinSummaryPlugin)
|
||||||
|
.add_plugins(UiModalPlugin)
|
||||||
|
.add_plugins(UiFocusPlugin)
|
||||||
|
.add_plugins(UiTooltipPlugin)
|
||||||
|
.add_plugins(SplashPlugin)
|
||||||
|
.add_plugins(DiagnosticsHudPlugin);
|
||||||
|
|
||||||
|
// Smart default window sizing: when no saved geometry was loaded,
|
||||||
|
// resize the freshly-opened 1280×800 window to ~70 % of the primary
|
||||||
|
// monitor's logical size on the first frame. Without this, a 4K
|
||||||
|
// monitor opens the same 1280×800 window that a 1080p monitor
|
||||||
|
// does — visually tiny relative to screen. Skipped entirely when
|
||||||
|
// saved geometry was applied; the player's preference always wins.
|
||||||
|
//
|
||||||
|
// Players who specifically want the literal 1280×800 baseline on
|
||||||
|
// every fresh launch can flip `disable_smart_default_size` in
|
||||||
|
// Settings to opt out. The flag is checked once at startup; a
|
||||||
|
// mid-session change applies on the next launch.
|
||||||
|
if !had_saved_geometry && !settings.disable_smart_default_size {
|
||||||
|
app.add_systems(Update, apply_smart_default_window_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot Update system that runs only on launches without saved
|
||||||
|
/// window geometry. Resizes the primary window to a fraction of the
|
||||||
|
/// primary monitor's *logical* size — bigger monitors get bigger
|
||||||
|
/// windows automatically. Logical size already accounts for the OS's
|
||||||
|
/// HiDPI scale factor, so a 2880×1800 Retina display reporting
|
||||||
|
/// scale_factor 2.0 yields a 1440×900 logical size and a 1008×630
|
||||||
|
/// target window — same physical inches as a 1920×1080 monitor with
|
||||||
|
/// scale_factor 1.0 yielding 1344×756.
|
||||||
|
///
|
||||||
|
/// Uses `Local<bool>` to make itself one-shot rather than introducing
|
||||||
|
/// a dedicated resource. The Update tick is necessary because Bevy
|
||||||
|
/// populates the `Monitor` entities asynchronously after winit's
|
||||||
|
/// Resumed event fires; they may not exist on the first Startup pass.
|
||||||
|
fn apply_smart_default_window_size(
|
||||||
|
mut applied: Local<bool>,
|
||||||
|
monitors: Query<&Monitor, With<PrimaryMonitor>>,
|
||||||
|
mut windows: Query<&mut Window, With<PrimaryWindow>>,
|
||||||
|
) {
|
||||||
|
if *applied {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(monitor) = monitors.single() else {
|
||||||
|
// Primary monitor not yet spawned by bevy_winit. Try again
|
||||||
|
// next frame; the cost is one early-exit per tick until
|
||||||
|
// monitors arrive (typically frame 1 or 2).
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(mut window) = windows.single_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let scale = monitor.scale_factor as f32;
|
||||||
|
if scale <= 0.0 {
|
||||||
|
// Defensive: a zero or negative scale factor would NaN the
|
||||||
|
// arithmetic below. Bail and accept the default size.
|
||||||
|
*applied = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let logical_w = monitor.physical_width as f32 / scale;
|
||||||
|
let logical_h = monitor.physical_height as f32 / scale;
|
||||||
|
|
||||||
|
// Target 70 % of monitor in each dimension, clamped to the
|
||||||
|
// existing 800×600 minimum and the monitor's own logical size
|
||||||
|
// (so we never request a window larger than the screen).
|
||||||
|
let target_w = (logical_w * 0.7).clamp(800.0, logical_w);
|
||||||
|
let target_h = (logical_h * 0.7).clamp(600.0, logical_h);
|
||||||
|
|
||||||
|
// Resize only when the change is meaningful — at exactly 1280×800
|
||||||
|
// on a 1920×1080 monitor the new target is 1344×756 (only ~5 %
|
||||||
|
// wider), worth the resize; at the same default on an 800×600
|
||||||
|
// monitor the clamp pins us at 800×600 and we shouldn't resize.
|
||||||
|
let curr_w = window.resolution.width();
|
||||||
|
let curr_h = window.resolution.height();
|
||||||
|
if (curr_w - target_w).abs() > 8.0 || (curr_h - target_h).abs() > 8.0 {
|
||||||
|
window.resolution.set(target_w, target_h);
|
||||||
|
}
|
||||||
|
*applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps the default panic hook with one that also appends a crash log
|
||||||
|
/// to `<data_dir>/crash.log` (next to `settings.json`). The default hook
|
||||||
|
/// still runs afterwards, so stderr output and debugger integration are
|
||||||
|
/// unchanged. If the data directory is unavailable, the wrapper silently
|
||||||
|
/// falls through — the default hook handles output either way.
|
||||||
|
fn install_crash_log_hook() {
|
||||||
|
let crash_log_path = settings_file_path().and_then(|p| {
|
||||||
|
p.parent()
|
||||||
|
.map(|parent| parent.join("crash.log"))
|
||||||
|
});
|
||||||
|
let default_hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
if let Some(path) = crash_log_path.as_ref()
|
||||||
|
&& let Ok(mut file) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(path)
|
||||||
|
{
|
||||||
|
// Plain unix-seconds timestamp keeps the format trivially
|
||||||
|
// parseable and avoids pulling in chrono just for this.
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_or(0, |d| d.as_secs());
|
||||||
|
let _ = writeln!(file, "----- t={secs} -----\n{info}\n");
|
||||||
|
}
|
||||||
|
default_hook(info);
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,249 +1,9 @@
|
|||||||
use std::fs::OpenOptions;
|
//! Desktop entry point for `solitaire_app`.
|
||||||
use std::io::Write;
|
//!
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
//! The body of the app lives in `lib.rs` so cargo-apk can package the
|
||||||
|
//! same code into an Android `cdylib`. This shim is the desktop /
|
||||||
use bevy::prelude::*;
|
//! `cargo run` path — it just delegates to [`solitaire_app::run`].
|
||||||
use bevy::window::{
|
|
||||||
Monitor, MonitorSelection, PresentMode, PrimaryMonitor, PrimaryWindow, WindowPosition,
|
|
||||||
};
|
|
||||||
use solitaire_data::{load_settings_from, provider_for_backend, settings_file_path, Settings};
|
|
||||||
use solitaire_engine::{
|
|
||||||
register_theme_asset_sources, AchievementPlugin, AnimationPlugin, AssetSourcesPlugin,
|
|
||||||
AudioPlugin, AutoCompletePlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
|
||||||
CursorPlugin, DailyChallengePlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
|
||||||
HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin, OnboardingPlugin, PausePlugin,
|
|
||||||
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin,
|
|
||||||
SelectionPlugin, SettingsPlugin, SplashPlugin,
|
|
||||||
StatsPlugin, SyncPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
|
||||||
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Install a panic hook that writes a crash log next to the save files
|
solitaire_app::run();
|
||||||
// before re-running the default hook (so stderr still gets the message
|
|
||||||
// and any debugger attached still sees the panic).
|
|
||||||
install_crash_log_hook();
|
|
||||||
|
|
||||||
// Initialise the platform keyring store before any token operations.
|
|
||||||
// On Linux this uses the Secret Service (GNOME Keyring / KWallet); on
|
|
||||||
// macOS it uses the Keychain; on Windows it uses the Credential store.
|
|
||||||
// If the platform has no OS keyring (e.g. a headless CI box), keyring
|
|
||||||
// operations will fail gracefully with TokenError::KeychainUnavailable.
|
|
||||||
if let Err(e) = keyring::use_native_store(true) {
|
|
||||||
eprintln!(
|
|
||||||
"warn: could not initialise OS keyring ({e}); \
|
|
||||||
server sync login will be unavailable"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load settings before building the app so we can construct the right
|
|
||||||
// sync provider. Falls back to defaults if no settings file exists yet.
|
|
||||||
let settings: Settings = settings_file_path()
|
|
||||||
.map(|p| load_settings_from(&p))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let sync_provider = provider_for_backend(&settings.sync_backend);
|
|
||||||
|
|
||||||
// Restore the previous window geometry if the player has one saved.
|
|
||||||
// Otherwise open at the platform default (1280×800, centred on the
|
|
||||||
// primary monitor) — `apply_smart_default_window_size` will resize
|
|
||||||
// up to a monitor-relative target on the first frame so HiDPI / 4K
|
|
||||||
// sessions don't end up with a comparatively tiny window.
|
|
||||||
let had_saved_geometry = settings.window_geometry.is_some();
|
|
||||||
let (window_resolution, window_position) = match settings.window_geometry {
|
|
||||||
Some(geom) => (
|
|
||||||
(geom.width, geom.height).into(),
|
|
||||||
WindowPosition::At(IVec2::new(geom.x, geom.y)),
|
|
||||||
),
|
|
||||||
None => (
|
|
||||||
(1280u32, 800u32).into(),
|
|
||||||
WindowPosition::Centered(MonitorSelection::Primary),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut app = App::new();
|
|
||||||
|
|
||||||
// The card-theme system's `themes://` asset source must be
|
|
||||||
// registered *before* `DefaultPlugins` builds `AssetPlugin`,
|
|
||||||
// because that plugin freezes the asset-source list at build
|
|
||||||
// time. The matching `AssetSourcesPlugin` (added below) finishes
|
|
||||||
// the wiring after `DefaultPlugins` by populating the embedded
|
|
||||||
// default theme into Bevy's `EmbeddedAssetRegistry`.
|
|
||||||
register_theme_asset_sources(&mut app);
|
|
||||||
|
|
||||||
app
|
|
||||||
.add_plugins(
|
|
||||||
DefaultPlugins
|
|
||||||
.set(WindowPlugin {
|
|
||||||
primary_window: Some(Window {
|
|
||||||
title: "Solitaire Quest".into(),
|
|
||||||
// X11/Wayland WM_CLASS so taskbar managers group
|
|
||||||
// multiple windows of this app correctly.
|
|
||||||
name: Some("solitaire-quest".into()),
|
|
||||||
resolution: window_resolution,
|
|
||||||
position: window_position,
|
|
||||||
// AutoNoVsync prefers Mailbox (triple-buffered) and
|
|
||||||
// falls back to Immediate, eliminating the vsync stall
|
|
||||||
// that AutoVsync produces during continuous window
|
|
||||||
// resize on X11 / Wayland. The game's frame budget is
|
|
||||||
// small enough that a few stray dropped frames from
|
|
||||||
// disabling vsync are imperceptible.
|
|
||||||
present_mode: PresentMode::AutoNoVsync,
|
|
||||||
resize_constraints: bevy::window::WindowResizeConstraints {
|
|
||||||
min_width: 800.0,
|
|
||||||
min_height: 600.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
..default()
|
|
||||||
}),
|
|
||||||
..default()
|
|
||||||
})
|
|
||||||
// The `assets/` directory lives at the workspace root, but
|
|
||||||
// Bevy resolves `AssetPlugin::file_path` relative to the
|
|
||||||
// binary package's `CARGO_MANIFEST_DIR` (`solitaire_app/`).
|
|
||||||
// Point one level up so `cargo run -p solitaire_app` finds
|
|
||||||
// card faces, backs, backgrounds, and the UI font.
|
|
||||||
.set(bevy::asset::AssetPlugin {
|
|
||||||
file_path: "../assets".to_string(),
|
|
||||||
..default()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.add_plugins(AssetSourcesPlugin)
|
|
||||||
.add_plugins(ThemePlugin)
|
|
||||||
.add_plugins(ThemeRegistryPlugin)
|
|
||||||
.add_plugins(FontPlugin)
|
|
||||||
.add_plugins(GamePlugin)
|
|
||||||
.add_plugins(TablePlugin)
|
|
||||||
.add_plugins(CardPlugin)
|
|
||||||
.add_plugins(CursorPlugin)
|
|
||||||
.add_plugins(InputPlugin)
|
|
||||||
.add_plugins(RadialMenuPlugin)
|
|
||||||
.add_plugins(SelectionPlugin)
|
|
||||||
.add_plugins(AnimationPlugin)
|
|
||||||
.add_plugins(FeedbackAnimPlugin)
|
|
||||||
.add_plugins(CardAnimationPlugin)
|
|
||||||
.add_plugins(AutoCompletePlugin)
|
|
||||||
.add_plugins(ReplayPlaybackPlugin)
|
|
||||||
.add_plugins(ReplayOverlayPlugin)
|
|
||||||
.add_plugins(StatsPlugin::default())
|
|
||||||
.add_plugins(ProgressPlugin::default())
|
|
||||||
.add_plugins(AchievementPlugin::default())
|
|
||||||
.add_plugins(DailyChallengePlugin)
|
|
||||||
.add_plugins(WeeklyGoalsPlugin)
|
|
||||||
.add_plugins(ChallengePlugin)
|
|
||||||
.add_plugins(TimeAttackPlugin)
|
|
||||||
.add_plugins(HudPlugin)
|
|
||||||
.add_plugins(HelpPlugin)
|
|
||||||
.add_plugins(HomePlugin::default())
|
|
||||||
.add_plugins(ProfilePlugin)
|
|
||||||
.add_plugins(PausePlugin)
|
|
||||||
.add_plugins(SettingsPlugin::default())
|
|
||||||
.add_plugins(AudioPlugin)
|
|
||||||
.add_plugins(OnboardingPlugin)
|
|
||||||
.add_plugins(SyncPlugin::new(sync_provider))
|
|
||||||
.add_plugins(LeaderboardPlugin)
|
|
||||||
.add_plugins(WinSummaryPlugin)
|
|
||||||
.add_plugins(UiModalPlugin)
|
|
||||||
.add_plugins(UiFocusPlugin)
|
|
||||||
.add_plugins(UiTooltipPlugin)
|
|
||||||
.add_plugins(SplashPlugin);
|
|
||||||
|
|
||||||
// Smart default window sizing: when no saved geometry was loaded,
|
|
||||||
// resize the freshly-opened 1280×800 window to ~70 % of the primary
|
|
||||||
// monitor's logical size on the first frame. Without this, a 4K
|
|
||||||
// monitor opens the same 1280×800 window that a 1080p monitor
|
|
||||||
// does — visually tiny relative to screen. Skipped entirely when
|
|
||||||
// saved geometry was applied; the player's preference always wins.
|
|
||||||
if !had_saved_geometry {
|
|
||||||
app.add_systems(Update, apply_smart_default_window_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One-shot Update system that runs only on launches without saved
|
|
||||||
/// window geometry. Resizes the primary window to a fraction of the
|
|
||||||
/// primary monitor's *logical* size — bigger monitors get bigger
|
|
||||||
/// windows automatically. Logical size already accounts for the OS's
|
|
||||||
/// HiDPI scale factor, so a 2880×1800 Retina display reporting
|
|
||||||
/// scale_factor 2.0 yields a 1440×900 logical size and a 1008×630
|
|
||||||
/// target window — same physical inches as a 1920×1080 monitor with
|
|
||||||
/// scale_factor 1.0 yielding 1344×756.
|
|
||||||
///
|
|
||||||
/// Uses `Local<bool>` to make itself one-shot rather than introducing
|
|
||||||
/// a dedicated resource. The Update tick is necessary because Bevy
|
|
||||||
/// populates the `Monitor` entities asynchronously after winit's
|
|
||||||
/// Resumed event fires; they may not exist on the first Startup pass.
|
|
||||||
fn apply_smart_default_window_size(
|
|
||||||
mut applied: Local<bool>,
|
|
||||||
monitors: Query<&Monitor, With<PrimaryMonitor>>,
|
|
||||||
mut windows: Query<&mut Window, With<PrimaryWindow>>,
|
|
||||||
) {
|
|
||||||
if *applied {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Ok(monitor) = monitors.single() else {
|
|
||||||
// Primary monitor not yet spawned by bevy_winit. Try again
|
|
||||||
// next frame; the cost is one early-exit per tick until
|
|
||||||
// monitors arrive (typically frame 1 or 2).
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(mut window) = windows.single_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scale = monitor.scale_factor as f32;
|
|
||||||
if scale <= 0.0 {
|
|
||||||
// Defensive: a zero or negative scale factor would NaN the
|
|
||||||
// arithmetic below. Bail and accept the default size.
|
|
||||||
*applied = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let logical_w = monitor.physical_width as f32 / scale;
|
|
||||||
let logical_h = monitor.physical_height as f32 / scale;
|
|
||||||
|
|
||||||
// Target 70 % of monitor in each dimension, clamped to the
|
|
||||||
// existing 800×600 minimum and the monitor's own logical size
|
|
||||||
// (so we never request a window larger than the screen).
|
|
||||||
let target_w = (logical_w * 0.7).clamp(800.0, logical_w);
|
|
||||||
let target_h = (logical_h * 0.7).clamp(600.0, logical_h);
|
|
||||||
|
|
||||||
// Resize only when the change is meaningful — at exactly 1280×800
|
|
||||||
// on a 1920×1080 monitor the new target is 1344×756 (only ~5 %
|
|
||||||
// wider), worth the resize; at the same default on an 800×600
|
|
||||||
// monitor the clamp pins us at 800×600 and we shouldn't resize.
|
|
||||||
let curr_w = window.resolution.width();
|
|
||||||
let curr_h = window.resolution.height();
|
|
||||||
if (curr_w - target_w).abs() > 8.0 || (curr_h - target_h).abs() > 8.0 {
|
|
||||||
window.resolution.set(target_w, target_h);
|
|
||||||
}
|
|
||||||
*applied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps the default panic hook with one that also appends a crash log
|
|
||||||
/// to `<data_dir>/crash.log` (next to `settings.json`). The default hook
|
|
||||||
/// still runs afterwards, so stderr output and debugger integration are
|
|
||||||
/// unchanged. If the data directory is unavailable, the wrapper silently
|
|
||||||
/// falls through — the default hook handles output either way.
|
|
||||||
fn install_crash_log_hook() {
|
|
||||||
let crash_log_path = settings_file_path().and_then(|p| {
|
|
||||||
p.parent()
|
|
||||||
.map(|parent| parent.join("crash.log"))
|
|
||||||
});
|
|
||||||
let default_hook = std::panic::take_hook();
|
|
||||||
std::panic::set_hook(Box::new(move |info| {
|
|
||||||
if let Some(path) = crash_log_path.as_ref()
|
|
||||||
&& let Ok(mut file) = OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(path)
|
|
||||||
{
|
|
||||||
// Plain unix-seconds timestamp keeps the format trivially
|
|
||||||
// parseable and avoids pulling in chrono just for this.
|
|
||||||
let secs = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map_or(0, |d| d.as_secs());
|
|
||||||
let _ = writeln!(file, "----- t={secs} -----\n{info}\n");
|
|
||||||
}
|
|
||||||
default_hook(info);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,19 @@ chrono = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
keyring-core = { workspace = true }
|
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
# `keyring-core` is the typed Entry/Error API used by
|
||||||
|
# `auth_tokens`. The crate's own dependency tree pulls in
|
||||||
|
# `rpassword` which uses `libc::__errno_location` — a symbol the
|
||||||
|
# Android NDK doesn't expose (`__errno` lives at a different path
|
||||||
|
# on bionic). On Android `auth_tokens` falls back to a stub
|
||||||
|
# implementation that always returns `KeychainUnavailable`; the
|
||||||
|
# real backend lands when we wire Android Keystore via JNI.
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
keyring-core = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solitaire_server = { path = "../solitaire_server" }
|
solitaire_server = { path = "../solitaire_server" }
|
||||||
solitaire_sync = { workspace = true }
|
solitaire_sync = { workspace = true }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const FILE_NAME: &str = "achievements.json";
|
|||||||
|
|
||||||
/// Platform-specific default path for `achievements.json`.
|
/// Platform-specific default path for `achievements.json`.
|
||||||
pub fn achievements_file_path() -> Option<PathBuf> {
|
pub fn achievements_file_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load achievements from an explicit path. Returns `Vec::new()` if the file
|
/// Load achievements from an explicit path. Returns `Vec::new()` if the file
|
||||||
|
|||||||
@@ -14,8 +14,19 @@
|
|||||||
//! the Bevy `App`). If no default store is set, all operations in this module
|
//! the Bevy `App`). If no default store is set, all operations in this module
|
||||||
//! will return [`TokenError::KeychainUnavailable`].
|
//! will return [`TokenError::KeychainUnavailable`].
|
||||||
//!
|
//!
|
||||||
|
//! # Android stub
|
||||||
|
//!
|
||||||
|
//! `keyring-core` cannot compile for the android target (its `rpassword`
|
||||||
|
//! transitive dep uses `libc::__errno_location`, which Android's bionic
|
||||||
|
//! doesn't expose). On Android every function in this module returns
|
||||||
|
//! [`TokenError::KeychainUnavailable`] so callers can detect the fallback
|
||||||
|
//! the same way they handle a Linux box without Secret Service. The
|
||||||
|
//! real Android backend will arrive in the Phase-Android round when we
|
||||||
|
//! wire Android Keystore via JNI.
|
||||||
|
//!
|
||||||
//! # Note: no unit tests — requires live OS keychain.
|
//! # Note: no unit tests — requires live OS keychain.
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
use keyring_core::Entry;
|
use keyring_core::Entry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -34,9 +45,11 @@ pub enum TokenError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Service name used to namespace all keychain entries for this application.
|
/// Service name used to namespace all keychain entries for this application.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
const SERVICE: &str = "solitaire_quest_server";
|
const SERVICE: &str = "solitaire_quest_server";
|
||||||
|
|
||||||
/// Map a `keyring_core::Error` to the appropriate `TokenError`.
|
/// Map a `keyring_core::Error` to the appropriate `TokenError`.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn map_keyring_err(err: keyring_core::Error, username: &str) -> TokenError {
|
fn map_keyring_err(err: keyring_core::Error, username: &str) -> TokenError {
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
match err {
|
match err {
|
||||||
@@ -51,6 +64,7 @@ fn map_keyring_err(err: keyring_core::Error, username: &str) -> TokenError {
|
|||||||
/// Store the access and refresh tokens for `username` in the OS keychain.
|
/// Store the access and refresh tokens for `username` in the OS keychain.
|
||||||
///
|
///
|
||||||
/// Any previously stored tokens for that username are overwritten.
|
/// Any previously stored tokens for that username are overwritten.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn store_tokens(
|
pub fn store_tokens(
|
||||||
username: &str,
|
username: &str,
|
||||||
access_token: &str,
|
access_token: &str,
|
||||||
@@ -72,6 +86,7 @@ pub fn store_tokens(
|
|||||||
/// Load the stored access token for `username` from the OS keychain.
|
/// Load the stored access token for `username` from the OS keychain.
|
||||||
///
|
///
|
||||||
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
|
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn load_access_token(username: &str) -> Result<String, TokenError> {
|
pub fn load_access_token(username: &str) -> Result<String, TokenError> {
|
||||||
Entry::new(SERVICE, &format!("{username}_access"))
|
Entry::new(SERVICE, &format!("{username}_access"))
|
||||||
.map_err(|e| map_keyring_err(e, username))?
|
.map_err(|e| map_keyring_err(e, username))?
|
||||||
@@ -82,6 +97,7 @@ pub fn load_access_token(username: &str) -> Result<String, TokenError> {
|
|||||||
/// Load the stored refresh token for `username` from the OS keychain.
|
/// Load the stored refresh token for `username` from the OS keychain.
|
||||||
///
|
///
|
||||||
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
|
/// Returns [`TokenError::NotFound`] if no token has been stored yet.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn load_refresh_token(username: &str) -> Result<String, TokenError> {
|
pub fn load_refresh_token(username: &str) -> Result<String, TokenError> {
|
||||||
Entry::new(SERVICE, &format!("{username}_refresh"))
|
Entry::new(SERVICE, &format!("{username}_refresh"))
|
||||||
.map_err(|e| map_keyring_err(e, username))?
|
.map_err(|e| map_keyring_err(e, username))?
|
||||||
@@ -93,6 +109,7 @@ pub fn load_refresh_token(username: &str) -> Result<String, TokenError> {
|
|||||||
///
|
///
|
||||||
/// Intended to be called on logout or account deletion. Missing entries are
|
/// Intended to be called on logout or account deletion. Missing entries are
|
||||||
/// silently ignored (the tokens are already gone, which is the desired state).
|
/// silently ignored (the tokens are already gone, which is the desired state).
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
|
pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
|
||||||
match Entry::new(SERVICE, &format!("{username}_access"))
|
match Entry::new(SERVICE, &format!("{username}_access"))
|
||||||
.map_err(|e| map_keyring_err(e, username))?
|
.map_err(|e| map_keyring_err(e, username))?
|
||||||
@@ -112,3 +129,37 @@ pub fn delete_tokens(username: &str) -> Result<(), TokenError> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Android stub — same public API, always returns KeychainUnavailable.
|
||||||
|
// Lets `sync_client::*` compile unchanged on Android; the runtime
|
||||||
|
// effect is "session login required every launch", same as a Linux
|
||||||
|
// box without Secret Service.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
const ANDROID_STUB_MSG: &str = "android stub: keychain not yet wired (Phase-Android task)";
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn store_tokens(
|
||||||
|
_username: &str,
|
||||||
|
_access_token: &str,
|
||||||
|
_refresh_token: &str,
|
||||||
|
) -> Result<(), TokenError> {
|
||||||
|
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn load_access_token(_username: &str) -> Result<String, TokenError> {
|
||||||
|
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn load_refresh_token(_username: &str) -> Result<String, TokenError> {
|
||||||
|
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn delete_tokens(_username: &str) -> Result<(), TokenError> {
|
||||||
|
Err(TokenError::KeychainUnavailable(ANDROID_STUB_MSG.to_string()))
|
||||||
|
}
|
||||||
|
|||||||
@@ -163,3 +163,6 @@ pub use replay::{
|
|||||||
replay_history_path, save_replay_history_to, Replay, ReplayHistory, ReplayMove,
|
replay_history_path, save_replay_history_to, Replay, ReplayHistory, ReplayMove,
|
||||||
REPLAY_HISTORY_CAP, REPLAY_HISTORY_SCHEMA_VERSION, REPLAY_SCHEMA_VERSION,
|
REPLAY_HISTORY_CAP, REPLAY_HISTORY_SCHEMA_VERSION, REPLAY_SCHEMA_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod platform;
|
||||||
|
pub use platform::data_dir;
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! Per-platform resolution of the per-user data directory.
|
||||||
|
//!
|
||||||
|
//! The rest of `solitaire_data` (settings, stats, achievements,
|
||||||
|
//! replays, progress, game state) and the engine's user-themes
|
||||||
|
//! discovery all need a base path under which to nest
|
||||||
|
//! `solitaire_quest/<file>`. On desktop the right answer is
|
||||||
|
//! `dirs::data_dir()` (which resolves to platform-appropriate
|
||||||
|
//! locations: `~/.local/share` on Linux, `~/Library/Application
|
||||||
|
//! Support` on macOS, `%APPDATA%` on Windows). On Android the
|
||||||
|
//! `dirs` crate returns `None`, which would silently disable
|
||||||
|
//! every persistence path — settings, stats, replays, the lot.
|
||||||
|
//!
|
||||||
|
//! [`data_dir`] is a thin shim that returns the right base path
|
||||||
|
//! per target. Callers continue to append
|
||||||
|
//! `solitaire_quest/<file>` themselves, so the on-disk layout is
|
||||||
|
//! identical across platforms (the per-app Android sandbox makes
|
||||||
|
//! the extra `solitaire_quest/` segment harmless, and a `tar`
|
||||||
|
//! export from one platform deserialises cleanly on another).
|
||||||
|
//!
|
||||||
|
//! # Why hardcode on Android?
|
||||||
|
//!
|
||||||
|
//! The "proper" Android answer is JNI: call back into Java to
|
||||||
|
//! invoke `Activity.getFilesDir()`. That requires plumbing an
|
||||||
|
//! `AndroidApp` context through Bevy's startup hooks and a
|
||||||
|
//! per-call JNI bridge — meaningfully more code than the
|
||||||
|
//! sandbox-guaranteed `/data/data/<package>/files` path. The
|
||||||
|
//! package name `com.solitairequest.app` is fixed at compile
|
||||||
|
//! time in `solitaire_app/Cargo.toml`'s
|
||||||
|
//! `[package.metadata.android]` block, so a hardcoded path is
|
||||||
|
//! safe until that ever changes (at which point this constant
|
||||||
|
//! moves with it).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Hardcoded per-app private files directory on Android.
|
||||||
|
///
|
||||||
|
/// Matches `[package.metadata.android]` in `solitaire_app/Cargo.toml`.
|
||||||
|
/// The Android sandbox guarantees this path exists, is writable,
|
||||||
|
/// and is private to the app — no JNI needed. Update both this
|
||||||
|
/// constant and the Cargo metadata together if the package id
|
||||||
|
/// ever changes.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
const ANDROID_APP_FILES_DIR: &str = "/data/data/com.solitairequest.app/files";
|
||||||
|
|
||||||
|
/// Returns the per-user data directory for the current target,
|
||||||
|
/// or `None` if the platform doesn't expose one (rare; usually
|
||||||
|
/// indicates a broken `$HOME` or `$XDG_*` configuration on a
|
||||||
|
/// minimal Linux container).
|
||||||
|
///
|
||||||
|
/// Callers append `solitaire_quest/<file>` themselves. See the
|
||||||
|
/// module-level doc comment for the per-platform behaviour and
|
||||||
|
/// why Android uses a hardcoded path.
|
||||||
|
pub fn data_dir() -> Option<PathBuf> {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
Some(PathBuf::from(ANDROID_APP_FILES_DIR))
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
dirs::data_dir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// On every supported desktop target the OS reports a usable
|
||||||
|
/// data directory. This test only runs on desktop because the
|
||||||
|
/// Android branch returns a fixed string regardless of host
|
||||||
|
/// state, and asserting on a fixed string is a tautology.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||||
|
#[test]
|
||||||
|
fn data_dir_returns_some_on_desktop_targets() {
|
||||||
|
let dir = data_dir().expect("desktop targets must report a data dir");
|
||||||
|
assert!(
|
||||||
|
dir.is_absolute(),
|
||||||
|
"data_dir() must return an absolute path on desktop, got {dir:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On Android the hardcoded path matches the package id pinned
|
||||||
|
/// in `solitaire_app/Cargo.toml`'s `[package.metadata.android]`.
|
||||||
|
/// If a future change rotates that id, this test fails loudly
|
||||||
|
/// so the path constant moves with it.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
#[test]
|
||||||
|
fn data_dir_returns_sandbox_path_on_android() {
|
||||||
|
let dir = data_dir().expect("android must report a data dir");
|
||||||
|
assert_eq!(dir, PathBuf::from("/data/data/com.solitairequest.app/files"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ pub fn xp_for_win(time_seconds: u64, used_undo: bool) -> u64 {
|
|||||||
|
|
||||||
/// Platform-specific default path for `progress.json`.
|
/// Platform-specific default path for `progress.json`.
|
||||||
pub fn progress_file_path() -> Option<PathBuf> {
|
pub fn progress_file_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load progress from an explicit path. Returns `default()` if missing/corrupt.
|
/// Load progress from an explicit path. Returns `default()` if missing/corrupt.
|
||||||
|
|||||||
@@ -230,21 +230,21 @@ impl ReplayHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the platform-specific path to `latest_replay.json`, or `None`
|
/// Returns the platform-specific path to `latest_replay.json`, or `None`
|
||||||
/// if `dirs::data_dir()` is unavailable (e.g. minimal Linux containers).
|
/// if `crate::data_dir()` is unavailable (e.g. minimal Linux containers).
|
||||||
#[deprecated(
|
#[deprecated(
|
||||||
note = "single-slot replay storage replaced by the rolling history at \
|
note = "single-slot replay storage replaced by the rolling history at \
|
||||||
replay_history_path(); kept for the one-shot legacy migration \
|
replay_history_path(); kept for the one-shot legacy migration \
|
||||||
in migrate_legacy_latest_replay"
|
in migrate_legacy_latest_replay"
|
||||||
)]
|
)]
|
||||||
pub fn latest_replay_path() -> Option<PathBuf> {
|
pub fn latest_replay_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(LATEST_REPLAY_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(LATEST_REPLAY_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the platform-specific path to `replays.json`, the rolling
|
/// Returns the platform-specific path to `replays.json`, the rolling
|
||||||
/// history file, or `None` if `dirs::data_dir()` is unavailable (e.g.
|
/// history file, or `None` if `crate::data_dir()` is unavailable (e.g.
|
||||||
/// minimal Linux containers).
|
/// minimal Linux containers).
|
||||||
pub fn replay_history_path() -> Option<PathBuf> {
|
pub fn replay_history_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(REPLAY_HISTORY_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(REPLAY_HISTORY_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a [`Replay`] atomically to `path` using the standard `.tmp` →
|
/// Save a [`Replay`] atomically to `path` using the standard `.tmp` →
|
||||||
|
|||||||
@@ -181,6 +181,20 @@ pub struct Settings {
|
|||||||
/// solver retry loop — see `solitaire_engine::handle_new_game`.
|
/// solver retry loop — see `solitaire_engine::handle_new_game`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub winnable_deals_only: bool,
|
pub winnable_deals_only: bool,
|
||||||
|
/// When `true`, suppresses the launch-time
|
||||||
|
/// `apply_smart_default_window_size` system: the window opens at
|
||||||
|
/// the literal `(1280, 800)` default instead of resizing to ~70 %
|
||||||
|
/// of the primary monitor's logical size on the first frame. For
|
||||||
|
/// players who specifically prefer the 1280×800 baseline on every
|
||||||
|
/// fresh launch (i.e. installs without saved geometry).
|
||||||
|
///
|
||||||
|
/// Older `settings.json` files written before this field existed
|
||||||
|
/// deserialize cleanly to `false` via `#[serde(default)]`, which
|
||||||
|
/// preserves the smart-default behaviour shipped in v0.19.0.
|
||||||
|
/// Saved-geometry launches are unaffected by this flag — the
|
||||||
|
/// player's last window size always wins.
|
||||||
|
#[serde(default)]
|
||||||
|
pub disable_smart_default_size: bool,
|
||||||
/// Per-move duration during replay playback, in seconds. Range
|
/// Per-move duration during replay playback, in seconds. Range
|
||||||
/// `[REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS]`;
|
/// `[REPLAY_MOVE_INTERVAL_MIN_SECS, REPLAY_MOVE_INTERVAL_MAX_SECS]`;
|
||||||
/// default mirrors `solitaire_engine::replay_playback::REPLAY_MOVE_INTERVAL_SECS`
|
/// default mirrors `solitaire_engine::replay_playback::REPLAY_MOVE_INTERVAL_SECS`
|
||||||
@@ -306,6 +320,7 @@ impl Default for Settings {
|
|||||||
tooltip_delay_secs: default_tooltip_delay(),
|
tooltip_delay_secs: default_tooltip_delay(),
|
||||||
time_bonus_multiplier: default_time_bonus_multiplier(),
|
time_bonus_multiplier: default_time_bonus_multiplier(),
|
||||||
winnable_deals_only: false,
|
winnable_deals_only: false,
|
||||||
|
disable_smart_default_size: false,
|
||||||
replay_move_interval_secs: default_replay_move_interval_secs(),
|
replay_move_interval_secs: default_replay_move_interval_secs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,9 +400,9 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the platform-specific path to `settings.json`, or `None` if
|
/// Returns the platform-specific path to `settings.json`, or `None` if
|
||||||
/// `dirs::data_dir()` is unavailable.
|
/// the platform's data directory is unavailable.
|
||||||
pub fn settings_file_path() -> Option<PathBuf> {
|
pub fn settings_file_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(SETTINGS_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(SETTINGS_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load settings from an explicit path. Returns `Settings::default()` if the
|
/// Load settings from an explicit path. Returns `Settings::default()` if the
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ const GAME_STATE_FILE_NAME: &str = "game_state.json";
|
|||||||
const TIME_ATTACK_SESSION_FILE_NAME: &str = "time_attack_session.json";
|
const TIME_ATTACK_SESSION_FILE_NAME: &str = "time_attack_session.json";
|
||||||
|
|
||||||
/// Returns the platform-specific path to `stats.json`, or `None` if
|
/// Returns the platform-specific path to `stats.json`, or `None` if
|
||||||
/// `dirs::data_dir()` is unavailable (e.g. minimal Linux containers).
|
/// `crate::data_dir()` is unavailable (e.g. minimal Linux containers).
|
||||||
pub fn stats_file_path() -> Option<PathBuf> {
|
pub fn stats_file_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(STATS_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(STATS_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load stats from an explicit path. Returns `StatsSnapshot::default()` if
|
/// Load stats from an explicit path. Returns `StatsSnapshot::default()` if
|
||||||
@@ -69,9 +69,9 @@ pub fn save_stats(stats: &StatsSnapshot) -> io::Result<()> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Returns the platform-specific path to `game_state.json`, or `None` if
|
/// Returns the platform-specific path to `game_state.json`, or `None` if
|
||||||
/// `dirs::data_dir()` is unavailable.
|
/// `crate::data_dir()` is unavailable.
|
||||||
pub fn game_state_file_path() -> Option<PathBuf> {
|
pub fn game_state_file_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(GAME_STATE_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(GAME_STATE_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load an in-progress `GameState` from `path`. Returns `None` if the file is
|
/// Load an in-progress `GameState` from `path`. Returns `None` if the file is
|
||||||
@@ -129,7 +129,7 @@ pub fn delete_game_state_at(path: &Path) -> io::Result<()> {
|
|||||||
/// in an atomic save. Safe to call on startup; missing or unreadable entries
|
/// in an atomic save. Safe to call on startup; missing or unreadable entries
|
||||||
/// are silently skipped.
|
/// are silently skipped.
|
||||||
pub fn cleanup_orphaned_tmp_files() -> io::Result<()> {
|
pub fn cleanup_orphaned_tmp_files() -> io::Result<()> {
|
||||||
let dir = match dirs::data_dir() {
|
let dir = match crate::data_dir() {
|
||||||
Some(d) => d.join(APP_DIR_NAME),
|
Some(d) => d.join(APP_DIR_NAME),
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
@@ -179,9 +179,9 @@ pub struct TimeAttackSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the platform-specific path to `time_attack_session.json`, or
|
/// Returns the platform-specific path to `time_attack_session.json`, or
|
||||||
/// `None` if `dirs::data_dir()` is unavailable.
|
/// `None` if `crate::data_dir()` is unavailable.
|
||||||
pub fn time_attack_session_path() -> Option<PathBuf> {
|
pub fn time_attack_session_path() -> Option<PathBuf> {
|
||||||
dirs::data_dir().map(|d| d.join(APP_DIR_NAME).join(TIME_ATTACK_SESSION_FILE_NAME))
|
crate::data_dir().map(|d| d.join(APP_DIR_NAME).join(TIME_ATTACK_SESSION_FILE_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a Time Attack session atomically. Mirrors `save_game_state_to`'s
|
/// Save a Time Attack session atomically. Mirrors `save_game_state_to`'s
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ tiny-skia = { workspace = true }
|
|||||||
ron = { workspace = true }
|
ron = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
|
|
||||||
|
# `arboard` provides clipboard access for the Stats overlay's
|
||||||
|
# "Copy share link" button. The crate has no Android backend
|
||||||
|
# (its `platform::Clipboard` module is unimplemented for the
|
||||||
|
# android target — `cargo apk build` fails with E0433 if this is
|
||||||
|
# left unconditional). On Android the same button surfaces an
|
||||||
|
# informational toast instead; see
|
||||||
|
# `stats_plugin::handle_copy_share_link_button`.
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
arboard = { workspace = true }
|
arboard = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ use crate::progress_plugin::LevelUpEvent;
|
|||||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||||
use crate::time_attack_plugin::TimeAttackEndedEvent;
|
use crate::time_attack_plugin::TimeAttackEndedEvent;
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
scaled_duration, ACCENT_PRIMARY, MOTION_CASCADE_SLIDE_SECS, MOTION_CASCADE_STAGGER_SECS,
|
scaled_duration, ACCENT_SECONDARY, BG_ELEVATED, MOTION_CASCADE_SLIDE_SECS,
|
||||||
MOTION_SLIDE_SECS, TEXT_PRIMARY, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, Z_TOAST,
|
MOTION_CASCADE_STAGGER_SECS, MOTION_SLIDE_SECS, RADIUS_MD, STATE_DANGER, STATE_INFO,
|
||||||
|
STATE_WARNING, TEXT_PRIMARY, TYPE_BODY_LG, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, Z_TOAST,
|
||||||
};
|
};
|
||||||
use crate::weekly_goals_plugin::WeeklyGoalCompletedEvent;
|
use crate::weekly_goals_plugin::WeeklyGoalCompletedEvent;
|
||||||
|
|
||||||
@@ -339,6 +340,7 @@ fn handle_achievement_toast(
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Achievement: {}", display_name_for(&ev.0.id)),
|
format!("Achievement: {}", display_name_for(&ev.0.id)),
|
||||||
ACHIEVEMENT_TOAST_SECS,
|
ACHIEVEMENT_TOAST_SECS,
|
||||||
|
ToastVariant::Celebration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +351,7 @@ fn handle_levelup_toast(mut commands: Commands, mut events: MessageReader<LevelU
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Level Up! → {}", ev.new_level),
|
format!("Level Up! → {}", ev.new_level),
|
||||||
LEVELUP_TOAST_SECS,
|
LEVELUP_TOAST_SECS,
|
||||||
|
ToastVariant::Celebration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,7 +361,12 @@ fn handle_daily_goal_announcement_toast(
|
|||||||
mut events: MessageReader<DailyGoalAnnouncementEvent>,
|
mut events: MessageReader<DailyGoalAnnouncementEvent>,
|
||||||
) {
|
) {
|
||||||
for ev in events.read() {
|
for ev in events.read() {
|
||||||
spawn_toast(&mut commands, format!("Goal: {}", ev.0), DAILY_TOAST_SECS);
|
spawn_toast(
|
||||||
|
&mut commands,
|
||||||
|
format!("Goal: {}", ev.0),
|
||||||
|
DAILY_TOAST_SECS,
|
||||||
|
ToastVariant::Info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +379,7 @@ fn handle_daily_toast(
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Daily Challenge Complete! (Streak: {})", ev.streak),
|
format!("Daily Challenge Complete! (Streak: {})", ev.streak),
|
||||||
DAILY_TOAST_SECS,
|
DAILY_TOAST_SECS,
|
||||||
|
ToastVariant::Celebration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +393,7 @@ fn handle_weekly_toast(
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Weekly Goal: {}", ev.description),
|
format!("Weekly Goal: {}", ev.description),
|
||||||
WEEKLY_TOAST_SECS,
|
WEEKLY_TOAST_SECS,
|
||||||
|
ToastVariant::Celebration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,6 +407,7 @@ fn handle_time_attack_toast(
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Time Attack: {} win{}", ev.wins, if ev.wins == 1 { "" } else { "s" }),
|
format!("Time Attack: {} win{}", ev.wins, if ev.wins == 1 { "" } else { "s" }),
|
||||||
TIME_ATTACK_TOAST_SECS,
|
TIME_ATTACK_TOAST_SECS,
|
||||||
|
ToastVariant::Info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +421,7 @@ fn handle_challenge_toast(
|
|||||||
&mut commands,
|
&mut commands,
|
||||||
format!("Challenge {} cleared!", ev.previous_index.saturating_add(1)),
|
format!("Challenge {} cleared!", ev.previous_index.saturating_add(1)),
|
||||||
CHALLENGE_TOAST_SECS,
|
CHALLENGE_TOAST_SECS,
|
||||||
|
ToastVariant::Celebration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,11 +441,21 @@ fn handle_settings_toast(
|
|||||||
*last_music = Some(music);
|
*last_music = Some(music);
|
||||||
if sfx_changed {
|
if sfx_changed {
|
||||||
let pct = (sfx * 100.0).round() as i32;
|
let pct = (sfx * 100.0).round() as i32;
|
||||||
spawn_toast(&mut commands, format!("SFX: {pct}%"), VOLUME_TOAST_SECS);
|
spawn_toast(
|
||||||
|
&mut commands,
|
||||||
|
format!("SFX: {pct}%"),
|
||||||
|
VOLUME_TOAST_SECS,
|
||||||
|
ToastVariant::Info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if music_changed {
|
if music_changed {
|
||||||
let pct = (music * 100.0).round() as i32;
|
let pct = (music * 100.0).round() as i32;
|
||||||
spawn_toast(&mut commands, format!("Music: {pct}%"), VOLUME_TOAST_SECS);
|
spawn_toast(
|
||||||
|
&mut commands,
|
||||||
|
format!("Music: {pct}%"),
|
||||||
|
VOLUME_TOAST_SECS,
|
||||||
|
ToastVariant::Info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,7 +471,12 @@ fn handle_auto_complete_toast(
|
|||||||
if s.active {
|
if s.active {
|
||||||
if !*shown {
|
if !*shown {
|
||||||
*shown = true;
|
*shown = true;
|
||||||
spawn_toast(&mut commands, "Auto-completing…".to_string(), 2.0);
|
spawn_toast(
|
||||||
|
&mut commands,
|
||||||
|
"Auto-completing…".to_string(),
|
||||||
|
2.0,
|
||||||
|
ToastVariant::Info,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
*shown = false;
|
*shown = false;
|
||||||
@@ -513,37 +540,72 @@ fn drive_toast_display(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns a centered top-of-screen `ToastEntity` for the queued toast system.
|
/// Visual variant of a toast — drives the 1px border accent per the
|
||||||
|
/// design-system toast spec
|
||||||
|
/// (`docs/ui-mockups/design-system.md` → "Toasts").
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ToastVariant {
|
||||||
|
/// Neutral system message — teal border. Default for `InfoToastEvent`,
|
||||||
|
/// settings volume notifications, and the auto-complete announcement.
|
||||||
|
Info,
|
||||||
|
/// Caution / penalty — gold border. Currently unused by an in-engine
|
||||||
|
/// event; kept so future warning-flavoured toasts have a slot.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Warning,
|
||||||
|
/// Failure / rejected action — pink border. Currently unused; kept so
|
||||||
|
/// future error-flavoured toasts have a slot.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Error,
|
||||||
|
/// Reward / milestone — lavender border. Used for XP awards,
|
||||||
|
/// achievement unlocks, level-ups, daily/weekly/challenge completions.
|
||||||
|
Celebration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToastVariant {
|
||||||
|
/// Returns the 1px border accent for this variant per the design
|
||||||
|
/// system. Single source of truth — `spawn_toast` and
|
||||||
|
/// `spawn_queued_toast` both consume it so a future palette swap
|
||||||
|
/// only has to touch the token, never every call site.
|
||||||
|
fn border_color(self) -> Color {
|
||||||
|
match self {
|
||||||
|
ToastVariant::Info => STATE_INFO,
|
||||||
|
ToastVariant::Warning => STATE_WARNING,
|
||||||
|
ToastVariant::Error => STATE_DANGER,
|
||||||
|
ToastVariant::Celebration => ACCENT_SECONDARY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a bottom-anchored `ToastEntity` for the queued toast system.
|
||||||
|
///
|
||||||
|
/// Queued toasts always carry [`ToastVariant::Info`] — the queue is fed
|
||||||
|
/// by [`InfoToastEvent`] which is by definition neutral system info.
|
||||||
|
/// Variants other than `Info` belong on the immediate-fire path
|
||||||
|
/// ([`spawn_toast`]) where the call site knows the semantic intent.
|
||||||
fn spawn_queued_toast(commands: &mut Commands, message: String) -> Entity {
|
fn spawn_queued_toast(commands: &mut Commands, message: String) -> Entity {
|
||||||
commands
|
spawn_toast_node(
|
||||||
.spawn((
|
commands,
|
||||||
ToastEntity,
|
ToastEntity,
|
||||||
Node {
|
message,
|
||||||
position_type: PositionType::Absolute,
|
ToastVariant::Info,
|
||||||
left: Val::Percent(15.0),
|
// Slightly taller anchor than the immediate-fire path so a
|
||||||
top: Val::Percent(8.0),
|
// queued info banner doesn't collide with a celebration toast
|
||||||
width: Val::Percent(70.0),
|
// fired in the same frame.
|
||||||
padding: UiRect::axes(VAL_SPACE_4, VAL_SPACE_2),
|
Val::Percent(6.0),
|
||||||
justify_content: JustifyContent::Center,
|
Val::Percent(15.0),
|
||||||
align_items: AlignItems::Center,
|
Val::Percent(70.0),
|
||||||
..default()
|
UiRect::axes(VAL_SPACE_4, VAL_SPACE_2),
|
||||||
},
|
)
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.60)),
|
|
||||||
ZIndex(Z_TOAST),
|
|
||||||
))
|
|
||||||
.with_children(|b| {
|
|
||||||
b.spawn((
|
|
||||||
Text::new(message),
|
|
||||||
TextFont { font_size: 22.0, ..default() },
|
|
||||||
TextColor(TEXT_PRIMARY),
|
|
||||||
));
|
|
||||||
})
|
|
||||||
.id()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_xp_awarded_toast(mut commands: Commands, mut events: MessageReader<XpAwardedEvent>) {
|
fn handle_xp_awarded_toast(mut commands: Commands, mut events: MessageReader<XpAwardedEvent>) {
|
||||||
for ev in events.read() {
|
for ev in events.read() {
|
||||||
spawn_toast(&mut commands, format!("+{} XP", ev.amount), 3.0);
|
spawn_toast(
|
||||||
|
&mut commands,
|
||||||
|
format!("+{} XP", ev.amount),
|
||||||
|
3.0,
|
||||||
|
ToastVariant::Celebration,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,33 +631,88 @@ fn tick_toasts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_toast(commands: &mut Commands, message: String, duration_secs: f32) {
|
/// Spawns a bottom-anchored fire-and-forget toast that despawns after
|
||||||
|
/// `duration_secs`. The `variant` selects the 1px accent border color
|
||||||
|
/// per the design-system toast spec.
|
||||||
|
fn spawn_toast(
|
||||||
|
commands: &mut Commands,
|
||||||
|
message: String,
|
||||||
|
duration_secs: f32,
|
||||||
|
variant: ToastVariant,
|
||||||
|
) {
|
||||||
|
spawn_toast_node(
|
||||||
|
commands,
|
||||||
|
(ToastOverlay, ToastTimer(duration_secs)),
|
||||||
|
message,
|
||||||
|
variant,
|
||||||
|
// Sits above the queued banner so a celebration toast spawned
|
||||||
|
// alongside a queued info message remains readable.
|
||||||
|
Val::Percent(14.0),
|
||||||
|
Val::Percent(25.0),
|
||||||
|
Val::Percent(50.0),
|
||||||
|
UiRect::axes(VAL_SPACE_4, VAL_SPACE_3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common toast-spawn primitive used by both the queued and the
|
||||||
|
/// fire-and-forget paths. Centralizes the design-system contract so a
|
||||||
|
/// future spec change (e.g. a different border thickness) is a
|
||||||
|
/// one-line edit.
|
||||||
|
///
|
||||||
|
/// The Terminal toast spec from `design-system.md`:
|
||||||
|
/// - Opaque [`BG_ELEVATED`] fill (no translucent dim).
|
||||||
|
/// - 1px border in the variant's accent color.
|
||||||
|
/// - [`TYPE_BODY_LG`] (18px) `TEXT_PRIMARY` caption — the spec calls
|
||||||
|
/// for 16px, but the engine type scale only carries 14/18/26/40/...
|
||||||
|
/// rungs; 18 is the closest rung that preserves the scale invariants
|
||||||
|
/// tested in `ui_theme::tests`.
|
||||||
|
/// - [`RADIUS_MD`] corners.
|
||||||
|
/// - Bottom-anchored absolute position; `bottom_pct` differs between
|
||||||
|
/// queued and immediate paths so they layer instead of overlap.
|
||||||
|
// The 8-argument signature is intentional — these are the per-toast
|
||||||
|
// layout values that genuinely differ between the queued and fire-and-
|
||||||
|
// forget call sites. A struct wrapper would just rename the same data.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn spawn_toast_node<B: Bundle>(
|
||||||
|
commands: &mut Commands,
|
||||||
|
bundle: B,
|
||||||
|
message: String,
|
||||||
|
variant: ToastVariant,
|
||||||
|
bottom_pct: Val,
|
||||||
|
left_pct: Val,
|
||||||
|
width_pct: Val,
|
||||||
|
padding: UiRect,
|
||||||
|
) -> Entity {
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
ToastOverlay,
|
bundle,
|
||||||
ToastTimer(duration_secs),
|
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
left: Val::Percent(25.0),
|
left: left_pct,
|
||||||
top: Val::Percent(42.0),
|
bottom: bottom_pct,
|
||||||
width: Val::Percent(50.0),
|
width: width_pct,
|
||||||
padding: UiRect::axes(VAL_SPACE_4, VAL_SPACE_3),
|
padding,
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
|
border: UiRect::all(Val::Px(1.0)),
|
||||||
|
border_radius: BorderRadius::all(Val::Px(RADIUS_MD)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.72)),
|
BackgroundColor(BG_ELEVATED),
|
||||||
|
BorderColor::all(variant.border_color()),
|
||||||
|
ZIndex(Z_TOAST),
|
||||||
))
|
))
|
||||||
.with_children(|b| {
|
.with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((
|
||||||
Text::new(message),
|
Text::new(message),
|
||||||
TextFont {
|
TextFont {
|
||||||
font_size: 32.0,
|
font_size: TYPE_BODY_LG,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
TextColor(ACCENT_PRIMARY),
|
TextColor(TEXT_PRIMARY),
|
||||||
));
|
));
|
||||||
});
|
})
|
||||||
|
.id()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -703,6 +820,41 @@ mod tests {
|
|||||||
assert!(anim_speed_to_secs(&AnimSpeed::Fast) < anim_speed_to_secs(&AnimSpeed::Normal));
|
assert!(anim_speed_to_secs(&AnimSpeed::Fast) < anim_speed_to_secs(&AnimSpeed::Normal));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pin every `ToastVariant` to its design-system border colour.
|
||||||
|
/// A future palette swap that touches `STATE_INFO`, `STATE_WARNING`,
|
||||||
|
/// `STATE_DANGER`, or `ACCENT_SECONDARY` flows through this mapping
|
||||||
|
/// automatically; this test guards against accidental remappings.
|
||||||
|
#[test]
|
||||||
|
fn toast_variant_border_colors_match_design_tokens() {
|
||||||
|
assert_eq!(ToastVariant::Info.border_color(), STATE_INFO);
|
||||||
|
assert_eq!(ToastVariant::Warning.border_color(), STATE_WARNING);
|
||||||
|
assert_eq!(ToastVariant::Error.border_color(), STATE_DANGER);
|
||||||
|
assert_eq!(ToastVariant::Celebration.border_color(), ACCENT_SECONDARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every `ToastVariant` resolves to a unique border colour so a
|
||||||
|
/// careless rebinding (e.g. accidentally setting `Warning` to the
|
||||||
|
/// same hue as `Info`) fails loudly. Pure check — does not run a
|
||||||
|
/// Bevy app.
|
||||||
|
#[test]
|
||||||
|
fn toast_variant_border_colors_are_distinct() {
|
||||||
|
let colors = [
|
||||||
|
ToastVariant::Info.border_color(),
|
||||||
|
ToastVariant::Warning.border_color(),
|
||||||
|
ToastVariant::Error.border_color(),
|
||||||
|
ToastVariant::Celebration.border_color(),
|
||||||
|
];
|
||||||
|
for i in 0..colors.len() {
|
||||||
|
for j in (i + 1)..colors.len() {
|
||||||
|
assert_ne!(
|
||||||
|
format!("{:?}", colors[i]),
|
||||||
|
format!("{:?}", colors[j]),
|
||||||
|
"variants {i} and {j} resolved to the same border colour",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn anim_speed_instant_is_zero() {
|
fn anim_speed_instant_is_zero() {
|
||||||
assert_eq!(anim_speed_to_secs(&AnimSpeed::Instant), 0.0);
|
assert_eq!(anim_speed_to_secs(&AnimSpeed::Instant), 0.0);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
//! Per-platform resolution of the user-themes directory.
|
//! Per-platform resolution of the user-themes directory.
|
||||||
//!
|
//!
|
||||||
//! The path is determined exactly once and exposed via
|
//! The path is determined exactly once and exposed via
|
||||||
//! [`user_theme_dir`]. On desktop platforms it is derived from
|
//! [`user_theme_dir`]. The base directory comes from
|
||||||
//! `dirs::data_dir()` (matching the rest of the project's
|
//! [`solitaire_data::data_dir`] (desktop: `dirs::data_dir()`;
|
||||||
//! per-app-storage convention); on mobile it must be supplied by the
|
//! Android: the hardcoded `/data/data/<package>/files` sandbox
|
||||||
//! platform entry point via [`set_user_theme_dir`] before any code
|
//! path). Mobile entry points may still override the path via
|
||||||
//! that needs the path executes — there is deliberately no silent
|
//! [`set_user_theme_dir`] when they need to point at a non-default
|
||||||
//! fallback because mobile sandboxing makes any guess we'd hard-code
|
//! location (e.g. tests, custom AssetManager wiring).
|
||||||
//! wrong.
|
|
||||||
//!
|
//!
|
||||||
//! # Why panic instead of returning Result?
|
//! # Why panic instead of returning Result?
|
||||||
//!
|
//!
|
||||||
@@ -35,17 +34,18 @@ const APP_DIR_NAME: &str = "solitaire_quest";
|
|||||||
/// Sub-folder under [`APP_DIR_NAME`] dedicated to user themes.
|
/// Sub-folder under [`APP_DIR_NAME`] dedicated to user themes.
|
||||||
const THEME_DIR_NAME: &str = "themes";
|
const THEME_DIR_NAME: &str = "themes";
|
||||||
|
|
||||||
/// Sets the user-themes directory at runtime — mobile-only API.
|
/// Sets the user-themes directory at runtime — escape hatch for
|
||||||
|
/// embedders or tests that need to override the platform default.
|
||||||
///
|
///
|
||||||
/// Returns `Err` containing the rejected path if the override has
|
/// Returns `Err` containing the rejected path if the override has
|
||||||
/// already been set. The first caller wins and subsequent calls are
|
/// already been set. The first caller wins and subsequent calls are
|
||||||
/// silently a no-op-with-feedback so a mis-configured embedder can't
|
/// silently a no-op-with-feedback so a mis-configured embedder can't
|
||||||
/// flip the path mid-session.
|
/// flip the path mid-session.
|
||||||
///
|
///
|
||||||
/// On desktop platforms this is functional but unnecessary —
|
/// Mostly unnecessary now that [`solitaire_data::data_dir`] handles
|
||||||
/// [`user_theme_dir`] derives the path from `dirs::data_dir` directly
|
/// every supported target — the override is kept for tests and for
|
||||||
/// and ignores the override. Setting it on desktop is harmless but
|
/// embedders that want a non-default location (e.g. a sandboxed
|
||||||
/// nearly always a sign of confusion.
|
/// AssetManager root on a future iOS port).
|
||||||
pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> {
|
pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> {
|
||||||
USER_THEME_DIR_OVERRIDE.set(path)
|
USER_THEME_DIR_OVERRIDE.set(path)
|
||||||
}
|
}
|
||||||
@@ -55,16 +55,10 @@ pub fn set_user_theme_dir(path: PathBuf) -> Result<(), PathBuf> {
|
|||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics on:
|
/// Panics if [`solitaire_data::data_dir`] returns `None`, which on
|
||||||
///
|
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration.
|
||||||
/// - Desktop, if `dirs::data_dir()` returns `None` (rare; usually
|
/// Android always returns `Some`. The panic message names the
|
||||||
/// indicates a broken `$HOME` or `$XDG_*` configuration).
|
/// supported workaround ([`set_user_theme_dir`]).
|
||||||
/// - Mobile, if no entry point has called [`set_user_theme_dir`] yet.
|
|
||||||
/// - Any other target, where the embedder is required to supply the
|
|
||||||
/// path manually.
|
|
||||||
///
|
|
||||||
/// The panic message names the missing piece so the failure is
|
|
||||||
/// immediately actionable.
|
|
||||||
pub fn user_theme_dir() -> PathBuf {
|
pub fn user_theme_dir() -> PathBuf {
|
||||||
if let Some(p) = USER_THEME_DIR_OVERRIDE.get() {
|
if let Some(p) = USER_THEME_DIR_OVERRIDE.get() {
|
||||||
return p.clone();
|
return p.clone();
|
||||||
@@ -79,45 +73,23 @@ fn user_theme_dir_for(data_dir: PathBuf) -> PathBuf {
|
|||||||
data_dir.join(APP_DIR_NAME).join(THEME_DIR_NAME)
|
data_dir.join(APP_DIR_NAME).join(THEME_DIR_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-target-os resolution of the platform's data dir. Split out so
|
/// Per-target-os resolution of the platform's data dir. Delegates
|
||||||
/// mobile branches can grow without disturbing desktop behaviour.
|
/// to [`solitaire_data::data_dir`] which encapsulates the
|
||||||
|
/// per-target shape (desktop: `dirs::data_dir()`; android: the
|
||||||
|
/// hardcoded `/data/data/<package>/files` sandbox path). Panics
|
||||||
|
/// only when the underlying resolver returns `None`, which on
|
||||||
|
/// desktop indicates a broken `$HOME` / `$XDG_*` configuration —
|
||||||
|
/// the panic message names the supported workaround.
|
||||||
fn detected_platform_data_dir() -> PathBuf {
|
fn detected_platform_data_dir() -> PathBuf {
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
solitaire_data::data_dir().unwrap_or_else(|| {
|
||||||
{
|
|
||||||
dirs::data_dir().unwrap_or_else(|| {
|
|
||||||
panic!(
|
|
||||||
"user_theme_dir(): platform data directory is unavailable. \
|
|
||||||
On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \
|
|
||||||
the OS reported no Application Support / AppData path. \
|
|
||||||
As a workaround call solitaire_engine::assets::user_dir::\
|
|
||||||
set_user_theme_dir() before App::run()."
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
||||||
{
|
|
||||||
panic!(
|
panic!(
|
||||||
"user_theme_dir(): mobile entry point must call \
|
"user_theme_dir(): platform data directory is unavailable. \
|
||||||
solitaire_engine::assets::user_dir::set_user_theme_dir() \
|
On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \
|
||||||
before App::run() — there is no platform default."
|
the OS reported no Application Support / AppData path. \
|
||||||
|
As a workaround call solitaire_engine::assets::user_dir::\
|
||||||
|
set_user_theme_dir() before App::run()."
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
#[cfg(not(any(
|
|
||||||
target_os = "linux",
|
|
||||||
target_os = "macos",
|
|
||||||
target_os = "windows",
|
|
||||||
target_os = "android",
|
|
||||||
target_os = "ios"
|
|
||||||
)))]
|
|
||||||
{
|
|
||||||
panic!(
|
|
||||||
"user_theme_dir(): unsupported platform; call \
|
|
||||||
solitaire_engine::assets::user_dir::set_user_theme_dir() \
|
|
||||||
from your entry point before App::run()."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -140,14 +112,16 @@ mod tests {
|
|||||||
assert_eq!(dir, PathBuf::from("solitaire_quest/themes"));
|
assert_eq!(dir, PathBuf::from("solitaire_quest/themes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detected_data_dir_yields_a_path_with_a_parent() {
|
fn detected_data_dir_yields_a_path_with_a_parent() {
|
||||||
// On every supported desktop platform the OS reports a
|
// On every supported target the platform resolver
|
||||||
// user-writable data directory; the test machine already has
|
// (`solitaire_data::data_dir`) returns a usable directory:
|
||||||
// one for `dirs::data_dir()` to discover. We don't pin the
|
// desktop targets via `dirs::data_dir()` (the test machine
|
||||||
// exact value because it depends on the user's $HOME, but it
|
// already has a `$HOME` for it to discover), Android via
|
||||||
// must at least be a non-empty path with a parent component.
|
// the hardcoded `/data/data/<package>/files` sandbox path.
|
||||||
|
// We don't pin the exact value because it depends on the
|
||||||
|
// user's `$HOME` on desktop, but it must at least be a
|
||||||
|
// non-empty path with a parent component.
|
||||||
let dir = detected_platform_data_dir();
|
let dir = detected_platform_data_dir();
|
||||||
assert!(dir.parent().is_some(), "data dir {dir:?} should be absolute");
|
assert!(dir.parent().is_some(), "data dir {dir:?} should be absolute");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ use crate::layout::{Layout, LayoutResource, LayoutSystem};
|
|||||||
use crate::pause_plugin::PausedResource;
|
use crate::pause_plugin::PausedResource;
|
||||||
use crate::resources::{DragState, GameStateResource};
|
use crate::resources::{DragState, GameStateResource};
|
||||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||||
use crate::table_plugin::PileMarker;
|
use crate::table_plugin::{PileMarker, PILE_MARKER_DEFAULT_COLOUR};
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_ALPHA_IDLE, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z,
|
CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_ALPHA_IDLE, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z,
|
||||||
CARD_SHADOW_OFFSET_DRAG, CARD_SHADOW_OFFSET_IDLE, CARD_SHADOW_PADDING_DRAG,
|
CARD_SHADOW_OFFSET_DRAG, CARD_SHADOW_OFFSET_IDLE, CARD_SHADOW_PADDING_DRAG,
|
||||||
CARD_SHADOW_PADDING_IDLE, STOCK_BADGE_BG, STOCK_BADGE_FG, TYPE_CAPTION, Z_STOCK_BADGE,
|
CARD_SHADOW_PADDING_IDLE, STOCK_BADGE_BG, STOCK_BADGE_FG, TEXT_PRIMARY, TYPE_CAPTION,
|
||||||
|
Z_STOCK_BADGE,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Fraction of card height used as vertical offset between face-up tableau cards.
|
/// Fraction of card height used as vertical offset between face-up tableau cards.
|
||||||
@@ -925,12 +926,17 @@ fn update_drag_shadow(
|
|||||||
commands.entity(e).insert(Transform::from_translation(shadow_pos));
|
commands.entity(e).insert(Transform::from_translation(shadow_pos));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Spawn a new shadow sprite.
|
// Spawn a new shadow sprite. Alpha tracks the per-card
|
||||||
|
// CARD_SHADOW_ALPHA_DRAG token so the Terminal palette's
|
||||||
|
// "no box-shadow" policy disables this stack shadow in
|
||||||
|
// lockstep with the per-card shadows. Re-enabling shadows
|
||||||
|
// is then a one-line change in `ui_theme`, not a hunt
|
||||||
|
// through plugin code.
|
||||||
let e = commands
|
let e = commands
|
||||||
.spawn((
|
.spawn((
|
||||||
ShadowEntity,
|
ShadowEntity,
|
||||||
Sprite {
|
Sprite {
|
||||||
color: Color::srgba(0.0, 0.0, 0.0, 0.35),
|
color: CARD_SHADOW_COLOR.with_alpha(CARD_SHADOW_ALPHA_DRAG),
|
||||||
custom_size: Some(Vec2::new(card_w + 8.0, card_h + 8.0)),
|
custom_size: Some(Vec2::new(card_w + 8.0, card_h + 8.0)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
@@ -1024,11 +1030,13 @@ fn tick_hint_highlight(
|
|||||||
// Task #46 — Right-click legal destination highlights
|
// Task #46 — Right-click legal destination highlights
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Color applied to a `PileMarker` sprite when it is a legal destination for
|
/// Lime tint applied to a `PileMarker` sprite when it is a legal
|
||||||
/// the right-clicked card.
|
/// destination for the right-clicked card. Same RGB as the design-
|
||||||
const RIGHT_CLICK_HIGHLIGHT_COLOUR: Color = Color::srgba(0.2, 0.8, 0.2, 0.6);
|
/// system [`STATE_SUCCESS`] token at 60% alpha. Spelled as a literal
|
||||||
/// Restored color for `PileMarker` sprites when the highlight is cleared.
|
/// because `Alpha::with_alpha` is not yet a `const` trait method on
|
||||||
const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
/// stable; the tracking test below pins the RGB to `STATE_SUCCESS`
|
||||||
|
/// so a palette swap can't drift the two apart silently.
|
||||||
|
const RIGHT_CLICK_HIGHLIGHT_COLOUR: Color = Color::srgba(0.675, 0.761, 0.404, 0.6);
|
||||||
|
|
||||||
/// Counts down `RightClickHighlightTimer` each frame and clears the highlight
|
/// Counts down `RightClickHighlightTimer` each frame and clears the highlight
|
||||||
/// when the timer expires.
|
/// when the timer expires.
|
||||||
@@ -1238,11 +1246,16 @@ fn find_top_card_at(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Sprite colour applied to the stock `PileMarker` when the stock pile is empty,
|
/// Sprite colour applied to the stock `PileMarker` when the stock pile is empty,
|
||||||
/// to signal to the player that there are no more cards to draw.
|
/// to signal to the player that there are no more cards to draw. Pure white
|
||||||
|
/// at 0.4 alpha — a deliberate brightness-boost over the default marker so
|
||||||
|
/// the "empty" state is more visible, not less. Not derived from a palette
|
||||||
|
/// token: this is a sprite tint, not chrome colour.
|
||||||
const STOCK_EMPTY_DIM_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.4);
|
const STOCK_EMPTY_DIM_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.4);
|
||||||
|
|
||||||
/// Sprite colour applied to the stock `PileMarker` when cards remain in stock.
|
/// Sprite colour applied to the stock `PileMarker` when cards remain in
|
||||||
const STOCK_NORMAL_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
/// stock. Aliased to [`PILE_MARKER_DEFAULT_COLOUR`] so it tracks the rest
|
||||||
|
/// of the engine's idle pile-marker tint automatically.
|
||||||
|
const STOCK_NORMAL_COLOUR: Color = PILE_MARKER_DEFAULT_COLOUR;
|
||||||
|
|
||||||
/// Shared logic for updating the stock pile marker's dim state and "↺" label.
|
/// Shared logic for updating the stock pile marker's dim state and "↺" label.
|
||||||
///
|
///
|
||||||
@@ -1283,7 +1296,7 @@ fn apply_stock_empty_indicator<F: bevy::ecs::query::QueryFilter>(
|
|||||||
StockEmptyLabel,
|
StockEmptyLabel,
|
||||||
Text2d::new("↺"),
|
Text2d::new("↺"),
|
||||||
TextFont { font_size, ..default() },
|
TextFont { font_size, ..default() },
|
||||||
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.7)),
|
TextColor(TEXT_PRIMARY.with_alpha(0.7)),
|
||||||
Transform::from_xyz(0.0, 0.0, 0.1),
|
Transform::from_xyz(0.0, 0.0, 0.1),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@@ -2329,9 +2342,15 @@ mod tests {
|
|||||||
|
|
||||||
assert_ne!(idle_offset, drag_offset, "drag offset must differ from idle");
|
assert_ne!(idle_offset, drag_offset, "drag offset must differ from idle");
|
||||||
assert_ne!(idle_padding, drag_padding, "drag padding must differ from idle");
|
assert_ne!(idle_padding, drag_padding, "drag padding must differ from idle");
|
||||||
|
// Under the Terminal design system both alphas are pinned to 0
|
||||||
|
// (depth comes from 1px borders + tonal layering, no `box-shadow`).
|
||||||
|
// The invariant we still enforce is "drag never weaker than idle"
|
||||||
|
// — so an accidental swap of the two constants fails loudly,
|
||||||
|
// and a future palette that re-enables shadows still has to keep
|
||||||
|
// the lift cue stronger than the rest state.
|
||||||
assert!(
|
assert!(
|
||||||
drag_alpha > idle_alpha,
|
drag_alpha >= idle_alpha,
|
||||||
"drag alpha must be stronger than idle (got drag={drag_alpha}, idle={idle_alpha})"
|
"drag alpha must not be weaker than idle (got drag={drag_alpha}, idle={idle_alpha})"
|
||||||
);
|
);
|
||||||
// Drag offset magnitude should be larger than idle so the parallax
|
// Drag offset magnitude should be larger than idle so the parallax
|
||||||
// reads as "lifted".
|
// reads as "lifted".
|
||||||
@@ -2700,4 +2719,20 @@ mod tests {
|
|||||||
"after a theme apply the theme_back slot must hold the theme's back handle",
|
"after a theme apply the theme_back slot must hold the theme's back handle",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `RIGHT_CLICK_HIGHLIGHT_COLOUR` is spelled as a literal because
|
||||||
|
/// `Alpha::with_alpha` is not a `const` trait method on stable.
|
||||||
|
/// This test pins its RGB to the design-system `STATE_SUCCESS`
|
||||||
|
/// token so a future palette swap that updates the token but
|
||||||
|
/// forgets the right-click highlight fails loudly here.
|
||||||
|
#[test]
|
||||||
|
fn right_click_highlight_rgb_tracks_state_success_token() {
|
||||||
|
use crate::ui_theme::STATE_SUCCESS;
|
||||||
|
let highlight = RIGHT_CLICK_HIGHLIGHT_COLOUR.to_srgba();
|
||||||
|
let success = STATE_SUCCESS.to_srgba();
|
||||||
|
assert!((highlight.red - success.red).abs() < 1e-6);
|
||||||
|
assert!((highlight.green - success.green).abs() < 1e-6);
|
||||||
|
assert!((highlight.blue - success.blue).abs() < 1e-6);
|
||||||
|
assert!((highlight.alpha - 0.6).abs() < 1e-6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,17 +41,28 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
|||||||
use crate::card_plugin::{RightClickHighlight, TABLEAU_FAN_FRAC};
|
use crate::card_plugin::{RightClickHighlight, TABLEAU_FAN_FRAC};
|
||||||
use crate::layout::{Layout, LayoutResource};
|
use crate::layout::{Layout, LayoutResource};
|
||||||
use crate::resources::{DragState, GameStateResource};
|
use crate::resources::{DragState, GameStateResource};
|
||||||
use crate::table_plugin::PileMarker;
|
use crate::table_plugin::{PileMarker, PILE_MARKER_DEFAULT_COLOUR};
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
DROP_TARGET_FILL, DROP_TARGET_OUTLINE, DROP_TARGET_OUTLINE_PX, Z_DROP_OVERLAY,
|
DROP_TARGET_FILL, DROP_TARGET_OUTLINE, DROP_TARGET_OUTLINE_PX, Z_DROP_OVERLAY,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Semi-transparent white that `table_plugin` uses for idle pile markers.
|
/// Idle pile-marker tint — re-exported from `table_plugin` so the
|
||||||
/// Kept in sync with the `marker_colour` constant there.
|
/// "valid drop" toggle in this plugin and the marker spawn in
|
||||||
const MARKER_DEFAULT: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
/// `table_plugin` cannot drift apart. Was previously a duplicated
|
||||||
|
/// literal kept in sync via doc comment.
|
||||||
|
const MARKER_DEFAULT: Color = PILE_MARKER_DEFAULT_COLOUR;
|
||||||
|
|
||||||
/// Green tint applied to pile markers that are valid drop targets during drag.
|
/// Lime tint applied to pile markers that are valid drop targets during
|
||||||
const MARKER_VALID: Color = Color::srgba(0.15, 0.85, 0.25, 0.55);
|
/// a drag. Same RGB as the design-system [`STATE_SUCCESS`] token at 55%
|
||||||
|
/// alpha, so the in-game "this is a legal target" colour stays
|
||||||
|
/// consistent with foundation-completion flourishes and other
|
||||||
|
/// valid-move signals. Spelled as a literal because `Alpha::with_alpha`
|
||||||
|
/// is not yet a `const` trait method on stable; the tracking test
|
||||||
|
/// below pins the RGB to `STATE_SUCCESS` so a palette swap can't drift
|
||||||
|
/// the two apart silently. Distinct from [`DROP_TARGET_FILL`] (10%
|
||||||
|
/// alpha) because the marker sprite is thin and would otherwise wash
|
||||||
|
/// out at a similar opacity.
|
||||||
|
const MARKER_VALID: Color = Color::srgba(0.675, 0.761, 0.404, 0.55);
|
||||||
|
|
||||||
/// Marker component on a parent entity that owns one drop-target overlay
|
/// Marker component on a parent entity that owns one drop-target overlay
|
||||||
/// (a translucent fill plus four outline edges as children). The wrapped
|
/// (a translucent fill plus four outline edges as children). The wrapped
|
||||||
@@ -524,6 +535,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn marker_valid_rgb_tracks_state_success_token() {
|
||||||
|
// `MARKER_VALID` is spelled as a literal because
|
||||||
|
// `Alpha::with_alpha` is not a `const` trait method on stable.
|
||||||
|
// This test pins its RGB to `STATE_SUCCESS` so a future
|
||||||
|
// palette swap that updates the token but forgets the marker
|
||||||
|
// fails loudly here.
|
||||||
|
use crate::ui_theme::STATE_SUCCESS;
|
||||||
|
let valid = MARKER_VALID.to_srgba();
|
||||||
|
let success = STATE_SUCCESS.to_srgba();
|
||||||
|
assert!((valid.red - success.red).abs() < 1e-6);
|
||||||
|
assert!((valid.green - success.green).abs() < 1e-6);
|
||||||
|
assert!((valid.blue - success.blue).abs() < 1e-6);
|
||||||
|
assert!((valid.alpha - 0.55).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// pick_cursor_icon priority-order tests
|
// pick_cursor_icon priority-order tests
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
//! Optional on-screen FPS / frame-time overlay.
|
||||||
|
//!
|
||||||
|
//! Wraps Bevy's [`FrameTimeDiagnosticsPlugin`] and renders a tiny
|
||||||
|
//! corner readout that the developer (or a curious player) can toggle
|
||||||
|
//! with `F3`. Hidden by default — production builds ship the plugin
|
||||||
|
//! but the overlay starts invisible, so the production HUD is never
|
||||||
|
//! cluttered unless explicitly summoned.
|
||||||
|
//!
|
||||||
|
//! Why this exists: with an Android port on the roadmap, "feels
|
||||||
|
//! slow" became a real risk to plan around. A togglable FPS / frame-
|
||||||
|
//! time pair gives us a numeric baseline we can quote across desktop
|
||||||
|
//! and mobile, instead of optimising on vibes.
|
||||||
|
//!
|
||||||
|
//! ## Display contract
|
||||||
|
//!
|
||||||
|
//! When visible, the overlay reads `"FPS NN \u{2022} M.MM ms"` in a
|
||||||
|
//! small monospaced cell, anchored top-right. Both numbers are the
|
||||||
|
//! `smoothed()` value (Bevy's exponential moving average) — peak
|
||||||
|
//! and worst-case readings would jitter the text every frame, which
|
||||||
|
//! is harder to glance at than a smoothed reading.
|
||||||
|
//!
|
||||||
|
//! ## Hotkey scope
|
||||||
|
//!
|
||||||
|
//! `F3` is a global, gameplay-blockable toggle: the system reads
|
||||||
|
//! `ButtonInput<KeyCode>` directly and ignores the rest of the modal
|
||||||
|
//! / pause stack. The overlay is informational and shouldn't depend
|
||||||
|
//! on game state.
|
||||||
|
|
||||||
|
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::font_plugin::FontResource;
|
||||||
|
use crate::ui_theme::Z_SPLASH;
|
||||||
|
|
||||||
|
/// Z-index for the diagnostics HUD — above every modal / toast /
|
||||||
|
/// splash layer so a developer can always see the readout, no matter
|
||||||
|
/// what overlay is up.
|
||||||
|
const Z_DIAGNOSTICS_HUD: i32 = Z_SPLASH + 100;
|
||||||
|
|
||||||
|
/// Width-stable font size for the readout. Hand-tuned literal — the
|
||||||
|
/// HUD is a developer affordance and uses its own sizing rather than
|
||||||
|
/// borrowing a typography token whose meaning may drift.
|
||||||
|
const DIAGNOSTICS_FONT_SIZE: f32 = 12.0;
|
||||||
|
|
||||||
|
/// Background alpha for the readout cell. Translucent so the HUD
|
||||||
|
/// doesn't fully obscure whatever's behind it but stays legible.
|
||||||
|
const DIAGNOSTICS_BG_ALPHA: f32 = 0.7;
|
||||||
|
|
||||||
|
/// Wires the FPS / frame-time HUD overlay.
|
||||||
|
///
|
||||||
|
/// Adds [`FrameTimeDiagnosticsPlugin`] (no-op if already added — the
|
||||||
|
/// plugin's `Plugin::build` is idempotent on duplicate registration
|
||||||
|
/// in our codebase since no other site adds it). Spawns the HUD
|
||||||
|
/// hidden, registers the toggle handler, and wires the per-frame
|
||||||
|
/// text refresh.
|
||||||
|
pub struct DiagnosticsHudPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DiagnosticsHudPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(FrameTimeDiagnosticsPlugin::default())
|
||||||
|
.init_resource::<DiagnosticsHudVisible>()
|
||||||
|
.add_systems(Startup, spawn_diagnostics_hud)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(toggle_diagnostics_hud, update_diagnostics_hud).chain(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks whether the overlay is currently visible. Flipped by the
|
||||||
|
/// `F3` toggle; defaults to hidden so production launches start clean.
|
||||||
|
#[derive(Resource, Debug, Default)]
|
||||||
|
struct DiagnosticsHudVisible(bool);
|
||||||
|
|
||||||
|
/// Marker on the overlay's root Node — used to flip `Visibility`.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct DiagnosticsHudRoot;
|
||||||
|
|
||||||
|
/// Marker on the readout `Text` node — used by the per-frame refresh
|
||||||
|
/// system to find the right text to overwrite.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct DiagnosticsHudText;
|
||||||
|
|
||||||
|
/// Spawns the (initially-hidden) overlay at startup. Anchored
|
||||||
|
/// top-right with absolute positioning so it never participates in
|
||||||
|
/// the rest of the UI flex tree.
|
||||||
|
fn spawn_diagnostics_hud(mut commands: Commands, font_res: Option<Res<FontResource>>) {
|
||||||
|
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
|
let bg = Color::srgba(0.0, 0.0, 0.0, DIAGNOSTICS_BG_ALPHA);
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
DiagnosticsHudRoot,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
top: Val::Px(8.0),
|
||||||
|
right: Val::Px(8.0),
|
||||||
|
padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(bg),
|
||||||
|
Visibility::Hidden,
|
||||||
|
GlobalZIndex(Z_DIAGNOSTICS_HUD),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
DiagnosticsHudText,
|
||||||
|
Text::new("FPS \u{2014}"),
|
||||||
|
TextFont {
|
||||||
|
font: font_handle,
|
||||||
|
font_size: DIAGNOSTICS_FONT_SIZE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::WHITE),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `F3` flips the visible flag and the overlay's `Visibility`. Reads
|
||||||
|
/// the keyboard input directly so it isn't gated by pause / modal
|
||||||
|
/// state — diagnostics should always be reachable.
|
||||||
|
fn toggle_diagnostics_hud(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut visible: ResMut<DiagnosticsHudVisible>,
|
||||||
|
mut roots: Query<&mut Visibility, With<DiagnosticsHudRoot>>,
|
||||||
|
) {
|
||||||
|
if !keys.just_pressed(KeyCode::F3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visible.0 = !visible.0;
|
||||||
|
let target = if visible.0 {
|
||||||
|
Visibility::Visible
|
||||||
|
} else {
|
||||||
|
Visibility::Hidden
|
||||||
|
};
|
||||||
|
for mut v in &mut roots {
|
||||||
|
*v = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the smoothed FPS + frame-time diagnostics each frame and
|
||||||
|
/// rewrites the readout text. Skipped while the overlay is hidden so
|
||||||
|
/// we don't pay the diagnostic-store lookup or the text mutation
|
||||||
|
/// when nobody's looking.
|
||||||
|
fn update_diagnostics_hud(
|
||||||
|
diagnostics: Res<DiagnosticsStore>,
|
||||||
|
visible: Res<DiagnosticsHudVisible>,
|
||||||
|
mut texts: Query<&mut Text, With<DiagnosticsHudText>>,
|
||||||
|
) {
|
||||||
|
if !visible.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fps = diagnostics
|
||||||
|
.get(&FrameTimeDiagnosticsPlugin::FPS)
|
||||||
|
.and_then(|d| d.smoothed())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let frame_time_ms = diagnostics
|
||||||
|
.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME)
|
||||||
|
.and_then(|d| d.smoothed())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
for mut text in &mut texts {
|
||||||
|
**text = format!("FPS {fps:.0} \u{2022} {frame_time_ms:.2} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,6 +184,8 @@ const CONTROL_SECTIONS: &[ControlSection] = &[
|
|||||||
ControlSection {
|
ControlSection {
|
||||||
title: "Overlays",
|
title: "Overlays",
|
||||||
rows: &[
|
rows: &[
|
||||||
|
ControlRow { keys: "M", description: "Mode launcher (Home)" },
|
||||||
|
ControlRow { keys: "P", description: "Profile" },
|
||||||
ControlRow { keys: "S", description: "Stats & progression" },
|
ControlRow { keys: "S", description: "Stats & progression" },
|
||||||
ControlRow { keys: "A", description: "Achievements" },
|
ControlRow { keys: "A", description: "Achievements" },
|
||||||
ControlRow { keys: "L", description: "Leaderboard" },
|
ControlRow { keys: "L", description: "Leaderboard" },
|
||||||
@@ -192,6 +194,7 @@ const CONTROL_SECTIONS: &[ControlSection] = &[
|
|||||||
ControlRow { keys: "F11", description: "Toggle fullscreen" },
|
ControlRow { keys: "F11", description: "Toggle fullscreen" },
|
||||||
ControlRow { keys: "Esc", description: "Pause / resume" },
|
ControlRow { keys: "Esc", description: "Pause / resume" },
|
||||||
ControlRow { keys: "[ / ]", description: "SFX volume down / up" },
|
ControlRow { keys: "[ / ]", description: "SFX volume down / up" },
|
||||||
|
ControlRow { keys: "Enter", description: "Play Again (on the Win Summary)" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ use crate::card_plugin::{
|
|||||||
CardEntity, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC, TABLEAU_FACEDOWN_FAN_FRAC,
|
CardEntity, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC, TABLEAU_FACEDOWN_FAN_FRAC,
|
||||||
TABLEAU_FAN_FRAC,
|
TABLEAU_FAN_FRAC,
|
||||||
};
|
};
|
||||||
use crate::ui_theme::MOTION_DRAG_REJECT_SECS;
|
use crate::ui_theme::{MOTION_DRAG_REJECT_SECS, STATE_WARNING};
|
||||||
use solitaire_core::game_state::DrawMode;
|
use solitaire_core::game_state::DrawMode;
|
||||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
@@ -335,8 +335,11 @@ pub fn emit_hint_visuals(
|
|||||||
for (entity, card_entity, mut sprite) in card_entities.iter_mut() {
|
for (entity, card_entity, mut sprite) in card_entities.iter_mut() {
|
||||||
if card_entity.card_id == card_id {
|
if card_entity.card_id == card_id {
|
||||||
// Tint the card gold without replacing the Sprite (which would
|
// Tint the card gold without replacing the Sprite (which would
|
||||||
// discard the image handle set by CardImageSet).
|
// discard the image handle set by CardImageSet). Uses the
|
||||||
sprite.color = Color::srgba(1.0, 1.0, 0.4, 1.0);
|
// design-system `STATE_WARNING` token so the source-card
|
||||||
|
// tint matches the destination pile highlight, both of
|
||||||
|
// which signal "look here" for the hint.
|
||||||
|
sprite.color = STATE_WARNING;
|
||||||
commands.entity(entity)
|
commands.entity(entity)
|
||||||
.insert(HintHighlight { remaining: 2.0 })
|
.insert(HintHighlight { remaining: 2.0 })
|
||||||
.insert(HintHighlightTimer(2.0));
|
.insert(HintHighlightTimer(2.0));
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub mod feedback_anim_plugin;
|
|||||||
pub mod challenge_plugin;
|
pub mod challenge_plugin;
|
||||||
pub mod cursor_plugin;
|
pub mod cursor_plugin;
|
||||||
pub mod daily_challenge_plugin;
|
pub mod daily_challenge_plugin;
|
||||||
|
pub mod diagnostics_hud;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod game_plugin;
|
pub mod game_plugin;
|
||||||
pub mod help_plugin;
|
pub mod help_plugin;
|
||||||
@@ -85,6 +86,7 @@ pub use card_plugin::{
|
|||||||
};
|
};
|
||||||
pub use font_plugin::{FontPlugin, FontResource};
|
pub use font_plugin::{FontPlugin, FontResource};
|
||||||
pub use cursor_plugin::CursorPlugin;
|
pub use cursor_plugin::CursorPlugin;
|
||||||
|
pub use diagnostics_hud::DiagnosticsHudPlugin;
|
||||||
pub use events::{
|
pub use events::{
|
||||||
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
AchievementUnlockedEvent, CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent,
|
||||||
ForfeitEvent, ForfeitRequestEvent, FoundationCompletedEvent, GameWonEvent, HelpRequestEvent,
|
ForfeitEvent, ForfeitRequestEvent, FoundationCompletedEvent, GameWonEvent, HelpRequestEvent,
|
||||||
|
|||||||
@@ -208,8 +208,9 @@ fn spawn_overlay(
|
|||||||
GlobalZIndex(Z_REPLAY_OVERLAY),
|
GlobalZIndex(Z_REPLAY_OVERLAY),
|
||||||
))
|
))
|
||||||
.with_children(|banner| {
|
.with_children(|banner| {
|
||||||
// Left: "Replay" label in the loud yellow accent so it reads
|
// Left: "Replay" label in the cyan primary accent
|
||||||
// unmistakably as a non-gameplay surface.
|
// (`ACCENT_PRIMARY`) so it reads unmistakably as a
|
||||||
|
// non-gameplay surface.
|
||||||
banner.spawn((
|
banner.spawn((
|
||||||
ReplayOverlayBannerText,
|
ReplayOverlayBannerText,
|
||||||
Text::new(banner_label),
|
Text::new(banner_label),
|
||||||
@@ -236,7 +237,7 @@ fn spawn_overlay(
|
|||||||
|
|
||||||
// Right: Stop button. Tertiary variant — the action is
|
// Right: Stop button. Tertiary variant — the action is
|
||||||
// available but not the loudest element in the banner; the
|
// available but not the loudest element in the banner; the
|
||||||
// "Replay" yellow accent owns that slot. `spawn_modal_button`
|
// "Replay" cyan accent owns that slot. `spawn_modal_button`
|
||||||
// gives us hover / press paint and focus rings for free via
|
// gives us hover / press paint and focus rings for free via
|
||||||
// the existing `UiModalPlugin` paint system.
|
// the existing `UiModalPlugin` paint system.
|
||||||
banner
|
banner
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ use crate::input_plugin::{best_destination, best_tableau_destination_for_stack};
|
|||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
use crate::pause_plugin::PausedResource;
|
use crate::pause_plugin::PausedResource;
|
||||||
use crate::resources::{DragState, GameStateResource};
|
use crate::resources::{DragState, GameStateResource};
|
||||||
|
use crate::ui_theme::{ACCENT_PRIMARY, STATE_SUCCESS, STATE_WARNING};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public types
|
// Public types
|
||||||
@@ -660,14 +661,18 @@ fn update_selection_highlight(
|
|||||||
};
|
};
|
||||||
let card_size = layout.0.card_size;
|
let card_size = layout.0.card_size;
|
||||||
|
|
||||||
// Choose colours per mode: cyan in source-pick, gold while lifted.
|
// Highlight tints follow the Terminal palette's semantic state
|
||||||
|
// tokens: cyan focus/selection while picking the source, gold
|
||||||
|
// attention/commitment once the cards are lifted, lime valid-move
|
||||||
|
// tint on the destination. Alphas are kept non-zero so the card
|
||||||
|
// face beneath remains readable through the wash.
|
||||||
let lifted = kbd_drag.is_lifted();
|
let lifted = kbd_drag.is_lifted();
|
||||||
let source_color = if lifted {
|
let source_color = if lifted {
|
||||||
Color::srgba(1.0, 0.84, 0.0, 0.6)
|
STATE_WARNING.with_alpha(0.6)
|
||||||
} else {
|
} else {
|
||||||
Color::srgba(0.0, 1.0, 1.0, 0.5)
|
ACCENT_PRIMARY.with_alpha(0.5)
|
||||||
};
|
};
|
||||||
let dest_color = Color::srgba(0.0, 1.0, 0.4, 0.6);
|
let dest_color = STATE_SUCCESS.with_alpha(0.6);
|
||||||
|
|
||||||
// Resolve the source pile from KeyboardDragState (when lifted) or
|
// Resolve the source pile from KeyboardDragState (when lifted) or
|
||||||
// SelectionState (otherwise). Lifted takes precedence so the gold
|
// SelectionState (otherwise). Lifted takes precedence so the gold
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ struct ReplayMoveIntervalText;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
struct WinnableDealsOnlyText;
|
struct WinnableDealsOnlyText;
|
||||||
|
|
||||||
|
/// Marks the `Text` node showing the current "Smart window size"
|
||||||
|
/// state ("ON" / "OFF") in the Gameplay section. The flag is stored
|
||||||
|
/// negatively in `Settings::disable_smart_default_size`, so the
|
||||||
|
/// label inverts: "ON" = smart sizing enabled (the default).
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct SmartDefaultSizeText;
|
||||||
|
|
||||||
/// Marks the scrollable inner card so the mouse-wheel system can target it.
|
/// Marks the scrollable inner card so the mouse-wheel system can target it.
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
struct SettingsPanelScrollable;
|
struct SettingsPanelScrollable;
|
||||||
@@ -199,6 +206,13 @@ enum SettingsButton {
|
|||||||
/// [`solitaire_core::solver::try_solve`] until one is provably
|
/// [`solitaire_core::solver::try_solve`] until one is provably
|
||||||
/// winnable (or the retry cap is hit). Off by default.
|
/// winnable (or the retry cap is hit). Off by default.
|
||||||
ToggleWinnableDealsOnly,
|
ToggleWinnableDealsOnly,
|
||||||
|
/// Toggle the inverse of [`Settings::disable_smart_default_size`].
|
||||||
|
/// When the visible label reads "ON", the launch-time window
|
||||||
|
/// sizer scales the window to ~70 % of the primary monitor on a
|
||||||
|
/// fresh install; "OFF" pins the literal 1280×800 baseline. The
|
||||||
|
/// flag only affects launches without saved geometry — the
|
||||||
|
/// player's last window size always wins.
|
||||||
|
ToggleSmartDefaultSize,
|
||||||
SyncNow,
|
SyncNow,
|
||||||
Done,
|
Done,
|
||||||
/// Select a specific card-back by index from the picker row.
|
/// Select a specific card-back by index from the picker row.
|
||||||
@@ -236,6 +250,8 @@ impl SettingsButton {
|
|||||||
// sits between TimeBonusUp (48) and the Cosmetic section.
|
// sits between TimeBonusUp (48) and the Cosmetic section.
|
||||||
SettingsButton::ReplayMoveIntervalDown => 49,
|
SettingsButton::ReplayMoveIntervalDown => 49,
|
||||||
SettingsButton::ReplayMoveIntervalUp => 49,
|
SettingsButton::ReplayMoveIntervalUp => 49,
|
||||||
|
// Smart-default-size toggle — sits at the end of Gameplay.
|
||||||
|
SettingsButton::ToggleSmartDefaultSize => 50,
|
||||||
// Cosmetic section
|
// Cosmetic section
|
||||||
SettingsButton::ToggleTheme => 55,
|
SettingsButton::ToggleTheme => 55,
|
||||||
SettingsButton::ToggleColorBlind => 60,
|
SettingsButton::ToggleColorBlind => 60,
|
||||||
@@ -330,6 +346,7 @@ impl Plugin for SettingsPlugin {
|
|||||||
update_time_bonus_multiplier_text,
|
update_time_bonus_multiplier_text,
|
||||||
update_replay_move_interval_text,
|
update_replay_move_interval_text,
|
||||||
update_winnable_deals_only_text,
|
update_winnable_deals_only_text,
|
||||||
|
update_smart_default_size_text,
|
||||||
attach_focusable_to_settings_buttons,
|
attach_focusable_to_settings_buttons,
|
||||||
scroll_focus_into_view,
|
scroll_focus_into_view,
|
||||||
),
|
),
|
||||||
@@ -600,6 +617,21 @@ fn update_winnable_deals_only_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refreshes the live "Smart window size" toggle value whenever
|
||||||
|
/// `SettingsResource` changes. The flag is stored negatively as
|
||||||
|
/// `disable_smart_default_size`, so the label inverts.
|
||||||
|
fn update_smart_default_size_text(
|
||||||
|
settings: Res<SettingsResource>,
|
||||||
|
mut text_nodes: Query<&mut Text, With<SmartDefaultSizeText>>,
|
||||||
|
) {
|
||||||
|
if !settings.is_changed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for mut text in &mut text_nodes {
|
||||||
|
**text = smart_default_size_label(!settings.0.disable_smart_default_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Refreshes the live tooltip-delay value in the Gameplay section
|
/// Refreshes the live tooltip-delay value in the Gameplay section
|
||||||
/// whenever `SettingsResource` changes (slider buttons, hand-edited
|
/// whenever `SettingsResource` changes (slider buttons, hand-edited
|
||||||
/// settings.json reload, etc.).
|
/// settings.json reload, etc.).
|
||||||
@@ -854,6 +886,17 @@ fn handle_settings_buttons(
|
|||||||
// The Text node is refreshed by `update_winnable_deals_only_text`
|
// The Text node is refreshed by `update_winnable_deals_only_text`
|
||||||
// on the next frame via `settings.is_changed()`.
|
// on the next frame via `settings.is_changed()`.
|
||||||
}
|
}
|
||||||
|
SettingsButton::ToggleSmartDefaultSize => {
|
||||||
|
settings.0.disable_smart_default_size =
|
||||||
|
!settings.0.disable_smart_default_size;
|
||||||
|
persist(&path, &settings.0);
|
||||||
|
changed.write(SettingsChangedEvent(settings.0.clone()));
|
||||||
|
// The Text node is refreshed by
|
||||||
|
// `update_smart_default_size_text` next frame. The
|
||||||
|
// sizer system is gated only at startup, so flipping
|
||||||
|
// this mid-session takes effect on the next launch —
|
||||||
|
// documented on the field in `solitaire_data::Settings`.
|
||||||
|
}
|
||||||
SettingsButton::SelectCardBack(idx) => {
|
SettingsButton::SelectCardBack(idx) => {
|
||||||
settings.0.selected_card_back = *idx;
|
settings.0.selected_card_back = *idx;
|
||||||
persist(&path, &settings.0);
|
persist(&path, &settings.0);
|
||||||
@@ -915,6 +958,14 @@ fn winnable_deals_only_label(enabled: bool) -> String {
|
|||||||
if enabled { "ON".into() } else { "OFF".into() }
|
if enabled { "ON".into() } else { "OFF".into() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display string for the "Smart window size" toggle. The argument
|
||||||
|
/// is the *enabled* state (i.e. the inverse of the underlying
|
||||||
|
/// `disable_smart_default_size` field) so reading the label gives
|
||||||
|
/// the player intuitive ON/OFF semantics.
|
||||||
|
fn smart_default_size_label(enabled: bool) -> String {
|
||||||
|
if enabled { "ON".into() } else { "OFF".into() }
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats the tooltip-hover delay for display in the Settings panel.
|
/// Formats the tooltip-hover delay for display in the Settings panel.
|
||||||
/// `0.0` reads as `"Instant"` so the zero-delay case has a name; any
|
/// `0.0` reads as `"Instant"` so the zero-delay case has a name; any
|
||||||
/// other value prints as `"{n:.1} s"` (e.g. `"0.5 s"`, `"1.2 s"`).
|
/// other value prints as `"{n:.1} s"` (e.g. `"0.5 s"`, `"1.2 s"`).
|
||||||
@@ -1303,6 +1354,17 @@ fn spawn_settings_panel(
|
|||||||
settings.replay_move_interval_secs,
|
settings.replay_move_interval_secs,
|
||||||
font_res,
|
font_res,
|
||||||
);
|
);
|
||||||
|
toggle_row(
|
||||||
|
body,
|
||||||
|
"Smart window size",
|
||||||
|
SmartDefaultSizeText,
|
||||||
|
smart_default_size_label(!settings.disable_smart_default_size),
|
||||||
|
SettingsButton::ToggleSmartDefaultSize,
|
||||||
|
"When ON, fresh launches resize the window to ~70 % of the \
|
||||||
|
monitor. OFF pins the 1280\u{00D7}800 baseline. Saved \
|
||||||
|
window size always wins.",
|
||||||
|
font_res,
|
||||||
|
);
|
||||||
|
|
||||||
// --- Cosmetic ---
|
// --- Cosmetic ---
|
||||||
section_label(body, "Cosmetic", font_res);
|
section_label(body, "Cosmetic", font_res);
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ struct SplashTitle;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
struct SplashSubtitle;
|
struct SplashSubtitle;
|
||||||
|
|
||||||
|
/// Marker on the cyan "terminal cursor" block (`▌`) painted above the
|
||||||
|
/// title. Visual signature of the Terminal design system per
|
||||||
|
/// `docs/ui-mockups/design-system.md` — the same `#6fc2ef` block
|
||||||
|
/// appears on the card-back theme, on the splash, and (per spec) is
|
||||||
|
/// the project's cursor motif. Faded together with the rest of the
|
||||||
|
/// splash so the dissolve still reads as one layer.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
struct SplashCursor;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Systems
|
// Systems
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -129,6 +138,14 @@ fn spawn_splash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
let font_handle = font_res.map(|f| f.0.clone()).unwrap_or_default();
|
||||||
|
let cursor_font = TextFont {
|
||||||
|
font: font_handle.clone(),
|
||||||
|
// Larger than TYPE_DISPLAY so the cursor block reads as the
|
||||||
|
// signature element above the wordmark. Hand-tuned literal —
|
||||||
|
// a one-off display character outside the regular text scale.
|
||||||
|
font_size: 96.0,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
let title_font = TextFont {
|
let title_font = TextFont {
|
||||||
font: font_handle.clone(),
|
font: font_handle.clone(),
|
||||||
font_size: TYPE_DISPLAY,
|
font_size: TYPE_DISPLAY,
|
||||||
@@ -171,6 +188,12 @@ fn spawn_splash(
|
|||||||
GlobalZIndex(Z_SPLASH),
|
GlobalZIndex(Z_SPLASH),
|
||||||
))
|
))
|
||||||
.with_children(|root| {
|
.with_children(|root| {
|
||||||
|
root.spawn((
|
||||||
|
SplashCursor,
|
||||||
|
Text::new("\u{258C}"), // ▌ — the Terminal cursor block.
|
||||||
|
cursor_font,
|
||||||
|
TextColor(initial_title),
|
||||||
|
));
|
||||||
root.spawn((
|
root.spawn((
|
||||||
SplashTitle,
|
SplashTitle,
|
||||||
Text::new("Solitaire Quest"),
|
Text::new("Solitaire Quest"),
|
||||||
@@ -219,12 +242,23 @@ fn splash_alpha(age: Duration) -> Option<f32> {
|
|||||||
/// scrim + text alpha, despawning the splash once the timeline
|
/// scrim + text alpha, despawning the splash once the timeline
|
||||||
/// finishes. Despawns with descendants so the title and subtitle leave
|
/// finishes. Despawns with descendants so the title and subtitle leave
|
||||||
/// the world together.
|
/// the world together.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
fn advance_splash(
|
fn advance_splash(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut roots: Query<(Entity, &mut SplashAge, &mut BackgroundColor, &Children), With<SplashRoot>>,
|
mut roots: Query<(Entity, &mut SplashAge, &mut BackgroundColor, &Children), With<SplashRoot>>,
|
||||||
mut titles: Query<&mut TextColor, (With<SplashTitle>, Without<SplashSubtitle>)>,
|
mut titles: Query<
|
||||||
mut subtitles: Query<&mut TextColor, (With<SplashSubtitle>, Without<SplashTitle>)>,
|
&mut TextColor,
|
||||||
|
(With<SplashTitle>, Without<SplashSubtitle>, Without<SplashCursor>),
|
||||||
|
>,
|
||||||
|
mut subtitles: Query<
|
||||||
|
&mut TextColor,
|
||||||
|
(With<SplashSubtitle>, Without<SplashTitle>, Without<SplashCursor>),
|
||||||
|
>,
|
||||||
|
mut cursors: Query<
|
||||||
|
&mut TextColor,
|
||||||
|
(With<SplashCursor>, Without<SplashTitle>, Without<SplashSubtitle>),
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
for (entity, mut age, mut bg, children) in &mut roots {
|
for (entity, mut age, mut bg, children) in &mut roots {
|
||||||
age.0 = age.0.saturating_add(time.delta());
|
age.0 = age.0.saturating_add(time.delta());
|
||||||
@@ -239,9 +273,16 @@ fn advance_splash(
|
|||||||
bg.0 = scrim;
|
bg.0 = scrim;
|
||||||
|
|
||||||
// Walk the splash root's direct children for the title /
|
// Walk the splash root's direct children for the title /
|
||||||
// subtitle markers and update their alpha. The hierarchy is
|
// subtitle / cursor markers and update their alpha. The
|
||||||
// shallow (root → 2 text children) so a small loop is fine.
|
// hierarchy is shallow (root → 3 text children) so a small
|
||||||
|
// loop is fine.
|
||||||
for child in children.iter() {
|
for child in children.iter() {
|
||||||
|
if let Ok(mut color) = cursors.get_mut(child) {
|
||||||
|
let mut c = ACCENT_PRIMARY;
|
||||||
|
c.set_alpha(alpha);
|
||||||
|
color.0 = c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Ok(mut color) = titles.get_mut(child) {
|
if let Ok(mut color) = titles.get_mut(child) {
|
||||||
let mut c = ACCENT_PRIMARY;
|
let mut c = ACCENT_PRIMARY;
|
||||||
c.set_alpha(alpha);
|
c.set_alpha(alpha);
|
||||||
|
|||||||
@@ -317,25 +317,41 @@ fn handle_copy_share_link_button(
|
|||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match arboard::Clipboard::new() {
|
|
||||||
Ok(mut cb) => match cb.set_text(url.clone()) {
|
// Desktop: `arboard` writes the URL to the OS clipboard.
|
||||||
Ok(()) => {
|
// Android: `arboard` has no platform backend (would fail to
|
||||||
toast.write(InfoToastEvent(format!("Copied: {url}")));
|
// compile, so the dependency is target-gated in
|
||||||
}
|
// solitaire_engine/Cargo.toml). The button still spawns and
|
||||||
|
// resolves to a meaningful toast instead — when we wire the
|
||||||
|
// Android Phase, this becomes a JNI call into ClipboardManager.
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
match arboard::Clipboard::new() {
|
||||||
|
Ok(mut cb) => match cb.set_text(url.clone()) {
|
||||||
|
Ok(()) => {
|
||||||
|
toast.write(InfoToastEvent(format!("Copied: {url}")));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("clipboard write failed: {e}");
|
||||||
|
toast.write(InfoToastEvent(
|
||||||
|
"Couldn't write to clipboard \u{2014} share link wasn't copied.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("clipboard write failed: {e}");
|
warn!("clipboard init failed: {e}");
|
||||||
toast.write(InfoToastEvent(
|
toast.write(InfoToastEvent(
|
||||||
"Couldn't write to clipboard \u{2014} share link wasn't copied.".to_string(),
|
"Couldn't reach the clipboard \u{2014} share link wasn't copied.".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
warn!("clipboard init failed: {e}");
|
|
||||||
toast.write(InfoToastEvent(
|
|
||||||
"Couldn't reach the clipboard \u{2014} share link wasn't copied.".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
toast.write(InfoToastEvent(format!(
|
||||||
|
"Share link: {url}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_watch_replay_button(
|
fn handle_watch_replay_button(
|
||||||
@@ -847,8 +863,23 @@ fn spawn_stats_screen(
|
|||||||
// Surfaces the most recent winning game so the player can spot
|
// Surfaces the most recent winning game so the player can spot
|
||||||
// whether their last victory has been recorded. The Watch
|
// whether their last victory has been recorded. The Watch
|
||||||
// Replay action below is what the player clicks to revisit it.
|
// Replay action below is what the player clicks to revisit it.
|
||||||
|
//
|
||||||
|
// When the displayed replay carries a `share_url` (uploaded
|
||||||
|
// to a sync server, persisted by v0.19.0's share-link
|
||||||
|
// contract), append a "Shareable" badge so the player can
|
||||||
|
// tell at a glance whether the Copy share link button below
|
||||||
|
// will produce a URL — without it the button surfaces a
|
||||||
|
// toast explaining why nothing was copied, which is more
|
||||||
|
// friction than necessary when a quick visual cue suffices.
|
||||||
let replay_caption = match latest_replay {
|
let replay_caption = match latest_replay {
|
||||||
Some(r) => format!("Latest win: {}", format_replay_caption(r)),
|
Some(r) => {
|
||||||
|
let base = format!("Latest win: {}", format_replay_caption(r));
|
||||||
|
if r.share_url.is_some() {
|
||||||
|
format!("{base} \u{2022} Shareable")
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
}
|
||||||
|
}
|
||||||
None => "No replay recorded yet \u{2014} win a game first.".to_string(),
|
None => "No replay recorded yet \u{2014} win a game first.".to_string(),
|
||||||
};
|
};
|
||||||
body.spawn((
|
body.spawn((
|
||||||
|
|||||||
@@ -502,10 +502,28 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pull_failure_sets_error_status() {
|
fn pull_failure_sets_error_status() {
|
||||||
let mut app = headless_app_with(FailingProvider);
|
let mut app = headless_app_with(FailingProvider);
|
||||||
// Pump frames until the task resolves (it's synchronous under
|
// Wall-clock-bounded loop instead of a fixed 5-update budget.
|
||||||
// AsyncComputeTaskPool in test mode, so a few updates suffice).
|
// Under heavy parallel cargo-test load the AsyncComputeTaskPool
|
||||||
for _ in 0..5 {
|
// can be starved long enough that 5 updates aren't sufficient
|
||||||
|
// for the failing pull to surface. Pumping until either the
|
||||||
|
// status flips to `Error` or a 5-second deadline elapses
|
||||||
|
// mirrors the auto-save flake fix and turns this test from
|
||||||
|
// "pass on a fast machine" into "pass on any machine that
|
||||||
|
// makes meaningful progress".
|
||||||
|
let deadline =
|
||||||
|
std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||||
|
loop {
|
||||||
app.update();
|
app.update();
|
||||||
|
if matches!(
|
||||||
|
app.world().resource::<SyncStatusResource>().0,
|
||||||
|
SyncStatus::Error(_)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if std::time::Instant::now() >= deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::yield_now();
|
||||||
}
|
}
|
||||||
let status = &app.world().resource::<SyncStatusResource>().0;
|
let status = &app.world().resource::<SyncStatusResource>().0;
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -14,9 +14,21 @@ use crate::layout::{compute_layout, Layout, LayoutResource, LayoutSystem};
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::layout::TABLE_COLOUR;
|
use crate::layout::TABLE_COLOUR;
|
||||||
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource};
|
||||||
|
use crate::ui_theme::TEXT_PRIMARY;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use solitaire_data::Theme;
|
use solitaire_data::Theme;
|
||||||
|
|
||||||
|
/// Default tint applied to every empty-pile marker sprite. Pure white
|
||||||
|
/// at 8% alpha — soft enough that the marker reads as a "hint of a
|
||||||
|
/// slot" rather than a panel, but visible against every felt
|
||||||
|
/// background.
|
||||||
|
///
|
||||||
|
/// Re-exported as the source of truth for `cursor_plugin::MARKER_DEFAULT`,
|
||||||
|
/// which used to duplicate the literal alongside a "kept in sync" doc
|
||||||
|
/// comment. Pulling both call sites through this const makes drift a
|
||||||
|
/// compile error instead of a stale comment.
|
||||||
|
pub const PILE_MARKER_DEFAULT_COLOUR: Color = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
||||||
|
|
||||||
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
|
/// Holds pre-loaded [`Handle<Image>`]s for the 5 selectable table backgrounds.
|
||||||
///
|
///
|
||||||
/// Loaded once at startup by [`load_background_images`]. Index 0 is the
|
/// Loaded once at startup by [`load_background_images`]. Index 0 is the
|
||||||
@@ -218,7 +230,7 @@ pub fn suit_symbol(suit: &Suit) -> &'static str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
||||||
let marker_colour = Color::srgba(1.0, 1.0, 1.0, 0.08);
|
let marker_colour = PILE_MARKER_DEFAULT_COLOUR;
|
||||||
let marker_size = layout.card_size;
|
let marker_size = layout.card_size;
|
||||||
let font_size = layout.card_size.x * 0.28;
|
let font_size = layout.card_size.x * 0.28;
|
||||||
|
|
||||||
@@ -254,7 +266,7 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
|
|||||||
b.spawn((
|
b.spawn((
|
||||||
Text2d::new("K"),
|
Text2d::new("K"),
|
||||||
TextFont { font_size, ..default() },
|
TextFont { font_size, ..default() },
|
||||||
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.35)),
|
TextColor(TEXT_PRIMARY.with_alpha(0.35)),
|
||||||
Transform::from_xyz(0.0, 0.0, 0.1),
|
Transform::from_xyz(0.0, 0.0, 0.1),
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@@ -308,9 +320,14 @@ fn on_window_resized(
|
|||||||
// Task #6 — Hint pile-marker highlight
|
// Task #6 — Hint pile-marker highlight
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Gold tint applied to a `PileMarker` sprite when it is the current hint
|
/// Gold tint applied to a `PileMarker` sprite when it is the current
|
||||||
/// destination.
|
/// hint destination. Same RGB as the design-system [`STATE_WARNING`]
|
||||||
const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(1.0, 0.85, 0.1);
|
/// token (`#ddb26f`) so the in-game "look here" colour is the same hue
|
||||||
|
/// as every other warning/attention signal in the UI. Spelled as a
|
||||||
|
/// literal because `Alpha::with_alpha` is not yet a `const` trait
|
||||||
|
/// method on stable; the tracking test below pins the RGB to
|
||||||
|
/// `STATE_WARNING` so a future palette swap can't drift the two apart.
|
||||||
|
const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(0.867, 0.698, 0.435);
|
||||||
|
|
||||||
/// Listens for `HintVisualEvent` and tints the matching `PileMarker` entity
|
/// Listens for `HintVisualEvent` and tints the matching `PileMarker` entity
|
||||||
/// gold for 2 s, storing the original colour in `HintPileHighlight` so it can
|
/// gold for 2 s, storing the original colour in `HintPileHighlight` so it can
|
||||||
@@ -480,19 +497,33 @@ mod tests {
|
|||||||
/// default pile marker colour so the player can see which pile is highlighted.
|
/// default pile marker colour so the player can see which pile is highlighted.
|
||||||
#[test]
|
#[test]
|
||||||
fn hint_pile_highlight_colour_is_distinct_from_default() {
|
fn hint_pile_highlight_colour_is_distinct_from_default() {
|
||||||
let default = Color::srgba(1.0, 1.0, 1.0, 0.08); // PILE_MARKER_DEFAULT_COLOUR
|
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
HINT_PILE_HIGHLIGHT_COLOUR, default,
|
HINT_PILE_HIGHLIGHT_COLOUR, PILE_MARKER_DEFAULT_COLOUR,
|
||||||
"HINT_PILE_HIGHLIGHT_COLOUR must differ from the default pile marker colour"
|
"HINT_PILE_HIGHLIGHT_COLOUR must differ from the default pile marker colour"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `HINT_PILE_HIGHLIGHT_COLOUR` is spelled as a literal because
|
||||||
|
/// `Alpha::with_alpha` is not a `const` trait method on stable.
|
||||||
|
/// This test pins its RGB to the design-system `STATE_WARNING`
|
||||||
|
/// token so a future palette swap that updates the token but
|
||||||
|
/// forgets the hint highlight fails loudly here.
|
||||||
|
#[test]
|
||||||
|
fn hint_pile_highlight_rgb_tracks_state_warning_token() {
|
||||||
|
use crate::ui_theme::STATE_WARNING;
|
||||||
|
let hint = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
||||||
|
let warning = STATE_WARNING.to_srgba();
|
||||||
|
assert!((hint.red - warning.red).abs() < 1e-6);
|
||||||
|
assert!((hint.green - warning.green).abs() < 1e-6);
|
||||||
|
assert!((hint.blue - warning.blue).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
/// A freshly-created HintPileHighlight has a positive timer countdown.
|
/// A freshly-created HintPileHighlight has a positive timer countdown.
|
||||||
#[test]
|
#[test]
|
||||||
fn hint_pile_highlight_timer_starts_positive() {
|
fn hint_pile_highlight_timer_starts_positive() {
|
||||||
let h = HintPileHighlight {
|
let h = HintPileHighlight {
|
||||||
timer: 2.0,
|
timer: 2.0,
|
||||||
original_color: Color::srgba(1.0, 1.0, 1.0, 0.08),
|
original_color: PILE_MARKER_DEFAULT_COLOUR,
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
h.timer > 0.0,
|
h.timer > 0.0,
|
||||||
@@ -529,17 +560,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The gold hint colour must have a strong yellow component (r ≥ 0.9, g ≥ 0.8,
|
/// The hint colour must read as "gold-ish" — red dominant, green
|
||||||
/// b ≤ 0.3) to be clearly visible as a "destination" indicator.
|
/// close behind, blue noticeably lower — so a player intuitively
|
||||||
|
/// associates the highlight with attention/warning. Bounds are
|
||||||
|
/// loose enough to accommodate the Terminal palette's muted gold
|
||||||
|
/// (`STATE_WARNING`, `#ddb26f`) while still rejecting a stray
|
||||||
|
/// red, green, or neutral grey if someone refactors badly.
|
||||||
|
/// Exact-RGB tracking lives in
|
||||||
|
/// `hint_pile_highlight_rgb_tracks_state_warning_token`.
|
||||||
#[test]
|
#[test]
|
||||||
fn hint_pile_highlight_colour_is_gold() {
|
fn hint_pile_highlight_colour_is_gold() {
|
||||||
// Extract linear components. srgb(1.0, 0.85, 0.1) is the expected gold.
|
|
||||||
// We test the channel values rather than exact equality so future tweaks
|
|
||||||
// to the shade do not break the test, as long as the colour remains golden.
|
|
||||||
let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
let Srgba { red, green, blue, .. } = HINT_PILE_HIGHLIGHT_COLOUR.to_srgba();
|
||||||
assert!(red >= 0.9, "gold hint colour must have red ≥ 0.9, got {red}");
|
assert!(red >= 0.7, "gold hint colour must have red ≥ 0.7, got {red}");
|
||||||
assert!(green >= 0.7, "gold hint colour must have green ≥ 0.7, got {green}");
|
assert!(green >= 0.5, "gold hint colour must have green ≥ 0.5, got {green}");
|
||||||
assert!(blue <= 0.3, "gold hint colour must have blue ≤ 0.3, got {blue}");
|
assert!(blue <= 0.6, "gold hint colour must have blue ≤ 0.6, got {blue}");
|
||||||
|
assert!(red > blue, "gold hint colour must be warmer than cool, got r={red} b={blue}");
|
||||||
|
assert!(green > blue, "gold hint colour must be warmer than cool, got g={green} b={blue}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ pub const MODAL_ENTER_START_SCALE: f32 = 0.96;
|
|||||||
/// Visual emphasis tier applied to a [`ModalButton`].
|
/// Visual emphasis tier applied to a [`ModalButton`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ButtonVariant {
|
pub enum ButtonVariant {
|
||||||
/// Loud yellow CTA — Confirm, Play Again. One per modal; right-aligned.
|
/// Cyan CTA (`ACCENT_PRIMARY`) — Confirm, Play Again, Resume. One per
|
||||||
|
/// modal; right-aligned in the actions row.
|
||||||
Primary,
|
Primary,
|
||||||
/// Mid-emphasis — Cancel, Close, Done.
|
/// Mid-emphasis — Cancel, Close, Done.
|
||||||
Secondary,
|
Secondary,
|
||||||
@@ -332,14 +333,17 @@ pub fn spawn_modal_button<M: Component>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let label_color = match variant {
|
let label_color = match variant {
|
||||||
// Primary buttons sit on the loud yellow accent — dark text on
|
// Primary buttons sit on the cyan accent — `BG_BASE` text on
|
||||||
// top reads well and passes AAA contrast.
|
// top reads well and passes AAA contrast against `#6fc2ef`.
|
||||||
ButtonVariant::Primary => BG_BASE,
|
ButtonVariant::Primary => BG_BASE,
|
||||||
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_PRIMARY,
|
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_PRIMARY,
|
||||||
};
|
};
|
||||||
let caption_color = match variant {
|
let caption_color = match variant {
|
||||||
// Use a slightly muted version of the label colour so the chip
|
// Muted near-black on the cyan Primary so the hotkey chip reads
|
||||||
// reads as a secondary detail without disappearing.
|
// as a secondary detail without disappearing. Deliberately a
|
||||||
|
// pure-black-at-alpha rather than `BG_BASE.with_alpha(...)`:
|
||||||
|
// `BG_BASE` is `#151515` (not 0,0,0), so the alpha-on-cyan
|
||||||
|
// composite would tint slightly cooler than intended here.
|
||||||
ButtonVariant::Primary => Color::srgba(0.0, 0.0, 0.0, 0.55),
|
ButtonVariant::Primary => Color::srgba(0.0, 0.0, 0.0, 0.55),
|
||||||
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_SECONDARY,
|
ButtonVariant::Secondary | ButtonVariant::Tertiary => TEXT_SECONDARY,
|
||||||
};
|
};
|
||||||
@@ -395,9 +399,10 @@ fn hover_bg(variant: ButtonVariant) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pressed-state background colour. Primary swaps to the magenta
|
/// Pressed-state background colour. Primary swaps to the lavender
|
||||||
/// secondary accent for a moment of celebration; Secondary darkens to
|
/// secondary accent (`ACCENT_SECONDARY`, `#e1a3ee`) for a moment of
|
||||||
/// the base elevation; Tertiary darkens further.
|
/// celebration; Secondary darkens to the base elevation; Tertiary
|
||||||
|
/// darkens further to `BG_ELEVATED_PRESSED`.
|
||||||
fn pressed_bg(variant: ButtonVariant) -> Color {
|
fn pressed_bg(variant: ButtonVariant) -> Color {
|
||||||
match variant {
|
match variant {
|
||||||
ButtonVariant::Primary => ACCENT_SECONDARY,
|
ButtonVariant::Primary => ACCENT_SECONDARY,
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
//! engine; collapsing them into one source of truth keeps the visual
|
//! engine; collapsing them into one source of truth keeps the visual
|
||||||
//! system coherent and makes future palette swaps a single-file change.
|
//! system coherent and makes future palette swaps a single-file change.
|
||||||
//!
|
//!
|
||||||
//! Palette is "Midnight Purple + Balatro accent" — see the 2026-04-30
|
//! Palette is "Terminal" (base16-eighties) — see
|
||||||
//! UX overhaul Phase 2 proposal for the rationale behind specific
|
//! `docs/ui-mockups/design-system.md` for the full token spec, mockup
|
||||||
//! values. The tokens are exposed as `pub const` so static contexts
|
//! library, and rationale. The tokens are exposed as `pub const` so
|
||||||
//! (default colours on Sprite components, etc.) can use them; a future
|
//! static contexts (default colours on Sprite components, etc.) can use
|
||||||
//! `UiTheme` resource can layer runtime switching on top without
|
//! them; a future `UiTheme` resource can layer runtime switching on top
|
||||||
//! changing the constant API.
|
//! without changing the constant API.
|
||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
@@ -21,93 +21,96 @@ use bevy::prelude::Val;
|
|||||||
use solitaire_data::AnimSpeed;
|
use solitaire_data::AnimSpeed;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Colours — Midnight Purple base with a Balatro-yellow primary accent.
|
// Colours — Terminal (base16-eighties): near-black surface ramp with a cyan
|
||||||
|
// primary accent and lime/lavender/gold/teal/pink semantic accents.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Window backstop and the default text colour on top of `ACCENT_PRIMARY`.
|
/// Window backstop and the default text colour on top of `ACCENT_PRIMARY`.
|
||||||
/// Deep midnight purple, near-black. `#1A0F2E`.
|
/// Near-black terminal background. `#151515`.
|
||||||
pub const BG_BASE: Color = Color::srgb(0.102, 0.059, 0.180);
|
pub const BG_BASE: Color = Color::srgb(0.082, 0.082, 0.082);
|
||||||
|
|
||||||
/// Elevated surface — modal cards, popover panels, button backgrounds.
|
/// Elevated surface — modal cards, popover panels, button backgrounds.
|
||||||
/// One step lighter than `BG_BASE` so cards visually float above the
|
/// One step lighter than `BG_BASE` so cards read as a separate plane
|
||||||
/// felt without needing real drop shadows. `#2D1B69`.
|
/// without needing drop shadows. `#202020`.
|
||||||
pub const BG_ELEVATED: Color = Color::srgb(0.176, 0.106, 0.412);
|
pub const BG_ELEVATED: Color = Color::srgb(0.125, 0.125, 0.125);
|
||||||
|
|
||||||
/// Hovered/highlighted surface — used on button hover and on the
|
/// Hovered/highlighted surface — used on button hover and on the
|
||||||
/// currently-active row of a popover. `#3A2580`.
|
/// currently-active row of a popover. `#2a2a2a`.
|
||||||
pub const BG_ELEVATED_HI: Color = Color::srgb(0.227, 0.145, 0.502);
|
pub const BG_ELEVATED_HI: Color = Color::srgb(0.165, 0.165, 0.165);
|
||||||
|
|
||||||
/// Top elevation step — Secondary button hover, popover currently-
|
/// Top elevation step — Secondary button hover, popover currently-
|
||||||
/// hovered row. One rung above `BG_ELEVATED_HI`. `#482F97`.
|
/// hovered row. One rung above `BG_ELEVATED_HI`. `#353535`.
|
||||||
pub const BG_ELEVATED_TOP: Color = Color::srgb(0.282, 0.184, 0.592);
|
pub const BG_ELEVATED_TOP: Color = Color::srgb(0.208, 0.208, 0.208);
|
||||||
|
|
||||||
/// Pressed-button surface — `BG_ELEVATED` darkened ~15%. `#26155B`.
|
/// Pressed-button surface — sits below `BG_ELEVATED` so a press reads
|
||||||
pub const BG_ELEVATED_PRESSED: Color = Color::srgb(0.149, 0.082, 0.357);
|
/// as the surface receding rather than rising. `#1a1a1a`.
|
||||||
|
pub const BG_ELEVATED_PRESSED: Color = Color::srgb(0.102, 0.102, 0.102);
|
||||||
|
|
||||||
/// Uniform scrim under every modal. The audit found 0.60–0.92 alpha
|
/// Uniform scrim under every modal. Per the Terminal design system,
|
||||||
/// drift across 11 overlay plugins; this single value replaces all of
|
/// modals dim the table aggressively (95% opacity) without blurring,
|
||||||
/// them. `rgba(13, 7, 28, 0.85)`.
|
/// to maintain the crisp synthwave-flat aesthetic. `rgba(21, 21, 21, 0.95)`.
|
||||||
pub const SCRIM: Color = Color::srgba(0.051, 0.027, 0.110, 0.85);
|
pub const SCRIM: Color = Color::srgba(0.082, 0.082, 0.082, 0.95);
|
||||||
|
|
||||||
/// Translucent fill for the top-of-window HUD band painted by
|
/// Solid fill for the top-of-window HUD band painted by
|
||||||
/// `hud_plugin::spawn_hud_band`. Same midnight-purple hue as `BG_BASE`,
|
/// `hud_plugin::spawn_hud_band`. Terminal HUD chips are opaque
|
||||||
/// but at 0.70 alpha so the green felt reads through subtly — enough
|
/// `surface-container` panels — no transparency — so the chrome reads
|
||||||
/// to mark the band as "UI" without feeling like a hard chrome strip.
|
/// as a status-line strip rather than a glassy overlay. `#202020`.
|
||||||
/// `rgba(26, 15, 46, 0.70)`.
|
pub const BG_HUD_BAND: Color = Color::srgba(0.125, 0.125, 0.125, 1.0);
|
||||||
pub const BG_HUD_BAND: Color = Color::srgba(0.102, 0.059, 0.180, 0.70);
|
|
||||||
|
|
||||||
/// Primary text — warm off-white with a hint of purple to fit the
|
/// Primary text — warm off-white. The base16-eighties foreground.
|
||||||
/// midnight palette without feeling clinical. `#F5F0FF`.
|
/// `#d0d0d0`.
|
||||||
pub const TEXT_PRIMARY: Color = Color::srgb(0.961, 0.941, 1.000);
|
pub const TEXT_PRIMARY: Color = Color::srgb(0.816, 0.816, 0.816);
|
||||||
|
|
||||||
/// Secondary text — captions, hints, muted labels. Lavender-grey.
|
/// Secondary text — captions, hints, muted labels. `#a0a0a0`.
|
||||||
/// `#B5A8D5`.
|
pub const TEXT_SECONDARY: Color = Color::srgb(0.627, 0.627, 0.627);
|
||||||
pub const TEXT_SECONDARY: Color = Color::srgb(0.710, 0.659, 0.835);
|
|
||||||
|
|
||||||
/// Disabled text — greyed-out buttons, locked items. `#6B5F85`.
|
/// Disabled text — greyed-out buttons, locked items. `#505050`.
|
||||||
pub const TEXT_DISABLED: Color = Color::srgb(0.420, 0.373, 0.522);
|
pub const TEXT_DISABLED: Color = Color::srgb(0.314, 0.314, 0.314);
|
||||||
|
|
||||||
/// Balatro-yellow primary accent — the loudest colour in the palette.
|
/// Cyan primary accent — the CTA colour of the system. Reserved for
|
||||||
/// Reserved for primary actions (Confirm, Play Again), win states, and
|
/// primary actions (Play, Resume, Save), focus rings, and selection.
|
||||||
/// "look here" callouts. `BG_BASE` text on top of this colour passes
|
/// `BG_BASE` text on top of this colour passes AAA contrast. `#6fc2ef`.
|
||||||
/// AAA contrast. `#FFD23F`.
|
pub const ACCENT_PRIMARY: Color = Color::srgb(0.435, 0.761, 0.937);
|
||||||
pub const ACCENT_PRIMARY: Color = Color::srgb(1.000, 0.824, 0.247);
|
|
||||||
|
|
||||||
/// Brightened `ACCENT_PRIMARY` for hover states on primary buttons.
|
/// Brightened `ACCENT_PRIMARY` for hover states on primary buttons.
|
||||||
/// Picks up saturation while keeping the same hue. `#FFE36B`.
|
/// Picks up luminance while keeping the same hue. `#a8dcf5`.
|
||||||
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(1.000, 0.890, 0.420);
|
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(0.659, 0.863, 0.961);
|
||||||
|
|
||||||
/// Warm magenta secondary accent — celebratory states (achievement
|
/// Lavender secondary accent — celebratory states (level-up,
|
||||||
/// unlocked, streak milestones). Used sparingly so it stays special.
|
/// achievement unlocked, streak milestones). Used sparingly so it stays
|
||||||
/// `#FF6B9D`.
|
/// special. `#e1a3ee`.
|
||||||
pub const ACCENT_SECONDARY: Color = Color::srgb(1.000, 0.420, 0.616);
|
pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);
|
||||||
|
|
||||||
/// Success — foundation completion, valid drop tint, sync OK. `#4ADE80`.
|
/// Success — foundation completion, valid drop tint, sync OK. Lime
|
||||||
pub const STATE_SUCCESS: Color = Color::srgb(0.290, 0.871, 0.502);
|
/// from base16-eighties. `#acc267`.
|
||||||
|
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);
|
||||||
|
|
||||||
/// Warning — penalty signal. **Both** Undo and Recycle counters use
|
/// Warning — penalty signal, daily-seed expiry countdown, sync-pending
|
||||||
/// this when non-zero (the audit found these were inconsistent — Undos
|
/// status. Gold from base16-eighties. **Both** Undo and Recycle
|
||||||
/// amber, Recycles white). `#FFA94D`.
|
/// counters use this when non-zero. `#ddb26f`.
|
||||||
pub const STATE_WARNING: Color = Color::srgb(1.000, 0.663, 0.302);
|
pub const STATE_WARNING: Color = Color::srgb(0.867, 0.698, 0.435);
|
||||||
|
|
||||||
/// Danger — rejection shake, illegal placement, sync error. `#F77272`.
|
/// Danger — rejection shake, illegal placement, sync error. Pink from
|
||||||
pub const STATE_DANGER: Color = Color::srgb(0.969, 0.447, 0.447);
|
/// base16-eighties (also doubles as `suit-red` per the design.md
|
||||||
|
/// rationale). `#fb9fb1`.
|
||||||
|
pub const STATE_DANGER: Color = Color::srgb(0.984, 0.624, 0.694);
|
||||||
|
|
||||||
/// Info — daily-challenge constraint, draw-cycle indicator. `#6BBBFF`.
|
/// Info — neutral system toasts, sync-connected indicator. Teal from
|
||||||
pub const STATE_INFO: Color = Color::srgb(0.420, 0.733, 1.000);
|
/// base16-eighties. `#12cfc0`.
|
||||||
|
pub const STATE_INFO: Color = Color::srgb(0.071, 0.812, 0.753);
|
||||||
|
|
||||||
/// Soft fill colour for the drop-target overlay shown over every legal
|
/// Soft fill colour for the drop-target overlay shown over every legal
|
||||||
/// destination pile while the player is dragging a card. Same green hue
|
/// destination pile while the player is dragging a card. Same lime hue
|
||||||
/// as `STATE_SUCCESS` (`#4ADE80`) so the visual language stays
|
/// as `STATE_SUCCESS` (`#acc267`) so the visual language stays
|
||||||
/// consistent, but at 10 % alpha so the underlying card faces remain
|
/// consistent, but at 10 % alpha so the underlying card faces remain
|
||||||
/// fully readable through the wash.
|
/// fully readable through the wash.
|
||||||
pub const DROP_TARGET_FILL: Color = Color::srgba(0.290, 0.871, 0.502, 0.10);
|
pub const DROP_TARGET_FILL: Color = Color::srgba(0.675, 0.761, 0.404, 0.10);
|
||||||
|
|
||||||
/// Outline colour for the drop-target overlay. Matches the
|
/// Outline colour for the drop-target overlay. Matches the
|
||||||
/// `STATE_SUCCESS` hue at 75 % alpha so the rectangle border reads
|
/// `STATE_SUCCESS` hue at 75 % alpha so the rectangle border reads
|
||||||
/// unmistakably against both the felt and stacked card faces without
|
/// unmistakably against both the felt and stacked card faces without
|
||||||
/// drowning the cards themselves.
|
/// drowning the cards themselves.
|
||||||
pub const DROP_TARGET_OUTLINE: Color = Color::srgba(0.290, 0.871, 0.502, 0.75);
|
pub const DROP_TARGET_OUTLINE: Color = Color::srgba(0.675, 0.761, 0.404, 0.75);
|
||||||
|
|
||||||
/// Thickness of the drop-target outline edges, in world-space pixels.
|
/// Thickness of the drop-target outline edges, in world-space pixels.
|
||||||
pub const DROP_TARGET_OUTLINE_PX: f32 = 3.0;
|
pub const DROP_TARGET_OUTLINE_PX: f32 = 3.0;
|
||||||
@@ -131,7 +134,7 @@ pub const STOCK_BADGE_BG: Color = BG_ELEVATED_HI;
|
|||||||
/// Foreground (text) colour of the stock-pile remaining-count chip.
|
/// Foreground (text) colour of the stock-pile remaining-count chip.
|
||||||
///
|
///
|
||||||
/// `ACCENT_PRIMARY` keeps the chip readable against the elevated
|
/// `ACCENT_PRIMARY` keeps the chip readable against the elevated
|
||||||
/// purple background and matches the Balatro accent already used for
|
/// surface background and matches the cyan accent already used for
|
||||||
/// other "look here" callouts.
|
/// other "look here" callouts.
|
||||||
pub const STOCK_BADGE_FG: Color = ACCENT_PRIMARY;
|
pub const STOCK_BADGE_FG: Color = ACCENT_PRIMARY;
|
||||||
|
|
||||||
@@ -159,15 +162,19 @@ pub const Z_STOCK_BADGE: f32 = 30.0;
|
|||||||
/// separately via [`CARD_SHADOW_ALPHA_IDLE`] / [`CARD_SHADOW_ALPHA_DRAG`].
|
/// separately via [`CARD_SHADOW_ALPHA_IDLE`] / [`CARD_SHADOW_ALPHA_DRAG`].
|
||||||
pub const CARD_SHADOW_COLOR: Color = Color::srgb(0.0, 0.0, 0.0);
|
pub const CARD_SHADOW_COLOR: Color = Color::srgb(0.0, 0.0, 0.0);
|
||||||
|
|
||||||
/// Alpha for the resting-state card shadow. Low enough that 52 stacked
|
/// Alpha for the resting-state card shadow. Set to 0 under the Terminal
|
||||||
/// shadows do not darken the felt into a uniform smear, high enough that
|
/// design system: depth is achieved through 1px suit-color borders and
|
||||||
/// each card reads as separated from the surface.
|
/// tonal layering, not blur shadows ("no `box-shadow` anywhere" is a
|
||||||
pub const CARD_SHADOW_ALPHA_IDLE: f32 = 0.25;
|
/// hard constraint of `docs/ui-mockups/design-system.md`). The shadow
|
||||||
|
/// rendering code path is left in place so a future palette swap can
|
||||||
|
/// re-enable it without touching consumers.
|
||||||
|
pub const CARD_SHADOW_ALPHA_IDLE: f32 = 0.0;
|
||||||
|
|
||||||
/// Alpha for the lifted/dragged card shadow. Stronger than the idle value
|
/// Alpha for the lifted/dragged card shadow. Set to 0 for the same
|
||||||
/// so the dragged stack visibly "casts more shadow" while the player holds
|
/// reason as [`CARD_SHADOW_ALPHA_IDLE`]. Drag affordance under the
|
||||||
/// it above the table.
|
/// Terminal system is the cyan focus glow + z-index lift, not a deeper
|
||||||
pub const CARD_SHADOW_ALPHA_DRAG: f32 = 0.40;
|
/// shadow.
|
||||||
|
pub const CARD_SHADOW_ALPHA_DRAG: f32 = 0.0;
|
||||||
|
|
||||||
/// World-space pixel offset of the resting-state card shadow relative to
|
/// World-space pixel offset of the resting-state card shadow relative to
|
||||||
/// its parent card centre. Down-and-right matches a soft top-left light
|
/// its parent card centre. Down-and-right matches a soft top-left light
|
||||||
@@ -197,15 +204,20 @@ pub const CARD_SHADOW_PADDING_DRAG: Vec2 = Vec2::new(8.0, 8.0);
|
|||||||
pub const CARD_SHADOW_LOCAL_Z: f32 = -0.05;
|
pub const CARD_SHADOW_LOCAL_Z: f32 = -0.05;
|
||||||
|
|
||||||
/// Subtle border — default popover, card, and idle button outline.
|
/// Subtle border — default popover, card, and idle button outline.
|
||||||
pub const BORDER_SUBTLE: Color = Color::srgba(0.647, 0.549, 1.000, 0.12);
|
/// `outline-variant` from the design system at full alpha; the Terminal
|
||||||
|
/// aesthetic uses solid 1px borders rather than translucent washes.
|
||||||
|
/// `#353535`.
|
||||||
|
pub const BORDER_SUBTLE: Color = Color::srgba(0.208, 0.208, 0.208, 1.0);
|
||||||
|
|
||||||
/// Strong border — hover outline, focused button, active popover.
|
/// Strong border — hover outline, focused button, active popover.
|
||||||
pub const BORDER_STRONG: Color = Color::srgba(0.647, 0.549, 1.000, 0.30);
|
/// `outline` from the design system. `#505050`.
|
||||||
|
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);
|
||||||
|
|
||||||
/// 2 px ring drawn around the focused interactive element. Balatro yellow
|
/// 2 px ring drawn around the focused interactive element. Cyan
|
||||||
/// (matches `ACCENT_PRIMARY`) at 85% alpha so the ring stays legible
|
/// (matches `ACCENT_PRIMARY`) at 85% alpha so the ring stays legible
|
||||||
/// against both elevated surfaces and the modal scrim backdrop.
|
/// against both elevated surfaces and the modal scrim backdrop.
|
||||||
pub const FOCUS_RING: Color = Color::srgba(1.0, 0.823, 0.247, 0.85);
|
/// `rgba(111, 194, 239, 0.85)`.
|
||||||
|
pub const FOCUS_RING: Color = Color::srgba(0.435, 0.761, 0.937, 0.85);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Typography scale (px) — 5 rungs replace the prior
|
// Typography scale (px) — 5 rungs replace the prior
|
||||||
|
|||||||