Skip to main content

dioxus_ui_system/molecules/
badge.rs

1//! Badge molecule component
2//!
3//! Small status indicators for highlighting items.
4
5use crate::atoms::{Icon, IconColor, IconSize};
6use crate::styles::Style;
7use crate::theme::{use_style, use_theme};
8use dioxus::prelude::*;
9
10/// Badge variants
11#[derive(Default, Clone, PartialEq)]
12pub enum BadgeVariant {
13    /// Default badge
14    #[default]
15    Default,
16    /// Secondary badge
17    Secondary,
18    /// Success badge
19    Success,
20    /// Warning badge
21    Warning,
22    /// Destructive/error badge
23    Destructive,
24    /// Outline badge
25    Outline,
26    /// Ghost/subtle badge
27    Ghost,
28}
29
30/// Badge sizes
31#[derive(Default, Copy, Clone, PartialEq)]
32pub enum BadgeSize {
33    /// Small badge
34    Sm,
35    /// Medium (default) badge
36    #[default]
37    Md,
38    /// Large badge
39    Lg,
40}
41
42/// Badge properties
43#[derive(Props, Clone, PartialEq)]
44pub struct BadgeProps {
45    /// Badge content
46    pub children: Element,
47    /// Visual variant
48    #[props(default)]
49    pub variant: BadgeVariant,
50    /// Badge size
51    #[props(default)]
52    pub size: BadgeSize,
53    /// Optional leading icon
54    #[props(default)]
55    pub icon: Option<String>,
56    /// Click handler (makes badge interactive)
57    #[props(default)]
58    pub onclick: Option<EventHandler<MouseEvent>>,
59    /// Custom inline styles
60    #[props(default)]
61    pub style: Option<String>,
62    /// Custom class name
63    #[props(default)]
64    pub class: Option<String>,
65}
66
67/// Badge molecule component
68///
69/// # Example
70/// ```rust,ignore
71/// use dioxus_ui_system::molecules::{Badge, BadgeVariant};
72///
73/// rsx! {
74///     Badge {
75///         variant: BadgeVariant::Success,
76///         icon: "check".to_string(),
77///         "Active"
78///     }
79/// }
80/// ```
81#[component]
82pub fn Badge(props: BadgeProps) -> Element {
83    let _theme = use_theme();
84    let variant = props.variant.clone();
85    let size = props.size.clone();
86    let has_icon = props.icon.is_some();
87    let has_onclick = props.onclick.is_some();
88
89    // Interactive state
90    let mut is_hovered = use_signal(|| false);
91
92    let style = use_style(move |t| {
93        let base = Style::new()
94            .rounded(&t.radius, "full")
95            .font_weight(600)
96            .transition("all 150ms ease")
97            .whitespace_nowrap()
98            .select_none();
99
100        // Size
101        let sized = match size {
102            BadgeSize::Sm => base.px(&t.spacing, "sm").py_px(2).font_size(11),
103            BadgeSize::Md => base.px(&t.spacing, "md").py_px(4).font_size(12),
104            BadgeSize::Lg => base.px(&t.spacing, "md").py(&t.spacing, "xs").font_size(14),
105        };
106
107        // Variant styles
108        let styled = match variant {
109            BadgeVariant::Default => {
110                let bg = if is_hovered() && has_onclick {
111                    t.colors.primary.darken(0.1)
112                } else {
113                    t.colors.primary.clone()
114                };
115                sized.bg(&bg).text_color(&t.colors.primary_foreground)
116            }
117            BadgeVariant::Secondary => {
118                let bg = if is_hovered() && has_onclick {
119                    t.colors.secondary.darken(0.05)
120                } else {
121                    t.colors.secondary.clone()
122                };
123                sized.bg(&bg).text_color(&t.colors.secondary_foreground)
124            }
125            BadgeVariant::Success => {
126                let bg = t.colors.success.clone();
127                let fg = if is_dark_color(&bg) {
128                    Color::new(255, 255, 255)
129                } else {
130                    Color::new(0, 0, 0)
131                };
132                sized.bg(&bg).text_color(&fg)
133            }
134            BadgeVariant::Warning => {
135                let bg = t.colors.warning.clone();
136                let fg = Color::new(0, 0, 0);
137                sized.bg(&bg).text_color(&fg)
138            }
139            BadgeVariant::Destructive => {
140                let bg = if is_hovered() && has_onclick {
141                    t.colors.destructive.darken(0.1)
142                } else {
143                    t.colors.destructive.clone()
144                };
145                sized.bg(&bg).text_color(&Color::new(255, 255, 255))
146            }
147            BadgeVariant::Outline => {
148                let border_color = if is_hovered() && has_onclick {
149                    t.colors.foreground.darken(0.2)
150                } else {
151                    t.colors.border.clone()
152                };
153                sized
154                    .border(1, &border_color)
155                    .text_color(&t.colors.foreground)
156            }
157            BadgeVariant::Ghost => {
158                let bg = if is_hovered() && has_onclick {
159                    t.colors.muted.clone()
160                } else {
161                    Color::new_rgba(0, 0, 0, 0.0)
162                };
163                sized.bg(&bg).text_color(&t.colors.foreground)
164            }
165        };
166
167        // Cursor
168        if has_onclick {
169            styled.cursor_pointer().build()
170        } else {
171            styled.build()
172        }
173    });
174
175    let final_style = if let Some(custom) = &props.style {
176        format!("{} {}", style(), custom)
177    } else {
178        style()
179    };
180
181    let class = props.class.clone().unwrap_or_default();
182    let icon_element = props.icon.clone();
183    let onclick_handler = props.onclick.clone();
184
185    rsx! {
186        div {
187            style: "{final_style} display: inline-flex; align-items: center; gap: 4px;",
188            class: "{class}",
189            onmouseenter: move |_| if has_onclick { is_hovered.set(true) },
190            onmouseleave: move |_| is_hovered.set(false),
191            onclick: move |e| {
192                if let Some(handler) = &onclick_handler {
193                    handler.call(e);
194                }
195            },
196
197            if has_icon {
198                Icon {
199                    name: icon_element.unwrap(),
200                    size: match size {
201                        BadgeSize::Sm => IconSize::ExtraSmall,
202                        BadgeSize::Md => IconSize::Small,
203                        BadgeSize::Lg => IconSize::Medium,
204                    },
205                    color: IconColor::Current,
206                }
207            }
208
209            {props.children}
210        }
211    }
212}
213
214use crate::theme::tokens::Color;
215
216/// Determine if a color is dark
217fn is_dark_color(color: &Color) -> bool {
218    let luminance =
219        (0.299 * color.r as f32 + 0.587 * color.g as f32 + 0.114 * color.b as f32) / 255.0;
220    luminance < 0.5
221}
222
223/// Status badge - convenience component for common status indicators
224#[derive(Props, Clone, PartialEq)]
225pub struct StatusBadgeProps {
226    /// Status text
227    pub status: String,
228    /// Status type
229    #[props(default)]
230    pub status_type: StatusType,
231}
232
233/// Status types
234#[derive(Default, Clone, PartialEq)]
235pub enum StatusType {
236    #[default]
237    Default,
238    Info,
239    Success,
240    Warning,
241    Error,
242}
243
244/// Status Badge component
245#[component]
246pub fn StatusBadge(props: StatusBadgeProps) -> Element {
247    let (variant, icon) = match props.status_type {
248        StatusType::Default => (BadgeVariant::Default, None),
249        StatusType::Info => (BadgeVariant::Secondary, Some("info".to_string())),
250        StatusType::Success => (BadgeVariant::Success, Some("check".to_string())),
251        StatusType::Warning => (BadgeVariant::Warning, Some("alert".to_string())),
252        StatusType::Error => (BadgeVariant::Destructive, Some("x".to_string())),
253    };
254
255    rsx! {
256        Badge {
257            variant: variant,
258            icon: icon,
259            "{props.status}"
260        }
261    }
262}