T45 Website: native captures stand in where a browser cannot run, an OG image, no web notes

This commit is contained in:
ZacharyZhang-NY committed 2026-09-30 00:03:23 -04:00
1 parent 587e08b5cb
commit 327bdf4b7f
68 files changed
+315 -155

No files matched your search

+2 -2
View File
@@ -26,7 +26,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `cargo run --example gallery -- --narrow 280 --capture <dir>` lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card.
- `RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown` checks the web build; gpui_web's `wasm_thread` needs the bootstrap, as Zed's web examples do.
- `ELY_GALLERY_ASSETS=<address of out/assets/> scripts/web.sh <out>` builds the gallery for the browser, profile `web`, and fails past Cloudflare's 25 MiB. It opens at `?page=<slug>&story=<title or slug>&theme=light|dark`; the host page calls `gallery.setTheme(mode)` or posts `{ ely: "theme", theme }`, before the wasm runs or after, and the page posts `{ ely: "ready" }` to its parent once it runs.
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`. `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one.
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32.
- `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
@@ -180,7 +180,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- gpui clears `debug_bounds` each frame: a test sees an element leave as well as appear.
- Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery.
- What cannot build for wasm32 is absent there through `cfg(not(target_family = "wasm"))`, never faked: the live terminal, `Terminal` and `Launch` with their grid, keys and search, and the gallery's capture harness.
- A gallery demo a browser cannot run says why on the web through `ui::web_note`, in the blocked style: a second window (`own_window`), files from the dialog or a drop (`NO_FILES`), the menu bar, window moves.
- What a browser cannot run draws natively alone: `ui::native_only` leaves a window's opener out on the web, and the web's menu bar keeps View alone. The site shows native captures instead, light and dark: a story whose demo needs the desktop (files, the terminal's shell, the tray, the Dock, window moves) as its capture, and a live story that opens windows with those windows below. `python3 scripts/shots.py` takes them with the native gallery, window in front, into `frontend/public/shots` and `frontend/src/shots.json`.
- A story is a gallery section: its title is a literal, one per page. Under `ui::Story` the one asked for draws without its title; the rest drop their demos (`ui::Section`), so nothing of theirs lays out, loads or moves. Their builders still run, so a demo that starts work on its own, a clock, a load or a shell, starts it through `ui::live`, which a story grants its own section alone and fails on a title no section has. A story the page lacks fails in the page.
- Embedded in a page, the web gallery keeps gpui_web's hidden text field from taking focus, and hands focus back to the host, until a press or a key reaches it (`examples/gallery/index.html`); gpui_web focuses that field whenever its window is active. The host shows its own state until `{ ely: "ready" }`, sends the mode on ready and on every turn, and puts a skip link before the frame, since Tab inside gpui cycles within it.
- The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there.
+3
View File
@@ -3,6 +3,8 @@
A component library for [GPUI](https://www.gpui.rs), the Rust UI framework behind Zed.
Every component ships in light and dark. The palette is warm and quiet. Color carries meaning, not decoration.
Every component runs live at https://ely-site.zhangyanghaha0407.workers.dev, compiled to WebAssembly.
Status: early. `TASKS.md` tracks every component, chapter by chapter.
## Use
@@ -46,6 +48,7 @@ cd frontend
pnpm install
ELY_GALLERY_ASSETS=https://<site>/gallery/assets/ pnpm gallery # the wasm gallery, into public/gallery
pnpm dev # or: pnpm build && pnpm preview
node scripts/og.mjs # with pnpm preview up: public/og.png from the hero
pnpm deploy # Cloudflare, through wrangler
```
+22 -19
View File
@@ -53,25 +53,28 @@ actions!(gallery, [Quit, About, LightMode, DarkMode, NewWindow]);
/// The gallery's menu bar, drawn natively and by MenuBar.
pub fn app_menus() -> Vec<Menu> {
vec![
Menu {
name: "Ely".into(),
items: vec![
MenuItem::action("About Ely", About),
MenuItem::separator(),
MenuItem::action("Quit Ely", Quit),
],
disabled: false,
},
Menu {
name: "View".into(),
items: vec![
MenuItem::action("Light", LightMode),
MenuItem::action("Dark", DarkMode),
],
disabled: false,
},
]
let view = Menu {
name: "View".into(),
items: vec![
MenuItem::action("Light", LightMode),
MenuItem::action("Dark", DarkMode),
],
disabled: false,
};
// A page opens no second window and cannot quit.
if cfg!(target_family = "wasm") {
return vec![view];
}
let ely = Menu {
name: "Ely".into(),
items: vec![
MenuItem::action("About Ely", About),
MenuItem::separator(),
MenuItem::action("Quit Ely", Quit),
],
disabled: false,
};
vec![ely, view]
}
/// Menu bar and Dock menu, set before the window opens.
+1 -2
View File
@@ -12,7 +12,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p
use crate::{
probe::probe,
ui::{NO_FILES, change, keep, section, web_note},
ui::{change, keep, section},
};
/// The switchers' demo: the account and workspace in use.
@@ -115,7 +115,6 @@ pub fn profile(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Someone at a glance, and their profile to edit: Save waits for a change, a name and an email that reads; Discard puts the saved profile back.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(
div()
.flex()
+1 -2
View File
@@ -15,7 +15,7 @@ use web_time::Instant;
use crate::{
probe::probe,
ui::{NO_FILES, keep, noise, section, set, web_note},
ui::{keep, noise, section, set},
};
const SYSTEM: &str = "You answer about color in plain words. Cite sources.";
@@ -160,7 +160,6 @@ pub fn composer(
"Where a message is written: attachments and context above the field, tools below, send at the end; Enter sends and Shift-Enter breaks the line. A slash at the start offers commands, an at sign offers context, and files dropped on it attach.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(probe(
"chat-composer",
div()
+1 -2
View File
@@ -15,7 +15,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div};
use crate::{
probe::probe,
ui::{NO_FILES, keep, picture, row, section, set, specimen, specimens, web_note},
ui::{keep, picture, row, section, set, specimen, specimens},
};
const DUNES: &str = asset!("dunes-square.jpg");
@@ -119,7 +119,6 @@ pub fn avatars(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"A picture, or initials on a tone the name keeps, or an icon. Presence sits on the corner. Hover the last one, then press it to choose a picture, or drop one on it.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(specimens().children(sizes.map(|(size, name)| {
specimen(name, Avatar::new(SharedString::from(format!("avatar-{name}")), "Kofi Mensah").size(size), cx)
})))
+1 -2
View File
@@ -10,7 +10,7 @@ use jiff::Timestamp;
use crate::{
probe::probe,
ui::{NO_FILES, change, keep, section, web_note},
ui::{change, keep, section},
};
pub fn context_menu(cx: &mut App) -> impl IntoElement + use<> {
@@ -84,7 +84,6 @@ pub fn drop_files(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
"Files dropped in, as forms::DropZone takes them; each lands in the list below.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(probe(
"files-drop",
div()
+1 -2
View File
@@ -5,7 +5,7 @@ use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{NO_FILES, keep, section, set, specimen, specimens, web_note},
ui::{keep, section, set, specimen, specimens},
};
fn names(paths: &[PathBuf]) -> String {
@@ -44,7 +44,6 @@ pub fn files(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Click a field or Browse for the system's dialog, or drop files from Finder. A folder, or two files on a one-file target, turns it red.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(
specimens()
.child(specimen(
+1 -2
View File
@@ -10,7 +10,7 @@ use gpui::{App, IntoElement, Keystroke, ParentElement, SharedString, Styled, Win
use super::text::field;
use crate::{
probe::probe,
ui::{NO_FILES, code, section, web_note},
ui::{code, section},
};
pub fn mention(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
@@ -99,7 +99,6 @@ pub fn path(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Type a path, or Browse with the system's dialog.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(probe(
"path",
div()
+1 -5
View File
@@ -9,7 +9,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p
use crate::{
probe::probe,
ui::{keep, picture, section, set, web_note},
ui::{keep, picture, section, set},
};
const ATRIUM: &str = asset!("atrium.jpg");
@@ -106,10 +106,6 @@ pub fn upload(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"A picture to send: drop one or browse for it, then crop it. Replace picks another; Remove empties it.",
cx,
)
.children(web_note(
"A browser hands gpui no files: on the web the file dialog fails, drops never arrive, and the picture this starts with, a file on disk, cannot be read.",
cx,
))
.child(probe(
"media-upload",
div().w(px(420.)).child(
+1 -5
View File
@@ -9,7 +9,7 @@ use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use super::rows::{ran, run};
use crate::{
probe::probe,
ui::{section, specimen, web_note},
ui::{section, specimen},
};
pub fn menu_bar(cx: &mut App) -> impl IntoElement + use<> {
@@ -26,10 +26,6 @@ pub fn menu_bar(cx: &mut App) -> impl IntoElement + use<> {
"The app's own menus, the ones cx.set_menus gave the system, drawn in the window. Picking a row dispatches its action.",
cx,
)
.children(web_note(
"A browser page has no menu bar, and gpui on the web keeps no cx.set_menus: this bar draws the gallery's own list of the same menus. About Ely would open a second window and Quit would end the app, neither of which a page can, so both only log.",
cx,
))
.child(specimen(
"hover moves between menus while one is open",
probe("menubar", MenuBar::new("app-bar", menus)),
@@ -8,7 +8,7 @@ use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Window};
use crate::{
probe::{Opened, probe},
ui::{keep, own_window, row, section, set, specimen},
ui::{keep, native_only, row, section, set, specimen},
};
/// Moves `value` to the front of a recent list, keeping `size` entries.
@@ -396,9 +396,9 @@ pub fn search_palette(window: &mut Window, cx: &mut App) -> impl IntoElement + u
),
cx,
))
.child(specimen(
.child(native_only(specimen(
"a window above all others",
own_window(probe(
probe(
"launcher",
Button::new("launcher-button", "Open the launcher")
.variant(ButtonVariant::Ghost)
@@ -415,9 +415,9 @@ pub fn search_palette(window: &mut Window, cx: &mut App) -> impl IntoElement + u
Err(error) => log::error!("gallery: launcher: {error:#}"),
}
}),
), cx),
),
cx,
)),
))),
)
.children(palette)
}
+1 -2
View File
@@ -11,7 +11,7 @@ use ely_gpui_component::{
use gpui::{AnyElement, App, Entity, IntoElement, ParentElement, Styled, div, px};
use super::window::{Fields, Prefs, SIZE};
use crate::ui::{NO_FILES, change, web_note};
use crate::ui::change;
/// The open section's page.
pub(super) fn page(prefs: &Entity<Prefs>, fields: &Fields, section: &str, cx: &App) -> AnyElement {
@@ -198,7 +198,6 @@ fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
}),
),
)
.children(web_note(NO_FILES, cx))
.child(
SettingsSection::new("Defaults")
.description("A larger font size shows here.")
+1 -17
View File
@@ -7,7 +7,7 @@ use ely_gpui_component::{
};
use gpui::{App, Div, IntoElement, ParentElement, Styled, Window, div, px};
use crate::ui::{code, row, section, specimen, specimens, web_note};
use crate::ui::{code, row, section, specimen, specimens};
/// A window drawn inside the page.
pub fn window_frame(width: f32, height: f32, cx: &App) -> Div {
@@ -116,10 +116,6 @@ pub fn window_controls(cx: &App) -> impl IntoElement + use<> {
"Minimize and zoom act on this window. Close here only logs.",
cx,
))
.children(web_note(
"A browser owns the page's window: gpui on the web only logs minimize and zoom.",
cx,
))
}
pub fn drag_area(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
@@ -146,10 +142,6 @@ pub fn drag_area(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
.child("Double-click to zoom this window"),
)
.child(code("drag_region(id, window, cx)", cx))
.children(web_note(
"A browser owns the page's window: gpui on the web neither moves nor zooms it.",
cx,
))
}
pub fn resize_border(cx: &App) -> impl IntoElement + use<> {
@@ -162,10 +154,6 @@ pub fn resize_border(cx: &App) -> impl IntoElement + use<> {
"Linux draws its own edges. This macOS window keeps the system's, so the border lays out only.",
))
.child(code("ResizeBorder::new().child(app)", cx))
.children(web_note(
"A browser sizes the page's canvas itself: gpui on the web resizes no window from its edges.",
cx,
))
}
pub fn menus(cx: &App) -> impl IntoElement + use<> {
@@ -189,8 +177,4 @@ pub fn menus(cx: &App) -> impl IntoElement + use<> {
.child(Caption::new(
"MenuBar draws the same menus inside the window. It lands with Menus, chapter 8.",
))
.children(web_note(
"A browser page has no menu bar and no Dock: gpui on the web keeps neither cx.set_menus nor cx.set_dock_menu.",
cx,
))
}
+19 -25
View File
@@ -11,7 +11,7 @@ use gpui::{App, IntoElement, ParentElement, Styled, Window, div};
use super::chrome::window_frame;
use crate::{
probe::{Opened, probe},
ui::{own_window, row, section, specimen},
ui::{native_only, row, section, specimen},
};
fn log_action(name: &'static str) -> impl Fn(&mut Window, &mut App) + 'static {
@@ -59,18 +59,15 @@ pub fn update_window(cx: &App) -> impl IntoElement + use<> {
.child(update_dialog().on_later(log_action("later"))),
cx,
))
.child(own_window(
probe(
"update-open",
Button::new("update-open", "Open update window")
.variant(ButtonVariant::Outline)
.on_click(|_, _, cx| match update_dialog().open(cx) {
Ok(handle) => Opened::insert("update", handle.into(), cx),
Err(error) => log::error!("gallery: update window failed: {error:#}"),
}),
),
cx,
))
.child(native_only(probe(
"update-open",
Button::new("update-open", "Open update window")
.variant(ButtonVariant::Outline)
.on_click(|_, _, cx| match update_dialog().open(cx) {
Ok(handle) => Opened::insert("update", handle.into(), cx),
Err(error) => log::error!("gallery: update window failed: {error:#}"),
}),
)))
}
const REPORT: &str = "Exception Type: EXC_BAD_ACCESS (SIGSEGV)
@@ -104,18 +101,15 @@ pub fn crash_reporter(cx: &App) -> impl IntoElement + use<> {
.child(reporter(true)),
cx,
))
.child(own_window(
probe(
"crash-open",
Button::new("crash-open", "Open crash window")
.variant(ButtonVariant::Outline)
.on_click(move |_, _, cx| match reporter(false).open(cx) {
Ok(handle) => Opened::insert("crash", handle.into(), cx),
Err(error) => log::error!("gallery: crash window failed: {error:#}"),
}),
),
cx,
))
.child(native_only(probe(
"crash-open",
Button::new("crash-open", "Open crash window")
.variant(ButtonVariant::Outline)
.on_click(move |_, _, cx| match reporter(false).open(cx) {
Ok(handle) => Opened::insert("crash", handle.into(), cx),
Err(error) => log::error!("gallery: crash window failed: {error:#}"),
}),
)))
}
pub fn offline(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
+1 -9
View File
@@ -7,7 +7,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Window};
use crate::{
probe::probe,
ui::{blocked, row, section, web_note},
ui::{blocked, row, section},
};
const ITEMS: [&str; 3] = ["Open gallery", "Toggle theme", "Quit"];
@@ -23,10 +23,6 @@ pub fn tray(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"An SF Symbol in the menu bar with a menu. macOS only.",
cx,
)
.children(web_note(
"A browser page has no menu bar to hold an icon, so showing one fails and logs why.",
cx,
))
.child(
row()
.child(probe(
@@ -117,10 +113,6 @@ pub fn dock(cx: &mut App) -> impl IntoElement + use<> {
"A count on the app's Dock icon. macOS only.",
cx,
)
.children(web_note(
"A browser page has no Dock icon to badge, so setting one fails and logs why.",
cx,
))
.child(
row()
.child(probe(
+18 -16
View File
@@ -15,7 +15,7 @@ use gpui::{
use super::chrome::window_frame;
use crate::{
probe::{Opened, probe},
ui::{code, own_window, picture, row, section, specimen},
ui::{code, native_only, picture, row, section, specimen},
};
const DUNES: &str = asset!("dunes.jpg");
@@ -80,13 +80,14 @@ pub fn splash_screen(cx: &App) -> impl IntoElement + use<> {
.child(splash().progress(Some(0.62))),
cx,
))
.child(own_window(
trigger("splash-open", "Show splash window", |cx| {
.child(native_only(trigger(
"splash-open",
"Show splash window",
|cx| {
let result = splash().open(cx);
opened("splash", result, cx)
}),
cx,
))
},
)))
}
pub fn open_about(cx: &mut App) {
@@ -108,10 +109,11 @@ pub fn about_dialog(cx: &App) -> impl IntoElement + use<> {
.child(about()),
cx,
))
.child(own_window(
trigger("about-open", "Open About window", open_about),
cx,
))
.child(native_only(trigger(
"about-open",
"Open About window",
open_about,
)))
}
struct Managed {
@@ -192,10 +194,11 @@ pub fn window_manager(cx: &mut App) -> impl IntoElement + use<> {
"Each new window cascades from the last; the manager knows them by name.",
cx,
)
.child(own_window(
trigger("managed-open", "New window", open_managed),
cx,
))
.child(native_only(trigger(
"managed-open",
"New window",
open_managed,
)))
.child(
row().children(open.into_iter().enumerate().map(|(ix, (handle, title))| {
Button::new(("managed-focus", ix), title)
@@ -335,7 +338,7 @@ pub fn mini_windows(cx: &App) -> impl IntoElement + use<> {
"Small windows above the rest. Hover for the bar; expand returns to the app.",
cx,
)
.child(own_window(
.child(native_only(
row()
.child(trigger("mini-open", "Mini player", |cx| {
let view = cx.new(|_| Player { playing: true });
@@ -359,7 +362,6 @@ pub fn mini_windows(cx: &App) -> impl IntoElement + use<> {
);
opened("pip", result, cx)
})),
cx,
))
.child(code(
"AlwaysOnTop is gpui's WindowOptions { kind: WindowKind::PopUp, .. }",
+2 -5
View File
@@ -37,7 +37,7 @@ const SCRIPT: &[Step] = &[
Step::Rest,
];
/// A live shell natively; on the web, why not.
/// A live shell natively; on the web, its header alone.
fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let section = section(
"Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink",
@@ -56,10 +56,7 @@ fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
#[cfg(target_family = "wasm")]
{
let _ = window;
section.child(crate::ui::blocked(
"A browser starts no processes and has no pseudo-terminal, so no shell runs here, and alacritty's grid, which draws the build's output, does not build for the web. The page's other stories, command blocks, colored output, logs and processes, draw without one.",
cx,
))
section
}
}
+1 -2
View File
@@ -4,7 +4,7 @@ use ely_gpui_component::{
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{NO_FILES, change, keep, section, specimen, specimens, web_note};
use crate::ui::{change, keep, section, specimen, specimens};
/// Which code palette the demo last chose.
struct Chosen(SharedString);
@@ -17,7 +17,6 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
"A VS Code color theme dropped here reads into Ely's palette, the colors it leaves out kept from Ely's; Apply sets it on the whole gallery. Code palettes choose the colors of code for the mode shown.",
cx,
)
.children(web_note(NO_FILES, cx))
.child(
specimens()
.child(specimen(
+3 -7
View File
@@ -36,19 +36,15 @@ pub fn platform(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
specimens()
.child(specimen("Platform", platforms(cx), cx))
.child(specimen("IconTheme", icons(window, cx), cx))
.child(specimen(
.child(native_only(specimen(
"Vibrancy",
native_only(
probe(
probe(
"frosted-open",
Button::new("frosted-open", "Open a frosted window")
.on_click(|_, _, cx| frosted(cx)),
),
"A browser page is a single window, and gpui leaves what lies behind it to the browser, so no frosted window opens and nothing blurs through.",
cx,
),
cx,
)),
))),
)
}
+6 -22
View File
@@ -208,31 +208,15 @@ pub const UNREAD: &str = if cfg!(target_family = "wasm") {
"gpui builds an AccessKit tree from elements with an id and a role, and no screen reader here reads it"
};
/// Why a demo takes no files on the web.
pub const NO_FILES: &str = "A browser hands gpui no files: on the web the file dialog fails and dropped files never arrive, so nothing is taken here.";
/// On the web, what a browser cannot do.
pub fn web_note(text: &'static str, cx: &App) -> Option<AnyElement> {
cfg!(target_family = "wasm").then(|| blocked(text, cx).into_any_element())
}
/// A demo browsers cannot run; there, its note instead.
pub fn native_only(element: impl IntoElement, note: &'static str, cx: &App) -> AnyElement {
match web_note(note, cx) {
Some(note) => note,
None => element.into_any_element(),
/// A demo browsers cannot run draws natively alone; the site shows its capture.
pub fn native_only(element: impl IntoElement) -> AnyElement {
if cfg!(target_family = "wasm") {
div().into_any_element()
} else {
element.into_any_element()
}
}
/// A button opening its own window, which browsers cannot.
pub fn own_window(button: impl IntoElement, cx: &App) -> AnyElement {
native_only(
button,
"A browser page is a single window: gpui opens no second window or popup on the web, so this one cannot open here.",
cx,
)
}
/// A demo's state, kept across frames under `key`.
pub fn keep<T: 'static>(
key: &'static str,
+19 -1
View File
@@ -13,7 +13,8 @@ test("the home page shows the pitch, the scene and the credits", async ({ page }
const errors = watch(page);
await page.goto("/");
await expect(page.getByRole("heading", { level: 1 })).toContainText("component library for GPUI");
await expect(page.locator(".hero canvas")).toBeVisible();
await expect(page.locator("canvas.iso")).toBeAttached();
await expect(page.locator(".hero-iso")).toBeVisible();
await expect(page.locator("#foot")).toContainText("Solar Icons by 480 Design");
expect(errors).toEqual([]);
});
@@ -83,3 +84,20 @@ test("an unknown chapter says so", async ({ page }) => {
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Not found");
expect(errors.join()).toContain("No chapter");
});
test("a story the web cannot run shows its native capture, in the site's mode", async ({ page }) => {
await page.emulateMedia({ colorScheme: "light" });
await page.goto("/components/forms/pathinput/");
const shot = page.locator("img.shot");
await expect(shot).toHaveAttribute("srcset", /pathinput-light\.jpg/);
await expect(page.locator(".stage iframe")).toHaveCount(0);
await page.locator("[data-mode]").click();
await expect(shot).toHaveAttribute("srcset", /pathinput-dark\.jpg/);
await expect.poll(() => shot.evaluate((img: HTMLImageElement) => img.naturalWidth)).toBeGreaterThan(0);
});
test("a live story that opens a window shows that window natively", async ({ page }) => {
await page.goto("/components/shell/aboutdialog/");
await expect(page.locator(".stage iframe")).toHaveCount(1);
await expect(page.locator(".native img.shot")).toHaveAttribute("srcset", /shell-about-window/);
});
+8
View File
@@ -5,6 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ely · GPUI components</title>
<meta name="description" content="A component library for GPUI, in light and dark. Every component runs live in the browser." />
<meta property="og:type" content="website" />
<meta property="og:title" content="Ely · GPUI components" />
<meta property="og:description" content="A component library for GPUI. Every part runs live in the browser, compiled from Rust to WebAssembly." />
<meta property="og:image" content="https://ely-site.zhangyanghaha0407.workers.dev/og.png" />
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://ely-site.zhangyanghaha0407.workers.dev/og.png" />
<script>
// The saved mode, or the system's, before first paint.
try {
Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+17
View File
@@ -0,0 +1,17 @@
// Renders public/og.png from the hero; needs pnpm preview.
import { chromium } from "@playwright/test";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 2, colorScheme: "light", reducedMotion: "reduce" });
await page.goto(process.env.SITE ?? "http://localhost:4173/");
await page.addStyleTag({
content: ".bar, .cta { display: none } .hero { min-height: 630px; padding: 56px } .words { zoom: 1.5 } .og { font-weight: 600; margin-bottom: 16px }",
});
await page.evaluate(() => {
document.querySelector(".glyphs").insertAdjacentHTML("afterend", '<div class="og">Ely</div>');
document.querySelector(".hero p").textContent = "Every component live in the browser, compiled from Rust.";
});
await page.waitForTimeout(1500);
await page.screenshot({ path: "public/og.png", clip: { x: 0, y: 0, width: 1200, height: 630 } });
await browser.close();
console.log("og: wrote public/og.png");
+7 -1
View File
@@ -1,5 +1,6 @@
import { all, chapter, chapters, esc, href, storyCount, type Chapter, type Story } from "./data";
import { icon } from "./icons";
import { isCaptured, native } from "./native";
import { preview } from "./preview";
/** Chapters on the left, a story or the index beside. */
@@ -22,7 +23,11 @@ export function components(main: HTMLElement, path: string[]): () => void {
}
document.title = `${story.title} · ${current.title} · Ely`;
body.innerHTML = detail(current, story);
return preview(body.querySelector<HTMLElement>(".live")!, current.slug, story.slug, story.title);
const live = body.querySelector<HTMLElement>(".live")!;
const shots = body.querySelector<HTMLElement>(".native")!;
if (isCaptured(current.slug, story.slug)) return native(live, current.slug, story.slug, story.title);
const [stopLive, stopShots] = [preview(live, current.slug, story.slug, story.title), native(shots, current.slug, story.slug, story.title)];
return () => (stopLive(), stopShots());
}
function sidebar(side: HTMLElement, current?: Chapter, story?: Story): void {
@@ -66,6 +71,7 @@ function detail(current: Chapter, story: Story): string {
<h1>${esc(story.title)}</h1>
<p class="names">${story.components.map(esc).join(" · ")}</p>
<div class="live"></div>
<div class="native"></div>
<nav class="pager" aria-label="More stories">${pager(prev, "prev", "Previous", icon("ArrowLeft"))}${pager(next, "next", "Next", icon("ArrowRight"))}</nav>
</article>`;
}
+33
View File
@@ -0,0 +1,33 @@
import shots from "./shots.json";
import { esc } from "./data";
import { mode, onMode, type Mode } from "./theme";
const captured = new Set<string>(shots.stories);
const windows: Record<string, string[]> = shots.windows;
/** Whether the site shows this story as a native capture. */
export function isCaptured(page: string, story: string): boolean {
return captured.has(`${page}/${story}`);
}
const src = (name: string, theme: Mode) => `/shots/${name}-${theme}.jpg`;
function figure(name: string, alt: string): string {
// Captures are at twice the pixels, drawn at their size.
return `<img class="shot" data-shot="${name}" srcset="${src(name, mode())} 2x" alt="${esc(alt)}" loading="lazy" />`;
}
/** Draws the story's capture, or the windows it opens, in the site's mode. */
export function native(host: HTMLElement, page: string, story: string, title: string): () => void {
const key = `${page}/${story}`;
if (captured.has(key)) {
host.innerHTML = `<p class="label">Native capture · macOS</p>${figure(`${page}--${story}`, `${title}, captured from the native gallery`)}`;
} else if (windows[key]) {
host.innerHTML = `<p class="label">The windows it opens, natively</p><div class="shots">${windows[key]
.map((name) => figure(name, `A window ${title} opens, captured natively`))
.join("")}</div>`;
} else {
return () => {};
}
return onMode((next) => host.querySelectorAll<HTMLImageElement>("img[data-shot]").forEach((img) => (img.srcset = `${src(img.dataset.shot!, next)} 2x`)));
}
+41
View File
@@ -0,0 +1,41 @@
{
"stories": [
"files/dragdropfiles",
"forms/fileinput-dropzone",
"forms/pathinput",
"media/imageupload",
"shell/dockbadge",
"shell/trayicon-traymenu",
"shell/windowdragregion",
"shell/windowresizeborder",
"terminal/terminal-terminaltabs-terminalsplit-terminaltoolbar-shellselector-terminalsearch-terminallink",
"theme/themeimporter-syntaxthemes"
],
"windows": {
"shell/splashscreen": [
"shell-splash-window"
],
"shell/aboutdialog": [
"shell-about-window"
],
"shell/multiwindow-manager": [
"shell-managed-window"
],
"shell/miniwindow-compactmode-pictureinpicture": [
"shell-mini-window",
"shell-pip-window"
],
"shell/updatedialog": [
"shell-update-window"
],
"shell/crashreporter": [
"shell-crash-window"
],
"navigation/searchpalette-spotlightsearch-quicklauncher": [
"navigation-launcher"
],
"theme/platform-style-iconthemeprovider-vibrancybackground": [
"theme-frosted-window"
]
}
}
+4
View File
@@ -126,6 +126,10 @@ canvas.iso { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-eve
.stage-skip { position: absolute; left: -9999px; }
.stage-skip:focus { position: static; display: inline-block; margin-bottom: 8px; font-size: 14px; color: var(--accent); }
img.shot { display: block; max-width: 100%; height: auto; margin-top: 12px; }
.native .label { margin-top: 48px; }
.shots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; }
.shots img.shot { max-width: min(100%, 560px); }
.pager { display: flex; justify-content: space-between; gap: 16px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--line); }
.pager a { display: flex; flex-direction: column; gap: 4px; max-width: 48%; font-size: 14px; line-height: 20px; }
.pager a:hover { color: var(--accent); }
+94
View File
@@ -0,0 +1,94 @@
"""Writes frontend/public/shots: native captures a browser cannot make."""
import json
import subprocess
import sys
import tempfile
from pathlib import Path
from PIL import Image, ImageChops
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "frontend/public/shots"
GROUND = {"light": (252, 250, 247, 255), "dark": (19, 17, 16, 255)}
# Stories shown as a capture: their demo needs the desktop.
STORIES = [
("terminal", "terminal-terminaltabs-terminalsplit-terminaltoolbar-shellselector-terminalsearch-terminallink"),
("forms", "fileinput-dropzone"),
("forms", "pathinput"),
("files", "dragdropfiles"),
("theme", "themeimporter-syntaxthemes"),
("media", "imageupload"),
("shell", "trayicon-traymenu"),
("shell", "dockbadge"),
("shell", "windowdragregion"),
("shell", "windowresizeborder"),
]
# Live stories, and the windows their page scripts open.
WINDOWS = {
"shell/splashscreen": ["shell-splash-window"],
"shell/aboutdialog": ["shell-about-window"],
"shell/multiwindow-manager": ["shell-managed-window"],
"shell/miniwindow-compactmode-pictureinpicture": ["shell-mini-window", "shell-pip-window"],
"shell/updatedialog": ["shell-update-window"],
"shell/crashreporter": ["shell-crash-window"],
"navigation/searchpalette-spotlightsearch-quicklauncher": ["navigation-launcher"],
"theme/platform-style-iconthemeprovider-vibrancybackground": ["theme-frosted-window"],
}
def capture(args, into):
"""Runs the native capture; its window must stay in front."""
subprocess.run(["cargo", "run", "-q", "--example", "gallery", "--", *args, "--capture", str(into)], cwd=ROOT, check=True)
def trimmed(path):
"""The shot cut to its content, a margin around it."""
im = Image.open(path).convert("RGB")
# The title bar and the window's rounded edge stay out.
edge, bar = 16, 112
inner = im.crop((edge, bar, im.width - edge, im.height - edge))
ground = Image.new("RGB", inner.size, inner.getpixel((inner.width // 2, inner.height - 8)))
box = ImageChops.difference(inner, ground).convert("L").point(lambda v: 255 if v > 24 else 0).getbbox()
if box is None:
sys.exit(f"shots.py: {path} is blank")
box = (box[0] + edge, box[1] + bar, box[2] + edge, box[3] + bar)
pad = 48
return im.crop((max(box[0] - pad, 0), max(box[1] - pad, 0), min(box[2] + pad, im.width), min(box[3] + pad, im.height)))
def grounded(path, mode):
"""A window's capture over the site's ground, its corners filled."""
window = Image.open(path).convert("RGBA")
ground = Image.new("RGBA", window.size, GROUND[mode])
return Image.alpha_composite(ground, window).convert("RGB")
def save(im, name):
im.save(OUT / f"{name}.jpg", quality=88, optimize=True)
print(f"shots.py: {name}.jpg {im.width}x{im.height}")
def main():
OUT.mkdir(parents=True, exist_ok=True)
manifest = {"stories": [], "windows": WINDOWS}
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
for page, story in STORIES:
into = tmp / f"{page}--{story}"
capture(["--page", page, "--story", story], into)
for mode in ("light", "dark"):
save(trimmed(into / f"{page}-{mode}.png"), f"{page}--{story}-{mode}")
manifest["stories"].append(f"{page}/{story}")
for page in sorted({key.split("/")[0] for key in WINDOWS}):
into = tmp / page
capture(["--page", page], into)
for name in (n for key, names in WINDOWS.items() if key.startswith(page + "/") for n in names):
for mode in ("light", "dark"):
save(grounded(into / f"{name}-{mode}.png", mode), f"{name}-{mode}")
(ROOT / "frontend/src/shots.json").write_text(json.dumps(manifest, indent=1) + "\n")
main()