Story/Storybook Enhancements (#3674)

Nate Butler created

[[PR Description]]

- Adds some Story components to reduce the amount of code needed to lay
out stories
- Added the ability to open a story in Zed using a link (see text story)
- Added sections with descriptions and usage.

Release Notes:

- N/A

Change summary

Cargo.lock                                       |   3 
crates/story/Cargo.toml                          |   2 
crates/story/src/story.rs                        | 376 +++++++++++++++++
crates/storybook2/Cargo.toml                     |   1 
crates/storybook2/src/stories/text.rs            | 215 +++++++--
crates/storybook2/src/storybook2.rs              |   1 
crates/ui2/src/components/stories/icon_button.rs | 214 +++++++--
7 files changed, 691 insertions(+), 121 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9263,6 +9263,8 @@ name = "story"
 version = "0.1.0"
 dependencies = [
  "gpui2",
+ "itertools 0.10.5",
+ "smallvec",
 ]
 
 [[package]]
@@ -9277,6 +9279,7 @@ dependencies = [
  "editor2",
  "fuzzy2",
  "gpui2",
+ "indoc",
  "itertools 0.11.0",
  "language2",
  "log",

crates/story/Cargo.toml 🔗

@@ -8,3 +8,5 @@ publish = false
 
 [dependencies]
 gpui = { package = "gpui2", path = "../gpui2" }
+smallvec.workspace = true
+itertools = {package = "itertools", version = "0.10"}

crates/story/src/story.rs 🔗

@@ -1,22 +1,199 @@
-use gpui::prelude::*;
-use gpui::{div, hsla, Div, SharedString};
+use gpui::{
+    div, hsla, prelude::*, px, rems, AnyElement, Div, ElementId, Hsla, SharedString, Stateful,
+    WindowContext,
+};
+use itertools::Itertools;
+use smallvec::SmallVec;
+
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+static COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+pub fn reasonably_unique_id() -> String {
+    let now = SystemTime::now();
+    let timestamp = now.duration_since(UNIX_EPOCH).unwrap();
+
+    let cnt = COUNTER.fetch_add(1, Ordering::Relaxed);
+
+    let id = format!("{}_{}", timestamp.as_nanos(), cnt);
+
+    id
+}
+
+pub struct StoryColor {
+    pub primary: Hsla,
+    pub secondary: Hsla,
+    pub border: Hsla,
+    pub background: Hsla,
+    pub card_background: Hsla,
+    pub divider: Hsla,
+    pub link: Hsla,
+}
+
+impl StoryColor {
+    pub fn new() -> Self {
+        Self {
+            primary: hsla(216. / 360., 11. / 100., 0. / 100., 1.),
+            secondary: hsla(216. / 360., 11. / 100., 16. / 100., 1.),
+            border: hsla(216. / 360., 11. / 100., 91. / 100., 1.),
+            background: hsla(0. / 360., 0. / 100., 100. / 100., 1.),
+            card_background: hsla(0. / 360., 0. / 100., 96. / 100., 1.),
+            divider: hsla(216. / 360., 11. / 100., 86. / 100., 1.),
+            link: hsla(206. / 360., 100. / 100., 50. / 100., 1.),
+        }
+    }
+}
+
+pub fn story_color() -> StoryColor {
+    StoryColor::new()
+}
+
+#[derive(IntoElement)]
+pub struct StoryContainer {
+    title: SharedString,
+    relative_path: &'static str,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StoryContainer {
+    pub fn new(title: impl Into<SharedString>, relative_path: &'static str) -> Self {
+        Self {
+            title: title.into(),
+            relative_path,
+            children: SmallVec::new(),
+        }
+    }
+}
+
+impl ParentElement for StoryContainer {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for StoryContainer {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .id("story_container")
+            .bg(story_color().background)
+            .child(
+                div()
+                    .flex()
+                    .flex_none()
+                    .w_full()
+                    .justify_between()
+                    .p_2()
+                    .bg(story_color().background)
+                    .border_b()
+                    .border_color(story_color().border)
+                    .child(Story::title(self.title))
+                    .child(
+                        div()
+                            .text_xs()
+                            .text_color(story_color().primary)
+                            .child(Story::open_story_link(self.relative_path)),
+                    ),
+            )
+            .child(
+                div()
+                    .w_full()
+                    .h_px()
+                    .flex_1()
+                    .id("story_body")
+                    .overflow_hidden_x()
+                    .overflow_y_scroll()
+                    .flex()
+                    .flex_col()
+                    .pb_4()
+                    .children(self.children),
+            )
+    }
+}
 
 pub struct Story {}
 
 impl Story {
     pub fn container() -> Div {
-        div().size_full().flex().flex_col().pt_2().px_4().bg(hsla(
-            0. / 360.,
-            0. / 100.,
-            100. / 100.,
-            1.,
-        ))
+        div().size_full().overflow_hidden().child(
+            div()
+                .id("story_container")
+                .overflow_y_scroll()
+                .w_full()
+                .min_h_full()
+                .flex()
+                .flex_col()
+                .bg(story_color().background),
+        )
+    }
+
+    // TODO: Move all stories to container2, then rename
+    pub fn container2<T>(relative_path: &'static str) -> Div {
+        div().size_full().child(
+            div()
+                .size_full()
+                .id("story_container")
+                .overflow_y_scroll()
+                .flex()
+                .flex_col()
+                .flex_none()
+                .child(
+                    div()
+                        .flex()
+                        .justify_between()
+                        .p_2()
+                        .border_b()
+                        .border_color(story_color().border)
+                        .child(Story::title_for::<T>())
+                        .child(
+                            div()
+                                .text_xs()
+                                .text_color(story_color().primary)
+                                .child(Story::open_story_link(relative_path)),
+                        ),
+                )
+                .child(
+                    div()
+                        .w_full()
+                        .min_h_full()
+                        .flex()
+                        .flex_col()
+                        .bg(story_color().background),
+                ),
+        )
+    }
+
+    pub fn open_story_link(relative_path: &'static str) -> impl Element {
+        let path = PathBuf::from_iter([relative_path]);
+
+        div()
+            .flex()
+            .gap_2()
+            .text_xs()
+            .text_color(story_color().primary)
+            .id(SharedString::from(format!("id_{}", relative_path)))
+            .on_click({
+                let path = path.clone();
+
+                move |_event, _cx| {
+                    let path = format!("{}:0:0", path.to_string_lossy());
+
+                    std::process::Command::new("zed").arg(path).spawn().ok();
+                }
+            })
+            .children(vec![div().child(Story::link("Open in Zed →"))])
     }
 
     pub fn title(title: impl Into<SharedString>) -> impl Element {
         div()
-            .text_xl()
-            .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+            .text_xs()
+            .text_color(story_color().primary)
             .child(title.into())
     }
 
@@ -24,12 +201,185 @@ impl Story {
         Self::title(std::any::type_name::<T>())
     }
 
+    pub fn section() -> Div {
+        div()
+            .p_4()
+            .m_4()
+            .border()
+            .border_color(story_color().border)
+    }
+
+    pub fn section_title() -> Div {
+        div().text_lg().text_color(story_color().primary)
+    }
+
+    pub fn group() -> Div {
+        div().my_2().bg(story_color().background)
+    }
+
+    pub fn code_block(code: impl Into<SharedString>) -> Div {
+        div()
+            .size_full()
+            .p_2()
+            .max_w(rems(36.))
+            .bg(gpui::black())
+            .rounded_md()
+            .text_sm()
+            .text_color(gpui::white())
+            .overflow_hidden()
+            .child(code.into())
+    }
+
+    pub fn divider() -> Div {
+        div().my_2().h(px(1.)).bg(story_color().divider)
+    }
+
+    pub fn link(link: impl Into<SharedString>) -> impl Element {
+        div()
+            .id(ElementId::from(SharedString::from(reasonably_unique_id())))
+            .text_xs()
+            .text_color(story_color().link)
+            .cursor(gpui::CursorStyle::PointingHand)
+            .child(link.into())
+    }
+
+    pub fn description(description: impl Into<SharedString>) -> impl Element {
+        div()
+            .text_sm()
+            .text_color(story_color().secondary)
+            .min_w_96()
+            .child(description.into())
+    }
+
     pub fn label(label: impl Into<SharedString>) -> impl Element {
         div()
-            .mt_4()
-            .mb_2()
             .text_xs()
-            .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+            .text_color(story_color().primary)
             .child(label.into())
     }
+
+    /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate.
+    pub fn v_stack() -> Div {
+        div().flex().flex_col().gap_1()
+    }
+}
+
+#[derive(IntoElement)]
+pub struct StoryItem {
+    label: SharedString,
+    item: AnyElement,
+    description: Option<SharedString>,
+    usage: Option<SharedString>,
+}
+
+impl StoryItem {
+    pub fn new(label: impl Into<SharedString>, item: impl IntoElement) -> Self {
+        Self {
+            label: label.into(),
+            item: item.into_any_element(),
+            description: None,
+            usage: None,
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    pub fn usage(mut self, code: impl Into<SharedString>) -> Self {
+        self.usage = Some(code.into());
+        self
+    }
+}
+
+impl RenderOnce for StoryItem {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .my_2()
+            .flex()
+            .gap_4()
+            .w_full()
+            .child(
+                Story::v_stack()
+                    .px_2()
+                    .w_1_2()
+                    .min_h_px()
+                    .child(Story::label(self.label))
+                    .child(
+                        div()
+                            .rounded_md()
+                            .bg(story_color().card_background)
+                            .border()
+                            .border_color(story_color().border)
+                            .py_1()
+                            .px_2()
+                            .overflow_hidden()
+                            .child(self.item),
+                    )
+                    .when_some(self.description, |this, description| {
+                        this.child(Story::description(description))
+                    }),
+            )
+            .child(
+                Story::v_stack()
+                    .px_2()
+                    .flex_none()
+                    .w_1_2()
+                    .min_h_px()
+                    .when_some(self.usage, |this, usage| {
+                        this.child(Story::label("Example Usage"))
+                            .child(Story::code_block(usage))
+                    }),
+            )
+    }
+}
+
+#[derive(IntoElement)]
+pub struct StorySection {
+    description: Option<SharedString>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl StorySection {
+    pub fn new() -> Self {
+        Self {
+            description: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+}
+
+impl RenderOnce for StorySection {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with(
+            self.children.into_iter(),
+            || Story::divider().into_any_element(),
+        ));
+
+        Story::section()
+            // Section title
+            .py_2()
+            // Section description
+            .when_some(self.description.clone(), |section, description| {
+                section.child(Story::description(description))
+            })
+            .child(div().flex().flex_col().gap_2().children(children))
+            .child(Story::divider())
+    }
+}
+
+impl ParentElement for StorySection {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
 }

crates/storybook2/Cargo.toml 🔗

@@ -18,6 +18,7 @@ dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
 editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
+indoc.workspace = true
 itertools = "0.11.0"
 language = { package = "language2", path = "../language2" }
 log.workspace = true

crates/storybook2/src/stories/text.rs 🔗

@@ -1,8 +1,9 @@
 use gpui::{
-    blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
-    Styled, StyledText, View, VisualContext, WindowContext,
+    div, green, red, Component, HighlightStyle, InteractiveText, IntoElement, ParentElement,
+    Render, Styled, StyledText, View, VisualContext, WindowContext,
 };
-use ui::v_stack;
+use indoc::indoc;
+use story::*;
 
 pub struct TextStory;
 
@@ -13,62 +14,164 @@ impl TextStory {
 }
 
 impl Render for TextStory {
-    type Element = Div;
+    type Element = Component<StoryContainer>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        v_stack()
-            .bg(blue())
-            .child(
-                div()
-                    .flex()
-                    .child(div().max_w_96().bg(white()).child(concat!(
-        "max-width: 96. The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    ))),
-            )
-            .child(div().h_5())
-            .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
-        "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    )))
-            .child(div().h_5())
-            .child(
-                div()
-                    .flex()
-                    .child(div().min_w_96().bg(white()).child(concat!(
-    "min-width: 96. The quick brown fox jumps over the lazy dog. ",
-    "Meanwhile, the lazy dog decided it was time for a change. ",
-    "He started daily workout routines, ate healthier and became the fastest dog in town.",
-))))
-            .child(div().h_5())
-            .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
-        "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
-        "Meanwhile, the lazy dog decided it was time for a change. ",
-        "He started daily workout routines, ate healthier and became the fastest dog in town.",
-    ))))
-            // NOTE: When rendering text in a horizonal flex container,
-            // Taffy will not pass width constraints down from the parent.
-            // To fix this, render text in a praent with overflow: hidden, which
-                    .child(div().h_5())
-                    .child(div().flex().w_96().bg(red()).child(concat!(
-                "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
-                "Meanwhile, the lazy dog decided it was time for a change. ",
-                "He started daily workout routines, ate healthier and became the fastest dog in town.",
-            ))).child(
-                InteractiveText::new(
-                    "interactive",
-                    StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
-                        (6..11, HighlightStyle {
-                            background_color: Some(green()),
-                            ..Default::default()
+        StoryContainer::new("Text Story", "crates/storybook2/src/stories/text.rs")
+            .children(
+                vec![
+
+            StorySection::new()
+                .child(
+                    StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!"))
+                        .usage(indoc! {r##"
+                            div()
+                                .child("Hello World!")
+                            "##
                         }),
-                    ]),
                 )
-                .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
-                    println!("Clicked range {range_ix}");
-                })
-            )
+                .child(
+                    StoryItem::new("Wrapping Text",
+                        div().max_w_96()
+                            .child(
+                                concat!(
+                                    "The quick brown fox jumps over the lazy dog. ",
+                                    "Meanwhile, the lazy dog decided it was time for a change. ",
+                                    "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                                )
+                            )
+                    )
+                    .description("Set a width or max-width to enable text wrapping.")
+                    .usage(indoc! {r##"
+                        div()
+                            .max_w_96()
+                            .child("Some text that you want to wrap.")
+                        "##
+                    })
+                )
+                .child(
+                    StoryItem::new("tbd",
+                    div().flex().w_96().child(div().overflow_hidden().child(concat!(
+                            "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+                            "Meanwhile, the lazy dog decided it was time for a change. ",
+                            "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                        )))
+                    )
+                )
+                .child(
+                    StoryItem::new("Text in Horizontal Flex",
+                        div().flex().w_96().bg(red()).child(concat!(
+                                        "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+                                        "Meanwhile, the lazy dog decided it was time for a change. ",
+                                        "He started daily workout routines, ate healthier and became the fastest dog in town.",
+                                    ))
+                    )
+                    .usage(indoc! {r##"
+                        // NOTE: When rendering text in a horizonal flex container,
+                        // Taffy will not pass width constraints down from the parent.
+                        // To fix this, render text in a parent with overflow: hidden
+
+                        div()
+                            .max_w_96()
+                            .child("Some text that you want to wrap.")
+                        "##
+                    })
+                )
+                .child(
+                    StoryItem::new("Interactive Text",
+                        InteractiveText::new(
+                            "interactive",
+                            StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+                                (6..11, HighlightStyle {
+                                    background_color: Some(green()),
+                                    ..Default::default()
+                                }),
+                            ]),
+                        )
+                        .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+                            println!("Clicked range {range_ix}");
+                        })
+                    )
+                    .usage(indoc! {r##"
+                        InteractiveText::new(
+                            "interactive",
+                            StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+                                (6..11, HighlightStyle {
+                                    background_color: Some(green()),
+                                    ..Default::default()
+                                }),
+                            ]),
+                        )
+                        .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+                            println!("Clicked range {range_ix}");
+                        })
+                        "##
+                    })
+                )
+        ]
+            ).into_element()
     }
 }
