dioxus_ui_system/molecules/
badge.rs1use crate::atoms::{Icon, IconColor, IconSize};
6use crate::styles::Style;
7use crate::theme::{use_style, use_theme};
8use dioxus::prelude::*;
9
10#[derive(Default, Clone, PartialEq)]
12pub enum BadgeVariant {
13 #[default]
15 Default,
16 Secondary,
18 Success,
20 Warning,
22 Destructive,
24 Outline,
26 Ghost,
28}
29
30#[derive(Default, Copy, Clone, PartialEq)]
32pub enum BadgeSize {
33 Sm,
35 #[default]
37 Md,
38 Lg,
40}
41
42#[derive(Props, Clone, PartialEq)]
44pub struct BadgeProps {
45 pub children: Element,
47 #[props(default)]
49 pub variant: BadgeVariant,
50 #[props(default)]
52 pub size: BadgeSize,
53 #[props(default)]
55 pub icon: Option<String>,
56 #[props(default)]
58 pub onclick: Option<EventHandler<MouseEvent>>,
59 #[props(default)]
61 pub style: Option<String>,
62 #[props(default)]
64 pub class: Option<String>,
65}
66
67#[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 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 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 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 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
216fn 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#[derive(Props, Clone, PartialEq)]
225pub struct StatusBadgeProps {
226 pub status: String,
228 #[props(default)]
230 pub status_type: StatusType,
231}
232
233#[derive(Default, Clone, PartialEq)]
235pub enum StatusType {
236 #[default]
237 Default,
238 Info,
239 Success,
240 Warning,
241 Error,
242}
243
244#[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}