Compare commits

...

19 Commits

Author SHA1 Message Date
funman300 41a009a693 docs: cut v0.20.0 — Terminal design system + Android persistence
Promotes the [Unreleased] section to [0.20.0] dated 2026-05-07
and opens a fresh empty [Unreleased]. The cycle's two through-
lines:

- **Terminal visual-identity port.** ui_theme token system
  (0d477ac) is load-bearing; downstream chrome migrations cover
  the modal scaffold, gameplay-feedback layer (ceec4fc), toasts
  with a new ToastVariant enum (a137607), table chrome (651f406),
  card chrome (d752870), splash cursor (cdcadda), and final
  hint-source / dest pairing (9891ae4). Card-face / suit / card-
  back palette intentionally NOT migrated — those track PNG
  artwork that hasn't been regenerated yet. The 24 Stitch-rendered
  mockups and design-system.md spec landed in fa7f98a.
- **Android persistence shim.** solitaire_data::data_dir
  routes through a per-platform shim (4b51e50) closing the
  CLAUDE.md §10 dirs::data_dir() = None pitfall on Android.
  Settings, stats, achievements, replays, game-state, time-attack
  sessions, and user themes now persist on a real APK.

Also closes three v0.19.0 punch-list candidates that landed
earlier in the cycle (pull_failure flake at 67c150b, smart-window-
size opt-out at e1b8766, Shareable badge at 9b065e5).

Tests: 1176 passing / 0 failing (six new this cycle: ui_theme
invariant guards, toast-variant-border-mapping, palette-tracking
guards on MARKER_VALID / HINT_PILE_HIGHLIGHT_COLOUR /
RIGHT_CLICK_HIGHLIGHT_COLOUR / toast-border distinctness).

SESSION_HANDOFF.md refreshed: HEAD pointer, test count, the
v0.20.0 changelog summary, the open punch list (Phase Android
runtime gaps, visual-identity follow-ups including the artwork
regeneration item), the updated design-direction box (was
Midnight Purple + Balatro yellow; now base16-eighties Terminal),
and a refreshed Resume Prompt offering A–F next-step options.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:58:51 -07:00
funman300 fa7f98ac52 docs(ui): land the Terminal design system + 24-mockup library
Adds the spec the recent visual-identity port pass referenced:

- design-system.md — base16-eighties palette, type scale, spacing
  scale, motion budget, component library, accessibility notes
  (color-blind toggle, high-contrast mode, glyph differentiation),
  and the canonical "Terminal" card-back theme.
- 24 Stitch-rendered mockups (HTML + PNG): 12 redesigned existing
  screens, 1 desktop home variant, 2 onboarding steps, and 9
  missing-plugin screens (splash, challenge, time-attack,
  weekly-goals, leaderboard, sync, level-up, replay, radial-menu).

These mockups are the source the engine plugins were ported
against in commits 0d477ac through 9891ae4 (token system,
modal scaffold, gameplay-feedback layer, toasts, table chrome,
card chrome, splash cursor, hint highlight). Future plugin work
should diff against the matching mockup before touching pixels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:47:57 -07:00
funman300 9891ae4ba3 refactor(engine): final hint-highlight + replay-overlay token cleanup
- input_plugin's hint-source card tint moves from raw bright-yellow
  `srgba(1.0, 1.0, 0.4, 1.0)` to the design-system STATE_WARNING
  token, so the source card and the destination pile (which already
  uses STATE_WARNING via HINT_PILE_HIGHLIGHT_COLOUR) wear the same
  attention colour as a coherent pair.
- replay_overlay had two stale doc comments referencing the old
  "loud yellow accent" — Primary is now cyan (ACCENT_PRIMARY).
  Comments updated; no behaviour change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:45:02 -07:00
funman300 cdcaddaabe feat(engine): add Terminal cursor block to splash overlay
Splash now renders the design system's signature `▌` cyan terminal-
cursor glyph (96px) above the wordmark, matching docs/ui-mockups/
splash-mobile.html. The cursor uses ACCENT_PRIMARY and fades on the
same per-frame alpha schedule as the title and subtitle so the
brand beat still dissolves as a single layer.

