fix(theme): WCAG-aware btn-primary-text token across all themes

Frontend (62 components, ~64 rules):
- Replace hardcoded `color: white` / `#fff` with `var(--btn-primary-text)`
  in every rule that paints text on `var(--accent-primary)`. Covers in-rule
  cases (BulkActionBar, GenreFilterButton, KeybindingsSettings, settings
  views, login, modal primary buttons, wizard steps, etc.) plus descendant
  selectors (`.parent.selected .child`).
- Semi-transparent rgba(255,255,255,X) over selected accent rows now use
  `color-mix(in srgb, var(--btn-primary-text) X%, transparent)` so the mute
  level inherits the per-theme token color.

Theme tokens in app.css (--btn-primary-text fixes):
- rose-pine-dawn: #faf4ed → #575279 (was the bg color, ratio 2.60 FAIL;
  now 5.71 AA against coral accent)
- kurosaki:       #fbf9f2 → #262424 (was the bg color, ratio 1.76 FAIL;
  now 8.33 AA against gold accent)
- snow-storm:     #eceff4 → #2e3440 (was the bg color, ratio 3.50 AA-Large;
  now 6.13 AA against blue accent)
- stratego:       #0a0a0a → #ffffff (visual choice; 4.14 AA-Large is
  acceptable for button labels and matches palette intent)

Auto-theme generator (src-tauri/src/auto_theme/generator.rs):
- pick_btn_text_for_accent_set(): WCAG-aware FG selection that holds
  across accent / hover / active states. Replaces the broken
  `luminance < 0.5` threshold which produced 2.0:1 ratios on mid-luminance
  accents (Catppuccin lavender, Dracula purple, rose-pine coral).
- ensure_text_contrast_target(text, bg, is_dark, target): generalize the
  iterative contrast helper to accept a target ratio. Apply to
  text_secondary (4.5) and text_muted (3.0).
- Add --btn-danger-text and --btn-warning-text tokens via
  pick_btn_text_for_bg for future status-button consumers.

Misc fixes:
- SettingsView.svelte: extract initialTitlebarMode to const, avoids the
  Svelte 5 state_referenced_locally warning.
- QueuePanel.svelte: wrap .stop-after-marker in :global() (the class is
  forwarded to a Svelte component child, not a native element) and fix
  var(--accent) typo to the correct var(--accent-primary).

Tests: 13 generator tests pass (8 prior + 5 new). svelte-check 0 errors,
31 warnings (down from 33).

cc
This commit is contained in:
vicrodh committed 2026-05-05 16:43:45 -06:00
1 parent cdf99f9709
commit 075e32b680
41 files changed
+341 -139

No files matched your search

