1use dioxus::prelude::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum BadgeStatus {
6 Default,
7 Success,
8 Warning,
9 Error,
10}
11
12impl BadgeStatus {
13 fn as_class(&self) -> &'static str {
14 match self {
15 BadgeStatus::Default => "adui-badge-status-default",
16 BadgeStatus::Success => "adui-badge-status-success",
17 BadgeStatus::Warning => "adui-badge-status-warning",
18 BadgeStatus::Error => "adui-badge-status-error",
19 }
20 }
21}
22
23fn compute_badge_indicator(
24 count: Option<u32>,
25 overflow_count: u32,
26 dot: bool,
27 show_zero: bool,
28) -> (bool, bool, String) {
29 if dot {
30 (true, true, String::new())
31 } else if let Some(c) = count {
32 if c == 0 && !show_zero {
33 (false, false, String::new())
34 } else {
35 let text = if c > overflow_count {
36 format!("{}+", overflow_count)
37 } else {
38 c.to_string()
39 };
40 (true, false, text)
41 }
42 } else {
43 (false, false, String::new())
44 }
45}
46
47#[derive(Clone, Debug, PartialEq)]
49pub enum BadgeColor {
50 Preset(String),
52 Custom(String),
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub enum BadgeSize {
59 #[default]
60 Default,
61 Small,
62}
63
64#[derive(Props, Clone, PartialEq)]
66pub struct BadgeProps {
67 #[props(optional)]
70 pub count: Option<Element>,
71 #[props(optional)]
73 pub count_number: Option<u32>,
74 #[props(default = 99)]
76 pub overflow_count: u32,
77 #[props(default)]
79 pub dot: bool,
80 #[props(default)]
82 pub show_zero: bool,
83 #[props(optional)]
85 pub status: Option<BadgeStatus>,
86 #[props(optional)]
88 pub color: Option<BadgeColor>,
89 #[props(optional)]
91 pub text: Option<String>,
92 #[props(default)]
94 pub size: BadgeSize,
95 #[props(optional)]
97 pub offset: Option<(f32, f32)>,
98 #[props(optional)]
100 pub title: Option<String>,
101 #[props(optional)]
103 pub class: Option<String>,
104 #[props(optional)]
106 pub style: Option<String>,
107 pub children: Option<Element>,
109}
110
111#[component]
113pub fn Badge(props: BadgeProps) -> Element {
114 let BadgeProps {
115 count,
116 count_number,
117 overflow_count,
118 dot,
119 show_zero,
120 status,
121 color,
122 text,
123 size,
124 offset,
125 title,
126 class,
127 style,
128 children,
129 } = props;
130
131 let mut class_list = vec!["adui-badge".to_string()];
132 if let Some(st) = status {
133 class_list.push(st.as_class().into());
134 }
135 if matches!(size, BadgeSize::Small) {
136 class_list.push("adui-badge-sm".into());
137 }
138 if let Some(extra) = class {
139 class_list.push(extra);
140 }
141 let class_attr = class_list.join(" ");
142
143 let mut style_attr = style.unwrap_or_default();
144 if let Some((x, y)) = offset {
145 style_attr.push_str(&format!(
146 "--adui-badge-offset-x: {}px; --adui-badge-offset-y: {}px;",
147 x, y
148 ));
149 }
150 if let Some(BadgeColor::Custom(color_str)) = color {
151 style_attr.push_str(&format!("--adui-badge-color: {};", color_str));
152 } else if let Some(BadgeColor::Preset(preset)) = color {
153 class_list.push(format!("adui-badge-{}", preset));
154 }
155
156 let count_value = count_number;
158 let (show_indicator, is_dot, display_text) =
159 compute_badge_indicator(count_value, overflow_count, dot, show_zero);
160
161 let title_attr = title.unwrap_or_default();
162
163 rsx! {
164 span {
165 class: "{class_attr}",
166 style: "{style_attr}",
167 title: "{title_attr}",
168 if let Some(node) = children { {node} }
169 if show_indicator {
170 if is_dot {
171 span { class: "adui-badge-dot" }
172 } else {
173 span {
174 class: "adui-badge-count",
175 if let Some(custom_count) = count {
176 {custom_count}
177 } else {
178 "{display_text}"
179 }
180 }
181 }
182 }
183 if let Some(status_text) = text {
184 if status.is_some() {
185 span { class: "adui-badge-status-text", "{status_text}" }
186 }
187 }
188 }
189 }
190}
191
192#[derive(Props, Clone, PartialEq)]
194pub struct RibbonProps {
195 pub text: String,
197 #[props(optional)]
199 pub color: Option<BadgeColor>,
200 #[props(default)]
202 pub placement: RibbonPlacement,
203 #[props(optional)]
204 pub class: Option<String>,
205 #[props(optional)]
206 pub style: Option<String>,
207 pub children: Element,
208}
209
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
212pub enum RibbonPlacement {
213 #[default]
214 End,
215 Start,
216}
217
218#[component]
220pub fn Ribbon(props: RibbonProps) -> Element {
221 let RibbonProps {
222 text,
223 color,
224 placement,
225 class,
226 style,
227 children,
228 } = props;
229
230 let mut class_list = vec!["adui-badge-ribbon".to_string()];
231 if matches!(placement, RibbonPlacement::Start) {
232 class_list.push("adui-badge-ribbon-start".into());
233 } else {
234 class_list.push("adui-badge-ribbon-end".into());
235 }
236 if let Some(extra) = class {
237 class_list.push(extra);
238 }
239 let class_attr = class_list.join(" ");
240 let mut style_attr = style.unwrap_or_default();
241 if let Some(BadgeColor::Custom(color_str)) = color {
242 style_attr.push_str(&format!("--adui-badge-ribbon-color: {};", color_str));
243 } else if let Some(BadgeColor::Preset(preset)) = color {
244 class_list.push(format!("adui-badge-ribbon-{}", preset));
245 }
246
247 rsx! {
248 div { class: "adui-badge-ribbon-wrapper",
249 {children}
250 div { class: "{class_attr}", style: "{style_attr}",
251 span { class: "adui-badge-ribbon-text", "{text}" }
252 }
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn dot_mode_ignores_count_and_shows_dot() {
263 let (show, is_dot, text) = compute_badge_indicator(Some(5), 99, true, false);
264 assert!(show);
265 assert!(is_dot);
266 assert!(text.is_empty());
267 }
268
269 #[test]
270 fn zero_count_respects_show_zero_flag() {
271 let (show1, _, _) = compute_badge_indicator(Some(0), 99, false, false);
272 assert!(!show1);
273
274 let (show2, is_dot2, text2) = compute_badge_indicator(Some(0), 99, false, true);
275 assert!(show2);
276 assert!(!is_dot2);
277 assert_eq!(text2, "0");
278 }
279
280 #[test]
281 fn count_overflow_is_capped() {
282 let (show, is_dot, text) = compute_badge_indicator(Some(120), 99, false, true);
283 assert!(show);
284 assert!(!is_dot);
285 assert_eq!(text, "99+");
286 }
287
288 #[test]
289 fn compute_badge_indicator_no_count() {
290 let (show, is_dot, text) = compute_badge_indicator(None, 99, false, false);
291 assert!(!show);
292 assert!(!is_dot);
293 assert!(text.is_empty());
294 }
295
296 #[test]
297 fn compute_badge_indicator_exact_overflow() {
298 let (show, is_dot, text) = compute_badge_indicator(Some(99), 99, false, true);
299 assert!(show);
300 assert!(!is_dot);
301 assert_eq!(text, "99");
302 }
303
304 #[test]
305 fn compute_badge_indicator_one_over_overflow() {
306 let (show, is_dot, text) = compute_badge_indicator(Some(100), 99, false, true);
307 assert!(show);
308 assert!(!is_dot);
309 assert_eq!(text, "99+");
310 }
311
312 #[test]
313 fn compute_badge_indicator_normal_count() {
314 let (show, is_dot, text) = compute_badge_indicator(Some(5), 99, false, true);
315 assert!(show);
316 assert!(!is_dot);
317 assert_eq!(text, "5");
318 }
319
320 #[test]
321 fn badge_status_class_mapping() {
322 assert_eq!(BadgeStatus::Default.as_class(), "adui-badge-status-default");
323 assert_eq!(BadgeStatus::Success.as_class(), "adui-badge-status-success");
324 assert_eq!(BadgeStatus::Warning.as_class(), "adui-badge-status-warning");
325 assert_eq!(BadgeStatus::Error.as_class(), "adui-badge-status-error");
326 }
327
328 #[test]
329 fn badge_status_all_variants() {
330 let variants = [
331 BadgeStatus::Default,
332 BadgeStatus::Success,
333 BadgeStatus::Warning,
334 BadgeStatus::Error,
335 ];
336 for variant in variants.iter() {
337 let class = variant.as_class();
338 assert!(!class.is_empty());
339 assert!(class.starts_with("adui-badge-status-"));
340 }
341 }
342
343 #[test]
344 fn badge_status_equality() {
345 assert_eq!(BadgeStatus::Default, BadgeStatus::Default);
346 assert_eq!(BadgeStatus::Success, BadgeStatus::Success);
347 assert_ne!(BadgeStatus::Default, BadgeStatus::Error);
348 }
349
350 #[test]
351 fn badge_status_clone() {
352 let original = BadgeStatus::Warning;
353 let cloned = original;
354 assert_eq!(original, cloned);
355 assert_eq!(original.as_class(), cloned.as_class());
356 }
357
358 #[test]
359 fn badge_color_preset() {
360 let color = BadgeColor::Preset("primary".to_string());
361 match color {
362 BadgeColor::Preset(s) => assert_eq!(s, "primary"),
363 _ => panic!("Expected Preset variant"),
364 }
365 }
366
367 #[test]
368 fn badge_color_custom() {
369 let color = BadgeColor::Custom("#ff0000".to_string());
370 match color {
371 BadgeColor::Custom(s) => assert_eq!(s, "#ff0000"),
372 _ => panic!("Expected Custom variant"),
373 }
374 }
375
376 #[test]
377 fn badge_color_equality() {
378 let preset1 = BadgeColor::Preset("primary".to_string());
379 let preset2 = BadgeColor::Preset("primary".to_string());
380 let preset3 = BadgeColor::Preset("success".to_string());
381 assert_eq!(preset1, preset2);
382 assert_ne!(preset1, preset3);
383
384 let custom1 = BadgeColor::Custom("#ff0000".to_string());
385 let custom2 = BadgeColor::Custom("#ff0000".to_string());
386 let custom3 = BadgeColor::Custom("#00ff00".to_string());
387 assert_eq!(custom1, custom2);
388 assert_ne!(custom1, custom3);
389
390 assert_ne!(preset1, custom1);
391 }
392
393 #[test]
394 fn badge_color_clone() {
395 let original = BadgeColor::Preset("primary".to_string());
396 let cloned = original.clone();
397 assert_eq!(original, cloned);
398 }
399
400 #[test]
401 fn badge_size_default_value() {
402 assert_eq!(BadgeSize::Default, BadgeSize::default());
403 }
404
405 #[test]
406 fn badge_size_all_variants() {
407 assert_eq!(BadgeSize::Default, BadgeSize::Default);
408 assert_eq!(BadgeSize::Small, BadgeSize::Small);
409 assert_ne!(BadgeSize::Default, BadgeSize::Small);
410 }
411
412 #[test]
413 fn badge_size_equality() {
414 let size1 = BadgeSize::Default;
415 let size2 = BadgeSize::Default;
416 let size3 = BadgeSize::Small;
417 assert_eq!(size1, size2);
418 assert_ne!(size1, size3);
419 }
420
421 #[test]
422 fn badge_size_clone() {
423 let original = BadgeSize::Small;
424 let cloned = original;
425 assert_eq!(original, cloned);
426 }
427
428 #[test]
429 fn ribbon_placement_default() {
430 assert_eq!(RibbonPlacement::End, RibbonPlacement::default());
431 }
432
433 #[test]
434 fn ribbon_placement_all_variants() {
435 assert_eq!(RibbonPlacement::End, RibbonPlacement::End);
436 assert_eq!(RibbonPlacement::Start, RibbonPlacement::Start);
437 assert_ne!(RibbonPlacement::End, RibbonPlacement::Start);
438 }
439
440 #[test]
441 fn ribbon_placement_equality() {
442 let placement1 = RibbonPlacement::End;
443 let placement2 = RibbonPlacement::End;
444 let placement3 = RibbonPlacement::Start;
445 assert_eq!(placement1, placement2);
446 assert_ne!(placement1, placement3);
447 }
448
449 #[test]
450 fn ribbon_placement_clone() {
451 let original = RibbonPlacement::Start;
452 let cloned = original;
453 assert_eq!(original, cloned);
454 }
455
456 #[test]
457 fn badge_props_defaults() {
458 }
464
465 #[test]
466 fn compute_badge_indicator_edge_cases() {
467 let (show, is_dot, text) = compute_badge_indicator(Some(999999), 99, false, true);
469 assert!(show);
470 assert!(!is_dot);
471 assert_eq!(text, "99+");
472 }
473
474 #[test]
475 fn compute_badge_indicator_one_below_overflow() {
476 let (show, is_dot, text) = compute_badge_indicator(Some(98), 99, false, true);
477 assert!(show);
478 assert!(!is_dot);
479 assert_eq!(text, "98");
480 }
481
482 #[test]
483 fn compute_badge_indicator_dot_with_show_zero() {
484 let (show, is_dot, text) = compute_badge_indicator(Some(0), 99, true, false);
486 assert!(show);
487 assert!(is_dot);
488 assert!(text.is_empty());
489 }
490
491 #[test]
492 fn badge_status_all_variants_equality() {
493 let statuses = [
494 BadgeStatus::Default,
495 BadgeStatus::Success,
496 BadgeStatus::Warning,
497 BadgeStatus::Error,
498 ];
499 for (i, status1) in statuses.iter().enumerate() {
500 for (j, status2) in statuses.iter().enumerate() {
501 if i == j {
502 assert_eq!(status1, status2);
503 } else {
504 assert_ne!(status1, status2);
505 }
506 }
507 }
508 }
509
510 #[test]
511 fn badge_color_preset_vs_custom() {
512 let preset = BadgeColor::Preset("primary".to_string());
513 let custom = BadgeColor::Custom("#ff0000".to_string());
514 assert_ne!(preset, custom);
515 }
516
517 #[test]
518 fn compute_badge_indicator_negative_overflow() {
519 let (show, is_dot, text) = compute_badge_indicator(Some(50), 10, false, true);
521 assert!(show);
522 assert!(!is_dot);
523 assert_eq!(text, "10+");
524 }
525}