Did NOT pull in the mockup's full boot-loader treatment (scanline
overlay, ✓ check log lines, progress bar, ROOT@SOLITAIRE prompt) —
those are aesthetic features that warrant their own commit, not
this token-port pass. The splash already consumed every relevant
ui_theme token; the cursor glyph is the single highest-signal
visual element the spec called for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:42:29 -07:00
funman300 d752870007 refactor(engine): migrate card_plugin chrome to Terminal tokens
- Drag-elevation shadow now sources its colour from CARD_SHADOW_COLOR
  + CARD_SHADOW_ALPHA_DRAG, so the Terminal "no box-shadow" policy
  disables the stack shadow in lockstep with the per-card shadows.
  Re-enabling shadows for a future palette swap is now a one-line
  edit in ui_theme, not a hunt across plugins.
- RIGHT_CLICK_HIGHLIGHT_COLOUR retuned from raw `srgba(0.2, 0.8, 0.2, 0.6)`
  to STATE_SUCCESS's RGB at 60% alpha. Spelled as a literal because
  Alpha::with_alpha isn't const on stable; a new test pins the RGB
  to STATE_SUCCESS so a palette swap can't drift the two apart.
- Drop the duplicated PILE_MARKER_DEFAULT_COLOUR const — import the
  promoted const from table_plugin instead. STOCK_NORMAL_COLOUR is
  now an alias of that const so all idle pile-marker tints track a
  single source of truth.
- Stock recycle "↺" text changed from raw `srgba(1.0, 1.0, 1.0, 0.7)`
  to TEXT_PRIMARY at 0.7 alpha, picking up the off-white foreground
  used elsewhere in the Terminal UI.

