T00 foundation: theme, motion, assets, icon, buttons, gallery

Theme tokens in light and dark with high contrast, a palette cross-fade,
motion curves and a damped spring, embedded Lucide icons with Inter and
JetBrains Mono, Icon, Button, IconButton, and a gallery that captures
its own window. Reviewed by codex gpt-6-astra (max): PASS in round 3.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-24 23:59:55 -04:00
commit abf4cbcfc6
258 files changed
+16908

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
use ely_gpui_component::{
buttons::{Button, ButtonVariant, IconButton},
primitives::IconName,
theme::ControlSize,
};
use gpui::{AnyElement, App, ElementId, IntoElement, ParentElement, Styled, Window, div};
use super::Page;
use crate::ui::{row, section, specimen, specimens};
pub const PAGE: Page = Page {
number: 5,
slug: "buttons",
title: "Buttons & Actions",
summary: "Seven variants, three sizes, one focus ring.",
render,
};
const VARIANTS: [(ButtonVariant, &str); 7] = [
(ButtonVariant::Primary, "Primary"),
(ButtonVariant::Secondary, "Secondary"),
(ButtonVariant::Outline, "Outline"),
(ButtonVariant::Ghost, "Ghost"),
(ButtonVariant::Subtle, "Subtle"),
(ButtonVariant::Danger, "Danger"),
(ButtonVariant::Link, "Link"),
];
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
let sizes = [
(ControlSize::Sm, "Small"),
(ControlSize::Md, "Medium"),
(ControlSize::Lg, "Large"),
];
div()
.child(
section("Button", "Primary is ink. Color is saved for meaning.", cx)
.child(row().children(VARIANTS.map(|(variant, name)| {
Button::new(name, name)
.variant(variant)
.on_click(move |_, _, _| log::info!("gallery: {name} clicked"))
})))
.child(row().children(VARIANTS.map(|(variant, name)| {
Button::new(ElementId::Name(format!("{name}-off").into()), name)
.variant(variant)
.disabled(true)
}))),
)
.child(
section(
"Sizes",
"Heights follow density. Tab moves the focus ring.",
cx,
)
.child(row().children(sizes.map(|(size, name)| {
Button::new(ElementId::Name(format!("size-{name}").into()), name)
.primary()
.size(size)
}))),
)
.child(
section(
"With icons",
"Leading icon for verbs. Trailing for menus.",
cx,
)
.child(
row()
.child(
Button::new("new", "New file")
.primary()
.icon(IconName::Plus),
)
.child(Button::new("share", "Share").icon(IconName::Share2))
.child(
Button::new("sort", "Sort")
.variant(ButtonVariant::Ghost)
.trailing_icon(IconName::ChevronDown),
)
.child(
Button::new("delete", "Delete")
.variant(ButtonVariant::Danger)
.icon(IconName::Trash2),
),
),
)
.child(
section("Icon button", "Square. Ghost by default.", cx).child(specimens().children(
sizes.map(|(size, name)| {
specimen(
name,
row()
.gap_1()
.child(
IconButton::new(
ElementId::Name(format!("{name}-a").into()),
IconName::Bold,
)
.size(size),
)
.child(
IconButton::new(
ElementId::Name(format!("{name}-b").into()),
IconName::Italic,
)
.size(size),
)
.child(
IconButton::new(
ElementId::Name(format!("{name}-c").into()),
IconName::Underline,
)
.size(size)
.variant(ButtonVariant::Subtle),
)
.child(
IconButton::new(
ElementId::Name(format!("{name}-d").into()),
IconName::Link,
)
.size(size)
.variant(ButtonVariant::Outline),
),
cx,
)
}),
)),
)
.into_any_element()
}
+19
View File
@@ -0,0 +1,19 @@
mod buttons;
mod primitives;
mod theme;
use gpui::{AnyElement, App, Window};
pub struct Page {
pub number: u8,
pub slug: &'static str,
pub title: &'static str,
pub summary: &'static str,
pub render: fn(&mut Window, &mut App) -> AnyElement,
}
pub const ALL: &[Page] = &[primitives::PAGE, buttons::PAGE, theme::PAGE];
pub fn find(slug: &str) -> Option<usize> {
ALL.iter().position(|page| page.slug == slug)
}
+73
View File
@@ -0,0 +1,73 @@
use ely_gpui_component::{
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize},
};
use gpui::{AnyElement, App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div};
use super::Page;
use crate::ui::{section, specimen, specimens};
pub const PAGE: Page = Page {
number: 1,
slug: "primitives",
title: "Primitives",
summary: "The smallest parts. Icons, dividers, overlays, focus.",
render,
};
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
let colors = &cx.theme().colors;
let sizes = [
(IconSize::Xs, "12"),
(IconSize::Sm, "14"),
(IconSize::Md, "16"),
(IconSize::Lg, "20"),
(IconSize::Xl, "24"),
(IconSize::Xxl, "32"),
];
let tones = [
(colors.fg, "fg"),
(colors.fg_muted, "muted"),
(colors.focus, "focus"),
(colors.success, "success"),
(colors.warning, "warning"),
(colors.danger, "danger"),
];
div()
.child(
section(
"Icon",
"Lucide, drawn as a mask. Color comes from the theme.",
cx,
)
.child(specimens().children(
sizes.map(|(size, name)| {
specimen(name, Icon::new(IconName::Sparkles).size(size), cx)
}),
))
.child(specimens().children(tones.map(|(color, name)| {
specimen(name, Icon::new(IconName::CircleCheck).color(color), cx)
}))),
)
.child(
section("Icon set", "Every bundled icon.", cx).child(
div()
.flex()
.flex_wrap()
.gap_1()
.children(IconName::ALL.iter().enumerate().map(|(ix, &name)| {
div()
.id(ix)
.size_9()
.flex()
.items_center()
.justify_center()
.rounded_md()
.hover(|style| style.bg(colors.hover))
.child(Icon::new(name).color(colors.fg_muted))
})),
),
)
.into_any_element()
}
+324
View File
@@ -0,0 +1,324 @@
use std::time::Duration;
use ely_gpui_component::{
motion,
theme::{ActiveTheme, Elevation, Radius, TextSize},
};
use gpui::{
Animation, AnimationExt, AnyElement, App, FontStyle, FontWeight, HighlightStyle, Hsla,
IntoElement, ParentElement, SharedString, Styled, StyledText, Window, div, px,
};
use super::Page;
use crate::ui::{section, specimen, specimens};
pub const PAGE: Page = Page {
number: 39,
slug: "theme",
title: "Theme",
summary: "Tokens every component reads. Light and dark share one grammar.",
render,
};
fn render(_: &mut Window, cx: &mut App) -> AnyElement {
let colors = cx.theme().colors.clone();
let surfaces = [
("bg", colors.bg),
("surface", colors.surface),
("sunken", colors.sunken),
("hover", colors.hover),
("active", colors.active),
("border", colors.border),
("border strong", colors.border_strong),
];
let inks = [
("fg", colors.fg),
("muted", colors.fg_muted),
("subtle", colors.fg_subtle),
("disabled", colors.fg_disabled),
("focus", colors.focus),
("link", colors.link),
];
let signals = [
("success", colors.success, colors.success_subtle),
("warning", colors.warning, colors.warning_subtle),
("danger", colors.danger, colors.danger_subtle),
("info", colors.info, colors.info_subtle),
];
div()
.child(
section(
"Surfaces",
"Warm neutrals. Depth comes from tone, not shadow.",
cx,
)
.child(
specimens()
.gap_3()
.children(surfaces.map(|(name, color)| swatch(name, color, cx))),
),
)
.child(
section("Ink", "Four steps of text. Blue marks focus and links.", cx).child(
specimens()
.gap_3()
.children(inks.map(|(name, color)| swatch(name, color, cx))),
),
)
.child(
section("Signals", "Status colors, each with a quiet fill.", cx).child(
specimens()
.gap_3()
.children(signals.map(|(name, strong, quiet)| {
specimen(name, signal_pair(strong, quiet, cx), cx)
})),
),
)
.child(
section("Series", "Eight chart colors, even in weight.", cx).child(
specimens()
.gap_2()
.children(colors.chart.iter().enumerate().map(|(ix, &color)| {
specimen(format!("{}", ix + 1), chip(color, 40.0, cx), cx)
})),
),
)
.child(
section("Syntax", "Code tokens sit below text in weight.", cx).child(code_sample(cx)),
)
.child(
section("Type", "Inter for words. JetBrains Mono for code.", cx).child(type_scale(cx)),
)
.child(
section("Shape", "Four radii. Three elevations.", cx)
.child(
specimens().children([Radius::Sm, Radius::Md, Radius::Lg, Radius::Xl].map(
|radius| {
specimen(
format!("{radius:?}"),
div()
.size(px(64.0))
.rounded(cx.theme().radius(radius))
.bg(colors.sunken)
.border_1()
.border_color(colors.border_strong),
cx,
)
},
)),
)
.child(specimens().gap_8().pt_4().children(
[Elevation::Raised, Elevation::Floating, Elevation::Modal].map(|level| {
specimen(
format!("{level:?}"),
div()
.w(px(120.0))
.h(px(72.0))
.rounded(cx.theme().radius(Radius::Lg))
.bg(colors.overlay)
.shadow(cx.theme().elevation(level)),
cx,
)
}),
)),
)
.child(
section(
"Motion",
"Ease out to arrive. Springs only where a hand leads. Slowed to one second here.",
cx,
)
.child(track(
"ease-out",
"ease out cubic",
motion::ease_out_cubic,
cx,
))
.child(track(
"ease-in-out",
"ease in out cubic",
motion::ease_in_out_cubic,
cx,
))
.child(track("spring", "spring · damping 0.75", motion::spring, cx)),
)
.into_any_element()
}
fn chip(color: Hsla, side: f32, cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
div()
.w(px(side))
.h(px(side))
.rounded(theme.radius(Radius::Md))
.bg(color)
.border_1()
.border_color(theme.colors.border)
}
fn swatch(name: &'static str, color: Hsla, cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
specimen(
name,
div()
.w(px(88.0))
.h(px(56.0))
.rounded(theme.radius(Radius::Lg))
.bg(color)
.border_1()
.border_color(theme.colors.border),
cx,
)
}
fn signal_pair(strong: Hsla, quiet: Hsla, cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
div()
.flex()
.items_center()
.gap_2()
.h(px(40.0))
.px_3()
.rounded(theme.radius(Radius::Md))
.bg(quiet)
.child(div().size(px(8.0)).rounded_full().bg(strong))
.child(
div()
.text_color(strong)
.font_weight(FontWeight::MEDIUM)
.child("Aa 12"),
)
}
fn code_sample(cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let syntax = &theme.colors.syntax;
let text = "fn answer() -> u32 { let x = 42; x } // truth";
let color = |c: Hsla| HighlightStyle {
color: Some(c),
..Default::default()
};
let spans = [
(0..2, color(syntax.keyword)),
(3..9, color(syntax.function)),
(9..11, color(syntax.punctuation)),
(12..14, color(syntax.operator)),
(15..18, color(syntax.type_name)),
(19..20, color(syntax.punctuation)),
(21..24, color(syntax.keyword)),
(25..26, color(syntax.variable)),
(27..28, color(syntax.operator)),
(29..31, color(syntax.number)),
(31..32, color(syntax.punctuation)),
(35..36, color(syntax.punctuation)),
(
37..45,
HighlightStyle {
color: Some(syntax.comment),
font_style: Some(FontStyle::Italic),
..Default::default()
},
),
];
div()
.px_4()
.py_3()
.rounded(theme.radius(Radius::Lg))
.bg(theme.colors.sunken)
.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Base))
.text_color(theme.colors.fg)
.child(StyledText::new(text).with_highlights(spans))
}
fn type_scale(cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let steps = [
(TextSize::Display, "Display 32", FontWeight::SEMIBOLD),
(TextSize::Xxl, "Heading 24", FontWeight::SEMIBOLD),
(TextSize::Xl, "Title 20", FontWeight::SEMIBOLD),
(TextSize::Lg, "Subtitle 16", FontWeight::MEDIUM),
(TextSize::Md, "Body large 14", FontWeight::NORMAL),
(TextSize::Base, "Body 13", FontWeight::NORMAL),
(TextSize::Sm, "Small 12", FontWeight::NORMAL),
(TextSize::Xs, "Caption 11", FontWeight::NORMAL),
];
div()
.flex()
.flex_col()
.gap_3()
.children(steps.map(|(size, name, weight)| {
div()
.flex()
.items_center()
.gap_6()
.child(
div()
.w(px(120.0))
.text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_subtle)
.child(SharedString::from(name)),
)
.child(
div()
.text_size(theme.text_size(size))
.font_weight(weight)
.text_color(theme.colors.fg)
.child("Quiet interfaces age well."),
)
}))
}
fn track(
id: &'static str,
name: &'static str,
ease: fn(f32) -> f32,
cx: &App,
) -> impl IntoElement + use<> {
let theme = cx.theme();
let width = 360.0;
let travel = width - 12.0;
let knob = div()
.absolute()
.top(px(6.0))
.size(px(12.0))
.rounded_full()
.bg(theme.colors.fg);
let knob = if theme.reduced_motion {
knob.left(px(travel)).into_any_element()
} else {
knob.with_animation(
id,
Animation::new(Duration::from_millis(2400)).repeat(),
move |knob, t| {
let offset = match t {
t if t < 0.42 => ease(t / 0.42),
t if t < 0.5 => 1.0,
t if t < 0.92 => 1.0 - ease((t - 0.5) / 0.42),
_ => 0.0,
};
knob.left(px(offset * travel))
},
)
.into_any_element()
};
specimen(
name,
div()
.relative()
.w(px(width))
.h(px(24.0))
.child(
div()
.absolute()
.top(px(11.5))
.left_0()
.w(px(width))
.h(px(1.0))
.bg(theme.colors.border),
)
.child(knob),
cx,
)
}