Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ index.scip
node_modules/
.claude/worktrees/
docs/superpowers/

wr.sh
# WASM build artifacts
crates/story-web/www/src/wasm/*.js
crates/story-web/www/src/wasm/*.wasm
Expand Down
1 change: 1 addition & 0 deletions crates/story/src/gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl Gallery {
StoryContainer::panel::<DividerStory>(window, cx),
StoryContainer::panel::<DropdownButtonStory>(window, cx),
StoryContainer::panel::<EditorStory>(window, cx),
StoryContainer::panel::<EmptyStory>(window, cx),
StoryContainer::panel::<FormStory>(window, cx),
StoryContainer::panel::<GroupBoxStory>(window, cx),
StoryContainer::panel::<HoverCardStory>(window, cx),
Expand Down
70 changes: 70 additions & 0 deletions crates/story/src/stories/empty_story.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use gpui::{
App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render,
Styled, Window,
};
use gpui_component::{
button::{Button, ButtonVariants},
empty::Empty,
v_flex, IconName,
};

use crate::section;

pub struct EmptyStory {
focus_handle: FocusHandle,
}

impl super::Story for EmptyStory {
fn title() -> &'static str {
"Empty"
}

fn description() -> &'static str {
"Placeholder for empty states."
}

fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render> {
Self::view(window, cx)
}
}

impl EmptyStory {
pub fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self::new(window, cx))
}

fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}

impl Focusable for EmptyStory {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}

impl Render for EmptyStory {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_4()
.child(section("Minimal").child(
Empty::new()
.title("No items")
.description("Your list is empty."),
))
.child(section("With icon and action").child(
Empty::new()
.icon(IconName::Inbox)
.title("Inbox zero!")
.description("You have no new messages.")
.action(
Button::new("refresh")
.primary()
.label("Refresh"),
),
))
}
}
2 changes: 2 additions & 0 deletions crates/story/src/stories/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod dialog_story;
mod divider_story;
mod dropdown_button_story;
mod editor_story;
mod empty_story;
mod form_story;
mod group_box_story;
mod hover_card_story;
Expand Down Expand Up @@ -81,6 +82,7 @@ pub use dialog_story::DialogStory;
pub use divider_story::DividerStory;
pub use dropdown_button_story::DropdownButtonStory;
pub use editor_story::EditorStory;
pub use empty_story::EmptyStory;
pub use form_story::FormStory;
pub use group_box_story::GroupBoxStory;
pub use hover_card_story::HoverCardStory;
Expand Down
116 changes: 116 additions & 0 deletions crates/ui/src/empty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use gpui::{
div, prelude::FluentBuilder as _, AnyElement, App, IntoElement, ParentElement, RenderOnce,
SharedString, StyleRefinement, Styled, Window,
};

use crate::{button::Button, v_flex, ActiveTheme, StyledExt};

/// An empty‑state container with optional icon, title, description, and action.
#[derive(IntoElement)]
pub struct Empty {
style: StyleRefinement,
icon: Option<AnyElement>,
title: Option<SharedString>,
description: Option<SharedString>,
action: Option<Button>,
}

impl Empty {
/// Create a new empty‑state container.
pub fn new() -> Self {
Self {
style: StyleRefinement::default(),
icon: None,
title: None,
description: None,
action: None,
}
}

/// Set the icon element.
pub fn icon(mut self, icon: impl IntoElement) -> Self {
self.icon = Some(icon.into_any_element());
self
}

/// Set the title text.
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = Some(title.into());
self
}

/// Set the description text.
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}

/// Set the action button.
pub fn action(mut self, action: impl Into<Button>) -> Self {
self.action = Some(action.into());
self
}
}

impl Styled for Empty {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}

impl RenderOnce for Empty {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.items_center()
.justify_center()
.gap_4()
.p_8()
.refine_style(&self.style)
.when_some(self.icon, |this, icon| this.child(icon))
.when_some(self.title, |this, title| {
this.child(
div()
.text_xl()
.font_semibold()
.text_color(cx.theme().foreground)
.child(title),
)
})
.when_some(self.description, |this, desc| {
this.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(desc),
)
})
.when_some(self.action, |this, action| this.child(action))
}
}

#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;

#[gpui::test]
fn test_empty_minimal(cx: &mut TestAppContext) {
cx.update(|_| {
let el = Empty::new().into_any_element();
let _ = el;
});
}

#[gpui::test]
fn test_empty_full(cx: &mut TestAppContext) {
cx.update(|_| {
let el = Empty::new()
.icon(gpui::div())
.title("Nothing here")
.description("Try adding some content")
.action(crate::button::Button::new("add").label("Add Item"))
.into_any_element();
let _ = el;
});
}
}
1 change: 1 addition & 0 deletions crates/ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod description_list;
pub mod dialog;
pub mod divider;
pub mod dock;
pub mod empty;
pub mod form;
pub mod group_box;
pub mod highlighter;
Expand Down
52 changes: 52 additions & 0 deletions docs/docs/components/empty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: Empty
description: Placeholder for empty states.
---

# Empty

Use the `Empty` component to fill a space when there is no content to display. It can show an icon, a title, a description, and an optional action button.

## Import

```sh
use gpui_component::empty::Empty;
```

## Usage

### Basic Empty State

```sh
Empty::new()
.title("No items")
.description("Your list is empty.")
```

### Full Example

```sh
Empty::new()
.icon(IconName::Inbox)
.title("Inbox zero!")
.description("You have no new messages.")
.action(
Button::new("refresh")
.primary()
.label("Refresh"),
)
```

## API Reference

- **`Empty::new()`** – Creates an empty container.
- **`.icon(…)`** – Sets an icon element.
- **`.title(…)`** – Sets the title text.
- **`.description(…)`** – Sets a short description.
- **`.action(…)`** – Adds a button.

The component implements `Styled`, so you can tailor the container with any standard style methods.

## Styling

All default styles are minimal. You can override the layout and spacing via `Styled` to match your design system.
16 changes: 16 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

wr:
watchexec -w ./wr.sh --clear -r "sh ./wr.sh"
test:
cargo nextest run

# -----------------------------------------------------------------------
# GPUI Component story gallery & documentation
# -----------------------------------------------------------------------
# Launch the interactive story gallery (native desktop app)
story:
cargo run -p gpui-component-story

# Start the VitePress documentation website locally
docs:
cd docs && bun install && bun run dev
Loading