Card-face / suit / card-back palette constants are intentionally
NOT migrated: the runtime path renders PNG artwork that's still on
the previous "white card" palette, so swapping the fallback
constants ahead of artwork regeneration would mix two visual
systems for any code path where image loading fails.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:39:02 -07:00
funman300 1d1543e4bc test(engine): align card-shadow drag-vs-idle assertion with Terminal "no shadow" intent
Commit 0d477ac (the Terminal token system) pinned both
CARD_SHADOW_ALPHA_IDLE and CARD_SHADOW_ALPHA_DRAG to 0.0 because the
Terminal design system explicitly disallows box-shadow ("depth via
1px borders and tonal layering"). The existing invariant
\`drag_alpha > idle_alpha\` then fails — \`0.0 > 0.0\` is false.

Loosen the assertion to \`drag_alpha >= idle_alpha\` and document the
intent: under Terminal both are 0; under any future palette that
re-enables shadows, drag still must not be weaker than idle. The
useful regression-guard (catching an accidental swap of the two
constants) is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:33:34 -07:00
funman300 651f4060e6 refactor(engine): migrate table_plugin chrome to Terminal tokens
- Promote `marker_colour` to module-level const PILE_MARKER_DEFAULT_COLOUR
  and re-export it. cursor_plugin::MARKER_DEFAULT now imports the const
  directly, replacing the prior duplicated literal kept in sync only by
  doc comment. Drift becomes a compile error instead of a stale claim.
- Empty-tableau "K" placeholder text now uses TEXT_PRIMARY at 0.35 alpha
  (was raw `Color::srgba(1.0, 1.0, 1.0, 0.35)`) so it picks up the
  Terminal off-white foreground.
- HINT_PILE_HIGHLIGHT_COLOUR retuned from bright `srgb(1.0, 0.85, 0.1)`
  to the design-system STATE_WARNING token (`#ddb26f`). Spelled as a
  literal because Alpha::with_alpha is not yet const on stable; a new
  test pins the RGB to STATE_WARNING so a palette swap can't drift the
  two apart silently.
- The existing "is gold" character test was hardcoded to the old bright
  palette (red ≥ 0.9). Loosened to "warmer than cool" + ranges that the
  Terminal muted gold satisfies, with exact-RGB tracking handled by the
  new STATE_WARNING test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:32:03 -07:00
funman300 a1376075bd feat(engine): port toasts to the Terminal design-system spec
Toasts now follow `docs/ui-mockups/design-system.md`:
- Bottom-anchored absolute position (was top / mid-screen)
- Opaque BG_ELEVATED fill (was translucent black-at-alpha)
- 1px accent border keyed off a new ToastVariant enum
- TYPE_BODY_LG caption (was 22 / 32 px literals)
- RADIUS_MD corners

ToastVariant exposes Info / Warning / Error / Celebration, each
mapped to its design-system token via border_color(). Variants are
threaded through every spawn_toast call site:

- Achievement / Level-up / XP / Daily / Weekly / Challenge → Celebration
- Goal-announcement / Time-attack / Settings volume / Auto-complete → Info

Queued banner and fire-and-forget toasts use slightly different
bottom anchors (6% vs. 14%) so a celebration toast spawned in the
same frame as a queued info banner layers above it instead of
overlapping. Two new tests pin variant→border mapping to the
design tokens and require all four borders to be visually distinct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:26:55 -07:00
funman300 ceec4fc486 refactor(engine): route gameplay-feedback colours through Terminal tokens
Selection-highlight tints in selection_plugin and the valid-drop
marker tint in cursor_plugin were hand-tuned RGB literals from the
prior Premium-Solitaire palette. Migrate them to the semantic
state tokens introduced in ui_theme:

- keyboard-drag source highlight (picking)  → ACCENT_PRIMARY
- keyboard-drag source highlight (lifted)   → STATE_WARNING
- keyboard-drag destination highlight       → STATE_SUCCESS
- cursor_plugin::MARKER_VALID               → STATE_SUCCESS @ 0.55α

`MARKER_VALID` stays a Color literal (Alpha::with_alpha is not yet
const on stable); a new tracking test pins its RGB to STATE_SUCCESS
so a future palette swap can't drift the two apart silently.

Also fix three stale doc comments in ui_modal that still described
the previous yellow / magenta palette ("Loud yellow CTA",
"Primary swaps to the magenta secondary accent"). Cyan and lavender
now, matching the actual token values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:06:57 -07:00
funman300 0d477ac9fd feat(engine): Terminal design-token system in ui_theme
Replaces the prior Premium-Solitaire palette and ad-hoc constants
with the full Terminal (base16-eighties) token set: near-black
surface ramp, cyan primary CTA, lime/lavender/gold/teal/pink
semantic accents, 5-rung type scale, 7-rung 4-multiple spacing
scale, 3-step radius, 14-rung z-index hierarchy, and a complete
motion budget. Card drop-shadow alphas pinned to 0 — Terminal
depth is 1px borders + tonal layering, not box-shadow.

Tokens stay as `pub const` so static contexts (default Sprite
colours etc.) keep compiling; a future UiTheme resource can layer
runtime switching on top without breaking the constant API. Four
unit tests pin the spacing/type/z-index invariants so a careless
edit can't silently break the scale. Plugin-by-plugin migration
to consume these tokens follows in subsequent commits.

Spec: docs/ui-mockups/design-system.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:56:08 -07:00
funman300 4b51e50203 fix(data): route data_dir() through a per-platform shim so Android persists
dirs::data_dir() returns None on Android, which silently disabled
every persistence path (settings, stats, achievements, replays,
game-state, time-attack sessions, user themes). New
solitaire_data::platform::data_dir() shim falls through to
dirs::data_dir() on desktop and returns the per-app sandbox at
/data/data/com.solitairequest.app/files on Android — no JNI needed,
since the package id is pinned in
[package.metadata.android].

CLAUDE.md §10 already flagged this as a known pitfall; the shim
pays it down at the one chokepoint instead of per feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:55:49 -07:00
funman300 f2d2119db5 docs: refresh handoff + populate CHANGELOG [Unreleased] for v0.20
The v0.19.0 handoff had drifted material across seven commits:
HEAD pointer was wrong (still claimed 6037596; actually 59424a3),
"Tags on origin" still claimed v0.19.0 wasn't pushed, the
known-flake list still mentioned `pull_failure_sets_error_status`
(fixed in 67c150b), and three of four v0.19.0 punch-list
"candidates" had silently shipped without the doc tracking it.
The Android build target landing in fb8b2ac wasn't mentioned at
all despite being the largest single change in the cycle.

CHANGELOG [Unreleased] populated with all seven commits grouped
into Added / Fixed:

  Added:
    - Android build target — first working APK (fb8b2ac)
    - Android developer setup + build runbook (59424a3)
    - F3 FPS / frame-time overlay (690e1d2)
    - "Smart window size" Settings toggle (e1b8766)
    - "Shareable" badge on Latest-win caption (9b065e5)
    - Help: M / P / Win-Summary-Enter rows (35516d3)

  Fixed:
    - pull_failure_sets_error_status flake (67c150b)

SESSION_HANDOFF.md fully rewritten:
  - Status section reflects HEAD 59424a3, clean working tree (apart
    from this commit's docs), 1170 passing tests, no known flakes
  - "Where we are" tracks v0.19.0 candidates' close status (3 of 4
    shipped, App icon still open and now blocked on a re-export)
  - New v0.20 candidates table covers all seven commits
  - New "Phase Android" punch-list section captures the unblocked-
    by-fb8b2ac work: APK launch verification, dirs::data_dir port,
    JNI ClipboardManager, Android Keystore, gpgs integration, the
    cosmetic cargo-apk panic workaround
  - Process notes call out the async-test starvation pattern
    (seen twice now), the bin→lib+shim refactor as a reusable
    pattern, and target-gating-by-default for cross-platform deps
  - Resume prompt's A–D menu refreshed to reflect actually-open
    work: APK verification, Phase-Android persistence, app icon,
    and a v0.20 cut

Build: cargo clippy --workspace --all-targets -- -D warnings clean.
Tests: 1170 passing / 0 failing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:28:04 -07:00
funman300 59424a370c docs(android): developer setup + build runbook
Captures the toolchain install for Debian 13 (the path Quat ran on
this dev box, including the JDK 21 / unzip / SDK-licence prompts),
the `cargo apk build` invocation, the cosmetic post-pass panic
workaround, and the table of what's wired vs. stubbed for the
android target. Runnable on a fresh box from a clone — no
machine-local context required.

Pairs with the workspace cfg-gating in fb8b2ac. Future Phase-Android
work (dirs::data_dir port, JNI ClipboardManager, Android Keystore,
gpgs) is listed as the not-yet-done section so a contributor can
pick it up without re-deriving the punch list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:36:36 +00:00
funman300 fb8b2ac684 feat(app): Android build target — first working APK at 54 MB
Wires the workspace through `cargo apk build`. After this commit
`cargo apk build -p solitaire_app --target x86_64-linux-android`
produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk`
containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable
on the AVD or a physical x86_64 device.

The five gating points discovered by iterating compile cycles:

1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib`
   to bundle as `libmain.so`; pure-bin crates panic with
   "Bin is not compatible with Cdylib". `src/lib.rs` carries the
   ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim
   that delegates for the desktop `cargo run` path.

2. `[package.metadata.android]` pins target SDK 34 / min SDK 26
   so cargo-apk doesn't probe for whatever default it ships
   (which on this machine was an uninstalled API 30). `assets =
   "../assets"` lets the same asset directory feed both desktop
   and APK.

3. Workspace `bevy` features add `android-native-activity` (the
   Bevy-side glue that pairs with cargo-apk's NativeActivity
   wrapper). The feature is target-gated inside bevy_internal so
   desktop builds compile it out.

4. `arboard` (clipboard, used by Stats's "Copy share link") has
   no Android backend — `cargo apk build` fails with E0433 on
   `platform::Clipboard` if unconditional. Target-gated to
   `cfg(not(target_os = "android"))`; the system surfaces an
   informational toast on Android until JNI ClipboardManager is
   wired in the Phase-Android round.

5. `keyring` + `keyring-core` cannot compile for android — the
   transitive `rpassword` uses `libc::__errno_location` which
   bionic doesn't expose. Both crates target-gated; `auth_tokens`
   ships a stub on Android that returns `KeychainUnavailable` for
   every call, matching how callers already handle a Linux box
   without Secret Service.

Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed
when it tries to also wrap the bin target. The APK on disk is
unaffected. Working around this with `cargo apk build --lib` is
the next small step.

What's verified:
- Desktop `cargo build`, `cargo clippy --workspace --all-targets`,
  and `cargo test --workspace` all clean.
- `cargo apk build -p solitaire_app --target x86_64-linux-android`
  produces 54 MB debug APK with libsolitaire_app.so + assets.

What's NOT yet verified:
- Whether the APK actually launches on the AVD / a phone (next
  step: `adb install` + `adb logcat` against the bevy_test AVD).
- Whether `dirs::data_dir()` on Android returns a usable path
  (sync / persistence will surface this if not).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:34:48 +00:00
funman300 690e1d2ad6 feat(engine): F3-toggleable FPS / frame-time overlay
Performance work for the upcoming Android port needs a numeric
baseline we can quote across desktop and mobile, instead of "feels
slow". DiagnosticsHudPlugin wraps Bevy's FrameTimeDiagnosticsPlugin
and renders a tiny corner readout the developer can toggle with F3.

- Hidden by default — production builds ship the plugin but the
  overlay starts invisible.
- F3 reads ButtonInput<KeyCode> directly (not gated by pause /
  modal state); diagnostics should always be reachable.
- Reads `smoothed()` FPS + frame_time so the cell isn't a jittery
  per-frame scoreboard. Format: "FPS NN \u{2022} M.MM ms".
- Anchored top-right at z = Z_SPLASH + 100 so the readout sits
  above every modal / toast / splash layer.
- Update system bails when hidden so we don't pay the
  diagnostic-store lookup or text mutation when nobody's looking.

Next up on the perf track: get the Android build target wired so we
can put real numbers in this readout from a phone or emulator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:03:18 +00:00
funman300 35516d31f6 docs(help): add M / P / Win-Summary-Enter to the Overlays section
The Help (F1) modal's Overlays section listed S/A/L/O but skipped
two post-v0.18 entries — M (Home / Mode launcher) and P (Profile) —
and never mentioned the recently-shipped Enter accelerator that
dismisses the Win Summary.

Help is the canonical keyboard-discovery surface. Three new rows
cover the gap so a player who opens F1 sees every overlay-toggle
key, plus the contextual Enter shortcut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:40:44 +00:00
funman300 9b065e5ac6 feat(stats): append "Shareable" badge to the Latest-win caption
The Copy share link button on the Stats overlay only produces a URL
when the displayed replay has a `share_url` populated; otherwise it
surfaces a toast explaining the upload prerequisite. Players had no
way to know the button would work without clicking it.

Adds a "\u{2022} Shareable" suffix to the Latest-win caption when
the displayed replay carries a share_url, matching the format the
v0.19.0 handoff sketched ("Replay 3 / 8 \u{2022} Shareable") for
the future Prev/Next selector. The Prev/Next markers exist in
stats_plugin but no spawn site renders them today, so the live
fix is on the existing single-replay caption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:04:55 +00:00
funman300 e1b8766e15 feat(settings): "Smart window size" toggle to opt out of monitor-relative
launch sizing

Players who specifically prefer the literal 1280×800 baseline on
every fresh-install launch had no way to opt out of the v0.19.0
smart-default sizer. Adds a Gameplay-section toggle (mirrors the
"Winnable deals only" pattern) so they can flip it off.

- New `Settings::disable_smart_default_size: bool` field with
  `#[serde(default)]` so legacy `settings.json` files load to the
  shipped behaviour (smart sizer enabled).
- Settings panel gains a "Smart window size" row with ON/OFF label
  inverting the negative flag, and a tooltip clarifying that saved
  window geometry always wins over both branches.
- `solitaire_app::main` reads the flag once at startup and skips
  the `apply_smart_default_window_size` registration when it's set.
  Mid-session changes apply on next launch (documented on the
  field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:00:43 +00:00
funman300 67c150bd7b test(engine): wall-clock-bounded loop for pull_failure flake
The fixed 5-update budget in `pull_failure_sets_error_status` was
the last test still subject to the AsyncComputeTaskPool starvation
mode that v0.19.0's auto-save fix already cleared. Under heavy
parallel cargo-test load, 5 updates wasn't always enough for the
failing pull task to surface its Err and flip
SyncStatusResource to Error.

Pumps updates in a loop bounded by a 5-second deadline (with
std::thread::yield_now between iterations to give the task pool a
chance to run), exiting as soon as the status flips. Mirrors the
auto-save flake fix shape — a healthy run hits the assertion in a
handful of frames, while a starved run gets the budget it needs
without hanging the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 03:54:51 +00:00
83 changed files with 8336 additions and 652 deletions
+236 -1
View File
@@ -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
+8 -1
View File
@@ -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",
+233 -131
View File
@@ -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 (AD) 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, 161024 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 AD. Don't pick unilaterally. OPEN AT THE START: ask which of AF. Don't pick unilaterally.
``` ```
+228
View File
@@ -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.
+293
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+219
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+258
View File
@@ -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&amp;family=Inter:wght@400;500;600&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+278
View File
@@ -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 0007 form a monochrome ramp and 080F 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).
+253
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+200
View File
@@ -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&amp;family=Inter:wght@400&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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 &amp; 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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+343
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

+225
View File
@@ -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&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&amp;family=Inter:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+315
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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 &amp; 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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+259
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+206
View File
@@ -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&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700;800&amp;family=Inter:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+211
View File
@@ -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&amp;family=Inter:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

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&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+212
View File
@@ -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&amp;family=JetBrains+Mono:wght@400;500;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+274
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+271
View File
@@ -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&amp;family=Inter:wght@400;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+284
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+258
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+213
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+279
View File
@@ -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&amp;family=Inter:wght@400;500;600&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+262
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+250
View File
@@ -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&amp;family=Inter:wght@400;500;600&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+215
View File
@@ -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&amp;family=Inter:wght@400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+265
View File
@@ -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&amp;family=Inter:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+200
View File
@@ -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&amp;family=Inter:wght@400;500;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+60
View File
@@ -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.
+281
View File
@@ -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);
}));
}
+6 -246
View File
@@ -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);
}));
} }
+10 -1
View File
@@ -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 }
+1 -1
View File
@@ -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
+51
View 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()))
}
+3
View File
@@ -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;
+92
View File
@@ -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"));
}
}
+1 -1
View File
@@ -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.
+4 -4
View File
@@ -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` →
+17 -2
View File
@@ -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
+7 -7
View File
@@ -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
+9
View File
@@ -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]
+194 -42
View File
@@ -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);
+32 -58
View File
@@ -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,12 +73,15 @@ 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!( panic!(
"user_theme_dir(): platform data directory is unavailable. \ "user_theme_dir(): platform data directory is unavailable. \
On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \ On Linux check $XDG_DATA_HOME or $HOME; on macOS / Windows \
@@ -93,31 +90,6 @@ fn detected_platform_data_dir() -> PathBuf {
set_user_theme_dir() before App::run()." set_user_theme_dir() before App::run()."
) )
}) })
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
panic!(
"user_theme_dir(): mobile entry point must call \
solitaire_engine::assets::user_dir::set_user_theme_dir() \
before App::run() there is no platform default."
)
}
#[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");
} }
+50 -15
View File
@@ -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);
}
} }
+33 -6
View File
@@ -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
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
+164
View File
@@ -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");
}
}
+3
View File
@@ -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)" },
], ],
}, },
]; ];
+6 -3
View File
@@ -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));
+2
View File
@@ -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,
+4 -3
View File
@@ -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
+9 -4
View File
@@ -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
+62
View File
@@ -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);
+45 -4
View File
@@ -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);
+32 -1
View File
@@ -317,6 +317,15 @@ fn handle_copy_share_link_button(
)); ));
return; return;
}; };
// Desktop: `arboard` writes the URL to the OS clipboard.
// Android: `arboard` has no platform backend (would fail to
// 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() { match arboard::Clipboard::new() {
Ok(mut cb) => match cb.set_text(url.clone()) { Ok(mut cb) => match cb.set_text(url.clone()) {
Ok(()) => { Ok(()) => {
@@ -336,6 +345,13 @@ fn handle_copy_share_link_button(
)); ));
} }
} }
}
#[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((
+21 -3
View File
@@ -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!(
+52 -16
View File
@@ -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]
+13 -8
View File
@@ -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,
+86 -74
View File
@@ -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.600.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