diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 2264ae8537..7bedd3e4d8 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -1,4 +1,5 @@ mod avatar; +mod banner; mod button; mod content_group; mod context_menu; @@ -37,6 +38,7 @@ mod tooltip; mod stories; pub use avatar::*; +pub use banner::*; pub use button::*; pub use content_group::*; pub use context_menu::*; diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs new file mode 100644 index 0000000000..2cf265882a --- /dev/null +++ b/crates/ui/src/components/banner.rs @@ -0,0 +1,192 @@ +use crate::prelude::*; +use gpui::{AnyElement, IntoElement, ParentElement, Styled}; + +/// Severity levels that determine the style of the banner. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Info, + Success, + Warning, + Error, +} + +/// Banners provide informative and brief messages without interrupting the user. +/// This component offers four severity levels that can be used depending on the message. +/// +/// # Usage Example +/// +/// ``` +/// use ui::{Banner}; +/// +/// Banner::new() +/// .severity(Severity::Info) +/// .children(Label::new("This is an informational message")) +/// .action_slot( +/// Button::new("learn-more", "Learn More") +/// .icon(IconName::ArrowUpRight) +/// .icon_size(IconSize::XSmall) +/// .icon_position(IconPosition::End), +/// ) +/// ``` +#[derive(IntoElement, IntoComponent)] +#[component(scope = "Notification")] +pub struct Banner { + severity: Severity, + children: Option, + icon: Option<(IconName, Option)>, + action_slot: Option, +} + +impl Banner { + /// Creates a new `Banner` component with default styling. + pub fn new() -> Self { + Self { + severity: Severity::Info, + children: None, + icon: None, + action_slot: None, + } + } + + /// Sets the severity of the banner. + pub fn severity(mut self, severity: Severity) -> Self { + self.severity = severity; + self + } + + /// Sets an icon to display in the banner with an optional color. + pub fn icon(mut self, icon: IconName, color: Option>) -> Self { + self.icon = Some((icon, color.map(|c| c.into()))); + self + } + + /// A slot for actions, such as CTA or dismissal buttons. + pub fn action_slot(mut self, element: impl IntoElement) -> Self { + self.action_slot = Some(element.into_any_element()); + self + } + + /// A general container for the banner's main content. + pub fn children(mut self, element: impl IntoElement) -> Self { + self.children = Some(element.into_any_element()); + self + } +} + +impl RenderOnce for Banner { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let base = h_flex() + .py_0p5() + .rounded_sm() + .flex_wrap() + .justify_between() + .border_1(); + + let (icon, icon_color, bg_color, border_color) = match self.severity { + Severity::Info => ( + IconName::Info, + Color::Muted, + cx.theme().status().info_background.opacity(0.5), + cx.theme().colors().border_variant, + ), + Severity::Success => ( + IconName::Check, + Color::Success, + cx.theme().status().success.opacity(0.1), + cx.theme().status().success.opacity(0.2), + ), + Severity::Warning => ( + IconName::Warning, + Color::Warning, + cx.theme().status().warning_background.opacity(0.5), + cx.theme().status().warning_border.opacity(0.4), + ), + Severity::Error => ( + IconName::XCircle, + Color::Error, + cx.theme().status().error.opacity(0.1), + cx.theme().status().error.opacity(0.2), + ), + }; + + let mut container = base.bg(bg_color).border_color(border_color); + + let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll(); + + if self.icon.is_none() { + content_area = + content_area.child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)); + } + + if let Some(children) = self.children { + content_area = content_area.child(children); + } + + if let Some(action_slot) = self.action_slot { + container = container + .pl_2() + .pr_0p5() + .gap_2() + .child(content_area) + .child(action_slot); + } else { + container = container.px_2().child(content_area); + } + + container + } +} + +impl ComponentPreview for Banner { + fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement { + let severity_examples = vec![ + single_example( + "Default", + Banner::new() + .children(Label::new("This is a default banner with no customization")) + .into_any_element(), + ), + single_example( + "Info", + Banner::new() + .severity(Severity::Info) + .children(Label::new("This is an informational message")) + .action_slot( + Button::new("learn-more", "Learn More") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::XSmall) + .icon_position(IconPosition::End), + ) + .into_any_element(), + ), + single_example( + "Success", + Banner::new() + .severity(Severity::Success) + .children(Label::new("Operation completed successfully")) + .action_slot(Button::new("dismiss", "Dismiss")) + .into_any_element(), + ), + single_example( + "Warning", + Banner::new() + .severity(Severity::Warning) + .children(Label::new("Your settings file uses deprecated settings")) + .action_slot(Button::new("update", "Update Settings")) + .into_any_element(), + ), + single_example( + "Error", + Banner::new() + .severity(Severity::Error) + .children(Label::new("Connection error: unable to connect to server")) + .action_slot(Button::new("reconnect", "Retry")) + .into_any_element(), + ), + ]; + + example_group(severity_examples) + .vertical() + .into_any_element() + } +}