ui: More component previews, UI component cleanup (#25302)

Nate Butler created

- Don't require ui component docs (this isn't really working)
- Add more component previews
- Update component preview style & navigation

Release Notes:

- N/A

Change summary

crates/collab_ui/src/collab_panel.rs                             |   4 
crates/component/src/component.rs                                |  15 
crates/component_preview/src/component_preview.rs                | 186 
crates/storybook/src/story_selector.rs                           |   4 
crates/ui/docs/building-ui.md                                    |  49 
crates/ui/docs/hello-world.md                                    | 160 -
crates/ui/src/components/avatar.rs                               | 307 +
crates/ui/src/components/avatar/avatar.rs                        | 156 -
crates/ui/src/components/avatar/avatar_audio_status_indicator.rs |  72 
crates/ui/src/components/avatar/avatar_availability_indicator.rs |  49 
crates/ui/src/components/button/button.rs                        |  26 
crates/ui/src/components/button/button_icon.rs                   |   1 
crates/ui/src/components/button/button_like.rs                   |   1 
crates/ui/src/components/button/icon_button.rs                   | 163 +
crates/ui/src/components/button/toggle_button.rs                 | 131 
crates/ui/src/components/context_menu.rs                         |   1 
crates/ui/src/components/disclosure.rs                           |   1 
crates/ui/src/components/divider.rs                              |   1 
crates/ui/src/components/dropdown_menu.rs                        |   1 
crates/ui/src/components/facepile.rs                             | 113 
crates/ui/src/components/icon.rs                                 |   2 
crates/ui/src/components/image.rs                                |   1 
crates/ui/src/components/indent_guides.rs                        |   1 
crates/ui/src/components/indicator.rs                            |   1 
crates/ui/src/components/keybinding.rs                           |   1 
crates/ui/src/components/label/highlighted_label.rs              |   2 
crates/ui/src/components/list/list.rs                            |   2 
crates/ui/src/components/list/list_header.rs                     |   2 
crates/ui/src/components/list/list_item.rs                       |   2 
crates/ui/src/components/list/list_separator.rs                  |   2 
crates/ui/src/components/list/list_sub_header.rs                 |   2 
crates/ui/src/components/modal.rs                                |   2 
crates/ui/src/components/numeric_stepper.rs                      |   2 
crates/ui/src/components/popover.rs                              |   2 
crates/ui/src/components/popover_menu.rs                         |   2 
crates/ui/src/components/radio.rs                                |   2 
crates/ui/src/components/right_click_menu.rs                     |   2 
crates/ui/src/components/scrollbar.rs                            |   1 
crates/ui/src/components/settings_container.rs                   |   2 
crates/ui/src/components/settings_group.rs                       |   2 
crates/ui/src/components/stack.rs                                |   2 
crates/ui/src/components/stories.rs                              |   7 
crates/ui/src/components/stories/avatar.rs                       |  64 
crates/ui/src/components/stories/button.rs                       |  38 
crates/ui/src/components/tab.rs                                  |   1 
crates/ui/src/components/tab_bar.rs                              |   1 
crates/ui/src/components/tooltip.rs                              |   2 
crates/ui/src/styles/typography.rs                               |  65 
crates/ui/src/ui.rs                                              |   2 
crates/ui/src/utils/format_distance.rs                           |   1 
crates/ui/src/utils/search_input.rs                              |   2 
crates/workspace/src/theme_preview.rs                            |   8 
52 files changed, 813 insertions(+), 856 deletions(-)

