component: Component crate cleanup (#29967)

Nate Butler created

This PR further organizes and documents the component crate. It:

- Simplifies the component registry
- Gives access to `ComponentMetadata` sooner
- Enables lookup by id in preview extension implementations
(`ComponentId` -> `ComponentMetadata`)
- Should slightly improve the performance of ComponentPreview

It also brings component statuses to the Component trait:

![CleanShot 2025-05-05 at 23 27
11@2x](https://github.com/user-attachments/assets/dd95ede6-bc90-4de4-90c6-3e5e064fd676)

![CleanShot 2025-05-05 at 23 27
40@2x](https://github.com/user-attachments/assets/9520aece-04c2-418b-95e1-c11aa60a66ca)

![CleanShot 2025-05-05 at 23 27
57@2x](https://github.com/user-attachments/assets/db1713d5-9831-4d00-9b29-1fd51c25fcba)

Release Notes:

- N/A

Change summary

Cargo.lock                                           |   1 
crates/component/Cargo.toml                          |   1 
crates/component/src/component.rs                    | 580 +++++--------
crates/component/src/component_layout.rs             | 205 ++++
crates/component_preview/src/component_preview.rs    | 274 +++--
crates/ui/src/component_prelude.rs                   |   3 
crates/ui/src/components/notification/alert_modal.rs |   5 
7 files changed, 598 insertions(+), 471 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3237,6 +3237,7 @@ dependencies = [
  "gpui",
  "linkme",
  "parking_lot",
+ "strum 0.27.1",
  "theme",
  "workspace-hack",
 ]

crates/component/Cargo.toml 🔗

@@ -16,6 +16,7 @@ collections.workspace = true
 gpui.workspace = true
 linkme.workspace = true
 parking_lot.workspace = true
+strum.workspace = true
 theme.workspace = true
 workspace-hack.workspace = true
 

crates/component/src/component.rs 🔗

@@ -1,90 +1,98 @@
-use std::fmt::Display;
-use std::ops::{Deref, DerefMut};
+//! # Component
+//!
+//! This module provides the Component trait, which is used to define
+//! components for visual testing and debugging.
+//!
+//! Additionally, it includes layouts for rendering component examples
+//! and example groups, as well as the distributed slice mechanism for
+//! registering components.
+
+mod component_layout;
+
+pub use component_layout::*;
+
 use std::sync::LazyLock;
 
 use collections::HashMap;
-use gpui::{
-    AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
-    prelude::*, px, rems,
-};
+use gpui::{AnyElement, App, SharedString, Window};
 use linkme::distributed_slice;
 use parking_lot::RwLock;
-use theme::ActiveTheme;
+use strum::{Display, EnumString};
 
-pub trait Component {
-    fn scope() -> ComponentScope {
-        ComponentScope::None
-    }
-    fn name() -> &'static str {
-        std::any::type_name::<Self>()
-    }
-    fn id() -> ComponentId {
-        ComponentId(Self::name())
-    }
-    /// Returns a name that the component should be sorted by.
-    ///
-    /// Implement this if the component should be sorted in an alternate order than its name.
-    ///
-    /// Example:
-    ///
-    /// For example, to group related components together when sorted:
-    ///
-    /// - Button      -> ButtonA
-    /// - IconButton  -> ButtonBIcon
-    /// - ToggleButton -> ButtonCToggle
-    ///
-    /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
-    fn sort_name() -> &'static str {
-        Self::name()
-    }
-    fn description() -> Option<&'static str> {
-        None
-    }
-    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
-        None
+pub fn components() -> ComponentRegistry {
+    COMPONENT_DATA.read().clone()
+}
+
+pub fn init() {
+    let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
+    for f in component_fns {
+        f();
     }
 }
 
+pub fn register_component<T: Component>() {
+    let id = T::id();
+    let metadata = ComponentMetadata {
+        id: id.clone(),
+        description: T::description().map(Into::into),
+        name: SharedString::new_static(T::name()),
+        preview: Some(T::preview),
+        scope: T::scope(),
+        sort_name: SharedString::new_static(T::sort_name()),
+        status: T::status(),
+    };
+
+    let mut data = COMPONENT_DATA.write();
+    data.components.insert(id, metadata);
+}
+
 #[distributed_slice]
 pub static __ALL_COMPONENTS: [fn()] = [..];
 
 pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
-    LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
+    LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
 
+#[derive(Default, Clone)]
 pub struct ComponentRegistry {
-    components: Vec<(
-        ComponentScope,
-        // name
-        &'static str,
-        // sort name
-        &'static str,
-        // description
-        Option<&'static str>,
-    )>,
-    previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
+    components: HashMap<ComponentId, ComponentMetadata>,
 }
 
 impl ComponentRegistry {
-    fn new() -> Self {
-        ComponentRegistry {
-            components: Vec::new(),
-            previews: HashMap::default(),
-        }
+    pub fn previews(&self) -> Vec<&ComponentMetadata> {
+        self.components
+            .values()
+            .filter(|c| c.preview.is_some())
+            .collect()
     }
-}
 
-pub fn init() {
-    let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
-    for f in component_fns {
-        f();
+    pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
+        let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
+        previews.sort_by_key(|a| a.name());
+        previews
     }
-}
 
-pub fn register_component<T: Component>() {
-    let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
-    let mut data = COMPONENT_DATA.write();
-    data.components.push(component_data);
-    data.previews.insert(T::id().0, T::preview);
+    pub fn components(&self) -> Vec<&ComponentMetadata> {
+        self.components.values().collect()
+    }
+
+    pub fn sorted_components(&self) -> Vec<ComponentMetadata> {
+        let mut components: Vec<ComponentMetadata> =
+            self.components().into_iter().cloned().collect();
+        components.sort_by_key(|a| a.name());
+        components
+    }
+
+    pub fn component_map(&self) -> HashMap<ComponentId, ComponentMetadata> {
+        self.components.clone()
+    }
+
+    pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
+        self.components.get(id)
+    }
+
+    pub fn len(&self) -> usize {
+        self.components.len()
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -93,21 +101,35 @@ pub struct ComponentId(pub &'static str);
 #[derive(Clone)]
 pub struct ComponentMetadata {
     id: ComponentId,
-    name: SharedString,
-    sort_name: SharedString,
-    scope: ComponentScope,
     description: Option<SharedString>,
+    name: SharedString,
     preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
+    scope: ComponentScope,
+    sort_name: SharedString,
+    status: ComponentStatus,
 }
 
 impl ComponentMetadata {
     pub fn id(&self) -> ComponentId {
         self.id.clone()
     }
+
+    pub fn description(&self) -> Option<SharedString> {
+        self.description.clone()
+    }
+
     pub fn name(&self) -> SharedString {
         self.name.clone()
     }
 
+    pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
+        self.preview
+    }
+
+    pub fn scope(&self) -> ComponentScope {
+        self.scope.clone()
+    }
+
     pub fn sort_name(&self) -> SharedString {
         self.sort_name.clone()
     }
@@ -122,323 +144,171 @@ impl ComponentMetadata {
             .into()
     }
 
-    pub fn scope(&self) -> ComponentScope {
-        self.scope.clone()
-    }
-    pub fn description(&self) -> Option<SharedString> {
-        self.description.clone()
-    }
-    pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
-        self.preview
+    pub fn status(&self) -> ComponentStatus {
+        self.status.clone()
     }
 }
 
-pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
-
-impl AllComponents {
-    pub fn new() -> Self {
-        AllComponents(HashMap::default())
+/// Implement this trait to define a UI component. This will allow you to
+/// derive `RegisterComponent` on it, in tutn allowing you to preview the
+/// contents of the preview fn in `workspace: open component preview`.
+///
+/// This can be useful for visual debugging and testing, documenting UI
+/// patterns, or simply showing all the variants of a component.
+///
+/// Generally you will want to implement at least `scope` and `preview`
+/// from this trait, so you can preview the component, and it will show up
+/// in a section that makes sense.
+pub trait Component {
+    /// The component's unique identifier.
+    ///
+    /// Used to access previews, or state for more
+    /// complex, stateful components.
+    fn id() -> ComponentId {
+        ComponentId(Self::name())
     }
-    pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
-        self.0.values().filter(|c| c.preview.is_some()).collect()
+    /// Returns the scope of the component.
+    ///
+    /// This scope is used to determine how components and
+    /// their previews are displayed and organized.
+    fn scope() -> ComponentScope {
+        ComponentScope::None
     }
-    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
+    /// The ready status of this component.
+    ///
+    /// Use this to mark when components are:
+    /// - `WorkInProgress`: Still being designed or are partially implemented.
+    /// - `EngineeringReady`: Ready to be implemented.
+    /// - `Deprecated`: No longer recommended for use.
+    ///
+    /// Defaults to [`Live`](ComponentStatus::Live).
+    fn status() -> ComponentStatus {
+        ComponentStatus::Live
     }
-    pub fn all(&self) -> Vec<&ComponentMetadata> {
-        self.0.values().collect()
+    /// The name of the component.
+    ///
+    /// This name is used to identify the component
+    /// and is usually derived from the component's type.
+    fn name() -> &'static str {
+        std::any::type_name::<Self>()
     }
-    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
+    /// Returns a name that the component should be sorted by.
+    ///
+    /// Implement this if the component should be sorted in an alternate order than its name.
+    ///
+    /// Example:
+    ///
+    /// For example, to group related components together when sorted:
+    ///
+    /// - Button      -> ButtonA
+    /// - IconButton  -> ButtonBIcon
+    /// - ToggleButton -> ButtonCToggle
+    ///
+    /// This naming scheme keeps these components together and allows them to /// be sorted in a logical order.
+    fn sort_name() -> &'static str {
+        Self::name()
     }
-}
-
-impl Deref for AllComponents {
-    type Target = HashMap<ComponentId, ComponentMetadata>;
-    fn deref(&self) -> &Self::Target {
-        &self.0
+    /// An optional description of the component.
+    ///
+    /// This will be displayed in the component's preview. To show a
+    /// component's doc comment as it's description, derive `Documented`.
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// /// This is a doc comment.
+    /// #[derive(Documented)]
+    /// struct MyComponent;
+    ///
+    /// impl MyComponent {
+    ///     fn description() -> Option<&'static str> {
+    ///         Some(Self::DOCS)
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// This will result in "This is a doc comment." being passed
+    /// to the component's description.
+    fn description() -> Option<&'static str> {
+        None
+    }
+    /// The component's preview.
+    ///
+    /// An element returned here will be shown in the component's preview.
+    ///
+    /// Useful component helpers:
+    /// - [`component::single_example`]
+    /// - [`component::component_group`]
+    /// - [`component::component_group_with_title`]
+    ///
+    /// Note: Any arbitrary element can be returned here.
+    ///
+    /// This is useful for displaying related UI to the component you are
+    /// trying to preview, such as a button that opens a modal or shows a
+    /// tooltip on hover, or a grid of icons showcasing all the icons available.
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        None
     }
 }
 
-impl DerefMut for AllComponents {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
-    }
+/// The ready status of this component.
+///
+/// Use this to mark when components are:
+/// - `WorkInProgress`: Still being designed or are partially implemented.
+/// - `EngineeringReady`: Ready to be implemented.
+/// - `Deprecated`: No longer recommended for use.
+///
+/// Defaults to [`Live`](ComponentStatus::Live).
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
+pub enum ComponentStatus {
+    #[strum(serialize = "Work In Progress")]
+    WorkInProgress,
+    #[strum(serialize = "Ready To Build")]
+    EngineeringReady,
+    Live,
+    Deprecated,
 }
 
-pub fn components() -> AllComponents {
-    let data = COMPONENT_DATA.read();
-    let mut all_components = AllComponents::new();
-    for (scope, name, sort_name, description) in &data.components {
-        let preview = data.previews.get(name).cloned();
-        let component_name = SharedString::new_static(name);
-        let sort_name = SharedString::new_static(sort_name);
-        let id = ComponentId(name);
-        all_components.insert(
-            id.clone(),
-            ComponentMetadata {
-                id,
-                name: component_name,
-                sort_name,
-                scope: scope.clone(),
-                description: description.map(Into::into),
-                preview,
-            },
-        );
+impl ComponentStatus {
+    pub fn description(&self) -> &str {
+        match self {
+            ComponentStatus::WorkInProgress => {
+                "These components are still being designed or refined. They shouldn't be used in the app yet."
+            }
+            ComponentStatus::EngineeringReady => {
+                "These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
+            }
+            ComponentStatus::Live => "These components are ready for use in the app.",
+            ComponentStatus::Deprecated => {
+                "These components are no longer recommended for use in the app, and may be removed in a future release."
+            }
+        }
     }
-    all_components
 }
 
-// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-// pub enum ComponentStatus {
-//     WorkInProgress,
-//     EngineeringReady,
-//     Live,
-//     Deprecated,
-// }
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
 pub enum ComponentScope {
     Agent,
     Collaboration,
+    #[strum(serialize = "Data Display")]
     DataDisplay,
     Editor,
+    #[strum(serialize = "Images & Icons")]
     Images,
+    #[strum(serialize = "Forms & Input")]
     Input,
+    #[strum(serialize = "Layout & Structure")]
     Layout,
+    #[strum(serialize = "Loading & Progress")]
     Loading,
     Navigation,
+    #[strum(serialize = "Unsorted")]
     None,
     Notification,
+    #[strum(serialize = "Overlays & Layering")]
     Overlays,
     Status,
     Typography,
+    #[strum(serialize = "Version Control")]
     VersionControl,
 }
-
-impl Display for ComponentScope {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            ComponentScope::Agent => write!(f, "Agent"),
-            ComponentScope::Collaboration => write!(f, "Collaboration"),
-            ComponentScope::DataDisplay => write!(f, "Data Display"),
-            ComponentScope::Editor => write!(f, "Editor"),
-            ComponentScope::Images => write!(f, "Images & Icons"),
-            ComponentScope::Input => write!(f, "Forms & Input"),
-            ComponentScope::Layout => write!(f, "Layout & Structure"),
-            ComponentScope::Loading => write!(f, "Loading & Progress"),
-            ComponentScope::Navigation => write!(f, "Navigation"),
-            ComponentScope::None => write!(f, "Unsorted"),
-            ComponentScope::Notification => write!(f, "Notification"),
-            ComponentScope::Overlays => write!(f, "Overlays & Layering"),
-            ComponentScope::Status => write!(f, "Status"),
-            ComponentScope::Typography => write!(f, "Typography"),
-            ComponentScope::VersionControl => write!(f, "Version Control"),
-        }
-    }
-}
-
-/// A single example of a component.
-#[derive(IntoElement)]
-pub struct ComponentExample {
-    pub variant_name: SharedString,
-    pub description: Option<SharedString>,
-    pub element: AnyElement,
-    pub width: Option<Pixels>,
-}
-
-impl RenderOnce for ComponentExample {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        div()
-            .pt_2()
-            .map(|this| {
-                if let Some(width) = self.width {
-                    this.w(width)
-                } else {
-                    this.w_full()
-                }
-            })
-            .flex()
-            .flex_col()
-            .gap_3()
-            .child(
-                div()
-                    .flex()
-                    .flex_col()
-                    .child(
-                        div()
-                            .child(self.variant_name.clone())
-                            .text_size(rems(1.0))
-                            .text_color(cx.theme().colors().text),
-                    )
-                    .when_some(self.description, |this, description| {
-                        this.child(
-                            div()
-                                .text_size(rems(0.875))
-                                .text_color(cx.theme().colors().text_muted)
-                                .child(description.clone()),
-                        )
-                    }),
-            )
-            .child(
-                div()
-                    .flex()
-                    .w_full()
-                    .rounded_xl()
-                    .min_h(px(100.))
-                    .justify_center()
-                    .p_8()
-                    .border_1()
-                    .border_color(cx.theme().colors().border.opacity(0.5))
-                    .bg(pattern_slash(
-                        cx.theme().colors().surface_background.opacity(0.5),
-                        12.0,
-                        12.0,
-                    ))
-                    .shadow_sm()
-                    .child(self.element),
-            )
-            .into_any_element()
-    }
-}
-
-impl ComponentExample {
-    pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
-        Self {
-            variant_name: variant_name.into(),
-            element,
-            description: None,
-            width: None,
-        }
-    }
-
-    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
-        self.description = Some(description.into());
-        self
-    }
-
-    pub fn width(mut self, width: Pixels) -> Self {
-        self.width = Some(width);
-        self
-    }
-}
-
-/// A group of component examples.
-#[derive(IntoElement)]
-pub struct ComponentExampleGroup {
-    pub title: Option<SharedString>,
-    pub examples: Vec<ComponentExample>,
-    pub width: Option<Pixels>,
-    pub grow: bool,
-    pub vertical: 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)
-            .map(|this| {
-                if let Some(width) = self.width {
-                    this.w(width)
-                } else {
-                    this.w_full()
-                }
-            })
-            .when_some(self.title, |this, title| {
-                this.gap_4().child(
-                    div()
-                        .flex()
-                        .items_center()
-                        .gap_3()
-                        .pb_1()
-                        .child(div().h_px().w_4().bg(cx.theme().colors().border))
-                        .child(
-                            div()
-                                .flex_none()
-                                .text_size(px(10.))
-                                .child(title.to_uppercase()),
-                        )
-                        .child(
-                            div()
-                                .h_px()
-                                .w_full()
-                                .flex_1()
-                                .bg(cx.theme().colors().border),
-                        ),
-                )
-            })
-            .child(
-                div()
-                    .flex()
-                    .flex_col()
-                    .items_start()
-                    .w_full()
-                    .gap_6()
-                    .children(self.examples)
-                    .into_any_element(),
-            )
-            .into_any_element()
-    }
-}
-
-impl ComponentExampleGroup {
-    pub fn new(examples: Vec<ComponentExample>) -> Self {
-        Self {
-            title: None,
-            examples,
-            width: None,
-            grow: false,
-            vertical: false,
-        }
-    }
-    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
-        Self {
-            title: Some(title.into()),
-            examples,
-            width: None,
-            grow: false,
-            vertical: false,
-        }
-    }
-    pub fn width(mut self, width: Pixels) -> Self {
-        self.width = Some(width);
-        self
-    }
-    pub fn grow(mut self) -> Self {
-        self.grow = true;
-        self
-    }
-    pub fn vertical(mut self) -> Self {
-        self.vertical = true;
-        self
-    }
-}
-
-pub fn single_example(
-    variant_name: impl Into<SharedString>,
-    example: AnyElement,
-) -> ComponentExample {
-    ComponentExample::new(variant_name, example)
-}
-
-pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
-    ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
-}
-
-pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
-    ComponentExampleGroup::new(examples)
-}
-
-pub fn example_group_with_title(
-    title: impl Into<SharedString>,
-    examples: Vec<ComponentExample>,
-) -> ComponentExampleGroup {
-    ComponentExampleGroup::with_title(title, examples)
-}

