From ed1014bc34ba7a9ed8012a113b90f67be09f1322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 28 Sep 2026 08:57:52 -0400 Subject: [PATCH] =?UTF-8?q?Tooling=20=C2=B7=20Catalogs:=20the=20audit=20re?= =?UTF-8?q?ads=20active=20and=20code;=20avatar=20and=20container=20sizes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- examples/gallery/pages/tooling/catalogs.rs | 4 +- src/theme/palette.rs | 14 +++--- src/tooling/a11y.rs | 51 ++++++++++++++++--- src/tooling/tests/catalogs.rs | 58 +++++++++++++++++++--- src/tooling/tokens.rs | 47 ++++++++++++++++-- tasks/ch31-43.md | 4 +- 7 files changed, 153 insertions(+), 27 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 460c1f9..06612c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -224,4 +224,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - 2026-09-28: QRCodeScanner proven with `rqrr` 0.11: the host hands in frames as `RenderImage`s, read off the main thread; gpui 0.2.2 has no camera. - 2026-09-28: WebView proven with `wry` 0.57: a child WKWebView over the window, placed from a canvas in prepaint at its box each frame and hidden once the box leaves the view. It draws above everything gpui paints and gpui's clipping does not reach it; gpui's test windows have no native handle, so the capture proves it. - 2026-09-28: InspectorOverlay is gpui's inspector drawn by Ely, `tooling::install_inspector`, in debug builds only, as gpui has it. -- 2026-09-28: `fg_subtle` meets 4.5:1 on every surface it sits on, #6f6d6a light and #8c8a87 dark; the contrast audit found #82807d and #858380 under it. +- 2026-09-28: every text color meets 4.5:1 on the surfaces it sits on: `fg_subtle` is #696764 light and #94928f dark, `syntax.comment` with it, and the light syntax's number, constant and attribute are #a3591b, #aa5529 and #9f5d12; the contrast audit found them under. diff --git a/examples/gallery/pages/tooling/catalogs.rs b/examples/gallery/pages/tooling/catalogs.rs index f46e2c5..db77ad2 100644 --- a/examples/gallery/pages/tooling/catalogs.rs +++ b/examples/gallery/pages/tooling/catalogs.rs @@ -17,7 +17,7 @@ pub fn sections(window: &mut Window, cx: &mut App) -> Vec { vec![ section( "DesignTokenViewer", - "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 the 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 the spacing steps.", cx, ) .child(div().max_w(px(840.0)).child(DesignTokenViewer::new())) @@ -56,7 +56,7 @@ pub fn sections(window: &mut Window, cx: &mut App) -> Vec { .into_any_element(), section( "A11yChecker", - "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. Ely's palettes pass in both modes, with high contrast and without.", + "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 at 4.5:1, the focus ring and the chart hues beside the page at 3:1, failures first. Ely's palettes pass in both modes, with high contrast and without.", cx, ) .child(div().max_w(px(560.0)).child(A11yChecker::new())) diff --git a/src/theme/palette.rs b/src/theme/palette.rs index daebade..3622d66 100644 --- a/src/theme/palette.rs +++ b/src/theme/palette.rs @@ -185,7 +185,7 @@ impl Palette { border_strong: c(0xcccac8), fg: c(0x181613), fg_muted: c(0x605d5a), - fg_subtle: c(0x6f6d6a), + fg_subtle: c(0x696764), fg_disabled: c(0xacaaa8), accent: c(0x181613), accent_hover: c(0x302d2b), @@ -221,14 +221,14 @@ impl Palette { syntax: Syntax { keyword: c(0x714ca6), string: c(0x317a45), - number: c(0xb3621e), - comment: c(0x82807d), + number: c(0xa3591b), + comment: c(0x696764), function: c(0x2863ab), type_name: c(0x1e7777), - constant: c(0xb1582b), + constant: c(0xaa5529), property: c(0xa14759), tag: c(0xa14759), - attribute: c(0xa25f12), + attribute: c(0x9f5d12), operator: c(0x605d5a), punctuation: c(0x605d5a), variable: c(0x181613), @@ -256,7 +256,7 @@ impl Palette { border_strong: c(0x413f3d), fg: c(0xf3f1f0), fg_muted: c(0xafadab), - fg_subtle: c(0x8c8a87), + fg_subtle: c(0x94928f), fg_disabled: c(0x5c5a58), accent: c(0xf3f1f0), accent_hover: c(0xd9d7d5), @@ -293,7 +293,7 @@ impl Palette { keyword: c(0xbba3e8), string: c(0x8ac596), number: c(0xeaab78), - comment: c(0x858380), + comment: c(0x94928f), function: c(0x85b6e9), type_name: c(0x84c9c8), constant: c(0xeda382), diff --git a/src/tooling/a11y.rs b/src/tooling/a11y.rs index febb800..e0f373d 100644 --- a/src/tooling/a11y.rs +++ b/src/tooling/a11y.rs @@ -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 { 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 { .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 { 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); diff --git a/src/tooling/tests/catalogs.rs b/src/tooling/tests/catalogs.rs index 45f53f8..2cf6a27 100644 --- a/src/tooling/tests/catalogs.rs +++ b/src/tooling/tests/catalogs.rs @@ -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") diff --git a/src/tooling/tokens.rs b/src/tooling/tokens.rs index 163423d..b1f10d6 100644 --- a/src/tooling/tokens.rs +++ b/src/tooling/tokens.rs @@ -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)), diff --git a/tasks/ch31-43.md b/tasks/ch31-43.md index e2dd70a..1534733 100644 --- a/tasks/ch31-43.md +++ b/tasks/ch31-43.md @@ -366,7 +366,7 @@ ## T43c Tooling · Catalogs -- [x] DesignTokenViewer → `tooling::DesignTokenViewer`: every token of the theme in the mode shown, named as code reads it, with its value +- [x] DesignTokenViewer → `tooling::DesignTokenViewer`: the theme's shared colors and scales in the mode shown, named as code reads them, with their values - [x] IconBrowser → `forms::IconPicker`: the library's icons, searchable -- [x] A11yChecker — contrast audit → `tooling::A11yChecker`: each text color over its surfaces at 4.5:1, the focus ring and chart hues beside the page at 3:1, failures first +- [x] A11yChecker — contrast audit → `tooling::A11yChecker`: each text color over its surfaces and each code color over the editor's at 4.5:1, the focus ring and chart hues beside the page at 3:1, failures first - [x] ScreenshotTest Harness — self-capture; scripted input since T01 → `examples/gallery --capture`: every page light and dark, then its scripted states