Detailed changes

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -2458,8 +2458,8 @@ impl CollabPanel {
                 Avatar::new(contact.user.avatar_uri.clone())
                     .indicator::<AvatarAvailabilityIndicator>(if online {
                         Some(AvatarAvailabilityIndicator::new(match busy {
-                            true => ui::Availability::Busy,
-                            false => ui::Availability::Free,
+                            true => ui::CollaboratorAvailability::Busy,
+                            false => ui::CollaboratorAvailability::Free,
                         }))
                     } else {
                         None

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

@@ -173,9 +173,9 @@ pub enum ExampleLabelSide {
     Left,
     /// Right side
     Right,
-    #[default]
     /// Top side
     Top,
+    #[default]
     /// Bottom side
     Bottom,
 }
@@ -200,10 +200,10 @@ impl RenderOnce for ComponentExample {
             ExampleLabelSide::Top => base.flex_col_reverse(),
         };
 
-        base.gap_1()
+        base.gap_2()
             .p_2()
-            .text_sm()
-            .text_color(cx.theme().colors().text)
+            .text_size(px(10.))
+            .text_color(cx.theme().colors().text_muted)
             .when(self.grow, |this| this.flex_1())
             .child(self.element)
             .child(self.variant_name)
@@ -245,12 +245,13 @@ impl RenderOnce for ComponentExampleGroup {
             .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().pb_5().child(
+                this.gap_4().child(
                     div()
                         .flex()
                         .items_center()
                         .gap_3()
-                        .child(div().h_px().w_4().bg(cx.theme().colors().border_variant))
+                        .pb_1()
+                        .child(div().h_px().w_4().bg(cx.theme().colors().border))
                         .child(
                             div()
                                 .flex_none()
@@ -271,7 +272,7 @@ impl RenderOnce for ComponentExampleGroup {
                     .flex()
                     .items_start()
                     .w_full()
-                    .gap_8()
+                    .gap_6()
                     .children(self.examples)
                     .into_any_element(),
             )

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

@@ -3,8 +3,9 @@
 //! A view for exploring Zed components.
 
 use component::{components, ComponentMetadata};
-use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window};
-use ui::prelude::*;
+use gpui::{list, prelude::*, uniform_list, App, EventEmitter, FocusHandle, Focusable, Window};
+use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
+use ui::{prelude::*, ListItem};
 
 use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
 
@@ -12,7 +13,7 @@ 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);
+                let component_preview = cx.new(|cx| ComponentPreview::new(window, cx));
                 workspace.add_item_to_active_pane(
                     Box::new(component_preview),
                     None,
@@ -28,124 +29,161 @@ pub fn init(cx: &mut App) {
 
 struct ComponentPreview {
     focus_handle: FocusHandle,
+    _view_scroll_handle: ScrollHandle,
+    nav_scroll_handle: UniformListScrollHandle,
+    components: Vec<ComponentMetadata>,
+    component_list: ListState,
+    selected_index: usize,
 }
 
 impl ComponentPreview {
-    pub fn new(cx: &mut Context<Self>) -> Self {
+    pub fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let components = components().all_sorted();
+        let initial_length = components.len();
+
+        let component_list = ListState::new(initial_length, gpui::ListAlignment::Top, px(500.0), {
+            let this = cx.entity().downgrade();
+            move |ix, window: &mut Window, cx: &mut App| {
+                this.update(cx, |this, cx| {
+                    this.render_preview(ix, window, cx).into_any_element()
+                })
+                .unwrap()
+            }
+        });
+
         Self {
             focus_handle: cx.focus_handle(),
+            _view_scroll_handle: ScrollHandle::new(),
+            nav_scroll_handle: UniformListScrollHandle::new(),
+            components,
+            component_list,
+            selected_index: 0,
         }
     }
 
-    fn render_sidebar(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
-        let components = components().all_sorted();
-        let sorted_components = components.clone();
+    fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
+        self.component_list.scroll_to_reveal_item(ix);
+        self.selected_index = ix;
+        cx.notify();
+    }
 
-        v_flex()
-            .max_w_48()
-            .gap_px()
-            .p_1()
-            .children(
-                sorted_components
-                    .into_iter()
-                    .map(|component| self.render_sidebar_entry(&component, _cx)),
-            )
-            .child(
-                Label::new("These will be clickable once the layout is moved to a gpui::List.")
-                    .color(Color::Muted)
-                    .size(LabelSize::XSmall)
-                    .italic(),
-            )
+    fn get_component(&self, ix: usize) -> ComponentMetadata {
+        self.components[ix].clone()
     }
 
     fn render_sidebar_entry(
         &self,
-        component: &ComponentMetadata,
-        _cx: &Context<Self>,
+        ix: usize,
+        selected: bool,
+        cx: &Context<Self>,
     ) -> impl IntoElement {
-        h_flex()
-            .w_40()
-            .px_1p5()
-            .py_0p5()
-            .text_sm()
-            .child(component.name().clone())
+        let component = self.get_component(ix);
+
+        ListItem::new(ix)
+            .child(Label::new(component.name().clone()).color(Color::Default))
+            .selectable(true)
+            .toggle_state(selected)
+            .inset(true)
+            .on_click(cx.listener(move |this, _, _, cx| {
+                this.scroll_to_preview(ix, cx);
+            }))
     }
 
     fn render_preview(
         &self,
-        component: &ComponentMetadata,
+        ix: usize,
         window: &mut Window,
         cx: &Context<Self>,
     ) -> impl IntoElement {
+        let component = self.get_component(ix);
+
         let name = component.name();
         let scope = component.scope();
 
         let description = component.description();
 
         v_flex()
-            .border_b_1()
-            .border_color(cx.theme().colors().border)
-            .w_full()
-            .gap_3()
-            .py_6()
+            .py_2()
             .child(
                 v_flex()
-                    .gap_1()
+                    .border_1()
+                    .border_color(cx.theme().colors().border)
+                    .rounded_md()
+                    .w_full()
+                    .gap_4()
+                    .py_4()
+                    .px_6()
+                    .flex_none()
                     .child(
-                        h_flex()
+                        v_flex()
                             .gap_1()
-                            .text_2xl()
-                            .child(div().child(name))
-                            .when_some(scope, |this, scope| {
-                                this.child(div().opacity(0.5).child(format!("({})", scope)))
+                            .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(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))
                     }),
             )
-            .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_4()
-            .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 {
+    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))
+            .child(
+                uniform_list(
+                    cx.entity().clone(),
+                    "component-nav",
+                    self.components.len(),
+                    move |this, range, _window, cx| {
+                        range
+                            .map(|ix| this.render_sidebar_entry(ix, ix == this.selected_index, cx))
+                            .collect()
+                    },
+                )
+                .track_scroll(self.nav_scroll_handle.clone())
+                .pt_4()
+                .w(px(240.))
+                .h_full()
+                .flex_grow(),
+            )
+            .child(
+                v_flex()
+                    .id("component-list")
+                    .px_8()
+                    .pt_4()
+                    .size_full()
+                    .child(
+                        list(self.component_list.clone())
+                            .flex_grow()
+                            .with_sizing_behavior(gpui::ListSizingBehavior::Auto),
+                    ),
+            )
     }
 }
 
@@ -175,13 +213,13 @@ impl Item for ComponentPreview {
     fn clone_on_split(
         &self,
         _workspace_id: Option<WorkspaceId>,
-        _window: &mut Window,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<gpui::Entity<Self>>
     where
         Self: Sized,
     {
-        Some(cx.new(Self::new))
+        Some(cx.new(|cx| Self::new(window, cx)))
     }
 
     fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {

crates/storybook/src/story_selector.rs πŸ”—

@@ -14,8 +14,6 @@ use ui::prelude::*;
 pub enum ComponentStory {
     ApplicationMenu,
     AutoHeightEditor,
-    Avatar,
-    Button,
     CollabNotification,
     ContextMenu,
     Cursor,
@@ -47,8 +45,6 @@ impl ComponentStory {
                 .new(|cx| title_bar::ApplicationMenuStory::new(window, cx))
                 .into(),
             Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(),
-            Self::Avatar => cx.new(|_| ui::AvatarStory).into(),
-            Self::Button => cx.new(|_| ui::ButtonStory).into(),
             Self::CollabNotification => cx
                 .new(|_| collab_ui::notifications::CollabNotificationStory)
                 .into(),

crates/ui/docs/building-ui.md πŸ”—

@@ -1,49 +0,0 @@
-# Building UI with GPUI
-
-## Common patterns
-
-### Method ordering
-
-- id
-- Flex properties
-- Position properties
-- Size properties
-- Style properties
-- Handlers
-- State properties
-
-### Using the Label Component to Create UI Text
-
-The `Label` component helps in displaying text on user interfaces. It creates an interface where specific parameters such as label color, line height style, and strikethrough can be set.
-
-Firstly, to create a `Label` instance, use the `Label::new()` function. This function takes a string that will be displayed as text in the interface.
-
-```rust
-Label::new("Hello, world!");
-```
-
-Now let's dive a bit deeper into how to customize `Label` instances:
-
-- **Setting Color:** To set the color of the label using various predefined color options such as `Default`, `Muted`, `Created`, `Modified`, `Deleted`, etc, the `color()` function is called on the `Label` instance:
-
-    ```rust
-    Label::new("Hello, world!").color(LabelColor::Default);
-    ```
-
-- **Setting Line Height Style:** To set the line height style, the `line_height_style()` function is utilized:
-
-    ```rust
-    Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel);
-    ```
-
-- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the  `set_strikethrough()` function is used:
-
-    ```rust
-    Label::new("Hello, world!").set_strikethrough(true);
-    ```
-
-That's it! Now you can use the `Label` component to create and customize text on your application's interface.
-
-## Building a new component
-
-TODO

crates/ui/docs/hello-world.md πŸ”—

@@ -1,160 +0,0 @@
-# Hello World
-
-Let's work through the prototypical "Build a todo app" example to showcase how we might build a simple component from scratch.
-
-## Setup
-
-We'll create a headline, a list of todo items, and a form to add new items.
-
-~~~rust
-struct TodoList<V: 'static> {
-    headline: SharedString,
-    items: Vec<TodoItem>,
-    submit_form: ClickHandler<V>
-}
-
-struct TodoItem<V: 'static> {
-    text: SharedString,
-    completed: bool,
-    delete: ClickHandler<V>
-}
-
-impl<V: 'static> TodoList<V> {
-    pub fn new(
-        // Here we impl Into<SharedString>
-        headline: impl Into<SharedString>,
-        items: Vec<TodoItem>,
-        submit_form: ClickHandler<V>
-    ) -> Self {
-        Self {
-            // and here we call .into() so we can simply pass a string
-            // when creating the headline. This pattern is used throughout
-            // outr components
-            headline: headline.into(),
-            items: Vec::new(),
-            submit_form,
-        }
-    }
-}
-~~~
-
-All of this is relatively straightforward.
-
-We use [gpui::SharedString] in components instead of [std::string::String]. This allows us to efficiently handle shared string data across multiple components and threads without the performance overhead of copying strings.
-
-When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`.
-
-~~~rust
-use gpui::hsla
-
-impl<V: 'static> TodoList<V> {
-    // ...
-    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-        div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
-    }
-}
-~~~
-
-Every component needs a render method, and it should return `impl Element<V>`. This basic component will render a 16x16px yellow square on the screen.
-
-A couple of questions might come to mind:
-
-**Why is `size_4()` 16px, not 4px?**
-
-gpui's style system is based on conventions created by [Tailwind CSS](https://tailwindcss.com/). Here is an example of the list of sizes for `width`: [Width - TailwindCSS Docs](https://tailwindcss.com/docs/width).
-
-I'll quote from the Tailwind [Core Concepts](https://tailwindcss.com/docs/utility-first) docs here:
-
-> Now I know what you’re thinking, β€œthis is an atrocity, what a horrible mess!”
-> and you’re right, it’s kind of ugly. In fact it’s just about impossible to
-> think this is a good idea the first time you see it β€”
-> you have to actually try it.
-
-As you start using the Tailwind-style conventions you will be surprised how quick it makes it to build out UIs.
-
-**Why `50.0/360.0` in `hsla()`?**
-
-gpui [gpui::Hsla] use `0.0-1.0` for all its values, but it is common for tools to use `0-360` for hue.
-
-This may change in the future, but this is a little trick that let's you use familiar looking values.
-
-## Building out the container
-
-Let's grab our [theme::colors::ThemeColors] from the theme and start building out a basic container.
-
-We can access the current theme's colors like this:
-
-~~~rust
-impl<V: 'static> TodoList<V> {
-    // ...
-    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-        let color = cx.theme().colors()
-
-        div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
-    }
-}
-~~~
-
-Now we have access to the complete set of colors defined in the theme.
-
-~~~rust
-use gpui::hsla
-
-impl<V: 'static> TodoList<V> {
-    // ...
-    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-        let color = cx.theme().colors()
-
-        div().size_4().bg(color.surface)
-    }
-}
-~~~
-
-Let's finish up some basic styles for the container then move on to adding the other elements.
-
-~~~rust
-use gpui::hsla
-
-impl<V: 'static> TodoList<V> {
-    // ...
-    fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
-        let color = cx.theme().colors()
-
-        div()
-            // Flex properties
-            .flex()
-            .flex_col()             // Stack elements vertically
-            .gap_2()                // Add 8px of space between elements
-            // Size properties
-            .w_96()                 // Set width to 384px
-            .p_4()                  // Add 16px of padding on all sides
-            // Color properties
-            .bg(color.surface)      // Set background color
-            .text_color(color.text) // Set text color
-            // Border properties
-            .rounded_md()           // Add 4px of border radius
-            .border_1()             // Add a 1px border
-            .border_color(color.border)
-            .child(
-                "Hello, world!"
-            )
-    }
-}
-~~~
-
-### Headline
-
-TODO
-
-### List of todo items
-
-TODO
-
-### Input
-
-TODO
-
-
-### End result
-
-TODO

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

@@ -1,7 +1,302 @@
-mod avatar;
-mod avatar_audio_status_indicator;
-mod avatar_availability_indicator;
+use crate::prelude::*;
 
-pub use avatar::*;
-pub use avatar_audio_status_indicator::*;
-pub use avatar_availability_indicator::*;
+use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
+
+/// An element that renders a user avatar with customizable appearance options.
+///
+/// # Examples
+///
+/// ```
+/// use ui::{Avatar, AvatarShape};
+///
+/// Avatar::new("path/to/image.png")
+///     .shape(AvatarShape::Circle)
+///     .grayscale(true)
+///     .border_color(gpui::red());
+/// ```
+#[derive(IntoElement, IntoComponent)]
+pub struct Avatar {
+    image: Img,
+    size: Option<AbsoluteLength>,
+    border_color: Option<Hsla>,
+    indicator: Option<AnyElement>,
+}
+
+impl Avatar {
+    /// Creates a new avatar element with the specified image source.
+    pub fn new(src: impl Into<ImageSource>) -> Self {
+        Avatar {
+            image: img(src),
+            size: None,
+            border_color: None,
+            indicator: None,
+        }
+    }
+
+    /// Applies a grayscale filter to the avatar image.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use ui::{Avatar, AvatarShape};
+    ///
+    /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
+    /// ```
+    pub fn grayscale(mut self, grayscale: bool) -> Self {
+        self.image = self.image.grayscale(grayscale);
+        self
+    }
+
+    /// Sets the border color of the avatar.
+    ///
+    /// This might be used to match the border to the background color of
+    /// the parent element to create the illusion of cropping another
+    /// shape underneath (for example in face piles.)
+    pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
+        self.border_color = Some(color.into());
+        self
+    }
+
+    /// Size overrides the avatar size. By default they are 1rem.
+    pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
+        self.size = size.into().map(Into::into);
+        self
+    }
+
+    /// Sets the current indicator to be displayed on the avatar, if any.
+    pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
+        self.indicator = indicator.into().map(IntoElement::into_any_element);
+        self
+    }
+}
+
+impl RenderOnce for Avatar {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let border_width = if self.border_color.is_some() {
+            px(2.)
+        } else {
+            px(0.)
+        };
+
+        let image_size = self.size.unwrap_or_else(|| rems(1.).into());
+        let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
+
+        div()
+            .size(container_size)
+            .rounded_full()
+            .when_some(self.border_color, |this, color| {
+                this.border(border_width).border_color(color)
+            })
+            .child(
+                self.image
+                    .size(image_size)
+                    .rounded_full()
+                    .bg(cx.theme().colors().ghost_element_background),
+            )
+            .children(self.indicator.map(|indicator| div().child(indicator)))
+    }
+}
+
+use gpui::AnyView;
+
+/// The audio status of an player, for use in representing
+/// their status visually on their avatar.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum AudioStatus {
+    /// The player's microphone is muted.
+    Muted,
+    /// The player's microphone is muted, and collaboration audio is disabled.
+    Deafened,
+}
+
+/// An indicator that shows the audio status of a player.
+#[derive(IntoElement)]
+pub struct AvatarAudioStatusIndicator {
+    audio_status: AudioStatus,
+    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
+}
+
+impl AvatarAudioStatusIndicator {
+    /// Creates a new `AvatarAudioStatusIndicator`
+    pub fn new(audio_status: AudioStatus) -> Self {
+        Self {
+            audio_status,
+            tooltip: None,
+        }
+    }
+
+    /// Sets the tooltip for the indicator.
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl RenderOnce for AvatarAudioStatusIndicator {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let icon_size = IconSize::Indicator;
+
+        let width_in_px = icon_size.rems() * window.rem_size();
+        let padding_x = px(4.);
+
+        div()
+            .absolute()
+            .bottom(rems_from_px(-3.))
+            .right(rems_from_px(-6.))
+            .w(width_in_px + padding_x)
+            .h(icon_size.rems())
+            .child(
+                h_flex()
+                    .id("muted-indicator")
+                    .justify_center()
+                    .px(padding_x)
+                    .py(px(2.))
+                    .bg(cx.theme().status().error_background)
+                    .rounded_md()
+                    .child(
+                        Icon::new(match self.audio_status {
+                            AudioStatus::Muted => IconName::MicMute,
+                            AudioStatus::Deafened => IconName::AudioOff,
+                        })
+                        .size(icon_size)
+                        .color(Color::Error),
+                    )
+                    .when_some(self.tooltip, |this, tooltip| {
+                        this.tooltip(move |window, cx| tooltip(window, cx))
+                    }),
+            )
+    }
+}
+
+/// Represents the availability status of a collaborator.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum CollaboratorAvailability {
+    Free,
+    Busy,
+}
+
+/// Represents the availability and presence status of a collaborator.
+#[derive(IntoElement)]
+pub struct AvatarAvailabilityIndicator {
+    availability: CollaboratorAvailability,
+    avatar_size: Option<Pixels>,
+}
+
+impl AvatarAvailabilityIndicator {
+    /// Creates a new indicator
+    pub fn new(availability: CollaboratorAvailability) -> Self {
+        Self {
+            availability,
+            avatar_size: None,
+        }
+    }
+
+    /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on.
+    pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+        self.avatar_size = size.into();
+        self
+    }
+}
+
+impl RenderOnce for AvatarAvailabilityIndicator {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size());
+
+        // HACK: non-integer sizes result in oval indicators.
+        let indicator_size = (avatar_size * 0.4).round();
+
+        div()
+            .absolute()
+            .bottom_0()
+            .right_0()
+            .size(indicator_size)
+            .rounded(indicator_size)
+            .bg(match self.availability {
+                CollaboratorAvailability::Free => cx.theme().status().created,
+                CollaboratorAvailability::Busy => cx.theme().status().deleted,
+            })
+    }
+}
+
+// View this component preview using `workspace: open component-preview`
+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(example_avatar).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(cx.theme().colors().border)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Audio Status",
+                    vec![
+                        single_example(
+                            "Muted",
+                            Avatar::new(example_avatar)
+                                .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Deafened",
+                            Avatar::new(example_avatar)
+                                .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Availability",
+                    vec![
+                        single_example(
+                            "Free",
+                            Avatar::new(example_avatar)
+                                .indicator(AvatarAvailabilityIndicator::new(
+                                    CollaboratorAvailability::Free,
+                                ))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Busy",
+                            Avatar::new(example_avatar)
+                                .indicator(AvatarAvailabilityIndicator::new(
+                                    CollaboratorAvailability::Busy,
+                                ))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

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

@@ -1,156 +0,0 @@
-use crate::{prelude::*, Indicator};
-
-use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
-
-/// An element that renders a user avatar with customizable appearance options.
-///
-/// # Examples
-///
-/// ```
-/// use ui::{Avatar, AvatarShape};
-///
-/// Avatar::new("path/to/image.png")
-///     .shape(AvatarShape::Circle)
-///     .grayscale(true)
-///     .border_color(gpui::red());
-/// ```
-#[derive(IntoElement, IntoComponent)]
-pub struct Avatar {
-    image: Img,
-    size: Option<AbsoluteLength>,
-    border_color: Option<Hsla>,
-    indicator: Option<AnyElement>,
-}
-
-impl Avatar {
-    /// Creates a new avatar element with the specified image source.
-    pub fn new(src: impl Into<ImageSource>) -> Self {
-        Avatar {
-            image: img(src),
-            size: None,
-            border_color: None,
-            indicator: None,
-        }
-    }
-
-    /// Applies a grayscale filter to the avatar image.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// use ui::{Avatar, AvatarShape};
-    ///
-    /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
-    /// ```
-    pub fn grayscale(mut self, grayscale: bool) -> Self {
-        self.image = self.image.grayscale(grayscale);
-        self
-    }
-
-    /// Sets the border color of the avatar.
-    ///
-    /// This might be used to match the border to the background color of
-    /// the parent element to create the illusion of cropping another
-    /// shape underneath (for example in face piles.)
-    pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
-        self.border_color = Some(color.into());
-        self
-    }
-
-    /// Size overrides the avatar size. By default they are 1rem.
-    pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
-        self.size = size.into().map(Into::into);
-        self
-    }
-
-    /// Sets the current indicator to be displayed on the avatar, if any.
-    pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
-        self.indicator = indicator.into().map(IntoElement::into_any_element);
-        self
-    }
-}
-
-impl RenderOnce for Avatar {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let border_width = if self.border_color.is_some() {
-            px(2.)
-        } else {
-            px(0.)
-        };
-
-        let image_size = self.size.unwrap_or_else(|| rems(1.).into());
-        let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
-
-        div()
-            .size(container_size)
-            .rounded_full()
-            .when_some(self.border_color, |this, color| {
-                this.border(border_width).border_color(color)
-            })
-            .child(
-                self.image
-                    .size(image_size)
-                    .rounded_full()
-                    .bg(cx.theme().colors().ghost_element_background),
-            )
-            .children(self.indicator.map(|indicator| div().child(indicator)))
-    }
-}
-
-// View this component preview using `workspace: open component-preview`
-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/avatar/avatar_audio_status_indicator.rs πŸ”—

