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>
This commit is contained in:
funman300
2026-05-07 17:56:08 -07:00
parent 4b51e50203
commit 0d477ac9fd
+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