diff --git a/AGENTS.md b/AGENTS.md index bdc68c5..50e8c61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,9 +25,9 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - `cargo run --example gallery -- --capture ` writes PNGs of every page, light and dark, top to bottom, then each page's scripted states, including windows the demos open. macOS only. - `cargo run --example gallery -- --narrow 280 --capture ` lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card. - `RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown` checks the web build; gpui_web's `wasm_thread` needs the bootstrap, as Zed's web examples do. -- `ELY_GALLERY_ASSETS=
scripts/web.sh ` builds the gallery for the browser, profile `web`, and fails past Cloudflare's 25 MiB. It opens at `?page=&story=&theme=light|dark`; the host page calls `gallery.setTheme(mode)` or posts `{ ely: "theme", theme }` later. -- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site. `scripts/check.sh` refuses a stale one. -- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules. +- `ELY_GALLERY_ASSETS=<address of out/assets/> scripts/web.sh <out>` builds the gallery for the browser, profile `web`, and fails past Cloudflare's 25 MiB. It opens at `?page=<slug>&story=<title or slug>&theme=light|dark`; the host page calls `gallery.setTheme(mode)` or posts `{ ely: "theme", theme }`, before the wasm runs or after, and the page posts `{ ely: "ready" }` to its parent once it runs. +- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one. +- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32. - `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk. - `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`. - `python3 scripts/countries.py <ne_110m_admin_0_countries.geojson>` writes the bundled world; `python3 scripts/tiles.py <same file>` writes the gallery's map tiles. @@ -180,9 +180,10 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery. - What cannot build for wasm32 is absent there through `cfg(not(target_family = "wasm"))`, never faked: the live terminal, `Terminal` and `Launch` with their grid, keys and search, and the gallery's capture harness. - A gallery demo a browser cannot run says why on the web through `ui::web_note`, in the blocked style: a second window (`own_window`), files from the dialog or a drop (`NO_FILES`), the menu bar, window moves, a clipboard read. -- A story is a gallery section: its title is a literal, one per page. Under `ui::Story` the one asked for draws without its title and the rest hide, which skips their prepaint and paint; a story the page lacks fails in the page. +- A story is a gallery section: its title is a literal, one per page. Under `ui::Story` the one asked for draws without its title; the rest drop their demos (`ui::Section`), so nothing of theirs lays out, loads or moves. A story the page lacks fails in the page. - The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there. -- The first frame wears the host's mode through `Theme::set_mode_now`; `set_mode` fades from the colors on screen. +- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`: gpui_web reads no clipboard on demand, so Paste stays unbound there (`TextInput`, `CodeEditor`). +- The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen. - The system time zone comes from `typography::format::system_zone`, which fails loud; jiff's `TimeZone::system()` falls back to UTC in silence. - A Select with nothing to pick stays shut: disabled, it shows its placeholder; open, it fails (`media::DeviceSelector` says no device was found). - A list opens with its cursor on the current choice: Select on the chosen row, Combobox on its value, MultiSelect on the first one ticked, SelectableList on its first selected row. A selection the owner makes, not the echo of the list's own pick, moves SelectableList's cursor there. diff --git a/examples/gallery/index.html b/examples/gallery/index.html index 5442c2e..e2621bf 100644 --- a/examples/gallery/index.html +++ b/examples/gallery/index.html @@ -16,16 +16,22 @@ </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 }. + // The host sets the mode with gallery.setTheme(mode) or a message { ely: "theme", theme }, early or late. + let early = null; + let apply = (theme) => { early = theme; }; const setTheme = (theme) => { - set_theme(theme); + apply(theme); document.documentElement.classList.toggle("dark", theme === "dark"); }; window.gallery = { setTheme }; addEventListener("message", ({ data }) => { if (data && data.ely === "theme") setTheme(data.theme); }); + await init(); + apply = set_theme; + if (early) set_theme(early); + // A host that waits for this knows the mode it sends lands. + window.parent.postMessage({ ely: "ready" }, "*"); </script> </head> <body></body> diff --git a/examples/gallery/pages/i18n/a11y.rs b/examples/gallery/pages/i18n/a11y.rs index ef0368c..a516a22 100644 --- a/examples/gallery/pages/i18n/a11y.rs +++ b/examples/gallery/pages/i18n/a11y.rs @@ -10,7 +10,7 @@ use gpui::{ }; use crate::probe::probe; -use crate::ui::{blocked, section, specimen, specimens}; +use crate::ui::{UNREAD, blocked, section, specimen, specimens}; const NAV: [&str; 5] = ["Home", "Docs", "Pricing", "Blog", "About"]; @@ -65,7 +65,7 @@ pub fn skip_link(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> ), ) .child(blocked( - "LiveRegion / Announcer, AccessibleIcon, AriaLabel, ScreenReaderOnly: not yet shown: gpui builds an AccessKit tree from elements with an id and a role, and no screen reader here reads it.", + format!("LiveRegion / Announcer, AccessibleIcon, AriaLabel, ScreenReaderOnly: not yet shown: {UNREAD}."), cx, )) } diff --git a/examples/gallery/pages/interaction/pointer.rs b/examples/gallery/pages/interaction/pointer.rs index f47861c..dbf1aaa 100644 --- a/examples/gallery/pages/interaction/pointer.rs +++ b/examples/gallery/pages/interaction/pointer.rs @@ -193,7 +193,7 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { .child(*name), ) }); - section( + let moves = section( "Resizable · Rotatable · Selectable / SelectionArea · RubberBandSelection · Sortable", "A box resized from its edges or its grip; a picture turned by its knob, as gpui turns svgs alone; tiles taken by a band drawn on empty space; rows sorted by drag through motion::Reorder.", cx, @@ -254,73 +254,72 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { })), cx, )), + ); + let presses = section( + "LongPress · DoubleClick · Gesture · PanZoom", + "A long press is primitives::Pressable; a double press is gpui's click_count. The wheel pans and, with Command, zooms layout::Viewport; two fingers swipe lists::SwipeableListItem.", + cx, ) .child( - section( - "LongPress · DoubleClick · Gesture · PanZoom", - "A long press is primitives::Pressable; a double press is gpui's click_count. The wheel pans and, with Command, zooms layout::Viewport; two fingers swipe lists::SwipeableListItem.", - cx, - ) - .child( - specimens() - .child(specimen( - "LongPress → primitives::Pressable", - Pressable::new("interaction-press", move |down, _, cx| { - let theme = cx.theme(); - div() - .px_4() - .py_3() - .rounded_lg() - .border_1() - .border_color(theme.colors.border) - .bg(if down { theme.colors.active } else { theme.colors.surface }) - .text_size(theme.text_size(TextSize::Sm)) - .child(format!("Pressed {presses} · held {held}")) - .into_any_element() - }) - .on_press(move |_, cx| change(&press, cx, |shapes| shapes.presses += 1)) - .on_long_press(move |_, cx| change(&long, cx, |shapes| shapes.held += 1)), - cx, - )) - .child(specimen( - "DoubleClick", + specimens() + .child(specimen( + "LongPress → primitives::Pressable", + Pressable::new("interaction-press", move |down, _, cx| { + let theme = cx.theme(); div() - .id("interaction-double") .px_4() .py_3() .rounded_lg() .border_1() .border_color(theme.colors.border) - .tab_index(0) - .focus_ring(cx) + .bg(if down { theme.colors.active } else { theme.colors.surface }) .text_size(theme.text_size(TextSize::Sm)) - .on_click(move |event, _, cx| { - if event.click_count() == 2 { - change(&double, cx, |shapes| shapes.doubles += 1) - } - }) - .child(format!("Opened {}", plural(doubles as u64, "time", "times"))), - cx, - )) - .child(specimen( - "PanZoom → layout::Viewport", - div().w(px(240.)).h(px(140.)).rounded_lg().border_1().border_color(theme.colors.border).overflow_hidden().child( - Viewport::new("interaction-viewport", |view: Transform, _, cx| { - let theme = cx.theme(); - let corner = view.apply(point(px(80.), px(30.))); - div() - .absolute() - .left(corner.x) - .top(corner.y) - .size(px(80.) * view.scale) - .rounded_lg() - .bg(theme.colors.sunken) - .into_any_element() - }), - ), - cx, - )), - ) - .child(blocked("Pinch: gpui hands a trackpad pinch to on_pinch, and the viewport does not zoom on it yet.", cx)), + .child(format!("Pressed {presses} · held {held}")) + .into_any_element() + }) + .on_press(move |_, cx| change(&press, cx, |shapes| shapes.presses += 1)) + .on_long_press(move |_, cx| change(&long, cx, |shapes| shapes.held += 1)), + cx, + )) + .child(specimen( + "DoubleClick", + div() + .id("interaction-double") + .px_4() + .py_3() + .rounded_lg() + .border_1() + .border_color(theme.colors.border) + .tab_index(0) + .focus_ring(cx) + .text_size(theme.text_size(TextSize::Sm)) + .on_click(move |event, _, cx| { + if event.click_count() == 2 { + change(&double, cx, |shapes| shapes.doubles += 1) + } + }) + .child(format!("Opened {}", plural(doubles as u64, "time", "times"))), + cx, + )) + .child(specimen( + "PanZoom → layout::Viewport", + div().w(px(240.)).h(px(140.)).rounded_lg().border_1().border_color(theme.colors.border).overflow_hidden().child( + Viewport::new("interaction-viewport", |view: Transform, _, cx| { + let theme = cx.theme(); + let corner = view.apply(point(px(80.), px(30.))); + div() + .absolute() + .left(corner.x) + .top(corner.y) + .size(px(80.) * view.scale) + .rounded_lg() + .bg(theme.colors.sunken) + .into_any_element() + }), + ), + cx, + )), ) + .child(blocked("Pinch: gpui hands a trackpad pinch to on_pinch, and the viewport does not zoom on it yet.", cx)); + div().child(moves).child(presses) } diff --git a/examples/gallery/pages/menus/more.rs b/examples/gallery/pages/menus/more.rs index a4e6c6d..1b7621d 100644 --- a/examples/gallery/pages/menus/more.rs +++ b/examples/gallery/pages/menus/more.rs @@ -27,7 +27,7 @@ pub fn menu_bar(cx: &mut App) -> impl IntoElement + use<> { cx, ) .children(web_note( - "A browser page has no menu bar, and gpui on the web keeps no cx.set_menus: this bar draws the gallery's own list of the same menus.", + "A browser page has no menu bar, and gpui on the web keeps no cx.set_menus: this bar draws the gallery's own list of the same menus. About Ely would open a second window and Quit would end the app, neither of which a page can, so both only log.", cx, )) .child(specimen( diff --git a/examples/gallery/pages/primitives/parts.rs b/examples/gallery/pages/primitives/parts.rs index 6060e7f..60de628 100644 --- a/examples/gallery/pages/primitives/parts.rs +++ b/examples/gallery/pages/primitives/parts.rs @@ -15,7 +15,7 @@ use gpui::{ use crate::{ probe::probe, - ui::{blocked, code, picture, row, section, specimen, specimens}, + ui::{UNREAD, blocked, code, picture, row, section, specimen, specimens}, }; const ATRIUM: &str = asset!("atrium.jpg"); @@ -193,10 +193,8 @@ pub fn backdrop(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { } pub fn visually_hidden(cx: &App) -> impl IntoElement + use<> { - section("VisuallyHidden", "Text for screen readers only.", cx).child(blocked( - "Not yet shown: gpui builds an AccessKit tree from elements with an id and a role, and no screen reader here reads it.", - cx, - )) + section("VisuallyHidden", "Text for screen readers only.", cx) + .child(blocked(format!("Not yet shown: {UNREAD}."), cx)) } fn chip(id: &'static str, label: &'static str, cx: &App) -> impl IntoElement + use<> { diff --git a/examples/gallery/pages/tables/grids.rs b/examples/gallery/pages/tables/grids.rs index 06a2924..c5683da 100644 --- a/examples/gallery/pages/tables/grids.rs +++ b/examples/gallery/pages/tables/grids.rs @@ -8,7 +8,7 @@ use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, p use crate::{ probe::probe, - ui::{keep, section, set}, + ui::{keep, section, set, web_note}, }; type Cells = HashMap<(usize, usize), SharedString>; @@ -35,6 +35,10 @@ pub fn data_grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> "Cells to edit like a spreadsheet's. Arrows walk, Shift stretches a range, typing or a double press edits, Delete clears, and Cmd-C and Cmd-V copy and paste.", cx, ) + .children(web_note( + "A browser reads its clipboard only as it pastes into a text field, so Cmd-V pastes no cells here.", + cx, + )) .child(probe( "grid", div().child( diff --git a/examples/gallery/pages/terminal/mod.rs b/examples/gallery/pages/terminal/mod.rs index 3a64c53..0913998 100644 --- a/examples/gallery/pages/terminal/mod.rs +++ b/examples/gallery/pages/terminal/mod.rs @@ -50,7 +50,7 @@ fn live(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { { 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.", + "A browser starts no processes and has no pseudo-terminal, so no shell runs here, and alacritty's grid, which draws the build's output, does not build for the web. The page's other stories, command blocks, colored output, logs and processes, draw without one.", cx, )) } diff --git a/examples/gallery/shell.rs b/examples/gallery/shell.rs index 644b452..2b5405e 100644 --- a/examples/gallery/shell.rs +++ b/examples/gallery/shell.rs @@ -288,9 +288,10 @@ impl Render for Gallery { return self.root(cx).child( on_axis(div().id(("story", self.pass))) .size_full() - .overflow_y_scroll() + .overflow_scroll() .track_scroll(&self.scroll) - .child(div().p_6().child(body)), + // The page column's 848 plus padding; narrower, the story scrolls sideways. + .child(div().min_w(px(896.0)).p_6().child(body)), ); } let sidebar = self.sidebar(cx); diff --git a/examples/gallery/stories.json b/examples/gallery/stories.json index eb1b69e..b53fde6 100644 --- a/examples/gallery/stories.json +++ b/examples/gallery/stories.json @@ -2,9 +2,9 @@ {"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": 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", "AlwaysOnTop"]}, {"title": "UpdateBanner", "slug": "updatebanner", "components": ["UpdateBanner"]}, {"title": "UpdateDialog", "slug": "updatedialog", "components": ["UpdateDialog"]}, {"title": "CrashReporter", "slug": "crashreporter", "components": ["CrashReporter"]}, {"title": "OfflineIndicator", "slug": "offlineindicator", "components": ["OfflineIndicator"]}, {"title": "ZoomControl", "slug": "zoomcontrol", "components": ["ZoomControl"]}]}, {"number": 5, "slug": "buttons", "title": "Buttons & Actions", "summary": "Seven variants, three sizes, one focus ring, and the verbs around them.", "stories": [{"title": "Button", "slug": "button", "components": ["Button"]}, {"title": "Sizes", "slug": "sizes", "components": []}, {"title": "With icons", "slug": "with-icons", "components": []}, {"title": "Icon button", "slug": "icon-button", "components": ["IconButton"]}, {"title": "ShortcutHint", "slug": "shortcuthint", "components": ["ShortcutHint"]}, {"title": "ButtonGroup", "slug": "buttongroup", "components": ["ButtonGroup"]}, {"title": "SplitButton / DropdownButton / MenuButton / LoadingButton", "slug": "splitbutton-dropdownbutton-menubutton-loadingbutton", "components": ["SplitButton", "DropdownButton", "MenuButton", "LoadingButton"]}, {"title": "ToggleButton", "slug": "togglebutton", "components": ["ToggleButton"]}, {"title": "ToggleGroup", "slug": "togglegroup", "components": ["ToggleGroup"]}, {"title": "SegmentedControl", "slug": "segmentedcontrol", "components": ["SegmentedControl"]}, {"title": "FloatingActionButton", "slug": "floatingactionbutton", "components": ["FloatingActionButton"]}, {"title": "ConfirmButton / HoldToConfirm", "slug": "confirmbutton-holdtoconfirm", "components": ["ConfirmButton", "HoldToConfirm"]}, {"title": "CopyButton / ShareButton / CloseButton / BackButton / MoreButton", "slug": "copybutton-sharebutton-closebutton-backbutton-morebutton", "components": ["CopyButton", "ShareButton", "CloseButton", "BackButton", "MoreButton"]}, {"title": "ActionBar", "slug": "actionbar", "components": ["ActionBar"]}, {"title": "BulkActionBar", "slug": "bulkactionbar", "components": ["BulkActionBar"]}, {"title": "ActionSheet", "slug": "actionsheet", "components": ["ActionSheet"]}, {"title": "QuickActions", "slug": "quickactions", "components": ["QuickActions"]}]}, -{"number": 6, "slug": "forms", "title": "Forms", "summary": "Text, choices, dates, colors, files: every way to say what you mean.", "stories": [{"title": "Input / TextField / ClearableInput", "slug": "input-textfield-clearableinput", "components": ["Input", "TextField", "ClearableInput"]}, {"title": "TextArea / AutoResizeInput", "slug": "textarea-autoresizeinput", "components": ["TextArea", "AutoResizeInput"]}, {"title": "PasswordInput", "slug": "passwordinput", "components": ["PasswordInput"]}, {"title": "SearchInput", "slug": "searchinput", "components": ["SearchInput"]}, {"title": "NumberInput", "slug": "numberinput", "components": ["NumberInput"]}, {"title": "ScrubInput", "slug": "scrubinput", "components": ["ScrubInput"]}, {"title": "CurrencyInput / PercentInput / UnitInput", "slug": "currencyinput-percentinput-unitinput", "components": ["CurrencyInput", "PercentInput", "UnitInput"]}, {"title": "EmailInput / UrlInput", "slug": "emailinput-urlinput", "components": ["EmailInput", "UrlInput"]}, {"title": "PhoneInput / MaskedInput", "slug": "phoneinput-maskedinput", "components": ["PhoneInput", "MaskedInput"]}, {"title": "PinInput / OTPInput", "slug": "pininput-otpinput", "components": ["PinInput", "OTPInput"]}, {"title": "InlineEdit / EditableText", "slug": "inlineedit-editabletext", "components": ["InlineEdit", "EditableText"]}, {"title": "InputGroup / InputAddon", "slug": "inputgroup-inputaddon", "components": ["InputGroup", "InputAddon"]}, {"title": "MentionInput / HashtagInput", "slug": "mentioninput-hashtaginput", "components": ["MentionInput", "HashtagInput"]}, {"title": "TagInput / TokenInput", "slug": "taginput-tokeninput", "components": ["TagInput", "TokenInput"]}, {"title": "KeyValueInput / ListInput", "slug": "keyvalueinput-listinput", "components": ["KeyValueInput", "ListInput"]}, {"title": "PathInput", "slug": "pathinput", "components": ["PathInput"]}, {"title": "RegexInput", "slug": "regexinput", "components": ["RegexInput"]}, {"title": "ExpressionInput", "slug": "expressioninput", "components": ["ExpressionInput"]}, {"title": "HotkeyInput / ShortcutRecorder", "slug": "hotkeyinput-shortcutrecorder", "components": ["HotkeyInput", "ShortcutRecorder"]}, {"title": "Checkbox / CheckboxGroup", "slug": "checkbox-checkboxgroup", "components": ["Checkbox", "CheckboxGroup"]}, {"title": "Radio / RadioGroup", "slug": "radio-radiogroup", "components": ["Radio", "RadioGroup"]}, {"title": "CheckboxCard / RadioCard", "slug": "checkboxcard-radiocard", "components": ["CheckboxCard", "RadioCard"]}, {"title": "Switch / Toggle", "slug": "switch-toggle", "components": ["Switch", "Toggle"]}, {"title": "Select / NativeSelect", "slug": "select-nativeselect", "components": ["Select", "NativeSelect"]}, {"title": "ListBox", "slug": "listbox", "components": ["ListBox"]}, {"title": "Combobox / Autocomplete", "slug": "combobox-autocomplete", "components": ["Combobox", "Autocomplete"]}, {"title": "MultiSelect", "slug": "multiselect", "components": ["MultiSelect"]}, {"title": "CascadeSelect / Cascader", "slug": "cascadeselect-cascader", "components": ["CascadeSelect", "Cascader"]}, {"title": "TransferList", "slug": "transferlist", "components": ["Transfer List"]}, {"title": "ChoiceChips / FilterChips", "slug": "choicechips-filterchips", "components": ["ChoiceChips", "FilterChips"]}, {"title": "Rating", "slug": "rating", "components": ["Rating"]}, {"title": "Slider / RangeSlider / VerticalSlider", "slug": "slider-rangeslider-verticalslider", "components": ["Slider", "RangeSlider", "VerticalSlider"]}, {"title": "Knob / Dial / Stepper", "slug": "knob-dial-stepper", "components": ["Knob", "Dial", "Stepper"]}, {"title": "Calendar", "slug": "calendar", "components": ["Calendar"]}, {"title": "DatePicker / DateRangePicker / DateTimePicker / TimePicker", "slug": "datepicker-daterangepicker-datetimepicker-timepicker", "components": ["DatePicker", "DateRangePicker", "DateTimePicker", "TimePicker"]}, {"title": "MonthPicker / YearPicker / QuarterPicker / WeekPicker", "slug": "monthpicker-yearpicker-quarterpicker-weekpicker", "components": ["MonthPicker", "YearPicker", "QuarterPicker", "WeekPicker"]}, {"title": "RelativeDatePicker / TimezoneSelect / DurationPicker", "slug": "relativedatepicker-timezoneselect-durationpicker", "components": ["RelativeDatePicker", "TimezoneSelect", "DurationPicker"]}, {"title": "CronEditor", "slug": "croneditor", "components": ["CronEditor"]}, {"title": "ColorPicker", "slug": "colorpicker", "components": ["ColorPicker"]}, {"title": "ColorSwatch / ColorPalette / EyeDropper", "slug": "colorswatch-colorpalette-eyedropper", "components": ["ColorSwatch", "ColorPalette", "EyeDropper"]}, {"title": "GradientEditor", "slug": "gradienteditor", "components": ["GradientEditor"]}, {"title": "FileInput / DropZone", "slug": "fileinput-dropzone", "components": ["FileInput", "DropZone"]}, {"title": "SignaturePad", "slug": "signaturepad", "components": ["SignaturePad"]}, {"title": "CodeInput / JsonInput", "slug": "codeinput-jsoninput", "components": ["CodeInput", "JsonInput"]}, {"title": "FontPicker", "slug": "fontpicker", "components": ["FontPicker"]}, {"title": "IconPicker / EmojiPicker", "slug": "iconpicker-emojipicker", "components": ["IconPicker", "EmojiPicker"]}, {"title": "LanguageSelect / CountrySelect / CurrencySelect", "slug": "languageselect-countryselect-currencyselect", "components": ["LanguageSelect", "CountrySelect", "CurrencySelect"]}, {"title": "Form / FormSection / FormField / FieldArray / DirtyIndicator", "slug": "form-formsection-formfield-fieldarray-dirtyindicator", "components": ["Form", "FormSection", "FormField", "FieldArray", "DirtyIndicator"]}, {"title": "InlineForm", "slug": "inlineform", "components": ["InlineForm"]}]}, +{"number": 6, "slug": "forms", "title": "Forms", "summary": "Text, choices, dates, colors, files: every way to say what you mean.", "stories": [{"title": "Input / TextField / ClearableInput", "slug": "input-textfield-clearableinput", "components": ["Input", "TextField", "ClearableInput"]}, {"title": "TextArea / AutoResizeInput", "slug": "textarea-autoresizeinput", "components": ["TextArea", "AutoResizeInput"]}, {"title": "PasswordInput", "slug": "passwordinput", "components": ["PasswordInput"]}, {"title": "SearchInput", "slug": "searchinput", "components": ["SearchInput"]}, {"title": "NumberInput", "slug": "numberinput", "components": ["NumberInput"]}, {"title": "ScrubInput", "slug": "scrubinput", "components": ["ScrubInput"]}, {"title": "CurrencyInput / PercentInput / UnitInput", "slug": "currencyinput-percentinput-unitinput", "components": ["CurrencyInput", "PercentInput", "UnitInput"]}, {"title": "EmailInput / UrlInput", "slug": "emailinput-urlinput", "components": ["EmailInput", "UrlInput"]}, {"title": "PhoneInput / MaskedInput", "slug": "phoneinput-maskedinput", "components": ["PhoneInput", "MaskedInput"]}, {"title": "PinInput / OTPInput", "slug": "pininput-otpinput", "components": ["PinInput", "OTPInput"]}, {"title": "InlineEdit / EditableText", "slug": "inlineedit-editabletext", "components": ["InlineEdit", "EditableText"]}, {"title": "InputGroup / InputAddon", "slug": "inputgroup-inputaddon", "components": ["InputGroup", "InputAddon"]}, {"title": "MentionInput / HashtagInput", "slug": "mentioninput-hashtaginput", "components": ["MentionInput", "HashtagInput"]}, {"title": "TagInput / TokenInput", "slug": "taginput-tokeninput", "components": ["TagInput", "TokenInput"]}, {"title": "KeyValueInput / ListInput", "slug": "keyvalueinput-listinput", "components": ["KeyValueInput", "ListInput"]}, {"title": "PathInput", "slug": "pathinput", "components": ["PathInput"]}, {"title": "RegexInput", "slug": "regexinput", "components": ["RegexInput"]}, {"title": "ExpressionInput", "slug": "expressioninput", "components": ["ExpressionInput"]}, {"title": "HotkeyInput / ShortcutRecorder", "slug": "hotkeyinput-shortcutrecorder", "components": ["HotkeyInput", "ShortcutRecorder"]}, {"title": "Checkbox / CheckboxGroup", "slug": "checkbox-checkboxgroup", "components": ["Checkbox", "CheckboxGroup"]}, {"title": "Radio / RadioGroup", "slug": "radio-radiogroup", "components": ["Radio", "RadioGroup"]}, {"title": "CheckboxCard / RadioCard", "slug": "checkboxcard-radiocard", "components": ["CheckboxCard", "RadioCard"]}, {"title": "Switch / Toggle", "slug": "switch-toggle", "components": ["Switch", "Toggle"]}, {"title": "Select / NativeSelect", "slug": "select-nativeselect", "components": ["Select", "NativeSelect"]}, {"title": "ListBox", "slug": "listbox", "components": ["ListBox"]}, {"title": "Combobox / Autocomplete", "slug": "combobox-autocomplete", "components": ["Combobox", "Autocomplete"]}, {"title": "MultiSelect", "slug": "multiselect", "components": ["MultiSelect"]}, {"title": "CascadeSelect / Cascader", "slug": "cascadeselect-cascader", "components": ["CascadeSelect", "Cascader"]}, {"title": "TransferList", "slug": "transferlist", "components": ["Transfer List"]}, {"title": "ChoiceChips / FilterChips", "slug": "choicechips-filterchips", "components": ["ChoiceChips", "FilterChips"]}, {"title": "Rating", "slug": "rating", "components": ["Rating"]}, {"title": "Slider / RangeSlider / VerticalSlider", "slug": "slider-rangeslider-verticalslider", "components": ["Slider", "RangeSlider", "VerticalSlider"]}, {"title": "Knob / Dial / Stepper", "slug": "knob-dial-stepper", "components": ["Knob", "Dial", "Stepper"]}, {"title": "Calendar", "slug": "calendar", "components": ["Calendar"]}, {"title": "DatePicker / DateRangePicker / DateTimePicker / TimePicker", "slug": "datepicker-daterangepicker-datetimepicker-timepicker", "components": ["DatePicker", "DateRangePicker", "DateTimePicker", "TimePicker"]}, {"title": "MonthPicker / YearPicker / QuarterPicker / WeekPicker", "slug": "monthpicker-yearpicker-quarterpicker-weekpicker", "components": ["MonthPicker", "YearPicker", "QuarterPicker", "WeekPicker"]}, {"title": "RelativeDatePicker / TimezoneSelect / DurationPicker", "slug": "relativedatepicker-timezoneselect-durationpicker", "components": ["RelativeDatePicker", "TimezoneSelect", "DurationPicker"]}, {"title": "CronEditor", "slug": "croneditor", "components": ["CronEditor"]}, {"title": "ColorPicker", "slug": "colorpicker", "components": ["ColorPicker"]}, {"title": "ColorSwatch / ColorPalette / EyeDropper", "slug": "colorswatch-colorpalette-eyedropper", "components": ["ColorSwatch", "ColorPalette", "EyeDropper"]}, {"title": "GradientEditor", "slug": "gradienteditor", "components": ["GradientEditor"]}, {"title": "FileInput / DropZone", "slug": "fileinput-dropzone", "components": ["FileInput", "DropZone"]}, {"title": "SignaturePad", "slug": "signaturepad", "components": ["SignaturePad"]}, {"title": "CodeInput / JsonInput", "slug": "codeinput-jsoninput", "components": ["CodeInput", "JsonInput"]}, {"title": "FontPicker", "slug": "fontpicker", "components": ["FontPicker"]}, {"title": "IconPicker / EmojiPicker", "slug": "iconpicker-emojipicker", "components": ["IconPicker", "EmojiPicker"]}, {"title": "LanguageSelect / CountrySelect / CurrencySelect", "slug": "languageselect-countryselect-currencyselect", "components": ["LanguageSelect", "CountrySelect", "CurrencySelect"]}, {"title": "Form / FormSection / FormField / FieldArray / DirtyIndicator", "slug": "form-formsection-formfield-fieldarray-dirtyindicator", "components": ["Form", "FormSection", "FormField", "FieldArray", "DirtyIndicator", "FormLabel", "FormError"]}, {"title": "InlineForm", "slug": "inlineform", "components": ["InlineForm"]}]}, {"number": 7, "slug": "navigation", "title": "Navigation", "summary": "Tabs, paths, places and palettes: where you are, and the ways out.", "stories": [{"title": "Tabs", "slug": "tabs", "components": ["Tabs"]}, {"title": "EditorTabs / TabOverflowMenu", "slug": "editortabs-taboverflowmenu", "components": ["EditorTabs", "TabOverflowMenu"]}, {"title": "Breadcrumb", "slug": "breadcrumb", "components": ["Breadcrumb"]}, {"title": "Pagination", "slug": "pagination", "components": ["Pagination"]}, {"title": "Steps / Wizard", "slug": "steps-wizard", "components": ["Steps", "Wizard"]}, {"title": "Sidebar Navigation / NavItem / NavGroup", "slug": "sidebar-navigation-navitem-navgroup", "components": ["Sidebar Navigation", "NavItem", "NavGroup"]}, {"title": "NavigationMenu", "slug": "navigationmenu", "components": ["NavigationMenu"]}, {"title": "BackForwardNavigation / History", "slug": "backforwardnavigation-history", "components": ["BackForwardNavigation"]}, {"title": "Anchor / TableOfContents", "slug": "anchor-tableofcontents", "components": ["Anchor", "TableOfContents"]}, {"title": "JumpTo / GoToLine", "slug": "jumpto-gotoline", "components": ["JumpTo", "GoToLine"]}, {"title": "CommandPalette", "slug": "commandpalette", "components": ["CommandPalette"]}, {"title": "QuickOpen", "slug": "quickopen", "components": ["QuickOpen"]}, {"title": "QuickSwitcher", "slug": "quickswitcher", "components": ["QuickSwitcher"]}, {"title": "SearchPalette / SpotlightSearch / QuickLauncher", "slug": "searchpalette-spotlightsearch-quicklauncher", "components": ["SearchPalette", "SpotlightSearch", "QuickLauncher"]}]}, {"number": 8, "slug": "menus", "title": "Menus", "summary": "Rows of actions that open from a button, a right click or an arrow, and nest.", "stories": [{"title": "Menu / MenuItem / MenuGroup / MenuSeparator / SubMenu / MenuItemShortcut / MenuItemIcon", "slug": "menu-menuitem-menugroup-menuseparator-submenu-menuitemshortcut-menuitemicon", "components": ["Menu", "MenuItem", "MenuGroup", "MenuSeparator", "SubMenu", "MenuItemShortcut", "MenuItemIcon"]}, {"title": "CheckboxMenuItem / RadioMenuItem", "slug": "checkboxmenuitem-radiomenuitem", "components": ["CheckboxMenuItem", "RadioMenuItem"]}, {"title": "DropdownMenu / SplitButton / OverflowMenu", "slug": "dropdownmenu-splitbutton-overflowmenu", "components": ["DropdownMenu", "SplitButton", "OverflowMenu"]}, {"title": "ContextMenu", "slug": "contextmenu", "components": ["ContextMenu"]}, {"title": "MenuBar Menu", "slug": "menubar-menu", "components": ["MenuBar Menu"]}, {"title": "MegaMenu", "slug": "megamenu", "components": ["MegaMenu"]}, {"title": "SearchableMenu", "slug": "searchablemenu", "components": ["SearchableMenu"]}, {"title": "PieMenu / RadialMenu", "slug": "piemenu-radialmenu", "components": ["PieMenu", "RadialMenu"]}]}, {"number": 9, "slug": "overlays", "title": "Overlays", "summary": "Panels over the page: anchored to a control, over a scrim, or over everything.", "stories": [{"title": "Popover / DropdownPanel / InlinePopup", "slug": "popover-dropdownpanel-inlinepopup", "components": ["Popover", "DropdownPanel", "InlinePopup"]}, {"title": "HoverCard", "slug": "hovercard", "components": ["HoverCard"]}, {"title": "Dialog / Modal / AlertDialog / ConfirmDialog / PromptDialog / FullscreenDialog", "slug": "dialog-modal-alertdialog-confirmdialog-promptdialog-fullscreendialog", "components": ["Dialog", "Modal", "AlertDialog", "ConfirmDialog", "PromptDialog", "FullscreenDialog"]}, {"title": "Lightbox", "slug": "lightbox", "components": ["Lightbox"]}, {"title": "Spotlight / Coachmark / Tour", "slug": "spotlight-coachmark-tour", "components": ["Spotlight", "Coachmark", "Tour"]}, {"title": "FloatingToolbar", "slug": "floatingtoolbar", "components": ["FloatingToolbar"]}, {"title": "Callout / Peek", "slug": "callout-peek", "components": ["Callout", "Peek"]}, {"title": "Tooltip / Sheet / Drawer / DropdownPanel / InlinePopup / ToastViewport", "slug": "tooltip-sheet-drawer-dropdownpanel-inlinepopup-toastviewport", "components": ["Tooltip", "Sheet", "Drawer", "DropdownPanel", "InlinePopup", "ToastViewport"]}]}, @@ -12,7 +12,7 @@ {"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": 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", "Sparkline"]}, {"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"]}]}, @@ -21,7 +21,7 @@ {"number": 20, "slug": "debug", "title": "Debug", "summary": "A paused program: its breakpoints, stack, threads and values, memory and machine code; profiles and network.", "stories": [{"title": "DebugToolbar", "slug": "debugtoolbar", "components": ["DebugToolbar"]}, {"title": "BreakpointList / BreakpointGutter / CallStack / ThreadList", "slug": "breakpointlist-breakpointgutter-callstack-threadlist", "components": ["BreakpointList", "BreakpointGutter", "CallStack", "ThreadList"]}, {"title": "VariablesPanel / WatchPanel / ConsoleREPL", "slug": "variablespanel-watchpanel-consolerepl", "components": ["VariablesPanel", "WatchPanel", "ConsoleREPL"]}, {"title": "MemoryViewer / HexViewer / Disassembly", "slug": "memoryviewer-hexviewer-disassembly", "components": ["MemoryViewer", "HexViewer", "Disassembly"]}, {"title": "PerformanceProfiler / Flamegraph / TimelineProfiler", "slug": "performanceprofiler-flamegraph-timelineprofiler", "components": ["PerformanceProfiler", "Flamegraph", "TimelineProfiler"]}, {"title": "NetworkInspector", "slug": "networkinspector", "components": ["NetworkInspector"]}]}, {"number": 21, "slug": "documents", "title": "Documents", "summary": "Markdown written and read, blocks like a notebook's, and a knowledge base around them.", "stories": [{"title": "RichTextEditor / FixedFormatToolbar / FloatingFormatToolbar / LinkEditor / MentionMenu / EmojiAutocomplete / WordCount / ReadingTime", "slug": "richtexteditor-fixedformattoolbar-floatingformattoolbar-linkeditor-mentionmenu-emojiautocomplete-wordcount-readingtime", "components": ["RichTextEditor", "FixedFormatToolbar", "FloatingFormatToolbar", "LinkEditor", "MentionMenu", "EmojiAutocomplete", "WordCount", "ReadingTime"]}, {"title": "MarkdownEditor", "slug": "markdowneditor", "components": ["MarkdownEditor"]}, {"title": "BlockEditor / BlockHandle / SlashMenu / PlaceholderText", "slug": "blockeditor-blockhandle-slashmenu-placeholdertext", "components": ["BlockEditor", "BlockHandle", "SlashMenu", "PlaceholderText"]}, {"title": "Block types / TableEditor / ImageEditorBlock / MathEditor / DiagramBlock / CodeBlockEditor", "slug": "block-types-tableeditor-imageeditorblock-matheditor-diagramblock-codeblockeditor", "components": ["Block types", "TableEditor", "ImageEditorBlock", "MathEditor", "DiagramBlock", "CodeBlockEditor"]}, {"title": "TodoItem / Checklist", "slug": "todoitem-checklist", "components": ["TodoItem", "Checklist"]}, {"title": "MarkdownRenderer / TableOfContents / DocumentOutline / ReadingProgress", "slug": "markdownrenderer-tableofcontents-documentoutline-readingprogress", "components": ["MarkdownRenderer", "TableOfContents", "DocumentOutline", "ReadingProgress"]}, {"title": "DocumentViewer / PDFViewer / PageThumbnailList", "slug": "documentviewer-pdfviewer-pagethumbnaillist", "components": ["DocumentViewer", "PDFViewer", "PageThumbnailList"]}, {"title": "OfficePreview", "slug": "officepreview", "components": ["OfficePreview"]}, {"title": "EpubReader", "slug": "epubreader", "components": ["EpubReader"]}, {"title": "Footnote / Glossary / TooltipTerm", "slug": "footnote-glossary-tooltipterm", "components": ["Footnote", "Glossary", "TooltipTerm"]}, {"title": "FocusMode / ZenMode", "slug": "focusmode-zenmode", "components": ["FocusMode", "ZenMode"]}, {"title": "PrintPreview", "slug": "printpreview", "components": ["PrintPreview"]}, {"title": "PageTree / Favorites / Pinned", "slug": "pagetree-favorites-pinned", "components": ["PageTree", "Favorites", "Pinned"]}, {"title": "PageCover / PageIcon / PageProperties", "slug": "pagecover-pageicon-pageproperties", "components": ["PageCover", "PageIcon", "PageProperties"]}, {"title": "Backlinks / GraphView", "slug": "backlinks-graphview", "components": ["Backlinks", "GraphView"]}, {"title": "TemplatePicker", "slug": "templatepicker", "components": ["TemplatePicker"]}, {"title": "TrashBin", "slug": "trashbin", "components": ["TrashBin"]}, {"title": "VersionHistory / PageHistoryDiff", "slug": "versionhistory-pagehistorydiff", "components": ["VersionHistory", "PageHistoryDiff"]}]}, {"number": 22, "slug": "collab", "title": "Collaboration", "summary": "People in the same document: who is here and where, comments and reactions, suggested changes, and sharing.", "stories": [{"title": "PresenceAvatars / LiveIndicator / FollowMode / RemoteCursor / RemoteSelection", "slug": "presenceavatars-liveindicator-followmode-remotecursor-remoteselection", "components": ["PresenceAvatars", "LiveIndicator", "FollowMode", "RemoteCursor", "RemoteSelection"]}, {"title": "CommentThread / Reply / Reactions / ReactionPicker", "slug": "commentthread-reply-reactions-reactionpicker", "components": ["CommentThread", "Reply", "Reactions", "ReactionPicker"]}, {"title": "CommentBubble / CommentMarker", "slug": "commentbubble-commentmarker", "components": ["CommentBubble", "CommentMarker"]}, {"title": "CommentSidebar", "slug": "commentsidebar", "components": ["CommentSidebar"]}, {"title": "Annotation / Highlight", "slug": "annotation-highlight", "components": ["Annotation", "Highlight"]}, {"title": "SuggestionMode / TrackChanges", "slug": "suggestionmode-trackchanges", "components": ["SuggestionMode", "TrackChanges"]}, {"title": "ShareDialog / InviteInput / PermissionSelect / AccessList", "slug": "sharedialog-inviteinput-permissionselect-accesslist", "components": ["ShareDialog", "InviteInput", "PermissionSelect", "AccessList"]}, {"title": "ActivityLog → data_display::ActivityFeed", "slug": "activitylog-data-display-activityfeed", "components": ["ActivityLog"]}]}, -{"number": 23, "slug": "chat", "title": "AI Chat", "summary": "A conversation with an assistant: messages that stream in, code and media inside them, and what can be done with each.", "stories": [{"title": "ChatContainer / MessageList / MessageBubble / MessageAvatar / MessageHeader / MessageFooter / DateSeparator / ScrollToBottomButton", "slug": "chatcontainer-messagelist-messagebubble-messageavatar-messageheader-messagefooter-dateseparator-scrolltobottombutton", "components": ["ChatContainer", "MessageList", "MessageBubble", "MessageAvatar", "MessageHeader", "MessageFooter", "DateSeparator", "ScrollToBottomButton"]}, {"title": "StreamingText / StreamingMarkdown / StreamingCursor", "slug": "streamingtext-streamingmarkdown-streamingcursor", "components": ["StreamingText", "StreamingMarkdown", "StreamingCursor"]}, {"title": "CodeBlock", "slug": "codeblock", "components": ["CodeBlock"]}, {"title": "MathBlock / TableBlock / MermaidRenderer", "slug": "mathblock-tableblock-mermaidrenderer", "components": ["MathBlock", "TableBlock", "MermaidRenderer"]}, {"title": "ImageMessage / ImageGrid / FileMessage / AudioMessage / VideoMessage / LinkPreviewCard", "slug": "imagemessage-imagegrid-filemessage-audiomessage-videomessage-linkpreviewcard", "components": ["ImageMessage", "ImageGrid", "FileMessage", "AudioMessage", "VideoMessage", "LinkPreviewCard"]}, {"title": "QuoteReply / MessageActions / MessageEditor / BranchNavigator / RegenerateButton / StopGeneratingButton / ContinueButton", "slug": "quotereply-messageactions-messageeditor-branchnavigator-regeneratebutton-stopgeneratingbutton-continuebutton", "components": ["QuoteReply", "MessageActions", "MessageEditor", "BranchNavigator", "RegenerateButton", "StopGeneratingButton", "ContinueButton"]}, {"title": "ErrorMessage / RetryMessage / RateLimitNotice / FeedbackForm", "slug": "errormessage-retrymessage-ratelimitnotice-feedbackform", "components": ["ErrorMessage", "RetryMessage", "RateLimitNotice", "FeedbackForm"]}, {"title": "ThinkingBlock / ReasoningPanel / ThinkingIndicator / ThinkingDuration", "slug": "thinkingblock-reasoningpanel-thinkingindicator-thinkingduration", "components": ["ThinkingBlock", "ReasoningPanel", "ThinkingIndicator", "ThinkingDuration"]}, {"title": "CitationBadge / InlineCitation / SourceCard / SourceList / SourcesPanel", "slug": "citationbadge-inlinecitation-sourcecard-sourcelist-sourcespanel", "components": ["CitationBadge", "InlineCitation", "SourceCard", "SourceList", "SourcesPanel"]}, {"title": "SearchProgress / WebResultCard / DocumentChunkPreview", "slug": "searchprogress-webresultcard-documentchunkpreview", "components": ["SearchProgress", "WebResultCard", "DocumentChunkPreview"]}, {"title": "PromptInput / Composer / AttachmentButton / AttachmentChip / AttachmentPreview / PasteImagePreview / DragDropOverlay / ContextChips / SlashCommandMenu / ContextMentionMenu / SendButton / InputHint", "slug": "promptinput-composer-attachmentbutton-attachmentchip-attachmentpreview-pasteimagepreview-dragdropoverlay-contextchips-slashcommandmenu-contextmentionmenu-sendbutton-inputhint", "components": ["PromptInput", "Composer", "AttachmentButton", "AttachmentChip", "AttachmentPreview", "PasteImagePreview", "DragDropOverlay", "ContextChips", "SlashCommandMenu", "ContextMentionMenu", "SendButton", "InputHint"]}, {"title": "ModelSelector / ModeSelector / ToolToggleMenu / PromptTemplateMenu", "slug": "modelselector-modeselector-tooltogglemenu-prompttemplatemenu", "components": ["ModelSelector", "ModeSelector", "ToolToggleMenu", "PromptTemplateMenu"]}, {"title": "TemperatureSlider / ParameterPanel / SystemPromptEditor / TokenCounter / ContextWindowMeter / CostEstimator", "slug": "temperatureslider-parameterpanel-systemprompteditor-tokencounter-contextwindowmeter-costestimator", "components": ["TemperatureSlider", "ParameterPanel", "SystemPromptEditor", "TokenCounter", "ContextWindowMeter", "CostEstimator"]}, {"title": "ConversationList / ConversationItem / ConversationSearch / NewChatButton", "slug": "conversationlist-conversationitem-conversationsearch-newchatbutton", "components": ["ConversationList", "ConversationItem", "ConversationSearch", "NewChatButton"]}, {"title": "FolderList / ProjectList / ProjectKnowledgePanel", "slug": "folderlist-projectlist-projectknowledgepanel", "components": ["FolderList", "ProjectList", "ProjectKnowledgePanel"]}, {"title": "SharedConversationView / ConversationExport", "slug": "sharedconversationview-conversationexport", "components": ["SharedConversationView", "ConversationExport"]}, {"title": "WelcomeScreen / Greeting / SuggestionChips / StarterPrompts / CapabilityCards", "slug": "welcomescreen-greeting-suggestionchips-starterprompts-capabilitycards", "components": ["WelcomeScreen", "Greeting", "SuggestionChips", "StarterPrompts", "CapabilityCards"]}, {"title": "FollowUpSuggestions", "slug": "followupsuggestions", "components": ["FollowUpSuggestions"]}, {"title": "PromptLibrary", "slug": "promptlibrary", "components": ["PromptLibrary"]}]}, +{"number": 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", "VoiceInputButton", "VoiceWaveform"]}, {"title": "ModelSelector / ModeSelector / ToolToggleMenu / PromptTemplateMenu", "slug": "modelselector-modeselector-tooltogglemenu-prompttemplatemenu", "components": ["ModelSelector", "ModeSelector", "ToolToggleMenu", "PromptTemplateMenu"]}, {"title": "TemperatureSlider / ParameterPanel / SystemPromptEditor / TokenCounter / ContextWindowMeter / CostEstimator", "slug": "temperatureslider-parameterpanel-systemprompteditor-tokencounter-contextwindowmeter-costestimator", "components": ["TemperatureSlider", "ParameterPanel", "SystemPromptEditor", "TokenCounter", "ContextWindowMeter", "CostEstimator"]}, {"title": "ConversationList / ConversationItem / ConversationSearch / NewChatButton", "slug": "conversationlist-conversationitem-conversationsearch-newchatbutton", "components": ["ConversationList", "ConversationItem", "ConversationSearch", "NewChatButton"]}, {"title": "FolderList / ProjectList / ProjectKnowledgePanel", "slug": "folderlist-projectlist-projectknowledgepanel", "components": ["FolderList", "ProjectList", "ProjectKnowledgePanel"]}, {"title": "SharedConversationView / ConversationExport", "slug": "sharedconversationview-conversationexport", "components": ["SharedConversationView", "ConversationExport"]}, {"title": "WelcomeScreen / Greeting / SuggestionChips / StarterPrompts / CapabilityCards", "slug": "welcomescreen-greeting-suggestionchips-starterprompts-capabilitycards", "components": ["WelcomeScreen", "Greeting", "SuggestionChips", "StarterPrompts", "CapabilityCards"]}, {"title": "FollowUpSuggestions", "slug": "followupsuggestions", "components": ["FollowUpSuggestions"]}, {"title": "PromptLibrary", "slug": "promptlibrary", "components": ["PromptLibrary"]}]}, {"number": 24, "slug": "agent", "title": "Agent", "summary": "An assistant at work: the tools it calls, what it asks before it acts, how far it has come, what it would change, what it sees and what it makes.", "stories": [{"title": "ToolCallCard / ToolCallGroup", "slug": "toolcallcard-toolcallgroup", "components": ["ToolCallCard", "ToolCallGroup"]}, {"title": "ToolApprovalDialog / PermissionPrompt", "slug": "toolapprovaldialog-permissionprompt", "components": ["ToolApprovalDialog", "PermissionPrompt"]}, {"title": "AgentStepList / TaskTimeline / AgentPlan / TodoList / AgentStatus / AgentProgress", "slug": "agentsteplist-tasktimeline-agentplan-todolist-agentstatus-agentprogress", "components": ["AgentStepList", "TaskTimeline", "AgentPlan", "TodoList", "AgentStatus", "AgentProgress"]}, {"title": "SubAgentTree", "slug": "subagenttree", "components": ["SubAgentTree"]}, {"title": "FileChangeCard / MultiFileDiff Review / CommandExecutionCard", "slug": "filechangecard-multifilediff-review-commandexecutioncard", "components": ["FileChangeCard", "MultiFileDiff Review", "CommandExecutionCard"]}, {"title": "BrowserPreview / ScreenshotStream / ComputerUseViewer", "slug": "browserpreview-screenshotstream-computeruseviewer", "components": ["BrowserPreview", "ScreenshotStream", "ComputerUseViewer"]}, {"title": "ArtifactPanel / ArtifactVersionSwitcher / LivePreview", "slug": "artifactpanel-artifactversionswitcher-livepreview", "components": ["ArtifactPanel", "ArtifactVersionSwitcher", "LivePreview"]}, {"title": "SandboxStatus", "slug": "sandboxstatus", "components": ["SandboxStatus"]}, {"title": "MCPServerList / ConnectorList / ToolRegistry Panel", "slug": "mcpserverlist-connectorlist-toolregistry-panel", "components": ["MCPServerList", "ConnectorList", "ToolRegistry Panel"]}, {"title": "MemoryPanel / CheckpointList / Rewind", "slug": "memorypanel-checkpointlist-rewind", "components": ["MemoryPanel", "CheckpointList", "Rewind"]}, {"title": "InterruptButton / HumanInputRequest", "slug": "interruptbutton-humaninputrequest", "components": ["InterruptButton", "HumanInputRequest"]}, {"title": "TokenUsageChart / CostBreakdown", "slug": "tokenusagechart-costbreakdown", "components": ["TokenUsageChart", "CostBreakdown"]}, {"title": "TraceViewer / EvalResultTable", "slug": "traceviewer-evalresulttable", "components": ["TraceViewer", "EvalResultTable"]}]}, {"number": 25, "slug": "generative", "title": "Generative", "summary": "Making pictures, sound and video with a model: the prompt and its settings, the queue and what comes back, the canvas, the models behind them, and the data and prompts they work from.", "stories": [{"title": "ImageGenerationPanel / PromptEnhancer / NegativePromptInput / StylePresetPicker / AspectRatioPicker / SeedInput", "slug": "imagegenerationpanel-promptenhancer-negativepromptinput-stylepresetpicker-aspectratiopicker-seedinput", "components": ["ImageGenerationPanel", "PromptEnhancer", "NegativePromptInput", "StylePresetPicker", "AspectRatioPicker", "SeedInput"]}, {"title": "GenerationQueue", "slug": "generationqueue", "components": ["GenerationQueue"]}, {"title": "GenerationGrid / ImageCompare", "slug": "generationgrid-imagecompare", "components": ["GenerationGrid", "ImageCompare"]}, {"title": "VariationPicker / ABCompareView", "slug": "variationpicker-abcompareview", "components": ["VariationPicker", "ABCompareView"]}, {"title": "InpaintCanvas / MaskBrush", "slug": "inpaintcanvas-maskbrush", "components": ["InpaintCanvas", "MaskBrush"]}, {"title": "AudioGenerationPlayer", "slug": "audiogenerationplayer", "components": ["AudioGenerationPlayer"]}, {"title": "TTSVoicePicker", "slug": "ttsvoicepicker", "components": ["TTSVoicePicker"]}, {"title": "VideoGenerationTimeline", "slug": "videogenerationtimeline", "components": ["VideoGenerationTimeline"]}, {"title": "ModelCard", "slug": "modelcard", "components": ["ModelCard"]}, {"title": "ModelDownloadManager", "slug": "modeldownloadmanager", "components": ["ModelDownloadManager"]}, {"title": "ModelStatus / HardwareMonitor", "slug": "modelstatus-hardwaremonitor", "components": ["ModelStatus", "HardwareMonitor"]}, {"title": "FineTuneJobCard", "slug": "finetunejobcard", "components": ["FineTuneJobCard"]}, {"title": "DatasetViewer → tables::DataTable", "slug": "datasetviewer-tables-datatable", "components": ["DatasetViewer"]}, {"title": "EmbeddingVisualizer", "slug": "embeddingvisualizer", "components": ["EmbeddingVisualizer"]}, {"title": "PromptPlayground", "slug": "promptplayground", "components": ["PromptPlayground"]}, {"title": "PromptDiff / PromptVersionHistory", "slug": "promptdiff-promptversionhistory", "components": ["PromptDiff", "PromptVersionHistory"]}]}, {"number": 26, "slug": "media", "title": "Media", "summary": "Pictures, video and sound: looking closely, cropping and marking up, playing, and the controls and devices around them.", "stories": [{"title": "ImageViewer", "slug": "imageviewer", "components": ["ImageViewer"]}, {"title": "ImageThumbnail / Lightbox → overlays::Lightbox", "slug": "imagethumbnail-lightbox-overlays-lightbox", "components": ["ImageThumbnail", "Lightbox"]}, {"title": "ImageCropper", "slug": "imagecropper", "components": ["ImageCropper"]}, {"title": "ImageUpload", "slug": "imageupload", "components": ["ImageUpload"]}, {"title": "ImageAnnotator", "slug": "imageannotator", "components": ["ImageAnnotator"]}, {"title": "VideoPlayer", "slug": "videoplayer", "components": ["VideoPlayer"]}, {"title": "VideoTimeline / Scrubber", "slug": "videotimeline-scrubber", "components": ["VideoTimeline", "Scrubber"]}, {"title": "VideoThumbnailStrip", "slug": "videothumbnailstrip", "components": ["VideoThumbnailStrip"]}, {"title": "SubtitleEditor", "slug": "subtitleeditor", "components": ["SubtitleEditor"]}, {"title": "AudioPlayer", "slug": "audioplayer", "components": ["AudioPlayer"]}, {"title": "AudioWaveform", "slug": "audiowaveform", "components": ["AudioWaveform"]}, {"title": "AudioSpectrum / Visualizer", "slug": "audiospectrum-visualizer", "components": ["AudioSpectrum", "Visualizer"]}, {"title": "VolumeControl", "slug": "volumecontrol", "components": ["VolumeControl"]}, {"title": "PlaybackSpeedControl", "slug": "playbackspeedcontrol", "components": ["PlaybackSpeedControl"]}, {"title": "MediaControls", "slug": "mediacontrols", "components": ["MediaControls"]}, {"title": "Playlist", "slug": "playlist", "components": ["Playlist"]}, {"title": "ScreenRecorderControls", "slug": "screenrecordercontrols", "components": ["ScreenRecorderControls"]}, {"title": "CameraPreview", "slug": "camerapreview", "components": ["CameraPreview"]}, {"title": "MicLevelMeter", "slug": "miclevelmeter", "components": ["MicLevelMeter"]}, {"title": "DeviceSelector", "slug": "deviceselector", "components": ["DeviceSelector"]}, {"title": "3DModelViewer", "slug": "3dmodelviewer", "components": ["3DModelViewer"]}]}, @@ -33,11 +33,11 @@ {"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": 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", "ThemeSelector", "AccentColorPicker", "FontSizeControl", "DensitySelector", "KeyboardShortcutsList", "ProxySettings", "PrivacySettings", "NotificationSettings", "StartupSettings", "StorageSettings", "ResetToDefault", "ImportExportSettings", "AdvancedSettings", "DeveloperMode Toggle", "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": 39, "slug": "theme", "title": "Theme", "summary": "Tokens every component reads. Light and dark share one grammar.", "stories": [{"title": "Surfaces", "slug": "surfaces", "components": ["ColorTokens"]}, {"title": "Ink", "slug": "ink", "components": ["ColorTokens"]}, {"title": "Signals", "slug": "signals", "components": ["ColorTokens"]}, {"title": "Series", "slug": "series", "components": ["ColorTokens"]}, {"title": "Syntax", "slug": "syntax", "components": ["ColorTokens"]}, {"title": "Type", "slug": "type", "components": []}, {"title": "Shape", "slug": "shape", "components": ["Elevation"]}, {"title": "Motion", "slug": "motion", "components": ["Motion", "Spring"]}, {"title": "ThemeEditor · DensityProvider · RadiusProvider · FontProvider · HighContrastMode · ReducedMotionProvider", "slug": "themeeditor-densityprovider-radiusprovider-fontprovider-highcontrastmode-reducedmotionprovider", "components": ["ThemeEditor", "ThemeProvider", "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"]}]}, diff --git a/examples/gallery/ui.rs b/examples/gallery/ui.rs index 4aa8571..5bf571e 100644 --- a/examples/gallery/ui.rs +++ b/examples/gallery/ui.rs @@ -55,7 +55,7 @@ pub fn slug(title: &str) -> 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 { +pub fn section(title: impl Into<SharedString>, note: impl Into<SharedString>, cx: &App) -> Section { let title = title.into(); let theme = cx.theme(); let story = cx.try_global::<Story>(); @@ -63,33 +63,58 @@ pub fn section(title: impl Into<SharedString>, note: impl Into<SharedString>, cx 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( + Section { + div: div() + .flex() + .flex_col() + .gap_4() + .when(story.is_none(), |section| section.pt_10()) + .when(!shown, |section| section.hidden()), + shown, + } + .child( + div() + .flex() + .flex_col() + .gap_1() + .when(story.is_none(), |header| { + header.child( div() - .text_size(theme.text_size(TextSize::Sm)) - .text_color(theme.colors.fg_muted) - .child(note.into()), - ), - ) + .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 section's box. One a story passes over drops its demos, so none lays out, loads or moves. +pub struct Section { + div: Div, + shown: bool, +} + +impl ParentElement for Section { + fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { + if self.shown { + self.div.extend(elements); + } + } +} + +impl IntoElement for Section { + type Element = Div; + + fn into_element(self) -> Div { + self.div + } } /// A picture under the gallery's assets: a file natively, an address on the web. @@ -163,6 +188,13 @@ pub fn blocked(text: impl Into<SharedString>, cx: &App) -> impl IntoElement { .child(div().flex_1().min_w_0().child(text.into())) } +/// Why no screen reader reads a demo here. +pub const UNREAD: &str = if cfg!(target_family = "wasm") { + "gpui builds no AccessKit tree on the web, so no screen reader reads it here" +} else { + "gpui builds an AccessKit tree from elements with an id and a role, and no screen reader here reads it" +}; + /// Why a demo that takes files takes none on the web. pub const NO_FILES: &str = "A browser hands gpui no files: on the web the file dialog fails and dropped files never arrive, so nothing is taken here."; diff --git a/examples/gallery/web.rs b/examples/gallery/web.rs index 47998cb..948dabb 100644 --- a/examples/gallery/web.rs +++ b/examples/gallery/web.rs @@ -1,4 +1,9 @@ -use std::{borrow::Cow, cell::RefCell, rc::Rc, sync::Arc}; +use std::{ + borrow::Cow, + cell::{Cell, RefCell}, + rc::Rc, + sync::Arc, +}; use anyhow::{Context as _, Result, anyhow, bail}; use ely_gpui_component::Assets; @@ -18,6 +23,8 @@ const HEBREW: &[u8] = include_bytes!("fonts/NotoSansHebrew-Regular.ttf"); 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) }; + /// A mode the host page chose before the window opened, which it opens in. + static EARLY: Cell<Option<Choice>> = const { Cell::new(None) }; } fn js(error: JsValue) -> anyhow::Error { @@ -74,7 +81,7 @@ pub fn start() { .location() .search() .expect("the page's address"); - let start = read(&search).unwrap_or_else(|error| { + let mut start = read(&search).unwrap_or_else(|error| { log::error!("gallery: {search} -> {error:#}"); Start { failure: Some(SharedString::from(format!("{search}: {error:#}"))), @@ -95,21 +102,24 @@ pub fn start() { cx.text_system() .add_fonts(vec![Cow::Borrowed(HEBREW)]) .expect("the gallery's Hebrew face registers"); + if let Some(choice) = EARLY.take() { + start.choice = choice; + } 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`. +/// Switches Ely's mode from the host page: `light` or `dark`, kept until the window opens. #[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(()) - }) + HOST.with(|host| match host.borrow().as_ref() { + Some((app, window)) => app.update(|cx| choose(*window, choice, cx)), + None => { + log::info!("gallery: theme {theme} waits for the window"); + EARLY.set(Some(choice)); + } + }); + Ok(()) } diff --git a/scripts/check.sh b/scripts/check.sh index fbfa4da..6e092bc 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -4,6 +4,13 @@ set -eu cargo fmt --check cargo clippy --all-targets --features test-support -- -D warnings cargo test --lib --features test-support --quiet +rustup target list --installed | grep -qx wasm32-unknown-unknown || { + echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2 + exit 1 +} +# gpui_web's wasm_thread asks for an unstable feature, as in scripts/web.sh. +RUSTC_BOOTSTRAP=1 ELY_GALLERY_ASSETS=https://example.invalid/ \ + cargo clippy --example gallery --target wasm32-unknown-unknown -- -D warnings python3 scripts/stories.py --check fail=0 files=$(git ls-files -co --exclude-standard '*.rs') diff --git a/scripts/stories.py b/scripts/stories.py index 9c176a0..87e887b 100644 --- a/scripts/stories.py +++ b/scripts/stories.py @@ -1,9 +1,4 @@ -"""Writes examples/gallery/stories.json: every gallery page and its sections, in the order they draw. - -A section is a story the web gallery shows alone at ?page=<page slug>&story=<story slug>. Its -components are the names in its title that are entries in tasks/*.md, so "Sizes" names none. -`--check` fails if the committed file differs. -""" +"""Writes examples/gallery/stories.json: each page's stories and the components they show.""" import json import re @@ -13,10 +8,34 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PAGES = ROOT / "examples/gallery/pages" OUT = ROOT / "examples/gallery/stories.json" +CALL = re.compile(r"(?<![\w.])(?:(\w+)::)?(\w+)\s*\(") +CHAIN = re.compile(r"\s*\.\s*\w+\s*(?:::<[^>]*>\s*)?\(") + +# Components a story draws beyond its title's, by page and the title's first name. +SHOWN = { + ("forms", "Form"): ["FormLabel", "FormError"], + ("tables", "DataTable"): ["Sparkline"], + ("chat", "PromptInput"): ["VoiceInputButton", "VoiceWaveform"], + ("shell", "MiniWindow"): ["AlwaysOnTop"], + ("settings", "SettingsLayout"): [ + "ThemeSelector", "AccentColorPicker", "FontSizeControl", "DensitySelector", + "KeyboardShortcutsList", "ProxySettings", "PrivacySettings", "NotificationSettings", + "StartupSettings", "StorageSettings", "ResetToDefault", "ImportExportSettings", + "AdvancedSettings", "DeveloperMode Toggle", + ], + ("theme", "Surfaces"): ["ColorTokens"], + ("theme", "Ink"): ["ColorTokens"], + ("theme", "Signals"): ["ColorTokens"], + ("theme", "Series"): ["ColorTokens"], + ("theme", "Syntax"): ["ColorTokens"], + ("theme", "Shape"): ["Elevation"], + ("theme", "Motion"): ["Spring"], + ("theme", "ThemeEditor"): ["ThemeProvider"], +} def strip(text): - """Blanks comments and literals' insides, keeping offsets, so braces and calls parse.""" + """Blanks comments and literals' insides, keeping every offset.""" out, i, n = list(text), 0, len(text) def blank(a, b): @@ -60,7 +79,7 @@ def literal(text, at): def functions(folder): - """Every `fn` in a page's folder: (module, name) -> (source, stripped, body start, body end).""" + """Every `fn` in a page's folder, by module and name, with its body's span.""" found = {} for file in sorted(folder.rglob("*.rs")): module = file.stem @@ -80,26 +99,52 @@ def functions(folder): return found +def close(bare, at): + """The offset past the bracket that closes the one at `at`.""" + depth = 0 + for k in range(at, len(bare)): + depth += {"(": 1, "[": 1, "{": 1, ")": -1, "]": -1, "}": -1}.get(bare[k], 0) + if depth == 0: + return k + 1 + + def sections(folder): - """Section titles in the order the page's render reaches them; fails on any it never reaches.""" + """Section titles in drawing order; fails on one never drawn or drawn inside another.""" fns = functions(folder) - order, seen = [], set() + order, seen, opens = [], set(), {} + + def calls(module, name): + text, bare, start, end = fns[(module, name)] + for m in CALL.finditer(bare, start, end): + owner = m.group(1) or module + yield m, (owner, m.group(2)), m.group(2) == "section" and m.group(1) is None + + def draws(fn): + """Whether the fn opens a section, itself or through what it calls.""" + if fn not in opens: + opens[fn] = False + opens[fn] = any(own or (callee in fns and draws(callee)) for _, callee, own in calls(*fn)) + return opens[fn] def walk(module, name): if (module, name) in seen: return seen.add((module, name)) - text, bare, start, end = fns[(module, name)] - call = re.compile(r"(?<![\w.])(?:(\w+)::)?(\w+)\s*\(") - for m in call.finditer(bare, start, end): - owner, callee = m.group(1) or module, m.group(2) - if callee == "section" and m.group(1) is None: + text, bare, _, _ = fns[(module, name)] + inside = 0 + for m, callee, own in calls(module, name): + if m.start() < inside and (own or (callee in fns and draws(callee))): + sys.exit(f"{folder.name}/{module}.rs: a section opens inside {order[-1][0]!r}") + if own: at = m.end() + len(bare[m.end():]) - len(bare[m.end():].lstrip()) if text[at] != '"': sys.exit(f"{folder.name}/{module}.rs: a section whose title is not a literal") order.append((literal(text, at), module)) - elif (owner, callee) in fns: - walk(owner, callee) + inside = close(bare, m.end() - 1) + while chained := CHAIN.match(bare, inside): + inside = close(bare, chained.end() - 1) + elif callee in fns: + walk(*callee) walk("mod", "render") reached = {title for title, _ in order} @@ -112,31 +157,42 @@ def sections(folder): def slug(title): - """As the gallery's ui::slug: ASCII letters and digits, lowercased, dashes between.""" + """As the gallery's ui::slug: lowercase letters and digits, dashes between.""" out = re.sub(r"[^a-z0-9]+", "-", "".join(c.lower() if c.isascii() else " " for c in title)) return out.strip("-") def entries(): - """Names the task lists give components, keyed by letters alone.""" - names = {} + """The task lists' component names by key, and the ticked entries with a home.""" + names, homed = {}, [] for file in sorted((ROOT / "tasks").glob("*.md")): for line in file.read_text().splitlines(): - if m := re.match(r"- \[[ x]\] (.+?)(?: — | → | \(T\d|$)", line): - for name in m.group(1).split(" / "): - names[key(name)] = re.sub(r"\s*\([^)]*\)", "", name).strip() - return names + if m := re.match(r"- \[([ x])\] (.+?)(?: — | → | \(T\d|$)", line): + aliases = [re.sub(r"\s*\([^)]*\)", "", name).strip() for name in m.group(2).split(" / ")] + for name in aliases: + names[key(name)] = name + if m.group(1) == "x" and home(line, aliases): + homed.append(aliases) + return names, homed + + +def home(line, aliases): + """False for a pointer: `A → b::B` lives at B, another entry.""" + if " → " not in line: + return True + target = re.search(r" → `([\w:]+)", line) + return bool(target) and any(key(part) in map(key, aliases) for part in target.group(1).split("::")) def key(name): return re.sub(r"[^a-z0-9]", "", re.sub(r"\([^)]*\)", "", name).lower()) -def components(title, known): - """The task lists' names for what the title names: "Icon button" is IconButton.""" +def components(title, extra, known): + """The task lists' names for what the title and `extra` name.""" names = [] for part in re.sub(r"\s*\([^)]*\)", "", title).split(" · "): - for name in part.split(" → ")[0].split(" / "): + for name in part.split(" → ")[0].split(" / ") + extra: if key(name) in known and known[key(name)] not in names: names.append(known[key(name)]) return names @@ -145,28 +201,40 @@ def components(title, known): def pages(): listing = (PAGES / "mod.rs").read_text() order = re.findall(r"^\s+(\w+)::PAGE,$", listing, re.M) - known = entries() - out = [] + known, homed = entries() + for (page, lead), extra in SHOWN.items(): + for name in extra: + if key(name) not in known: + sys.exit(f"stories.py: {name} in {page}/{lead} is no entry in tasks/*.md") + out, shown = [], dict(SHOWN) for module in order: folder = PAGES / module head = (folder / "mod.rs").read_text() field = lambda name: re.search(rf"^\s+{name}: (.+),$", head, re.M).group(1) + page = json.loads(field("slug")) titles = sections(folder) slugs = [slug(title) for title in titles] for twice in {s for s in slugs if slugs.count(s) > 1}: sys.exit(f"{module}: two stories answer to {twice}") + stories = [] + for t, s in zip(titles, slugs): + extra = shown.pop((page, re.split(r" · | / ", t)[0]), []) + stories.append({"title": t, "slug": s, "components": components(t, extra, known)}) out.append( { "number": int(field("number")), - "slug": json.loads(field("slug")), + "slug": page, "title": json.loads(field("title")), "summary": json.loads(field("summary")), - "stories": [ - {"title": t, "slug": s, "components": components(t, known)} - for t, s in zip(titles, slugs) - ], + "stories": stories, } ) + if shown: + sys.exit(f"stories.py: SHOWN names no story for {sorted(shown)}") + named = {key(name) for page in out for story in page["stories"] for name in story["components"]} + for aliases in homed: + if not any(key(name) in named for name in aliases): + sys.exit(f"no story shows {' / '.join(aliases)}: name it in a title or in SHOWN") return out diff --git a/src/editor/edit.rs b/src/editor/edit.rs index 505e646..3af4735 100644 --- a/src/editor/edit.rs +++ b/src/editor/edit.rs @@ -382,13 +382,10 @@ impl CodeEditor { } /// Pastes one line at each cursor when the counts match, else the whole text at every one. - pub(crate) fn paste(&mut self, cx: &mut Context<Self>) { + pub(crate) fn paste(&mut self, text: &str, cx: &mut Context<Self>) { if !self.editable() { return; } - let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { - return; - }; let lines: Vec<&str> = text.split('\n').collect(); let edits = if lines.len() == self.selections.len() && lines.len() > 1 { self.in_order() @@ -399,7 +396,7 @@ impl CodeEditor { } else { self.selections .iter() - .map(|selection| (selection.range(), text.clone())) + .map(|selection| (selection.range(), text.to_string())) .collect() }; self.apply(edits, false, cx); diff --git a/src/editor/ime.rs b/src/editor/ime.rs index 4ea404f..020d855 100644 --- a/src/editor/ime.rs +++ b/src/editor/ime.rs @@ -1,8 +1,8 @@ use std::ops::Range; use gpui::{ - App, Bounds, Context, EntityInputHandler, FontStyle, HighlightStyle, Pixels, Point, - UTF16Selection, Window, point, + App, Bounds, ClipboardItem, Context, EntityInputHandler, FontStyle, HighlightStyle, Pixels, + Point, UTF16Selection, Window, point, }; use super::{ @@ -107,6 +107,12 @@ impl CodeEditor { } impl EntityInputHandler for CodeEditor { + fn paste(&mut self, item: ClipboardItem, _: &mut Window, cx: &mut Context<Self>) { + if let Some(text) = item.text() { + self.paste(&text, cx); + } + } + fn text_for_range( &mut self, range_utf16: Range<usize>, diff --git a/src/editor/keys.rs b/src/editor/keys.rs index d6c15a4..4eba029 100644 --- a/src/editor/keys.rs +++ b/src/editor/keys.rs @@ -91,13 +91,16 @@ pub(crate) fn bind_keys(cx: &mut App) { KeyBinding::new("secondary-a", SelectAll, context), KeyBinding::new("secondary-c", Copy, context), KeyBinding::new("secondary-x", Cut, context), - KeyBinding::new("secondary-v", Paste, context), KeyBinding::new("secondary-d", SelectNext, context), KeyBinding::new("escape", SingleCursor, context), KeyBinding::new("secondary-/", ToggleComment, context), KeyBinding::new("secondary-alt-[", Fold, context), KeyBinding::new("secondary-alt-]", Unfold, context), ]; + // The browser's paste event carries the clipboard to the input handler. + if !cfg!(target_family = "wasm") { + bindings.push(KeyBinding::new("secondary-v", Paste, context)); + } if cfg!(target_os = "macos") { bindings.extend([ KeyBinding::new("cmd-left", LineStart, context), @@ -261,7 +264,11 @@ pub(crate) fn listen<E: InteractiveElement>(root: E, cx: &mut Context<CodeEditor let root = on!(root, SelectAll, |e, _, cx| e.select_all(cx)); let root = on!(root, Copy, |e, _, cx| e.copy(cx)); let root = on!(root, Cut, |e, _, cx| e.cut(cx)); - let root = on!(root, Paste, |e, _, cx| e.paste(cx)); + let root = on!(root, Paste, |e, _, cx| { + if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { + e.paste(&text, cx) + } + }); let root = on!(root, AddCursorAbove, |e, _, cx| e.add_cursor(false, cx)); let root = on!(root, AddCursorBelow, |e, _, cx| e.add_cursor(true, cx)); let root = on!(root, SelectNext, |e, _, cx| e.select_next(cx)); diff --git a/src/editor/tests.rs b/src/editor/tests.rs index 091c627..6e712cd 100644 --- a/src/editor/tests.rs +++ b/src/editor/tests.rs @@ -1,6 +1,9 @@ use std::time::Duration; -use gpui::{Entity, EntityInputHandler, KeyUpEvent, Keystroke, TestAppContext, VisualTestContext}; +use gpui::{ + ClipboardItem, Entity, EntityInputHandler, KeyUpEvent, Keystroke, TestAppContext, + VisualTestContext, +}; use super::{CodeEditor, GhostText, InlayHint, LineNumbers, layout::Row}; use crate::theme::Theme; @@ -240,3 +243,16 @@ impl CodeEditor { super::layout::rows(&self.buffer, &hide, &self.marks).contains(&Row::Line(line)) } } + +#[gpui::test] +fn a_browser_paste_gives_each_cursor_its_line(cx: &mut TestAppContext) { + let (editor, cx) = editor("a\nb", cx); + cx.update(|window, cx| { + editor.update(cx, |editor, cx| { + let (first, second) = (1..1, 3..3); + editor.select([first, second], cx); + EntityInputHandler::paste(editor, ClipboardItem::new_string("1\n2".into()), window, cx); + assert_eq!(editor.text(), "a1\nb2"); + }) + }); +} diff --git a/src/forms/tests/mod.rs b/src/forms/tests/mod.rs index 279c09b..eab494d 100644 --- a/src/forms/tests/mod.rs +++ b/src/forms/tests/mod.rs @@ -7,8 +7,8 @@ mod tags; mod upload; use gpui::{ - AppContext as _, Context, Entity, EntityInputHandler, IntoElement, KeyUpEvent, Keystroke, - ParentElement, Render, TestAppContext, VisualTestContext, Window, div, px, + AppContext as _, ClipboardItem, Context, Entity, EntityInputHandler, IntoElement, KeyUpEvent, + Keystroke, ParentElement, Render, TestAppContext, VisualTestContext, Window, div, px, }; use super::{Input, MaskedInput, NumberInput, PinInput, TextInput, bind_keys}; @@ -146,6 +146,22 @@ fn a_committed_composition_obeys_the_filter_and_length(cx: &mut TestAppContext) assert_eq!(text(field, cx), ""); } +#[gpui::test] +fn a_browser_paste_undoes_apart_from_the_typing(cx: &mut TestAppContext) { + let (fields, cx) = open(1, false, cx); + let field = &fields[0]; + focus(field, cx); + cx.simulate_input("ab"); + cx.update(|window, cx| { + field.update(cx, |input, cx| { + EntityInputHandler::paste(input, ClipboardItem::new_string("cd".into()), window, cx) + }) + }); + assert_eq!(text(field, cx), "abcd"); + cx.simulate_keystrokes("cmd-z"); + assert_eq!(text(field, cx), "ab"); +} + #[gpui::test] fn undo_leaves_a_disabled_field_alone(cx: &mut TestAppContext) { let (fields, cx) = open(1, false, cx); diff --git a/src/forms/text/actions.rs b/src/forms/text/actions.rs index b183369..91b94df 100644 --- a/src/forms/text/actions.rs +++ b/src/forms/text/actions.rs @@ -82,7 +82,6 @@ pub(crate) fn bind_keys(cx: &mut App) { KeyBinding::new("secondary-a", SelectAll, Some(CONTEXT)), KeyBinding::new("secondary-c", Copy, Some(CONTEXT)), KeyBinding::new("secondary-x", Cut, Some(CONTEXT)), - KeyBinding::new("secondary-v", Paste, Some(CONTEXT)), KeyBinding::new("secondary-z", Undo, Some(CONTEXT)), KeyBinding::new("secondary-shift-z", Redo, Some(CONTEXT)), KeyBinding::new("enter", Enter, Some(CONTEXT)), @@ -90,6 +89,10 @@ pub(crate) fn bind_keys(cx: &mut App) { KeyBinding::new("secondary-enter", Submit, Some(CONTEXT)), KeyBinding::new("secondary-enter", Submit, Some(FORM_CONTEXT)), ]; + // The browser's paste event carries the clipboard to the input handler. + if !cfg!(target_family = "wasm") { + bindings.push(KeyBinding::new("secondary-v", Paste, Some(CONTEXT))); + } if cfg!(target_os = "macos") { bindings.extend([ KeyBinding::new("cmd-left", Home, Some(CONTEXT)), diff --git a/src/forms/text/ime.rs b/src/forms/text/ime.rs index dcd5dcf..e40df66 100644 --- a/src/forms/text/ime.rs +++ b/src/forms/text/ime.rs @@ -1,6 +1,8 @@ use std::ops::Range; -use gpui::{Bounds, Context, EntityInputHandler, Pixels, Point, UTF16Selection, Window}; +use gpui::{ + Bounds, ClipboardItem, Context, EntityInputHandler, Pixels, Point, UTF16Selection, Window, +}; use super::{ TextInput, @@ -18,6 +20,12 @@ impl TextInput { } impl EntityInputHandler for TextInput { + fn paste(&mut self, item: ClipboardItem, _: &mut Window, cx: &mut Context<Self>) { + if let Some(text) = item.text() { + self.replace(self.selection.clone(), &text, false, cx); + } + } + fn text_for_range( &mut self, range_utf16: Range<usize>, diff --git a/src/tables/gridkeys.rs b/src/tables/gridkeys.rs index 9a9aa2c..60c1642 100644 --- a/src/tables/gridkeys.rs +++ b/src/tables/gridkeys.rs @@ -284,6 +284,7 @@ pub(crate) fn keys( ))), ("v", true) => { let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + log::info!("grid: the clipboard holds no text to paste"); return; }; let edits = pasted(&text, (row, col), (rows, cols));