@@ -1,72 +0,0 @@
-use gpui::AnyView;
-
-use crate::prelude::*;
-
-/// The audio status of an player, for use in representing
-/// their status visually on their avatar.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum AudioStatus {
-    /// The player's microphone is muted.
-    Muted,
-    /// The player's microphone is muted, and collaboration audio is disabled.
-    Deafened,
-}
-
-/// An indicator that shows the audio status of a player.
-#[derive(IntoElement)]
-pub struct AvatarAudioStatusIndicator {
-    audio_status: AudioStatus,
-    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
-}
-
-impl AvatarAudioStatusIndicator {
-    /// Creates a new `AvatarAudioStatusIndicator`
-    pub fn new(audio_status: AudioStatus) -> Self {
-        Self {
-            audio_status,
-            tooltip: None,
-        }
-    }
-
-    /// Sets the tooltip for the indicator.
-    pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
-        self.tooltip = Some(Box::new(tooltip));
-        self
-    }
-}
-
-impl RenderOnce for AvatarAudioStatusIndicator {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let icon_size = IconSize::Indicator;
-
-        let width_in_px = icon_size.rems() * window.rem_size();
-        let padding_x = px(4.);
-
-        div()
-            .absolute()
-            .bottom(rems_from_px(-3.))
-            .right(rems_from_px(-6.))
-            .w(width_in_px + padding_x)
-            .h(icon_size.rems())
-            .child(
-                h_flex()
-                    .id("muted-indicator")
-                    .justify_center()
-                    .px(padding_x)
-                    .py(px(2.))
-                    .bg(cx.theme().status().error_background)
-                    .rounded_md()
-                    .child(
-                        Icon::new(match self.audio_status {
-                            AudioStatus::Muted => IconName::MicMute,
-                            AudioStatus::Deafened => IconName::AudioOff,
-                        })
-                        .size(icon_size)
-                        .color(Color::Error),
-                    )
-                    .when_some(self.tooltip, |this, tooltip| {
-                        this.tooltip(move |window, cx| tooltip(window, cx))
-                    }),
-            )
-    }
-}

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

