component: Add `component` and `component_preview` crates to power UI components (#24456)

Nate Butler and Marshall Bowers created

This PR formalizes design components with the Component and
ComponentPreview traits.

You can open the preview UI with `workspace: open component preview`.

Component previews no longer need to return `Self` allowing for more
complex previews, and previews of components like `ui::Tooltip` that
supplement other components rather than are rendered by default.

`cargo-machete` incorrectly identifies `linkme` as an unused dep on
crates that have components deriving `IntoComponent`, so you may need to
add this to that crate's `Cargo.toml`:

```toml
# cargo-machete doesn't understand that linkme is used in the component macro
[package.metadata.cargo-machete]
ignored = ["linkme"]
```

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>

Change summary

Cargo.lock                                        |  51 +
Cargo.toml                                        |   5 
crates/component/Cargo.toml                       |  23 
crates/component/LICENSE-GPL                      |   1 
crates/component/src/component.rs                 | 305 +++++++++++
crates/component_preview/Cargo.toml               |  21 
crates/component_preview/LICENSE-GPL              |   1 
crates/component_preview/src/component_preview.rs | 178 ++++++
crates/ui/Cargo.toml                              |   6 
crates/ui/src/components/avatar/avatar.rs         |  61 ++
crates/ui/src/components/button/button.rs         | 221 ++++---
crates/ui/src/components/content_group.rs         |  27 
crates/ui/src/components/facepile.rs              | 112 ++--
crates/ui/src/components/icon.rs                  |  65 +
crates/ui/src/components/icon/decorated_icon.rs   |  59 +-
crates/ui/src/components/icon/icon_decoration.rs  |  22 
crates/ui/src/components/indicator.rs             |  31 -
crates/ui/src/components/keybinding_hint.rs       | 187 +++---
crates/ui/src/components/label/label.rs           |  54 +
crates/ui/src/components/radio.rs                 |   3 
crates/ui/src/components/tab.rs                   |  47 +
crates/ui/src/components/table.rs                 | 186 +++---
crates/ui/src/components/toggle.rs                | 443 ++++++----------
crates/ui/src/components/tooltip.rs               |  15 
crates/ui/src/prelude.rs                          |   4 
crates/ui/src/styles/typography.rs                |  47 +
crates/ui/src/traits.rs                           |   1 
crates/ui/src/traits/component_preview.rs         | 205 -------
crates/ui_macros/Cargo.toml                       |   3 
crates/ui_macros/src/derive_component.rs          |  97 +++
crates/ui_macros/src/ui_macros.rs                 |  25 
crates/workspace/Cargo.toml                       |   1 
crates/workspace/src/theme_preview.rs             |  27 -
crates/workspace/src/workspace.rs                 |   2 
crates/zed/Cargo.toml                             |   3 
crates/zed/src/main.rs                            |   1 
36 files changed, 1,573 insertions(+), 967 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2942,6 +2942,28 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "component"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "gpui",
+ "linkme",
+ "once_cell",
+ "parking_lot",
+ "theme",
+]
+
+[[package]]
+name = "component_preview"
+version = "0.1.0"
+dependencies = [
+ "component",
+ "gpui",
+ "ui",
+ "workspace",
+]
+
 [[package]]
 name = "concurrent-queue"
 version = "2.5.0"
@@ -7280,6 +7302,26 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "linkme"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -8693,9 +8735,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 
 [[package]]
 name = "oo7"
@@ -14320,8 +14362,10 @@ name = "ui"
 version = "0.1.0"
 dependencies = [
  "chrono",
+ "component",
  "gpui",
  "itertools 0.14.0",
+ "linkme",
  "menu",
  "serde",
  "settings",
@@ -14349,6 +14393,7 @@ name = "ui_macros"
 version = "0.1.0"
 dependencies = [
  "convert_case 0.7.1",
+ "linkme",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -16120,6 +16165,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "component",
  "db",
  "derive_more",
  "env_logger 0.11.6",
@@ -16554,6 +16600,7 @@ dependencies = [
  "collections",
  "command_palette",
  "command_palette_hooks",
+ "component_preview",
  "copilot",
  "db",
  "diagnostics",

Cargo.toml πŸ”—

@@ -26,6 +26,8 @@ members = [
     "crates/collections",
     "crates/command_palette",
     "crates/command_palette_hooks",
+    "crates/component",
+    "crates/component_preview",
     "crates/context_server",
     "crates/context_server_settings",
     "crates/copilot",
@@ -226,6 +228,8 @@ collab_ui = { path = "crates/collab_ui" }
 collections = { path = "crates/collections" }
 command_palette = { path = "crates/command_palette" }
 command_palette_hooks = { path = "crates/command_palette_hooks" }
+component = { path = "crates/component" }
+component_preview = { path = "crates/component_preview" }
 context_server = { path = "crates/context_server" }
 context_server_settings = { path = "crates/context_server_settings" }
 copilot = { path = "crates/copilot" }
@@ -426,6 +430,7 @@ jupyter-websocket-client = { version = "0.9.0" }
 libc = "0.2"
 libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
 linkify = "0.10.0"
+linkme = "0.3.31"
 livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
     "dispatcher",
     "services-dispatcher",

crates/component/Cargo.toml πŸ”—

@@ -0,0 +1,23 @@
+[package]
+name = "component"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/component.rs"
+
+[dependencies]
+collections.workspace = true
+gpui.workspace = true
+linkme.workspace = true
+once_cell = "1.20.3"
+parking_lot.workspace = true
+theme.workspace = true
+
+[features]
+default = []

crates/component/src/component.rs πŸ”—

@@ -0,0 +1,305 @@
+use std::ops::{Deref, DerefMut};
+
+use collections::HashMap;
+use gpui::{div, prelude::*, AnyElement, App, IntoElement, RenderOnce, SharedString, Window};
+use linkme::distributed_slice;
+use once_cell::sync::Lazy;
+use parking_lot::RwLock;
+use theme::ActiveTheme;
+
+pub trait Component {
+    fn scope() -> Option<&'static str>;
+    fn name() -> &'static str {
+        std::any::type_name::<Self>()
+    }
+    fn description() -> Option<&'static str> {
+        None
+    }
+}
+
+pub trait ComponentPreview: Component {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement;
+}
+
+#[distributed_slice]
+pub static __ALL_COMPONENTS: [fn()] = [..];
+
+#[distributed_slice]
+pub static __ALL_PREVIEWS: [fn()] = [..];
+
+pub static COMPONENT_DATA: Lazy<RwLock<ComponentRegistry>> =
+    Lazy::new(|| RwLock::new(ComponentRegistry::new()));
+
+pub struct ComponentRegistry {
+    components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
+    previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentRegistry {
+    fn new() -> Self {
+        ComponentRegistry {
+            components: Vec::new(),
+            previews: HashMap::default(),
+        }
+    }
+}
+
+pub fn init() {
+    let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
+    let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
+
+    for f in component_fns {
+        f();
+    }
+    for f in preview_fns {
+        f();
+    }
+}
+
+pub fn register_component<T: Component>() {
+    let component_data = (T::scope(), T::name(), T::description());
+    COMPONENT_DATA.write().components.push(component_data);
+}
+
+pub fn register_preview<T: ComponentPreview>() {
+    let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement);
+    COMPONENT_DATA
+        .write()
+        .previews
+        .insert(preview_data.0, preview_data.1);
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ComponentId(pub &'static str);
+
+#[derive(Clone)]
+pub struct ComponentMetadata {
+    name: SharedString,
+    scope: Option<SharedString>,
+    description: Option<SharedString>,
+    preview: Option<fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentMetadata {
+    pub fn name(&self) -> SharedString {
+        self.name.clone()
+    }
+
+    pub fn scope(&self) -> Option<SharedString> {
+        self.scope.clone()
+    }
+
+    pub fn description(&self) -> Option<SharedString> {
+        self.description.clone()
+    }
+
+    pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
+        self.preview
+    }
+}
+
+pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
+
+impl AllComponents {
+    pub fn new() -> Self {
+        AllComponents(HashMap::default())
+    }
+
+    /// Returns all components with previews
+    pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
+        self.0.values().filter(|c| c.preview.is_some()).collect()
+    }
+
+    /// Returns all components with previews sorted by name
+    pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
+        let mut previews: Vec<ComponentMetadata> =
+            self.all_previews().into_iter().cloned().collect();
+        previews.sort_by_key(|a| a.name());
+        previews
+    }
+
+    /// Returns all components
+    pub fn all(&self) -> Vec<&ComponentMetadata> {
+        self.0.values().collect()
+    }
+
+    /// Returns all components sorted by name
+    pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
+        let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
+        components.sort_by_key(|a| a.name());
+        components
+    }
+}
+
+impl Deref for AllComponents {
+    type Target = HashMap<ComponentId, ComponentMetadata>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for AllComponents {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+pub fn components() -> AllComponents {
+    let data = COMPONENT_DATA.read();
+    let mut all_components = AllComponents::new();
+
+    for &(scope, name, description) in &data.components {
+        let scope = scope.map(Into::into);
+        let preview = data.previews.get(name).cloned();
+        all_components.insert(
+            ComponentId(name),
+            ComponentMetadata {
+                name: name.into(),
+                scope,
+                description: description.map(Into::into),
+                preview,
+            },
+        );
+    }
+
+    all_components
+}
+
+/// Which side of the preview to show labels on
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ExampleLabelSide {
+    /// Left side
+    Left,
+    /// Right side
+    Right,
+    #[default]
+    /// Top side
+    Top,
+    /// Bottom side
+    Bottom,
+}
+
+/// A single example of a component.
+#[derive(IntoElement)]
+pub struct ComponentExample {
+    variant_name: SharedString,
+    element: AnyElement,
+    label_side: ExampleLabelSide,
+    grow: bool,
+}
+
+impl RenderOnce for ComponentExample {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let base = div().flex();
+
+        let base = match self.label_side {
+            ExampleLabelSide::Right => base.flex_row(),
+            ExampleLabelSide::Left => base.flex_row_reverse(),
+            ExampleLabelSide::Bottom => base.flex_col(),
+            ExampleLabelSide::Top => base.flex_col_reverse(),
+        };
+
+        base.gap_1()
+            .text_xs()
+            .text_color(cx.theme().colors().text_muted)
+            .when(self.grow, |this| this.flex_1())
+            .child(self.element)
+            .child(self.variant_name)
+            .into_any_element()
+    }
+}
+
+impl ComponentExample {
+    /// Create a new example with the given variant name and example value.
+    pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
+        Self {
+            variant_name: variant_name.into(),
+            element,
+            label_side: ExampleLabelSide::default(),
+            grow: false,
+        }
+    }
+
+    /// Set the example to grow to fill the available horizontal space.
+    pub fn grow(mut self) -> Self {
+        self.grow = true;
+        self
+    }
+}
+
+/// A group of component examples.
+#[derive(IntoElement)]
+pub struct ComponentExampleGroup {
+    pub title: Option<SharedString>,
+    pub examples: Vec<ComponentExample>,
+    pub grow: bool,
+}
+
+impl RenderOnce for ComponentExampleGroup {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        div()
+            .flex_col()
+            .text_sm()
+            .text_color(cx.theme().colors().text_muted)
+            .when(self.grow, |this| this.w_full().flex_1())
+            .when_some(self.title, |this, title| this.gap_4().child(title))
+            .child(
+                div()
+                    .flex()
+                    .items_start()
+                    .w_full()
+                    .gap_6()
+                    .children(self.examples)
+                    .into_any_element(),
+            )
+            .into_any_element()
+    }
+}
+
+impl ComponentExampleGroup {
+    /// Create a new group of examples with the given title.
+    pub fn new(examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: None,
+            examples,
+            grow: false,
+        }
+    }
+
+    /// Create a new group of examples with the given title.
+    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: Some(title.into()),
+            examples,
+            grow: false,
+        }
+    }
+
+    /// Set the group to grow to fill the available horizontal space.
+    pub fn grow(mut self) -> Self {
+        self.grow = true;
+        self
+    }
+}
+
+/// Create a single example
+pub fn single_example(
+    variant_name: impl Into<SharedString>,
+    example: AnyElement,
+) -> ComponentExample {
+    ComponentExample::new(variant_name, example)
+}
+
+/// Create a group of examples without a title
+pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
+    ComponentExampleGroup::new(examples)
+}
+
+/// Create a group of examples with a title
+pub fn example_group_with_title(
+    title: impl Into<SharedString>,
+    examples: Vec<ComponentExample>,
+) -> ComponentExampleGroup {
+    ComponentExampleGroup::with_title(title, examples)
+}

