theme: add info severity color token
This commit is contained in:
@@ -221,7 +221,7 @@ impl RenderOnce for ToastsView {
|
|||||||
let toasts = self.toasts.clone();
|
let toasts = self.toasts.clone();
|
||||||
|
|
||||||
let accent = match toast.severity {
|
let accent = match toast.severity {
|
||||||
ToastSeverity::Info => theme.ramps.primary.base,
|
ToastSeverity::Info => theme.ramps.info.base,
|
||||||
ToastSeverity::Success => theme.ramps.success.base,
|
ToastSeverity::Success => theme.ramps.success.base,
|
||||||
ToastSeverity::Warning => theme.ramps.warning.base,
|
ToastSeverity::Warning => theme.ramps.warning.base,
|
||||||
ToastSeverity::Danger => theme.ramps.destructive.base,
|
ToastSeverity::Danger => theme.ramps.destructive.base,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub struct ColorTokens {
|
|||||||
pub success_foreground: Hsla,
|
pub success_foreground: Hsla,
|
||||||
pub warning: Hsla,
|
pub warning: Hsla,
|
||||||
pub warning_foreground: Hsla,
|
pub warning_foreground: Hsla,
|
||||||
|
pub info: Hsla,
|
||||||
|
pub info_foreground: Hsla,
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
pub input: Hsla,
|
pub input: Hsla,
|
||||||
pub ring: Hsla,
|
pub ring: Hsla,
|
||||||
@@ -65,6 +67,8 @@ impl ColorTokens {
|
|||||||
success_foreground: hex(0xffffff),
|
success_foreground: hex(0xffffff),
|
||||||
warning: hex(0xd97706),
|
warning: hex(0xd97706),
|
||||||
warning_foreground: hex(0xffffff),
|
warning_foreground: hex(0xffffff),
|
||||||
|
info: hex(0x0284c7),
|
||||||
|
info_foreground: hex(0xffffff),
|
||||||
border: hex(0xe4e4e7),
|
border: hex(0xe4e4e7),
|
||||||
input: hex(0xe4e4e7),
|
input: hex(0xe4e4e7),
|
||||||
ring: hex(0x93c5fd),
|
ring: hex(0x93c5fd),
|
||||||
@@ -96,6 +100,8 @@ impl ColorTokens {
|
|||||||
success_foreground: hex(0x052e16),
|
success_foreground: hex(0x052e16),
|
||||||
warning: hex(0xf59e0b),
|
warning: hex(0xf59e0b),
|
||||||
warning_foreground: hex(0x451a03),
|
warning_foreground: hex(0x451a03),
|
||||||
|
info: hex(0x38bdf8),
|
||||||
|
info_foreground: hex(0x082f49),
|
||||||
border: hex(0x27272a),
|
border: hex(0x27272a),
|
||||||
input: hex(0x27272a),
|
input: hex(0x27272a),
|
||||||
ring: hex(0x1d4ed8),
|
ring: hex(0x1d4ed8),
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub struct ThemeDefinition {
|
|||||||
pub success_foreground: Option<String>,
|
pub success_foreground: Option<String>,
|
||||||
pub warning: Option<String>,
|
pub warning: Option<String>,
|
||||||
pub warning_foreground: Option<String>,
|
pub warning_foreground: Option<String>,
|
||||||
|
pub info: Option<String>,
|
||||||
|
pub info_foreground: Option<String>,
|
||||||
pub border: Option<String>,
|
pub border: Option<String>,
|
||||||
pub input: Option<String>,
|
pub input: Option<String>,
|
||||||
pub ring: Option<String>,
|
pub ring: Option<String>,
|
||||||
@@ -114,6 +116,14 @@ impl ThemeDefinition {
|
|||||||
parse_hex(self.warning_foreground.as_deref()?)
|
parse_hex(self.warning_foreground.as_deref()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.info.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info_foreground(&self) -> Option<Hsla> {
|
||||||
|
parse_hex(self.info_foreground.as_deref()?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn border(&self) -> Option<Hsla> {
|
pub fn border(&self) -> Option<Hsla> {
|
||||||
parse_hex(self.border.as_deref()?)
|
parse_hex(self.border.as_deref()?)
|
||||||
}
|
}
|
||||||
@@ -206,11 +216,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_a_full_definition() {
|
fn parses_a_full_definition() {
|
||||||
let def = ThemeDefinition::parse_toml(
|
let def = ThemeDefinition::parse_toml(
|
||||||
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\n",
|
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\ninfo = \"#38bdf8\"\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
||||||
assert!(def.accent().is_some());
|
assert!(def.accent().is_some());
|
||||||
|
assert!(def.info().is_some());
|
||||||
assert!(def.foreground().is_none());
|
assert!(def.foreground().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub struct Ramps {
|
|||||||
pub destructive: AccentShades,
|
pub destructive: AccentShades,
|
||||||
pub success: AccentShades,
|
pub success: AccentShades,
|
||||||
pub warning: AccentShades,
|
pub warning: AccentShades,
|
||||||
|
pub info: AccentShades,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything a component paints from, after the overlay chain.
|
/// Everything a component paints from, after the overlay chain.
|
||||||
@@ -70,6 +71,7 @@ pub(crate) fn resolve(
|
|||||||
system_palette.warning,
|
system_palette.warning,
|
||||||
base.warning,
|
base.warning,
|
||||||
);
|
);
|
||||||
|
let info = pick(def_color(|d| d.info()), system_palette.info, base.info);
|
||||||
|
|
||||||
let mut colors = ColorTokens {
|
let mut colors = ColorTokens {
|
||||||
background: pick(
|
background: pick(
|
||||||
@@ -126,18 +128,24 @@ pub(crate) fn resolve(
|
|||||||
None,
|
None,
|
||||||
contrast_foreground(destructive),
|
contrast_foreground(destructive),
|
||||||
),
|
),
|
||||||
success: pick(def_color(|d| d.success()), None, base.success),
|
success,
|
||||||
success_foreground: pick(
|
success_foreground: pick(
|
||||||
def_color(|d| d.success_foreground()),
|
def_color(|d| d.success_foreground()),
|
||||||
None,
|
None,
|
||||||
contrast_foreground(success),
|
contrast_foreground(success),
|
||||||
),
|
),
|
||||||
warning: pick(def_color(|d| d.warning()), None, base.warning),
|
warning,
|
||||||
warning_foreground: pick(
|
warning_foreground: pick(
|
||||||
def_color(|d| d.warning_foreground()),
|
def_color(|d| d.warning_foreground()),
|
||||||
None,
|
None,
|
||||||
contrast_foreground(warning),
|
contrast_foreground(warning),
|
||||||
),
|
),
|
||||||
|
info,
|
||||||
|
info_foreground: pick(
|
||||||
|
def_color(|d| d.info_foreground()),
|
||||||
|
None,
|
||||||
|
contrast_foreground(info),
|
||||||
|
),
|
||||||
border: pick(
|
border: pick(
|
||||||
def_color(|d| d.border()),
|
def_color(|d| d.border()),
|
||||||
system_palette.border,
|
system_palette.border,
|
||||||
@@ -208,6 +216,7 @@ pub(crate) fn resolve(
|
|||||||
),
|
),
|
||||||
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
success: derive_shades(success, colors.background, variant == ThemeVariant::Dark),
|
||||||
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
warning: derive_shades(warning, colors.background, variant == ThemeVariant::Dark),
|
||||||
|
info: derive_shades(info, colors.background, variant == ThemeVariant::Dark),
|
||||||
},
|
},
|
||||||
colors,
|
colors,
|
||||||
radius,
|
radius,
|
||||||
@@ -523,4 +532,38 @@ mod tests {
|
|||||||
assert!(resolved.colors.ring != resolved.colors.background);
|
assert!(resolved.colors.ring != resolved.colors.background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn info_stays_calm_when_the_accent_warms_up() {
|
||||||
|
// The whole point of the info role: a warm platform accent (GNOME
|
||||||
|
// red, Omarchy red themes) must not make Info toasts read as Danger.
|
||||||
|
let red_accent = hsla(0xdc2626);
|
||||||
|
let resolved = resolve(
|
||||||
|
&system(
|
||||||
|
ThemeVariant::Dark,
|
||||||
|
PartialPalette::default(),
|
||||||
|
Some(red_accent),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(resolved.ramps.primary.base, red_accent);
|
||||||
|
assert_ne!(resolved.colors.info, resolved.colors.destructive);
|
||||||
|
assert_ne!(resolved.ramps.info.base, resolved.ramps.destructive.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn info_follows_the_severity_overlay_chain() {
|
||||||
|
let palette = PartialPalette {
|
||||||
|
info: Some(hsla(0x155e75)),
|
||||||
|
..PartialPalette::default()
|
||||||
|
};
|
||||||
|
let system_stated = resolve(&system(ThemeVariant::Dark, palette, None), None, None);
|
||||||
|
assert_eq!(system_stated.colors.info, hsla(0x155e75));
|
||||||
|
assert_eq!(system_stated.ramps.info.base, hsla(0x155e75));
|
||||||
|
|
||||||
|
let def = ThemeDefinition::parse_toml("info = \"#0ea5e9\"\n").unwrap();
|
||||||
|
let preset_stated = resolve(&system(ThemeVariant::Dark, palette, None), Some(&def), None);
|
||||||
|
assert_eq!(preset_stated.colors.info, hsla(0x0ea5e9));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ impl MacosSource {
|
|||||||
destructive: Self::color_to_hsla(&NSColor::systemRedColor()),
|
destructive: Self::color_to_hsla(&NSColor::systemRedColor()),
|
||||||
success: None,
|
success: None,
|
||||||
warning: None,
|
warning: None,
|
||||||
|
info: None,
|
||||||
border: Self::color_to_hsla(&NSColor::separatorColor()),
|
border: Self::color_to_hsla(&NSColor::separatorColor()),
|
||||||
input: Self::color_to_hsla(&NSColor::controlBackgroundColor()),
|
input: Self::color_to_hsla(&NSColor::controlBackgroundColor()),
|
||||||
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub struct PartialPalette {
|
|||||||
/// No platform states severity colors; presets override them.
|
/// No platform states severity colors; presets override them.
|
||||||
pub success: Option<Hsla>,
|
pub success: Option<Hsla>,
|
||||||
pub warning: Option<Hsla>,
|
pub warning: Option<Hsla>,
|
||||||
|
pub info: Option<Hsla>,
|
||||||
pub border: Option<Hsla>,
|
pub border: Option<Hsla>,
|
||||||
pub input: Option<Hsla>,
|
pub input: Option<Hsla>,
|
||||||
pub ring: Option<Hsla>,
|
pub ring: Option<Hsla>,
|
||||||
|
|||||||
+2
-1
@@ -156,7 +156,7 @@ The set stays role-minimal and grows one concept at a time, driven by the unit t
|
|||||||
|
|
||||||
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
||||||
- ~~**Scrollbar thumb token + metrics**~~ — ✅ landed with the ScrollArea unit (2026-10-29): `colors.scrollbar_thumb` (translucent over content: black in light, light gray in dark; TOML-overridable) and `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
- ~~**Scrollbar thumb token + metrics**~~ — ✅ landed with the ScrollArea unit (2026-10-29): `colors.scrollbar_thumb` (translucent over content: black in light, light gray in dark; TOML-overridable) and `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
||||||
- ~~**`success` / `warning` roles**~~ — ✅ landed with Toast (2026-10-29): `colors.success`/`warning` (+ foregrounds; light and dark variants; TOML-overridable; no platform states them) and derived `Ramps.success`/`warning` via the OKLCH engine, matching the destructive treatment.
|
- ~~**`success` / `warning` roles**~~ — ✅ landed with Toast (2026-10-29): `colors.success`/`warning` (+ foregrounds; light and dark variants; TOML-overridable; no platform states them) and derived `Ramps.success`/`warning` via the OKLCH engine, matching the destructive treatment. **`info` joined them (2026-10-31)** — the fourth severity, same treatment (`colors.info` + foreground + `Ramps.info`, neutral sky blue, no platform states it, TOML `info =`). Trigger: `ToastSeverity::Info` was painting from the primary ramp, so a warm platform accent (GNOME red, Omarchy red themes) made Info read as Danger; the token keeps Info calm however the accent turns out. Severity tokens now use the same picked value for the ramp and the token (the success/warning tokens used to re-pick with the system slot dropped, diverging from their own ramps if a platform ever stated one).
|
||||||
- **Motion tokens** — after the reverted framework attempt (2026-10-30), motion stays minimal: `Motion.overlay` (200 ms) is the only token. Components reach for gpui's own easings and `SpringConfig` inline (D21); a token is added only when a third component shares the same value (D16). TOML `[motion]` overrides and per-platform motion presets wait for a real need. ~~The reduce-motion forwarding fix~~ ✅ landed (pass a, 2026-10-30): `reapply` calls `cx.set_reduce_motion(resolved.accessibility.reduce_motion)`, so gpui's animation elements snap under the OS setting.
|
- **Motion tokens** — after the reverted framework attempt (2026-10-30), motion stays minimal: `Motion.overlay` (200 ms) is the only token. Components reach for gpui's own easings and `SpringConfig` inline (D21); a token is added only when a third component shares the same value (D16). TOML `[motion]` overrides and per-platform motion presets wait for a real need. ~~The reduce-motion forwarding fix~~ ✅ landed (pass a, 2026-10-30): `reapply` calls `cx.set_reduce_motion(resolved.accessibility.reduce_motion)`, so gpui's animation elements snap under the OS setting.
|
||||||
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
||||||
|
|
||||||
@@ -314,6 +314,7 @@ Pure tests for resolve/overlay chain, payload parsing (fixtures), icon resolutio
|
|||||||
| D19 | System icons via semantic `IconRole`s with per-platform resolvers (SF Symbols / Segoe Fluent Icons / freedesktop) and a bundled Lucide fallback; platform glyph sets are used at runtime only, never bundled |
|
| D19 | System icons via semantic `IconRole`s with per-platform resolvers (SF Symbols / Segoe Fluent Icons / freedesktop) and a bundled Lucide fallback; platform glyph sets are used at runtime only, never bundled |
|
||||||
| D20 | Components land one element at a time behind a review gate: API sketch → implementation → tests → gallery page → review → next. bezel and gpuikit are the API-design references for these units; conventions like controlled state, one activation path, semantic states, and a11y labels are adopted, simplified. gpui-kit is no longer consulted — judged over-engineered for our needs |
|
| D20 | Components land one element at a time behind a review gate: API sketch → implementation → tests → gallery page → review → next. bezel and gpuikit are the API-design references for these units; conventions like controlled state, one activation path, semantic states, and a11y labels are adopted, simplified. gpui-kit is no longer consulted — judged over-engineered for our needs |
|
||||||
| D21 | Motion is per component and inline: each component uses gpui's own animation/spring primitives (`with_animation`, `with_spring`, the gpui easings, `with_max_fps`) directly in its file; the theme owns only the durations components actually share (today `Motion.overlay`), growing per the role-minimal rule. No shared motion module, no exit ghosts, no hover-wash machinery — exits and washes stay out until a genuinely simple approach exists. Reduce motion is gpui-side: forward the OS flag once in `reapply` and every primitive snaps. Supersedes the reverted 2026-10-30 framework attempt (which proved the substrate but over-built the plumbing) |
|
| D21 | Motion is per component and inline: each component uses gpui's own animation/spring primitives (`with_animation`, `with_spring`, the gpui easings, `with_max_fps`) directly in its file; the theme owns only the durations components actually share (today `Motion.overlay`), growing per the role-minimal rule. No shared motion module, no exit ghosts, no hover-wash machinery — exits and washes stay out until a genuinely simple approach exists. Reduce motion is gpui-side: forward the OS flag once in `reapply` and every primitive snaps. Supersedes the reverted 2026-10-30 framework attempt (which proved the substrate but over-built the plumbing) |
|
||||||
|
| D22 | The color set does not expand along gpuikit's `Themeable` shape (evaluated 2026-10-31): their per-component token layers (`button_*`, `input_*`) are passthroughs of their own primitives — a drift-prone parallel layer our role-plus-ramp model replaces with OKLCH-derived hover/pressed shades; depth variants (`surface_secondary`), the badge palette, and `fg_disabled` wait for real consumers (Badge, nested elevation, a theme that can't express disabled by opacity). The one adoption: `info` as the fourth severity — `ToastSeverity::Info` had a live consumer painting from the primary ramp, which collides with Danger under warm accents. Domain palettes (editor diff colors) may revisit gpuikit's `ThemeExtension` idea when `crates/editor` lands |
|
||||||
|
|
||||||
Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in a lighter form as D20. D10 (no icon bundling) is superseded by D19.
|
Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in a lighter form as D20. D10 (no icon bundling) is superseded by D19.
|
||||||
|
|
||||||
|
|||||||
@@ -555,6 +555,21 @@ impl Render for Gallery {
|
|||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
div().flex().gap_2().children([
|
div().flex().gap_2().children([
|
||||||
|
{
|
||||||
|
let toasts = self.toasts.clone();
|
||||||
|
Button::new("gallery-toast-info")
|
||||||
|
.label("Info")
|
||||||
|
.variant(ButtonVariant::Secondary)
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
toasts.update(cx, |toasts, cx| {
|
||||||
|
toasts.push(
|
||||||
|
Toast::new("New version available")
|
||||||
|
.message("Reload to update to 2.4.0."),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
{
|
{
|
||||||
let toasts = self.toasts.clone();
|
let toasts = self.toasts.clone();
|
||||||
Button::new("gallery-toast-success")
|
Button::new("gallery-toast-success")
|
||||||
|
|||||||
Reference in New Issue
Block a user