crates/component/src/component_layout.rs 🔗

@@ -0,0 +1,205 @@
+use gpui::{
+    AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
+    prelude::*, px, rems,
+};
+use theme::ActiveTheme;
+
+/// A single example of a component.
+#[derive(IntoElement)]
+pub struct ComponentExample {
+    pub variant_name: SharedString,
+    pub description: Option<SharedString>,
+    pub element: AnyElement,
+    pub width: Option<Pixels>,
+}
+
+impl RenderOnce for ComponentExample {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        div()
+            .pt_2()
+            .map(|this| {
+                if let Some(width) = self.width {
+                    this.w(width)
+                } else {
+                    this.w_full()
+                }
+            })
+            .flex()
+            .flex_col()
+            .gap_3()
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .child(
+                        div()
+                            .child(self.variant_name.clone())
+                            .text_size(rems(1.0))
+                            .text_color(cx.theme().colors().text),
+                    )
+                    .when_some(self.description, |this, description| {
+                        this.child(
+                            div()
+                                .text_size(rems(0.875))
+                                .text_color(cx.theme().colors().text_muted)
+                                .child(description.clone()),
+                        )
+                    }),
+            )
+            .child(
+                div()
+                    .flex()
+                    .w_full()
+                    .rounded_xl()
+                    .min_h(px(100.))
+                    .justify_center()
+                    .p_8()
+                    .border_1()
+                    .border_color(cx.theme().colors().border.opacity(0.5))
+                    .bg(pattern_slash(
+                        cx.theme().colors().surface_background.opacity(0.5),
+                        12.0,
+                        12.0,
+                    ))
+                    .shadow_sm()
+                    .child(self.element),
+            )
+            .into_any_element()
+    }
+}
+
+impl ComponentExample {
+    pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
+        Self {
+            variant_name: variant_name.into(),
+            element,
+            description: None,
+            width: None,
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    pub fn width(mut self, width: Pixels) -> Self {
+        self.width = Some(width);
+        self
+    }
+}
+
+/// A group of component examples.
+#[derive(IntoElement)]
+pub struct ComponentExampleGroup {
+    pub title: Option<SharedString>,
+    pub examples: Vec<ComponentExample>,
+    pub width: Option<Pixels>,
+    pub grow: bool,
+    pub vertical: 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)
+            .map(|this| {
+                if let Some(width) = self.width {
+                    this.w(width)
+                } else {
+                    this.w_full()
+                }
+            })
+            .when_some(self.title, |this, title| {
+                this.gap_4().child(
+                    div()
+                        .flex()
+                        .items_center()
+                        .gap_3()
+                        .pb_1()
+                        .child(div().h_px().w_4().bg(cx.theme().colors().border))
+                        .child(
+                            div()
+                                .flex_none()
+                                .text_size(px(10.))
+                                .child(title.to_uppercase()),
+                        )
+                        .child(
+                            div()
+                                .h_px()
+                                .w_full()
+                                .flex_1()
+                                .bg(cx.theme().colors().border),
+                        ),
+                )
+            })
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .items_start()
+                    .w_full()
+                    .gap_6()
+                    .children(self.examples)
+                    .into_any_element(),
+            )
+            .into_any_element()
+    }
+}
+
+impl ComponentExampleGroup {
+    pub fn new(examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: None,
+            examples,
+            width: None,
+            grow: false,
+            vertical: false,
+        }
+    }
+    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: Some(title.into()),
+            examples,
+            width: None,
+            grow: false,
+            vertical: false,
+        }
+    }
+    pub fn width(mut self, width: Pixels) -> Self {
+        self.width = Some(width);
+        self
+    }
+    pub fn grow(mut self) -> Self {
+        self.grow = true;
+        self
+    }
+    pub fn vertical(mut self) -> Self {
+        self.vertical = true;
+        self
+    }
+}
+
+pub fn single_example(
+    variant_name: impl Into<SharedString>,
+    example: AnyElement,
+) -> ComponentExample {
+    ComponentExample::new(variant_name, example)
+}
+
+pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
+    ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
+}
+
+pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
+    ComponentExampleGroup::new(examples)
+}
+
+pub fn example_group_with_title(
+    title: impl Into<SharedString>,
+    examples: Vec<ComponentExample>,
+) -> ComponentExampleGroup {
+    ComponentExampleGroup::with_title(title, examples)
+}