crates/component_preview/Cargo.toml πŸ”—

@@ -0,0 +1,21 @@
+[package]
+name = "component_preview"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/component_preview.rs"
+
+[features]
+default = []
+
+[dependencies]
+component.workspace = true
+gpui.workspace = true
+ui.workspace = true
+workspace.workspace = true

crates/component_preview/src/component_preview.rs πŸ”—

@@ -0,0 +1,178 @@
+//! # Component Preview
+//!
+//! A view for exploring Zed components.
+
+use component::{components, ComponentMetadata};
+use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window};
+use ui::prelude::*;
+
+use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
+        workspace.register_action(
+            |workspace, _: &workspace::OpenComponentPreview, window, cx| {
+                let component_preview = cx.new(ComponentPreview::new);
+                workspace.add_item_to_active_pane(
+                    Box::new(component_preview),
+                    None,
+                    true,
+                    window,
+                    cx,
+                )
+            },
+        );
+    })
+    .detach();
+}
+
+struct ComponentPreview {
+    focus_handle: FocusHandle,
+}
+
+impl ComponentPreview {
+    pub fn new(cx: &mut Context<Self>) -> Self {
+        Self {
+            focus_handle: cx.focus_handle(),
+        }
+    }
+
+    fn render_sidebar(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
+        let components = components().all_sorted();
+        let sorted_components = components.clone();
+
+        v_flex().gap_px().p_1().children(
+            sorted_components
+                .into_iter()
+                .map(|component| self.render_sidebar_entry(&component, _cx)),
+        )
+    }
+
+    fn render_sidebar_entry(
+        &self,
+        component: &ComponentMetadata,
+        _cx: &Context<Self>,
+    ) -> impl IntoElement {
+        h_flex()
+            .w_40()
+            .px_1p5()
+            .py_1()
+            .child(component.name().clone())
+    }
+
+    fn render_preview(
+        &self,
+        component: &ComponentMetadata,
+        window: &mut Window,
+        cx: &Context<Self>,
+    ) -> impl IntoElement {
+        let name = component.name();
+        let scope = component.scope();
+
+        let description = component.description();
+
+        v_group()
+            .w_full()
+            .gap_4()
+            .p_8()
+            .rounded_md()
+            .child(
+                v_flex()
+                    .gap_1()
+                    .child(
+                        h_flex()
+                            .gap_1()
+                            .text_xl()
+                            .child(div().child(name))
+                            .when_some(scope, |this, scope| {
+                                this.child(div().opacity(0.5).child(format!("({})", scope)))
+                            }),
+                    )
+                    .when_some(description, |this, description| {
+                        this.child(
+                            div()
+                                .text_ui_sm(cx)
+                                .text_color(cx.theme().colors().text_muted)
+                                .max_w(px(600.0))
+                                .child(description),
+                        )
+                    }),
+            )
+            .when_some(component.preview(), |this, preview| {
+                this.child(preview(window, cx))
+            })
+            .into_any_element()
+    }
+
+    fn render_previews(&self, window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
+        v_flex()
+            .id("component-previews")
+            .size_full()
+            .overflow_y_scroll()
+            .p_4()
+            .gap_2()
+            .children(
+                components()
+                    .all_previews_sorted()
+                    .iter()
+                    .map(|component| self.render_preview(component, window, cx)),
+            )
+    }
+}
+
+impl Render for ComponentPreview {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+        h_flex()
+            .id("component-preview")
+            .key_context("ComponentPreview")
+            .items_start()
+            .overflow_hidden()
+            .size_full()
+            .max_h_full()
+            .track_focus(&self.focus_handle)
+            .px_2()
+            .bg(cx.theme().colors().editor_background)
+            .child(self.render_sidebar(window, cx))
+            .child(self.render_previews(window, cx))
+    }
+}
+
+impl EventEmitter<ItemEvent> for ComponentPreview {}
+
+impl Focusable for ComponentPreview {
+    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for ComponentPreview {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
+        Some("Component Preview".into())
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: Option<WorkspaceId>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<gpui::Entity<Self>>
+    where
+        Self: Sized,
+    {
+        Some(cx.new(Self::new))
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
+}

crates/ui/Cargo.toml πŸ”—

@@ -14,8 +14,10 @@ path = "src/ui.rs"
 
 [dependencies]
 chrono.workspace = true
+component.workspace = true
 gpui.workspace = true
 itertools = { workspace = true, optional = true }
+linkme.workspace = true
 menu.workspace = true
 serde.workspace = true
 settings.workspace = true
@@ -31,3 +33,7 @@ windows.workspace = true
 [features]
 default = []
 stories = ["dep:itertools", "dep:story"]
+
+# cargo-machete doesn't understand that linkme is used in the component macro
+[package.metadata.cargo-machete]
+ignored = ["linkme"]

crates/ui/src/components/avatar/avatar.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::prelude::*;
+use crate::{prelude::*, Indicator};
 
 use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
 
@@ -14,7 +14,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
 ///     .grayscale(true)
 ///     .border_color(gpui::red());
 /// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Avatar {
     image: Img,
     size: Option<AbsoluteLength>,
@@ -96,3 +96,60 @@ impl RenderOnce for Avatar {
             .children(self.indicator.map(|indicator| div().child(indicator)))
     }
 }
+
+impl ComponentPreview for Avatar {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
+
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example(
+                            "Default",
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Small",
+                            Avatar::new(example_avatar).size(px(24.)).into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            Avatar::new(example_avatar).size(px(48.)).into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example("Default", Avatar::new(example_avatar).into_any_element()),
+                        single_example(
+                            "Grayscale",
+                            Avatar::new(example_avatar)
+                                .grayscale(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Border",
+                            Avatar::new(example_avatar)
+                                .border_color(gpui::red())
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Indicator",
+                    vec![single_example(
+                        "Dot",
+                        Avatar::new(example_avatar)
+                            .indicator(Indicator::dot().color(Color::Success))
+                            .into_any_element(),
+                    )],
+                ),
+            ])
+            .into_any_element()
+    }
+}