+
+// TODO: Check all were updated to new style and remove
+
+// impl Render for TextStory {
+//     type Element = Div;
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+//         v_stack()
+//             .bg(blue())
+//             .child(
+//                 div()
+//                     .flex()
+//                     .child(div().max_w_96().bg(white()).child(concat!(
+//         "max-width: 96. The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     ))),
+//             )
+//             .child(div().h_5())
+//             .child(div().flex().flex_col().w_96().bg(white()).child(concat!(
+//         "flex-col. width: 96; The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     )))
+//             .child(div().h_5())
+//             .child(
+//                 div()
+//                     .flex()
+//                     .child(div().min_w_96().bg(white()).child(concat!(
+//     "min-width: 96. The quick brown fox jumps over the lazy dog. ",
+//     "Meanwhile, the lazy dog decided it was time for a change. ",
+//     "He started daily workout routines, ate healthier and became the fastest dog in town.",
+// ))))
+//             .child(div().h_5())
+//             .child(div().flex().w_96().bg(white()).child(div().overflow_hidden().child(concat!(
+//         "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ",
+//         "Meanwhile, the lazy dog decided it was time for a change. ",
+//         "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//     ))))
+//             // NOTE: When rendering text in a horizonal flex container,
+//             // Taffy will not pass width constraints down from the parent.
+//             // To fix this, render text in a parent with overflow: hidden
+//                     .child(div().h_5())
+//                     .child(div().flex().w_96().bg(red()).child(concat!(
+//                 "flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
+//                 "Meanwhile, the lazy dog decided it was time for a change. ",
+//                 "He started daily workout routines, ate healthier and became the fastest dog in town.",
+//             ))).child(
+//                 InteractiveText::new(
+//                     "interactive",
+//                     StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+//                         (6..11, HighlightStyle {
+//                             background_color: Some(green()),
+//                             ..Default::default()
+//                         }),
+//                     ]),
+//                 )
+//                 .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
+//                     println!("Clicked range {range_ix}");
+//                 })
+//             )
+//     }
+// }