crates/component_preview/src/component_preview.rs 🔗

@@ -5,12 +5,13 @@
 mod persistence;
 mod preview_support;
 
-use std::iter::Iterator;
 use std::sync::Arc;
 
+use std::iter::Iterator;
+
 use agent::{ActiveThread, TextThreadStore, ThreadStore};
 use client::UserStore;
-use component::{ComponentId, ComponentMetadata, components};
+use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
 use gpui::{
     App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
 };
@@ -25,7 +26,7 @@ use preview_support::active_thread::{
     load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
 };
 use project::Project;
-use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
+use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
 use ui_input::SingleLineInput;
 use util::ResultExt as _;
 use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
@@ -104,26 +105,24 @@ enum PreviewPage {
 }
 
 struct ComponentPreview {
-    workspace_id: Option<WorkspaceId>,
-    focus_handle: FocusHandle,
-    _view_scroll_handle: ScrollHandle,
-    nav_scroll_handle: UniformListScrollHandle,
-    component_map: HashMap<ComponentId, ComponentMetadata>,
     active_page: PreviewPage,
-    components: Vec<ComponentMetadata>,
+    active_thread: Option<Entity<ActiveThread>>,
     component_list: ListState,
+    component_map: HashMap<ComponentId, ComponentMetadata>,
+    components: Vec<ComponentMetadata>,
     cursor_index: usize,
-    language_registry: Arc<LanguageRegistry>,
-    workspace: WeakEntity<Workspace>,
-    project: Entity<Project>,
-    user_store: Entity<UserStore>,
     filter_editor: Entity<SingleLineInput>,
     filter_text: String,
-
-    // preview support
-    thread_store: Option<Entity<ThreadStore>>,
+    focus_handle: FocusHandle,
+    language_registry: Arc<LanguageRegistry>,
+    nav_scroll_handle: UniformListScrollHandle,
+    project: Entity<Project>,
     text_thread_store: Option<Entity<TextThreadStore>>,
-    active_thread: Option<Entity<ActiveThread>>,
+    thread_store: Option<Entity<ThreadStore>>,
+    user_store: Entity<UserStore>,
+    workspace: WeakEntity<Workspace>,
+    workspace_id: Option<WorkspaceId>,
+    _view_scroll_handle: ScrollHandle,
 }
 
 impl ComponentPreview {
@@ -164,7 +163,8 @@ impl ComponentPreview {
         })
         .detach();
 
