add brand theme

This commit is contained in:
2026-08-20 17:52:02 +07:00
parent 584a8385a1
commit 8be65b7904
9 changed files with 746 additions and 83 deletions
+3
View File
@@ -10,3 +10,6 @@ gpui-component.workspace = true
anyhow.workspace = true
log.workspace = true
rust-embed.workspace = true
[dev-dependencies]
serde_json.workspace = true
+576
View File
@@ -0,0 +1,576 @@
{
"$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json",
"name": "Signed",
"author": "Signed",
"url": "https://git.reya.su/reya/signed",
"themes": [
{
"name": "Signed Light",
"mode": "light",
"colors": {
"accent.background": "#F4F4F5",
"accent.foreground": "#18181B",
"accordion.background": "#FFFFFF",
"background": "#FFFFFF",
"border": "#E4E4E7",
"button.background": "#FFFFFF",
"button.active.background": "#E4E4E7",
"button.foreground": "#18181B",
"button.hover.background": "#F4F4F5",
"button.danger.background": "#DC2626",
"button.danger.active.background": "#B91C1C",
"button.danger.foreground": "#FFFFFF",
"button.danger.hover.background": "#EF4444",
"button.info.background": "#0284C7",
"button.info.active.background": "#0369A1",
"button.info.foreground": "#FFFFFF",
"button.info.hover.background": "#0EA5E9",
"button.primary.background": "#C6FF4D",
"button.primary.active.background": "#65A30D",
"button.primary.foreground": "#0B0F0C",
"button.primary.hover.background": "#B2EC3C",
"button.secondary.background": "#F4F4F5",
"button.secondary.active.background": "#D4D4D8",
"button.secondary.foreground": "#18181B",
"button.secondary.hover.background": "#E4E4E7",
"button.success.background": "#2FBF71",
"button.success.active.background": "#1F9A58",
"button.success.foreground": "#0B0F0C",
"button.success.hover.background": "#2AB568",
"button.warning.background": "#D97706",
"button.warning.active.background": "#B45309",
"button.warning.foreground": "#FFFFFF",
"button.warning.hover.background": "#F59E0B",
"group_box.background": "#F4F4F5",
"group_box.foreground": "#18181B",
"group_box.title.foreground": "#18181B",
"caret": "#18181B",
"chart.1": "#65A30D",
"chart.2": "#2FBF71",
"chart.3": "#3F6212",
"chart.4": "#7A8B5F",
"chart.5": "#A1A1AA",
"chart_bullish": "#2FBF71",
"chart_bearish": "#DC2626",
"danger.background": "#DC2626",
"danger.active.background": "#B91C1C",
"danger.foreground": "#FFFFFF",
"danger.hover.background": "#EF4444",
"description_list.label.background": "#F4F4F5",
"description_list.label.foreground": "#71717A",
"drag.border": "#65A30D",
"drop_target.background": "#65A30D33",
"foreground": "#18181B",
"info.background": "#0284C7",
"info.active.background": "#0369A1",
"info.foreground": "#FFFFFF",
"info.hover.background": "#0EA5E9",
"input.border": "#D4D4D8",
"link": "#65A30D",
"link.active": "#3F6212",
"link.hover": "#4A7A0B",
"list.background": "#FFFFFF",
"list.active.background": "#C6FF4D40",
"list.active.border": "#65A30D80",
"list.even.background": "#FAFAFA",
"list.head.background": "#FAFAFA",
"list.hover.background": "#F4F4F5",
"muted.background": "#F4F4F5",
"muted.foreground": "#71717A",
"popover.background": "#FFFFFF",
"popover.foreground": "#18181B",
"primary.background": "#C6FF4D",
"primary.active.background": "#65A30D",
"primary.foreground": "#0B0F0C",
"primary.hover.background": "#B2EC3C",
"progress.bar.background": "#65A30D",
"ring": "#65A30D",
"scrollbar.background": "#FFFFFF00",
"scrollbar.thumb.background": "#A1A1AA99",
"scrollbar.thumb.hover.background": "#A1A1AA",
"secondary.background": "#F4F4F5",
"secondary.active.background": "#D4D4D8",
"secondary.foreground": "#18181B",
"secondary.hover.background": "#E4E4E7",
"selection.background": "#C6FF4D59",
"sidebar.background": "#FAFAFA",
"sidebar.accent.background": "#C6FF4D40",
"sidebar.accent.foreground": "#65A30D",
"sidebar.border": "#E4E4E7",
"sidebar.foreground": "#18181B",
"sidebar.primary.background": "#F4F4F5",
"sidebar.primary.foreground": "#18181B",
"skeleton.background": "#E4E4E7",
"slider.background": "#E4E4E7",
"slider.thumb.background": "#65A30D",
"success.background": "#2FBF71",
"success.active.background": "#1F9A58",
"success.foreground": "#0B0F0C",
"success.hover.background": "#2AB568",
"switch.background": "#D4D4D8",
"switch.thumb.background": "#FFFFFF",
"tab.background": "#F4F4F5",
"tab.active.background": "#EBFFC1",
"tab.active.foreground": "#3F6212",
"tab.foreground": "#71717A",
"tab_bar.background": "#F4F4F5",
"tab_bar.segmented.background": "#E4E4E7",
"table.background": "#FFFFFF",
"table.active.background": "#C6FF4D40",
"table.active.border": "#65A30D80",
"table.even.background": "#FAFAFA",
"table.head.background": "#FAFAFA",
"table.head.foreground": "#71717A",
"table.foot.background": "#FAFAFA",
"table.foot.foreground": "#71717A",
"table.hover.background": "#F4F4F5",
"table.row.border": "#E4E4E7",
"title_bar.background": "#FAFAFA",
"title_bar.border": "#E4E4E7",
"status_bar.background": "#FAFAFA",
"status_bar.border": "#E4E4E7",
"tiles.background": "#FFFFFF",
"warning.background": "#D97706",
"warning.active.background": "#B45309",
"warning.foreground": "#FFFFFF",
"warning.hover.background": "#F59E0B",
"overlay": "#0000004D",
"window.border": "#E4E4E7",
"base.red": "#DC2626",
"base.green": "#16A34A",
"base.yellow": "#CA8A04",
"base.blue": "#2563EB",
"base.magenta": "#9333EA",
"base.cyan": "#0891B2"
},
"highlight": {
"editor.background": "#FFFFFF",
"editor.foreground": "#383A42",
"editor.active_line.background": "#F2F3F4",
"editor.line_number": "#A0A1A7",
"editor.active_line_number": "#383A42",
"editor.invisible": "#A0A1A766",
"conflict": "#DC2626",
"created": "#16A34A",
"deleted": "#DC2626",
"error": "#DC2626",
"error.background": "#FEF2F2",
"error.border": "#F87171",
"hidden": "#A0A1A7",
"hint": "#A626A4",
"hint.background": "#FAF5FF",
"hint.border": "#C4B5FD",
"ignored": "#A0A1A7",
"info": "#4078F2",
"info.background": "#EFF6FF",
"info.border": "#93C5FD",
"modified": "#C18401",
"modified.background": "#FFFBEB",
"predictive": "#A0A1A7",
"renamed": "#A626A4",
"success": "#16A34A",
"success.background": "#F0FDF4",
"unreachable": "#A0A1A7",
"warning": "#C18401",
"warning.background": "#FFFBEB",
"warning.border": "#FCD34D",
"syntax": {
"attribute": {
"color": "#986801"
},
"boolean": {
"color": "#986801"
},
"comment": {
"color": "#A0A1A7",
"font_style": "italic"
},
"comment.doc": {
"color": "#A0A1A7",
"font_style": "italic"
},
"constant": {
"color": "#986801"
},
"constructor": {
"color": "#4078F2"
},
"embedded": {
"color": "#C18401"
},
"function": {
"color": "#4078F2"
},
"keyword": {
"color": "#A626A4"
},
"label": {
"color": "#4078F2"
},
"link_text": {
"color": "#4078F2",
"font_style": "underline"
},
"link_uri": {
"color": "#4078F2",
"font_style": "italic"
},
"number": {
"color": "#986801"
},
"operator": {
"color": "#383A42"
},
"preproc": {
"color": "#C18401"
},
"property": {
"color": "#E45649"
},
"punctuation": {
"color": "#383A42"
},
"punctuation.bracket": {
"color": "#383A42"
},
"punctuation.delimiter": {
"color": "#383A42"
},
"punctuation.list_marker": {
"color": "#986801"
},
"punctuation.special": {
"color": "#A626A4"
},
"string": {
"color": "#50A14F"
},
"string.escape": {
"color": "#A626A4"
},
"string.regex": {
"color": "#50A14F"
},
"string.special": {
"color": "#50A14F"
},
"string.special.symbol": {
"color": "#986801"
},
"tag": {
"color": "#E45649"
},
"tag.doctype": {
"color": "#A0A1A7"
},
"text.code.span": {
"color": "#383A42"
},
"text.literal": {
"color": "#50A14F"
},
"title": {
"color": "#E45649",
"font_weight": 700
},
"type": {
"color": "#0184BC"
},
"variable": {
"color": "#383A42"
},
"variable.special": {
"color": "#E45649"
},
"variant": {
"color": "#0184BC"
}
}
}
},
{
"name": "Signed Dark",
"mode": "dark",
"colors": {
"accent.background": "#18181B",
"accent.foreground": "#FAFAFA",
"accordion.background": "#0A0A0A",
"background": "#0A0A0A",
"border": "#27272A",
"button.background": "#18181B",
"button.active.background": "#3F3F46",
"button.foreground": "#FAFAFA",
"button.hover.background": "#27272A",
"button.danger.background": "#EF4444",
"button.danger.active.background": "#DC2626",
"button.danger.foreground": "#FFFFFF",
"button.danger.hover.background": "#F87171",
"button.info.background": "#0EA5E9",
"button.info.active.background": "#0284C7",
"button.info.foreground": "#FFFFFF",
"button.info.hover.background": "#38BDF8",
"button.primary.background": "#C6FF4D",
"button.primary.active.background": "#65A30D",
"button.primary.foreground": "#0B0F0C",
"button.primary.hover.background": "#B2EC3C",
"button.secondary.background": "#18181B",
"button.secondary.active.background": "#3F3F46",
"button.secondary.foreground": "#FAFAFA",
"button.secondary.hover.background": "#27272A",
"button.success.background": "#2FBF71",
"button.success.active.background": "#24A35D",
"button.success.foreground": "#0B0F0C",
"button.success.hover.background": "#2AB568",
"button.warning.background": "#F59E0B",
"button.warning.active.background": "#D97706",
"button.warning.foreground": "#18181B",
"button.warning.hover.background": "#FBBF24",
"group_box.background": "#18181B",
"group_box.foreground": "#FAFAFA",
"group_box.title.foreground": "#FAFAFA",
"caret": "#FAFAFA",
"chart.1": "#C6FF4D",
"chart.2": "#A6E22E",
"chart.3": "#2FBF71",
"chart.4": "#7A8B5F",
"chart.5": "#A1A1AA",
"chart_bullish": "#2FBF71",
"chart_bearish": "#EF4444",
"danger.background": "#EF4444",
"danger.active.background": "#DC2626",
"danger.foreground": "#FFFFFF",
"danger.hover.background": "#F87171",
"description_list.label.background": "#18181B",
"description_list.label.foreground": "#A1A1AA",
"drag.border": "#C6FF4D",
"drop_target.background": "#C6FF4D33",
"foreground": "#FAFAFA",
"info.background": "#0EA5E9",
"info.active.background": "#0284C7",
"info.foreground": "#FFFFFF",
"info.hover.background": "#38BDF8",
"input.border": "#3F3F46",
"link": "#C6FF4D",
"link.active": "#A6E22E",
"link.hover": "#B2EC3C",
"list.background": "#0A0A0A",
"list.active.background": "#C6FF4D40",
"list.active.border": "#C6FF4D80",
"list.even.background": "#18181B",
"list.head.background": "#18181B",
"list.hover.background": "#27272A",
"muted.background": "#18181B",
"muted.foreground": "#A1A1AA",
"popover.background": "#18181B",
"popover.foreground": "#FAFAFA",
"primary.background": "#C6FF4D",
"primary.active.background": "#65A30D",
"primary.foreground": "#0B0F0C",
"primary.hover.background": "#B2EC3C",
"progress.bar.background": "#C6FF4D",
"ring": "#C6FF4D",
"scrollbar.background": "#0A0A0A00",
"scrollbar.thumb.background": "#3F3F46",
"scrollbar.thumb.hover.background": "#52525B",
"secondary.background": "#18181B",
"secondary.active.background": "#3F3F46",
"secondary.foreground": "#FAFAFA",
"secondary.hover.background": "#27272A",
"selection.background": "#C6FF4D59",
"sidebar.background": "#0A0A0A",
"sidebar.accent.background": "#C6FF4D40",
"sidebar.accent.foreground": "#C6FF4D",
"sidebar.border": "#27272A",
"sidebar.foreground": "#FAFAFA",
"sidebar.primary.background": "#18181B",
"sidebar.primary.foreground": "#FAFAFA",
"skeleton.background": "#27272A",
"slider.background": "#27272A",
"slider.thumb.background": "#C6FF4D",
"success.background": "#2FBF71",
"success.active.background": "#24A35D",
"success.foreground": "#0B0F0C",
"success.hover.background": "#2AB568",
"switch.background": "#3F3F46",
"switch.thumb.background": "#FAFAFA",
"tab.background": "#18181B",
"tab.active.background": "#19200A",
"tab.active.foreground": "#C6FF4D",
"tab.foreground": "#A1A1AA",
"tab_bar.background": "#18181B",
"tab_bar.segmented.background": "#27272A",
"table.background": "#0A0A0A",
"table.active.background": "#C6FF4D40",
"table.active.border": "#C6FF4D80",
"table.even.background": "#18181B",
"table.head.background": "#18181B",
"table.head.foreground": "#A1A1AA",
"table.foot.background": "#18181B",
"table.foot.foreground": "#A1A1AA",
"table.hover.background": "#27272A",
"table.row.border": "#27272A",
"title_bar.background": "#18181B",
"title_bar.border": "#27272A",
"status_bar.background": "#18181B",
"status_bar.border": "#27272A",
"tiles.background": "#0A0A0A",
"warning.background": "#F59E0B",
"warning.active.background": "#D97706",
"warning.foreground": "#18181B",
"warning.hover.background": "#FBBF24",
"overlay": "#00000080",
"window.border": "#27272A",
"base.red": "#EF4444",
"base.green": "#22C55E",
"base.yellow": "#EAB308",
"base.blue": "#3B82F6",
"base.magenta": "#A855F7",
"base.cyan": "#06B6D4"
},
"highlight": {
"editor.background": "#0A0A0A",
"editor.foreground": "#ABB2BF",
"editor.active_line.background": "#18181B",
"editor.line_number": "#71717A",
"editor.active_line_number": "#FAFAFA",
"editor.invisible": "#71717A66",
"conflict": "#E06C75",
"created": "#98C379",
"deleted": "#E06C75",
"error": "#E06C75",
"error.background": "#3A1D1D",
"error.border": "#E06C75",
"hidden": "#5C6370",
"hint": "#C678DD",
"hint.background": "#2E2740",
"hint.border": "#C678DD",
"ignored": "#5C6370",
"info": "#61AFEF",
"info.background": "#1D2E3A",
"info.border": "#61AFEF",
"modified": "#E5C07B",
"modified.background": "#3A320E",
"predictive": "#5C6370",
"renamed": "#C678DD",
"success": "#98C379",
"success.background": "#1D3A2E",
"unreachable": "#5C6370",
"warning": "#E5C07B",
"warning.background": "#3A320E",
"warning.border": "#E5C07B",
"syntax": {
"attribute": {
"color": "#D19A66"
},
"boolean": {
"color": "#D19A66"
},
"comment": {
"color": "#5C6370",
"font_style": "italic"
},
"comment.doc": {
"color": "#5C6370",
"font_style": "italic"
},
"constant": {
"color": "#D19A66"
},
"constructor": {
"color": "#E5C07B"
},
"embedded": {
"color": "#98C379"
},
"function": {
"color": "#61AFEF"
},
"keyword": {
"color": "#C678DD"
},
"label": {
"color": "#61AFEF"
},
"link_text": {
"color": "#61AFEF",
"font_style": "underline"
},
"link_uri": {
"color": "#61AFEF",
"font_style": "italic"
},
"number": {
"color": "#D19A66"
},
"operator": {
"color": "#56B6C2"
},
"preproc": {
"color": "#E5C07B"
},
"property": {
"color": "#E06C75"
},
"punctuation": {
"color": "#ABB2BF"
},
"punctuation.bracket": {
"color": "#ABB2BF"
},
"punctuation.delimiter": {
"color": "#ABB2BF"
},
"punctuation.list_marker": {
"color": "#98C379"
},
"punctuation.special": {
"color": "#ABB2BF"
},
"string": {
"color": "#98C379"
},
"string.escape": {
"color": "#56B6C2"
},
"string.regex": {
"color": "#E06C75"
},
"string.special": {
"color": "#98C379"
},
"string.special.symbol": {
"color": "#D19A66"
},
"tag": {
"color": "#E06C75"
},
"tag.doctype": {
"color": "#5C6370"
},
"text.code.span": {
"color": "#ABB2BF"
},
"text.literal": {
"color": "#98C379"
},
"title": {
"color": "#61AFEF",
"font_weight": 700
},
"type": {
"color": "#E5C07B"
},
"variable": {
"color": "#E06C75"
},
"variable.special": {
"color": "#D19A66"
},
"variant": {
"color": "#E5C07B"
}
}
}
}
]
}
+81
View File
@@ -6,6 +6,7 @@ use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "assets"]
#[include = "icons/**/*.svg"]
#[include = "themes/**/*.json"]
#[exclude = "*.DS_Store"]
pub struct Assets;
@@ -30,6 +31,28 @@ impl AssetSource for Assets {
}
impl Assets {
/// Returns the embedded theme files as `(file name, JSON content)` pairs,
/// e.g. `("signed.json", ...)`. The content is a `ThemeSet` that can be
/// loaded into the [`ThemeRegistry`](gpui_component::ThemeRegistry).
pub fn themes(&self) -> Vec<(String, String)> {
Self::iter()
.filter(|path| path.starts_with("themes/"))
.filter_map(|path| {
let data = Self::get(path.as_ref())?;
let name = path.strip_prefix("themes/").unwrap_or(path.as_ref());
// Debug builds read files from disk (owned), release builds
// embed them in the binary (borrowed).
let content = match data.data {
std::borrow::Cow::Borrowed(bytes) => {
std::str::from_utf8(bytes).ok()?.to_owned()
}
std::borrow::Cow::Owned(bytes) => String::from_utf8(bytes).ok()?,
};
Some((name.to_owned(), content))
})
.collect()
}
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
@@ -85,3 +108,61 @@ impl IconNamed for CustomIconName {
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn signed_theme_set() -> gpui_component::ThemeSet {
let themes = Assets.themes();
assert_eq!(themes.len(), 1, "expected exactly one embedded theme file");
let (name, content) = &themes[0];
assert_eq!(name, "signed.json");
serde_json::from_str(content).expect("theme file must be a valid ThemeSet")
}
#[test]
fn signed_theme_set_parses() {
let set = signed_theme_set();
let names: Vec<&str> = set.themes.iter().map(|t| t.name.as_ref()).collect();
assert_eq!(names, vec!["Signed Light", "Signed Dark"]);
}
#[test]
fn signed_theme_palette_applies() {
let set = signed_theme_set();
let parse = |hex: &str| gpui_component::try_parse_color(hex).unwrap();
for config in &set.themes {
let mut theme = gpui_component::Theme::default();
theme.apply_config(&std::rc::Rc::new(config.clone()));
assert_eq!(theme.mode, config.mode);
// The resolved colors must match the brand palette.
assert_eq!(theme.primary, parse("#C6FF4D")); // nostr-lime
assert_eq!(theme.success, parse("#2FBF71")); // merge
assert_eq!(theme.primary_active, parse("#65A30D")); // lime-600
if config.mode.is_dark() {
// Dark theme chrome is neutral, mirroring the light theme.
assert_eq!(theme.background, parse("#0A0A0A")); // neutral-950
assert_eq!(theme.border, parse("#27272A")); // neutral-800
assert_eq!(theme.green, parse("#22C55E")); // green-500
} else {
// Light theme chrome is neutral; lime is a brand accent only.
assert_eq!(theme.background, parse("#FFFFFF"));
assert_eq!(theme.foreground, parse("#18181B"));
assert_eq!(theme.border, parse("#E4E4E7"));
assert_eq!(theme.green, parse("#16A34A"));
}
// Active tab: a paler lime on light, a dim moss on dark — each
// paired with readable, contrasting text.
if config.mode.is_dark() {
assert_eq!(theme.tab_active, parse("#19200A")); // dim lime
assert_eq!(theme.tab_active_foreground, parse("#C6FF4D")); // nostr-lime
} else {
assert_eq!(theme.tab_active, parse("#EBFFC1")); // pale nostr-lime
assert_eq!(theme.tab_active_foreground, parse("#3F6212")); // deep-lime
}
}
}
}
@@ -303,7 +303,7 @@ impl CommitDiffView {
};
let body: AnyElement = if file.binary {
placeholder("Binary file — diff not available", cx)
placeholder("Diff not available", cx)
} else if file.hunks.is_empty() {
placeholder("No content changes", cx)
} else {
@@ -525,7 +525,6 @@ impl CommitDiffView {
.text_xs()
.child(
Tag::primary()
.outline()
.small()
.child(format!("{files} files changed")),
)
@@ -1212,7 +1212,6 @@ impl Render for RepoDetailView {
v_flex()
.id("repo")
.size_full()
.gap_4()
.child(self.render_header(cx))
.map(|this| match self.active_tab {
0 => this.child(