T02 typography: text styles, keys, formatters, fit, links, math, motion text

Headings to overlines, Code, Kbd and KbdCombo on gpui keystrokes,
Highlight, MiddleEllipsis and EllipsisTooltip, links, eight formatted
values on jiff with a typographic minus, CopyableText, SelectableText,
Emoji, a TeX subset, rolling AnimatedNumber, Typewriter, tonal
GradientText, ShimmerText. Gallery probes now hug their targets.
Codex gpt-6-astra (max): PASS in round 2.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 01:12:52 -04:00
1 parent a13343c2a5
commit b35530c80d
30 files changed
+3125 -58

No files matched your search

+7 -1
View File
@@ -1,6 +1,7 @@
mod buttons;
mod primitives;
mod theme;
mod typography;
use gpui::{AnyElement, App, Window};
@@ -16,7 +17,12 @@ pub struct Page {
pub script: &'static [Step],
}
pub const ALL: &[Page] = &[primitives::PAGE, buttons::PAGE, theme::PAGE];
pub const ALL: &[Page] = &[
primitives::PAGE,
typography::PAGE,
buttons::PAGE,
theme::PAGE,
];
pub fn find(slug: &str) -> Option<usize> {
ALL.iter().position(|page| page.slug == slug)
+69
View File
@@ -0,0 +1,69 @@
mod motion;
mod text;
mod values;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 2,
slug: "typography",
title: "Typography",
summary: "Inter for words, tabular figures for numbers, one ink for everything.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Hover("fit-tooltip"),
Step::Wait(700),
Step::Shot("ellipsis-tooltip"),
Step::Rest,
Step::ClickEnd("copy-token"),
Step::Shot("copied"),
Step::Click("select-text"),
Step::Key("cmd-a"),
Step::Shot("select-all"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(text::headings(cx))
.child(text::title(cx))
.child(text::paragraph(cx))
.child(text::label(cx))
.child(text::caption(cx))
.child(text::overline(cx))
.child(text::inline_code(cx))
.child(text::kbd(cx))
.child(text::kbd_combo(cx))
.child(text::blockquote(cx))
.child(text::highlight(cx))
.child(text::truncate(cx))
.child(text::line_clamp(cx))
.child(text::middle(cx))
.child(text::ellipsis_tooltip(cx))
.child(text::link(cx))
.child(text::external_link(cx))
.child(values::relative_time(cx))
.child(values::date_time(cx))
.child(values::numbers(cx))
.child(values::currency(cx))
.child(values::percent(cx))
.child(values::file_size(cx))
.child(values::duration(cx))
.child(values::plural(cx))
.child(values::copyable(cx))
.child(values::selectable(cx))
.child(values::emoji(cx))
.child(values::latex(cx))
.child(motion::animated_number(window, cx))
.child(motion::typewriter(window, cx))
.child(motion::gradient(cx))
.child(motion::shimmer(cx))
.into_any_element()
}
+120
View File
@@ -0,0 +1,120 @@
use std::time::Duration;
use ely_gpui_component::{
buttons::{Button, ButtonVariant},
theme::{ActiveTheme, TextSize},
typography::{AnimatedNumber, GradientText, ShimmerText, Typewriter},
};
use gpui::{App, IntoElement, ParentElement, Styled, Task, Window, div};
use crate::ui::{row, section, specimen, specimens};
const STEP: Duration = Duration::from_millis(1600);
const VALUES: [f64; 5] = [1_284.0, 1_917.0, 12_406.0, 9_998.0, 10_031.0];
struct Ticker {
index: usize,
_task: Task<()>,
}
pub fn animated_number(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let ticker = window.use_keyed_state("type-ticker", cx, |window, cx| Ticker {
index: 0,
_task: cx.spawn_in(window, async move |ticker, cx| {
loop {
cx.background_executor().timer(STEP).await;
let advanced = ticker.update(cx, |ticker: &mut Ticker, cx| {
ticker.index = (ticker.index + 1) % VALUES.len();
cx.notify();
});
if advanced.is_err() {
return;
}
}
}),
});
let value = VALUES[ticker.read(cx).index];
section(
"AnimatedNumber",
"Digits roll by place; or the value counts up.",
cx,
)
.child(
specimens()
.gap_12()
.child(specimen("roll", AnimatedNumber::new("roll", value), cx))
.child(specimen(
"count up",
AnimatedNumber::new("count", value * 3.7)
.decimals(1)
.count_up(),
cx,
)),
)
}
pub fn typewriter(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let take = window.use_keyed_state("type-take", cx, |_, _| 0usize);
let lines = [
"Writing, one grapheme at a time.",
"中文与 emoji ✨ 也按字形逐个出现。",
];
let line = lines[*take.read(cx) % lines.len()];
section(
"Typewriter",
"Reveals by grapheme, so emoji and CJK stay whole.",
cx,
)
.child(
row()
.gap_6()
.child(div().min_w_80().child(Typewriter::new("typewriter", line)))
.child(
Button::new("type-again", "Next line")
.variant(ButtonVariant::Ghost)
.on_click(move |_, _, cx| {
take.update(cx, |take, cx| {
*take += 1;
cx.notify();
})
}),
),
)
}
pub fn gradient(cx: &App) -> impl IntoElement + use<> {
let colors = &cx.theme().colors;
let (ink, focus) = (colors.fg, colors.focus);
let display = cx.theme().text_size(TextSize::Display);
section("GradientText", "A tonal ramp, glyph by glyph. No neon.", cx).child(
specimens()
.gap_10()
.child(specimen(
"ink to muted",
div()
.text_size(display)
.child(GradientText::new("Quiet type")),
cx,
))
.child(specimen(
"ink to focus",
div()
.text_size(display)
.child(GradientText::new("Quiet type").colors(ink, focus)),
cx,
)),
)
}
pub fn shimmer(cx: &App) -> impl IntoElement + use<> {
section(
"ShimmerText",
"A slow band of light while something loads.",
cx,
)
.child(
div()
.text_size(cx.theme().text_size(TextSize::Lg))
.child(ShimmerText::new("shimmer", "Thinking through the request…")),
)
}
+220
View File
@@ -0,0 +1,220 @@
use ely_gpui_component::{
theme::{ActiveTheme, Radius},
typography::{
Blockquote, Caption, Code, EllipsisTooltip, ExternalLink, Heading, Highlight, Kbd,
KbdCombo, Label, Link, MiddleEllipsis, Overline, Paragraph, Subtitle, Title,
},
};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use crate::{
probe::probe,
ui::{code, row, section, specimen, specimens},
};
const PROSE: &str = "Good interfaces are quiet. They leave room around each idea, \
use one voice for text, and save color for the moments that need it. The rest is spacing.";
pub fn headings(cx: &App) -> impl IntoElement + use<> {
section(
"Heading (H1–H6)",
"Six steps, one weight. Size carries the order.",
cx,
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(Heading::h1("Quiet interfaces"))
.child(Heading::h2("Quiet interfaces"))
.child(Heading::h3("Quiet interfaces"))
.child(Heading::h4("Quiet interfaces"))
.child(Heading::h5("Quiet interfaces"))
.child(Heading::h6("Quiet interfaces")),
)
}
pub fn title(cx: &App) -> impl IntoElement + use<> {
section("Title / Subtitle", "A pair for page and panel headers.", cx).child(
div()
.flex()
.flex_col()
.gap_1()
.child(Title::new("Quarterly review"))
.child(Subtitle::new("Numbers, notes and the next three moves.")),
)
}
pub fn paragraph(cx: &App) -> impl IntoElement + use<> {
section("Paragraph", "Reading text at 14 with loose leading.", cx)
.child(Paragraph::new(PROSE).max_w(px(560.0)))
}
pub fn label(cx: &App) -> impl IntoElement + use<> {
section("Label", "Names a control or a value.", cx).child(
row()
.child(Label::new("Display name"))
.child(Label::new("Workspace")),
)
}
pub fn caption(cx: &App) -> impl IntoElement + use<> {
section("Caption", "The quiet line beneath.", cx)
.child(Caption::new("Updated a moment ago · visible to your team"))
}
pub fn overline(cx: &App) -> impl IntoElement + use<> {
section("Overline", "An eyebrow in small capitals.", cx).child(
div()
.flex()
.flex_col()
.gap_1()
.child(Overline::new("Release notes"))
.child(Title::new("Version 0.1")),
)
}
pub fn inline_code(cx: &App) -> impl IntoElement + use<> {
section("Code", "Monospace on a quiet fill.", cx).child(
row()
.child("Run")
.child(Code::new("cargo run --example gallery"))
.child("to open this window."),
)
}
pub fn kbd(cx: &App) -> impl IntoElement + use<> {
section(
"Kbd",
"gpui key syntax. secondary means ⌘ on macOS and Ctrl elsewhere.",
cx,
)
.child(
specimens()
.child(specimen("secondary-s", Kbd::new("secondary-s"), cx))
.child(specimen("shift-tab", Kbd::new("shift-tab"), cx))
.child(specimen("escape", Kbd::new("escape"), cx))
.child(specimen("enter", Kbd::new("enter"), cx)),
)
}
pub fn kbd_combo(cx: &App) -> impl IntoElement + use<> {
section("KbdCombo", "Sequences read left to right.", cx).child(
specimens()
.child(specimen(
"secondary-shift-p",
KbdCombo::new("secondary-shift-p"),
cx,
))
.child(specimen(
"secondary-k secondary-s",
KbdCombo::new("secondary-k secondary-s"),
cx,
)),
)
}
pub fn blockquote(cx: &App) -> impl IntoElement + use<> {
section("Blockquote", "A rule and a softer ink.", cx).child(div().max_w(px(520.0)).child(
Blockquote::new().child(
"Simplicity is not the absence of clutter. It is the right thing, in the right place.",
),
))
}
pub fn highlight(cx: &App) -> impl IntoElement + use<> {
section(
"Highlight / Mark",
"Every match of a query, case-insensitive.",
cx,
)
.child(Highlight::matching(
"Quiet interfaces age well. Loud ones AGE fast.",
"age",
))
.child(code("Highlight::matching(text, \"age\")", cx))
}
fn narrow(cx: &App) -> gpui::Div {
let theme = cx.theme();
div()
.w(px(240.0))
.px_3()
.py_2()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
}
pub fn truncate(cx: &App) -> impl IntoElement + use<> {
section(
"Truncate",
"gpui's .truncate(): one line, an ellipsis at the end.",
cx,
)
.child(narrow(cx).child(div().truncate().child(PROSE)))
}
pub fn line_clamp(cx: &App) -> impl IntoElement + use<> {
section(
"LineClamp",
"gpui's .line_clamp(n): n lines, then a hard cut.",
cx,
)
.child(
narrow(cx)
.w(px(360.0))
.child(div().line_clamp(2).child(PROSE)),
)
}
pub fn middle(cx: &App) -> impl IntoElement + use<> {
section(
"MiddleEllipsis",
"Cuts the middle so both ends stay legible.",
cx,
)
.child(narrow(cx).w(px(280.0)).child(MiddleEllipsis::new(
"~/Documents/GitHub/Ely-GPUI-Components/src/typography/fit.rs",
)))
}
pub fn ellipsis_tooltip(cx: &App) -> impl IntoElement + use<> {
section(
"EllipsisTooltip",
"Hover shows the whole line, only when it was cut.",
cx,
)
.child(probe(
"fit-tooltip",
narrow(cx).child(EllipsisTooltip::new(
"fit-tooltip-text",
"Design review · Tuesday, with the full product team and guests",
)),
))
}
pub fn link(cx: &App) -> impl IntoElement + use<> {
section(
"Link",
"Blue marks where a click goes. Underline on hover.",
cx,
)
.child(
row()
.child("Read the")
.child(
Link::new("guide", "style guide").on_click(|_, _, _| log::info!("gallery: link")),
)
.child("before you start."),
)
}
pub fn external_link(cx: &App) -> impl IntoElement + use<> {
section("ExternalLink", "Leaves the app. The arrow says so.", cx).child(ExternalLink::new(
"gpui",
"gpui.rs",
"https://www.gpui.rs",
))
}
+226
View File
@@ -0,0 +1,226 @@
use ely_gpui_component::{
theme::TextSize,
typography::{
CopyableText, CurrencyText, DateTimeText, DurationStyle, DurationText, Emoji, FileSizeText,
Latex, NumberText, PercentText, PluralText, RelativeTime, SelectableText,
},
};
use gpui::{App, IntoElement, ParentElement, Styled, div, px};
use jiff::{SignedDuration, Timestamp};
use crate::{
probe::probe,
ui::{code, section, specimen, specimens},
};
/// Fixed moment, so shots repeat: 24 Sep 2026, 09:41 UTC.
fn moment() -> Timestamp {
Timestamp::from_second(1_790_242_860).expect("valid timestamp")
}
pub fn relative_time(cx: &App) -> impl IntoElement + use<> {
let ago = |seconds: i64| {
Timestamp::now()
.checked_sub(SignedDuration::from_secs(seconds))
.expect("in range")
};
section(
"RelativeTime",
"Refreshes every thirty seconds while on screen.",
cx,
)
.child(
specimens()
.child(specimen("10 s", RelativeTime::new("rel-a", ago(10)), cx))
.child(specimen("3 min", RelativeTime::new("rel-b", ago(180)), cx))
.child(specimen("2 h", RelativeTime::new("rel-c", ago(7_200)), cx))
.child(specimen(
"3 d",
RelativeTime::new("rel-d", ago(259_200)),
cx,
)),
)
}
pub fn date_time(cx: &App) -> impl IntoElement + use<> {
section("DateTimeText", "strftime patterns in the system zone.", cx).child(
specimens()
.child(specimen("default", DateTimeText::new(moment()), cx))
.child(specimen(
"%Y-%m-%d",
DateTimeText::new(moment()).pattern("%Y-%m-%d"),
cx,
))
.child(specimen(
"%A %H:%M",
DateTimeText::new(moment()).pattern("%A %H:%M"),
cx,
)),
)
}
pub fn numbers(cx: &App) -> impl IntoElement + use<> {
section(
"NumberText",
"Grouped, fixed precision, tabular figures.",
cx,
)
.child(
specimens()
.child(specimen(
"2 decimals",
NumberText::new(1_234_567.891).decimals(2),
cx,
))
.child(specimen("integer", NumberText::new(42_000.0), cx))
.child(specimen(
"negative",
NumberText::new(-1_234.5).decimals(1),
cx,
)),
)
}
pub fn currency(cx: &App) -> impl IntoElement + use<> {
section(
"CurrencyText",
"ISO 4217 codes. Yen and won have no minor units.",
cx,
)
.child(
specimens()
.child(specimen("USD", CurrencyText::new(1_234.5, "USD"), cx))
.child(specimen("EUR", CurrencyText::new(-42.0, "EUR"), cx))
.child(specimen("JPY", CurrencyText::new(1_500.4, "JPY"), cx))
.child(specimen("CHF", CurrencyText::new(9.99, "CHF"), cx)),
)
}
pub fn percent(cx: &App) -> impl IntoElement + use<> {
section(
"PercentText",
"Signed values carry direction in word and color.",
cx,
)
.child(
specimens()
.child(specimen("plain", PercentText::new(0.1234).decimals(2), cx))
.child(specimen("gain", PercentText::new(0.052).signed(), cx))
.child(specimen("loss", PercentText::new(-0.031).signed(), cx)),
)
}
pub fn file_size(cx: &App) -> impl IntoElement + use<> {
section(
"FileSizeText",
"Decimal units by default, binary on request.",
cx,
)
.child(
specimens()
.child(specimen("1536 B", FileSizeText::new(1_536), cx))
.child(specimen("3.2 GB", FileSizeText::new(3_200_000_000), cx))
.child(specimen("binary", FileSizeText::new(3 << 30).binary(), cx)),
)
}
pub fn duration(cx: &App) -> impl IntoElement + use<> {
section(
"DurationText",
"A clock for media, a compact span for logs.",
cx,
)
.child(
specimens()
.child(specimen(
"clock",
DurationText::new(5_025, DurationStyle::Clock),
cx,
))
.child(specimen(
"clock",
DurationText::new(1_425, DurationStyle::Clock),
cx,
))
.child(specimen(
"compact",
DurationText::new(245, DurationStyle::Compact),
cx,
)),
)
}
pub fn plural(cx: &App) -> impl IntoElement + use<> {
section("PluralText", "The noun follows the count.", cx).child(
specimens()
.child(specimen("1", PluralText::new(1, "file", "files"), cx))
.child(specimen(
"12345",
PluralText::new(12_345, "file", "files"),
cx,
)),
)
}
pub fn copyable(cx: &App) -> impl IntoElement + use<> {
section(
"CopyableText",
"One click copies. A check confirms, then leaves.",
cx,
)
.child(probe(
"copy-token",
CopyableText::new("token", "ely_live_4f9a2c81d7").mono(),
))
}
pub fn selectable(cx: &App) -> impl IntoElement + use<> {
section(
"SelectableText",
"Drag to select. ⌘C copies, ⌘A takes it all.",
cx,
)
.child(probe(
"select-text",
div().max_w(px(520.0)).child(SelectableText::new(
"selectable",
"Selection works on read-only text too: logs, messages, receipts.",
)),
))
}
pub fn emoji(cx: &App) -> impl IntoElement + use<> {
let glyph = |code: &str| Emoji::shortcode(code).expect("known shortcode");
section("Emoji", "Shortcodes resolve to the system color font.", cx).child(
specimens()
.child(specimen("sparkles", glyph("sparkles"), cx))
.child(specimen("herb", glyph("herb"), cx))
.child(specimen("coffee", glyph("coffee"), cx))
.child(specimen("xl", glyph("sparkles").size(TextSize::Xxl), cx)),
)
}
pub fn latex(cx: &App) -> impl IntoElement + use<> {
let math = |source: &str| Latex::parse(source).expect("gallery formula parses");
section(
"Latex / MathInline",
"A TeX subset: scripts, fractions, roots, Greek.",
cx,
)
.child(
specimens()
.gap_10()
.child(specimen("euler", math(r"e^{i\pi} + 1 = 0"), cx))
.child(specimen(
"sum",
math(r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
cx,
))
.child(specimen(
"root",
math(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
cx,
)),
)
.child(code("Latex::parse(r\"e^{i\\pi} + 1 = 0\")?", cx))
}