-        let sorted_components = components().all_sorted();
+        let component_registry = Arc::new(components());
+        let sorted_components = component_registry.sorted_components();
         let selected_index = selected_index.into().unwrap_or(0);
         let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
         let filter_editor =
@@ -188,24 +188,24 @@ impl ComponentPreview {
         );
 
         let mut component_preview = Self {
-            workspace_id: None,
-            focus_handle: cx.focus_handle(),
-            _view_scroll_handle: ScrollHandle::new(),
-            nav_scroll_handle: UniformListScrollHandle::new(),
-            language_registry,
-            user_store,
-            workspace,
-            project,
             active_page,
-            component_map: components().0,
-            components: sorted_components,
+            active_thread: None,
             component_list,
+            component_map: component_registry.component_map(),
+            components: sorted_components,
             cursor_index: selected_index,
             filter_editor,
             filter_text: String::new(),
-            thread_store: None,
+            focus_handle: cx.focus_handle(),
+            language_registry,
+            nav_scroll_handle: UniformListScrollHandle::new(),
+            project,
             text_thread_store: None,
-            active_thread: None,
+            thread_store: None,
+            user_store,
+            workspace,
+            workspace_id: None,
+            _view_scroll_handle: ScrollHandle::new(),
         };
 
         if component_preview.cursor_index > 0 {
@@ -412,6 +412,88 @@ impl ComponentPreview {
         entries
     }
 
+    fn update_component_list(&mut self, cx: &mut Context<Self>) {
+        let entries = self.scope_ordered_entries();
+        let new_len = entries.len();
+        let weak_entity = cx.entity().downgrade();
+
+        if new_len > 0 {
+            self.nav_scroll_handle
+                .scroll_to_item(0, ScrollStrategy::Top);
+        }
+
+        let filtered_components = self.filtered_components();
+
+        if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
+            if let PreviewPage::Component(ref component_id) = self.active_page {
+                let component_still_visible = filtered_components
+                    .iter()
+                    .any(|component| component.id() == *component_id);
+
+                if !component_still_visible {
+                    if !filtered_components.is_empty() {
+                        let first_component = &filtered_components[0];
+                        self.set_active_page(PreviewPage::Component(first_component.id()), cx);
+                    } else {
+                        self.set_active_page(PreviewPage::AllComponents, cx);
+                    }
+                }
+            }
+        }
+
+        self.component_list = ListState::new(
+            filtered_components.len(),
+            gpui::ListAlignment::Top,
+            px(1500.0),
+            {
+                let components = filtered_components.clone();
+                let this = cx.entity().downgrade();
+                move |ix, window: &mut Window, cx: &mut App| {
+                    if ix >= components.len() {
+                        return div().w_full().h_0().into_any_element();
+                    }
+
+                    this.update(cx, |this, cx| {
+                        let component = &components[ix];
+                        this.render_preview(component, window, cx)
+                            .into_any_element()
+                    })
+                    .unwrap()
+                }
+            },
+        );
+
+        let new_list = ListState::new(
+            new_len,
+            gpui::ListAlignment::Top,
+            px(1500.0),
+            move |ix, window, cx| {
+                if ix >= entries.len() {
+                    return div().w_full().h_0().into_any_element();
+                }
+
+                let entry = &entries[ix];
+
+                weak_entity
+                    .update(cx, |this, cx| match entry {
+                        PreviewEntry::Component(component, _) => this
+                            .render_preview(component, window, cx)
+                            .into_any_element(),
+                        PreviewEntry::SectionHeader(shared_string) => this
+                            .render_scope_header(ix, shared_string.clone(), window, cx)
+                            .into_any_element(),
+                        PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
+                        PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
+                        PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
+                    })
+                    .unwrap()
+            },
+        );
+
+        self.component_list = new_list;
+        cx.emit(ItemEvent::UpdateTab);
+    }
+
     fn render_sidebar_entry(
         &self,
         ix: usize,
@@ -495,88 +577,6 @@ impl ComponentPreview {
         }
     }
 