@@ -1,49 +0,0 @@
-#![allow(missing_docs)]
-use crate::prelude::*;
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum Availability {
-    Free,
-    Busy,
-}
-
-#[derive(IntoElement)]
-pub struct AvatarAvailabilityIndicator {
-    availability: Availability,
-    avatar_size: Option<Pixels>,
-}
-
-impl AvatarAvailabilityIndicator {
-    pub fn new(availability: Availability) -> Self {
-        Self {
-            availability,
-            avatar_size: None,
-        }
-    }
-
-    /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on.
-    pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
-        self.avatar_size = size.into();
-        self
-    }
-}
-
-impl RenderOnce for AvatarAvailabilityIndicator {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size());
-
-        // HACK: non-integer sizes result in oval indicators.
-        let indicator_size = (avatar_size * 0.4).round();
-
-        div()
-            .absolute()
-            .bottom_0()
-            .right_0()
-            .size(indicator_size)
-            .rounded(indicator_size)
-            .bg(match self.availability {
-                Availability::Free => cx.theme().status().created,
-                Availability::Busy => cx.theme().status().deleted,
-            })
-    }
-}

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

@@ -1,4 +1,3 @@
-#![allow(missing_docs)]
 use component::{example_group_with_title, single_example, ComponentPreview};
 use gpui::{AnyElement, AnyView, DefiniteLength};
 use ui_macros::IntoComponent;