crates/ui/src/components/button/button.rs πŸ”—

@@ -1,5 +1,7 @@
 #![allow(missing_docs)]
-use gpui::{AnyView, DefiniteLength};
+use component::{example_group_with_title, single_example, ComponentPreview};
+use gpui::{AnyElement, AnyView, DefiniteLength};
+use ui_macros::IntoComponent;
 
 use crate::{
     prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
@@ -78,7 +80,7 @@ use super::button_icon::ButtonIcon;
 ///     });
 /// ```
 ///
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Button {
     base: ButtonLike,
     label: SharedString,
@@ -455,101 +457,124 @@ impl RenderOnce for Button {
 }
 
 impl ComponentPreview for Button {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A button allows users to take actions, and make choices, with a single tap."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Styles",
-                vec![
-                    single_example("Default", Button::new("default", "Default")),
-                    single_example(
-                        "Filled",
-                        Button::new("filled", "Filled").style(ButtonStyle::Filled),
-                    ),
-                    single_example(
-                        "Subtle",
-                        Button::new("outline", "Subtle").style(ButtonStyle::Subtle),
-                    ),
-                    single_example(
-                        "Transparent",
-                        Button::new("transparent", "Transparent").style(ButtonStyle::Transparent),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Tinted",
-                vec![
-                    single_example(
-                        "Accent",
-                        Button::new("tinted_accent", "Accent")
-                            .style(ButtonStyle::Tinted(TintColor::Accent)),
-                    ),
-                    single_example(
-                        "Error",
-                        Button::new("tinted_negative", "Error")
-                            .style(ButtonStyle::Tinted(TintColor::Error)),
-                    ),
-                    single_example(
-                        "Warning",
-                        Button::new("tinted_warning", "Warning")
-                            .style(ButtonStyle::Tinted(TintColor::Warning)),
-                    ),
-                    single_example(
-                        "Success",
-                        Button::new("tinted_positive", "Success")
-                            .style(ButtonStyle::Tinted(TintColor::Success)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "States",
-                vec![
-                    single_example("Default", Button::new("default_state", "Default")),
-                    single_example(
-                        "Disabled",
-                        Button::new("disabled", "Disabled").disabled(true),
-                    ),
-                    single_example(
-                        "Selected",
-                        Button::new("selected", "Selected").toggle_state(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "With Icons",
-                vec![
-                    single_example(
-                        "Icon Start",
-                        Button::new("icon_start", "Icon Start")
-                            .icon(IconName::Check)
-                            .icon_position(IconPosition::Start),
-                    ),
-                    single_example(
-                        "Icon End",
-                        Button::new("icon_end", "Icon End")
-                            .icon(IconName::Check)
-                            .icon_position(IconPosition::End),
-                    ),
-                    single_example(
-                        "Icon Color",
-                        Button::new("icon_color", "Icon Color")
-                            .icon(IconName::Check)
-                            .icon_color(Color::Accent),
-                    ),
-                    single_example(
-                        "Tinted Icons",
-                        Button::new("tinted_icons", "Error")
-                            .style(ButtonStyle::Tinted(TintColor::Error))
-                            .color(Color::Error)
-                            .icon_color(Color::Error)
-                            .icon(IconName::Trash)
-                            .icon_position(IconPosition::Start),
-                    ),
-                ],
-            ),
-        ]
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example(
+                            "Default",
+                            Button::new("default", "Default").into_any_element(),
+                        ),
+                        single_example(
+                            "Filled",
+                            Button::new("filled", "Filled")
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Subtle",
+                            Button::new("outline", "Subtle")
+                                .style(ButtonStyle::Subtle)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Transparent",
+                            Button::new("transparent", "Transparent")
+                                .style(ButtonStyle::Transparent)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Tinted",
+                    vec![
+                        single_example(
+                            "Accent",
+                            Button::new("tinted_accent", "Accent")
+                                .style(ButtonStyle::Tinted(TintColor::Accent))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Error",
+                            Button::new("tinted_negative", "Error")
+                                .style(ButtonStyle::Tinted(TintColor::Error))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Warning",
+                            Button::new("tinted_warning", "Warning")
+                                .style(ButtonStyle::Tinted(TintColor::Warning))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Success",
+                            Button::new("tinted_positive", "Success")
+                                .style(ButtonStyle::Tinted(TintColor::Success))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Default",
+                            Button::new("default_state", "Default").into_any_element(),
+                        ),
+                        single_example(
+                            "Disabled",
+                            Button::new("disabled", "Disabled")
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Button::new("selected", "Selected")
+                                .toggle_state(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Icons",
+                    vec![
+                        single_example(
+                            "Icon Start",
+                            Button::new("icon_start", "Icon Start")
+                                .icon(IconName::Check)
+                                .icon_position(IconPosition::Start)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Icon End",
+                            Button::new("icon_end", "Icon End")
+                                .icon(IconName::Check)
+                                .icon_position(IconPosition::End)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Icon Color",
+                            Button::new("icon_color", "Icon Color")
+                                .icon(IconName::Check)
+                                .icon_color(Color::Accent)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Tinted Icons",
+                            Button::new("tinted_icons", "Error")
+                                .style(ButtonStyle::Tinted(TintColor::Error))
+                                .color(Color::Error)
+                                .icon_color(Color::Error)
+                                .icon(IconName::Trash)
+                                .icon_position(IconPosition::Start)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

crates/ui/src/components/content_group.rs πŸ”—

@@ -1,4 +1,5 @@
 use crate::prelude::*;
+use component::{example_group, single_example, ComponentPreview};
 use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
 use smallvec::SmallVec;
 
@@ -22,7 +23,8 @@ pub fn h_group() -> ContentGroup {
 }
 
 /// A flexible container component that can hold other elements.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "layout")]
 pub struct ContentGroup {
     base: Div,
     border: bool,
@@ -87,16 +89,8 @@ impl RenderOnce for ContentGroup {
 }
 
 impl ComponentPreview for ContentGroup {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A flexible container component that can hold other elements. It can be customized with or without a border and background fill."
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::Bottom
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![example_group(vec![
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        example_group(vec![
             single_example(
                 "Default",
                 ContentGroup::new()
@@ -104,7 +98,8 @@ impl ComponentPreview for ContentGroup {
                     .items_center()
                     .justify_center()
                     .h_48()
-                    .child(Label::new("Default ContentBox")),
+                    .child(Label::new("Default ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
             single_example(
@@ -115,7 +110,8 @@ impl ComponentPreview for ContentGroup {
                     .justify_center()
                     .h_48()
                     .borderless()
-                    .child(Label::new("Borderless ContentBox")),
+                    .child(Label::new("Borderless ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
             single_example(
@@ -126,10 +122,11 @@ impl ComponentPreview for ContentGroup {
                     .justify_center()
                     .h_48()
                     .unfilled()
-                    .child(Label::new("Unfilled ContentBox")),
+                    .child(Label::new("Unfilled ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
         ])
-        .grow()]
+        .into_any_element()
     }
 }

crates/ui/src/components/facepile.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::{prelude::*, Avatar};
+use crate::prelude::*;
 use gpui::{AnyElement, StyleRefinement};
 use smallvec::SmallVec;
 
@@ -60,60 +60,60 @@ impl RenderOnce for Facepile {
     }
 }
 
-impl ComponentPreview for Facepile {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A facepile is a collection of faces stacked horizontally–\
-        always with the leftmost face on top and descending in z-index.\
-        \n\nFacepiles are used to display a group of people or things,\
-        such as a list of participants in a collaboration session."
-    }
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let few_faces: [&'static str; 3] = [
-            "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
-        ];
+// impl ComponentPreview for Facepile {
+//     fn description() -> impl Into<Option<&'static str>> {
+//         "A facepile is a collection of faces stacked horizontally–\
+//         always with the leftmost face on top and descending in z-index.\
+//         \n\nFacepiles are used to display a group of people or things,\
+//         such as a list of participants in a collaboration session."
+//     }
+//     fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
+//         let few_faces: [&'static str; 3] = [
+//             "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+//         ];
 
-        let many_faces: [&'static str; 6] = [
-            "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
-        ];
+//         let many_faces: [&'static str; 6] = [
+//             "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+//         ];
 
-        vec![example_group_with_title(
-            "Examples",
-            vec![
-                single_example(
-                    "Few Faces",
-                    Facepile::new(
-                        few_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).into_any_element())
-                            .collect(),
-                    ),
-                ),
-                single_example(
-                    "Many Faces",
-                    Facepile::new(
-                        many_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).into_any_element())
-                            .collect(),
-                    ),
-                ),
-                single_example(
-                    "Custom Size",
-                    Facepile::new(
-                        few_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
-                            .collect(),
-                    ),
-                ),
-            ],
-        )]
-    }
-}
+//         vec![example_group_with_title(
+//             "Examples",
+//             vec![
+//                 single_example(
+//                     "Few Faces",
+//                     Facepile::new(
+//                         few_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//                 single_example(
+//                     "Many Faces",
+//                     Facepile::new(
+//                         many_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//                 single_example(
+//                     "Custom Size",
+//                     Facepile::new(
+//                         few_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//             ],
+//         )]
+//     }
+// }

crates/ui/src/components/icon.rs πŸ”—

@@ -7,17 +7,13 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 pub use decorated_icon::*;
-use gpui::{img, svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
+use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
 pub use icon_decoration::*;
 use serde::{Deserialize, Serialize};
 use strum::{EnumIter, EnumString, IntoStaticStr};
 use ui_macros::DerivePathStr;
 
-use crate::{
-    prelude::*,
-    traits::component_preview::{ComponentExample, ComponentPreview},
-    Indicator,
-};
+use crate::{prelude::*, Indicator};
 
 #[derive(IntoElement)]
 pub enum AnyIcon {
@@ -364,7 +360,7 @@ impl IconSource {
     }
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Icon {
     source: IconSource,
     color: Color,
@@ -494,24 +490,41 @@ impl RenderOnce for IconWithIndicator {
 }
 
 impl ComponentPreview for Icon {
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
-        let arrow_icons = vec![
-            IconName::ArrowDown,
-            IconName::ArrowLeft,
-            IconName::ArrowRight,
-            IconName::ArrowUp,
-            IconName::ArrowCircle,
-        ];
-
-        vec![example_group_with_title(
-            "Arrow Icons",
-            arrow_icons
-                .into_iter()
-                .map(|icon| {
-                    let name = format!("{:?}", icon).to_string();
-                    ComponentExample::new(name, Icon::new(icon))
-                })
-                .collect(),
-        )]
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
+                        single_example(
+                            "Small",
+                            Icon::new(IconName::Star)
+                                .size(IconSize::Small)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            Icon::new(IconName::Star)
+                                .size(IconSize::XLarge)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Colors",
+                    vec![
+                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
+                        single_example(
+                            "Custom Color",
+                            Icon::new(IconName::Bell)
+                                .color(Color::Error)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

crates/ui/src/components/icon/decorated_icon.rs πŸ”—

@@ -1,10 +1,8 @@
-use gpui::{IntoElement, Point};
+use gpui::{AnyElement, IntoElement, Point};
 
-use crate::{
-    prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind,
-};
+use crate::{prelude::*, IconDecoration, IconDecorationKind};
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct DecoratedIcon {
     icon: Icon,
     decoration: Option<IconDecoration>,
@@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon {
 }
 
 impl ComponentPreview for DecoratedIcon {
-    fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let icon_1 = Icon::new(IconName::FileDoc);
-        let icon_2 = Icon::new(IconName::FileDoc);
-        let icon_3 = Icon::new(IconName::FileDoc);
-        let icon_4 = Icon::new(IconName::FileDoc);
-
+    fn preview(_window: &mut Window, cx: &App) -> AnyElement {
         let decoration_x = IconDecoration::new(
             IconDecorationKind::X,
             cx.theme().colors().surface_background,
@@ -66,22 +59,32 @@ impl ComponentPreview for DecoratedIcon {
             y: px(-2.),
         });
 
-        let examples = vec![
-            single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_2, Some(decoration_x)),
-            ),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_3, Some(decoration_triangle)),
-            ),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_4, Some(decoration_dot)),
-            ),
-        ];
-
-        vec![example_group(examples)]
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Decorations",
+                vec![
+                    single_example(
+                        "No Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(),
+                    ),
+                    single_example(
+                        "X Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Triangle Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Dot Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot))
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
     }
 }

crates/ui/src/components/icon/icon_decoration.rs πŸ”—

@@ -1,8 +1,8 @@
 use gpui::{svg, Hsla, IntoElement, Point};
-use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
+use strum::{EnumIter, EnumString, IntoStaticStr};
 use ui_macros::DerivePathStr;
 
-use crate::{prelude::*, traits::component_preview::ComponentPreview};
+use crate::prelude::*;
 
 const ICON_DECORATION_SIZE: Pixels = px(11.);
 
@@ -149,21 +149,3 @@ impl RenderOnce for IconDecoration {
             .child(background)
     }
 }
-
-impl ComponentPreview for IconDecoration {
-    fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
-
-        let examples = all_kinds
-            .iter()
-            .map(|kind| {
-                single_example(
-                    format!("{kind:?}"),
-                    IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
-                )
-            })
-            .collect();
-
-        vec![example_group(examples)]
-    }
-}

crates/ui/src/components/indicator.rs πŸ”—

@@ -83,34 +83,3 @@ impl RenderOnce for Indicator {
         }
     }
 }
-
-impl ComponentPreview for Indicator {
-    fn description() -> impl Into<Option<&'static str>> {
-        "An indicator visually represents a status or state."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Types",
-                vec![
-                    single_example("Dot", Indicator::dot().color(Color::Info)),
-                    single_example("Bar", Indicator::bar().color(Color::Player(2))),
-                    single_example(
-                        "Icon",
-                        Indicator::icon(Icon::new(IconName::Check).color(Color::Success)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Examples",
-                vec![
-                    single_example("Info", Indicator::dot().color(Color::Info)),
-                    single_example("Success", Indicator::dot().color(Color::Success)),
-                    single_example("Warning", Indicator::dot().color(Color::Warning)),
-                    single_example("Error", Indicator::dot().color(Color::Error)),
-                ],
-            ),
-        ]
-    }
-}

crates/ui/src/components/keybinding_hint.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{h_flex, prelude::*};
 use crate::{ElevationIndex, KeyBinding};
-use gpui::{point, App, BoxShadow, IntoElement, Window};
+use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
 use smallvec::smallvec;
 
 /// Represents a hint for a keybinding, optionally with a prefix and suffix.
@@ -17,7 +17,7 @@ use smallvec::smallvec;
 ///     .prefix("Save:")
 ///     .size(Pixels::from(14.0));
 /// ```
-#[derive(Debug, IntoElement, Clone)]
+#[derive(Debug, IntoElement, IntoComponent)]
 pub struct KeybindingHint {
     prefix: Option<SharedString>,
     suffix: Option<SharedString>,
@@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint {
 }
 
 impl ComponentPreview for KeybindingHint {
-    fn description() -> impl Into<Option<&'static str>> {
-        "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
-    }
-
-    fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None);
-        let home = KeyBinding::for_action(&menu::SelectFirst, window)
-            .unwrap_or(KeyBinding::new(home_fallback));
-
-        let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None);
-        let end = KeyBinding::for_action(&menu::SelectLast, window)
-            .unwrap_or(KeyBinding::new(end_fallback));
-
+    fn preview(window: &mut Window, _cx: &App) -> AnyElement {
         let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
         let enter = KeyBinding::for_action(&menu::Confirm, window)
             .unwrap_or(KeyBinding::new(enter_fallback));
 
-        let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None);
-        let escape = KeyBinding::for_action(&menu::Cancel, window)
-            .unwrap_or(KeyBinding::new(escape_fallback));
-
-        vec![
-            example_group_with_title(
-                "Basic",
-                vec![
-                    single_example(
-                        "With Prefix",
-                        KeybindingHint::with_prefix("Go to Start:", home.clone()),
-                    ),
-                    single_example(
-                        "With Suffix",
-                        KeybindingHint::with_suffix(end.clone(), "Go to End"),
-                    ),
-                    single_example(
-                        "With Prefix and Suffix",
-                        KeybindingHint::new(enter.clone())
-                            .prefix("Confirm:")
-                            .suffix("Execute selected action"),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Sizes",
-                vec![
-                    single_example(
-                        "Small",
-                        KeybindingHint::new(home.clone())
-                            .size(Pixels::from(12.0))
-                            .prefix("Small:"),
-                    ),
-                    single_example(
-                        "Medium",
-                        KeybindingHint::new(end.clone())
-                            .size(Pixels::from(16.0))
-                            .suffix("Medium"),
-                    ),
-                    single_example(
-                        "Large",
-                        KeybindingHint::new(enter.clone())
-                            .size(Pixels::from(20.0))
-                            .prefix("Large:")
-                            .suffix("Size"),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Elevations",
-                vec![
-                    single_example(
-                        "Surface",
-                        KeybindingHint::new(home.clone())
-                            .elevation(ElevationIndex::Surface)
-                            .prefix("Surface:"),
-                    ),
-                    single_example(
-                        "Elevated Surface",
-                        KeybindingHint::new(end.clone())
-                            .elevation(ElevationIndex::ElevatedSurface)
-                            .suffix("Elevated"),
-                    ),
-                    single_example(
-                        "Editor Surface",
-                        KeybindingHint::new(enter.clone())
-                            .elevation(ElevationIndex::EditorSurface)
-                            .prefix("Editor:")
-                            .suffix("Surface"),
-                    ),
-                    single_example(
-                        "Modal Surface",
-                        KeybindingHint::new(escape.clone())
-                            .elevation(ElevationIndex::ModalSurface)
-                            .prefix("Modal:")
-                            .suffix("Escape"),
-                    ),
-                ],
-            ),
-        ]
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Basic",
+                    vec![
+                        single_example(
+                            "With Prefix",
+                            KeybindingHint::with_prefix("Go to Start:", enter.clone())
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Suffix",
+                            KeybindingHint::with_suffix(enter.clone(), "Go to End")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Prefix and Suffix",
+                            KeybindingHint::new(enter.clone())
+                                .prefix("Confirm:")
+                                .suffix("Execute selected action")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example(
+                            "Small",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(12.0))
+                                .prefix("Small:")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Medium",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(16.0))
+                                .suffix("Medium")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(20.0))
+                                .prefix("Large:")
+                                .suffix("Size")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Elevations",
+                    vec![
+                        single_example(
+                            "Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::Surface)
+                                .prefix("Surface:")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Elevated Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::ElevatedSurface)
+                                .suffix("Elevated")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Editor Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::EditorSurface)
+                                .prefix("Editor:")
+                                .suffix("Surface")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Modal Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::ModalSurface)
+                                .prefix("Modal:")
+                                .suffix("Enter")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

crates/ui/src/components/label/label.rs πŸ”—

@@ -1,6 +1,6 @@
 #![allow(missing_docs)]
 
-use gpui::{App, StyleRefinement, Window};
+use gpui::{AnyElement, App, StyleRefinement, Window};
 
 use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
 
@@ -32,7 +32,7 @@ use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
 ///
 /// let my_label = Label::new("Deleted").strikethrough(true);
 /// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Label {
     base: LabelLike,
     label: SharedString,
@@ -184,3 +184,53 @@ impl RenderOnce for Label {
         self.base.child(self.label)
     }
 }
+
+impl ComponentPreview for Label {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example("Default", Label::new("Default Label").into_any_element()),
+                        single_example("Small", Label::new("Small Label").size(LabelSize::Small).into_any_element()),
+                        single_example("Large", Label::new("Large Label").size(LabelSize::Large).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Colors",
+                    vec![
+                        single_example("Default", Label::new("Default Color").into_any_element()),
+                        single_example("Accent", Label::new("Accent Color").color(Color::Accent).into_any_element()),
+                        single_example("Error", Label::new("Error Color").color(Color::Error).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example("Default", Label::new("Default Style").into_any_element()),
+                        single_example("Bold", Label::new("Bold Style").weight(gpui::FontWeight::BOLD).into_any_element()),
+                        single_example("Italic", Label::new("Italic Style").italic(true).into_any_element()),
+                        single_example("Strikethrough", Label::new("Strikethrough Style").strikethrough(true).into_any_element()),
+                        single_example("Underline", Label::new("Underline Style").underline(true).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Line Height Styles",
+                    vec![
+                        single_example("Default", Label::new("Default Line Height").into_any_element()),
+                        single_example("UI Label", Label::new("UI Label Line Height").line_height_style(LineHeightStyle::UiLabel).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Special Cases",
+                    vec![
+                        single_example("Single Line", Label::new("Single\nLine\nText").single_line().into_any_element()),
+                        single_example("Text Ellipsis", Label::new("This is a very long text that should be truncated with an ellipsis").text_ellipsis().into_any_element()),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

crates/ui/src/components/radio.rs πŸ”—

@@ -4,9 +4,6 @@ use std::sync::Arc;
 
 use crate::prelude::*;
 
-/// A [`Checkbox`] that has a [`Label`].
-///
-/// [`Checkbox`]: crate::components::Checkbox
 #[derive(IntoElement)]
 pub struct RadioWithLabel {
     id: ElementId,

crates/ui/src/components/tab.rs πŸ”—

@@ -27,7 +27,7 @@ pub enum TabCloseSide {
     End,
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Tab {
     div: Stateful<Div>,
     selected: bool,
@@ -171,3 +171,48 @@ impl RenderOnce for Tab {
             )
     }
 }
+
+impl ComponentPreview for Tab {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Variations",
+                vec![
+                    single_example(
+                        "Default",
+                        Tab::new("default").child("Default Tab").into_any_element(),
+                    ),
+                    single_example(
+                        "Selected",
+                        Tab::new("selected")
+                            .toggle_state(true)
+                            .child("Selected Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "First",
+                        Tab::new("first")
+                            .position(TabPosition::First)
+                            .child("First Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Middle",
+                        Tab::new("middle")
+                            .position(TabPosition::Middle(Ordering::Equal))
+                            .child("Middle Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Last",
+                        Tab::new("last")
+                            .position(TabPosition::Last)
+                            .child("Last Tab")
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
+    }
+}

crates/ui/src/components/table.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{prelude::*, Indicator};
 use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
 
 /// A table component
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Table {
     column_headers: Vec<SharedString>,
     rows: Vec<Vec<TableCell>>,
@@ -152,88 +152,110 @@ where
 }
 
 impl ComponentPreview for Table {
-    fn description() -> impl Into<Option<&'static str>> {
-        "Used for showing tabular data. Tables may show both text and elements in their cells."
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::Top
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group(vec![
-                single_example(
-                    "Simple Table",
-                    Table::new(vec!["Name", "Age", "City"])
-                        .width(px(400.))
-                        .row(vec!["Alice", "28", "New York"])
-                        .row(vec!["Bob", "32", "San Francisco"])
-                        .row(vec!["Charlie", "25", "London"]),
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Basic Tables",
+                    vec![
+                        single_example(
+                            "Simple Table",
+                            Table::new(vec!["Name", "Age", "City"])
+                                .width(px(400.))
+                                .row(vec!["Alice", "28", "New York"])
+                                .row(vec!["Bob", "32", "San Francisco"])
+                                .row(vec!["Charlie", "25", "London"])
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Two Column Table",
+                            Table::new(vec!["Category", "Value"])
+                                .width(px(300.))
+                                .row(vec!["Revenue", "$100,000"])
+                                .row(vec!["Expenses", "$75,000"])
+                                .row(vec!["Profit", "$25,000"])
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styled Tables",
+                    vec![
+                        single_example(
+                            "Default",
+                            Table::new(vec!["Product", "Price", "Stock"])
+                                .width(px(400.))
+                                .row(vec!["Laptop", "$999", "In Stock"])
+                                .row(vec!["Phone", "$599", "Low Stock"])
+                                .row(vec!["Tablet", "$399", "Out of Stock"])
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Striped",
+                            Table::new(vec!["Product", "Price", "Stock"])
+                                .width(px(400.))
+                                .striped()
+                                .row(vec!["Laptop", "$999", "In Stock"])
+                                .row(vec!["Phone", "$599", "Low Stock"])
+                                .row(vec!["Tablet", "$399", "Out of Stock"])
+                                .row(vec!["Headphones", "$199", "In Stock"])
+                                .into_any_element(),
+                        ),
+                    ],
                 ),
-                single_example(
-                    "Two Column Table",
-                    Table::new(vec!["Category", "Value"])
-                        .width(px(300.))
-                        .row(vec!["Revenue", "$100,000"])
-                        .row(vec!["Expenses", "$75,000"])
-                        .row(vec!["Profit", "$25,000"]),
+                example_group_with_title(
+                    "Mixed Content Table",
+                    vec![single_example(
+                        "Table with Elements",
+                        Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
+                            .width(px(840.))
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Success).into_any_element(),
+                                ),
+                                string_cell("Project A"),
+                                string_cell("High"),
+                                string_cell("2023-12-31"),
+                                element_cell(
+                                    Button::new("view_a", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Warning).into_any_element(),
+                                ),
+                                string_cell("Project B"),
+                                string_cell("Medium"),
+                                string_cell("2024-03-15"),
+                                element_cell(
+                                    Button::new("view_b", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Error).into_any_element(),
+                                ),
+                                string_cell("Project C"),
+                                string_cell("Low"),
+                                string_cell("2024-06-30"),
+                                element_cell(
+                                    Button::new("view_c", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .into_any_element(),
+                    )],
                 ),
-            ]),
-            example_group(vec![single_example(
-                "Striped Table",
-                Table::new(vec!["Product", "Price", "Stock"])
-                    .width(px(600.))
-                    .striped()
-                    .row(vec!["Laptop", "$999", "In Stock"])
-                    .row(vec!["Phone", "$599", "Low Stock"])
-                    .row(vec!["Tablet", "$399", "Out of Stock"])
-                    .row(vec!["Headphones", "$199", "In Stock"]),
-            )]),
-            example_group_with_title(
-                "Mixed Content Table",
-                vec![single_example(
-                    "Table with Elements",
-                    Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
-                        .width(px(840.))
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Success).into_any_element()),
-                            string_cell("Project A"),
-                            string_cell("High"),
-                            string_cell("2023-12-31"),
-                            element_cell(
-                                Button::new("view_a", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ])
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Warning).into_any_element()),
-                            string_cell("Project B"),
-                            string_cell("Medium"),
-                            string_cell("2024-03-15"),
-                            element_cell(
-                                Button::new("view_b", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ])
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Error).into_any_element()),
-                            string_cell("Project C"),
-                            string_cell("Low"),
-                            string_cell("2024-06-30"),
-                            element_cell(
-                                Button::new("view_c", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ]),
-                )],
-            ),
-        ]
+            ])
+            .into_any_element()
     }
 }

crates/ui/src/components/toggle.rs πŸ”—

@@ -1,5 +1,6 @@
 use gpui::{
-    div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
+    div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled,
+    Window,
 };
 use std::sync::Arc;
 
@@ -38,7 +39,8 @@ pub enum ToggleStyle {
 /// Checkboxes are used for multiple choices, not for mutually exclusive choices.
 /// Each checkbox works independently from other checkboxes in the list,
 /// therefore checking an additional box does not affect any other selections.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct Checkbox {
     id: ElementId,
     toggle_state: ToggleState,
@@ -237,7 +239,8 @@ impl RenderOnce for Checkbox {
 }
 
 /// A [`Checkbox`] that has a [`Label`].
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct CheckboxWithLabel {
     id: ElementId,
     label: Label,
@@ -314,7 +317,8 @@ impl RenderOnce for CheckboxWithLabel {
 /// # Switch
 ///
 /// Switches are used to represent opposite states, such as enabled or disabled.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct Switch {
     id: ElementId,
     toggle_state: ToggleState,
@@ -446,285 +450,190 @@ impl RenderOnce for Switch {
 }
 
 impl ComponentPreview for Checkbox {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Unselected",
+                            Checkbox::new("checkbox_unselected", ToggleState::Unselected)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Indeterminate",
+                            Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Checkbox::new("checkbox_selected", ToggleState::Selected)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example(
+                            "Default",
+                            Checkbox::new("checkbox_default", ToggleState::Selected)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Filled",
+                            Checkbox::new("checkbox_filled", ToggleState::Selected)
+                                .fill()
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "ElevationBased",
+                            Checkbox::new("checkbox_elevation", ToggleState::Selected)
+                                .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Custom Color",
+                            Checkbox::new("checkbox_custom", ToggleState::Selected)
+                                .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7)))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Disabled",
+                    vec![
+                        single_example(
+                            "Unselected",
+                            Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Label",
+                    vec![single_example(
+                        "Default",
+                        Checkbox::new("checkbox_with_label", ToggleState::Selected)
+                            .label("Always save on quit")
+                            .into_any_element(),
+                    )],
+                ),
+            ])
+            .into_any_element()
     }
+}
 
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Default",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unselected", ToggleState::Unselected),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_selected", ToggleState::Selected),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Default (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "ElevationBased",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_unfilled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "ElevationBased (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Custom Color",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Custom Color (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
-                            .fill()
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_custom_filled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .fill()
-                        .style(ToggleStyle::Custom(hsla(
-                            142.0 / 360.,
-                            0.68,
-                            0.45,
-                            0.7,
-                        ))),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
-                            .disabled(true),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_disabled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .disabled(true),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
-                            .disabled(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled (Filled)",
+impl ComponentPreview for Switch {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Off",
+                            Switch::new("switch_off", ToggleState::Unselected)
+                                .on_click(|_, _, _cx| {})
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On",
+                            Switch::new("switch_on", ToggleState::Selected)
+                                .on_click(|_, _, _cx| {})
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Disabled",
+                    vec![
+                        single_example(
+                            "Off",
+                            Switch::new("switch_disabled_off", ToggleState::Unselected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On",
+                            Switch::new("switch_disabled_on", ToggleState::Selected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Label",
+                    vec![
+                        single_example(
+                            "Label",
+                            Switch::new("switch_with_label", ToggleState::Selected)
+                                .label("Always save on quit")
+                                .into_any_element(),
+                        ),
+                        // TODO: Where did theme_preview_keybinding go?
+                        // single_example(
+                        //     "Keybinding",
+                        //     Switch::new("switch_with_keybinding", ToggleState::Selected)
+                        //         .key_binding(theme_preview_keybinding("cmd-shift-e"))
+                        //         .into_any_element(),
+                        // ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}
+
+impl ComponentPreview for CheckboxWithLabel {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "States",
                 vec![
                     single_example(
                         "Unselected",
-                        Checkbox::new(
-                            "checkbox_disabled_filled_unselected",
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_unselected",
+                            Label::new("Always save on quit"),
                             ToggleState::Unselected,
+                            |_, _, _| {},
                         )
-                        .fill()
-                        .disabled(true),
+                        .into_any_element(),
                     ),
                     single_example(
                         "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_disabled_filled_indeterminate",
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_indeterminate",
+                            Label::new("Always save on quit"),
                             ToggleState::Indeterminate,
+                            |_, _, _| {},
                         )
-                        .fill()
-                        .disabled(true),
+                        .into_any_element(),
                     ),
                     single_example(
                         "Selected",
-                        Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .disabled(true),
-                    ),
-                ],
-            ),
-        ]
-    }
-}
-
-impl ComponentPreview for Switch {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
-    }
-
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Default",
-                vec![
-                    single_example(
-                        "Off",
-                        Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}),
-                    ),
-                    single_example(
-                        "On",
-                        Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled",
-                vec![
-                    single_example(
-                        "Off",
-                        Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
-                    ),
-                    single_example(
-                        "On",
-                        Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Label Permutations",
-                vec![
-                    single_example(
-                        "Label",
-                        Switch::new("switch_with_label", ToggleState::Selected)
-                            .label("Always save on quit"),
-                    ),
-                    single_example(
-                        "Keybinding",
-                        Switch::new("switch_with_label", ToggleState::Selected)
-                            .key_binding(theme_preview_keybinding("cmd-shift-e")),
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_selected",
+                            Label::new("Always save on quit"),
+                            ToggleState::Selected,
+                            |_, _, _| {},
+                        )
+                        .into_any_element(),
                     ),
                 ],
-            ),
-        ]
-    }
-}
-
-impl ComponentPreview for CheckboxWithLabel {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![example_group(vec![
-            single_example(
-                "Unselected",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_unselected",
-                    Label::new("Always save on quit"),
-                    ToggleState::Unselected,
-                    |_, _, _| {},
-                ),
-            ),
-            single_example(
-                "Indeterminate",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_indeterminate",
-                    Label::new("Always save on quit"),
-                    ToggleState::Indeterminate,
-                    |_, _, _| {},
-                ),
-            ),
-            single_example(
-                "Selected",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_selected",
-                    Label::new("Always save on quit"),
-                    ToggleState::Selected,
-                    |_, _, _| {},
-                ),
-            ),
-        ])]
+            )])
+            .into_any_element()
     }
 }

crates/ui/src/components/tooltip.rs πŸ”—

@@ -1,12 +1,13 @@
 #![allow(missing_docs)]
 
-use gpui::{Action, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
+use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
 use settings::Settings;
 use theme::ThemeSettings;
 
 use crate::prelude::*;
 use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt};
 
+#[derive(IntoComponent)]
 pub struct Tooltip {
     title: SharedString,
     meta: Option<SharedString>,
@@ -204,3 +205,15 @@ impl Render for LinkPreview {
         })
     }
 }
+
+impl ComponentPreview for Tooltip {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        example_group(vec![single_example(
+            "Text only",
+            Button::new("delete-example", "Delete")
+                .tooltip(Tooltip::text("This is a tooltip!"))
+                .into_any_element(),
+        )])
+        .into_any_element()
+    }
+}

crates/ui/src/prelude.rs πŸ”—

@@ -6,9 +6,11 @@ pub use gpui::{
     InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window,
 };
 
+pub use component::{example_group, example_group_with_title, single_example, ComponentPreview};
+pub use ui_macros::IntoComponent;
+
 pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
 pub use crate::traits::clickable::*;
-pub use crate::traits::component_preview::*;
 pub use crate::traits::disableable::*;
 pub use crate::traits::fixed::*;
 pub use crate::traits::styled_ext::*;

crates/ui/src/styles/typography.rs πŸ”—

@@ -1,5 +1,7 @@
+use crate::prelude::*;
 use gpui::{
-    div, rems, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window,
+    div, rems, AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled,
+    Window,
 };
 use settings::Settings;
 use theme::{ActiveTheme, ThemeSettings};
@@ -188,7 +190,7 @@ impl HeadlineSize {
 
 /// A headline element, used to emphasize some text and
 /// create a visual hierarchy.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Headline {
     size: HeadlineSize,
     text: SharedString,
@@ -230,3 +232,44 @@ impl Headline {
         self
     }
 }
+
+impl ComponentPreview for Headline {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Headline Sizes",
+                vec![
+                    single_example(
+                        "XLarge",
+                        Headline::new("XLarge Headline")
+                            .size(HeadlineSize::XLarge)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Large",
+                        Headline::new("Large Headline")
+                            .size(HeadlineSize::Large)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Medium (Default)",
+                        Headline::new("Medium Headline").into_any_element(),
+                    ),
+                    single_example(
+                        "Small",
+                        Headline::new("Small Headline")
+                            .size(HeadlineSize::Small)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "XSmall",
+                        Headline::new("XSmall Headline")
+                            .size(HeadlineSize::XSmall)
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
+    }
+}

crates/ui/src/traits/component_preview.rs πŸ”—

@@ -1,205 +0,0 @@
-#![allow(missing_docs)]
-use crate::{prelude::*, KeyBinding};
-use gpui::{AnyElement, SharedString};
-
-/// Which side of the preview to show labels on
-#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ExampleLabelSide {
-    /// Left side
-    Left,
-    /// Right side
-    Right,
-    #[default]
-    /// Top side
-    Top,
-    /// Bottom side
-    Bottom,
-}
-
-/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
-pub trait ComponentPreview: IntoElement {
-    fn title() -> &'static str {
-        std::any::type_name::<Self>()
-    }
-
-    fn description() -> impl Into<Option<&'static str>> {
-        None
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::default()
-    }
-
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>>;
-
-    fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into<Option<AnyElement>> {
-        None::<AnyElement>
-    }
-
-    fn component_previews(window: &mut Window, cx: &mut App) -> Vec<AnyElement> {
-        Self::examples(window, cx)
-            .into_iter()
-            .map(|example| Self::render_example_group(example))
-            .collect()
-    }
-
-    fn render_component_previews(window: &mut Window, cx: &mut App) -> AnyElement {
-        let title = Self::title();
-        let (source, title) = title
-            .rsplit_once("::")
-            .map_or((None, title), |(s, t)| (Some(s), t));
-        let description = Self::description().into();
-
-        v_flex()
-            .w_full()
-            .gap_6()
-            .p_4()
-            .border_1()
-            .border_color(cx.theme().colors().border)
-            .rounded_md()
-            .child(
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .gap_1()
-                            .child(Headline::new(title).size(HeadlineSize::Small))
-                            .when_some(source, |this, source| {
-                                this.child(Label::new(format!("({})", source)).color(Color::Muted))
-                            }),
-                    )
-                    .when_some(description, |this, description| {
-                        this.child(
-                            div()
-                                .text_ui_sm(cx)
-                                .text_color(cx.theme().colors().text_muted)
-                                .max_w(px(600.0))
-                                .child(description),
-                        )
-                    }),
-            )
-            .when_some(
-                Self::custom_example(window, cx).into(),
-                |this, custom_example| this.child(custom_example),
-            )
-            .children(Self::component_previews(window, cx))
-            .into_any_element()
-    }
-
-    fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
-        v_flex()
-            .gap_6()
-            .when(group.grow, |this| this.w_full().flex_1())
-            .when_some(group.title, |this, title| {
-                this.child(Label::new(title).size(LabelSize::Small))
-            })
-            .child(
-                h_flex()
-                    .w_full()
-                    .gap_6()
-                    .children(group.examples.into_iter().map(Self::render_example))
-                    .into_any_element(),
-            )
-            .into_any_element()
-    }
-
-    fn render_example(example: ComponentExample<Self>) -> AnyElement {
-        let base = div().flex();
-
-        let base = match Self::example_label_side() {
-            ExampleLabelSide::Right => base.flex_row(),
-            ExampleLabelSide::Left => base.flex_row_reverse(),
-            ExampleLabelSide::Bottom => base.flex_col(),
-            ExampleLabelSide::Top => base.flex_col_reverse(),
-        };
-
-        base.gap_1()
-            .when(example.grow, |this| this.flex_1())
-            .child(example.element)
-            .child(
-                Label::new(example.variant_name)
-                    .size(LabelSize::XSmall)
-                    .color(Color::Muted),
-            )
-            .into_any_element()
-    }
-}
-
-/// A single example of a component.
-pub struct ComponentExample<T> {
-    variant_name: SharedString,
-    element: T,
-    grow: bool,
-}
-
-impl<T> ComponentExample<T> {
-    /// Create a new example with the given variant name and example value.
-    pub fn new(variant_name: impl Into<SharedString>, example: T) -> Self {
-        Self {
-            variant_name: variant_name.into(),
-            element: example,
-            grow: false,
-        }
-    }
-
-    /// Set the example to grow to fill the available horizontal space.
-    pub fn grow(mut self) -> Self {
-        self.grow = true;
-        self
-    }
-}
-
-/// A group of component examples.
-pub struct ComponentExampleGroup<T> {
-    pub title: Option<SharedString>,
-    pub examples: Vec<ComponentExample<T>>,
-    pub grow: bool,
-}
-
-impl<T> ComponentExampleGroup<T> {
-    /// Create a new group of examples with the given title.
-    pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
-        Self {
-            title: None,
-            examples,
-            grow: false,
-        }
-    }
-
-    /// Create a new group of examples with the given title.
-    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
-        Self {
-            title: Some(title.into()),
-            examples,
-            grow: false,
-        }
-    }
-
-    /// Set the group to grow to fill the available horizontal space.
-    pub fn grow(mut self) -> Self {
-        self.grow = true;
-        self
-    }
-}
-
-/// Create a single example
-pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
-    ComponentExample::new(variant_name, example)
-}
-
-/// Create a group of examples without a title
-pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
-    ComponentExampleGroup::new(examples)
-}
-
-/// Create a group of examples with a title
-pub fn example_group_with_title<T>(
-    title: impl Into<SharedString>,
-    examples: Vec<ComponentExample<T>>,
-) -> ComponentExampleGroup<T> {
-    ComponentExampleGroup::with_title(title, examples)
-}
-
-pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding {
-    KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None))
-}

crates/ui_macros/Cargo.toml πŸ”—

@@ -13,7 +13,8 @@ path = "src/ui_macros.rs"
 proc-macro = true
 
 [dependencies]
+convert_case.workspace = true
+linkme.workspace = true
 proc-macro2.workspace = true
 quote.workspace = true
 syn.workspace = true
-convert_case.workspace = true

crates/ui_macros/src/derive_component.rs πŸ”—

@@ -0,0 +1,97 @@
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput, Lit, Meta, MetaList, MetaNameValue, NestedMeta};
+
+pub fn derive_into_component(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let mut scope_val = None;
+    let mut description_val = None;
+
+    for attr in &input.attrs {
+        if attr.path.is_ident("component") {
+            if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
+                for item in nested {
+                    if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
+                        path,
+                        lit: Lit::Str(s),
+                        ..
+                    })) = item
+                    {
+                        let ident = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
+                        if ident == "scope" {
+                            scope_val = Some(s.value());
+                        } else if ident == "description" {
+                            description_val = Some(s.value());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    let name = &input.ident;
+
+    let scope_impl = if let Some(s) = scope_val {
+        quote! {
+            fn scope() -> Option<&'static str> {
+                Some(#s)
+            }
+        }
+    } else {
+        quote! {
+            fn scope() -> Option<&'static str> {
+                None
+            }
+        }
+    };
+
+    let description_impl = if let Some(desc) = description_val {
+        quote! {
+            fn description() -> Option<&'static str> {
+                Some(#desc)
+            }
+        }
+    } else {
+        quote! {}
+    };
+
+    let register_component_name = syn::Ident::new(
+        &format!(
+            "__register_component_{}",
+            Casing::to_case(&name.to_string(), Case::Snake)
+        ),
+        name.span(),
+    );
+    let register_preview_name = syn::Ident::new(
+        &format!(
+            "__register_preview_{}",
+            Casing::to_case(&name.to_string(), Case::Snake)
+        ),
+        name.span(),
+    );
+
+    let expanded = quote! {
+        impl component::Component for #name {
+            #scope_impl
+
+            fn name() -> &'static str {
+                stringify!(#name)
+            }
+
+            #description_impl
+        }
+
+        #[linkme::distributed_slice(component::__ALL_COMPONENTS)]
+        fn #register_component_name() {
+            component::register_component::<#name>();
+        }
+
+        #[linkme::distributed_slice(component::__ALL_PREVIEWS)]
+        fn #register_preview_name() {
+            component::register_preview::<#name>();
+        }
+    };
+
+    expanded.into()
+}

crates/ui_macros/src/ui_macros.rs πŸ”—

@@ -1,3 +1,4 @@
+mod derive_component;
 mod derive_path_str;
 mod dynamic_spacing;
 
@@ -58,3 +59,27 @@ pub fn path_str(_args: TokenStream, input: TokenStream) -> TokenStream {
 pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream {
     dynamic_spacing::derive_spacing(input)
 }
+
+/// Derives the `Component` trait for a struct.
+///
+/// This macro generates implementations for the `Component` trait and associated
+/// registration functions for the component system.
+///
+/// # Attributes
+///
+/// - `#[component(scope = "...")]`: Required. Specifies the scope of the component.
+/// - `#[component(description = "...")]`: Optional. Provides a description for the component.
+///
+/// # Example
+///
+/// ```
+/// use ui_macros::Component;
+///
+/// #[derive(Component)]
+/// #[component(scope = "toggle", description = "A element that can be toggled on and off")]
+/// struct Checkbox;
+/// ```
+#[proc_macro_derive(IntoComponent, attributes(component))]
+pub fn derive_component(input: TokenStream) -> TokenStream {
+    derive_component::derive_into_component(input)
+}

crates/workspace/Cargo.toml πŸ”—

@@ -34,6 +34,7 @@ call.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
+component.workspace = true
 db.workspace = true
 derive_more.workspace = true
 fs.workspace = true

crates/workspace/src/theme_preview.rs πŸ”—

@@ -27,7 +27,6 @@ pub fn init(cx: &mut App) {
 enum ThemePreviewPage {
     Overview,
     Typography,
-    Components,
 }
 
 impl ThemePreviewPage {
@@ -35,7 +34,6 @@ impl ThemePreviewPage {
         match self {
             Self::Overview => "Overview",
             Self::Typography => "Typography",
-            Self::Components => "Components",
         }
     }
 }
@@ -64,9 +62,6 @@ impl ThemePreview {
             ThemePreviewPage::Typography => {
                 self.render_typography_page(window, cx).into_any_element()
             }
-            ThemePreviewPage::Components => {
-                self.render_components_page(window, cx).into_any_element()
-            }
         }
     }
 }
@@ -392,28 +387,6 @@ impl ThemePreview {
             )
     }
 
-    fn render_components_page(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let layer = ElevationIndex::Surface;
-
-        v_flex()
-            .id("theme-preview-components")
-            .overflow_scroll()
-            .size_full()
-            .gap_2()
-            .child(Button::render_component_previews(window, cx))
-            .child(Checkbox::render_component_previews(window, cx))
-            .child(CheckboxWithLabel::render_component_previews(window, cx))
-            .child(ContentGroup::render_component_previews(window, cx))
-            .child(DecoratedIcon::render_component_previews(window, cx))
-            .child(Facepile::render_component_previews(window, cx))
-            .child(Icon::render_component_previews(window, cx))
-            .child(IconDecoration::render_component_previews(window, cx))
-            .child(KeybindingHint::render_component_previews(window, cx))
-            .child(Indicator::render_component_previews(window, cx))
-            .child(Switch::render_component_previews(window, cx))
-            .child(Table::render_component_previews(window, cx))
-    }
-
     fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         h_flex()
             .id("theme-preview-nav")

crates/workspace/src/workspace.rs πŸ”—

@@ -148,6 +148,7 @@ actions!(
         Open,
         OpenFiles,
         OpenInTerminal,
+        OpenComponentPreview,
         ReloadActiveItem,
         SaveAs,
         SaveWithoutFormat,
@@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
 
 pub fn init(app_state: Arc<AppState>, cx: &mut App) {
     init_settings(cx);
+    component::init();
     theme_preview::init(cx);
 
     cx.on_action(Workspace::close_global);

crates/zed/Cargo.toml πŸ”—

@@ -39,6 +39,7 @@ collab_ui.workspace = true
 collections.workspace = true
 command_palette.workspace = true
 command_palette_hooks.workspace = true
+component_preview.workspace = true
 copilot.workspace = true
 db.workspace = true
 diagnostics.workspace = true
@@ -54,8 +55,8 @@ file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true
-git_ui.workspace = true
 git_hosting_providers.workspace = true
+git_ui.workspace = true
 go_to_line.workspace = true
 gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
 gpui_tokio.workspace = true

crates/zed/src/main.rs πŸ”—

@@ -490,6 +490,7 @@ fn main() {
         project_panel::init(Assets, cx);
         git_ui::git_panel::init(cx);
         outline_panel::init(Assets, cx);
+        component_preview::init(cx);
         tasks_ui::init(cx);
         snippets_ui::init(cx);
         channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);