Tooling · Catalogs: the audit reads active and code; avatar and container sizes

The audit now holds fg_subtle on active, a selected row's fill, and
each syntax color over the editor's surface, a code block and the
current line. What it found is fixed at the source: fg_subtle is
#696764 light and #94928f dark, syntax.comment follows it, and the
light number, constant and attribute darken to #a3591b, #aa5529 and
#9f5d12. The token viewer adds avatar and container sizes, draws a
container at its share of the widest so it stays in its row, and
names what it shows: the shared colors and scales. Tests pin every
variant, the hue each chart row reads and the code rows.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 08:57:52 -04:00
1 parent d5271d03ea
commit a7e492f28e
7 files changed
+153 -27

No files matched your search

+45 -6
View File
@@ -6,7 +6,7 @@ use gpui::{
use crate::{
data_display::{Badge, Tone},
devtools::{contrast, over},
theme::{ActiveTheme, HUE_NAMES, Palette, Radius, TextSize},
theme::{ActiveTheme, HUE_NAMES, Palette, Radius, Syntax, TextSize},
typography::{Ellipsis, literal, tabular},
};
@@ -27,7 +27,7 @@ const TEXT_PAIRS: &[(&str, &[&str])] = &[
),
(
"fg_subtle",
&["bg", "surface", "sunken", "overlay", "hover"],
&["bg", "surface", "sunken", "overlay", "hover", "active"],
),
("link", &["bg", "surface", "sunken"]),
("on_accent", &["accent", "accent_hover"]),
@@ -38,6 +38,9 @@ const TEXT_PAIRS: &[(&str, &[&str])] = &[
("info", &["bg", "info_subtle"]),
];
/// The surfaces code sits on: the editor, a code block, the editor's current line.
const CODE: [&str; 3] = ["surface", "sunken", "hover"];
/// A color, what it stands on, the contrast between them, and the contrast it needs.
#[derive(Clone, Debug)]
struct Check {
@@ -63,7 +66,7 @@ impl Check {
}
}
/// Every pair in `palette`: its text over its surfaces at 4.5:1, the focus ring and the chart hues beside the page at 3:1.
/// Every pair in `palette`: its text over its surfaces and its code colors over the code surfaces at 4.5:1, the focus ring and the chart hues beside the page at 3:1.
fn audit(palette: &Palette) -> Vec<Check> {
let check = |front: String, fg: Hsla, back: &'static str, need: f32| {
let bg = palette.token(back);
@@ -81,6 +84,16 @@ fn audit(palette: &Palette) -> Vec<Check> {
.iter()
.map(move |back| check(front.to_string(), palette.token(front), back, TEXT))
});
let code = Syntax::NAMES.iter().flat_map(|name| {
CODE.iter().map(move |back| {
check(
format!("syntax.{name}"),
palette.syntax.token(name),
back,
TEXT,
)
})
});
let focus = ["bg", "surface"]
.into_iter()
.map(|back| check("focus".into(), palette.focus, back, MARK));
@@ -92,10 +105,10 @@ fn audit(palette: &Palette) -> Vec<Check> {
MARK,
)
});
text.chain(focus).chain(hues).collect()
text.chain(code).chain(focus).chain(hues).collect()
}
/// Audits the theme's contrast in the mode shown: each text color over the surfaces it sits on at 4.5:1, the focus ring and the chart hues beside the page at 3:1, failures first under a count. It reads the theme as it stands, an owner's palette and high contrast included.
/// Audits the theme's contrast in the mode shown: each text color over the surfaces it sits on and each code color over the editor's surfaces at 4.5:1, the focus ring and the chart hues beside the page at 3:1, failures first under a count. It reads the theme as it stands, an owner's palette and high contrast included.
#[derive(IntoElement, Default)]
pub struct A11yChecker;
@@ -206,7 +219,12 @@ mod tests {
let checks = audit(&palette);
let subtle: Vec<&Check> = checks.iter().filter(|c| c.front == "fg_subtle").collect();
assert!(subtle.iter().all(|check| check.grade() == "Fails"));
assert_eq!(checks.len(), 41, "31 text pairs, 2 for the ring, 8 hues");
assert_eq!(
checks.len(),
81,
"32 text pairs, 39 for code, 2 for the ring, 8 hues"
);
assert_eq!(subtle.len(), 6);
}
#[test]
@@ -225,6 +243,27 @@ mod tests {
assert_eq!(grade("fg_muted"), Some("Fails"), "text at 3.3:1");
}
#[test]
fn code_and_hues_read_their_own_colors() {
let mut palette = Palette::light(false);
palette.syntax.comment = palette.surface;
let checks = audit(&palette);
let failing: Vec<&str> = checks
.iter()
.filter(|check| !check.passes())
.map(|check| check.back)
.collect();
assert_eq!(
failing,
["surface", "sunken", "hover"],
"the comment over code"
);
for (ix, name) in crate::theme::HUE_NAMES.iter().enumerate() {
let hue = checks.iter().find(|c| c.front == format!("chart {name}"));
assert_eq!(hue.map(|check| check.fg), Some(palette.chart[ix]));
}
}
#[test]
fn translucent_text_counts_as_it_shows() {
let mut palette = Palette::light(false);
+52 -6
View File
@@ -37,8 +37,47 @@ fn the_viewer_names_each_token_with_its_value(cx: &mut TestAppContext) {
"token-colors.hue(0) Blue-{}",
hex(Rgba::from(light.chart[0]))
);
let measures: [(&str, &[&str], &[u32]); 7] = [
(
"TextSize::",
&["Xs", "Sm", "Base", "Md", "Lg", "Xl", "Xxl", "Display"],
&[11, 12, 13, 14, 16, 20, 24, 32],
),
("Radius::", &["Sm", "Md", "Lg", "Xl"], &[4, 6, 8, 12]),
("ControlSize::", &["Sm", "Md", "Lg"], &[24, 28, 32]),
(
"IconSize::",
&["Xs", "Sm", "Md", "Lg", "Xl", "Xxl"],
&[12, 14, 16, 20, 24, 32],
),
(
"AvatarSize::",
&["Xs", "Sm", "Md", "Lg", "Xl"],
&[20, 24, 32, 40, 56],
),
("ContainerSize::", &["Sm", "Md", "Lg"], &[640, 960, 1200]),
(
"gap_",
&["1", "2", "3", "4", "6", "8"],
&[4, 8, 12, 16, 24, 32],
),
];
for (kind, names, sizes) in measures {
for (name, size) in names.iter().zip(sizes) {
let selector = format!("token-{kind}{name}-{size}");
assert!(shown(&selector, cx), "{selector}");
}
}
for level in ["Raised", "Floating", "Modal"] {
assert!(shown(&format!("token-Elevation::{level}"), cx), "{level}");
}
for (ix, name) in [(0, "black"), (15, "bright white")] {
let code = hex(Rgba::from(light.ansi[ix]));
let selector = format!("token-colors.ansi[{ix}] {name}-{code}");
assert!(shown(&selector, cx), "{selector}");
}
for selector in [
"token-colors.fg_subtle-#6f6d6a",
"token-colors.fg_subtle-#696764",
&keyword,
&blue,
"token-TextSize::Sm-12",
@@ -58,7 +97,7 @@ fn the_viewer_names_each_token_with_its_value(cx: &mut TestAppContext) {
});
cx.run_until_parked();
assert!(
shown("token-colors.fg_subtle-#8c8a87", cx),
shown("token-colors.fg_subtle-#94928f", cx),
"the dark palette"
);
let compact = cx.update(|window, cx| {
@@ -85,16 +124,23 @@ fn the_checker_counts_failures_first(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Audit);
cx.run_until_parked();
assert!(shown("contrast-summary-41-0", cx), "every pair passes");
assert!(shown("contrast-summary-81-0", cx), "every pair passes");
assert!(shown("contrast-fg-bg-AAA", cx));
assert!(shown("contrast-fg_subtle-hover-AA", cx), "4.53:1 on hover");
assert!(
shown("contrast-fg_subtle-active-AA", cx),
"a selected row's detail"
);
assert!(
shown("contrast-syntax.comment-hover-AA", cx),
"a comment on the current line"
);
assert!(shown("contrast-focus-surface-AA", cx));
assert!(shown("contrast-chart Blue-bg-AA", cx));
cx.update(|_, cx| Theme::update(cx, |theme| theme.colors.fg_subtle = theme.colors.bg));
cx.run_until_parked();
assert!(
shown("contrast-summary-41-5", cx),
"five surfaces under fg_subtle"
shown("contrast-summary-81-6", cx),
"six surfaces under fg_subtle"
);
let failing = cx
.debug_bounds("contrast-fg_subtle-hover-Fails")
+44 -3
View File
@@ -1,13 +1,14 @@
use gpui::{
AnyElement, App, FontWeight, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
Rems, RenderOnce, Rgba, Styled, Window, div, rems,
Rems, RenderOnce, Rgba, Styled, Window, div, relative, rems,
};
use crate::{
forms::hex,
primitives::{Icon, IconName},
theme::{
ActiveTheme, ControlSize, Elevation, HUE_NAMES, IconSize, Palette, Radius, Syntax, TextSize,
ActiveTheme, AvatarSize, ContainerSize, ControlSize, Elevation, HUE_NAMES, IconSize,
Palette, Radius, Syntax, TextSize,
},
typography::{Ellipsis, literal, tabular},
};
@@ -114,7 +115,7 @@ fn measure(name: String, size: Pixels, sample: impl IntoElement, cx: &App) -> An
.into_any_element()
}
/// Every token of the theme in the mode shown, each named as code reads it: the colors with their hex, the syntax, chart and terminal colors, the type scale, radii, control and icon sizes, elevations and gpui's spacing steps.
/// The theme's shared colors and scales in the mode shown, each named as code reads it: every palette, syntax, chart and terminal color with its hex, and the type, radius, control, icon, avatar and container sizes, the elevations and gpui's spacing steps. A chapter's own measures stay with the chapter.
#[derive(IntoElement, Default)]
pub struct DesignTokenViewer;
@@ -213,6 +214,44 @@ impl RenderOnce for DesignTokenViewer {
.bg(colors.surface);
measure(format!("ControlSize::{name}"), at(height), sample, cx)
});
let avatars = [
(AvatarSize::Xs, "Xs"),
(AvatarSize::Sm, "Sm"),
(AvatarSize::Md, "Md"),
(AvatarSize::Lg, "Lg"),
(AvatarSize::Xl, "Xl"),
]
.map(|(size, name)| {
let side = theme.avatar_size(size);
let sample = div()
.size(side)
.rounded_full()
.border_1()
.border_color(colors.border_strong)
.bg(colors.sunken);
measure(format!("AvatarSize::{name}"), at(side), sample, cx)
});
let containers = [
(ContainerSize::Sm, "Sm"),
(ContainerSize::Md, "Md"),
(ContainerSize::Lg, "Lg"),
]
.map(|(size, name)| {
let width = theme.container_width(size);
let widest = theme.container_width(ContainerSize::Lg);
// Drawn at its share of the widest, so the bar stays inside the row.
let sample = div().relative().flex_1().h_2().child(
div()
.absolute()
.top_0()
.left_0()
.h_full()
.w(relative(width.0 / widest.0))
.rounded(theme.radius(Radius::Sm))
.bg(colors.border_strong),
);
measure(format!("ContainerSize::{name}"), at(width), sample, cx)
});
let icons = [
(IconSize::Xs, "Xs"),
(IconSize::Sm, "Sm"),
@@ -276,6 +315,8 @@ impl RenderOnce for DesignTokenViewer {
.child(group("Radii", cx).children(radii))
.child(group("Controls", cx).children(controls))
.child(group("Icons", cx).children(icons))
.child(group("Avatars", cx).children(avatars))
.child(group("Containers", cx).children(containers))
.child(
group("Elevation", cx)
.child(div().flex().flex_wrap().gap_6().p_2().children(elevations)),