-    fn update_component_list(&mut self, cx: &mut Context<Self>) {
-        let entries = self.scope_ordered_entries();
-        let new_len = entries.len();
-        let weak_entity = cx.entity().downgrade();
-
-        if new_len > 0 {
-            self.nav_scroll_handle
-                .scroll_to_item(0, ScrollStrategy::Top);
-        }
-
-        let filtered_components = self.filtered_components();
-
-        if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
-            if let PreviewPage::Component(ref component_id) = self.active_page {
-                let component_still_visible = filtered_components
-                    .iter()
-                    .any(|component| component.id() == *component_id);
-
-                if !component_still_visible {
-                    if !filtered_components.is_empty() {
-                        let first_component = &filtered_components[0];
-                        self.set_active_page(PreviewPage::Component(first_component.id()), cx);
-                    } else {
-                        self.set_active_page(PreviewPage::AllComponents, cx);
-                    }
-                }
-            }
-        }
-
-        self.component_list = ListState::new(
-            filtered_components.len(),
-            gpui::ListAlignment::Top,
-            px(1500.0),
-            {
-                let components = filtered_components.clone();
-                let this = cx.entity().downgrade();
-                move |ix, window: &mut Window, cx: &mut App| {
-                    if ix >= components.len() {
-                        return div().w_full().h_0().into_any_element();
-                    }
-
-                    this.update(cx, |this, cx| {
-                        let component = &components[ix];
-                        this.render_preview(component, window, cx)
-                            .into_any_element()
-                    })
-                    .unwrap()
-                }
-            },
-        );
-
-        let new_list = ListState::new(
-            new_len,
-            gpui::ListAlignment::Top,
-            px(1500.0),
-            move |ix, window, cx| {
-                if ix >= entries.len() {
-                    return div().w_full().h_0().into_any_element();
-                }
-
-                let entry = &entries[ix];
-
-                weak_entity
-                    .update(cx, |this, cx| match entry {
-                        PreviewEntry::Component(component, _) => this
-                            .render_preview(component, window, cx)
-                            .into_any_element(),
-                        PreviewEntry::SectionHeader(shared_string) => this
-                            .render_scope_header(ix, shared_string.clone(), window, cx)
-                            .into_any_element(),
-                        PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
-                        PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
-                        PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
-                    })
-                    .unwrap()
-            },
-        );
-
-        self.component_list = new_list;
-        cx.emit(ItemEvent::UpdateTab);
-    }
-
     fn render_scope_header(
         &self,
         _ix: usize,
@@ -695,7 +695,7 @@ impl ComponentPreview {
         if let Some(component) = component {
             v_flex()
                 .id("render-component-page")
-                .size_full()
+                .flex_1()
                 .child(ComponentPreviewPage::new(
                     component.clone(),
                     self.workspace.clone(),
@@ -971,7 +971,7 @@ impl SerializableItem for ComponentPreview {
         } else {
             let component_str = deserialized_active_page.0;
             let component_registry = components();
-            let all_components = component_registry.all();
+            let all_components = component_registry.components();
             let found_component = all_components.iter().find(|c| c.id().0 == component_str);
 
             if let Some(component) = found_component {
@@ -1065,6 +1065,43 @@ impl ComponentPreviewPage {
         }
     }
 
+    /// Renders the component status when it would be useful
+    ///
+    /// Doesn't render if the component is `ComponentStatus::Live`
+    /// as that is the default state
+    fn render_component_status(&self, cx: &App) -> Option<impl IntoElement> {
+        let status = self.component.status();
+        let status_description = status.description().to_string();
+
+        let color = match status {
+            ComponentStatus::Deprecated => Color::Error,
+            ComponentStatus::EngineeringReady => Color::Info,
+            ComponentStatus::Live => Color::Success,
+            ComponentStatus::WorkInProgress => Color::Warning,
+        };
+
+        if status != ComponentStatus::Live {
+            Some(
+                ButtonLike::new("component_status")
+                    .child(
+                        div()
+                            .px_1p5()
+                            .rounded_sm()
+                            .bg(color.color(cx).alpha(0.12))
+                            .child(
+                                Label::new(status.clone().to_string())
+                                    .size(LabelSize::Small)
+                                    .color(color),
+                            ),
+                    )
+                    .tooltip(Tooltip::text(status_description))
+                    .disabled(true),
+            )
+        } else {
+            None
+        }
+    }
+
     fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
         v_flex()
             .px_12()
@@ -1083,7 +1120,14 @@ impl ComponentPreviewPage {
                             .color(Color::Muted),
                     )
                     .child(
-                        Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
+                        h_flex()
+                            .items_center()
+                            .gap_2()
+                            .child(
+                                Headline::new(self.component.scopeless_name())
+                                    .size(HeadlineSize::XLarge),
+                            )
+                            .children(self.render_component_status(cx)),
                     ),
             )
             .when_some(self.component.description(), |this, description| {

crates/ui/src/component_prelude.rs 🔗

@@ -1,5 +1,6 @@
 pub use component::{
-    Component, ComponentScope, example_group, example_group_with_title, single_example,
+    Component, ComponentId, ComponentScope, ComponentStatus, example_group,
+    example_group_with_title, single_example,
 };
 pub use documented::Documented;
 pub use ui_macros::RegisterComponent;

crates/ui/src/components/notification/alert_modal.rs 🔗

@@ -1,3 +1,4 @@
+use crate::component_prelude::*;
 use crate::prelude::*;
 use gpui::IntoElement;
 use smallvec::{SmallVec, smallvec};
@@ -81,6 +82,10 @@ impl Component for AlertModal {
         ComponentScope::Notification
     }
 
+    fn status() -> ComponentStatus {
+        ComponentStatus::WorkInProgress
+    }
+
     fn description() -> Option<&'static str> {
         Some("A modal dialog that presents an alert message with primary and dismiss actions.")
     }