From 48c7b09d8d7999443f68db0dfd0fb422a4d580da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 29 Sep 2026 19:16:41 -0400 Subject: [PATCH] =?UTF-8?q?T44d=20Web=20=C2=B7=20Gallery=20builds=20for=20?= =?UTF-8?q?wasm32:=20one=20story=20per=20section=20by=20URL,=20themed=20by?= =?UTF-8?q?=20the=20host?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gallery's entry runs natively or, on wasm32, from a wasm_bindgen start through gpui_web, single-threaded with WebGL2 fallback. ?page, ?story and ?theme choose what shows; an unknown one fails in the page and the log. set_theme switches the mode later. Pictures load from ELY_GALLERY_ASSETS. scripts/stories.py writes examples/gallery/stories.json from the pages. --- Cargo.lock | 4 + Cargo.toml | 13 ++ examples/gallery/index.html | 32 +++ examples/gallery/main.rs | 157 ++++++++++----- examples/gallery/pages/account/mod.rs | 2 +- examples/gallery/pages/agent/mod.rs | 2 +- examples/gallery/pages/buttons/mod.rs | 2 +- examples/gallery/pages/calendar/mod.rs | 2 +- examples/gallery/pages/canvas/mod.rs | 2 +- examples/gallery/pages/charts/mod.rs | 2 +- examples/gallery/pages/chat/compose.rs | 3 +- examples/gallery/pages/chat/controls.rs | 3 +- examples/gallery/pages/chat/mod.rs | 2 +- examples/gallery/pages/collab/mod.rs | 2 +- examples/gallery/pages/dashboard/mod.rs | 2 +- examples/gallery/pages/data/media.rs | 10 +- examples/gallery/pages/data/mod.rs | 2 +- examples/gallery/pages/data/people.rs | 13 +- examples/gallery/pages/debug/mod.rs | 2 +- examples/gallery/pages/devtools/mod.rs | 2 +- examples/gallery/pages/documents/editing.rs | 5 +- examples/gallery/pages/documents/knowledge.rs | 15 +- examples/gallery/pages/documents/mod.rs | 2 +- examples/gallery/pages/documents/reading.rs | 22 +-- examples/gallery/pages/editor/mod.rs | 2 +- examples/gallery/pages/feedback/mod.rs | 2 +- examples/gallery/pages/feedback/states.rs | 3 +- examples/gallery/pages/files/find.rs | 13 +- examples/gallery/pages/files/mod.rs | 2 +- examples/gallery/pages/finance/assets.rs | 3 +- examples/gallery/pages/finance/mod.rs | 2 +- examples/gallery/pages/forms/mod.rs | 2 +- examples/gallery/pages/generative/mod.rs | 2 +- examples/gallery/pages/generative/results.rs | 8 +- examples/gallery/pages/git/mod.rs | 2 +- examples/gallery/pages/i18n/mod.rs | 2 +- examples/gallery/pages/interaction/mod.rs | 2 +- examples/gallery/pages/layout/mod.rs | 2 +- examples/gallery/pages/lists/mod.rs | 2 +- examples/gallery/pages/mail/mod.rs | 2 +- examples/gallery/pages/maps/mod.rs | 10 +- examples/gallery/pages/media/audio.rs | 11 +- examples/gallery/pages/media/devices.rs | 11 +- examples/gallery/pages/media/edit.rs | 23 +-- examples/gallery/pages/media/images.rs | 55 +----- examples/gallery/pages/media/mod.rs | 2 +- examples/gallery/pages/media/video.rs | 23 +-- examples/gallery/pages/menus/mod.rs | 2 +- examples/gallery/pages/messaging/calls.rs | 11 +- examples/gallery/pages/messaging/mod.rs | 2 +- examples/gallery/pages/messaging/pickers.rs | 12 +- examples/gallery/pages/misc/codes.rs | 11 +- examples/gallery/pages/misc/mod.rs | 2 +- examples/gallery/pages/mod.rs | 3 +- examples/gallery/pages/motion/mod.rs | 2 +- examples/gallery/pages/navigation/mod.rs | 2 +- examples/gallery/pages/onboarding/mod.rs | 2 +- examples/gallery/pages/overlays/guides.rs | 24 +-- examples/gallery/pages/overlays/mod.rs | 2 +- examples/gallery/pages/primitives/mod.rs | 2 +- examples/gallery/pages/primitives/parts.rs | 9 +- examples/gallery/pages/project/mod.rs | 2 +- examples/gallery/pages/project/time.rs | 3 +- examples/gallery/pages/settings/mod.rs | 2 +- examples/gallery/pages/shell/mod.rs | 2 +- examples/gallery/pages/shell/windows.rs | 44 +++-- examples/gallery/pages/tables/mod.rs | 2 +- examples/gallery/pages/terminal/live.rs | 12 +- examples/gallery/pages/terminal/mod.rs | 24 ++- examples/gallery/pages/theme/mod.rs | 2 +- examples/gallery/pages/tooling/mod.rs | 2 +- examples/gallery/pages/typography/mod.rs | 2 +- examples/gallery/probe.rs | 3 + examples/gallery/script.rs | 43 +--- examples/gallery/shell.rs | 175 ++++++++++------ examples/gallery/step.rs | 41 ++++ examples/gallery/stories.json | 45 +++++ examples/gallery/ui.rs | 137 +++++++++++-- examples/gallery/web.rs | 115 +++++++++++ scripts/check.sh | 3 +- scripts/stories.py | 186 ++++++++++++++++++ src/theme/mod.rs | 10 + src/theme/tests.rs | 25 +++ 83 files changed, 1000 insertions(+), 455 deletions(-) create mode 100644 examples/gallery/index.html create mode 100644 examples/gallery/step.rs create mode 100644 examples/gallery/stories.json create mode 100644 examples/gallery/web.rs create mode 100644 scripts/stories.py diff --git a/Cargo.lock b/Cargo.lock index a1494f3..af3d242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1659,11 +1659,13 @@ dependencies = [ "futures", "gpui", "gpui_platform", + "gpui_web", "image", "iso_currency", "isocountry", "isolang", "jiff", + "js-sys", "log", "objc", "pulldown-cmark", @@ -1679,6 +1681,8 @@ dependencies = [ "smallvec", "unicode-segmentation", "vte", + "wasm-bindgen", + "web-sys", "web-time", "wry", ] diff --git a/Cargo.toml b/Cargo.toml index 01c9cfe..c801b67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,12 @@ rustix = { version = "1", default-features = false, features = ["std", "fs"] } [dev-dependencies] env_logger = "0.11" +[target.'cfg(target_family = "wasm")'.dev-dependencies] +gpui_web = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" } +js-sys = "0.3" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Location", "UrlSearchParams", "Window"] } + [target.'cfg(target_os = "macos")'.dev-dependencies] core-foundation = "0.10" core-graphics = "0.24" @@ -71,6 +77,13 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-cli [lints.clippy] all = { level = "warn", priority = -1 } +# The web gallery: small over fast, one unit so LTO sees everything. +[profile.web] +inherits = "release" +opt-level = "z" +lto = true +codegen-units = 1 + [[example]] name = "gallery" path = "examples/gallery/main.rs" diff --git a/examples/gallery/index.html b/examples/gallery/index.html new file mode 100644 index 0000000..5442c2e --- /dev/null +++ b/examples/gallery/index.html @@ -0,0 +1,32 @@ + + + + + + Ely Gallery + + + + + + diff --git a/examples/gallery/main.rs b/examples/gallery/main.rs index 8c3514b..f9995eb 100644 --- a/examples/gallery/main.rs +++ b/examples/gallery/main.rs @@ -1,4 +1,7 @@ +#![cfg_attr(target_family = "wasm", no_main)] + /// A file under the gallery's assets, by name. +#[cfg(not(target_family = "wasm"))] macro_rules! asset { ($name:literal) => { concat!( @@ -9,28 +12,48 @@ macro_rules! asset { }; } +/// On the web, the address the site serves it at: the build names the folder. +#[cfg(target_family = "wasm")] +macro_rules! asset { + ($name:literal) => { + concat!(env!("ELY_GALLERY_ASSETS"), $name) + }; +} + +#[cfg(not(target_family = "wasm"))] mod capture; mod pages; mod probe; +#[cfg(not(target_family = "wasm"))] mod script; mod shell; +// Native capture alone plays the scripts. +#[cfg_attr(target_family = "wasm", allow(dead_code))] +mod step; +#[cfg(not(target_family = "wasm"))] mod swipe; mod ui; +#[cfg(target_family = "wasm")] +mod web; +#[cfg(not(target_family = "wasm"))] use std::path::PathBuf; +#[cfg(not(target_family = "wasm"))] use anyhow::{Context as _, Result, bail}; +#[cfg(not(target_family = "wasm"))] use ely_gpui_component::Assets; +use ely_gpui_component::theme::{Mode, Theme}; use gpui::{ - App, AppContext, Bounds, KeyBinding, Menu, MenuItem, TitlebarOptions, WindowBounds, - WindowHandle, WindowOptions, actions, point, px, size, + App, AppContext, Bounds, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions, + WindowBounds, WindowHandle, WindowOptions, actions, point, px, size, }; actions!(gallery, [Quit, About, LightMode, DarkMode, NewWindow]); -/// The gallery's menu bar and Dock menu: NativeMenu and JumpList at work. Set before the window opens, whose first frame may draw them. -fn menus(cx: &mut App) { - cx.set_menus(vec![ +/// The gallery's menu bar: NativeMenu at work, and what MenuBar draws. +pub fn app_menus() -> Vec { + vec![ Menu { name: "Ely".into(), items: vec![ @@ -48,7 +71,12 @@ fn menus(cx: &mut App) { ], disabled: false, }, - ]); + ] +} + +/// The menu bar and Dock menu (JumpList). Set before the window opens, whose first frame may draw them. +fn menus(cx: &mut App) { + cx.set_menus(app_menus()); cx.set_dock_menu(vec![MenuItem::action("New Window", NewWindow)]); } @@ -67,17 +95,74 @@ fn choose(window: WindowHandle, choice: shell::Choice, cx: &mut } } -struct Args { - page: Option, - capture: Option, - narrow: Option, +/// What the gallery opens on. +pub struct Start { + pub page: usize, + /// One section, by title or slug, drawn alone. + pub story: Option, + pub choice: shell::Choice, + /// Narrow check width, if any. + pub narrow: Option, + /// Why the start could not be read, shown in place of a page. + pub failure: Option, } +/// Sets Ely up and opens the gallery's window. +fn launch(start: Start, cx: &mut App) -> WindowHandle { + ely_gpui_component::init(cx); + #[cfg(debug_assertions)] + ely_gpui_component::tooling::install_inspector(cx); + pages::bind_keys(cx); + pages::install_i18n(cx); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.on_action(|_: &Quit, cx| cx.quit()); + menus(cx); + if start.choice == shell::Choice::Dark { + Theme::set_mode_now(Mode::Dark, cx); + } + let bounds = Bounds::centered(None, size(px(1280.0), px(820.0)), cx); + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(TitlebarOptions { + title: Some("Ely".into()), + appears_transparent: true, + traffic_light_position: Some(point(px(16.0), px(18.0))), + }), + window_min_size: Some(size(px(880.0), px(560.0))), + // Keeps drawing while a demo window is key. + inactive_frame_interval: None, + ..Default::default() + }; + let window = cx + .open_window(options, |window, cx| { + cx.new(|cx| shell::Gallery::new(start, window, cx)) + }) + .expect("gallery window failed to open"); + menu_actions(window, cx); + cx.activate(true); + window +} + +#[cfg(not(target_family = "wasm"))] +struct Args { + start: Start, + /// The page asked for, which a capture shoots alone. + page: Option, + capture: Option, +} + +#[cfg(not(target_family = "wasm"))] fn parse_args() -> Result { let mut args = Args { + start: Start { + page: 0, + story: None, + choice: shell::Choice::Light, + narrow: None, + failure: None, + }, page: None, capture: None, - narrow: None, }; let mut iter = std::env::args().skip(1); while let Some(flag) = iter.next() { @@ -88,9 +173,10 @@ fn parse_args() -> Result { "--page" => { args.page = Some(pages::find(&value).with_context(|| format!("no page {value}"))?) } + "--story" => args.start.story = Some(value.into()), "--capture" => args.capture = Some(PathBuf::from(value)), "--narrow" => { - args.narrow = Some( + args.start.narrow = Some( value .parse() .with_context(|| format!("bad width {value}"))?, @@ -99,47 +185,28 @@ fn parse_args() -> Result { other => bail!("unknown flag {other}"), } } + if args.start.story.is_some() && args.page.is_none() { + bail!("--story needs --page"); + } + args.start.page = args.page.unwrap_or(0); Ok(args) } +#[cfg(not(target_family = "wasm"))] fn main() -> Result<()> { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - let args = parse_args()?; + let Args { + start, + page, + capture, + } = parse_args()?; + let scripted = start.narrow.is_none(); gpui_platform::application() .with_assets(Assets) .run(move |cx: &mut App| { - ely_gpui_component::init(cx); - #[cfg(debug_assertions)] - ely_gpui_component::tooling::install_inspector(cx); - pages::bind_keys(cx); - pages::install_i18n(cx); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.on_action(|_: &Quit, cx| cx.quit()); - menus(cx); - let bounds = Bounds::centered(None, size(px(1280.0), px(820.0)), cx); - let options = WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - titlebar: Some(TitlebarOptions { - title: Some("Ely".into()), - appears_transparent: true, - traffic_light_position: Some(point(px(16.0), px(18.0))), - }), - window_min_size: Some(size(px(880.0), px(560.0))), - // Keeps drawing while a demo window is key. - inactive_frame_interval: None, - ..Default::default() - }; - let window = cx - .open_window(options, |window, cx| { - cx.new(|cx| { - shell::Gallery::new(args.page.unwrap_or(0), args.narrow, window, cx) - }) - }) - .expect("gallery window failed to open"); - menu_actions(window, cx); - cx.activate(true); - if let Some(dir) = args.capture { - capture::run(window, dir, args.page, args.narrow.is_none(), cx); + let window = launch(start, cx); + if let Some(dir) = capture { + capture::run(window, dir, page, scripted, cx); } }); Ok(()) diff --git a/examples/gallery/pages/account/mod.rs b/examples/gallery/pages/account/mod.rs index 3be74ef..4832a82 100644 --- a/examples/gallery/pages/account/mod.rs +++ b/examples/gallery/pages/account/mod.rs @@ -5,7 +5,7 @@ mod billing; mod sign_in; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 36, diff --git a/examples/gallery/pages/agent/mod.rs b/examples/gallery/pages/agent/mod.rs index 37027d9..591a955 100644 --- a/examples/gallery/pages/agent/mod.rs +++ b/examples/gallery/pages/agent/mod.rs @@ -8,7 +8,7 @@ mod tools; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 24, diff --git a/examples/gallery/pages/buttons/mod.rs b/examples/gallery/pages/buttons/mod.rs index cea3f98..b8758e3 100644 --- a/examples/gallery/pages/buttons/mod.rs +++ b/examples/gallery/pages/buttons/mod.rs @@ -5,7 +5,7 @@ mod choices; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 5, diff --git a/examples/gallery/pages/calendar/mod.rs b/examples/gallery/pages/calendar/mod.rs index b5d3cf0..3524c73 100644 --- a/examples/gallery/pages/calendar/mod.rs +++ b/examples/gallery/pages/calendar/mod.rs @@ -13,7 +13,7 @@ mod people; mod times; use super::Page; -use crate::{script::Step, ui::keep}; +use crate::{step::Step, ui::keep}; pub const PAGE: Page = Page { number: 30, diff --git a/examples/gallery/pages/canvas/mod.rs b/examples/gallery/pages/canvas/mod.rs index 226af7d..29d6d32 100644 --- a/examples/gallery/pages/canvas/mod.rs +++ b/examples/gallery/pages/canvas/mod.rs @@ -8,7 +8,7 @@ mod plane; mod tools; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 32, diff --git a/examples/gallery/pages/charts/mod.rs b/examples/gallery/pages/charts/mod.rs index 9d2e9f7..0012626 100644 --- a/examples/gallery/pages/charts/mod.rs +++ b/examples/gallery/pages/charts/mod.rs @@ -6,7 +6,7 @@ mod wholes; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 15, diff --git a/examples/gallery/pages/chat/compose.rs b/examples/gallery/pages/chat/compose.rs index aea9b94..f2223b4 100644 --- a/examples/gallery/pages/chat/compose.rs +++ b/examples/gallery/pages/chat/compose.rs @@ -1,5 +1,3 @@ -use std::time::Instant; - use ely_gpui_component::{ buttons::SegmentedControl, chat::{ @@ -13,6 +11,7 @@ use ely_gpui_component::{ primitives::IconName, }; use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; +use web_time::Instant; use crate::{ probe::probe, diff --git a/examples/gallery/pages/chat/controls.rs b/examples/gallery/pages/chat/controls.rs index 2b7dd77..98ff7ac 100644 --- a/examples/gallery/pages/chat/controls.rs +++ b/examples/gallery/pages/chat/controls.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use ely_gpui_component::{ buttons::{Button, ButtonVariant}, @@ -11,6 +11,7 @@ use ely_gpui_component::{ primitives::IconName, }; use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; +use web_time::Instant; use crate::{ probe::probe, diff --git a/examples/gallery/pages/chat/mod.rs b/examples/gallery/pages/chat/mod.rs index 9eacfc3..15bad4f 100644 --- a/examples/gallery/pages/chat/mod.rs +++ b/examples/gallery/pages/chat/mod.rs @@ -10,7 +10,7 @@ use ely_gpui_component::feedback::{ToastViewport, Toaster}; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::{script::Step, ui::keep}; +use crate::{step::Step, ui::keep}; pub const PAGE: Page = Page { number: 23, diff --git a/examples/gallery/pages/collab/mod.rs b/examples/gallery/pages/collab/mod.rs index 05022a3..fdf0fce 100644 --- a/examples/gallery/pages/collab/mod.rs +++ b/examples/gallery/pages/collab/mod.rs @@ -5,7 +5,7 @@ mod talk; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 22, diff --git a/examples/gallery/pages/dashboard/mod.rs b/examples/gallery/pages/dashboard/mod.rs index a16941a..23364ef 100644 --- a/examples/gallery/pages/dashboard/mod.rs +++ b/examples/gallery/pages/dashboard/mod.rs @@ -4,7 +4,7 @@ mod board; mod monitor; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 34, diff --git a/examples/gallery/pages/data/media.rs b/examples/gallery/pages/data/media.rs index e49257a..fd4868e 100644 --- a/examples/gallery/pages/data/media.rs +++ b/examples/gallery/pages/data/media.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use ely_gpui_component::{ buttons::{Button, ButtonVariant}, data_display::{BeforeAfter, Carousel, Gallery, Watermark}, @@ -12,7 +10,7 @@ use gpui::{App, IntoElement, ObjectFit, ParentElement, Styled, Window, div, px}; use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, picture, section, set}, }; const PICTURES: [(&str, &str); 6] = [ @@ -35,7 +33,7 @@ pub fn carousel(cx: &mut App) -> impl IntoElement + use<> { .relative() .size_full() .child( - Image::new(("carousel-picture", ix), Path::new(*path)) + Image::new(("carousel-picture", ix), picture(*path)) .fit(ObjectFit::Cover) .size_full(), ) @@ -74,14 +72,14 @@ pub fn gallery(cx: &mut App) -> impl IntoElement + use<> { "gallery", div().w(px(420.)).child(Gallery::new( "gallery", - PICTURES.map(|(path, caption)| Slide::new(Path::new(path), caption)), + PICTURES.map(|(path, caption)| Slide::new(picture(path), caption)), )), ))) } pub fn before_after(cx: &mut App) -> impl IntoElement + use<> { let picture = |id: &'static str, path: &'static str| { - Image::new(id, Path::new(path)) + Image::new(id, picture(path)) .fit(ObjectFit::Cover) .size_full() }; diff --git a/examples/gallery/pages/data/mod.rs b/examples/gallery/pages/data/mod.rs index 8f0ab85..1298848 100644 --- a/examples/gallery/pages/data/mod.rs +++ b/examples/gallery/pages/data/mod.rs @@ -6,7 +6,7 @@ mod records; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 12, diff --git a/examples/gallery/pages/data/people.rs b/examples/gallery/pages/data/people.rs index 5228140..e4b1d0b 100644 --- a/examples/gallery/pages/data/people.rs +++ b/examples/gallery/pages/data/people.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use ely_gpui_component::{ buttons::{Button, ButtonVariant}, @@ -15,13 +15,10 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div}; use crate::{ probe::probe, - ui::{keep, row, section, set, specimen, specimens}, + ui::{keep, picture, row, section, set, specimen, specimens}, }; -const DUNES: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes-square.jpg" -); +const DUNES: &str = asset!("dunes-square.jpg"); pub fn badges(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let unread = keep("badge-unread", || 3usize, window, cx); @@ -127,7 +124,7 @@ pub fn avatars(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { }))) .child( specimens() - .child(specimen("picture", Avatar::new("avatar-photo", "Dunes").image(Path::new(DUNES)).size(AvatarSize::Lg), cx)) + .child(specimen("picture", Avatar::new("avatar-photo", "Dunes").image(picture(DUNES)).size(AvatarSize::Lg), cx)) .child(specimen("online", Avatar::new("avatar-online", "Aiko Tanaka").size(AvatarSize::Lg).presence(Presence::Online), cx)) .child(specimen("away", Avatar::new("avatar-away", "Lucía Romero").size(AvatarSize::Lg).presence(Presence::Away), cx)) .child(specimen("busy", Avatar::new("avatar-busy", "Ben Carter").size(AvatarSize::Lg).presence(Presence::Busy), cx)) @@ -182,7 +179,7 @@ pub fn groups(cx: &mut App) -> impl IntoElement + use<> { .child("Assigned to") .child(UserChip::new("chip-grace", "Grace Hopper")) .child("and") - .child(UserChip::new("chip-dunes", "Dune Studio").image(Path::new(DUNES))) + .child(UserChip::new("chip-dunes", "Dune Studio").image(picture(DUNES))) .child("for review."), ) } diff --git a/examples/gallery/pages/debug/mod.rs b/examples/gallery/pages/debug/mod.rs index 7e5b856..600f1b2 100644 --- a/examples/gallery/pages/debug/mod.rs +++ b/examples/gallery/pages/debug/mod.rs @@ -5,7 +5,7 @@ mod session; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 20, diff --git a/examples/gallery/pages/devtools/mod.rs b/examples/gallery/pages/devtools/mod.rs index 27eee1c..8ad7daa 100644 --- a/examples/gallery/pages/devtools/mod.rs +++ b/examples/gallery/pages/devtools/mod.rs @@ -7,7 +7,7 @@ mod formats; mod tools; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 33, diff --git a/examples/gallery/pages/documents/editing.rs b/examples/gallery/pages/documents/editing.rs index bc86a0e..e7a0e74 100644 --- a/examples/gallery/pages/documents/editing.rs +++ b/examples/gallery/pages/documents/editing.rs @@ -12,10 +12,7 @@ use crate::{ ui::{keep, section, set}, }; -const ATRIUM: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" -); +const ATRIUM: &str = asset!("atrium.jpg"); const PEOPLE: [&str; 4] = ["ada", "grace", "linus", "margaret"]; diff --git a/examples/gallery/pages/documents/knowledge.rs b/examples/gallery/pages/documents/knowledge.rs index f06df07..976d358 100644 --- a/examples/gallery/pages/documents/knowledge.rs +++ b/examples/gallery/pages/documents/knowledge.rs @@ -20,18 +20,9 @@ use crate::{ }; const COVERS: [&str; 3] = [ - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes.jpg" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-olive.jpg" - ), + asset!("atrium.jpg"), + asset!("dunes.jpg"), + asset!("atrium-olive.jpg"), ]; fn favorite(key: &str, icon: &str, title: &str) -> Favorite { diff --git a/examples/gallery/pages/documents/mod.rs b/examples/gallery/pages/documents/mod.rs index ba8785b..4286d36 100644 --- a/examples/gallery/pages/documents/mod.rs +++ b/examples/gallery/pages/documents/mod.rs @@ -5,7 +5,7 @@ mod reading; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 21, diff --git a/examples/gallery/pages/documents/reading.rs b/examples/gallery/pages/documents/reading.rs index c2a5ec5..062b036 100644 --- a/examples/gallery/pages/documents/reading.rs +++ b/examples/gallery/pages/documents/reading.rs @@ -22,26 +22,8 @@ use crate::{ ui::{keep, section, set}, }; -const PAGES: [&str; 2] = [ - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/pages/lift-1.png" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/pages/lift-2.png" - ), -]; -const SLIDES: [&str; 2] = [ - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/pages/slide-1.png" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/pages/slide-2.png" - ), -]; +const PAGES: [&str; 2] = [asset!("pages/lift-1.png"), asset!("pages/lift-2.png")]; +const SLIDES: [&str; 2] = [asset!("pages/slide-1.png"), asset!("pages/slide-2.png")]; const WORDS: &str = include_str!("../../assets/pages/lift.json"); const GUIDE: &str = "# Lift\n\nA lift blends a color toward white.\n\n## In light\n\nPaper white under ink near black; accents at half a lift.\n\n## In dark\n\nWarm charcoal under off-white text; accents lift further, so they read with the same weight.\n\n### Surfaces\n\nA card sits one lift above the page, a menu one above the card.\n\n## In motion\n\nHover, press and focus are lifts of the accent, eased on one scale of durations.\n\n## Closing\n\nWhatever the hue, a step of lift means the same step of emphasis."; diff --git a/examples/gallery/pages/editor/mod.rs b/examples/gallery/pages/editor/mod.rs index cf4a55a..af7771c 100644 --- a/examples/gallery/pages/editor/mod.rs +++ b/examples/gallery/pages/editor/mod.rs @@ -9,7 +9,7 @@ mod status; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 17, diff --git a/examples/gallery/pages/feedback/mod.rs b/examples/gallery/pages/feedback/mod.rs index 6a90ab5..f35f4bc 100644 --- a/examples/gallery/pages/feedback/mod.rs +++ b/examples/gallery/pages/feedback/mod.rs @@ -4,7 +4,7 @@ mod states; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 10, diff --git a/examples/gallery/pages/feedback/states.rs b/examples/gallery/pages/feedback/states.rs index cc2c4fb..c2ecf47 100644 --- a/examples/gallery/pages/feedback/states.rs +++ b/examples/gallery/pages/feedback/states.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use anyhow::anyhow; use ely_gpui_component::{ @@ -13,6 +13,7 @@ use ely_gpui_component::{ typography::Caption, }; use gpui::{App, Div, IntoElement, ParentElement, Styled, Window, div, prelude::FluentBuilder}; +use web_time::Instant; use crate::{ probe::probe, diff --git a/examples/gallery/pages/files/find.rs b/examples/gallery/pages/files/find.rs index 4acd2fb..ea2a0b3 100644 --- a/examples/gallery/pages/files/find.rs +++ b/examples/gallery/pages/files/find.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use ely_gpui_component::{ debug::HexViewer, documents::{Favorite, Favorites}, @@ -18,13 +16,10 @@ use jiff::{SignedDuration, Timestamp, ToSpan}; use crate::{ probe::probe, - ui::{change, keep, section}, + ui::{change, keep, picture, section}, }; -const ATRIUM: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" -); +const ATRIUM: &str = asset!("atrium.jpg"); const BRIEF: &str = "# Atrium\n\nA quiet hall for the new library.\n\n- Light from above, all day\n- One stair, curved, in plaster\n- Olive trees by the windows\n\nNext: the model walkthrough on Friday."; fn entry(name: &str, bytes: u64, hours: i64) -> DirEntry { @@ -38,7 +33,7 @@ pub fn previews(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let chosen = picked.read(cx).clone(); let preview = |name: &SharedString| match name.as_ref() { "Atrium.jpg" => FilePreview::new("files-look", entry("Atrium.jpg", 2_140_000, 50)) - .picture(Path::new(ATRIUM), 1.5), + .picture(picture(ATRIUM), 1.5), "Brief.md" => FilePreview::new("files-look", entry("Brief.md", 4_800, 2)).text(BRIEF), other => FilePreview::new("files-look", entry(other, 48_000_000, 700)), }; @@ -289,7 +284,7 @@ pub fn archive(cx: &mut App) -> impl IntoElement + use<> { pub fn hex(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let bytes = keep( "files-hex", - || std::fs::read(ATRIUM).expect("the gallery's picture reads")[..512].to_vec(), + || include_bytes!("../../assets/atrium.jpg")[..512].to_vec(), window, cx, ); diff --git a/examples/gallery/pages/files/mod.rs b/examples/gallery/pages/files/mod.rs index 91ea65c..6af8e6f 100644 --- a/examples/gallery/pages/files/mod.rs +++ b/examples/gallery/pages/files/mod.rs @@ -14,7 +14,7 @@ mod transfers; use super::Page; use crate::{ probe::probe, - script::Step, + step::Step, ui::{change, keep, section}, }; diff --git a/examples/gallery/pages/finance/assets.rs b/examples/gallery/pages/finance/assets.rs index 52bd42e..8140f09 100644 --- a/examples/gallery/pages/finance/assets.rs +++ b/examples/gallery/pages/finance/assets.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use ely_gpui_component::{ charts::PieChart, @@ -13,6 +13,7 @@ use ely_gpui_component::{ }; use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; use jiff::{Timestamp, civil::date, tz::TimeZone}; +use web_time::Instant; use crate::ui::{keep, noise, row, section, set}; diff --git a/examples/gallery/pages/finance/mod.rs b/examples/gallery/pages/finance/mod.rs index 97efd66..61a3865 100644 --- a/examples/gallery/pages/finance/mod.rs +++ b/examples/gallery/pages/finance/mod.rs @@ -7,7 +7,7 @@ mod trading; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 16, diff --git a/examples/gallery/pages/forms/mod.rs b/examples/gallery/pages/forms/mod.rs index 04bc253..1215f48 100644 --- a/examples/gallery/pages/forms/mod.rs +++ b/examples/gallery/pages/forms/mod.rs @@ -14,7 +14,7 @@ mod values; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 6, diff --git a/examples/gallery/pages/generative/mod.rs b/examples/gallery/pages/generative/mod.rs index ee5361d..3d69a37 100644 --- a/examples/gallery/pages/generative/mod.rs +++ b/examples/gallery/pages/generative/mod.rs @@ -8,7 +8,7 @@ mod sound; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 25, diff --git a/examples/gallery/pages/generative/results.rs b/examples/gallery/pages/generative/results.rs index aa6239e..a9e774c 100644 --- a/examples/gallery/pages/generative/results.rs +++ b/examples/gallery/pages/generative/results.rs @@ -1,4 +1,4 @@ -use std::{path::Path, time::Duration}; +use std::time::Duration; use ely_gpui_component::{ data_display::BeforeAfter, @@ -12,7 +12,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, picture, section, set}, }; fn jobs() -> Vec { @@ -137,8 +137,8 @@ pub fn results(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .child( BeforeAfter::new( "gen-compare", - Image::new("gen-compare-before", Path::new(asset!("atrium-before.jpg"))).size_full(), - Image::new("gen-compare-after", Path::new(asset!("atrium.jpg"))).size_full(), + Image::new("gen-compare-before", picture(asset!("atrium-before.jpg"))).size_full(), + Image::new("gen-compare-after", picture(asset!("atrium.jpg"))).size_full(), ) .w(px(420.)) .h(px(280.)), diff --git a/examples/gallery/pages/git/mod.rs b/examples/gallery/pages/git/mod.rs index ef0fcf3..069b091 100644 --- a/examples/gallery/pages/git/mod.rs +++ b/examples/gallery/pages/git/mod.rs @@ -5,7 +5,7 @@ mod work; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 19, diff --git a/examples/gallery/pages/i18n/mod.rs b/examples/gallery/pages/i18n/mod.rs index a092bb9..56bf710 100644 --- a/examples/gallery/pages/i18n/mod.rs +++ b/examples/gallery/pages/i18n/mod.rs @@ -12,7 +12,7 @@ mod a11y; use super::Page; use crate::probe::probe; -use crate::script::Step; +use crate::step::Step; use crate::ui::{section, specimen, specimens}; pub const PAGE: Page = Page { diff --git a/examples/gallery/pages/interaction/mod.rs b/examples/gallery/pages/interaction/mod.rs index 6b9c6b3..2cbe1c9 100644 --- a/examples/gallery/pages/interaction/mod.rs +++ b/examples/gallery/pages/interaction/mod.rs @@ -6,7 +6,7 @@ mod pointer; pub use keys::bind_keys; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 38, diff --git a/examples/gallery/pages/layout/mod.rs b/examples/gallery/pages/layout/mod.rs index b1cbc9d..aa19125 100644 --- a/examples/gallery/pages/layout/mod.rs +++ b/examples/gallery/pages/layout/mod.rs @@ -7,7 +7,7 @@ mod systems; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 3, diff --git a/examples/gallery/pages/lists/mod.rs b/examples/gallery/pages/lists/mod.rs index 20d0eb4..d9d1c9e 100644 --- a/examples/gallery/pages/lists/mod.rs +++ b/examples/gallery/pages/lists/mod.rs @@ -6,7 +6,7 @@ mod trees; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 13, diff --git a/examples/gallery/pages/mail/mod.rs b/examples/gallery/pages/mail/mod.rs index 3d2d5ae..456c892 100644 --- a/examples/gallery/pages/mail/mod.rs +++ b/examples/gallery/pages/mail/mod.rs @@ -16,7 +16,7 @@ mod write; use super::Page; use crate::{ probe::probe, - script::Step, + step::Step, ui::{change, keep, section}, }; diff --git a/examples/gallery/pages/maps/mod.rs b/examples/gallery/pages/maps/mod.rs index b6b8e2c..6ae3af5 100644 --- a/examples/gallery/pages/maps/mod.rs +++ b/examples/gallery/pages/maps/mod.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use ely_gpui_component::{ maps::{ CoordFormat, CoordinateDisplay, LatLon, LocationPicker, MapMarker, MapPopup, MapView, @@ -16,8 +14,8 @@ mod world; use super::Page; use crate::probe::probe; -use crate::script::Step; -use crate::ui::{keep, section, set, specimen, specimens}; +use crate::step::Step; +use crate::ui::{keep, picture, section, set, specimen, specimens}; pub const PAGE: Page = Page { number: 41, @@ -48,7 +46,7 @@ pub const PAGE: Page = Page { ], }; -const TILES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/gallery/assets/tiles"); +const TILES: &str = asset!("tiles"); pub(super) const ATTRIBUTION: &str = "Land: Natural Earth"; const CITIES: [(&str, &str, f64, f64, usize); 6] = [ @@ -62,7 +60,7 @@ const CITIES: [(&str, &str, f64, f64, usize); 6] = [ /// The gallery's tiles, cut from Natural Earth by `scripts/tiles.py`. pub(super) fn tile(tile: Tile) -> ImageSource { - PathBuf::from(format!("{TILES}/{}/{}/{}.png", tile.z, tile.x, tile.y)).into() + picture(format!("{TILES}/{}/{}/{}.png", tile.z, tile.x, tile.y)) } fn render(window: &mut Window, cx: &mut App) -> AnyElement { diff --git a/examples/gallery/pages/media/audio.rs b/examples/gallery/pages/media/audio.rs index fea9f22..88596f8 100644 --- a/examples/gallery/pages/media/audio.rs +++ b/examples/gallery/pages/media/audio.rs @@ -1,4 +1,4 @@ -use std::{path::Path, time::Duration}; +use std::time::Duration; use ely_gpui_component::media::{ AudioPlayer, AudioSpectrum, AudioWaveform, MediaControls, PlaybackSpeedControl, Playlist, @@ -10,13 +10,10 @@ use gpui::{ use crate::{ probe::probe, - ui::{change, keep, noise, section}, + ui::{change, keep, noise, picture, section}, }; -const COVER: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes-square.jpg" -); +const COVER: &str = asset!("dunes-square.jpg"); /// How often the demo's clock moves while a song plays. const TICK: Duration = Duration::from_millis(100); const SONGS: [(&str, &str, &str, u64); 5] = [ @@ -181,7 +178,7 @@ pub fn player(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let (title, artist) = (SONGS[now.song].1, SONGS[now.song].2); let seek = state.clone(); let player = AudioPlayer::new("media-audio", title, artist, now.length(), now.at) - .artwork(Path::new(COVER)) + .artwork(picture(COVER)) .controls(controls("media-audio-transport", &state, &now)) .volume(volume("media-audio-volume", &state, &now)) .on_seek(move |at, _, cx| change(&seek, cx, |now| now.at = at)); diff --git a/examples/gallery/pages/media/devices.rs b/examples/gallery/pages/media/devices.rs index 4ceb001..6a3f05f 100644 --- a/examples/gallery/pages/media/devices.rs +++ b/examples/gallery/pages/media/devices.rs @@ -1,4 +1,4 @@ -use std::{path::Path, sync::Arc}; +use std::sync::Arc; use ely_gpui_component::{ media::{ @@ -15,13 +15,10 @@ use gpui::{ use super::sculpt; use crate::{ probe::probe, - ui::{change, keep, section}, + ui::{change, keep, picture, section}, }; -const OLIVE: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-olive.jpg" -); +const OLIVE: &str = asset!("atrium-olive.jpg"); /// The model's frame, in pixels: twice the viewer's points. const FRAME: (u32, u32) = (960, 640); @@ -105,7 +102,7 @@ pub fn camera(cx: &mut App) -> impl IntoElement + use<> { .child( div().w(px(300.)).child( CameraPreview::new("media-camera-on", 1.5) - .frame(Path::new(OLIVE)) + .frame(picture(OLIVE)) .name("Studio camera"), ), ) diff --git a/examples/gallery/pages/media/edit.rs b/examples/gallery/pages/media/edit.rs index 995431a..655a635 100644 --- a/examples/gallery/pages/media/edit.rs +++ b/examples/gallery/pages/media/edit.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use ely_gpui_component::{ buttons::SegmentedControl, @@ -9,21 +9,12 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, picture, section, set}, }; -const ATRIUM: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" -); -const STAIR: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-stair.jpg" -); -const DUNES: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes.jpg" -); +const ATRIUM: &str = asset!("atrium.jpg"); +const STAIR: &str = asset!("atrium-stair.jpg"); +const DUNES: &str = asset!("dunes.jpg"); /// Crop shapes to pick from, width over height; none is free. const ASPECTS: [(&str, Option); 4] = [ @@ -89,7 +80,7 @@ pub fn cropper(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .child(probe( "media-cropper", div().w(px(560.)).child( - ImageCropper::new("media-cropper", Path::new(ATRIUM), 1.5, crop) + ImageCropper::new("media-cropper", picture(ATRIUM), 1.5, crop) .aspect(aspect) .on_change(move |crop, _, cx| { let shape = moved.read(cx).1; @@ -165,7 +156,7 @@ pub fn annotator(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> .child(probe( "media-annotator", div().w(px(560.)).child( - ImageAnnotator::new("media-annotator", Path::new(STAIR), 1.5, marks) + ImageAnnotator::new("media-annotator", picture(STAIR), 1.5, marks) .on_change(move |marks, _, cx| set(&changed, marks.to_vec(), cx)), ), )) diff --git a/examples/gallery/pages/media/images.rs b/examples/gallery/pages/media/images.rs index 7330b02..7f3ae76 100644 --- a/examples/gallery/pages/media/images.rs +++ b/examples/gallery/pages/media/images.rs @@ -1,5 +1,3 @@ -use std::path::{Path, PathBuf}; - use ely_gpui_component::{ media::{ImageThumbnail, ImageViewer}, overlays::{Lightbox, Slide}, @@ -8,56 +6,21 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p use crate::{ probe::probe, - ui::{keep, row, section, set}, + ui::{keep, picture, resource, row, section, set}, }; /// The gallery's pictures: a file, its width over height, a caption, and a corner label. const PICTURES: [(&str, f32, &str, &str); 5] = [ + (asset!("atrium.jpg"), 1.5, "An atrium, noon", ""), + (asset!("dunes.jpg"), 1.5, "Dunes at first light", ""), ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" - ), - 1.5, - "An atrium, noon", - "", - ), - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes.jpg" - ), - 1.5, - "Dunes at first light", - "", - ), - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-olive.jpg" - ), + asset!("atrium-olive.jpg"), 1.5, "An olive tree by the stair", "HDR", ), - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes-grass.jpg" - ), - 1.5, - "Grass on the ridge", - "", - ), - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes-square.jpg" - ), - 1.0, - "The long shadow", - "+2", - ), + (asset!("dunes-grass.jpg"), 1.5, "Grass on the ridge", ""), + (asset!("dunes-square.jpg"), 1.0, "The long shadow", "+2"), ]; pub fn viewer(cx: &mut App) -> impl IntoElement + use<> { @@ -68,7 +31,7 @@ pub fn viewer(cx: &mut App) -> impl IntoElement + use<> { ) .child(probe( "media-viewer", - div().w(px(640.)).h(px(440.)).child(ImageViewer::new("media-viewer", PathBuf::from(PICTURES[0].0))), + div().w(px(640.)).h(px(440.)).child(ImageViewer::new("media-viewer", resource(PICTURES[0].0))), )) } @@ -83,7 +46,7 @@ pub fn thumbnails(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> let (open, mark) = (shown.clone(), opened.clone()); let thumb = ImageThumbnail::new( SharedString::from(format!("media-thumb-{ix}")), - Path::new(*path), + picture(*path), *ratio, ) .selected(ix == last) @@ -102,7 +65,7 @@ pub fn thumbnails(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> let (step, close) = (shown.clone(), shown.clone()); Lightbox::new( "media-lightbox", - PICTURES.map(|(path, _, caption, _)| Slide::new(Path::new(path), caption)), + PICTURES.map(|(path, _, caption, _)| Slide::new(picture(path), caption)), at, move |_, cx| set(&close, None, cx), ) diff --git a/examples/gallery/pages/media/mod.rs b/examples/gallery/pages/media/mod.rs index 930ada1..6a622be 100644 --- a/examples/gallery/pages/media/mod.rs +++ b/examples/gallery/pages/media/mod.rs @@ -8,7 +8,7 @@ mod video; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 26, diff --git a/examples/gallery/pages/media/video.rs b/examples/gallery/pages/media/video.rs index b15e277..2c12692 100644 --- a/examples/gallery/pages/media/video.rs +++ b/examples/gallery/pages/media/video.rs @@ -1,4 +1,4 @@ -use std::{path::Path, time::Duration}; +use std::time::Duration; use ely_gpui_component::media::{Cue, Scrubber, SubtitleEditor, VideoPlayer, VideoThumbnailStrip}; use gpui::{ @@ -7,22 +7,13 @@ use gpui::{ use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, picture, section, set}, }; const FRAMES: [&str; 3] = [ - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-stair.jpg" - ), - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium-olive.jpg" - ), + asset!("atrium.jpg"), + asset!("atrium-stair.jpg"), + asset!("atrium-olive.jpg"), ]; const LENGTH: Duration = Duration::from_secs(90); /// How often the demo's clock moves while it plays. @@ -115,7 +106,7 @@ pub fn video(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { (state.clone(), state.clone(), state.clone(), state.clone()); let player = VideoPlayer::new( "media-player", - Some(Path::new(frame(now.at)).into()), + Some(picture(frame(now.at))), 1.5, LENGTH, now.at, @@ -190,7 +181,7 @@ pub fn strip(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let state = state(window, cx); let now = state.read(cx).clone(); let (seek, cut) = (state.clone(), state.clone()); - let frames = (0..8).map(|ix| Path::new(FRAMES[ix * 3 / 8])); + let frames = (0..8).map(|ix| picture(FRAMES[ix * 3 / 8])); section( "VideoThumbnailStrip", "Frames along the video under its playhead, and the part kept between two handles. A press seeks; a handle trims; I and O set the ends at the playhead.", diff --git a/examples/gallery/pages/menus/mod.rs b/examples/gallery/pages/menus/mod.rs index 42d0bb3..b18b8ba 100644 --- a/examples/gallery/pages/menus/mod.rs +++ b/examples/gallery/pages/menus/mod.rs @@ -4,7 +4,7 @@ mod rows; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 8, diff --git a/examples/gallery/pages/messaging/calls.rs b/examples/gallery/pages/messaging/calls.rs index 83829c5..42cdaba 100644 --- a/examples/gallery/pages/messaging/calls.rs +++ b/examples/gallery/pages/messaging/calls.rs @@ -1,5 +1,3 @@ -use std::{path::Path, time::Instant}; - use ely_gpui_component::{ buttons::Button, data_display::Avatar, @@ -10,10 +8,11 @@ use ely_gpui_component::{ theme::{ActiveTheme, TextSize}, }; use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; +use web_time::Instant; use crate::{ probe::probe, - ui::{change, keep, section}, + ui::{change, keep, picture, section}, }; const ROOM: &str = asset!("atrium-olive.jpg"); @@ -126,7 +125,7 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let tile = |key: &'static str, name: &'static str| ParticipantTile::new(key, name, CAMERA); let you = tile("messaging-tile-you", "You").muted(!now.mic); let you = match now.camera { - true => you.frame(Path::new(ROOM)), + true => you.frame(picture(ROOM)), false => you, }; section( @@ -138,7 +137,7 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { "messaging-grid", div().w(px(640.)).child( VideoCallGrid::new("messaging-grid") - .child(tile("messaging-tile-ana", "Ana Lima").frame(Path::new(STAIR)).speaking(true)) + .child(tile("messaging-tile-ana", "Ana Lima").frame(picture(STAIR)).speaking(true)) .child(tile("messaging-tile-ben", "Ben Ito").muted(true)) .child(you) .child(tile("messaging-tile-chloe", "Chloé Martin")) @@ -152,7 +151,7 @@ pub fn share(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let sharing = state.read(cx).share; let tile = |key: &'static str, name: &'static str| ParticipantTile::new(key, name, CAMERA); let view = ScreenShareView::new("messaging-share", "Ana Lima", 16.0 / 10.0) - .frame(Path::new(SCREEN)) + .frame(picture(SCREEN)) .child(tile("messaging-strip-ben", "Ben Ito").muted(true)) .child(tile("messaging-strip-chloe", "Chloé Martin").speaking(true)); let view = match sharing { diff --git a/examples/gallery/pages/messaging/mod.rs b/examples/gallery/pages/messaging/mod.rs index 39b237c..25902d6 100644 --- a/examples/gallery/pages/messaging/mod.rs +++ b/examples/gallery/pages/messaging/mod.rs @@ -15,7 +15,7 @@ mod pickers; use super::Page; use crate::{ probe::probe, - script::Step, + step::Step, ui::{change, keep, section}, }; diff --git a/examples/gallery/pages/messaging/pickers.rs b/examples/gallery/pages/messaging/pickers.rs index d942ab5..7f8e3c2 100644 --- a/examples/gallery/pages/messaging/pickers.rs +++ b/examples/gallery/pages/messaging/pickers.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use ely_gpui_component::{ messaging::{ChatMessage, Gif, GifPicker, Gifs, Sticker, StickerPack, StickerPicker}, primitives::Image, @@ -12,7 +10,7 @@ use jiff::{civil::date, tz::TimeZone}; use crate::{ probe::probe, - ui::{keep, section, set, specimen, specimens}, + ui::{keep, picture, section, set, specimen, specimens}, }; const ATRIUM: [(&str, &str, &str); 4] = [ @@ -60,7 +58,7 @@ const GIFS: [(&str, &str, &str, f32); 4] = [ fn pack(name: &str, stickers: [(&'static str, &'static str, &'static str); 4]) -> StickerPack { StickerPack::new( name.to_string(), - stickers.map(|(key, title, file)| Sticker::new(key, title, Path::new(file))), + stickers.map(|(key, title, file)| Sticker::new(key, title, picture(file))), ) } @@ -101,7 +99,7 @@ pub fn stickers(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { ElementId::from("messaging-sticker-picture"), key.to_string(), ), - Path::new(*file), + picture(*file), ) .fit(ObjectFit::Contain) .size_full(), @@ -146,7 +144,7 @@ fn found(words: &str) -> Vec { let words = words.to_lowercase(); GIFS.iter() .filter(|(_, title, _, _)| title.to_lowercase().contains(&words)) - .map(|(key, title, file, ratio)| Gif::new(*key, *title, Path::new(*file), *ratio)) + .map(|(key, title, file, ratio)| Gif::new(*key, *title, picture(*file), *ratio)) .collect() } @@ -163,7 +161,7 @@ pub fn gifs(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { div().w(px(200.)).h(px(200. / ratio)).child( Image::new( (ElementId::from("messaging-gif-picture"), key.to_string()), - Path::new(*file), + picture(*file), ) .size_full() .rounded(round), diff --git a/examples/gallery/pages/misc/codes.rs b/examples/gallery/pages/misc/codes.rs index 1962c71..30bf693 100644 --- a/examples/gallery/pages/misc/codes.rs +++ b/examples/gallery/pages/misc/codes.rs @@ -1,16 +1,13 @@ -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; use ely_gpui_component::misc::{Captcha, CaptchaState, QrCodeScanner}; use gpui::{App, IntoElement, ParentElement, RenderImage, Styled, Window, div, px}; use crate::probe::probe; -use crate::ui::section; +use crate::ui::{picture, section}; const ANSWERS: [&str; 2] = ["W7XK", "Q3NP"]; -const PICTURES: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/captcha" -); +const PICTURES: &str = asset!("captcha"); /// The frame a camera would hand in: a QR code on a card over a dim scene, in gpui's BGRA pixels. fn scene() -> Arc { @@ -46,7 +43,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let (ix, state) = *desk.read(cx); let answer = ANSWERS[ix]; let (checked, refreshed) = (desk.clone(), desk); - let picture = PathBuf::from(format!("{PICTURES}/{}.png", answer.to_lowercase())); + let picture = picture(format!("{PICTURES}/{}.png", answer.to_lowercase())); div() .child( section( diff --git a/examples/gallery/pages/misc/mod.rs b/examples/gallery/pages/misc/mod.rs index 91aceef..6d3ae6d 100644 --- a/examples/gallery/pages/misc/mod.rs +++ b/examples/gallery/pages/misc/mod.rs @@ -4,7 +4,7 @@ use jiff::tz::TimeZone; use super::Page; use crate::probe::probe; -use crate::script::Step; +use crate::step::Step; use crate::ui::{section, specimen, specimens}; mod asking; diff --git a/examples/gallery/pages/mod.rs b/examples/gallery/pages/mod.rs index c1315f8..e7ba482 100644 --- a/examples/gallery/pages/mod.rs +++ b/examples/gallery/pages/mod.rs @@ -44,7 +44,7 @@ mod typography; use gpui::{AnyElement, App, Window}; -use crate::script::Step; +use crate::step::Step; pub struct Page { pub number: u8, @@ -53,6 +53,7 @@ pub struct Page { pub summary: &'static str, pub render: fn(&mut Window, &mut App) -> AnyElement, /// Interactions shot after the static pages. + #[cfg_attr(target_family = "wasm", allow(dead_code))] pub script: &'static [Step], } diff --git a/examples/gallery/pages/motion/mod.rs b/examples/gallery/pages/motion/mod.rs index 1fe511a..f9bdd5b 100644 --- a/examples/gallery/pages/motion/mod.rs +++ b/examples/gallery/pages/motion/mod.rs @@ -6,7 +6,7 @@ mod progress; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 11, diff --git a/examples/gallery/pages/navigation/mod.rs b/examples/gallery/pages/navigation/mod.rs index cb80226..196cb64 100644 --- a/examples/gallery/pages/navigation/mod.rs +++ b/examples/gallery/pages/navigation/mod.rs @@ -6,7 +6,7 @@ mod tabs; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 7, diff --git a/examples/gallery/pages/onboarding/mod.rs b/examples/gallery/pages/onboarding/mod.rs index aa1d06e..a76038a 100644 --- a/examples/gallery/pages/onboarding/mod.rs +++ b/examples/gallery/pages/onboarding/mod.rs @@ -4,7 +4,7 @@ mod flows; mod help; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 37, diff --git a/examples/gallery/pages/overlays/guides.rs b/examples/gallery/pages/overlays/guides.rs index 17ec1f0..f8f3956 100644 --- a/examples/gallery/pages/overlays/guides.rs +++ b/examples/gallery/pages/overlays/guides.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use ely_gpui_component::{ buttons::{Button, ButtonVariant, IconButton}, forms::{Input, TextInput}, @@ -14,24 +12,12 @@ use gpui::{ use crate::{ probe::probe, - ui::{keep, row, section, set, specimen}, + ui::{keep, picture, row, section, set, specimen}, }; const PHOTOS: [(&str, &str); 2] = [ - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes.jpg" - ), - "Dunes at first light", - ), - ( - concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" - ), - "An atrium, noon", - ), + (asset!("dunes.jpg"), "Dunes at first light"), + (asset!("atrium.jpg"), "An atrium, noon"), ]; pub fn lightbox(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { @@ -46,7 +32,7 @@ pub fn lightbox(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .h_32() .cursor_pointer() .child( - Image::new(("thumb-image", ix), Path::new(*path)) + Image::new(("thumb-image", ix), picture(*path)) .fit(ObjectFit::Cover) .size_full(), ) @@ -57,7 +43,7 @@ pub fn lightbox(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let (step, close) = (shown.clone(), shown.clone()); Lightbox::new( "lightbox", - PHOTOS.map(|(path, caption)| Slide::new(Path::new(path), caption)), + PHOTOS.map(|(path, caption)| Slide::new(picture(path), caption)), at, move |_, cx| set(&close, None, cx), ) diff --git a/examples/gallery/pages/overlays/mod.rs b/examples/gallery/pages/overlays/mod.rs index ac4c032..421b6ca 100644 --- a/examples/gallery/pages/overlays/mod.rs +++ b/examples/gallery/pages/overlays/mod.rs @@ -5,7 +5,7 @@ mod guides; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 9, diff --git a/examples/gallery/pages/primitives/mod.rs b/examples/gallery/pages/primitives/mod.rs index 80a0c00..327c78c 100644 --- a/examples/gallery/pages/primitives/mod.rs +++ b/examples/gallery/pages/primitives/mod.rs @@ -4,7 +4,7 @@ mod parts; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 1, diff --git a/examples/gallery/pages/primitives/parts.rs b/examples/gallery/pages/primitives/parts.rs index 2137246..6060e7f 100644 --- a/examples/gallery/pages/primitives/parts.rs +++ b/examples/gallery/pages/primitives/parts.rs @@ -15,13 +15,10 @@ use gpui::{ use crate::{ probe::probe, - ui::{blocked, code, row, section, specimen, specimens}, + ui::{blocked, code, picture, row, section, specimen, specimens}, }; -const ATRIUM: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" -); +const ATRIUM: &str = asset!("atrium.jpg"); fn counter(window: &mut Window, cx: &mut App, key: &'static str) -> gpui::Entity { window.use_keyed_state(key, cx, |_, _| 0u32) @@ -104,7 +101,7 @@ pub fn image(cx: &App) -> impl IntoElement + use<> { specimens() .child(specimen( "loaded", - tile(Image::new("img-atrium", Path::new(ATRIUM)), cx), + tile(Image::new("img-atrium", picture(ATRIUM)), cx), cx, )) .child(specimen( diff --git a/examples/gallery/pages/project/mod.rs b/examples/gallery/pages/project/mod.rs index 57c8c5c..cf56787 100644 --- a/examples/gallery/pages/project/mod.rs +++ b/examples/gallery/pages/project/mod.rs @@ -14,7 +14,7 @@ mod tasks; mod time; use super::Page; -use crate::{script::Step, ui::keep}; +use crate::{step::Step, ui::keep}; pub const PAGE: Page = Page { number: 31, diff --git a/examples/gallery/pages/project/time.rs b/examples/gallery/pages/project/time.rs index 2ac8a62..e81d112 100644 --- a/examples/gallery/pages/project/time.rs +++ b/examples/gallery/pages/project/time.rs @@ -1,7 +1,8 @@ -use std::time::{Duration, Instant}; +use std::time::Duration; use ely_gpui_component::project::{DatabaseView, PomodoroTimer, Status, TimeEntry, TimeTracker}; use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; +use web_time::Instant; use super::{board, quiet}; use crate::{ diff --git a/examples/gallery/pages/settings/mod.rs b/examples/gallery/pages/settings/mod.rs index 2cbd924..4bbfbe3 100644 --- a/examples/gallery/pages/settings/mod.rs +++ b/examples/gallery/pages/settings/mod.rs @@ -4,7 +4,7 @@ mod pages; mod window; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 35, diff --git a/examples/gallery/pages/shell/mod.rs b/examples/gallery/pages/shell/mod.rs index 89ea5b2..8f393a7 100644 --- a/examples/gallery/pages/shell/mod.rs +++ b/examples/gallery/pages/shell/mod.rs @@ -7,7 +7,7 @@ mod windows; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub use windows::{open_about, open_managed}; pub const PAGE: Page = Page { diff --git a/examples/gallery/pages/shell/windows.rs b/examples/gallery/pages/shell/windows.rs index 4f524ec..cc198cb 100644 --- a/examples/gallery/pages/shell/windows.rs +++ b/examples/gallery/pages/shell/windows.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use ely_gpui_component::{ buttons::{Button, ButtonVariant, IconButton}, primitives::{IconName, Image}, @@ -17,17 +15,11 @@ use gpui::{ use super::chrome::window_frame; use crate::{ probe::{Opened, probe}, - ui::{code, row, section, specimen}, + ui::{code, own_window, picture, row, section, specimen}, }; -const DUNES: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/dunes.jpg" -); -const ATRIUM: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/gallery/assets/atrium.jpg" -); +const DUNES: &str = asset!("dunes.jpg"); +const ATRIUM: &str = asset!("atrium.jpg"); fn about() -> AboutDialog { AboutDialog::new("Ely", "0.1.0 (2026.09)") @@ -88,10 +80,13 @@ pub fn splash_screen(cx: &App) -> impl IntoElement + use<> { .child(splash().progress(Some(0.62))), cx, )) - .child(trigger("splash-open", "Show splash window", |cx| { - let result = splash().open(cx); - opened("splash", result, cx) - })) + .child(own_window( + trigger("splash-open", "Show splash window", |cx| { + let result = splash().open(cx); + opened("splash", result, cx) + }), + cx, + )) } pub fn open_about(cx: &mut App) { @@ -113,7 +108,10 @@ pub fn about_dialog(cx: &App) -> impl IntoElement + use<> { .child(about()), cx, )) - .child(trigger("about-open", "Open About window", open_about)) + .child(own_window( + trigger("about-open", "Open About window", open_about), + cx, + )) } struct Managed { @@ -194,7 +192,10 @@ 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(trigger("managed-open", "New window", open_managed)) + .child(own_window( + trigger("managed-open", "New window", open_managed), + cx, + )) .child( row().children(open.into_iter().enumerate().map(|(ix, (handle, title))| { Button::new(("managed-focus", ix), title) @@ -270,7 +271,7 @@ impl Render for Player { .rounded(theme.radius(Radius::Md)) .overflow_hidden() .child( - Image::new("player-art", Path::new(DUNES)) + Image::new("player-art", picture(DUNES)) .fit(ObjectFit::Cover) .size_full(), ), @@ -309,7 +310,7 @@ impl Render for Picture { .relative() .size_full() .child( - Image::new("pip-frame", Path::new(ATRIUM)) + Image::new("pip-frame", picture(ATRIUM)) .fit(ObjectFit::Cover) .size_full(), ) @@ -334,7 +335,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( + .child(own_window( row() .child(trigger("mini-open", "Mini player", |cx| { let view = cx.new(|_| Player { playing: true }); @@ -358,7 +359,8 @@ pub fn mini_windows(cx: &App) -> impl IntoElement + use<> { ); opened("pip", result, cx) })), - ) + cx, + )) .child(code( "AlwaysOnTop is gpui's WindowOptions { kind: WindowKind::PopUp, .. }", cx, diff --git a/examples/gallery/pages/tables/mod.rs b/examples/gallery/pages/tables/mod.rs index 498082c..c1a8ba4 100644 --- a/examples/gallery/pages/tables/mod.rs +++ b/examples/gallery/pages/tables/mod.rs @@ -5,7 +5,7 @@ mod grids; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 14, diff --git a/examples/gallery/pages/terminal/live.rs b/examples/gallery/pages/terminal/live.rs index b2e9edb..7c2da39 100644 --- a/examples/gallery/pages/terminal/live.rs +++ b/examples/gallery/pages/terminal/live.rs @@ -16,7 +16,7 @@ use gpui::{ use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, set}, }; /// A build's output as a terminal printed it, colors and all. @@ -84,7 +84,8 @@ fn field(key: &'static str, text: &str, window: &mut Window, cx: &mut App) -> En }) } -pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { +/// The shell and the build's output, in tabs. +pub fn shell(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let tab = keep("terminal-tab", || SharedString::from("shell"), window, cx); let shell = keep("terminal-shell-choice", || (0usize, 0usize), window, cx); let (now_tab, (now_shell, start)) = (tab.read(cx).clone(), *shell.read(cx)); @@ -155,12 +156,7 @@ pub fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { None => find, }; let theme = cx.theme(); - section( - "Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink", - "A real shell on a pseudo-terminal, drawn cell by cell in the theme's sixteen colors. Tabs hold shells and output; split for another shell beside it. Find lights every match through the scrollback. Cmd-press a link or a path:line.", - cx, - ) - .child( + div().child( div() .w(px(840.)) .rounded(theme.radius(Radius::Md)) diff --git a/examples/gallery/pages/terminal/mod.rs b/examples/gallery/pages/terminal/mod.rs index 938276b..3a64c53 100644 --- a/examples/gallery/pages/terminal/mod.rs +++ b/examples/gallery/pages/terminal/mod.rs @@ -1,11 +1,12 @@ mod blocks; +#[cfg(not(target_family = "wasm"))] mod live; mod panels; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::{step::Step, ui::section}; pub const PAGE: Page = Page { number: 18, @@ -36,9 +37,28 @@ const SCRIPT: &[Step] = &[ Step::Rest, ]; +/// A live shell natively; a browser runs no processes, so there it says so. +fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { + let section = section( + "Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink", + "A real shell on a pseudo-terminal, drawn cell by cell in the theme's sixteen colors. Tabs hold shells and output; split for another shell beside it. Find lights every match through the scrollback. Cmd-press a link or a path:line.", + cx, + ); + #[cfg(not(target_family = "wasm"))] + return section.child(live::shell(window, cx)); + #[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 blocks, colored output, logs and processes below draw without one.", + cx, + )) + } +} + fn render(window: &mut Window, cx: &mut App) -> AnyElement { div() - .child(live::live(window, cx)) + .child(live(window, cx)) .child(blocks::blocks(window, cx)) .child(blocks::ansi(cx)) .child(panels::logs(window, cx)) diff --git a/examples/gallery/pages/theme/mod.rs b/examples/gallery/pages/theme/mod.rs index fd8dee0..1c6cc53 100644 --- a/examples/gallery/pages/theme/mod.rs +++ b/examples/gallery/pages/theme/mod.rs @@ -14,7 +14,7 @@ mod importing; mod platform; use super::Page; -use crate::script::Step; +use crate::step::Step; use crate::ui::{section, specimen, specimens}; pub const PAGE: Page = Page { diff --git a/examples/gallery/pages/tooling/mod.rs b/examples/gallery/pages/tooling/mod.rs index dc4e06f..7b7aff9 100644 --- a/examples/gallery/pages/tooling/mod.rs +++ b/examples/gallery/pages/tooling/mod.rs @@ -11,7 +11,7 @@ mod inspection; use super::Page; use crate::probe::probe; -use crate::script::Step; +use crate::step::Step; use crate::ui::section; pub const PAGE: Page = Page { diff --git a/examples/gallery/pages/typography/mod.rs b/examples/gallery/pages/typography/mod.rs index 8952f42..30c3ce5 100644 --- a/examples/gallery/pages/typography/mod.rs +++ b/examples/gallery/pages/typography/mod.rs @@ -5,7 +5,7 @@ mod values; use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div}; use super::Page; -use crate::script::Step; +use crate::step::Step; pub const PAGE: Page = Page { number: 2, diff --git a/examples/gallery/probe.rs b/examples/gallery/probe.rs index 373278a..1aef15a 100644 --- a/examples/gallery/probe.rs +++ b/examples/gallery/probe.rs @@ -12,6 +12,7 @@ pub struct Probes(HashMap<&'static str, Bounds>); impl Global for Probes {} impl Probes { + #[cfg(not(target_family = "wasm"))] pub fn get(key: &str, cx: &App) -> Option> { cx.try_global::() .and_then(|probes| probes.0.get(key).copied()) @@ -39,10 +40,12 @@ impl Opened { cx.default_global::().0.insert(key, handle); } + #[cfg(not(target_family = "wasm"))] pub fn take(key: &str, cx: &mut App) -> Option { cx.default_global::().0.remove(key) } + #[cfg(not(target_family = "wasm"))] pub fn get(key: &str, cx: &App) -> Option { cx.try_global::() .and_then(|opened| opened.0.get(key).copied()) diff --git a/examples/gallery/script.rs b/examples/gallery/script.rs index 4b05969..517bb7a 100644 --- a/examples/gallery/script.rs +++ b/examples/gallery/script.rs @@ -8,51 +8,10 @@ use crate::{ capture::{number, popup_number, snapshot}, probe::{Opened, Probes}, shell::Gallery, + step::Step, swipe::{BEGAN, CHANGED, ENDED, swipe}, }; -/// One scripted input, aimed at a probe by key. -pub enum Step { - Rest, - Hover(&'static str), - /// Moves the pointer to an offset from the probe's top-left corner. - HoverAt(&'static str, f32, f32), - Down(&'static str), - Up(&'static str), - Click(&'static str), - /// Clicks just inside the probe's right edge. - ClickEnd(&'static str), - /// Presses at an offset from the probe's top-left corner. - DownAt(&'static str, f32, f32), - /// Right-clicks at an offset from the probe's top-left corner. - RightAt(&'static str, f32, f32), - /// Drags from the last point to an offset, in small steps. - DragTo(&'static str, f32, f32), - /// Swipes two fingers sideways across the probe's middle, as a trackpad does: begin, move, end. - Swipe(&'static str, f32), - UpAt(&'static str, f32, f32), - Key(&'static str), - /// Types text into whatever holds focus, one key at a time. - Type(&'static str), - Wait(u64), - Shot(&'static str), - /// Photographs a window a demo opened, by its key. - ShotWindow(&'static str, &'static str), - CloseWindow(&'static str), - /// Sends a keystroke to a window a demo opened. - KeyWindow(&'static str, &'static str), - /// Asks AppKit to close a window, as its close button would. - CloseNative(&'static str), - /// Fails unless the window is gone. - ExpectClosed(&'static str), - /// Photographs this app's frontmost popup, such as the share picker. - ShotPopup(&'static str), - /// Posts a key down to AppKit itself; `escape` and `5` are known. - NativeKey(&'static str), - /// Cancels the open file panel, whose content runs out of process. - CancelPanel, -} - const FRAME: Duration = Duration::from_millis(120); /// How long a shot waits for each frame. const SHOWN: Duration = Duration::from_secs(5); diff --git a/examples/gallery/shell.rs b/examples/gallery/shell.rs index 809db72..17672dc 100644 --- a/examples/gallery/shell.rs +++ b/examples/gallery/shell.rs @@ -1,16 +1,17 @@ use ely_gpui_component::{ buttons::{ButtonVariant, IconButton}, + feedback::Alert, layout::on_axis, - primitives::{FocusScope, IconName}, + primitives::{FocusScope, IconName, Severity}, theme::{ActiveTheme, ControlSize, Mode, Radius, TextSize, Theme}, }; use gpui::{ - AnyElement, App, Bounds, Context, FocusHandle, FontWeight, InteractiveElement, IntoElement, + AnyElement, App, Context, FocusHandle, FontWeight, InteractiveElement, IntoElement, ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, point, prelude::*, px, }; -use crate::pages; +use crate::{Start, pages, ui::Story}; #[derive(Clone, Copy, PartialEq, Eq)] pub enum Choice { @@ -21,6 +22,10 @@ pub enum Choice { pub struct Gallery { page: usize, + /// The one section drawn alone, full-bleed, if any. + story: Option, + /// Why nothing can be shown, in place of the page. + failure: Option, /// Counts fresh starts; the page's state lives under it. pass: usize, choice: Choice, @@ -32,12 +37,7 @@ pub struct Gallery { } impl Gallery { - pub fn new( - page: usize, - narrow: Option, - window: &mut Window, - cx: &mut Context, - ) -> Self { + pub fn new(start: Start, window: &mut Window, cx: &mut Context) -> Self { let appearance = cx.observe_window_appearance(window, |gallery, window, cx| { if gallery.choice == Choice::System { Theme::set_mode(window.appearance().into(), cx); @@ -46,12 +46,14 @@ impl Gallery { let focus = cx.focus_handle(); window.focus(&focus, cx); Self { - page, + page: start.page, + story: start.story, + failure: start.failure, pass: 0, - choice: Choice::Light, + choice: start.choice, focus, scroll: ScrollHandle::new(), - narrow: narrow.map(px), + narrow: start.narrow.map(px), _appearance: appearance, } } @@ -64,18 +66,21 @@ impl Gallery { } /// Drops the page's state, so it draws as new. + #[cfg(not(target_family = "wasm"))] pub fn fresh(&mut self, cx: &mut Context) { self.pass += 1; cx.notify(); } /// Viewport height and furthest scroll, in pixels. + #[cfg(not(target_family = "wasm"))] pub fn scroll_extent(&self) -> (Pixels, Pixels) { (self.scroll.bounds().size.height, self.scroll.max_offset().y) } /// Scrolls until `target` sits inside the page. True if it moved. - pub fn reveal(&mut self, target: Bounds, cx: &mut Context) -> bool { + #[cfg(not(target_family = "wasm"))] + pub fn reveal(&mut self, target: gpui::Bounds, cx: &mut Context) -> bool { let (view, margin) = (self.scroll.bounds(), px(96.0)); let shift = if target.top() < view.top() + margin { target.top() - view.top() - margin @@ -89,6 +94,7 @@ impl Gallery { true } + #[cfg(not(target_family = "wasm"))] pub fn scroll_to(&mut self, y: Pixels, cx: &mut Context) { self.scroll.set_offset(point(px(0.0), -y)); cx.notify(); @@ -220,72 +226,119 @@ impl Gallery { None => content.into_any_element(), } } + + /// The page's demos, or the story's alone; why not, once the start or the story failed. + fn body( + &mut self, + page: &pages::Page, + window: &mut Window, + cx: &mut Context, + ) -> Result { + if let Some(failure) = &self.failure { + return Err(failure.clone()); + } + if let Some(story) = &self.story { + cx.set_global(Story::new(story.clone())); + } + let body = window.with_id(("pass", self.pass), |window| (page.render)(window, cx)); + if self.story.is_none() { + return Ok(body); + } + match cx.remove_global::().missing(page.slug) { + None => Ok(body), + Some(missing) => { + log::error!("gallery: {missing}"); + let failure = SharedString::from(missing); + self.failure = Some(failure.clone()); + Err(failure) + } + } + } } impl Render for Gallery { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let page = &pages::ALL[self.page]; - let body = window.with_id(("pass", self.pass), |window| (page.render)(window, cx)); + let body = self.body(page, window, cx); let sidebar = self.sidebar(cx); let switcher = self.switcher(cx); let theme = cx.theme(); let colors = &theme.colors; - FocusScope::new(&self.focus) + let root = FocusScope::new(&self.focus) .root() .size_full() .flex() .bg(colors.bg) .text_color(colors.fg) .font_family(theme.font_family.clone()) - .text_size(theme.text_size(TextSize::Base)) - .child(sidebar) - .child( - on_axis(div().id(("page", self.pass))) - .flex_1() - .h_full() + .text_size(theme.text_size(TextSize::Base)); + let body = match body { + Ok(body) => body, + Err(failure) => { + return root.child( + div().w_full().p_6().child( + Alert::new("gallery-failure", Severity::Danger, "Nothing to show") + .body(failure), + ), + ); + } + }; + if self.story.is_some() { + return root.child( + on_axis(div().id(("story", self.pass))) + .size_full() .overflow_y_scroll() .track_scroll(&self.scroll) - .child( - div() - .max_w(px(960.0)) - .px(px(56.0)) - .pt(px(56.0)) - .pb(px(96.0)) - .child( - self.card( - div() - .flex() - .flex_wrap() - .items_start() - .justify_between() - .gap_6() - .child( - div() - .flex_1() - .min_w(px(200.0)) - .flex() - .flex_col() - .gap_2() - .child( - div() - .text_size(theme.text_size(TextSize::Xxl)) - .font_weight(FontWeight::SEMIBOLD) - .child(page.title), - ) - .child( - div() - .text_size(theme.text_size(TextSize::Md)) - .text_color(colors.fg_muted) - .child(page.summary), - ), - ) - .child(switcher), - body, - cx, - ), + .child(div().p_6().child(body)), + ); + } + root.child(sidebar).child( + on_axis(div().id(("page", self.pass))) + .flex_1() + .h_full() + .overflow_y_scroll() + .track_scroll(&self.scroll) + .child( + div() + .max_w(px(960.0)) + .px(px(56.0)) + .pt(px(56.0)) + .pb(px(96.0)) + .child( + self.card( + div() + .flex() + .flex_wrap() + .items_start() + .justify_between() + .gap_6() + .child( + div() + .flex_1() + .min_w(px(200.0)) + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_size(theme.text_size(TextSize::Xxl)) + .font_weight(FontWeight::SEMIBOLD) + .child(page.title), + ) + .child( + div() + .text_size(theme.text_size(TextSize::Md)) + .text_color(colors.fg_muted) + .child(page.summary), + ), + ) + .child(switcher), + body, + cx, ), - ), - ) + ), + ), + ) } } diff --git a/examples/gallery/step.rs b/examples/gallery/step.rs new file mode 100644 index 0000000..adce0cc --- /dev/null +++ b/examples/gallery/step.rs @@ -0,0 +1,41 @@ +/// One scripted input, aimed at a probe by key. +pub enum Step { + Rest, + Hover(&'static str), + /// Moves the pointer to an offset from the probe's top-left corner. + HoverAt(&'static str, f32, f32), + Down(&'static str), + Up(&'static str), + Click(&'static str), + /// Clicks just inside the probe's right edge. + ClickEnd(&'static str), + /// Presses at an offset from the probe's top-left corner. + DownAt(&'static str, f32, f32), + /// Right-clicks at an offset from the probe's top-left corner. + RightAt(&'static str, f32, f32), + /// Drags from the last point to an offset, in small steps. + DragTo(&'static str, f32, f32), + /// Swipes two fingers sideways across the probe's middle, as a trackpad does: begin, move, end. + Swipe(&'static str, f32), + UpAt(&'static str, f32, f32), + Key(&'static str), + /// Types text into whatever holds focus, one key at a time. + Type(&'static str), + Wait(u64), + Shot(&'static str), + /// Photographs a window a demo opened, by its key. + ShotWindow(&'static str, &'static str), + CloseWindow(&'static str), + /// Sends a keystroke to a window a demo opened. + KeyWindow(&'static str, &'static str), + /// Asks AppKit to close a window, as its close button would. + CloseNative(&'static str), + /// Fails unless the window is gone. + ExpectClosed(&'static str), + /// Photographs this app's frontmost popup, such as the share picker. + ShotPopup(&'static str), + /// Posts a key down to AppKit itself; `escape` and `5` are known. + NativeKey(&'static str), + /// Cancels the open file panel, whose content runs out of process. + CancelPanel, +} diff --git a/examples/gallery/stories.json b/examples/gallery/stories.json new file mode 100644 index 0000000..eb1b69e --- /dev/null +++ b/examples/gallery/stories.json @@ -0,0 +1,45 @@ +{"pages": [ +{"number": 1, "slug": "primitives", "title": "Primitives", "summary": "The smallest parts. Most are gpui itself; Ely adds the rest.", "stories": [{"title": "Box / View", "slug": "box-view", "components": ["Box", "View"]}, {"title": "Text", "slug": "text", "components": ["Text"]}, {"title": "Icon", "slug": "icon", "components": ["Icon"]}, {"title": "IconSet / IconRegistry", "slug": "iconset-iconregistry", "components": ["IconSet", "IconRegistry"]}, {"title": "Svg", "slug": "svg", "components": ["Svg"]}, {"title": "Image", "slug": "image", "components": ["Image"]}, {"title": "Canvas", "slug": "canvas", "components": ["Canvas"]}, {"title": "Divider / Separator", "slug": "divider-separator", "components": ["Divider", "Separator"]}, {"title": "Spacer", "slug": "spacer", "components": ["Spacer"]}, {"title": "Portal", "slug": "portal", "components": ["Portal"]}, {"title": "Overlay / Backdrop", "slug": "overlay-backdrop", "components": ["Overlay", "Backdrop"]}, {"title": "Slot", "slug": "slot", "components": ["Slot"]}, {"title": "Show / When", "slug": "show-when", "components": ["Show", "When"]}, {"title": "For / Each", "slug": "for-each", "components": ["For", "Each"]}, {"title": "Fragment", "slug": "fragment", "components": ["Fragment"]}, {"title": "VisuallyHidden", "slug": "visuallyhidden", "components": ["VisuallyHidden"]}, {"title": "FocusRing", "slug": "focusring", "components": ["FocusRing"]}, {"title": "FocusScope / FocusTrap", "slug": "focusscope-focustrap", "components": ["FocusScope", "FocusTrap"]}, {"title": "ClickOutside", "slug": "clickoutside", "components": ["ClickOutside"]}, {"title": "HoverArea", "slug": "hoverarea", "components": ["HoverArea"]}, {"title": "Pressable", "slug": "pressable", "components": ["Pressable"]}, {"title": "KeyboardHandler", "slug": "keyboardhandler", "components": ["KeyboardHandler"]}, {"title": "Measure", "slug": "measure", "components": ["Measure"]}, {"title": "IntersectionObserver", "slug": "intersectionobserver", "components": ["IntersectionObserver"]}, {"title": "Clipboard", "slug": "clipboard", "components": ["Clipboard"]}, {"title": "Tooltip Trigger", "slug": "tooltip-trigger", "components": ["Tooltip Trigger"]}]}, +{"number": 2, "slug": "typography", "title": "Typography", "summary": "Inter for words, tabular figures for numbers, one ink for everything.", "stories": [{"title": "Heading (H1–H6)", "slug": "heading-h1-h6", "components": ["Heading"]}, {"title": "Title / Subtitle", "slug": "title-subtitle", "components": ["Title", "Subtitle"]}, {"title": "Paragraph", "slug": "paragraph", "components": ["Paragraph"]}, {"title": "Label", "slug": "label", "components": ["Label"]}, {"title": "Caption", "slug": "caption", "components": ["Caption"]}, {"title": "Overline", "slug": "overline", "components": ["Overline"]}, {"title": "Code", "slug": "code", "components": ["Code"]}, {"title": "Kbd", "slug": "kbd", "components": ["Kbd"]}, {"title": "KbdCombo", "slug": "kbdcombo", "components": ["KbdCombo"]}, {"title": "Blockquote", "slug": "blockquote", "components": ["Blockquote"]}, {"title": "Highlight / Mark", "slug": "highlight-mark", "components": ["Highlight", "Mark"]}, {"title": "Truncate / Ellipsis", "slug": "truncate-ellipsis", "components": ["Truncate"]}, {"title": "LineClamp", "slug": "lineclamp", "components": ["LineClamp"]}, {"title": "MiddleEllipsis", "slug": "middleellipsis", "components": ["MiddleEllipsis"]}, {"title": "EllipsisTooltip", "slug": "ellipsistooltip", "components": ["EllipsisTooltip"]}, {"title": "Link", "slug": "link", "components": ["Link"]}, {"title": "ExternalLink", "slug": "externallink", "components": ["ExternalLink"]}, {"title": "RelativeTime", "slug": "relativetime", "components": ["RelativeTime"]}, {"title": "DateTimeText", "slug": "datetimetext", "components": ["DateTimeText"]}, {"title": "NumberText", "slug": "numbertext", "components": ["NumberText"]}, {"title": "CurrencyText", "slug": "currencytext", "components": ["CurrencyText"]}, {"title": "PercentText", "slug": "percenttext", "components": ["PercentText"]}, {"title": "FileSizeText", "slug": "filesizetext", "components": ["FileSizeText"]}, {"title": "DurationText", "slug": "durationtext", "components": ["DurationText"]}, {"title": "PluralText", "slug": "pluraltext", "components": ["PluralText"]}, {"title": "CopyableText", "slug": "copyabletext", "components": ["CopyableText"]}, {"title": "SelectableText", "slug": "selectabletext", "components": ["SelectableText"]}, {"title": "Emoji", "slug": "emoji", "components": ["Emoji"]}, {"title": "Latex / MathInline", "slug": "latex-mathinline", "components": ["Latex", "MathInline"]}, {"title": "AnimatedNumber", "slug": "animatednumber", "components": ["AnimatedNumber"]}, {"title": "Typewriter", "slug": "typewriter", "components": ["Typewriter"]}, {"title": "GradientText", "slug": "gradienttext", "components": ["GradientText"]}, {"title": "ShimmerText", "slug": "shimmertext", "components": ["ShimmerText"]}]}, +{"number": 3, "slug": "layout", "title": "Layout", "summary": "Stacks, grids, splits, docks. Structure that stays out of the way.", "stories": [{"title": "Stack / HStack / VStack", "slug": "stack-hstack-vstack", "components": ["Stack", "HStack", "VStack"]}, {"title": "Flex", "slug": "flex", "components": ["Flex"]}, {"title": "Grid / GridItem", "slug": "grid-griditem", "components": ["Grid", "GridItem"]}, {"title": "SimpleGrid", "slug": "simplegrid", "components": ["SimpleGrid"]}, {"title": "Masonry", "slug": "masonry", "components": ["Masonry"]}, {"title": "Center", "slug": "center", "components": ["Center"]}, {"title": "Container", "slug": "container", "components": ["Container"]}, {"title": "AspectRatio", "slug": "aspectratio", "components": ["AspectRatio"]}, {"title": "Inset / Padding", "slug": "inset-padding", "components": ["Inset", "Padding"]}, {"title": "Wrap", "slug": "wrap", "components": ["Wrap"]}, {"title": "Absolute / Positioned", "slug": "absolute-positioned", "components": ["Absolute", "Positioned"]}, {"title": "Layer / ZStack", "slug": "layer-zstack", "components": ["Layer", "ZStack"]}, {"title": "ScrollArea", "slug": "scrollarea", "components": ["ScrollArea"]}, {"title": "Scrollbar", "slug": "scrollbar", "components": ["Scrollbar"]}, {"title": "ScrollShadow", "slug": "scrollshadow", "components": ["ScrollShadow"]}, {"title": "ScrollToTop", "slug": "scrolltotop", "components": ["ScrollToTop"]}, {"title": "StickyHeader", "slug": "stickyheader", "components": ["StickyHeader"]}, {"title": "SplitPane", "slug": "splitpane", "components": ["SplitPane"]}, {"title": "ResizeHandle", "slug": "resizehandle", "components": ["ResizeHandle"]}, {"title": "Collapsible", "slug": "collapsible", "components": ["Collapsible"]}, {"title": "Accordion / AccordionItem", "slug": "accordion-accordionitem", "components": ["Accordion", "AccordionItem"]}, {"title": "Card / CardHeader / CardBody / CardFooter", "slug": "card-cardheader-cardbody-cardfooter", "components": ["Card", "CardHeader", "CardBody", "CardFooter"]}, {"title": "Panel / PanelHeader", "slug": "panel-panelheader", "components": ["Panel", "PanelHeader"]}, {"title": "Section / SectionHeader", "slug": "section-sectionheader", "components": ["Section", "SectionHeader"]}, {"title": "Group / Fieldset", "slug": "group-fieldset", "components": ["Group", "Fieldset"]}, {"title": "Frame", "slug": "frame", "components": ["Frame"]}, {"title": "Well", "slug": "well", "components": ["Well"]}, {"title": "Sidebar", "slug": "sidebar", "components": ["Sidebar"]}, {"title": "Drawer", "slug": "drawer", "components": ["Drawer"]}, {"title": "Sheet", "slug": "sheet", "components": ["Sheet"]}, {"title": "Page / PageHeader / PageContent", "slug": "page-pageheader-pagecontent", "components": ["Page", "PageHeader", "PageContent"]}, {"title": "AppShell", "slug": "appshell", "components": ["AppShell"]}, {"title": "MasterDetail", "slug": "masterdetail", "components": ["MasterDetail"]}, {"title": "Dock / DockPanel", "slug": "dock-dockpanel", "components": ["Dock", "DockPanel"]}, {"title": "DockZone / DropIndicator", "slug": "dockzone-dropindicator", "components": ["DockZone", "DropIndicator"]}, {"title": "FloatingPanel", "slug": "floatingpanel", "components": ["FloatingPanel"]}, {"title": "Pane / PaneGroup", "slug": "pane-panegroup", "components": ["Pane", "PaneGroup"]}, {"title": "Workspace", "slug": "workspace", "components": ["Workspace"]}, {"title": "Viewport", "slug": "viewport", "components": ["Viewport"]}]}, +{"number": 4, "slug": "shell", "title": "Window & Shell", "summary": "Title bars, rails, tabs, and the windows around an app.", "stories": [{"title": "TitleBar", "slug": "titlebar", "components": ["TitleBar"]}, {"title": "WindowControls / TrafficLights", "slug": "windowcontrols-trafficlights", "components": ["WindowControls", "TrafficLights"]}, {"title": "WindowDragRegion", "slug": "windowdragregion", "components": ["WindowDragRegion"]}, {"title": "WindowResizeBorder", "slug": "windowresizeborder", "components": ["WindowResizeBorder"]}, {"title": "NativeMenu / MenuBar / JumpList", "slug": "nativemenu-menubar-jumplist", "components": ["NativeMenu", "MenuBar", "JumpList"]}, {"title": "Toolbar / ToolbarGroup / ToolbarSeparator", "slug": "toolbar-toolbargroup-toolbarseparator", "components": ["Toolbar", "ToolbarGroup", "ToolbarSeparator"]}, {"title": "StatusBar / StatusBarItem", "slug": "statusbar-statusbaritem", "components": ["StatusBar", "StatusBarItem"]}, {"title": "ActivityBar / NavigationRail", "slug": "activitybar-navigationrail", "components": ["ActivityBar", "NavigationRail"]}, {"title": "TabBar", "slug": "tabbar", "components": ["TabBar"]}, {"title": "SplashScreen", "slug": "splashscreen", "components": ["SplashScreen"]}, {"title": "AboutDialog", "slug": "aboutdialog", "components": ["AboutDialog"]}, {"title": "TrayIcon / TrayMenu", "slug": "trayicon-traymenu", "components": ["TrayIcon", "TrayMenu"]}, {"title": "DockBadge", "slug": "dockbadge", "components": ["DockBadge"]}, {"title": "SystemNotification", "slug": "systemnotification", "components": ["SystemNotification"]}, {"title": "MultiWindow Manager", "slug": "multiwindow-manager", "components": ["MultiWindow Manager"]}, {"title": "WindowSwitcher", "slug": "windowswitcher", "components": ["WindowSwitcher"]}, {"title": "QuickLauncher", "slug": "quicklauncher", "components": ["QuickLauncher"]}, {"title": "MiniWindow / CompactMode / PictureInPicture", "slug": "miniwindow-compactmode-pictureinpicture", "components": ["MiniWindow", "CompactMode", "PictureInPicture"]}, {"title": "UpdateBanner", "slug": "updatebanner", "components": ["UpdateBanner"]}, {"title": "UpdateDialog", "slug": "updatedialog", "components": ["UpdateDialog"]}, {"title": "CrashReporter", "slug": "crashreporter", "components": ["CrashReporter"]}, {"title": "OfflineIndicator", "slug": "offlineindicator", "components": ["OfflineIndicator"]}, {"title": "ZoomControl", "slug": "zoomcontrol", "components": ["ZoomControl"]}]}, +{"number": 5, "slug": "buttons", "title": "Buttons & Actions", "summary": "Seven variants, three sizes, one focus ring, and the verbs around them.", "stories": [{"title": "Button", "slug": "button", "components": ["Button"]}, {"title": "Sizes", "slug": "sizes", "components": []}, {"title": "With icons", "slug": "with-icons", "components": []}, {"title": "Icon button", "slug": "icon-button", "components": ["IconButton"]}, {"title": "ShortcutHint", "slug": "shortcuthint", "components": ["ShortcutHint"]}, {"title": "ButtonGroup", "slug": "buttongroup", "components": ["ButtonGroup"]}, {"title": "SplitButton / DropdownButton / MenuButton / LoadingButton", "slug": "splitbutton-dropdownbutton-menubutton-loadingbutton", "components": ["SplitButton", "DropdownButton", "MenuButton", "LoadingButton"]}, {"title": "ToggleButton", "slug": "togglebutton", "components": ["ToggleButton"]}, {"title": "ToggleGroup", "slug": "togglegroup", "components": ["ToggleGroup"]}, {"title": "SegmentedControl", "slug": "segmentedcontrol", "components": ["SegmentedControl"]}, {"title": "FloatingActionButton", "slug": "floatingactionbutton", "components": ["FloatingActionButton"]}, {"title": "ConfirmButton / HoldToConfirm", "slug": "confirmbutton-holdtoconfirm", "components": ["ConfirmButton", "HoldToConfirm"]}, {"title": "CopyButton / ShareButton / CloseButton / BackButton / MoreButton", "slug": "copybutton-sharebutton-closebutton-backbutton-morebutton", "components": ["CopyButton", "ShareButton", "CloseButton", "BackButton", "MoreButton"]}, {"title": "ActionBar", "slug": "actionbar", "components": ["ActionBar"]}, {"title": "BulkActionBar", "slug": "bulkactionbar", "components": ["BulkActionBar"]}, {"title": "ActionSheet", "slug": "actionsheet", "components": ["ActionSheet"]}, {"title": "QuickActions", "slug": "quickactions", "components": ["QuickActions"]}]}, +{"number": 6, "slug": "forms", "title": "Forms", "summary": "Text, choices, dates, colors, files: every way to say what you mean.", "stories": [{"title": "Input / TextField / ClearableInput", "slug": "input-textfield-clearableinput", "components": ["Input", "TextField", "ClearableInput"]}, {"title": "TextArea / AutoResizeInput", "slug": "textarea-autoresizeinput", "components": ["TextArea", "AutoResizeInput"]}, {"title": "PasswordInput", "slug": "passwordinput", "components": ["PasswordInput"]}, {"title": "SearchInput", "slug": "searchinput", "components": ["SearchInput"]}, {"title": "NumberInput", "slug": "numberinput", "components": ["NumberInput"]}, {"title": "ScrubInput", "slug": "scrubinput", "components": ["ScrubInput"]}, {"title": "CurrencyInput / PercentInput / UnitInput", "slug": "currencyinput-percentinput-unitinput", "components": ["CurrencyInput", "PercentInput", "UnitInput"]}, {"title": "EmailInput / UrlInput", "slug": "emailinput-urlinput", "components": ["EmailInput", "UrlInput"]}, {"title": "PhoneInput / MaskedInput", "slug": "phoneinput-maskedinput", "components": ["PhoneInput", "MaskedInput"]}, {"title": "PinInput / OTPInput", "slug": "pininput-otpinput", "components": ["PinInput", "OTPInput"]}, {"title": "InlineEdit / EditableText", "slug": "inlineedit-editabletext", "components": ["InlineEdit", "EditableText"]}, {"title": "InputGroup / InputAddon", "slug": "inputgroup-inputaddon", "components": ["InputGroup", "InputAddon"]}, {"title": "MentionInput / HashtagInput", "slug": "mentioninput-hashtaginput", "components": ["MentionInput", "HashtagInput"]}, {"title": "TagInput / TokenInput", "slug": "taginput-tokeninput", "components": ["TagInput", "TokenInput"]}, {"title": "KeyValueInput / ListInput", "slug": "keyvalueinput-listinput", "components": ["KeyValueInput", "ListInput"]}, {"title": "PathInput", "slug": "pathinput", "components": ["PathInput"]}, {"title": "RegexInput", "slug": "regexinput", "components": ["RegexInput"]}, {"title": "ExpressionInput", "slug": "expressioninput", "components": ["ExpressionInput"]}, {"title": "HotkeyInput / ShortcutRecorder", "slug": "hotkeyinput-shortcutrecorder", "components": ["HotkeyInput", "ShortcutRecorder"]}, {"title": "Checkbox / CheckboxGroup", "slug": "checkbox-checkboxgroup", "components": ["Checkbox", "CheckboxGroup"]}, {"title": "Radio / RadioGroup", "slug": "radio-radiogroup", "components": ["Radio", "RadioGroup"]}, {"title": "CheckboxCard / RadioCard", "slug": "checkboxcard-radiocard", "components": ["CheckboxCard", "RadioCard"]}, {"title": "Switch / Toggle", "slug": "switch-toggle", "components": ["Switch", "Toggle"]}, {"title": "Select / NativeSelect", "slug": "select-nativeselect", "components": ["Select", "NativeSelect"]}, {"title": "ListBox", "slug": "listbox", "components": ["ListBox"]}, {"title": "Combobox / Autocomplete", "slug": "combobox-autocomplete", "components": ["Combobox", "Autocomplete"]}, {"title": "MultiSelect", "slug": "multiselect", "components": ["MultiSelect"]}, {"title": "CascadeSelect / Cascader", "slug": "cascadeselect-cascader", "components": ["CascadeSelect", "Cascader"]}, {"title": "TransferList", "slug": "transferlist", "components": ["Transfer List"]}, {"title": "ChoiceChips / FilterChips", "slug": "choicechips-filterchips", "components": ["ChoiceChips", "FilterChips"]}, {"title": "Rating", "slug": "rating", "components": ["Rating"]}, {"title": "Slider / RangeSlider / VerticalSlider", "slug": "slider-rangeslider-verticalslider", "components": ["Slider", "RangeSlider", "VerticalSlider"]}, {"title": "Knob / Dial / Stepper", "slug": "knob-dial-stepper", "components": ["Knob", "Dial", "Stepper"]}, {"title": "Calendar", "slug": "calendar", "components": ["Calendar"]}, {"title": "DatePicker / DateRangePicker / DateTimePicker / TimePicker", "slug": "datepicker-daterangepicker-datetimepicker-timepicker", "components": ["DatePicker", "DateRangePicker", "DateTimePicker", "TimePicker"]}, {"title": "MonthPicker / YearPicker / QuarterPicker / WeekPicker", "slug": "monthpicker-yearpicker-quarterpicker-weekpicker", "components": ["MonthPicker", "YearPicker", "QuarterPicker", "WeekPicker"]}, {"title": "RelativeDatePicker / TimezoneSelect / DurationPicker", "slug": "relativedatepicker-timezoneselect-durationpicker", "components": ["RelativeDatePicker", "TimezoneSelect", "DurationPicker"]}, {"title": "CronEditor", "slug": "croneditor", "components": ["CronEditor"]}, {"title": "ColorPicker", "slug": "colorpicker", "components": ["ColorPicker"]}, {"title": "ColorSwatch / ColorPalette / EyeDropper", "slug": "colorswatch-colorpalette-eyedropper", "components": ["ColorSwatch", "ColorPalette", "EyeDropper"]}, {"title": "GradientEditor", "slug": "gradienteditor", "components": ["GradientEditor"]}, {"title": "FileInput / DropZone", "slug": "fileinput-dropzone", "components": ["FileInput", "DropZone"]}, {"title": "SignaturePad", "slug": "signaturepad", "components": ["SignaturePad"]}, {"title": "CodeInput / JsonInput", "slug": "codeinput-jsoninput", "components": ["CodeInput", "JsonInput"]}, {"title": "FontPicker", "slug": "fontpicker", "components": ["FontPicker"]}, {"title": "IconPicker / EmojiPicker", "slug": "iconpicker-emojipicker", "components": ["IconPicker", "EmojiPicker"]}, {"title": "LanguageSelect / CountrySelect / CurrencySelect", "slug": "languageselect-countryselect-currencyselect", "components": ["LanguageSelect", "CountrySelect", "CurrencySelect"]}, {"title": "Form / FormSection / FormField / FieldArray / DirtyIndicator", "slug": "form-formsection-formfield-fieldarray-dirtyindicator", "components": ["Form", "FormSection", "FormField", "FieldArray", "DirtyIndicator"]}, {"title": "InlineForm", "slug": "inlineform", "components": ["InlineForm"]}]}, +{"number": 7, "slug": "navigation", "title": "Navigation", "summary": "Tabs, paths, places and palettes: where you are, and the ways out.", "stories": [{"title": "Tabs", "slug": "tabs", "components": ["Tabs"]}, {"title": "EditorTabs / TabOverflowMenu", "slug": "editortabs-taboverflowmenu", "components": ["EditorTabs", "TabOverflowMenu"]}, {"title": "Breadcrumb", "slug": "breadcrumb", "components": ["Breadcrumb"]}, {"title": "Pagination", "slug": "pagination", "components": ["Pagination"]}, {"title": "Steps / Wizard", "slug": "steps-wizard", "components": ["Steps", "Wizard"]}, {"title": "Sidebar Navigation / NavItem / NavGroup", "slug": "sidebar-navigation-navitem-navgroup", "components": ["Sidebar Navigation", "NavItem", "NavGroup"]}, {"title": "NavigationMenu", "slug": "navigationmenu", "components": ["NavigationMenu"]}, {"title": "BackForwardNavigation / History", "slug": "backforwardnavigation-history", "components": ["BackForwardNavigation"]}, {"title": "Anchor / TableOfContents", "slug": "anchor-tableofcontents", "components": ["Anchor", "TableOfContents"]}, {"title": "JumpTo / GoToLine", "slug": "jumpto-gotoline", "components": ["JumpTo", "GoToLine"]}, {"title": "CommandPalette", "slug": "commandpalette", "components": ["CommandPalette"]}, {"title": "QuickOpen", "slug": "quickopen", "components": ["QuickOpen"]}, {"title": "QuickSwitcher", "slug": "quickswitcher", "components": ["QuickSwitcher"]}, {"title": "SearchPalette / SpotlightSearch / QuickLauncher", "slug": "searchpalette-spotlightsearch-quicklauncher", "components": ["SearchPalette", "SpotlightSearch", "QuickLauncher"]}]}, +{"number": 8, "slug": "menus", "title": "Menus", "summary": "Rows of actions that open from a button, a right click or an arrow, and nest.", "stories": [{"title": "Menu / MenuItem / MenuGroup / MenuSeparator / SubMenu / MenuItemShortcut / MenuItemIcon", "slug": "menu-menuitem-menugroup-menuseparator-submenu-menuitemshortcut-menuitemicon", "components": ["Menu", "MenuItem", "MenuGroup", "MenuSeparator", "SubMenu", "MenuItemShortcut", "MenuItemIcon"]}, {"title": "CheckboxMenuItem / RadioMenuItem", "slug": "checkboxmenuitem-radiomenuitem", "components": ["CheckboxMenuItem", "RadioMenuItem"]}, {"title": "DropdownMenu / SplitButton / OverflowMenu", "slug": "dropdownmenu-splitbutton-overflowmenu", "components": ["DropdownMenu", "SplitButton", "OverflowMenu"]}, {"title": "ContextMenu", "slug": "contextmenu", "components": ["ContextMenu"]}, {"title": "MenuBar Menu", "slug": "menubar-menu", "components": ["MenuBar Menu"]}, {"title": "MegaMenu", "slug": "megamenu", "components": ["MegaMenu"]}, {"title": "SearchableMenu", "slug": "searchablemenu", "components": ["SearchableMenu"]}, {"title": "PieMenu / RadialMenu", "slug": "piemenu-radialmenu", "components": ["PieMenu", "RadialMenu"]}]}, +{"number": 9, "slug": "overlays", "title": "Overlays", "summary": "Panels over the page: anchored to a control, over a scrim, or over everything.", "stories": [{"title": "Popover / DropdownPanel / InlinePopup", "slug": "popover-dropdownpanel-inlinepopup", "components": ["Popover", "DropdownPanel", "InlinePopup"]}, {"title": "HoverCard", "slug": "hovercard", "components": ["HoverCard"]}, {"title": "Dialog / Modal / AlertDialog / ConfirmDialog / PromptDialog / FullscreenDialog", "slug": "dialog-modal-alertdialog-confirmdialog-promptdialog-fullscreendialog", "components": ["Dialog", "Modal", "AlertDialog", "ConfirmDialog", "PromptDialog", "FullscreenDialog"]}, {"title": "Lightbox", "slug": "lightbox", "components": ["Lightbox"]}, {"title": "Spotlight / Coachmark / Tour", "slug": "spotlight-coachmark-tour", "components": ["Spotlight", "Coachmark", "Tour"]}, {"title": "FloatingToolbar", "slug": "floatingtoolbar", "components": ["FloatingToolbar"]}, {"title": "Callout / Peek", "slug": "callout-peek", "components": ["Callout", "Peek"]}, {"title": "Tooltip / Sheet / Drawer / DropdownPanel / InlinePopup / ToastViewport", "slug": "tooltip-sheet-drawer-dropdownpanel-inlinepopup-toastviewport", "components": ["Tooltip", "Sheet", "Drawer", "DropdownPanel", "InlinePopup", "ToastViewport"]}]}, +{"number": 10, "slug": "feedback", "title": "Feedback", "summary": "What the app tells you: toasts, notifications, alerts and the states a view can be in.", "stories": [{"title": "Toast / Snackbar / UndoToast", "slug": "toast-snackbar-undotoast", "components": ["Toast", "Snackbar", "UndoToast"]}, {"title": "Notification / NotificationCenter", "slug": "notification-notificationcenter", "components": ["Notification", "NotificationCenter"]}, {"title": "Alert", "slug": "alert", "components": ["Alert"]}, {"title": "Banner", "slug": "banner", "components": ["Banner"]}, {"title": "Callout / Admonition", "slug": "callout-admonition", "components": ["Callout", "Admonition"]}, {"title": "InlineMessage / StatusMessage", "slug": "inlinemessage-statusmessage", "components": ["InlineMessage", "StatusMessage"]}, {"title": "EmptyState", "slug": "emptystate", "components": ["EmptyState"]}, {"title": "ErrorView / NotFound / NoPermission / Maintenance / Offline", "slug": "errorview-notfound-nopermission-maintenance-offline", "components": ["ErrorView", "NotFound", "NoPermission", "Maintenance", "Offline"]}, {"title": "ErrorBoundary", "slug": "errorboundary", "components": ["ErrorBoundary"]}, {"title": "Result", "slug": "result", "components": ["Result"]}, {"title": "ConfirmationCard", "slug": "confirmationcard", "components": ["ConfirmationCard"]}, {"title": "SavingIndicator / SyncStatus / ConnectionStatus", "slug": "savingindicator-syncstatus-connectionstatus", "components": ["SavingIndicator", "SyncStatus", "ConnectionStatus"]}, {"title": "Countdown / Timer", "slug": "countdown-timer", "components": ["Countdown", "Timer"]}]}, +{"number": 11, "slug": "motion", "title": "Loading & Motion", "summary": "Waiting, shown honestly, and the motion everything else moves with.", "stories": [{"title": "Spinner / DotsLoader / BarsLoader / PulseLoader / RingLoader / OrbitLoader / WaveLoader / InlineLoader", "slug": "spinner-dotsloader-barsloader-pulseloader-ringloader-orbitloader-waveloader-inlineloader", "components": ["Spinner", "DotsLoader", "BarsLoader", "PulseLoader", "RingLoader", "OrbitLoader", "WaveLoader", "InlineLoader"]}, {"title": "Skeleton / SkeletonText / SkeletonAvatar / SkeletonCard / SkeletonTable", "slug": "skeleton-skeletontext-skeletonavatar-skeletoncard-skeletontable", "components": ["Skeleton", "SkeletonText", "SkeletonAvatar", "SkeletonCard", "SkeletonTable"]}, {"title": "Shimmer", "slug": "shimmer", "components": ["Shimmer"]}, {"title": "LoadingButton", "slug": "loadingbutton", "components": ["LoadingButton"]}, {"title": "LoadMore", "slug": "loadmore", "components": ["LoadMore"]}, {"title": "ProgressBar / ProgressCircle / ProgressRing", "slug": "progressbar-progresscircle-progressring", "components": ["ProgressBar", "ProgressCircle", "ProgressRing"]}, {"title": "LoadingOverlay", "slug": "loadingoverlay", "components": ["LoadingOverlay"]}, {"title": "LazyLoad", "slug": "lazyload", "components": ["LazyLoad"]}, {"title": "Suspense / AsyncView", "slug": "suspense-asyncview", "components": ["Suspense", "AsyncView"]}, {"title": "RefreshIndicator / TypingIndicator", "slug": "refreshindicator-typingindicator", "components": ["RefreshIndicator", "TypingIndicator"]}, {"title": "UploadList", "slug": "uploadlist", "components": ["UploadList"]}, {"title": "Transition", "slug": "transition", "components": ["Transition"]}, {"title": "Stagger", "slug": "stagger", "components": ["Stagger"]}, {"title": "AnimatePresence", "slug": "animatepresence", "components": ["AnimatePresence"]}, {"title": "Flip", "slug": "flip", "components": ["Flip"]}, {"title": "Reorder", "slug": "reorder", "components": ["Reorder"]}, {"title": "Marquee", "slug": "marquee", "components": ["Marquee"]}, {"title": "Ripple / Confetti", "slug": "ripple-confetti", "components": ["Ripple", "Confetti"]}, {"title": "Glow / Pulse", "slug": "glow-pulse", "components": ["Glow", "Pulse"]}, {"title": "Blink / Flash / Shake", "slug": "blink-flash-shake", "components": ["Blink", "Flash", "Shake"]}, {"title": "ParticleBackground / AnimatedGradient / Lottie Player", "slug": "particlebackground-animatedgradient-lottie-player", "components": ["ParticleBackground", "AnimatedGradient", "Lottie Player"]}]}, +{"number": 12, "slug": "data", "title": "Data Display", "summary": "People, numbers and records, shown plainly.", "stories": [{"title": "Badge / CountBadge / DotBadge", "slug": "badge-countbadge-dotbadge", "components": ["Badge", "CountBadge", "DotBadge"]}, {"title": "Tag / Chip", "slug": "tag-chip", "components": ["Tag", "Chip"]}, {"title": "Avatar / AvatarUpload", "slug": "avatar-avatarupload", "components": ["Avatar", "AvatarUpload"]}, {"title": "AvatarGroup / UserChip", "slug": "avatargroup-userchip", "components": ["AvatarGroup", "UserChip"]}, {"title": "Statistic / KPI Card / Metric / TrendIndicator", "slug": "statistic-kpi-card-metric-trendindicator", "components": ["Statistic", "KPI Card", "Metric", "TrendIndicator"]}, {"title": "DescriptionList / KeyValueList / InfoRow", "slug": "descriptionlist-keyvaluelist-inforow", "components": ["DescriptionList", "KeyValueList", "InfoRow"]}, {"title": "PropertyGrid", "slug": "propertygrid", "components": ["PropertyGrid"]}, {"title": "Timeline / TimelineItem", "slug": "timeline-timelineitem", "components": ["Timeline", "TimelineItem"]}, {"title": "ActivityFeed", "slug": "activityfeed", "components": ["ActivityFeed"]}, {"title": "Changelog", "slug": "changelog", "components": ["Changelog"]}, {"title": "Carousel", "slug": "carousel", "components": ["Carousel"]}, {"title": "Gallery", "slug": "gallery", "components": ["Gallery"]}, {"title": "BeforeAfter", "slug": "beforeafter", "components": ["BeforeAfter"]}, {"title": "Watermark", "slug": "watermark", "components": ["Watermark"]}, {"title": "QRCode / Barcode", "slug": "qrcode-barcode", "components": ["QRCode", "Barcode"]}, {"title": "Meter / Gauge / UsageBar", "slug": "meter-gauge-usagebar", "components": ["Meter", "Gauge", "UsageBar"]}, {"title": "Stars / Rating display", "slug": "stars-rating-display", "components": ["Rating display"]}, {"title": "Comparison", "slug": "comparison", "components": ["Comparison"]}]}, +{"number": 13, "slug": "lists", "title": "Lists & Trees", "summary": "Rows to read, pick, reorder and swipe; lists long enough to need care.", "stories": [{"title": "List / ListItem", "slug": "list-listitem", "components": ["List", "ListItem"]}, {"title": "SelectableList", "slug": "selectablelist", "components": ["SelectableList"]}, {"title": "SortableList / ReorderableList", "slug": "sortablelist-reorderablelist", "components": ["SortableList", "ReorderableList"]}, {"title": "SwipeableListItem", "slug": "swipeablelistitem", "components": ["SwipeableListItem"]}, {"title": "VirtualList", "slug": "virtuallist", "components": ["VirtualList"]}, {"title": "InfiniteList", "slug": "infinitelist", "components": ["InfiniteList"]}, {"title": "GroupedList / SectionList", "slug": "groupedlist-sectionlist", "components": ["GroupedList", "SectionList"]}, {"title": "Tree / TreeView / VirtualTree", "slug": "tree-treeview-virtualtree", "components": ["Tree", "TreeView", "VirtualTree"]}, {"title": "CheckboxTree", "slug": "checkboxtree", "components": ["CheckboxTree"]}, {"title": "TreeSelect", "slug": "treeselect", "components": ["TreeSelect"]}, {"title": "Outline / NestedList", "slug": "outline-nestedlist", "components": ["Outline", "NestedList"]}, {"title": "FileTree", "slug": "filetree", "components": ["FileTree"]}, {"title": "DirectoryListing", "slug": "directorylisting", "components": ["DirectoryListing"]}]}, +{"number": 14, "slug": "tables", "title": "Tables", "summary": "Rows and columns that sort, filter, page and select, and stay quick at a hundred thousand rows.", "stories": [{"title": "Table", "slug": "table", "components": ["Table"]}, {"title": "DataTable / TableToolbar / FilterBuilder / SortBuilder / ColumnVisibility / CellRenderer / RowSelection / TableDensity / AggregationFooter / ConditionalFormatting", "slug": "datatable-tabletoolbar-filterbuilder-sortbuilder-columnvisibility-cellrenderer-rowselection-tabledensity-aggregationfooter-conditionalformatting", "components": ["DataTable", "TableToolbar", "FilterBuilder", "SortBuilder", "ColumnVisibility", "CellRenderer", "RowSelection", "TableDensity", "AggregationFooter", "ConditionalFormatting"]}, {"title": "ColumnPinning / ColumnResizer / ColumnReorder", "slug": "columnpinning-columnresizer-columnreorder", "components": ["ColumnPinning", "ColumnResizer", "ColumnReorder"]}, {"title": "RowExpansion / InlineRowEdit", "slug": "rowexpansion-inlinerowedit", "components": ["RowExpansion", "InlineRowEdit"]}, {"title": "RowGrouping", "slug": "rowgrouping", "components": ["RowGrouping"]}, {"title": "DataGrid", "slug": "datagrid", "components": ["DataGrid"]}, {"title": "Spreadsheet", "slug": "spreadsheet", "components": ["Spreadsheet"]}, {"title": "TreeTable", "slug": "treetable", "components": ["TreeTable"]}, {"title": "PivotTable", "slug": "pivottable", "components": ["PivotTable"]}, {"title": "VirtualTable", "slug": "virtualtable", "components": ["VirtualTable"]}, {"title": "HeatmapTable", "slug": "heatmaptable", "components": ["HeatmapTable"]}]}, +{"number": 15, "slug": "charts", "title": "Charts", "summary": "Lines, bars, parts of a whole, spreads, flows and schedules on quiet axes, with legends, tooltips, zoom and export.", "stories": [{"title": "LineChart / ChartAxis / ChartGrid / ChartLegend / ChartTooltip / ChartCrosshair / ChartAnnotation / ChartZoom / ChartExport", "slug": "linechart-chartaxis-chartgrid-chartlegend-charttooltip-chartcrosshair-chartannotation-chartzoom-chartexport", "components": ["LineChart", "ChartAxis", "ChartGrid", "ChartLegend", "ChartTooltip", "ChartCrosshair", "ChartAnnotation", "ChartZoom", "ChartExport"]}, {"title": "AreaChart / StackedArea / BarChart", "slug": "areachart-stackedarea-barchart", "components": ["AreaChart", "StackedArea", "BarChart"]}, {"title": "PieChart / DonutChart", "slug": "piechart-donutchart", "components": ["PieChart", "DonutChart"]}, {"title": "ScatterChart / BubbleChart", "slug": "scatterchart-bubblechart", "components": ["ScatterChart", "BubbleChart"]}, {"title": "RadarChart", "slug": "radarchart", "components": ["RadarChart"]}, {"title": "HeatmapChart / CalendarHeatmap", "slug": "heatmapchart-calendarheatmap", "components": ["HeatmapChart", "CalendarHeatmap"]}, {"title": "Treemap / Sunburst", "slug": "treemap-sunburst", "components": ["Treemap", "Sunburst"]}, {"title": "SankeyChart / FunnelChart", "slug": "sankeychart-funnelchart", "components": ["SankeyChart", "FunnelChart"]}, {"title": "WaterfallChart", "slug": "waterfallchart", "components": ["WaterfallChart"]}, {"title": "Histogram / BoxPlot / ViolinPlot", "slug": "histogram-boxplot-violinplot", "components": ["Histogram", "BoxPlot", "ViolinPlot"]}, {"title": "GanttChart", "slug": "ganttchart", "components": ["GanttChart"]}, {"title": "NetworkGraph / ForceGraph / ChordDiagram / ParallelCoordinates", "slug": "networkgraph-forcegraph-chorddiagram-parallelcoordinates", "components": ["NetworkGraph", "ForceGraph", "ChordDiagram", "ParallelCoordinates"]}, {"title": "Bullet Chart / ProgressChart", "slug": "bullet-chart-progresschart", "components": ["Bullet Chart", "ProgressChart"]}, {"title": "RealtimeChart", "slug": "realtimechart", "components": ["RealtimeChart"]}]}, +{"number": 16, "slug": "finance", "title": "Finance", "summary": "Market charts with the studies traders read, books, quotes, order entry and portfolios.", "stories": [{"title": "CandlestickChart / OHLCChart / HeikinAshiChart / LineQuoteChart / VolumeChart / ChartTypeSwitcher / TimeRangeSelector / IntervalSelector", "slug": "candlestickchart-ohlcchart-heikinashichart-linequotechart-volumechart-charttypeswitcher-timerangeselector-intervalselector", "components": ["CandlestickChart", "OHLCChart", "HeikinAshiChart", "LineQuoteChart", "VolumeChart", "ChartTypeSwitcher", "TimeRangeSelector", "IntervalSelector"]}, {"title": "IndicatorOverlay / IndicatorPane / IndicatorSelector / CompareSymbol / PriceAlertLine", "slug": "indicatoroverlay-indicatorpane-indicatorselector-comparesymbol-pricealertline", "components": ["IndicatorOverlay", "IndicatorPane", "IndicatorSelector", "CompareSymbol", "PriceAlertLine"]}, {"title": "DrawingTools / DrawingToolbar", "slug": "drawingtools-drawingtoolbar", "components": ["DrawingTools", "DrawingToolbar"]}, {"title": "DepthChart", "slug": "depthchart", "components": ["DepthChart"]}, {"title": "MarketProfile / VolumeProfile", "slug": "marketprofile-volumeprofile", "components": ["MarketProfile", "VolumeProfile"]}, {"title": "RenkoChart / PointAndFigure", "slug": "renkochart-pointandfigure", "components": ["RenkoChart", "PointAndFigure"]}, {"title": "MultiChartLayout / ChartSync", "slug": "multichartlayout-chartsync", "components": ["MultiChartLayout", "ChartSync"]}, {"title": "Ticker / TickerTape / QuoteCard / PriceText / PriceChangeBadge", "slug": "ticker-tickertape-quotecard-pricetext-pricechangebadge", "components": ["Ticker", "TickerTape", "QuoteCard", "PriceText", "PriceChangeBadge"]}, {"title": "OrderBook / SpreadIndicator / BidAskBar / Level2 Quotes", "slug": "orderbook-spreadindicator-bidaskbar-level2-quotes", "components": ["OrderBook", "SpreadIndicator", "BidAskBar", "Level2 Quotes"]}, {"title": "TimeAndSales / TradeTape / MarketDepthLadder / DOM", "slug": "timeandsales-tradetape-marketdepthladder-dom", "components": ["TimeAndSales", "TradeTape", "MarketDepthLadder", "DOM"]}, {"title": "OrderEntry / TradePanel / QuickTradeButtons / OrderConfirmDialog", "slug": "orderentry-tradepanel-quicktradebuttons-orderconfirmdialog", "components": ["OrderEntry", "TradePanel", "QuickTradeButtons", "OrderConfirmDialog"]}, {"title": "PositionTable / OrderTable / TradeHistoryTable / PnLDisplay", "slug": "positiontable-ordertable-tradehistorytable-pnldisplay", "components": ["PositionTable", "OrderTable", "TradeHistoryTable", "PnLDisplay"]}, {"title": "LeverageSlider / MarginIndicator / RiskMeter", "slug": "leverageslider-marginindicator-riskmeter", "components": ["LeverageSlider", "MarginIndicator", "RiskMeter"]}, {"title": "OptionChain / GreeksTable / PayoffDiagram", "slug": "optionchain-greekstable-payoffdiagram", "components": ["OptionChain", "GreeksTable", "PayoffDiagram"]}, {"title": "Watchlist / SymbolBadge / SymbolSearch", "slug": "watchlist-symbolbadge-symbolsearch", "components": ["Watchlist", "SymbolBadge", "SymbolSearch"]}, {"title": "MarketHeatmap / SectorTreemap", "slug": "marketheatmap-sectortreemap", "components": ["MarketHeatmap", "SectorTreemap"]}, {"title": "Screener / FilterPanel", "slug": "screener-filterpanel", "components": ["Screener", "FilterPanel"]}, {"title": "MarketOverview / IndexCard / MarketStatus / TradingSessionClock", "slug": "marketoverview-indexcard-marketstatus-tradingsessionclock", "components": ["MarketOverview", "IndexCard", "MarketStatus", "TradingSessionClock"]}, {"title": "EconomicCalendar / EarningsCalendar / NewsFeed", "slug": "economiccalendar-earningscalendar-newsfeed", "components": ["EconomicCalendar", "EarningsCalendar", "NewsFeed"]}, {"title": "PortfolioSummary / AssetAllocationChart / PerformanceChart / SentimentGauge", "slug": "portfoliosummary-assetallocationchart-performancechart-sentimentgauge", "components": ["PortfolioSummary", "AssetAllocationChart", "PerformanceChart", "SentimentGauge"]}, {"title": "DividendTable / FinancialStatementTable", "slug": "dividendtable-financialstatementtable", "components": ["DividendTable", "FinancialStatementTable"]}, {"title": "CurrencyConverter / CryptoWalletCard / TransactionList / Candle Countdown", "slug": "currencyconverter-cryptowalletcard-transactionlist-candle-countdown", "components": ["CurrencyConverter", "CryptoWalletCard", "TransactionList", "Candle Countdown"]}]}, +{"number": 17, "slug": "editor", "title": "Editor", "summary": "A code editor with the gutter, folds and cursors it needs, the help a language server gives, search, the panels around it and its status row.", "stories": [{"title": "CodeEditor / EditorGutter / LineNumbers / FoldingControl / Minimap / Ruler / IndentGuide / BracketMatcher / RainbowBrackets / MultiCursor / SelectionHighlight / StickyScroll / Breadcrumb / InlayHints / CodeLens / InlineDecoration", "slug": "codeeditor-editorgutter-linenumbers-foldingcontrol-minimap-ruler-indentguide-bracketmatcher-rainbowbrackets-multicursor-selectionhighlight-stickyscroll-breadcrumb-inlayhints-codelens-inlinedecoration", "components": ["CodeEditor", "EditorGutter", "LineNumbers", "FoldingControl", "MiniMap", "Ruler", "IndentGuide", "BracketMatcher", "RainbowBrackets", "MultiCursor", "SelectionHighlight", "StickyScroll", "Breadcrumb", "InlayHints", "CodeLens", "InlineDecoration"]}, {"title": "WhitespaceRenderer / CursorRenderer", "slug": "whitespacerenderer-cursorrenderer", "components": ["WhitespaceRenderer", "CursorRenderer"]}, {"title": "InlineDiff / GhostText / InlineChat / ReadOnlyBanner / VimModeIndicator", "slug": "inlinediff-ghosttext-inlinechat-readonlybanner-vimmodeindicator", "components": ["InlineDiff", "GhostText", "InlineChat", "ReadOnlyBanner", "VimModeIndicator"]}, {"title": "CompletionMenu / SignatureHelp / HoverInfo", "slug": "completionmenu-signaturehelp-hoverinfo", "components": ["CompletionMenu", "SignatureHelp", "HoverInfo"]}, {"title": "CodeActionMenu / RenameInput / PeekView / ReferencesPanel / SymbolOutline / GoToSymbol / CallHierarchy / TypeHierarchy", "slug": "codeactionmenu-renameinput-peekview-referencespanel-symboloutline-gotosymbol-callhierarchy-typehierarchy", "components": ["CodeActionMenu", "RenameInput", "PeekView", "ReferencesPanel", "SymbolOutline", "GoToSymbol", "CallHierarchy", "TypeHierarchy"]}, {"title": "FindWidget / ReplaceWidget", "slug": "findwidget-replacewidget", "components": ["FindWidget", "ReplaceWidget"]}, {"title": "SearchPanel / SearchResultItem / SearchFilters", "slug": "searchpanel-searchresultitem-searchfilters", "components": ["SearchPanel", "SearchResultItem", "SearchFilters"]}, {"title": "ProblemsPanel / DiagnosticsList / OutputPanel", "slug": "problemspanel-diagnosticslist-outputpanel", "components": ["ProblemsPanel", "DiagnosticsList", "OutputPanel"]}, {"title": "DebugConsole / ExtensionsPanel / PluginList", "slug": "debugconsole-extensionspanel-pluginlist", "components": ["DebugConsole", "ExtensionsPanel", "PluginList"]}, {"title": "SettingsEditor / KeybindingsEditor", "slug": "settingseditor-keybindingseditor", "components": ["SettingsEditor", "KeybindingsEditor"]}, {"title": "WelcomePage / RecentProjects / ProjectSwitcher / ThemePreview / TaskRunner / RunConfiguration", "slug": "welcomepage-recentprojects-projectswitcher-themepreview-taskrunner-runconfiguration", "components": ["WelcomePage", "RecentProjects", "ProjectSwitcher", "ThemePreview", "TaskRunner", "RunConfiguration"]}, {"title": "CursorPosition / LanguageMode / Encoding / LineEnding / IndentSettings / BranchIndicator / LspStatus / NotificationBell", "slug": "cursorposition-languagemode-encoding-lineending-indentsettings-branchindicator-lspstatus-notificationbell", "components": ["CursorPosition", "LanguageMode", "Encoding", "LineEnding", "IndentSettings", "BranchIndicator", "LspStatus", "NotificationBell"]}]}, +{"number": 18, "slug": "terminal", "title": "Terminal", "summary": "A real shell drawn cell by cell, with tabs, splits, find and links; command blocks and history; colored output, logs and processes.", "stories": [{"title": "Terminal / TerminalTabs / TerminalSplit / TerminalToolbar / ShellSelector / TerminalSearch / TerminalLink", "slug": "terminal-terminaltabs-terminalsplit-terminaltoolbar-shellselector-terminalsearch-terminallink", "components": ["Terminal", "TerminalTabs", "TerminalSplit", "TerminalToolbar", "ShellSelector", "TerminalSearch", "TerminalLink"]}, {"title": "CommandBlock / CommandHistory", "slug": "commandblock-commandhistory", "components": ["CommandBlock", "CommandHistory"]}, {"title": "AnsiText", "slug": "ansitext", "components": ["AnsiText"]}, {"title": "LogViewer", "slug": "logviewer", "components": ["LogViewer"]}, {"title": "ProcessList", "slug": "processlist", "components": ["ProcessList"]}]}, +{"number": 19, "slug": "git", "title": "Git", "summary": "Diffs and merges, changes and commits, the commit graph, branches, blame and review.", "stories": [{"title": "DiffViewer / InlineDiff", "slug": "diffviewer-inlinediff", "components": ["DiffViewer", "InlineDiff"]}, {"title": "ThreeWayMerge / ConflictResolver", "slug": "threewaymerge-conflictresolver", "components": ["ThreeWayMerge", "ConflictResolver"]}, {"title": "ChangesList / CommitInput", "slug": "changeslist-commitinput", "components": ["ChangesList", "CommitInput"]}, {"title": "CommitGraph / CommitList / CommitItem", "slug": "commitgraph-commitlist-commititem", "components": ["CommitGraph", "CommitList", "CommitItem"]}, {"title": "BranchSelector / BranchList / TagList / StashList", "slug": "branchselector-branchlist-taglist-stashlist", "components": ["BranchSelector", "BranchList", "TagList", "StashList"]}, {"title": "BlameView / GitBlameAnnotation / FileHistory / DiffStat / GitStatusBadge", "slug": "blameview-gitblameannotation-filehistory-diffstat-gitstatusbadge", "components": ["BlameView", "GitBlameAnnotation", "FileHistory", "DiffStat", "GitStatusBadge"]}, {"title": "PullRequestCard / ReviewComment", "slug": "pullrequestcard-reviewcomment", "components": ["PullRequestCard", "ReviewComment"]}]}, +{"number": 20, "slug": "debug", "title": "Debug", "summary": "A paused program: its breakpoints, stack, threads and values, memory and machine code; profiles and network.", "stories": [{"title": "DebugToolbar", "slug": "debugtoolbar", "components": ["DebugToolbar"]}, {"title": "BreakpointList / BreakpointGutter / CallStack / ThreadList", "slug": "breakpointlist-breakpointgutter-callstack-threadlist", "components": ["BreakpointList", "BreakpointGutter", "CallStack", "ThreadList"]}, {"title": "VariablesPanel / WatchPanel / ConsoleREPL", "slug": "variablespanel-watchpanel-consolerepl", "components": ["VariablesPanel", "WatchPanel", "ConsoleREPL"]}, {"title": "MemoryViewer / HexViewer / Disassembly", "slug": "memoryviewer-hexviewer-disassembly", "components": ["MemoryViewer", "HexViewer", "Disassembly"]}, {"title": "PerformanceProfiler / Flamegraph / TimelineProfiler", "slug": "performanceprofiler-flamegraph-timelineprofiler", "components": ["PerformanceProfiler", "Flamegraph", "TimelineProfiler"]}, {"title": "NetworkInspector", "slug": "networkinspector", "components": ["NetworkInspector"]}]}, +{"number": 21, "slug": "documents", "title": "Documents", "summary": "Markdown written and read, blocks like a notebook's, and a knowledge base around them.", "stories": [{"title": "RichTextEditor / FixedFormatToolbar / FloatingFormatToolbar / LinkEditor / MentionMenu / EmojiAutocomplete / WordCount / ReadingTime", "slug": "richtexteditor-fixedformattoolbar-floatingformattoolbar-linkeditor-mentionmenu-emojiautocomplete-wordcount-readingtime", "components": ["RichTextEditor", "FixedFormatToolbar", "FloatingFormatToolbar", "LinkEditor", "MentionMenu", "EmojiAutocomplete", "WordCount", "ReadingTime"]}, {"title": "MarkdownEditor", "slug": "markdowneditor", "components": ["MarkdownEditor"]}, {"title": "BlockEditor / BlockHandle / SlashMenu / PlaceholderText", "slug": "blockeditor-blockhandle-slashmenu-placeholdertext", "components": ["BlockEditor", "BlockHandle", "SlashMenu", "PlaceholderText"]}, {"title": "Block types / TableEditor / ImageEditorBlock / MathEditor / DiagramBlock / CodeBlockEditor", "slug": "block-types-tableeditor-imageeditorblock-matheditor-diagramblock-codeblockeditor", "components": ["Block types", "TableEditor", "ImageEditorBlock", "MathEditor", "DiagramBlock", "CodeBlockEditor"]}, {"title": "TodoItem / Checklist", "slug": "todoitem-checklist", "components": ["TodoItem", "Checklist"]}, {"title": "MarkdownRenderer / TableOfContents / DocumentOutline / ReadingProgress", "slug": "markdownrenderer-tableofcontents-documentoutline-readingprogress", "components": ["MarkdownRenderer", "TableOfContents", "DocumentOutline", "ReadingProgress"]}, {"title": "DocumentViewer / PDFViewer / PageThumbnailList", "slug": "documentviewer-pdfviewer-pagethumbnaillist", "components": ["DocumentViewer", "PDFViewer", "PageThumbnailList"]}, {"title": "OfficePreview", "slug": "officepreview", "components": ["OfficePreview"]}, {"title": "EpubReader", "slug": "epubreader", "components": ["EpubReader"]}, {"title": "Footnote / Glossary / TooltipTerm", "slug": "footnote-glossary-tooltipterm", "components": ["Footnote", "Glossary", "TooltipTerm"]}, {"title": "FocusMode / ZenMode", "slug": "focusmode-zenmode", "components": ["FocusMode", "ZenMode"]}, {"title": "PrintPreview", "slug": "printpreview", "components": ["PrintPreview"]}, {"title": "PageTree / Favorites / Pinned", "slug": "pagetree-favorites-pinned", "components": ["PageTree", "Favorites", "Pinned"]}, {"title": "PageCover / PageIcon / PageProperties", "slug": "pagecover-pageicon-pageproperties", "components": ["PageCover", "PageIcon", "PageProperties"]}, {"title": "Backlinks / GraphView", "slug": "backlinks-graphview", "components": ["Backlinks", "GraphView"]}, {"title": "TemplatePicker", "slug": "templatepicker", "components": ["TemplatePicker"]}, {"title": "TrashBin", "slug": "trashbin", "components": ["TrashBin"]}, {"title": "VersionHistory / PageHistoryDiff", "slug": "versionhistory-pagehistorydiff", "components": ["VersionHistory", "PageHistoryDiff"]}]}, +{"number": 22, "slug": "collab", "title": "Collaboration", "summary": "People in the same document: who is here and where, comments and reactions, suggested changes, and sharing.", "stories": [{"title": "PresenceAvatars / LiveIndicator / FollowMode / RemoteCursor / RemoteSelection", "slug": "presenceavatars-liveindicator-followmode-remotecursor-remoteselection", "components": ["PresenceAvatars", "LiveIndicator", "FollowMode", "RemoteCursor", "RemoteSelection"]}, {"title": "CommentThread / Reply / Reactions / ReactionPicker", "slug": "commentthread-reply-reactions-reactionpicker", "components": ["CommentThread", "Reply", "Reactions", "ReactionPicker"]}, {"title": "CommentBubble / CommentMarker", "slug": "commentbubble-commentmarker", "components": ["CommentBubble", "CommentMarker"]}, {"title": "CommentSidebar", "slug": "commentsidebar", "components": ["CommentSidebar"]}, {"title": "Annotation / Highlight", "slug": "annotation-highlight", "components": ["Annotation", "Highlight"]}, {"title": "SuggestionMode / TrackChanges", "slug": "suggestionmode-trackchanges", "components": ["SuggestionMode", "TrackChanges"]}, {"title": "ShareDialog / InviteInput / PermissionSelect / AccessList", "slug": "sharedialog-inviteinput-permissionselect-accesslist", "components": ["ShareDialog", "InviteInput", "PermissionSelect", "AccessList"]}, {"title": "ActivityLog → data_display::ActivityFeed", "slug": "activitylog-data-display-activityfeed", "components": ["ActivityLog"]}]}, +{"number": 23, "slug": "chat", "title": "AI Chat", "summary": "A conversation with an assistant: messages that stream in, code and media inside them, and what can be done with each.", "stories": [{"title": "ChatContainer / MessageList / MessageBubble / MessageAvatar / MessageHeader / MessageFooter / DateSeparator / ScrollToBottomButton", "slug": "chatcontainer-messagelist-messagebubble-messageavatar-messageheader-messagefooter-dateseparator-scrolltobottombutton", "components": ["ChatContainer", "MessageList", "MessageBubble", "MessageAvatar", "MessageHeader", "MessageFooter", "DateSeparator", "ScrollToBottomButton"]}, {"title": "StreamingText / StreamingMarkdown / StreamingCursor", "slug": "streamingtext-streamingmarkdown-streamingcursor", "components": ["StreamingText", "StreamingMarkdown", "StreamingCursor"]}, {"title": "CodeBlock", "slug": "codeblock", "components": ["CodeBlock"]}, {"title": "MathBlock / TableBlock / MermaidRenderer", "slug": "mathblock-tableblock-mermaidrenderer", "components": ["MathBlock", "TableBlock", "MermaidRenderer"]}, {"title": "ImageMessage / ImageGrid / FileMessage / AudioMessage / VideoMessage / LinkPreviewCard", "slug": "imagemessage-imagegrid-filemessage-audiomessage-videomessage-linkpreviewcard", "components": ["ImageMessage", "ImageGrid", "FileMessage", "AudioMessage", "VideoMessage", "LinkPreviewCard"]}, {"title": "QuoteReply / MessageActions / MessageEditor / BranchNavigator / RegenerateButton / StopGeneratingButton / ContinueButton", "slug": "quotereply-messageactions-messageeditor-branchnavigator-regeneratebutton-stopgeneratingbutton-continuebutton", "components": ["QuoteReply", "MessageActions", "MessageEditor", "BranchNavigator", "RegenerateButton", "StopGeneratingButton", "ContinueButton"]}, {"title": "ErrorMessage / RetryMessage / RateLimitNotice / FeedbackForm", "slug": "errormessage-retrymessage-ratelimitnotice-feedbackform", "components": ["ErrorMessage", "RetryMessage", "RateLimitNotice", "FeedbackForm"]}, {"title": "ThinkingBlock / ReasoningPanel / ThinkingIndicator / ThinkingDuration", "slug": "thinkingblock-reasoningpanel-thinkingindicator-thinkingduration", "components": ["ThinkingBlock", "ReasoningPanel", "ThinkingIndicator", "ThinkingDuration"]}, {"title": "CitationBadge / InlineCitation / SourceCard / SourceList / SourcesPanel", "slug": "citationbadge-inlinecitation-sourcecard-sourcelist-sourcespanel", "components": ["CitationBadge", "InlineCitation", "SourceCard", "SourceList", "SourcesPanel"]}, {"title": "SearchProgress / WebResultCard / DocumentChunkPreview", "slug": "searchprogress-webresultcard-documentchunkpreview", "components": ["SearchProgress", "WebResultCard", "DocumentChunkPreview"]}, {"title": "PromptInput / Composer / AttachmentButton / AttachmentChip / AttachmentPreview / PasteImagePreview / DragDropOverlay / ContextChips / SlashCommandMenu / ContextMentionMenu / SendButton / InputHint", "slug": "promptinput-composer-attachmentbutton-attachmentchip-attachmentpreview-pasteimagepreview-dragdropoverlay-contextchips-slashcommandmenu-contextmentionmenu-sendbutton-inputhint", "components": ["PromptInput", "Composer", "AttachmentButton", "AttachmentChip", "AttachmentPreview", "PasteImagePreview", "DragDropOverlay", "ContextChips", "SlashCommandMenu", "ContextMentionMenu", "SendButton", "InputHint"]}, {"title": "ModelSelector / ModeSelector / ToolToggleMenu / PromptTemplateMenu", "slug": "modelselector-modeselector-tooltogglemenu-prompttemplatemenu", "components": ["ModelSelector", "ModeSelector", "ToolToggleMenu", "PromptTemplateMenu"]}, {"title": "TemperatureSlider / ParameterPanel / SystemPromptEditor / TokenCounter / ContextWindowMeter / CostEstimator", "slug": "temperatureslider-parameterpanel-systemprompteditor-tokencounter-contextwindowmeter-costestimator", "components": ["TemperatureSlider", "ParameterPanel", "SystemPromptEditor", "TokenCounter", "ContextWindowMeter", "CostEstimator"]}, {"title": "ConversationList / ConversationItem / ConversationSearch / NewChatButton", "slug": "conversationlist-conversationitem-conversationsearch-newchatbutton", "components": ["ConversationList", "ConversationItem", "ConversationSearch", "NewChatButton"]}, {"title": "FolderList / ProjectList / ProjectKnowledgePanel", "slug": "folderlist-projectlist-projectknowledgepanel", "components": ["FolderList", "ProjectList", "ProjectKnowledgePanel"]}, {"title": "SharedConversationView / ConversationExport", "slug": "sharedconversationview-conversationexport", "components": ["SharedConversationView", "ConversationExport"]}, {"title": "WelcomeScreen / Greeting / SuggestionChips / StarterPrompts / CapabilityCards", "slug": "welcomescreen-greeting-suggestionchips-starterprompts-capabilitycards", "components": ["WelcomeScreen", "Greeting", "SuggestionChips", "StarterPrompts", "CapabilityCards"]}, {"title": "FollowUpSuggestions", "slug": "followupsuggestions", "components": ["FollowUpSuggestions"]}, {"title": "PromptLibrary", "slug": "promptlibrary", "components": ["PromptLibrary"]}]}, +{"number": 24, "slug": "agent", "title": "Agent", "summary": "An assistant at work: the tools it calls, what it asks before it acts, how far it has come, what it would change, what it sees and what it makes.", "stories": [{"title": "ToolCallCard / ToolCallGroup", "slug": "toolcallcard-toolcallgroup", "components": ["ToolCallCard", "ToolCallGroup"]}, {"title": "ToolApprovalDialog / PermissionPrompt", "slug": "toolapprovaldialog-permissionprompt", "components": ["ToolApprovalDialog", "PermissionPrompt"]}, {"title": "AgentStepList / TaskTimeline / AgentPlan / TodoList / AgentStatus / AgentProgress", "slug": "agentsteplist-tasktimeline-agentplan-todolist-agentstatus-agentprogress", "components": ["AgentStepList", "TaskTimeline", "AgentPlan", "TodoList", "AgentStatus", "AgentProgress"]}, {"title": "SubAgentTree", "slug": "subagenttree", "components": ["SubAgentTree"]}, {"title": "FileChangeCard / MultiFileDiff Review / CommandExecutionCard", "slug": "filechangecard-multifilediff-review-commandexecutioncard", "components": ["FileChangeCard", "MultiFileDiff Review", "CommandExecutionCard"]}, {"title": "BrowserPreview / ScreenshotStream / ComputerUseViewer", "slug": "browserpreview-screenshotstream-computeruseviewer", "components": ["BrowserPreview", "ScreenshotStream", "ComputerUseViewer"]}, {"title": "ArtifactPanel / ArtifactVersionSwitcher / LivePreview", "slug": "artifactpanel-artifactversionswitcher-livepreview", "components": ["ArtifactPanel", "ArtifactVersionSwitcher", "LivePreview"]}, {"title": "SandboxStatus", "slug": "sandboxstatus", "components": ["SandboxStatus"]}, {"title": "MCPServerList / ConnectorList / ToolRegistry Panel", "slug": "mcpserverlist-connectorlist-toolregistry-panel", "components": ["MCPServerList", "ConnectorList", "ToolRegistry Panel"]}, {"title": "MemoryPanel / CheckpointList / Rewind", "slug": "memorypanel-checkpointlist-rewind", "components": ["MemoryPanel", "CheckpointList", "Rewind"]}, {"title": "InterruptButton / HumanInputRequest", "slug": "interruptbutton-humaninputrequest", "components": ["InterruptButton", "HumanInputRequest"]}, {"title": "TokenUsageChart / CostBreakdown", "slug": "tokenusagechart-costbreakdown", "components": ["TokenUsageChart", "CostBreakdown"]}, {"title": "TraceViewer / EvalResultTable", "slug": "traceviewer-evalresulttable", "components": ["TraceViewer", "EvalResultTable"]}]}, +{"number": 25, "slug": "generative", "title": "Generative", "summary": "Making pictures, sound and video with a model: the prompt and its settings, the queue and what comes back, the canvas, the models behind them, and the data and prompts they work from.", "stories": [{"title": "ImageGenerationPanel / PromptEnhancer / NegativePromptInput / StylePresetPicker / AspectRatioPicker / SeedInput", "slug": "imagegenerationpanel-promptenhancer-negativepromptinput-stylepresetpicker-aspectratiopicker-seedinput", "components": ["ImageGenerationPanel", "PromptEnhancer", "NegativePromptInput", "StylePresetPicker", "AspectRatioPicker", "SeedInput"]}, {"title": "GenerationQueue", "slug": "generationqueue", "components": ["GenerationQueue"]}, {"title": "GenerationGrid / ImageCompare", "slug": "generationgrid-imagecompare", "components": ["GenerationGrid", "ImageCompare"]}, {"title": "VariationPicker / ABCompareView", "slug": "variationpicker-abcompareview", "components": ["VariationPicker", "ABCompareView"]}, {"title": "InpaintCanvas / MaskBrush", "slug": "inpaintcanvas-maskbrush", "components": ["InpaintCanvas", "MaskBrush"]}, {"title": "AudioGenerationPlayer", "slug": "audiogenerationplayer", "components": ["AudioGenerationPlayer"]}, {"title": "TTSVoicePicker", "slug": "ttsvoicepicker", "components": ["TTSVoicePicker"]}, {"title": "VideoGenerationTimeline", "slug": "videogenerationtimeline", "components": ["VideoGenerationTimeline"]}, {"title": "ModelCard", "slug": "modelcard", "components": ["ModelCard"]}, {"title": "ModelDownloadManager", "slug": "modeldownloadmanager", "components": ["ModelDownloadManager"]}, {"title": "ModelStatus / HardwareMonitor", "slug": "modelstatus-hardwaremonitor", "components": ["ModelStatus", "HardwareMonitor"]}, {"title": "FineTuneJobCard", "slug": "finetunejobcard", "components": ["FineTuneJobCard"]}, {"title": "DatasetViewer → tables::DataTable", "slug": "datasetviewer-tables-datatable", "components": ["DatasetViewer"]}, {"title": "EmbeddingVisualizer", "slug": "embeddingvisualizer", "components": ["EmbeddingVisualizer"]}, {"title": "PromptPlayground", "slug": "promptplayground", "components": ["PromptPlayground"]}, {"title": "PromptDiff / PromptVersionHistory", "slug": "promptdiff-promptversionhistory", "components": ["PromptDiff", "PromptVersionHistory"]}]}, +{"number": 26, "slug": "media", "title": "Media", "summary": "Pictures, video and sound: looking closely, cropping and marking up, playing, and the controls and devices around them.", "stories": [{"title": "ImageViewer", "slug": "imageviewer", "components": ["ImageViewer"]}, {"title": "ImageThumbnail / Lightbox → overlays::Lightbox", "slug": "imagethumbnail-lightbox-overlays-lightbox", "components": ["ImageThumbnail", "Lightbox"]}, {"title": "ImageCropper", "slug": "imagecropper", "components": ["ImageCropper"]}, {"title": "ImageUpload", "slug": "imageupload", "components": ["ImageUpload"]}, {"title": "ImageAnnotator", "slug": "imageannotator", "components": ["ImageAnnotator"]}, {"title": "VideoPlayer", "slug": "videoplayer", "components": ["VideoPlayer"]}, {"title": "VideoTimeline / Scrubber", "slug": "videotimeline-scrubber", "components": ["VideoTimeline", "Scrubber"]}, {"title": "VideoThumbnailStrip", "slug": "videothumbnailstrip", "components": ["VideoThumbnailStrip"]}, {"title": "SubtitleEditor", "slug": "subtitleeditor", "components": ["SubtitleEditor"]}, {"title": "AudioPlayer", "slug": "audioplayer", "components": ["AudioPlayer"]}, {"title": "AudioWaveform", "slug": "audiowaveform", "components": ["AudioWaveform"]}, {"title": "AudioSpectrum / Visualizer", "slug": "audiospectrum-visualizer", "components": ["AudioSpectrum", "Visualizer"]}, {"title": "VolumeControl", "slug": "volumecontrol", "components": ["VolumeControl"]}, {"title": "PlaybackSpeedControl", "slug": "playbackspeedcontrol", "components": ["PlaybackSpeedControl"]}, {"title": "MediaControls", "slug": "mediacontrols", "components": ["MediaControls"]}, {"title": "Playlist", "slug": "playlist", "components": ["Playlist"]}, {"title": "ScreenRecorderControls", "slug": "screenrecordercontrols", "components": ["ScreenRecorderControls"]}, {"title": "CameraPreview", "slug": "camerapreview", "components": ["CameraPreview"]}, {"title": "MicLevelMeter", "slug": "miclevelmeter", "components": ["MicLevelMeter"]}, {"title": "DeviceSelector", "slug": "deviceselector", "components": ["DeviceSelector"]}, {"title": "3DModelViewer", "slug": "3dmodelviewer", "components": ["3DModelViewer"]}]}, +{"number": 27, "slug": "files", "title": "Files", "summary": "Files and folders: their icons, rows and tiles, and the listings, searches and transfers around them.", "stories": [{"title": "FileIcon", "slug": "fileicon", "components": ["FileIcon"]}, {"title": "FileItem", "slug": "fileitem", "components": ["FileItem"]}, {"title": "FileList", "slug": "filelist", "components": ["FileList"]}, {"title": "FileGrid", "slug": "filegrid", "components": ["FileGrid"]}, {"title": "FileExplorer", "slug": "fileexplorer", "components": ["FileExplorer"]}, {"title": "FolderTree", "slug": "foldertree", "components": ["FolderTree"]}, {"title": "FileContextMenu", "slug": "filecontextmenu", "components": ["FileContextMenu"]}, {"title": "RenameInline", "slug": "renameinline", "components": ["RenameInline"]}, {"title": "DragDropFiles", "slug": "dragdropfiles", "components": ["DragDropFiles"]}, {"title": "FilePreview / QuickLook", "slug": "filepreview-quicklook", "components": ["FilePreview", "QuickLook"]}, {"title": "RecentFiles", "slug": "recentfiles", "components": ["RecentFiles"]}, {"title": "Favorites / Bookmarks", "slug": "favorites-bookmarks", "components": ["Favorites", "Bookmarks"]}, {"title": "FileSearch", "slug": "filesearch", "components": ["FileSearch"]}, {"title": "DuplicateFinder", "slug": "duplicatefinder", "components": ["DuplicateFinder"]}, {"title": "ArchiveViewer", "slug": "archiveviewer", "components": ["ArchiveViewer"]}, {"title": "HexViewer", "slug": "hexviewer", "components": ["HexViewer"]}, {"title": "FileOperationProgress", "slug": "fileoperationprogress", "components": ["FileOperationProgress"]}, {"title": "TransferQueue", "slug": "transferqueue", "components": ["TransferQueue"]}, {"title": "DownloadManager", "slug": "downloadmanager", "components": ["DownloadManager"]}, {"title": "StorageUsage", "slug": "storageusage", "components": ["StorageUsage"]}]}, +{"number": 28, "slug": "messaging", "title": "Messaging", "summary": "Channels and people: the lists and heads of chats, the messages in them, the people behind them, and calls.", "stories": [{"title": "ChannelList / ChatList", "slug": "channellist-chatlist", "components": ["ChannelList", "ChatList"]}, {"title": "ChannelItem / DirectMessageItem", "slug": "channelitem-directmessageitem", "components": ["ChannelItem", "DirectMessageItem"]}, {"title": "ChannelHeader", "slug": "channelheader", "components": ["ChannelHeader"]}, {"title": "PinnedMessages", "slug": "pinnedmessages", "components": ["PinnedMessages"]}, {"title": "ChatMessage / UnreadDivider / MessageReactions / TypingIndicator", "slug": "chatmessage-unreaddivider-messagereactions-typingindicator", "components": ["ChatMessage", "UnreadDivider", "MessageReactions", "TypingIndicator"]}, {"title": "MessageThread / ThreadPanel", "slug": "messagethread-threadpanel", "components": ["MessageThread", "ThreadPanel"]}, {"title": "ReadReceipt", "slug": "readreceipt", "components": ["ReadReceipt"]}, {"title": "MemberList / UserProfileCard", "slug": "memberlist-userprofilecard", "components": ["MemberList", "UserProfileCard"]}, {"title": "OnlineStatus", "slug": "onlinestatus", "components": ["OnlineStatus"]}, {"title": "StatusSetter", "slug": "statussetter", "components": ["StatusSetter"]}, {"title": "CallControls / VoiceCallBar / HuddleIndicator", "slug": "callcontrols-voicecallbar-huddleindicator", "components": ["CallControls", "VoiceCallBar", "HuddleIndicator"]}, {"title": "ParticipantTile / VideoCallGrid", "slug": "participanttile-videocallgrid", "components": ["ParticipantTile", "VideoCallGrid"]}, {"title": "ScreenShareView", "slug": "screenshareview", "components": ["ScreenShareView"]}, {"title": "IncomingCallDialog", "slug": "incomingcalldialog", "components": ["IncomingCallDialog"]}, {"title": "StickerPicker", "slug": "stickerpicker", "components": ["StickerPicker"]}, {"title": "GifPicker", "slug": "gifpicker", "components": ["GifPicker"]}]}, +{"number": 29, "slug": "mail", "title": "Mail", "summary": "Mailboxes and the messages in them: reading, writing, sorting and putting off.", "stories": [{"title": "MailboxList / FolderList", "slug": "mailboxlist-folderlist", "components": ["MailboxList", "FolderList"]}, {"title": "MailList / MailItem", "slug": "maillist-mailitem", "components": ["MailList", "MailItem"]}, {"title": "InboxZeroState → EmptyState", "slug": "inboxzerostate-emptystate", "components": ["InboxZeroState"]}, {"title": "MailReader / QuotedText", "slug": "mailreader-quotedtext", "components": ["MailReader", "QuotedText"]}, {"title": "MailThreadView", "slug": "mailthreadview", "components": ["MailThreadView"]}, {"title": "MailComposer / RecipientInput → TagInput", "slug": "mailcomposer-recipientinput-taginput", "components": ["MailComposer", "RecipientInput"]}, {"title": "SignatureEditor", "slug": "signatureeditor", "components": ["SignatureEditor"]}, {"title": "LabelPicker", "slug": "labelpicker", "components": ["LabelPicker"]}, {"title": "SnoozePicker", "slug": "snoozepicker", "components": ["SnoozePicker"]}, {"title": "ScheduleSend", "slug": "schedulesend", "components": ["ScheduleSend"]}]}, +{"number": 30, "slug": "calendar", "title": "Calendar", "summary": "Days, weeks, months and years, and what happens in them.", "stories": [{"title": "CalendarMonthView", "slug": "calendarmonthview", "components": ["CalendarMonthView"]}, {"title": "MiniCalendar → forms::Calendar", "slug": "minicalendar-forms-calendar", "components": ["MiniCalendar"]}, {"title": "CalendarWeekView / TimeGrid / AllDayRow / CurrentTimeIndicator", "slug": "calendarweekview-timegrid-alldayrow-currenttimeindicator", "components": ["CalendarWeekView", "TimeGrid", "AllDayRow", "CurrentTimeIndicator"]}, {"title": "CalendarDayView / TimezoneOverlay", "slug": "calendardayview-timezoneoverlay", "components": ["CalendarDayView", "TimezoneOverlay"]}, {"title": "AgendaView", "slug": "agendaview", "components": ["AgendaView"]}, {"title": "EventPopover / AttendeeList", "slug": "eventpopover-attendeelist", "components": ["EventPopover", "AttendeeList"]}, {"title": "EventEditor / RecurrenceEditor / ReminderPicker", "slug": "eventeditor-recurrenceeditor-reminderpicker", "components": ["EventEditor", "RecurrenceEditor", "ReminderPicker"]}, {"title": "AvailabilityPicker", "slug": "availabilitypicker", "components": ["AvailabilityPicker"]}, {"title": "YearView", "slug": "yearview", "components": ["YearView"]}, {"title": "EventCard / EventChip", "slug": "eventcard-eventchip", "components": ["EventCard", "EventChip"]}]}, +{"number": 31, "slug": "project", "title": "Project", "summary": "Tasks and issues, who has them, where they stand, and when they are due.", "stories": [{"title": "PriorityIndicator / IssueIdBadge", "slug": "priorityindicator-issueidbadge", "components": ["PriorityIndicator", "IssueIdBadge"]}, {"title": "StatusSelect / AssigneePicker / DueDatePicker", "slug": "statusselect-assigneepicker-duedatepicker", "components": ["StatusSelect", "AssigneePicker", "DueDatePicker"]}, {"title": "TaskItem / TaskList", "slug": "taskitem-tasklist", "components": ["TaskItem", "TaskList"]}, {"title": "SubtaskList", "slug": "subtasklist", "components": ["SubtaskList"]}, {"title": "KanbanBoard / KanbanColumn / KanbanCard · IssueCard · Sprint Board", "slug": "kanbanboard-kanbancolumn-kanbancard-issuecard-sprint-board", "components": ["KanbanBoard", "KanbanColumn", "KanbanCard", "IssueCard", "Sprint Board"]}, {"title": "TaskDetailPanel", "slug": "taskdetailpanel", "components": ["TaskDetailPanel"]}, {"title": "LabelManager", "slug": "labelmanager", "components": ["LabelManager"]}, {"title": "GanttChart / TimelineView · Roadmap", "slug": "ganttchart-timelineview-roadmap", "components": ["GanttChart", "TimelineView", "Roadmap"]}, {"title": "Burndown Chart", "slug": "burndown-chart", "components": ["Burndown Chart"]}, {"title": "MilestoneProgress", "slug": "milestoneprogress", "components": ["MilestoneProgress"]}, {"title": "WorkloadView", "slug": "workloadview", "components": ["WorkloadView"]}, {"title": "TimeTracker", "slug": "timetracker", "components": ["TimeTracker"]}, {"title": "Pomodoro Timer", "slug": "pomodoro-timer", "components": ["Pomodoro Timer"]}, {"title": "DatabaseView", "slug": "databaseview", "components": ["DatabaseView"]}]}, +{"number": 32, "slug": "canvas", "title": "Canvas & Design", "summary": "An endless plane to lay work on, the tools and panels around it, and graphs of nodes.", "stories": [{"title": "InfiniteCanvas · CanvasGrid / Guides · Ruler · ArtboardFrame · ZoomControls / ZoomIndicator · MiniMap", "slug": "infinitecanvas-canvasgrid-guides-ruler-artboardframe-zoomcontrols-zoomindicator-minimap", "components": ["InfiniteCanvas", "CanvasGrid", "Guides", "Ruler", "ArtboardFrame", "ZoomControls", "ZoomIndicator", "MiniMap"]}, {"title": "ToolPalette · SelectionBox · TransformHandles · SnapIndicator · ShapeTools · PenTool / PathEditor · TextTool · BrushTool / BrushSettings · HistoryPanel", "slug": "toolpalette-selectionbox-transformhandles-snapindicator-shapetools-pentool-patheditor-texttool-brushtool-brushsettings-historypanel", "components": ["ToolPalette", "SelectionBox", "TransformHandles", "SnapIndicator", "ShapeTools", "PenTool", "PathEditor", "TextTool", "BrushTool", "BrushSettings", "HistoryPanel"]}, {"title": "LayerPanel · InspectorPanel · AlignmentToolbar · AssetPanel", "slug": "layerpanel-inspectorpanel-alignmenttoolbar-assetpanel", "components": ["LayerPanel", "InspectorPanel", "AlignmentToolbar", "AssetPanel"]}, {"title": "ColorPanel · GradientPanel · ShadowEditor · BorderEditor · TypographyPanel", "slug": "colorpanel-gradientpanel-shadoweditor-bordereditor-typographypanel", "components": ["ColorPanel", "GradientPanel", "ShadowEditor", "BorderEditor", "TypographyPanel"]}, {"title": "ConstraintsEditor · AutoLayoutControls · ExportPanel", "slug": "constraintseditor-autolayoutcontrols-exportpanel", "components": ["ConstraintsEditor", "AutoLayoutControls", "ExportPanel"]}, {"title": "NodeEditor / NodeGraph · Node / Port / Edge / Connection", "slug": "nodeeditor-nodegraph-node-port-edge-connection", "components": ["NodeEditor", "NodeGraph", "Node", "Port", "Edge", "Connection"]}, {"title": "FlowChart / DiagramEditor · Connector Line", "slug": "flowchart-diagrameditor-connector-line", "components": ["FlowChart", "DiagramEditor", "Connector Line"]}, {"title": "MindMap", "slug": "mindmap", "components": ["MindMap"]}, {"title": "Whiteboard · StickyNote", "slug": "whiteboard-stickynote", "components": ["Whiteboard", "StickyNote"]}]}, +{"number": 33, "slug": "devtools", "title": "DB & Dev Tools", "summary": "Connections, schemas and queries, data to read, requests to send, and the machines that run them.", "stories": [{"title": "ConnectionManager · ConnectionForm", "slug": "connectionmanager-connectionform", "components": ["ConnectionManager", "ConnectionForm"]}, {"title": "SchemaTree · ERDiagram", "slug": "schematree-erdiagram", "components": ["SchemaTree", "ERDiagram"]}, {"title": "QueryEditor → editor::CodeEditor · QueryResultGrid → tables::DataGrid · QueryHistory · QueryPlanViewer", "slug": "queryeditor-editor-codeeditor-queryresultgrid-tables-datagrid-queryhistory-queryplanviewer", "components": ["QueryEditor", "QueryResultGrid", "QueryHistory", "QueryPlanViewer"]}, {"title": "TableStructureEditor · IndexManager", "slug": "tablestructureeditor-indexmanager", "components": ["TableStructureEditor", "IndexManager"]}, {"title": "RedisKeyBrowser", "slug": "rediskeybrowser", "components": ["RedisKeyBrowser"]}, {"title": "DocumentViewer (MongoDB / JSON) → devtools::JsonViewer · JsonViewer / JsonTree · JsonEditor · YamlViewer · XmlViewer", "slug": "documentviewer-mongodb-json-devtools-jsonviewer-jsonviewer-jsontree-jsoneditor-yamlviewer-xmlviewer", "components": ["DocumentViewer", "JsonViewer", "JsonTree", "JsonEditor", "YamlViewer", "XmlViewer"]}, {"title": "ApiRequestBuilder · ResponseViewer · HeadersTable · QueryParamsEditor → forms::KeyValueInput · EnvironmentSelector · CollectionTree", "slug": "apirequestbuilder-responseviewer-headerstable-queryparamseditor-forms-keyvalueinput-environmentselector-collectiontree", "components": ["ApiRequestBuilder", "ResponseViewer", "HeadersTable", "QueryParamsEditor", "EnvironmentSelector", "CollectionTree"]}, {"title": "WebSocketConsole", "slug": "websocketconsole", "components": ["WebSocketConsole"]}, {"title": "GraphQLExplorer", "slug": "graphqlexplorer", "components": ["GraphQLExplorer"]}, {"title": "RegexTester · Base64 / Encoder · ColorContrastChecker", "slug": "regextester-base64-encoder-colorcontrastchecker", "components": ["RegexTester", "Base64", "Encoder", "ColorContrastChecker"]}, {"title": "ContainerList · PodList · ResourceMonitor", "slug": "containerlist-podlist-resourcemonitor", "components": ["ContainerList", "PodList", "ResourceMonitor"]}]}, +{"number": 34, "slug": "dashboard", "title": "Dashboard & Monitoring", "summary": "Widgets on a grid you arrange, what is firing and what is up, and the stream of what happens.", "stories": [{"title": "Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge", "slug": "dashboard-grid-widget-dashboardcard-refreshintervalselector-dashboardfilterbar-statcard-data-display-statistic-metricschart-charts-linechart-resourcegauge-data-display-gauge", "components": ["Dashboard Grid", "Widget", "DashboardCard", "RefreshIntervalSelector", "DashboardFilterBar", "StatCard", "MetricsChart", "ResourceGauge"]}, {"title": "UptimeBar · ServiceStatus · HealthCheck Indicator", "slug": "uptimebar-servicestatus-healthcheck-indicator", "components": ["UptimeBar", "ServiceStatus", "HealthCheck Indicator"]}, {"title": "AlertList · IncidentCard", "slug": "alertlist-incidentcard", "components": ["AlertList", "IncidentCard"]}, {"title": "EventStream · LogStream → terminal::LogViewer · TraceWaterfall → debug::TimelineProfiler · SystemMonitor → devtools::ResourceMonitor · ProcessTable → terminal::ProcessList · NetworkGraph (throughput) → charts::RealtimeChart · MapView / GeoHeatmap → maps::MapView, maps::GeoHeatmap · WorldMap → maps::WorldMap", "slug": "eventstream-logstream-terminal-logviewer-tracewaterfall-debug-timelineprofiler-systemmonitor-devtools-resourcemonitor-processtable-terminal-processlist-networkgraph-throughput-charts-realtimechart-mapview-geoheatmap-maps-mapview-maps-geoheatmap-worldmap-maps-worldmap", "components": ["EventStream", "LogStream", "TraceWaterfall", "SystemMonitor", "ProcessTable", "NetworkGraph", "MapView", "GeoHeatmap", "WorldMap"]}]}, +{"number": 35, "slug": "settings", "title": "Settings", "summary": "A settings window: its sections, a search over them, and the controls each one holds.", "stories": [{"title": "SettingsLayout · SettingsSection · SettingsRow · SettingsSearch", "slug": "settingslayout-settingssection-settingsrow-settingssearch", "components": ["SettingsLayout", "SettingsSection", "SettingsRow", "SettingsSearch"]}]}, +{"number": 36, "slug": "account", "title": "Account", "summary": "Signing in and up, a link by email, a second factor, and the account once inside.", "stories": [{"title": "LoginForm · OAuthButtons", "slug": "loginform-oauthbuttons", "components": ["LoginForm", "OAuthButtons"]}, {"title": "SignupForm", "slug": "signupform", "components": ["SignupForm"]}, {"title": "MagicLinkForm · ForgotPassword", "slug": "magiclinkform-forgotpassword", "components": ["MagicLinkForm", "ForgotPassword"]}, {"title": "TwoFactorInput", "slug": "twofactorinput", "components": ["TwoFactorInput"]}, {"title": "AccountSwitcher · WorkspaceSwitcher / OrgSwitcher · UserMenu", "slug": "accountswitcher-workspaceswitcher-orgswitcher-usermenu", "components": ["AccountSwitcher", "WorkspaceSwitcher", "OrgSwitcher", "UserMenu"]}, {"title": "ProfileCard · ProfileEditor", "slug": "profilecard-profileeditor", "components": ["ProfileCard", "ProfileEditor"]}, {"title": "SessionList · ApiKeyManager", "slug": "sessionlist-apikeymanager", "components": ["SessionList", "ApiKeyManager"]}, {"title": "SubscriptionCard / PlanBadge · UsageQuota · PaymentMethodForm", "slug": "subscriptioncard-planbadge-usagequota-paymentmethodform", "components": ["SubscriptionCard", "PlanBadge", "UsageQuota", "PaymentMethodForm"]}, {"title": "BillingHistory · UpgradePrompt / Paywall", "slug": "billinghistory-upgradeprompt-paywall", "components": ["BillingHistory", "UpgradePrompt", "Paywall"]}, {"title": "TeamMemberTable · RoleSelector · InvitationList", "slug": "teammembertable-roleselector-invitationlist", "components": ["TeamMemberTable", "RoleSelector", "InvitationList"]}]}, +{"number": 37, "slug": "onboarding", "title": "Onboarding & Help", "summary": "A first run in steps, what is new called out, first things to do, and help where it is needed.", "stories": [{"title": "OnboardingWizard · WelcomeTour", "slug": "onboardingwizard-welcometour", "components": ["OnboardingWizard", "WelcomeTour"]}, {"title": "Coachmark / Hotspot · FeatureHighlight / NewBadge", "slug": "coachmark-hotspot-featurehighlight-newbadge", "components": ["Coachmark", "Hotspot", "FeatureHighlight", "NewBadge"]}, {"title": "Checklist (first-run tasks)", "slug": "checklist-first-run-tasks", "components": ["Checklist"]}, {"title": "HelpTooltip · InlineHelp · EmptyStateWithAction", "slug": "helptooltip-inlinehelp-emptystatewithaction", "components": ["HelpTooltip", "InlineHelp", "EmptyStateWithAction"]}, {"title": "HelpPanel / DocsSidebar", "slug": "helppanel-docssidebar", "components": ["HelpPanel", "DocsSidebar"]}, {"title": "KeyboardShortcutCheatsheet · WhatsNewDialog", "slug": "keyboardshortcutcheatsheet-whatsnewdialog", "components": ["KeyboardShortcutCheatsheet", "WhatsNewDialog"]}, {"title": "FeedbackWidget · ContactSupport", "slug": "feedbackwidget-contactsupport", "components": ["FeedbackWidget", "ContactSupport"]}]}, +{"number": 38, "slug": "interaction", "title": "Interaction", "summary": "Drag and drop, resizing and turning, band selection, presses and gestures, keys, scrolling and focus.", "stories": [{"title": "Draggable · Droppable / DropTarget · DragOverlay / DragPreview · DragGhost", "slug": "draggable-droppable-droptarget-dragoverlay-dragpreview-dragghost", "components": ["Draggable", "Droppable", "DropTarget", "DragOverlay", "DragPreview", "DragGhost"]}, {"title": "Resizable · Rotatable · Selectable / SelectionArea · RubberBandSelection · Sortable", "slug": "resizable-rotatable-selectable-selectionarea-rubberbandselection-sortable", "components": ["Resizable", "Rotatable", "Selectable", "SelectionArea", "RubberBandSelection", "Sortable"]}, {"title": "LongPress · DoubleClick · Gesture · PanZoom", "slug": "longpress-doubleclick-gesture-panzoom", "components": ["LongPress", "DoubleClick", "Gesture", "PanZoom"]}, {"title": "Hotkeys / KeyBinding · KeyChord", "slug": "hotkeys-keybinding-keychord", "components": ["Hotkeys", "KeyBinding", "KeyChord"]}, {"title": "Undo / Redo Manager", "slug": "undo-redo-manager", "components": ["Undo", "Redo Manager"]}, {"title": "VirtualScroller · ScrollSync · InfiniteScroll Trigger", "slug": "virtualscroller-scrollsync-infinitescroll-trigger", "components": ["VirtualScroller", "ScrollSync", "InfiniteScroll Trigger"]}, {"title": "Autofocus · RovingTabIndex · TypeAhead · FocusVisible", "slug": "autofocus-rovingtabindex-typeahead-focusvisible", "components": ["Autofocus", "RovingTabIndex", "TypeAhead", "FocusVisible"]}]}, +{"number": 39, "slug": "theme", "title": "Theme", "summary": "Tokens every component reads. Light and dark share one grammar.", "stories": [{"title": "Surfaces", "slug": "surfaces", "components": []}, {"title": "Ink", "slug": "ink", "components": []}, {"title": "Signals", "slug": "signals", "components": []}, {"title": "Series", "slug": "series", "components": []}, {"title": "Syntax", "slug": "syntax", "components": []}, {"title": "Type", "slug": "type", "components": []}, {"title": "Shape", "slug": "shape", "components": []}, {"title": "Motion", "slug": "motion", "components": ["Motion"]}, {"title": "ThemeEditor · DensityProvider · RadiusProvider · FontProvider · HighContrastMode · ReducedMotionProvider", "slug": "themeeditor-densityprovider-radiusprovider-fontprovider-highcontrastmode-reducedmotionprovider", "components": ["ThemeEditor", "DensityProvider", "RadiusProvider", "FontProvider", "HighContrastMode", "ReducedMotionProvider"]}, {"title": "ThemeImporter · SyntaxThemes", "slug": "themeimporter-syntaxthemes", "components": ["ThemeImporter", "SyntaxThemes"]}, {"title": "Platform Style · IconThemeProvider · VibrancyBackground", "slug": "platform-style-iconthemeprovider-vibrancybackground", "components": ["Platform Style", "IconThemeProvider", "VibrancyBackground"]}]}, +{"number": 40, "slug": "i18n", "title": "i18n & a11y", "summary": "Messages, numbers and dates in the reader's locale. A right-to-left locale mirrors its stacks.", "stories": [{"title": "I18nProvider · Trans / LocalizedText", "slug": "i18nprovider-trans-localizedtext", "components": ["I18nProvider", "Trans", "LocalizedText"]}, {"title": "LocaleNumber / LocaleDate", "slug": "localenumber-localedate", "components": ["LocaleNumber", "LocaleDate"]}, {"title": "RTL layout", "slug": "rtl-layout", "components": ["RTL layout"]}, {"title": "SkipLink · KeyboardNavigationHelper", "slug": "skiplink-keyboardnavigationhelper", "components": ["SkipLink", "KeyboardNavigationHelper"]}, {"title": "ColorBlindSafe Palette", "slug": "colorblindsafe-palette", "components": ["ColorBlindSafe Palette"]}]}, +{"number": 41, "slug": "maps", "title": "Maps", "summary": "Tiles the host supplies, pins with a popup, a picker's crosshair, routes, GeoJSON, heat, clusters and choropleths. The land is Natural Earth's.", "stories": [{"title": "MapView · MapMarker · MapPopup", "slug": "mapview-mapmarker-mappopup", "components": ["MapView", "MapMarker", "MapPopup"]}, {"title": "CoordinateDisplay", "slug": "coordinatedisplay", "components": ["CoordinateDisplay"]}, {"title": "LocationPicker", "slug": "locationpicker", "components": ["LocationPicker"]}, {"title": "RouteLine · GeoJSON Layer", "slug": "routeline-geojson-layer", "components": ["RouteLine", "GeoJSON Layer"]}, {"title": "GeoHeatmap", "slug": "geoheatmap", "components": ["GeoHeatmap"]}, {"title": "MapCluster", "slug": "mapcluster", "components": ["MapCluster"]}, {"title": "ChoroplethMap · WorldMap", "slug": "choroplethmap-worldmap", "components": ["ChoroplethMap", "WorldMap"]}]}, +{"number": 42, "slug": "misc", "title": "Misc", "summary": "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha, a poll, a survey, a quiz, flashcards, licenses, terms, a cookie banner, data out and in, and web views.", "stories": [{"title": "Clock / WorldClock", "slug": "clock-worldclock", "components": ["Clock", "WorldClock"]}, {"title": "Stopwatch", "slug": "stopwatch", "components": ["Stopwatch"]}, {"title": "Calculator", "slug": "calculator", "components": ["Calculator"]}, {"title": "UnitConverter", "slug": "unitconverter", "components": ["UnitConverter"]}, {"title": "QRCodeScanner", "slug": "qrcodescanner", "components": ["QRCodeScanner"]}, {"title": "Captcha", "slug": "captcha", "components": ["Captcha"]}, {"title": "BarcodeDisplay · Signature · Rating Widget", "slug": "barcodedisplay-signature-rating-widget", "components": ["BarcodeDisplay", "Signature", "Rating Widget"]}, {"title": "Poll / Survey", "slug": "poll-survey", "components": ["Poll", "Survey"]}, {"title": "Quiz / Flashcard", "slug": "quiz-flashcard", "components": ["Quiz", "Flashcard"]}, {"title": "ChangeLog Viewer", "slug": "changelog-viewer", "components": ["ChangeLog Viewer"]}, {"title": "LicenseViewer", "slug": "licenseviewer", "components": ["LicenseViewer"]}, {"title": "Terms / Consent Dialog / CookieBanner", "slug": "terms-consent-dialog-cookiebanner", "components": ["Terms", "Consent Dialog", "CookieBanner"]}, {"title": "ExportDialog / ImportDialog / CSVImporter", "slug": "exportdialog-importdialog-csvimporter", "components": ["ExportDialog", "ImportDialog", "CSVImporter"]}, {"title": "WebView / HTMLPreview", "slug": "webview-htmlpreview", "components": ["WebView", "HTMLPreview"]}, {"title": "IframeEmbed / EmbedCard", "slug": "iframeembed-embedcard", "components": ["IframeEmbed", "EmbedCard"]}, {"title": "PrintButton · ShareSheet", "slug": "printbutton-sharesheet", "components": ["PrintButton", "ShareSheet"]}]}, +{"number": 43, "slug": "tooling", "title": "Library Tooling", "summary": "Tools for building with the library: this gallery, a playground that edits a component live, tools that inspect a running window, its boxes, frames, renders and input, and catalogs of its tokens, icons and contrast.", "stories": [{"title": "InspectorOverlay", "slug": "inspectoroverlay", "components": ["InspectorOverlay"]}, {"title": "FPSMeter / PerfOverlay", "slug": "fpsmeter-perfoverlay", "components": ["FPSMeter", "PerfOverlay"]}, {"title": "RenderCounter", "slug": "rendercounter", "components": ["RenderCounter"]}, {"title": "EventLogger", "slug": "eventlogger", "components": ["EventLogger"]}, {"title": "DesignTokenViewer", "slug": "designtokenviewer", "components": ["DesignTokenViewer"]}, {"title": "IconBrowser", "slug": "iconbrowser", "components": ["IconBrowser"]}, {"title": "A11yChecker", "slug": "a11ychecker", "components": ["A11yChecker"]}, {"title": "ScreenshotTest Harness", "slug": "screenshottest-harness", "components": ["ScreenshotTest Harness"]}, {"title": "ComponentGallery / Storybook", "slug": "componentgallery-storybook", "components": ["ComponentGallery", "Storybook"]}, {"title": "PlaygroundPanel", "slug": "playgroundpanel", "components": ["PlaygroundPanel"]}]} +]} diff --git a/examples/gallery/ui.rs b/examples/gallery/ui.rs index 4471be9..3c9b26d 100644 --- a/examples/gallery/ui.rs +++ b/examples/gallery/ui.rs @@ -1,30 +1,109 @@ +use std::cell::RefCell; + use ely_gpui_component::theme::{ActiveTheme, TextSize}; use gpui::{ - App, Div, Entity, FontWeight, IntoElement, ParentElement, SharedString, Styled, Window, div, + AnyElement, App, Div, Entity, FontWeight, Global, ImageSource, IntoElement, ParentElement, + Resource, SharedString, Styled, Window, div, prelude::*, }; -/// Titled block of demos. +/// The one section a story draws, while a page renders; the rest draw nothing. +pub struct Story { + wanted: SharedString, + met: RefCell>, +} + +impl Global for Story {} + +impl Story { + pub fn new(wanted: SharedString) -> Self { + Self { + wanted, + met: RefCell::default(), + } + } + + fn answers(&self, title: &str) -> bool { + *self.wanted == *title || *self.wanted == slug(title) + } + + /// Why the page drew no single section by this name, if it did not. + pub fn missing(&self, page: &str) -> Option { + let met = self.met.borrow(); + match met.iter().filter(|title| self.answers(title)).count() { + 1 => None, + 0 => Some(format!( + "The page {page} has no story {}. Its stories: {}.", + self.wanted, + met.join(", ") + )), + n => Some(format!("{n} stories on {page} answer to {}.", self.wanted)), + } + } +} + +/// A title as a story's address: lowercase letters and digits, dashes between. +pub fn slug(title: &str) -> String { + let mut slug = String::new(); + for c in title.chars() { + if c.is_ascii_alphanumeric() { + slug.push(c.to_ascii_lowercase()); + } else if !slug.is_empty() && !slug.ends_with('-') { + slug.push('-'); + } + } + slug.trim_end_matches('-').to_string() +} + +/// Titled block of demos. Under a story, only the one asked for draws, without its title. pub fn section(title: impl Into, note: impl Into, cx: &App) -> Div { + let title = title.into(); let theme = cx.theme(); - div().flex().flex_col().gap_4().pt_10().child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_size(theme.text_size(TextSize::Md)) - .font_weight(FontWeight::SEMIBOLD) - .text_color(theme.colors.fg) - .child(title.into()), - ) - .child( - div() - .text_size(theme.text_size(TextSize::Sm)) - .text_color(theme.colors.fg_muted) - .child(note.into()), - ), - ) + let story = cx.try_global::(); + let shown = story.is_none_or(|story| { + story.met.borrow_mut().push(title.clone()); + story.answers(&title) + }); + div() + .flex() + .flex_col() + .gap_4() + .when(story.is_none(), |section| section.pt_10()) + .when(!shown, |section| section.hidden()) + .child( + div() + .flex() + .flex_col() + .gap_1() + .when(story.is_none(), |header| { + header.child( + div() + .text_size(theme.text_size(TextSize::Md)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(theme.colors.fg) + .child(title), + ) + }) + .child( + div() + .text_size(theme.text_size(TextSize::Sm)) + .text_color(theme.colors.fg_muted) + .child(note.into()), + ), + ) +} + +/// A picture under the gallery's assets: a file natively, an address on the web. +pub fn resource(path: impl Into) -> Resource { + let path = path.into(); + if cfg!(target_family = "wasm") { + Resource::Uri(path.into()) + } else { + Resource::Path(std::path::Path::new(path.as_ref()).into()) + } +} + +pub fn picture(path: impl Into) -> ImageSource { + ImageSource::Resource(resource(path)) } pub fn row() -> Div { @@ -84,6 +163,22 @@ pub fn blocked(text: impl Into, cx: &App) -> impl IntoElement { .child(div().flex_1().min_w_0().child(text.into())) } +/// What a browser cannot do here, and why: a note on the web, nothing natively. +pub fn web_note(text: &'static str, cx: &App) -> Option { + cfg!(target_family = "wasm").then(|| blocked(text, cx).into_any_element()) +} + +/// A button that opens a window of its own; a browser page is one window, so there a note stands in. +pub fn own_window(button: impl IntoElement, cx: &App) -> AnyElement { + match web_note( + "A browser page is a single window: gpui opens no second window or popup on the web, so this one cannot open here.", + cx, + ) { + Some(note) => note, + None => button.into_any_element(), + } +} + /// A demo's state, kept across frames under `key`. pub fn keep( key: &'static str, diff --git a/examples/gallery/web.rs b/examples/gallery/web.rs new file mode 100644 index 0000000..6635ec8 --- /dev/null +++ b/examples/gallery/web.rs @@ -0,0 +1,115 @@ +use std::{cell::RefCell, rc::Rc, sync::Arc}; + +use anyhow::{Context as _, Result, anyhow, bail}; +use ely_gpui_component::Assets; +use gpui::{Application, AsyncApp, SharedString, WindowHandle}; +use gpui_web::{CanvasFontFallback, WebBackendPreference, WebPlatform}; +use wasm_bindgen::prelude::*; +use web_sys::UrlSearchParams; + +use crate::{Start, choose, launch, pages, shell::Choice, shell::Gallery}; + +thread_local! { + /// The running app and its window, for the host page's calls. + static HOST: RefCell)>> = const { RefCell::new(None) }; +} + +fn js(error: JsValue) -> anyhow::Error { + anyhow!("{error:?}") +} + +fn choice(theme: &str) -> Result { + match theme { + "light" => Ok(Choice::Light), + "dark" => Ok(Choice::Dark), + other => bail!("theme is light or dark, not {other}"), + } +} + +/// `?page=&story=&theme=light|dark`, each optional; a story needs its page. +fn read(search: &str) -> Result<Start> { + let assets = asset!(""); + if !(assets.starts_with("https://") || assets.starts_with("http://")) || !assets.ends_with('/') + { + bail!("ELY_GALLERY_ASSETS is an address ending in /, not {assets}"); + } + let mut start = Start { + page: 0, + story: None, + choice: Choice::Light, + narrow: None, + failure: None, + }; + let params = UrlSearchParams::new_with_str(search).map_err(js)?; + for key in js_sys::try_iter(¶ms.keys()) + .map_err(js)? + .context("the address's parameters do not iterate")? + { + let key = key + .map_err(js)? + .as_string() + .context("a parameter without a name")?; + let value = params.get(&key).context("a parameter without a value")?; + match key.as_str() { + "page" => { + start.page = pages::find(&value).with_context(|| format!("no page {value}"))? + } + "story" => start.story = Some(value.into()), + "theme" => start.choice = choice(&value)?, + other => bail!("unknown parameter {other}; the gallery reads page, story and theme"), + } + } + if start.story.is_some() && params.get("page").is_none() { + bail!("a story needs its page: ?page=<slug>&story=<title>"); + } + Ok(start) +} + +/// The gallery in a browser: one window over the page's canvas. +#[wasm_bindgen(start)] +pub fn start() { + gpui_platform::web_init(); + let search = web_sys::window() + .expect("a browser window") + .location() + .search() + .expect("the page's address"); + let start = read(&search).unwrap_or_else(|error| { + log::error!("gallery: {search} -> {error:#}"); + Start { + page: 0, + story: None, + choice: Choice::Light, + narrow: None, + failure: Some(SharedString::from(format!("{search}: {error:#}"))), + } + }); + // One thread: a static site sends no cross-origin isolation, so there is no shared memory. + let platform = Rc::new(WebPlatform::new_with_backend_and_font_fallback( + false, + WebBackendPreference::Auto, + CanvasFontFallback::EmojiAndCjk, + )); + let http = Arc::new(platform.fetch_http_client()); + Application::with_platform(platform) + .with_http_client(http) + .with_assets(Assets) + .run(move |cx| { + let window = launch(start, cx); + HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window))); + }); +} + +/// Switches Ely's mode from the host page: `light` or `dark`. +#[wasm_bindgen] +pub fn set_theme(theme: &str) -> Result<(), JsError> { + let choice = choice(theme).map_err(|error| JsError::new(&format!("{error:#}")))?; + HOST.with(|host| { + let host = host.borrow(); + let (app, window) = host + .as_ref() + .ok_or_else(|| JsError::new("the gallery's window is not open yet"))?; + app.update(|cx| choose(*window, choice, cx)); + Ok(()) + }) +} diff --git a/scripts/check.sh b/scripts/check.sh index 86cc867..fbfa4da 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -4,6 +4,7 @@ set -eu cargo fmt --check cargo clippy --all-targets --features test-support -- -D warnings cargo test --lib --features test-support --quiet +python3 scripts/stories.py --check fail=0 files=$(git ls-files -co --exclude-standard '*.rs') multi=$(awk 'FNR==1{prev=0} /^[[:space:]]*\/\// {if(prev) print FILENAME":"FNR; prev=1; next} {prev=0}' $files) @@ -17,6 +18,6 @@ raw=$(awk '/#\[cfg\(test\)\]/{nextfile} /(^|[^.[:alnum:]_])px\(/{print FILENAME" [ -z "$raw" ] || { echo "raw px in components:"; echo "$raw"; fail=1; } deferred=$(grep -rn 'deferred(' src --include='*.rs' | grep -v '^src/primitives/layer.rs' || true) [ -z "$deferred" ] || { echo "gpui deferred outside primitives::raise:"; echo "$deferred"; fail=1; } -instant=$(grep -rnE '(^|[^_[:alnum:]])time::(Instant|\{[^}]*Instant)' src || true) +instant=$(grep -rnE '(^|[^_[:alnum:]])time::(Instant|\{[^}]*Instant)' src examples || true) [ -z "$instant" ] || { echo "std Instant panics in the browser, use web_time::Instant:"; echo "$instant"; fail=1; } exit $fail diff --git a/scripts/stories.py b/scripts/stories.py new file mode 100644 index 0000000..9c176a0 --- /dev/null +++ b/scripts/stories.py @@ -0,0 +1,186 @@ +"""Writes examples/gallery/stories.json: every gallery page and its sections, in the order they draw. + +A section is a story the web gallery shows alone at ?page=<page slug>&story=<story slug>. Its +components are the names in its title that are entries in tasks/*.md, so "Sizes" names none. +`--check` fails if the committed file differs. +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PAGES = ROOT / "examples/gallery/pages" +OUT = ROOT / "examples/gallery/stories.json" + + +def strip(text): + """Blanks comments and literals' insides, keeping offsets, so braces and calls parse.""" + out, i, n = list(text), 0, len(text) + + def blank(a, b): + for k in range(a, b): + if out[k] != "\n": + out[k] = " " + + while i < n: + if text.startswith("//", i): + j = text.find("\n", i) + j = n if j < 0 else j + blank(i, j) + i = j + elif text.startswith("/*", i): + j = text.index("*/", i) + 2 + blank(i, j) + i = j + elif m := re.match(r'b?r(#*)"', text[i:]): + end = '"' + m.group(1) + j = text.index(end, i + m.end()) + len(end) + blank(i + m.end(), j - len(end)) + i = j + elif text[i] == '"': + j = i + 1 + while text[j] != '"': + j += 2 if text[j] == "\\" else 1 + blank(i + 1, j) + i = j + 1 + elif m := re.match(r"'(\\.[^']*|[^'\\])'", text[i:]): + blank(i, i + m.end()) + i += m.end() + else: + i += 1 + return "".join(out) + + +def literal(text, at): + """The string literal starting at `at`, decoded.""" + m = re.compile(r'"((?:[^"\\]|\\.)*)"', re.S).match(text, at) + return json.loads('"' + re.sub(r"\\\n\s*", "", m.group(1)) + '"') + + +def functions(folder): + """Every `fn` in a page's folder: (module, name) -> (source, stripped, body start, body end).""" + found = {} + for file in sorted(folder.rglob("*.rs")): + module = file.stem + text = file.read_text() + bare = strip(text) + for m in re.finditer(r"\bfn\s+(\w+)\b", bare): + depth, j = 0, bare.index("{", m.end()) + if ";" in bare[m.end():j]: + continue + k = j + while True: + depth += {"{": 1, "}": -1}.get(bare[k], 0) + if depth == 0: + break + k += 1 + found[(module, m.group(1))] = (text, bare, j, k) + return found + + +def sections(folder): + """Section titles in the order the page's render reaches them; fails on any it never reaches.""" + fns = functions(folder) + order, seen = [], set() + + def walk(module, name): + if (module, name) in seen: + return + seen.add((module, name)) + text, bare, start, end = fns[(module, name)] + call = re.compile(r"(?<![\w.])(?:(\w+)::)?(\w+)\s*\(") + for m in call.finditer(bare, start, end): + owner, callee = m.group(1) or module, m.group(2) + if callee == "section" and m.group(1) is None: + at = m.end() + len(bare[m.end():]) - len(bare[m.end():].lstrip()) + if text[at] != '"': + sys.exit(f"{folder.name}/{module}.rs: a section whose title is not a literal") + order.append((literal(text, at), module)) + elif (owner, callee) in fns: + walk(owner, callee) + + walk("mod", "render") + reached = {title for title, _ in order} + for (module, _), (text, bare, start, end) in fns.items(): + for m in re.finditer(r"(?<![\w.])section\s*\(\s*\"", bare[start:end]): + title = literal(text, start + m.end() - 1) + if title not in reached: + sys.exit(f"{folder.name}/{module}.rs: render never reaches section {title!r}") + return [title for title, _ in order] + + +def slug(title): + """As the gallery's ui::slug: ASCII letters and digits, lowercased, dashes between.""" + out = re.sub(r"[^a-z0-9]+", "-", "".join(c.lower() if c.isascii() else " " for c in title)) + return out.strip("-") + + +def entries(): + """Names the task lists give components, keyed by letters alone.""" + names = {} + for file in sorted((ROOT / "tasks").glob("*.md")): + for line in file.read_text().splitlines(): + if m := re.match(r"- \[[ x]\] (.+?)(?: — | → | \(T\d|$)", line): + for name in m.group(1).split(" / "): + names[key(name)] = re.sub(r"\s*\([^)]*\)", "", name).strip() + return names + + +def key(name): + return re.sub(r"[^a-z0-9]", "", re.sub(r"\([^)]*\)", "", name).lower()) + + +def components(title, known): + """The task lists' names for what the title names: "Icon button" is IconButton.""" + names = [] + for part in re.sub(r"\s*\([^)]*\)", "", title).split(" · "): + for name in part.split(" → ")[0].split(" / "): + if key(name) in known and known[key(name)] not in names: + names.append(known[key(name)]) + return names + + +def pages(): + listing = (PAGES / "mod.rs").read_text() + order = re.findall(r"^\s+(\w+)::PAGE,$", listing, re.M) + known = entries() + out = [] + for module in order: + folder = PAGES / module + head = (folder / "mod.rs").read_text() + field = lambda name: re.search(rf"^\s+{name}: (.+),$", head, re.M).group(1) + titles = sections(folder) + slugs = [slug(title) for title in titles] + for twice in {s for s in slugs if slugs.count(s) > 1}: + sys.exit(f"{module}: two stories answer to {twice}") + out.append( + { + "number": int(field("number")), + "slug": json.loads(field("slug")), + "title": json.loads(field("title")), + "summary": json.loads(field("summary")), + "stories": [ + {"title": t, "slug": s, "components": components(t, known)} + for t, s in zip(titles, slugs) + ], + } + ) + return out + + +def main(): + rows = [json.dumps(page, ensure_ascii=False) for page in pages()] + text = '{"pages": [\n' + ",\n".join(rows) + "\n]}\n" + if sys.argv[1:] == ["--check"]: + if OUT.read_text() != text: + sys.exit(f"{OUT.relative_to(ROOT)} is stale: run python3 scripts/stories.py") + elif sys.argv[1:]: + sys.exit("usage: python3 scripts/stories.py [--check]") + else: + OUT.write_text(text) + + +if __name__ == "__main__": + main() diff --git a/src/theme/mod.rs b/src/theme/mod.rs index f3237b1..e8429c0 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -136,6 +136,16 @@ impl Theme { Self::fade_to_target(cx); } + /// Sets `mode` without the fade, as before a window's first frame; a fade under way stops. + pub fn set_mode_now(mode: Mode, cx: &mut App) { + log::info!("theme: mode -> {mode:?}, at once"); + let theme = cx.global_mut::<Theme>(); + theme.mode = mode; + theme.transition += 1; + theme.colors = theme.target(); + cx.refresh_windows(); + } + pub fn set_high_contrast(on: bool, cx: &mut App) { log::info!("theme: high contrast -> {on}"); cx.global_mut::<Theme>().high_contrast = on; diff --git a/src/theme/tests.rs b/src/theme/tests.rs index f2b1d73..863c5a4 100644 --- a/src/theme/tests.rs +++ b/src/theme/tests.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use gpui::{TestAppContext, hsla}; use super::{ActiveTheme, Mode, Palette, Syntax, Theme}; @@ -19,6 +21,29 @@ fn an_owners_palette_serves_its_mode_alone(cx: &mut TestAppContext) { assert_eq!(cx.read(|cx| cx.theme().palette()), Palette::light(false)); } +/// A mode set at once wears its palette at once, and a fade under way cannot pull it back. +#[gpui::test] +fn a_mode_set_at_once_skips_the_fade(cx: &mut TestAppContext) { + cx.update(|cx| { + Theme::init(cx); + Theme::update(cx, |theme| theme.reduced_motion = true); + Theme::set_mode(Mode::Dark, cx); + Theme::set_mode_now(Mode::Light, cx); + }); + assert_eq!( + cx.read(|cx| cx.theme().colors.clone()), + Palette::light(false) + ); + std::thread::sleep(Duration::from_millis(2)); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.run_until_parked(); + assert!(!cx.read(|cx| cx.theme().is_dark())); + assert_eq!( + cx.read(|cx| cx.theme().colors.clone()), + Palette::light(false) + ); +} + /// Every color of the palette and of code answers to its field's name, the quiet fills too. #[test] fn every_color_answers_to_its_name() {