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 <noreply@anthropic.com>
327 lines
11 KiB
Rust
327 lines
11 KiB
Rust
use gpui::{
|
|
AnyElement, App, FontWeight, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
|
Rems, RenderOnce, Rgba, Styled, Window, div, relative, rems,
|
|
};
|
|
|
|
use crate::{
|
|
forms::hex,
|
|
primitives::{Icon, IconName},
|
|
theme::{
|
|
ActiveTheme, AvatarSize, ContainerSize, ControlSize, Elevation, HUE_NAMES, IconSize,
|
|
Palette, Radius, Syntax, TextSize,
|
|
},
|
|
typography::{Ellipsis, literal, tabular},
|
|
};
|
|
|
|
const ANSI: [&str; 16] = [
|
|
"black",
|
|
"red",
|
|
"green",
|
|
"yellow",
|
|
"blue",
|
|
"magenta",
|
|
"cyan",
|
|
"white",
|
|
"bright black",
|
|
"bright red",
|
|
"bright green",
|
|
"bright yellow",
|
|
"bright blue",
|
|
"bright magenta",
|
|
"bright cyan",
|
|
"bright white",
|
|
];
|
|
|
|
/// gpui's spacing steps shown, each a quarter rem, as `gap_2` reads.
|
|
const STEPS: [f32; 6] = [1.0, 2.0, 3.0, 4.0, 6.0, 8.0];
|
|
|
|
fn pixels(value: Pixels) -> String {
|
|
((f32::from(value) * 10.0).round() / 10.0).to_string()
|
|
}
|
|
|
|
/// A titled group of tokens.
|
|
fn group(title: &'static str, cx: &App) -> gpui::Div {
|
|
div().flex().flex_col().gap_3().child(
|
|
div()
|
|
.text_size(cx.theme().text_size(TextSize::Sm))
|
|
.font_weight(FontWeight::SEMIBOLD)
|
|
.child(title),
|
|
)
|
|
}
|
|
|
|
/// A color's tile: its swatch, its name as code reads it, and its hex.
|
|
fn swatch(name: String, color: Hsla, cx: &App) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let sizes = theme.tooling();
|
|
let code = hex(Rgba::from(color));
|
|
let shown = format!("token-{name}-{code}");
|
|
div()
|
|
.debug_selector(move || shown)
|
|
.flex()
|
|
.items_center()
|
|
.gap_2()
|
|
.w(sizes.tile)
|
|
.min_w_0()
|
|
.child(
|
|
div()
|
|
.flex_none()
|
|
.size(sizes.swatch)
|
|
.rounded(theme.radius(Radius::Md))
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
.bg(color),
|
|
)
|
|
.child(
|
|
literal(div())
|
|
.flex_1()
|
|
.min_w_0()
|
|
.flex()
|
|
.flex_col()
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.child(Ellipsis::new(name))
|
|
.child(div().text_color(theme.colors.fg_muted).child(code)),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
/// A measure's row: its name as code reads it and its size in pixels, then a sample of it.
|
|
fn measure(name: String, size: Pixels, sample: impl IntoElement, cx: &App) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let value = pixels(size);
|
|
let shown = format!("token-{name}-{value}");
|
|
div()
|
|
.debug_selector(move || shown)
|
|
.flex()
|
|
.flex_wrap()
|
|
.items_center()
|
|
.gap_x_4()
|
|
.gap_y_1()
|
|
.child(
|
|
div()
|
|
.flex_none()
|
|
.w(theme.tooling().tile)
|
|
.flex()
|
|
.justify_between()
|
|
.gap_2()
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.child(literal(div()).child(name))
|
|
.child(
|
|
tabular(div())
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(format!("{value} px")),
|
|
),
|
|
)
|
|
.child(div().flex_1().min_w_0().flex().items_center().child(sample))
|
|
.into_any_element()
|
|
}
|
|
|
|
/// 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;
|
|
|
|
impl DesignTokenViewer {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for DesignTokenViewer {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let rem = window.rem_size();
|
|
let at = |size: Rems| size.to_pixels(rem);
|
|
let theme = cx.theme();
|
|
let (colors, sizes) = (&theme.colors, theme.tooling());
|
|
let tiles = |title, tiles: Vec<AnyElement>| {
|
|
group(title, cx).child(div().flex().flex_wrap().gap_3().children(tiles))
|
|
};
|
|
let palette = Palette::NAMES
|
|
.iter()
|
|
.map(|name| swatch(format!("colors.{name}"), colors.token(name), cx));
|
|
let syntax = Syntax::NAMES.iter().map(|name| {
|
|
swatch(
|
|
format!("colors.syntax.{name}"),
|
|
colors.syntax.token(name),
|
|
cx,
|
|
)
|
|
});
|
|
let chart = HUE_NAMES.iter().enumerate().map(|(ix, name)| {
|
|
swatch(
|
|
format!("colors.hue({ix}) {name}"),
|
|
colors.hue(ix, "token viewer"),
|
|
cx,
|
|
)
|
|
});
|
|
let ansi = ANSI
|
|
.iter()
|
|
.zip(colors.ansi)
|
|
.enumerate()
|
|
.map(|(ix, (name, color))| swatch(format!("colors.ansi[{ix}] {name}"), color, cx));
|
|
let text = [
|
|
(TextSize::Xs, "Xs"),
|
|
(TextSize::Sm, "Sm"),
|
|
(TextSize::Base, "Base"),
|
|
(TextSize::Md, "Md"),
|
|
(TextSize::Lg, "Lg"),
|
|
(TextSize::Xl, "Xl"),
|
|
(TextSize::Xxl, "Xxl"),
|
|
(TextSize::Display, "Display"),
|
|
]
|
|
.map(|(size, name)| {
|
|
let sample = div()
|
|
.min_w_0()
|
|
.text_size(theme.text_size(size))
|
|
.child(Ellipsis::new("Sphinx of black quartz"));
|
|
measure(
|
|
format!("TextSize::{name}"),
|
|
at(theme.text_size(size)),
|
|
sample,
|
|
cx,
|
|
)
|
|
});
|
|
let radii = [
|
|
(Radius::Sm, "Sm"),
|
|
(Radius::Md, "Md"),
|
|
(Radius::Lg, "Lg"),
|
|
(Radius::Xl, "Xl"),
|
|
]
|
|
.map(|(radius, name)| {
|
|
let sample = div()
|
|
.size(sizes.swatch)
|
|
.rounded(theme.radius(radius))
|
|
.border_1()
|
|
.border_color(colors.border_strong)
|
|
.bg(colors.sunken);
|
|
measure(
|
|
format!("Radius::{name}"),
|
|
at(theme.radius(radius)),
|
|
sample,
|
|
cx,
|
|
)
|
|
});
|
|
let controls = [
|
|
(ControlSize::Sm, "Sm"),
|
|
(ControlSize::Md, "Md"),
|
|
(ControlSize::Lg, "Lg"),
|
|
]
|
|
.map(|(size, name)| {
|
|
let height = theme.control_height(size);
|
|
let sample = div()
|
|
.h(height)
|
|
.w(sizes.tile)
|
|
.rounded(theme.radius(Radius::Md))
|
|
.border_1()
|
|
.border_color(colors.border_strong)
|
|
.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"),
|
|
(IconSize::Md, "Md"),
|
|
(IconSize::Lg, "Lg"),
|
|
(IconSize::Xl, "Xl"),
|
|
(IconSize::Xxl, "Xxl"),
|
|
]
|
|
.map(|(size, name)| {
|
|
let sample = Icon::new(IconName::Star).size(size).color(colors.fg);
|
|
measure(
|
|
format!("IconSize::{name}"),
|
|
at(theme.icon_size(size)),
|
|
sample,
|
|
cx,
|
|
)
|
|
});
|
|
let elevations = [
|
|
(Elevation::Raised, "Raised"),
|
|
(Elevation::Floating, "Floating"),
|
|
(Elevation::Modal, "Modal"),
|
|
]
|
|
.map(|(level, name)| {
|
|
let card = div()
|
|
.w(sizes.tile)
|
|
.h(sizes.swatch)
|
|
.rounded(theme.radius(Radius::Md))
|
|
.bg(colors.surface)
|
|
.shadow(theme.elevation(level));
|
|
let shown = format!("token-Elevation::{name}");
|
|
div()
|
|
.debug_selector(move || shown)
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.child(card)
|
|
.child(
|
|
literal(div())
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.child(format!("Elevation::{name}")),
|
|
)
|
|
});
|
|
let steps = STEPS.map(|step| {
|
|
let width = rems(step / 4.0);
|
|
let sample = div()
|
|
.h_2()
|
|
.w(width)
|
|
.rounded(theme.radius(Radius::Sm))
|
|
.bg(colors.accent);
|
|
measure(format!("gap_{step}"), at(width), sample, cx)
|
|
});
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_8()
|
|
.child(tiles("Colors", palette.collect()))
|
|
.child(tiles("Syntax", syntax.collect()))
|
|
.child(tiles("Chart", chart.collect()))
|
|
.child(tiles("Terminal", ansi.collect()))
|
|
.child(group("Type", cx).children(text))
|
|
.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)),
|
|
)
|
|
.child(group("Spacing", cx).children(steps))
|
|
}
|
|
}
|