T44d Web · Gallery builds for wasm32: one story per section by URL, themed by the host

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-29 19:16:41 -04:00
1 parent a03df0987c
commit 48c7b09d8d
83 files changed
+1000 -455

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ely Gallery</title>
<style>
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #fcfaf7; }
html.dark, html.dark body { background: #131110; }
</style>
<script>
// Ely's own background under the canvas until the first frame, in the mode asked for.
if (new URLSearchParams(location.search).get("theme") === "dark") {
document.documentElement.classList.add("dark");
}
</script>
<script type="module">
import init, { set_theme } from "./gallery.js";
await init();
// The host switches the mode later: gallery.setTheme("dark"), or a message { ely: "theme", theme }.
const setTheme = (theme) => {
set_theme(theme);
document.documentElement.classList.toggle("dark", theme === "dark");
};
window.gallery = { setTheme };
addEventListener("message", ({ data }) => {
if (data && data.ely === "theme") setTheme(data.theme);
});
</script>
</head>
<body></body>
</html>
+112 -45
View File
@@ -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<Menu> {
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<shell::Gallery>, choice: shell::Choice, cx: &mut
}
}
struct Args {
page: Option<usize>,
capture: Option<PathBuf>,
narrow: Option<f32>,
/// What the gallery opens on.
pub struct Start {
pub page: usize,
/// One section, by title or slug, drawn alone.
pub story: Option<SharedString>,
pub choice: shell::Choice,
/// Narrow check width, if any.
pub narrow: Option<f32>,
/// Why the start could not be read, shown in place of a page.
pub failure: Option<SharedString>,
}
/// Sets Ely up and opens the gallery's window.
fn launch(start: Start, cx: &mut App) -> WindowHandle<shell::Gallery> {
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<usize>,
capture: Option<PathBuf>,
}
#[cfg(not(target_family = "wasm"))]
fn parse_args() -> Result<Args> {
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<Args> {
"--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<Args> {
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(())
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -2
View File
@@ -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,
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+4 -6
View File
@@ -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()
};
+1 -1
View File
@@ -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,
+5 -8
View File
@@ -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."),
)
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -4
View File
@@ -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"];
+3 -12
View File
@@ -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 {
+1 -1
View File
@@ -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,
+2 -20
View File
@@ -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.";
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+4 -9
View File
@@ -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,
);
+1 -1
View File
@@ -14,7 +14,7 @@ mod transfers;
use super::Page;
use crate::{
probe::probe,
script::Step,
step::Step,
ui::{change, keep, section},
};
+2 -1
View File
@@ -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};
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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<Job> {
@@ -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.)),
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -16,7 +16,7 @@ mod write;
use super::Page;
use crate::{
probe::probe,
script::Step,
step::Step,
ui::{change, keep, section},
};
+4 -6
View File
@@ -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 {
+4 -7
View File
@@ -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));
+4 -7
View File
@@ -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"),
),
)
+7 -16
View File
@@ -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<f32>); 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)),
),
))
+9 -46
View File
@@ -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),
)
+1 -1
View File
@@ -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,
+7 -16
View File
@@ -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.",
+1 -1
View File
@@ -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,
+5 -6
View File
@@ -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 {
+1 -1
View File
@@ -15,7 +15,7 @@ mod pickers;
use super::Page;
use crate::{
probe::probe,
script::Step,
step::Step,
ui::{change, keep, section},
};
+5 -7
View File
@@ -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<Gif> {
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),
+4 -7
View File
@@ -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<RenderImage> {
@@ -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(
+1 -1
View File
@@ -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;
+2 -1
View File
@@ -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],
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+5 -19
View File
@@ -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),
)
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+3 -6
View File
@@ -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<u32> {
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(
+1 -1
View File
@@ -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,
+2 -1
View File
@@ -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::{
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 {
+23 -21
View File
@@ -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,
+1 -1
View File
@@ -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,
+4 -8
View File
@@ -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))
+22 -2
View File
@@ -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))
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+3
View File
@@ -12,6 +12,7 @@ pub struct Probes(HashMap<&'static str, Bounds<Pixels>>);
impl Global for Probes {}
impl Probes {
#[cfg(not(target_family = "wasm"))]
pub fn get(key: &str, cx: &App) -> Option<Bounds<Pixels>> {
cx.try_global::<Probes>()
.and_then(|probes| probes.0.get(key).copied())
@@ -39,10 +40,12 @@ impl Opened {
cx.default_global::<Opened>().0.insert(key, handle);
}
#[cfg(not(target_family = "wasm"))]
pub fn take(key: &str, cx: &mut App) -> Option<AnyWindowHandle> {
cx.default_global::<Opened>().0.remove(key)
}
#[cfg(not(target_family = "wasm"))]
pub fn get(key: &str, cx: &App) -> Option<AnyWindowHandle> {
cx.try_global::<Opened>()
.and_then(|opened| opened.0.get(key).copied())
+1 -42
View File
@@ -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);
+114 -61
View File
@@ -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<SharedString>,
/// Why nothing can be shown, in place of the page.
failure: Option<SharedString>,
/// 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<f32>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
pub fn new(start: Start, window: &mut Window, cx: &mut Context<Self>) -> 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>) {
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<Pixels>, cx: &mut Context<Self>) -> bool {
#[cfg(not(target_family = "wasm"))]
pub fn reveal(&mut self, target: gpui::Bounds<Pixels>, cx: &mut Context<Self>) -> 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>) {
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<Self>,
) -> Result<AnyElement, SharedString> {
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::<Story>().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<Self>) -> 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,
),
),
)
),
),
)
}
}
+41
View File
@@ -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,
}
+45
View File
@@ -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": "codeinputLine truncated
{"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": ["PLine truncated
{"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"]}]}
]}
+116 -21
View File
@@ -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<Vec<SharedString>>,
}
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<String> {
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<SharedString>, note: impl Into<SharedString>, 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::<Story>();
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<SharedString>) -> 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<SharedString>) -> ImageSource {
ImageSource::Resource(resource(path))
}
pub fn row() -> Div {
@@ -84,6 +163,22 @@ pub fn blocked(text: impl Into<SharedString>, 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<AnyElement> {
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<T: 'static>(
key: &'static str,
+115
View File
@@ -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<Option<(AsyncApp, WindowHandle<Gallery>)>> = const { RefCell::new(None) };
}
fn js(error: JsValue) -> anyhow::Error {
anyhow!("{error:?}")
}
fn choice(theme: &str) -> Result<Choice> {
match theme {
"light" => Ok(Choice::Light),
"dark" => Ok(Choice::Dark),
other => bail!("theme is light or dark, not {other}"),
}
}
/// `?page=<slug>&story=<title or slug>&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(&params.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(())
})
}