From 16cd0ae903cd1528f7d44953e063bf969f93452e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 8 Oct 2026 13:55:23 +0700 Subject: [PATCH] update theme --- crates/chat_ui/src/lib.rs | 6 ++--- crates/nip29_ui/src/message.rs | 4 +-- crates/theme/src/builtins.rs | 18 ++++++------- crates/theme/src/lib.rs | 4 +-- crates/theme/src/model.rs | 33 ++++++++++++++--------- crates/theme/src/resolved.rs | 3 +++ crates/theme/src/runtime.rs | 18 ------------- crates/ui/src/dock/mod.rs | 8 +++--- crates/ui/src/markdown.rs | 2 +- crates/ui/src/notification.rs | 2 +- crates/ui/src/resizable/mod.rs | 2 +- crates/workspace/src/dialogs/screening.rs | 2 +- crates/workspace/src/lib.rs | 9 +------ crates/workspace/src/sidebar/mod.rs | 2 +- 14 files changed, 49 insertions(+), 64 deletions(-) diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index f2dbe945..b9904c7c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1244,7 +1244,7 @@ impl ChatPanel { .w_full() .px_2() .border_l_2() - .border_color(cx.theme().accent.active) + .border_color(cx.theme().accent.strong) .text_sm() .child(div().font_semibold().child(author.name())) .child( @@ -1647,7 +1647,7 @@ impl ChatPanel { .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) - .child(Icon::new(IconName::Lock).text_color(cx.theme().accent.primary)) + .child(Icon::new(IconName::Lock).text_color(cx.theme().accent.strong)) .child( v_flex() .min_w_0() @@ -1753,7 +1753,7 @@ impl ChatPanel { .w_full() .pl_2() .border_l_2() - .border_color(cx.theme().accent.active) + .border_color(cx.theme().accent.strong) .child( div() .flex() diff --git a/crates/nip29_ui/src/message.rs b/crates/nip29_ui/src/message.rs index fbe5b48c..9b3116f9 100644 --- a/crates/nip29_ui/src/message.rs +++ b/crates/nip29_ui/src/message.rs @@ -180,8 +180,8 @@ pub(crate) fn render_activity(ix: usize, activity: &Activity, cx: &App) -> AnyEl fn badge(activity: &Activity, cx: &App) -> AnyElement { let (icon, color) = match activity.kind { - Kind::GroupJoinRequest => (IconName::Login, cx.theme().accent.primary), - Kind::GroupPutUser => (IconName::Login, cx.theme().accent.primary), + Kind::GroupJoinRequest => (IconName::Login, cx.theme().accent.strong), + Kind::GroupPutUser => (IconName::Login, cx.theme().accent.strong), Kind::GroupLeaveRequest => (IconName::Logout, cx.theme().danger), Kind::GroupRemoveUser => (IconName::Ban, cx.theme().danger), _ => (IconName::Info, cx.theme().text_faint), diff --git a/crates/theme/src/builtins.rs b/crates/theme/src/builtins.rs index 7bfbdd04..c72964f0 100644 --- a/crates/theme/src/builtins.rs +++ b/crates/theme/src/builtins.rs @@ -241,7 +241,7 @@ fn coop_dark() -> ThemeVariant { appearance: Appearance::Dark, treatment: SurfaceTreatment::Frosted, background: "#060606", - shell: "#0d0d0d", + shell: "#16161b", raised: "#343438", card: "#0e0e0e", text: "#e8e8ea", @@ -274,18 +274,18 @@ fn coop_light() -> ThemeVariant { name: "Coop Light", appearance: Appearance::Light, treatment: SurfaceTreatment::Frosted, - background: "#ffffff", - shell: "#f3f3f5", - raised: "#ededf0", + background: "#fffdf4", + shell: "#f7f3e8", + raised: "#ede8d8", card: "#ffffff", - text: "#303035", - muted: "#62626a", - faint: "#797981", - accent: "#a19200", + text: "#33302a", + muted: "#6d675a", + faint: "#87816f", + accent: "#FFE629", danger: "#dc2626", warning: "#a16207", success: "#15803d", - terminal_background: "#fafafa", + terminal_background: "#fbf8ee", ansi: ANSI_LIGHT, syntax: [ "#6b7280", "#5b43e8", "#15803d", "#a16207", "#7e22ce", "#2563eb", "#be185d", "#303035", diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 58da61ac..248f1072 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -29,7 +29,7 @@ pub fn init(cx: &mut App) { log::warn!("theme {}: {}", issue.variant_id, issue.message); } - Theme::sync_system_appearance(None, cx); + Theme::change(Appearance::default(), None, cx); Theme::sync_scrollbar_appearance(cx); } @@ -75,7 +75,7 @@ pub fn sync_base(cx: &mut App) { tokens.destructive_foreground = accent.on_danger; tokens.border = colors.border; tokens.input = colors.input; - tokens.ring = accent.primary; + tokens.ring = accent.strong; tokens.selection = accent.selection; } diff --git a/crates/theme/src/model.rs b/crates/theme/src/model.rs index f615e479..6c4ae091 100644 --- a/crates/theme/src/model.rs +++ b/crates/theme/src/model.rs @@ -109,10 +109,11 @@ impl Color { } pub fn best_on_color(self) -> Self { - if Self::WHITE.contrast(self) >= Self::BLACK.contrast(self) { - Self::WHITE - } else { + // Dark surfaces carry white text even where black would contrast more. + if self.luminance() > 0.3 { Self::BLACK + } else { + Self::WHITE } } @@ -275,7 +276,7 @@ impl AccentPreset { pub fn color(self, appearance: Appearance) -> Color { let (dark, light) = match self { - Self::Coop => ("#FFE629", "#a19200"), + Self::Coop => ("#FFE629", "#FFE629"), Self::Orange => ("#fb923c", "#c2410c"), Self::Amber => ("#fbbf24", "#a16207"), Self::Green => ("#4ade80", "#15803d"), @@ -320,11 +321,17 @@ pub struct AccentRoles { impl AccentRoles { pub fn derive(primary: Color, appearance: Appearance, background: Color) -> Self { - let primary = primary.ensure_contrast(background, 3.0); + // Light mode keeps the raw brand accent for fills; `strong` backs strokes at 3:1. + let (primary, mut strong) = if appearance.is_dark() { + let primary = primary.ensure_contrast(background, 3.0); + (primary, primary) + } else { + (primary, primary.ensure_contrast(background, 3.0)) + }; let on = primary.best_on_color(); - let mut strong = primary; - if on.contrast(strong) < 4.5 { - strong = strong.ensure_contrast(on, 4.5); + // 3:1 pairing so white-on-dark accents don't force the fill darker. + if on.contrast(strong) < 3.0 { + strong = strong.ensure_contrast(on, 3.0); } let light = primary.mix( if appearance.is_dark() { @@ -528,8 +535,8 @@ impl ThemeRegistry { validate_contrast( &mut issues, variant, - "accent", - variant.accent.primary, + "accent-strong", + variant.accent.strong, variant.colors.background, 3.0, ); @@ -539,7 +546,7 @@ impl ThemeRegistry { "on-accent", variant.accent.on, variant.accent.strong, - 4.5, + 3.0, ); validate_contrast( &mut issues, @@ -667,11 +674,11 @@ mod tests { for preset in AccentPreset::ALL { let roles = AccentRoles::derive(preset.color(appearance), appearance, background); assert!( - roles.primary.contrast(background) >= 3.0, + roles.strong.contrast(background) >= 3.0, "{appearance:?} {preset:?}" ); assert!( - roles.on.contrast(roles.strong) >= 4.5, + roles.on.contrast(roles.strong) >= 3.0, "{appearance:?} {preset:?}" ); } diff --git a/crates/theme/src/resolved.rs b/crates/theme/src/resolved.rs index f1667e18..acf0d2de 100644 --- a/crates/theme/src/resolved.rs +++ b/crates/theme/src/resolved.rs @@ -14,6 +14,8 @@ fn hsla(color: Color) -> Hsla { #[derive(Debug, Clone, Copy)] pub struct ResolvedAccentRoles { pub primary: Hsla, + /// Accent for strokes, icons, and text on the background; meets 3:1 contrast. + pub strong: Hsla, pub wash: Hsla, pub on: Hsla, pub selection: Hsla, @@ -32,6 +34,7 @@ impl ResolvedAccentRoles { let wash_background = accent.wash.blend_over(background); Self { primary: hsla(accent.primary), + strong: hsla(accent.strong), wash: hsla(accent.wash), on: hsla(accent.on), selection: hsla(accent.selection), diff --git a/crates/theme/src/runtime.rs b/crates/theme/src/runtime.rs index a5cb6f1b..fea39b96 100644 --- a/crates/theme/src/runtime.rs +++ b/crates/theme/src/runtime.rs @@ -10,15 +10,6 @@ use crate::model::{ use crate::resolved::{ResolvedAccentRoles, ResolvedColors}; use crate::{NotificationSettings, PlatformKind, ScrollbarMode, sync_base}; -impl From for Appearance { - fn from(appearance: gpui::WindowAppearance) -> Self { - match appearance { - gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => Self::Dark, - gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => Self::Light, - } - } -} - #[derive(Debug, Clone)] pub struct Theme { pub variant: Arc, @@ -99,15 +90,6 @@ impl Theme { } } - pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) { - let appearance = window - .as_ref() - .map(|window| window.appearance()) - .unwrap_or_else(|| cx.window_appearance()); - - Self::change(appearance, window, cx); - } - pub fn sync_scrollbar_appearance(cx: &mut App) { Theme::global_mut(cx).scrollbar_mode = if cx.should_auto_hide_scrollbars() { ScrollbarMode::Scrolling diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 3b12c1ed..7f9d26c1 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -465,7 +465,7 @@ impl TabGroupSkin { let bar = TabBar::new("tab-bar") .track_scroll(&self.scroll_handle) .h(TABBAR_HEIGHT) - .bg(cx.theme().shell) + .bg(cx.theme().background) .when(needs_traffic_light_padding, |this| { this.pl(px(TRAFFIC_LIGHT_PADDING)) }) @@ -567,7 +567,7 @@ impl TabGroupSkin { h_flex() .h(TABBAR_HEIGHT) .w_full() - .bg(cx.theme().shell) + .bg(cx.theme().background) .child(bar.flex_1()) .child(window_controls()) .into_any_element() @@ -717,7 +717,7 @@ impl TabGroupRenderer for TabGroupSkin { .child( div() .size_full() - .bg(cx.theme().shell) + .bg(cx.theme().background) .overflow_hidden() .child(panel.cached(StyleRefinement::default().v_flex().size_full())), ) @@ -760,7 +760,7 @@ impl TabGroupRenderer for TabGroupSkin { .h(height) .rounded(cx.theme().radius_lg) .border_1() - .border_color(cx.theme().accent.primary.opacity(0.3)) + .border_color(cx.theme().accent.strong.opacity(0.3)) .bg(cx.theme().accent.selection) .into_any_element(), ) diff --git a/crates/ui/src/markdown.rs b/crates/ui/src/markdown.rs index 19830451..2d9d136f 100644 --- a/crates/ui/src/markdown.rs +++ b/crates/ui/src/markdown.rs @@ -69,7 +69,7 @@ impl RenderedText { TextViewStyle::default() .with_foreground(theme.text) .with_muted_foreground(theme.text_muted) - .with_link(theme.accent.primary) + .with_link(theme.accent.strong) .with_code_background(theme.raised) .with_border(theme.border) .with_code_block(code_block) diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index a6a42f65..6fa3fc45 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -46,7 +46,7 @@ impl NotificationKind { .text_color(cx.theme().text_muted), Self::Success => Icon::new(IconName::CheckCircle) .with_size(Size::Medium) - .text_color(cx.theme().accent.primary), + .text_color(cx.theme().accent.strong), Self::Warning => Icon::new(IconName::Warning) .with_size(Size::Medium) .text_color(cx.theme().warning), diff --git a/crates/ui/src/resizable/mod.rs b/crates/ui/src/resizable/mod.rs index 1c69ef33..ecf48700 100644 --- a/crates/ui/src/resizable/mod.rs +++ b/crates/ui/src/resizable/mod.rs @@ -12,7 +12,7 @@ pub(crate) fn resize_handle_appearance() -> ResizeHandleRenderer { Rc::new( |context: &ResizeHandleContext, _: &mut Window, cx: &mut App| { let color = if context.is_active() { - cx.theme().accent.primary + cx.theme().accent.strong } else { cx.theme().border }; diff --git a/crates/workspace/src/dialogs/screening.rs b/crates/workspace/src/dialogs/screening.rs index 97940d76..1b72457e 100644 --- a/crates/workspace/src/dialogs/screening.rs +++ b/crates/workspace/src/dialogs/screening.rs @@ -533,7 +533,7 @@ fn status_badge(status: Option, cx: &App) -> Div { if let Some(status) = status { this.child(Icon::new(IconName::CheckCircle).small().text_color({ if status { - cx.theme().accent.primary + cx.theme().accent.strong } else { cx.theme().text_faint } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index d95ea6ae..a107fa5a 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -15,7 +15,7 @@ use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; -use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry, ThemeVariant}; +use theme::{ActiveTheme, SIDEBAR_WIDTH, ThemeRegistry, ThemeVariant}; use ui::button::{Button, ButtonVariants}; use ui::dock::{self, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::notification::{Notification, NotificationKind}; @@ -82,13 +82,6 @@ impl Workspace { let mut subscriptions = smallvec![]; - subscriptions.push( - // Observe system appearance and update theme - cx.observe_window_appearance(window, |_this, window, cx| { - Theme::sync_system_appearance(Some(window), cx); - }), - ); - subscriptions.push( // Subscribe to the nostr events cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| { diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index d29aff44..7652698d 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -336,7 +336,7 @@ impl Render for Sidebar { .text_xs() .font_semibold() .text_color(cx.theme().text_muted) - .child(Indicator::new().small().color(cx.theme().accent.primary)) + .child(Indicator::new().small().color(cx.theme().accent.strong)) .child("Getting messages..."), ), )