Skip to main content

ui_storybook/components/primitives/
badge.rs

1//! `Badge` — small inline label with semantic variants (M-UI.1 /
2//! AUT-121).
3//!
4//! Six kinds covering the recurring badge usages across the product:
5//! neutral counts, accent emphasis, danger states, the live-recording
6//! pulse, plan / pricing labels, and small numeric counts.
7
8use leptos::prelude::*;
9
10/// Semantic badge kind.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum BadgeKind {
13    /// Default — muted background, default text. Used for neutral
14    /// metadata ("Free", "Beta").
15    #[default]
16    Neutral,
17    /// Accent — calls attention without alarming. Used for highlights
18    /// ("New", "Recommended").
19    Accent,
20    /// Danger — red. Used for destructive states or error counts.
21    Danger,
22    /// Live — pulsing red. Used during active recording / capture.
23    Live,
24    /// Plan — outlined neutral. Used for plan / tier labels.
25    Plan,
26    /// Count — small monospace numeric badge. Used for unread / queue
27    /// counts inside menu items.
28    Count,
29}
30
31impl BadgeKind {
32    /// CSS class that controls the badge look.
33    #[must_use]
34    pub fn css(self) -> &'static str {
35        match self {
36            BadgeKind::Neutral => "badge-neutral",
37            BadgeKind::Accent => "badge-accent",
38            BadgeKind::Danger => "badge-danger",
39            BadgeKind::Live => "badge-live",
40            BadgeKind::Plan => "badge-plan",
41            BadgeKind::Count => "badge-count",
42        }
43    }
44}
45
46#[component]
47pub fn Badge(
48    #[prop(optional)] kind: BadgeKind,
49    #[prop(optional, into)] extra_class: String,
50    children: Children,
51) -> impl IntoView {
52    let class = format!(
53        "badge {}{}{}",
54        kind.css(),
55        if extra_class.is_empty() { "" } else { " " },
56        extra_class,
57    );
58    view! { <span class=class>{children()}</span> }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn each_kind_has_unique_class() {
67        let classes = [
68            BadgeKind::Neutral.css(),
69            BadgeKind::Accent.css(),
70            BadgeKind::Danger.css(),
71            BadgeKind::Live.css(),
72            BadgeKind::Plan.css(),
73            BadgeKind::Count.css(),
74        ];
75        let mut sorted = classes.to_vec();
76        sorted.sort_unstable();
77        sorted.dedup();
78        assert_eq!(sorted.len(), classes.len());
79    }
80}