crates/storybook2/src/storybook2.rs 🔗

@@ -19,6 +19,7 @@ use ui::prelude::*;
 
 use crate::assets::Assets;
 use crate::story_selector::{ComponentStory, StorySelector};
+pub use indoc::indoc;
 
 // gpui::actions! {
 //     storybook,

crates/ui2/src/components/stories/icon_button.rs 🔗

@@ -1,5 +1,5 @@
-use gpui::{Div, Render};
-use story::Story;
+use gpui::{Component, Render};
+use story::{StoryContainer, StoryItem, StorySection};
 
 use crate::{prelude::*, Tooltip};
 use crate::{Icon, IconButton};
@@ -7,57 +7,167 @@ use crate::{Icon, IconButton};
 pub struct IconButtonStory;
 
 impl Render for IconButtonStory {
-    type Element = Div;
+    type Element = Component<StoryContainer>;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        Story::container()
-            .child(Story::title_for::<IconButton>())
-            .child(Story::label("Default"))
-            .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
-            .child(Story::label("Selected"))
-            .child(
-                div()
-                    .w_8()
-                    .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
-            )
-            .child(Story::label("Selected with `selected_icon`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("icon_a", Icon::AudioOn)
-                        .selected(true)
-                        .selected_icon(Icon::AudioOff),
-                ),
-            )
-            .child(Story::label("Disabled"))
-            .child(
-                div()
-                    .w_8()
-                    .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
-            )
-            .child(Story::label("With `on_click`"))
-            .child(
-                div()
-                    .w_8()
-                    .child(
-                        IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
-                            println!("Clicked!");
-                        }),
-                    ),
-            )
-            .child(Story::label("With `tooltip`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("with_tooltip", Icon::MessageBubbles)
-                        .tooltip(|cx| Tooltip::text("Open messages", cx)),
-                ),
-            )
-            .child(Story::label("Selected with `tooltip`"))
-            .child(
-                div().w_8().child(
-                    IconButton::new("selected_with_tooltip", Icon::InlayHint)
-                        .selected(true)
-                        .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
-                ),
-            )
+        let default_button = StoryItem::new(
+            "Default",
+            IconButton::new("default_icon_button", Icon::Hash),
+        )
+        .description("Displays an icon button.")
+        .usage(
+            r#"
+            IconButton::new("default_icon_button", Icon::Hash)
+        "#,
+        );
+
+        let selected_button = StoryItem::new(
+            "Selected",
+            IconButton::new("selected_icon_button", Icon::Hash).selected(true),
+        )
+        .description("Displays an icon button that is selected.")
+        .usage(
+            r#"
+            IconButton::new("selected_icon_button", Icon::Hash).selected(true)
+        "#,
+        );
+
+        let selected_with_selected_icon = StoryItem::new(
+            "Selected with `selected_icon`",
+            IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+                .selected(true)
+                .selected_icon(Icon::AudioOff),
+        )
+        .description(
+            "Displays an icon button that is selected and shows a different icon when selected.",
+        )
+        .usage(
+            r#"
+            IconButton::new("selected_with_selected_icon_button", Icon::AudioOn)
+                .selected(true)
+                .selected_icon(Icon::AudioOff)
+        "#,
+        );
+
+        let disabled_button = StoryItem::new(
+            "Disabled",
+            IconButton::new("disabled_icon_button", Icon::Hash).disabled(true),
+        )
+        .description("Displays an icon button that is disabled.")
+        .usage(
+            r#"
+            IconButton::new("disabled_icon_button", Icon::Hash).disabled(true)
+        "#,
+        );
+
+        let with_on_click_button = StoryItem::new(
+            "With `on_click`",
+            IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+                println!("Clicked!");
+            }),
+        )
+        .description("Displays an icon button which triggers an event on click.")
+        .usage(
+            r#"
+            IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| {
+                println!("Clicked!");
+            })
+        "#,
+        );
+
+        let with_tooltip_button = StoryItem::new(
+            "With `tooltip`",
+            IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+                .tooltip(|cx| Tooltip::text("Open messages", cx)),
+        )
+        .description("Displays an icon button that has a tooltip when hovered.")
+        .usage(
+            r#"
+            IconButton::new("with_tooltip_button", Icon::MessageBubbles)
+                .tooltip(|cx| Tooltip::text("Open messages", cx))
+        "#,
+        );
+
+        let selected_with_tooltip_button = StoryItem::new(
+            "Selected with `tooltip`",
+            IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+                .selected(true)
+                .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+        )
+        .description("Displays a selected icon button with tooltip.")
+        .usage(
+            r#"
+            IconButton::new("selected_with_tooltip_button", Icon::InlayHint)
+                .selected(true)
+                .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx))
+        "#,
+        );
+
+        let buttons = vec![
+            default_button,
+            selected_button,
+            selected_with_selected_icon,
+            disabled_button,
+            with_on_click_button,
+            with_tooltip_button,
+            selected_with_tooltip_button,
+        ];
+
+        StoryContainer::new(
+            "Icon Button",
+            "crates/ui2/src/components/stories/icon_button.rs",
+        )
+        .children(vec![StorySection::new().children(buttons)])
+        .into_element()
+
+        // Story::container()
+        //     .child(Story::title_for::<IconButton>())
+        //     .child(Story::label("Default"))
+        //     .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+        //     .child(Story::label("Selected"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
+        //     )
+        //     .child(Story::label("Selected with `selected_icon`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("icon_a", Icon::AudioOn)
+        //                 .selected(true)
+        //                 .selected_icon(Icon::AudioOff),
+        //         ),
+        //     )
+        //     .child(Story::label("Disabled"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
+        //     )
+        //     .child(Story::label("With `on_click`"))
+        //     .child(
+        //         div()
+        //             .w_8()
+        //             .child(
+        //                 IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
+        //                     println!("Clicked!");
+        //                 }),
+        //             ),
+        //     )
+        //     .child(Story::label("With `tooltip`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("with_tooltip", Icon::MessageBubbles)
+        //                 .tooltip(|cx| Tooltip::text("Open messages", cx)),
+        //         ),
+        //     )
+        //     .child(Story::label("Selected with `tooltip`"))
+        //     .child(
+        //         div().w_8().child(
+        //             IconButton::new("selected_with_tooltip", Icon::InlayHint)
+        //                 .selected(true)
+        //                 .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
+        //         ),
+        //     )
     }
 }