@@ -81,6 +80,7 @@ use super::button_icon::ButtonIcon;
 /// ```
 ///
 #[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct Button {
     base: ButtonLike,
     label: SharedString,
@@ -463,7 +463,7 @@ impl ComponentPreview for Button {
             .gap_6()
             .children(vec![
                 example_group_with_title(
-                    "Styles",
+                    "Button Styles",
                     vec![
                         single_example(
                             "Default",
@@ -481,6 +481,12 @@ impl ComponentPreview for Button {
                                 .style(ButtonStyle::Subtle)
                                 .into_any_element(),
                         ),
+                        single_example(
+                            "Tinted",
+                            Button::new("tinted_accent_style", "Accent")
+                                .style(ButtonStyle::Tinted(TintColor::Accent))
+                                .into_any_element(),
+                        ),
                         single_example(
                             "Transparent",
                             Button::new("transparent", "Transparent")
@@ -490,7 +496,7 @@ impl ComponentPreview for Button {
                     ],
                 ),
                 example_group_with_title(
-                    "Tinted",
+                    "Tint Styles",
                     vec![
                         single_example(
                             "Accent",
@@ -519,7 +525,7 @@ impl ComponentPreview for Button {
                     ],
                 ),
                 example_group_with_title(
-                    "States",
+                    "Special States",
                     vec![
                         single_example(
                             "Default",
@@ -540,7 +546,7 @@ impl ComponentPreview for Button {
                     ],
                 ),
                 example_group_with_title(
-                    "With Icons",
+                    "Buttons with Icons",
                     vec![
                         single_example(
                             "Icon Start",
@@ -563,16 +569,6 @@ impl ComponentPreview for Button {
                                 .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(),
-                        ),
                     ],
                 ),
             ])

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

@@ -1,8 +1,7 @@
-#![allow(missing_docs)]
 use gpui::{AnyView, DefiniteLength, Hsla};
 
 use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
-use crate::{prelude::*, ElevationIndex, Indicator, SelectableButton};
+use crate::{prelude::*, ElevationIndex, Indicator, SelectableButton, TintColor};
 use crate::{IconName, IconSize};
 
 use super::button_icon::ButtonIcon;
@@ -14,7 +13,8 @@ pub enum IconButtonShape {
     Wide,
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct IconButton {
     base: ButtonLike,
     shape: IconButtonShape,
@@ -200,3 +200,160 @@ impl RenderOnce for IconButton {
             )
     }
 }
+
+impl ComponentPreview for IconButton {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Icon Button Styles",
+                    vec![
+                        single_example(
+                            "Default",
+                            IconButton::new("default", IconName::Check)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Filled",
+                            IconButton::new("filled", IconName::Check)
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Subtle",
+                            IconButton::new("subtle", IconName::Check)
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Subtle)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Tinted",
+                            IconButton::new("tinted", IconName::Check)
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Tinted(TintColor::Accent))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Transparent",
+                            IconButton::new("transparent", IconName::Check)
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Transparent)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Icon Button Shapes",
+                    vec![
+                        single_example(
+                            "Square",
+                            IconButton::new("square", IconName::Check)
+                                .shape(IconButtonShape::Square)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Wide",
+                            IconButton::new("wide", IconName::Check)
+                                .shape(IconButtonShape::Wide)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Icon Button Sizes",
+                    vec![
+                        single_example(
+                            "Small",
+                            IconButton::new("small", IconName::Check)
+                                .icon_size(IconSize::XSmall)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Small",
+                            IconButton::new("small", IconName::Check)
+                                .icon_size(IconSize::Small)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Medium",
+                            IconButton::new("medium", IconName::Check)
+                                .icon_size(IconSize::Medium)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "XLarge",
+                            IconButton::new("xlarge", IconName::Check)
+                                .icon_size(IconSize::XLarge)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Special States",
+                    vec![
+                        single_example(
+                            "Disabled",
+                            IconButton::new("disabled", IconName::Check)
+                                .disabled(true)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            IconButton::new("selected", IconName::Check)
+                                .toggle_state(true)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Indicator",
+                            IconButton::new("indicator", IconName::Check)
+                                .indicator(Indicator::dot().color(Color::Success))
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Custom Colors",
+                    vec![
+                        single_example(
+                            "Custom Icon Color",
+                            IconButton::new("custom_color", IconName::Check)
+                                .icon_color(Color::Accent)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Alpha",
+                            IconButton::new("alpha", IconName::Check)
+                                .alpha(0.5)
+                                .style(ButtonStyle::Filled)
+                                .layer(ElevationIndex::Background)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

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

@@ -1,4 +1,3 @@
-#![allow(missing_docs)]
 use gpui::{AnyView, ClickEvent};
 
 use crate::{prelude::*, ButtonLike, ButtonLikeRounding, ElevationIndex};
@@ -16,7 +15,8 @@ pub enum ToggleButtonPosition {
     Last,
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct ToggleButton {
     base: ButtonLike,
     position_in_group: Option<ToggleButtonPosition>,
@@ -142,3 +142,130 @@ impl RenderOnce for ToggleButton {
             )
     }
 }
+
+impl ComponentPreview for ToggleButton {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Button Styles",
+                    vec![
+                        single_example(
+                            "Off",
+                            ToggleButton::new("off", "Off")
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On",
+                            ToggleButton::new("on", "On")
+                                .layer(ElevationIndex::Background)
+                                .toggle_state(true)
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Off – Disabled",
+                            ToggleButton::new("disabled_off", "Disabled Off")
+                                .layer(ElevationIndex::Background)
+                                .disabled(true)
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On – Disabled",
+                            ToggleButton::new("disabled_on", "Disabled On")
+                                .layer(ElevationIndex::Background)
+                                .disabled(true)
+                                .toggle_state(true)
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Button Group",
+                    vec![
+                        single_example(
+                            "Three Buttons",
+                            h_flex()
+                                .child(
+                                    ToggleButton::new("three_btn_first", "First")
+                                        .layer(ElevationIndex::Background)
+                                        .style(ButtonStyle::Filled)
+                                        .first()
+                                        .into_any_element(),
+                                )
+                                .child(
+                                    ToggleButton::new("three_btn_middle", "Middle")
+                                        .layer(ElevationIndex::Background)
+                                        .style(ButtonStyle::Filled)
+                                        .middle()
+                                        .toggle_state(true)
+                                        .into_any_element(),
+                                )
+                                .child(
+                                    ToggleButton::new("three_btn_last", "Last")
+                                        .layer(ElevationIndex::Background)
+                                        .style(ButtonStyle::Filled)
+                                        .last()
+                                        .into_any_element(),
+                                )
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Two Buttons",
+                            h_flex()
+                                .child(
+                                    ToggleButton::new("two_btn_first", "First")
+                                        .layer(ElevationIndex::Background)
+                                        .style(ButtonStyle::Filled)
+                                        .first()
+                                        .into_any_element(),
+                                )
+                                .child(
+                                    ToggleButton::new("two_btn_last", "Last")
+                                        .layer(ElevationIndex::Background)
+                                        .style(ButtonStyle::Filled)
+                                        .last()
+                                        .into_any_element(),
+                                )
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Alternate Sizes",
+                    vec![
+                        single_example(
+                            "None",
+                            ToggleButton::new("none", "None")
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Filled)
+                                .size(ButtonSize::None)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Compact",
+                            ToggleButton::new("compact", "Compact")
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Filled)
+                                .size(ButtonSize::Compact)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            ToggleButton::new("large", "Large")
+                                .layer(ElevationIndex::Background)
+                                .style(ButtonStyle::Filled)
+                                .size(ButtonSize::Large)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

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

@@ -1,4 +1,3 @@
-#![allow(missing_docs)]
 use crate::{
     h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, IconSize, KeyBinding, Label,
     List, ListItem, ListSeparator, ListSubHeader,

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

@@ -1,4 +1,4 @@
-use crate::prelude::*;
+use crate::{prelude::*, Avatar};
 use gpui::{AnyElement, StyleRefinement};
 use smallvec::SmallVec;
 
@@ -7,7 +7,7 @@ use smallvec::SmallVec;
 ///
 /// Facepiles are used to display a group of people or things,
 /// such as a list of participants in a collaboration session.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Facepile {
     base: Div,
     faces: SmallVec<[AnyElement; 2]>,
@@ -60,60 +60,57 @@ 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 preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        let 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(),
-//                     ),
-//                 ),
-//             ],
-//         )]
-//     }
-// }
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Facepile Examples",
+                    vec![
+                        single_example(
+                            "Default",
+                            Facepile::new(
+                                faces
+                                    .iter()
+                                    .map(|&url| Avatar::new(url).into_any_element())
+                                    .collect(),
+                            )
+                            .into_any_element(),
+                        ),
+                        single_example(
+                            "Custom Size",
+                            Facepile::new(
+                                faces
+                                    .iter()
+                                    .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
+                                    .collect(),
+                            )
+                            .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Special Cases",
+                    vec![
+                        single_example("Empty Facepile", Facepile::empty().into_any_element()),
+                        single_example(
+                            "Single Face",
+                            Facepile::new(vec![Avatar::new(faces[0]).into_any_element()].into())
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

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

@@ -1,4 +1,3 @@
-#![allow(missing_docs)]
 use gpui::{svg, App, IntoElement, Rems, RenderOnce, Size, Styled, Window};
 use serde::{Deserialize, Serialize};
 use strum::{EnumIter, EnumString, IntoStaticStr};

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

@@ -1,5 +1,3 @@
-#![allow(missing_docs)]
-
 use crate::{
     h_flex, v_flex, Clickable, Color, DynamicSpacing, Headline, HeadlineSize, IconButton,
     IconButtonShape, IconName, Label, LabelCommon, LabelSize,

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

@@ -1,8 +1,3 @@
-// We allow missing docs for stories as the docs will more or less be
-// "This is the ___ story", which is not very useful.
-#![allow(missing_docs)]
-mod avatar;
-mod button;
 mod context_menu;
 mod disclosure;
 mod icon;
@@ -15,8 +10,6 @@ mod tab;
 mod tab_bar;
 mod toggle_button;
 
-pub use avatar::*;
-pub use button::*;
 pub use context_menu::*;
 pub use disclosure::*;
 pub use icon::*;

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

@@ -1,64 +0,0 @@
-use gpui::Render;
-use story::{Story, StoryItem, StorySection};
-
-use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator};
-use crate::{Avatar, AvatarAudioStatusIndicator};
-
-pub struct AvatarStory;
-
-impl Render for AvatarStory {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<Avatar>())
-            .child(
-                StorySection::new()
-                    .child(StoryItem::new(
-                        "Default",
-                        Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4"),
-                    ))
-                    .child(StoryItem::new(
-                        "Default",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4"),
-                    )),
-            )
-            .child(
-                StorySection::new()
-                    .child(StoryItem::new(
-                        "With free availability indicator",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
-                    ))
-                    .child(StoryItem::new(
-                        "With busy availability indicator",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
-                    )),
-            )
-            .child(
-                StorySection::new()
-                    .child(StoryItem::new(
-                        "With info border",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .border_color(cx.theme().status().info_border),
-                    ))
-                    .child(StoryItem::new(
-                        "With error border",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .border_color(cx.theme().status().error_border),
-                    )),
-            )
-            .child(
-                StorySection::new()
-                    .child(StoryItem::new(
-                        "With muted audio indicator",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
-                    ))
-                    .child(StoryItem::new(
-                        "With deafened audio indicator",
-                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
-                    )),
-            )
-    }
-}

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

@@ -1,38 +0,0 @@
-use gpui::Render;
-use story::Story;
-
-use crate::{prelude::*, IconName};
-use crate::{Button, ButtonStyle};
-
-pub struct ButtonStory;
-
-impl Render for ButtonStory {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<Button>())
-            .child(Story::label("Default"))
-            .child(Button::new("default_filled", "Click me"))
-            .child(Story::label("Selected"))
-            .child(Button::new("selected_filled", "Click me").toggle_state(true))
-            .child(Story::label("Selected with `selected_label`"))
-            .child(
-                Button::new("selected_label_filled", "Click me")
-                    .toggle_state(true)
-                    .selected_label("I have been selected"),
-            )
-            .child(Story::label("With `label_color`"))
-            .child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
-            .child(Story::label("With `icon`"))
-            .child(Button::new("filled_with_icon", "Click me").icon(IconName::FileGit))
-            .child(Story::label("Selected with `icon`"))
-            .child(
-                Button::new("filled_and_selected_with_icon", "Click me")
-                    .toggle_state(true)
-                    .icon(IconName::FileGit),
-            )
-            .child(Story::label("Default (Subtle)"))
-            .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
-            .child(Story::label("Default (Transparent)"))
-            .child(Button::new("default_transparent", "Click me").style(ButtonStyle::Transparent))
-    }
-}

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

@@ -1,5 +1,3 @@
-#![allow(missing_docs)]
-
 use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
 use settings::Settings;
 use theme::ThemeSettings;

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

@@ -237,40 +237,37 @@ impl Headline {
 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(),
-                    ),
-                ],
-            )])
+            .gap_1()
+            .children(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/ui.rs πŸ”—

@@ -1,5 +1,3 @@
-#![deny(missing_docs)]
-
 //! # UI – Zed UI Primitives & Components
 //!
 //! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI.

crates/ui/src/utils/format_distance.rs πŸ”—

@@ -1,5 +1,4 @@
 // This won't be documented further as it is intended to be removed, or merged with the `time_format` crate.
-#![allow(missing_docs)]
 
 use chrono::{DateTime, Local, NaiveDateTime};
 

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

@@ -3,10 +3,10 @@ use gpui::{actions, hsla, AnyElement, App, Entity, EventEmitter, FocusHandle, Fo
 use strum::IntoEnumIterator;
 use theme::all_theme_colors;
 use ui::{
-    element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
-    Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
-    Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
-    IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor, Tooltip,
+    element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus, Avatar,
+    AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox,
+    CheckboxWithLabel, CollaboratorAvailability, ContentGroup, DecoratedIcon, ElevationIndex,
+    Facepile, IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor, Tooltip,
 };
 
 use crate::{Item, Workspace};