+258 -57
View File
@@ -31,55 +31,61 @@ pub fn generate_theme(palette: &ThemePalette, source: &str) -> GeneratedTheme {
)
};
// Adjust text-primary for contrast if needed
// Verify each text tier against bg_primary at its target contrast ratio.
// text_disabled is intentionally low-contrast (visual cue for disabled state).
let text_primary = ensure_text_contrast(text_primary, &palette.bg_primary, palette.is_dark);
let text_secondary =
ensure_text_contrast_target(text_secondary, &palette.bg_primary, palette.is_dark, 4.5);
let text_muted =
ensure_text_contrast_target(text_muted, &palette.bg_primary, palette.is_dark, 3.0);
vars.insert("--text-primary".into(), text_primary.to_hex());
vars.insert("--text-secondary".into(), text_secondary.to_hex());
vars.insert("--text-muted".into(), text_muted.to_hex());
vars.insert("--text-disabled".into(), text_disabled.to_hex());
// Accent
vars.insert("--accent-primary".into(), palette.accent.to_hex());
vars.insert(
"--accent-hover".into(),
palette.accent.shift_lightness(0.10).to_hex(),
);
vars.insert(
"--accent-active".into(),
palette.accent.shift_lightness(-0.10).to_hex(),
);
// Accent triplet — pick btn-primary-text once that works across all three states.
let accent = palette.accent;
let accent_hover = accent.shift_lightness(0.10);
let accent_active = accent.shift_lightness(-0.10);
vars.insert("--accent-primary".into(), accent.to_hex());
vars.insert("--accent-hover".into(), accent_hover.to_hex());
vars.insert("--accent-active".into(), accent_active.to_hex());
// Button text: white if accent is dark, black if light
let btn_text = if palette.accent.luminance() < 0.5 {
PaletteColor::new(255, 255, 255)
} else {
PaletteColor::new(0, 0, 0)
};
let btn_text = pick_btn_text_for_accent_set(&accent, &accent_hover, &accent_active);
vars.insert("--btn-primary-text".into(), btn_text.to_hex());
// Status colors (fixed safe values, consistent with existing themes)
if palette.is_dark {
vars.insert("--danger".into(), "#ef4444".into());
vars.insert("--danger-bg".into(), "rgba(239, 68, 68, 0.1)".into());
vars.insert("--danger-border".into(), "rgba(239, 68, 68, 0.3)".into());
vars.insert("--danger-hover".into(), "rgba(239, 68, 68, 0.2)".into());
vars.insert("--warning".into(), "#fbbf24".into());
vars.insert("--warning-bg".into(), "rgba(251, 191, 36, 0.1)".into());
vars.insert("--warning-border".into(), "rgba(251, 191, 36, 0.3)".into());
vars.insert("--warning-hover".into(), "rgba(251, 191, 36, 0.2)".into());
// Status colors with paired FG tokens (--btn-danger-text, --btn-warning-text).
let (danger, warning) = if palette.is_dark {
(
PaletteColor::new(239, 68, 68),
PaletteColor::new(251, 191, 36),
)
} else {
vars.insert("--danger".into(), "#dc2626".into());
vars.insert("--danger-bg".into(), "rgba(220, 38, 38, 0.1)".into());
vars.insert("--danger-border".into(), "rgba(220, 38, 38, 0.3)".into());
vars.insert("--danger-hover".into(), "rgba(220, 38, 38, 0.15)".into());
(
PaletteColor::new(220, 38, 38),
PaletteColor::new(217, 119, 6),
)
};
let hover_alpha = if palette.is_dark { 0.2 } else { 0.15 };
vars.insert("--warning".into(), "#d97706".into());
vars.insert("--warning-bg".into(), "rgba(217, 119, 6, 0.1)".into());
vars.insert("--warning-border".into(), "rgba(217, 119, 6, 0.3)".into());
vars.insert("--warning-hover".into(), "rgba(217, 119, 6, 0.15)".into());
}
vars.insert("--danger".into(), danger.to_hex());
vars.insert("--danger-bg".into(), danger.to_rgba(0.1));
vars.insert("--danger-border".into(), danger.to_rgba(0.3));
vars.insert("--danger-hover".into(), danger.to_rgba(hover_alpha));
vars.insert(
"--btn-danger-text".into(),
pick_btn_text_for_bg(&danger).to_hex(),
);
vars.insert("--warning".into(), warning.to_hex());
vars.insert("--warning-bg".into(), warning.to_rgba(0.1));
vars.insert("--warning-border".into(), warning.to_rgba(0.3));
vars.insert("--warning-hover".into(), warning.to_rgba(hover_alpha));
vars.insert(
"--btn-warning-text".into(),
pick_btn_text_for_bg(&warning).to_hex(),
);
// Borders: subtle shifts from bg_primary
let border_subtle = if palette.is_dark {
@@ -194,42 +200,41 @@ pub fn generate_theme_from_scheme(scheme: &SystemColorScheme) -> GeneratedTheme
let text_primary = ensure_text_contrast(text_primary, &window_bg, is_dark);
vars.insert("--text-primary".into(), text_primary.to_hex());
// Secondary: slightly dimmed from primary
let text_secondary = scheme
// Secondary/muted/disabled — verify contrast (4.5 / 3.0; disabled stays low).
let text_secondary_raw = scheme
.view_fg
.unwrap_or_else(|| text_primary.shift_lightness(if is_dark { -0.10 } else { 0.10 }));
let text_secondary =
ensure_text_contrast_target(text_secondary_raw, &window_bg, is_dark, 4.5);
vars.insert("--text-secondary".into(), text_secondary.to_hex());
let text_muted = scheme
let text_muted_raw = scheme
.window_fg_inactive
.unwrap_or_else(|| text_primary.shift_lightness(if is_dark { -0.25 } else { 0.25 }));
let text_muted = ensure_text_contrast_target(text_muted_raw, &window_bg, is_dark, 3.0);
vars.insert("--text-muted".into(), text_muted.to_hex());
let text_disabled = text_muted.shift_lightness(if is_dark { -0.10 } else { 0.10 });
vars.insert("--text-disabled".into(), text_disabled.to_hex());
// ── Accent (selection) ───────────────────────────────────────────────
// ── Accent triplet (selection) ───────────────────────────────────────
let accent = scheme
.accent
.or(scheme.selection_bg)
.unwrap_or(PaletteColor::new(0, 120, 215));
vars.insert("--accent-primary".into(), accent.to_hex());
let accent_hover = scheme
.selection_hover
.unwrap_or_else(|| accent.shift_lightness(0.10));
let accent_active = accent.shift_lightness(-0.10);
vars.insert("--accent-primary".into(), accent.to_hex());
vars.insert("--accent-hover".into(), accent_hover.to_hex());
vars.insert(
"--accent-active".into(),
accent.shift_lightness(-0.10).to_hex(),
);
vars.insert("--accent-active".into(), accent_active.to_hex());
// Button text on accent
let btn_text = scheme.selection_fg.unwrap_or(if accent.luminance() < 0.5 {
PaletteColor::new(255, 255, 255)
} else {
PaletteColor::new(0, 0, 0)
});
// Trust DE-provided selection_fg if present — otherwise compute against the
// full triplet so hover/active states stay legible too.
let btn_text = scheme
.selection_fg
.unwrap_or_else(|| pick_btn_text_for_accent_set(&accent, &accent_hover, &accent_active));
vars.insert("--btn-primary-text".into(), btn_text.to_hex());
// ── Status colors ────────────────────────────────────────────────────
@@ -246,6 +251,10 @@ pub fn generate_theme_from_scheme(scheme: &SystemColorScheme) -> GeneratedTheme
"--danger-hover".into(),
danger.to_rgba(if is_dark { 0.2 } else { 0.15 }),
);
vars.insert(
"--btn-danger-text".into(),
pick_btn_text_for_bg(&danger).to_hex(),
);
let warning = scheme.fg_neutral.unwrap_or(if is_dark {
PaletteColor::new(251, 191, 36)
@@ -259,6 +268,10 @@ pub fn generate_theme_from_scheme(scheme: &SystemColorScheme) -> GeneratedTheme
"--warning-hover".into(),
warning.to_rgba(if is_dark { 0.2 } else { 0.15 }),
);
vars.insert(
"--btn-warning-text".into(),
pick_btn_text_for_bg(&warning).to_hex(),
);
// ── Borders ──────────────────────────────────────────────────────────
let border_subtle = if is_dark {
@@ -315,13 +328,55 @@ pub fn generate_theme_from_scheme(scheme: &SystemColorScheme) -> GeneratedTheme
}
}
/// Pick the best foreground color for text on the given background (typically `accent`).
///
/// Strategy mirrors what we do in the static themes:
/// 1. If pure white reaches WCAG AA Large (3:1) on the bg, prefer it. This matches the
/// visual convention "light text on accent button" used across the app, and accepts
/// AA-Large for button labels (which are typically ≥14px bold or ≥18px regular).
/// 2. Otherwise, pick whichever of white/black has the higher contrast ratio.
///
/// This avoids the trap of the simple `luminance < 0.5` threshold, which produces
/// catastrophic 2.0:1 ratios for mid-luminance accents like `#cba6f7` (lavender).
fn pick_btn_text_for_bg(bg: &PaletteColor) -> PaletteColor {
let white = PaletteColor::new(255, 255, 255);
let black = PaletteColor::new(0, 0, 0);
let white_ratio = white.contrast_ratio(bg);
let black_ratio = black.contrast_ratio(bg);
if white_ratio >= 3.0 {
white
} else if black_ratio > white_ratio {
black
} else {
white
}
}
/// Ensure text has at least WCAG AA contrast (4.5:1) against the background.
fn ensure_text_contrast(text: PaletteColor, bg: &PaletteColor, is_dark: bool) -> PaletteColor {
if text.contrast_ratio(bg) >= 4.5 {
ensure_text_contrast_target(text, bg, is_dark, 4.5)
}
/// Ensure text has at least the given contrast ratio against the background.
///
/// Iteratively shifts lightness toward white (dark themes) or black (light themes)
/// until the target is met, then falls back to pure white/black after 20 steps.
///
/// Common targets:
/// - `4.5` — WCAG AA Normal text (use for body/secondary copy)
/// - `3.0` — WCAG AA Large text or UI components (use for muted helper text on
/// buttons ≥14px bold or ≥18px regular)
fn ensure_text_contrast_target(
text: PaletteColor,
bg: &PaletteColor,
is_dark: bool,
target: f64,
) -> PaletteColor {
if text.contrast_ratio(bg) >= target {
return text;
}
// Shift text toward white (dark theme) or black (light theme) until contrast is met
let (h, s, l) = text.to_hsl();
let direction = if is_dark { 0.05 } else { -0.05 };
let mut new_l = l;
@@ -329,12 +384,11 @@ fn ensure_text_contrast(text: PaletteColor, bg: &PaletteColor, is_dark: bool) ->
for _ in 0..20 {
new_l = (new_l + direction).clamp(0.0, 1.0);
let candidate = PaletteColor::from_hsl(h, s, new_l);
if candidate.contrast_ratio(bg) >= 4.5 {
if candidate.contrast_ratio(bg) >= target {
return candidate;
}
}
// Absolute fallback
if is_dark {
PaletteColor::new(255, 255, 255)
} else {
@@ -342,6 +396,40 @@ fn ensure_text_contrast(text: PaletteColor, bg: &PaletteColor, is_dark: bool) ->
}
}
/// Pick the best foreground text color for the accent triplet (base, hover, active).
///
/// Like `pick_btn_text_for_bg` but considers the worst case across all three
/// accent variants — so the chosen FG works on `:hover` and `:active` states too.
/// Hover shifts lightness +0.10 (worst case for white text on dark accents);
/// active shifts -0.10 (worst case for black text on light accents).
fn pick_btn_text_for_accent_set(
accent: &PaletteColor,
accent_hover: &PaletteColor,
accent_active: &PaletteColor,
) -> PaletteColor {
let white = PaletteColor::new(255, 255, 255);
let black = PaletteColor::new(0, 0, 0);
// Worst case for white text is the lightest variant (lowest delta to white).
let white_worst = white
.contrast_ratio(accent)
.min(white.contrast_ratio(accent_hover))
.min(white.contrast_ratio(accent_active));
// Worst case for black text is the darkest variant.
let black_worst = black
.contrast_ratio(accent)
.min(black.contrast_ratio(accent_hover))
.min(black.contrast_ratio(accent_active));
if white_worst >= 3.0 {
white
} else if black_worst > white_worst {
black
} else {
white
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -455,6 +543,8 @@ mod tests {
"--alpha-85",
"--alpha-90",
"--alpha-95",
"--btn-danger-text",
"--btn-warning-text",
];
for key in &required {
@@ -465,4 +555,115 @@ mod tests {
);
}
}
#[test]
fn pick_btn_text_prefers_white_when_aa_large_passes() {
// Stratego red: white = 4.14:1 (AA-Large), black = 5.07:1 (AA Normal).
// The static-theme decision was to keep white for visual consistency.
let red = PaletteColor::new(0xed, 0x2f, 0x3d);
assert_eq!(pick_btn_text_for_bg(&red), PaletteColor::new(255, 255, 255));
}
#[test]
fn pick_btn_text_picks_black_when_white_fails_aa_large() {
// Catppuccin Mocha lavender: white = 2.03:1 (FAIL), black = 8.07:1 (AAA).
// The old luminance-threshold logic incorrectly picked white here because
// lum=0.55 ≥ 0.5 — but contrast was catastrophic.
let lavender = PaletteColor::new(0xcb, 0xa6, 0xf7);
assert_eq!(pick_btn_text_for_bg(&lavender), PaletteColor::new(0, 0, 0));
}
#[test]
fn pick_btn_text_picks_black_for_dracula_purple() {
// bd93f9: white = 2.41 (FAIL), black = 8.71 (AAA). Old code picked white
// (lum 0.43 < 0.5) — wrong.
let purple = PaletteColor::new(0xbd, 0x93, 0xf9);
assert_eq!(pick_btn_text_for_bg(&purple), PaletteColor::new(0, 0, 0));
}
#[test]
fn pick_btn_text_handles_high_contrast_yellow() {
// ffff00: white = 1.07 (utterly fails), black = 19.56 (AAA). Must pick black.
let yellow = PaletteColor::new(255, 255, 0);
assert_eq!(pick_btn_text_for_bg(&yellow), PaletteColor::new(0, 0, 0));
}
#[test]
fn pick_btn_text_white_on_dark_navy() {
// Deep blue: white = ~14:1, black = ~1.5:1. Trivially white.
let navy = PaletteColor::new(0x12, 0x1a, 0x40);
assert_eq!(pick_btn_text_for_bg(&navy), PaletteColor::new(255, 255, 255));
}
#[test]
fn ensure_text_contrast_target_aa_normal_meets_45() {
// Mid-grey text on dark bg: starts at ~3:1, must shift to ≥4.5.
let bg = PaletteColor::new(20, 20, 20);
let dim_text = PaletteColor::new(110, 110, 110);
let result = ensure_text_contrast_target(dim_text, &bg, true, 4.5);
assert!(
result.contrast_ratio(&bg) >= 4.5,
"got {:.2}",
result.contrast_ratio(&bg)
);
}
#[test]
fn ensure_text_contrast_target_aa_large_meets_3() {
// Same dim text but only need AA-Large — should converge faster (or be unchanged
// if already ≥3).
let bg = PaletteColor::new(20, 20, 20);
let dim_text = PaletteColor::new(110, 110, 110);
let result = ensure_text_contrast_target(dim_text, &bg, true, 3.0);
assert!(
result.contrast_ratio(&bg) >= 3.0,
"got {:.2}",
result.contrast_ratio(&bg)
);
}
#[test]
fn pick_btn_text_for_accent_set_holds_across_hover_active() {
// Catppuccin Mocha lavender + ±10% lightness variants.
// Black should be picked because white fails everywhere.
let accent = PaletteColor::new(0xcb, 0xa6, 0xf7);
let hover = accent.shift_lightness(0.10);
let active = accent.shift_lightness(-0.10);
assert_eq!(
pick_btn_text_for_accent_set(&accent, &hover, &active),
PaletteColor::new(0, 0, 0)
);
}
#[test]
fn pick_btn_text_for_accent_set_picks_white_for_deep_navy() {
// Deep navy stays dark across the triplet — white is the safe choice.
let accent = PaletteColor::new(0x12, 0x1a, 0x40);
let hover = accent.shift_lightness(0.10);
let active = accent.shift_lightness(-0.10);
assert_eq!(
pick_btn_text_for_accent_set(&accent, &hover, &active),
PaletteColor::new(255, 255, 255)
);
}
#[test]
fn generated_theme_includes_btn_status_text_tokens() {
let palette = ThemePalette {
bg_primary: PaletteColor::new(20, 20, 20),
bg_secondary: PaletteColor::new(30, 30, 30),
bg_tertiary: PaletteColor::new(45, 45, 45),
bg_hover: PaletteColor::new(35, 35, 35),
accent: PaletteColor::new(100, 200, 100),
is_dark: true,
all_colors: vec![],
};
let theme = generate_theme(&palette, "test");
assert!(theme.variables.contains_key("--btn-danger-text"));
assert!(theme.variables.contains_key("--btn-warning-text"));
// For #ef4444 (dark theme red): white passes AA-Large → expect white.
assert_eq!(theme.variables.get("--btn-danger-text").unwrap(), "#ffffff");
// For #fbbf24 (dark theme gold): white fails (1.92), black wins.
assert_eq!(theme.variables.get("--btn-warning-text").unwrap(), "#000000");
}
}
+4 -4
View File
@@ -479,7 +479,7 @@
--accent-primary: #d7827e;
--accent-hover: #e5a478;
--accent-active: #286983;
--btn-primary-text: #faf4ed;
--btn-primary-text: #575279;
--danger: #b4637a;
--danger-bg: rgba(180, 99, 122, 0.1);
--danger-border: rgba(180, 99, 122, 0.3);
@@ -901,7 +901,7 @@
--accent-primary: #5e81ac;
--accent-hover: #81a1c1;
--accent-active: #88c0d0;
--btn-primary-text: #eceff4;
--btn-primary-text: #2e3440;
--danger: #bf616a;
--danger-bg: rgba(191, 97, 106, 0.1);
--danger-border: rgba(191, 97, 106, 0.3);
@@ -1237,7 +1237,7 @@
--accent-primary: #ed2f3d;
--accent-hover: #f74a58;
--accent-active: #c41e2a;
--btn-primary-text: #0a0a0a;
--btn-primary-text: #ffffff;
/* Status — bright alert red (distinct from accent) + antique gold (dragon crest horns) */
--danger: #e63946;
@@ -1294,7 +1294,7 @@
--accent-primary: #d5be58;
--accent-hover: #e8ce66;
--accent-active: #b49f45;
--btn-primary-text: #fbf9f2;
--btn-primary-text: #262424;
/* Status colours */
--danger: #c0392b;
+1 -1
View File
@@ -334,7 +334,7 @@
.link-btn:hover {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.link-btn :global(svg:last-child) {
+1 -1
View File
@@ -746,7 +746,7 @@
align-items: center;
padding: 10px 20px;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 14px;
+1 -1
View File
@@ -685,7 +685,7 @@
background: var(--accent-primary);
border: none;
border-radius: 50%;
color: white;
color: var(--btn-primary-text);
cursor: pointer;
transition: transform 150ms ease, background 150ms ease;
}
+1 -1
View File
@@ -297,7 +297,7 @@
.action-btn.primary {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.action-btn.primary:hover {
+2 -2
View File
@@ -585,7 +585,7 @@
.protocol-tab.active {
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.protocol-tab .count {
@@ -708,7 +708,7 @@
.connect-btn {
padding: 8px 16px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 6px;
font-size: 13px;
+2 -2
View File
@@ -1094,7 +1094,7 @@
background: var(--accent-primary);
border: none;
border-radius: 6px;
color: white;
color: var(--btn-primary-text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
@@ -1366,7 +1366,7 @@
.btn-primary {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.btn-primary:hover:not(:disabled) {
+1 -1
View File
@@ -146,7 +146,7 @@
.genre-filter-btn.active {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
+8 -8
View File
@@ -630,7 +630,7 @@
}
.genre-card.selected .genre-name {
color: white;
color: var(--btn-primary-text);
}
.check-circle {
@@ -732,12 +732,12 @@
}
.node-row.selected .expand-btn {
color: rgba(255, 255, 255, 0.7);
color: color-mix(in srgb, var(--btn-primary-text) 70%, transparent);
}
.node-row.selected .expand-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
color: var(--btn-primary-text);
}
.expand-placeholder {
@@ -786,11 +786,11 @@
.check-box.partial {
border-color: var(--accent-primary);
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.node-row.selected .check-box {
border-color: white;
border-color: var(--btn-primary-text);
}
.node-row.selected .check-box.checked {
@@ -807,7 +807,7 @@
}
.node-row.selected .node-name {
color: white;
color: var(--btn-primary-text);
}
.descendant-count {
@@ -820,8 +820,8 @@
}
.node-row.selected .descendant-count {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.8);
background: color-mix(in srgb, var(--btn-primary-text) 20%, transparent);
color: color-mix(in srgb, var(--btn-primary-text) 80%, transparent);
}
.children-container {
@@ -229,7 +229,7 @@
font-size: 11px;
padding: 2px 8px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-radius: 10px;
font-weight: 500;
}
+2 -2
View File
@@ -217,7 +217,7 @@
.go-btn {
padding: 10px 20px;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 14px;
@@ -256,7 +256,7 @@
align-self: flex-start;
padding: 8px 16px;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border: none;
border-radius: 6px;
font-size: 13px;
+1 -1
View File
@@ -226,7 +226,7 @@
.tab.active {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
+1 -1
View File
@@ -309,7 +309,7 @@
background: var(--accent-primary);
border: none;
border-radius: 10px;
color: white;
color: var(--btn-primary-text);
font-size: 14px;
font-weight: 600;
cursor: pointer;
+1 -1
View File
@@ -171,7 +171,7 @@
}
.btn-primary {
background: var(--accent-primary);
color: #ffffff;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
margin-left: 8px;
}
+1 -1
View File
@@ -935,7 +935,7 @@
gap: 8px;
padding: 4px 12px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.02em;
@@ -590,7 +590,7 @@
margin-top: 12px;
padding: 6px 16px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 6px;
cursor: pointer;
@@ -739,7 +739,7 @@
.action-btn.add:hover {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.action-btn.dismiss {
@@ -796,7 +796,7 @@
.action-btn.add.added {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.btn-spinner {
+1 -1
View File
@@ -151,7 +151,7 @@
.tag-filter-btn.active {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
+1 -1
View File
@@ -774,7 +774,7 @@
.mini-btn:active {
transform: scale(0.95);
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.mini-btn.danger {
@@ -203,7 +203,7 @@
.btn.primary {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.btn.primary:hover {
+3 -3
View File
@@ -1078,7 +1078,7 @@
padding: 10px 16px;
background: var(--accent-primary, #6366f1);
background: linear-gradient(135deg, var(--accent-primary, #6366f1) 0%, color-mix(in srgb, var(--accent-primary, #6366f1) 80%, #000) 100%);
color: white;
color: var(--btn-primary-text);
font-size: 12px;
}
@@ -1205,7 +1205,7 @@
color: var(--text-primary);
}
.stop-after-marker {
color: var(--accent);
:global(.stop-after-marker) {
color: var(--accent-primary);
}
</style>
@@ -635,7 +635,7 @@
.platform-tab.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.platform-instructions {
@@ -730,7 +730,7 @@
.btn-primary {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
}
@@ -345,7 +345,7 @@
.search-btn {
padding: 6px 16px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 6px;
font-size: 13px;
@@ -525,7 +525,7 @@
.preview-btn:hover {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.selected-indicator {
@@ -535,7 +535,7 @@
align-items: center;
justify-content: center;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-radius: 50%;
}
@@ -571,7 +571,7 @@
.confirm-btn {
background: var(--accent-primary);
border: none;
color: white;
color: var(--btn-primary-text);
}
.confirm-btn:hover:not(:disabled) {
@@ -397,7 +397,7 @@
.seg.active {
background: var(--accent-primary);
color: #ffffff;
color: var(--btn-primary-text);
}
.seg + .seg {
+1 -1
View File
@@ -189,7 +189,7 @@
.control-btn.primary {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.control-btn:disabled {
@@ -1278,7 +1278,7 @@
}
.genre-card.selected .genre-name {
color: white;
color: var(--btn-primary-text);
}
.check-circle {
@@ -436,7 +436,7 @@
gap: 8px;
padding: 10px 20px;
background: var(--accent-primary);
color: #ffffff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 14px;
@@ -551,7 +551,7 @@
padding: 0 4px;
border-radius: 8px;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
font-size: 10px;
font-weight: 600;
}
@@ -613,7 +613,7 @@
}
.view-mode-group .control-btn.seg.active {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
@@ -1054,7 +1054,7 @@
}
.segment.active {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
}
/* ── State messages ── */
@@ -1416,7 +1416,7 @@
.primary-btn {
padding: 10px 20px;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 13px;
@@ -2739,7 +2739,7 @@
margin-top: 16px;
padding: 8px 24px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
cursor: pointer;
@@ -880,7 +880,7 @@
margin-top: 16px;
padding: 8px 24px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
cursor: pointer;
+1 -1
View File
@@ -1371,7 +1371,7 @@
@keyframes spin { to { transform: rotate(360deg); } }
.retry-btn {
margin-top: 16px; padding: 8px 24px;
background-color: var(--accent-primary); color: white;
background-color: var(--accent-primary); color: var(--btn-primary-text);
border: none; border-radius: 8px; cursor: pointer;
}
.retry-btn:hover { opacity: 0.9; }
@@ -5426,7 +5426,7 @@
.control-btn.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.control-btn.active:hover {
@@ -5516,7 +5516,7 @@
height: 18px;
padding: 0 5px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
font-size: 11px;
font-weight: 600;
border-radius: 9px;
@@ -5748,7 +5748,7 @@
margin-top: 16px;
padding: 8px 24px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
cursor: pointer;
@@ -6730,11 +6730,11 @@
}
.artist-card-compact.selected .artist-name {
color: white;
color: var(--btn-primary-text);
}
.artist-card-compact.selected .artist-meta {
color: rgba(255, 255, 255, 0.7);
color: color-mix(in srgb, var(--btn-primary-text) 70%, transparent);
}
.artist-card-image {
+2 -2
View File
@@ -627,7 +627,7 @@
.retry-btn {
padding: 10px 24px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
cursor: pointer;
@@ -699,7 +699,7 @@
justify-content: center;
gap: 8px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 16px;
@@ -2482,7 +2482,7 @@
font-size: 10px;
font-weight: 600;
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border-radius: 10px;
margin-left: 2px;
}
@@ -2508,7 +2508,7 @@
}
.view-mode-group .control-btn.seg.active {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
@@ -3089,7 +3089,7 @@
.m-btn-primary {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border: none;
}
.m-btn-primary:disabled {
+2 -2
View File
@@ -351,7 +351,7 @@
gap: 8px;
padding: 10px 20px;
background: var(--accent-primary);
color: #ffffff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 14px;
@@ -510,7 +510,7 @@
}
.view-mode-group .control-btn.seg.active {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
@@ -3053,7 +3053,7 @@
margin-top: 16px;
padding: 8px 24px;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
cursor: pointer;
@@ -1270,7 +1270,7 @@
.control-btn.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.control-btn.active:hover {
@@ -1371,7 +1371,7 @@
height: 18px;
padding: 0 5px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
font-size: 11px;
font-weight: 600;
border-radius: 9px;
@@ -1828,7 +1828,7 @@
.download-btn:hover {
background: var(--accent-primary);
color: #fff;
color: var(--btn-primary-text);
border-color: var(--accent-primary);
}
+8 -7
View File
@@ -1005,12 +1005,13 @@
let windowTitleTemplate = $state(DEFAULT_WINDOW_TITLE_TEMPLATE);
// Title bar settings
let titlebarMode = $state<TitlebarMode>(getMode());
const initialTitlebarMode: TitlebarMode = getMode();
let titlebarMode = $state<TitlebarMode>(initialTitlebarMode);
// Fallback list before v2_available_titlebar_modes resolves. Always include
// the currently active mode so the dropdown shows the user's selected value
// even when the backend invoke fails.
let availableModes = $state<TitlebarMode[]>(
titlebarMode === 'plasma' ? ['qbz', 'system', 'plasma', 'hidden'] : ['qbz', 'system', 'hidden']
initialTitlebarMode === 'plasma' ? ['qbz', 'system', 'plasma', 'hidden'] : ['qbz', 'system', 'hidden']
);
let isSandboxed = $state(false);
let matchSystemWindowChromeState = $state(getMatchSystemWindowChrome());
@@ -6391,7 +6392,7 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
background: var(--accent-primary);
border: none;
border-radius: 6px;
color: white;
color: var(--btn-primary-text);
cursor: pointer;
transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease, opacity 150ms ease;
flex-shrink: 0;
@@ -6636,7 +6637,7 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
display: flex;
align-items: center;
justify-content: center;
color: white;
color: var(--btn-primary-text);
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
@@ -7021,7 +7022,7 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
cursor: pointer;
transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease, opacity 150ms ease;
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
}
@@ -7280,7 +7281,7 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
.auth-start-btn {
background-color: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
}
@@ -7521,7 +7522,7 @@ flatpak override --user --filesystem=/home/USUARIO/Música com.blitzfc.qbz</pre>
.copy-btn {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border: none;
border-radius: 6px;
padding: 6px 14px;
@@ -197,7 +197,7 @@
background: var(--accent-primary);
border: none;
border-radius: 6px;
color: white;
color: var(--btn-primary-text);
font-size: 20px;
cursor: pointer;
transition: opacity 150ms ease;
@@ -233,7 +233,7 @@
gap: 4px;
padding: 4px 8px;
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
border-radius: 4px;
font-size: 13px;
font-family: var(--font-sans);
@@ -107,12 +107,12 @@
.step-button.complete .step-indicator {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
}
.step-button.active .step-indicator {
background: var(--accent-primary);
color: white;
color: var(--btn-primary-text);
box-shadow: 0 0 0 4px rgba(66, 133, 244, 0.2);
}
+1 -1
View File
@@ -7303,7 +7303,7 @@
.create-modal .primary-btn {
padding: 10px 20px;
background: var(--accent-primary);
color: #ffffff;
color: var(--btn-primary-text);
border: none;
border-radius: 8px;
font-size: 14px;