Skip to main content

badges_rs/
common.rs

1// Copyright 2026 Open SASS Core Maintainers.
2//
3// Licensed under the MIT license
4// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
5// option. This file may not be copied, modified, or distributed
6// except according to those terms.
7
8/// Rendered size of a [`Badge`] component.
9///
10/// Controls `min-width`, `height`, `padding`, and `font-size` when the badge
11/// has children (label mode), or `width` and `height` when it is a dot.
12///
13/// # Default
14///
15/// [`Size::Md`] is the default variant.
16///
17/// # Examples
18///
19/// ```rust
20/// use badges_rs::Size;
21///
22/// let cls = Size::Sm.to_class();
23/// assert_eq!(cls, "badge--sm");
24///
25/// let style = Size::Lg.to_label_style();
26/// assert!(style.contains("24px"));
27/// ```
28#[derive(Debug, Clone, PartialEq, Default, Copy)]
29pub enum Size {
30    /// Small: 16 x 16 px container, 10 px font.
31    Sm,
32
33    /// Medium: 20 x 20 px container, 11 px font. This is the default.
34    #[default]
35    Md,
36
37    /// Large: 24 x 24 px container, 12 px font.
38    Lg,
39}
40
41impl Size {
42    /// Returns the BEM modifier CSS class for this size.
43    ///
44    /// # Returns
45    ///
46    /// One of `"badge--sm"`, `"badge--md"`, or `"badge--lg"`.
47    pub fn to_class(self) -> &'static str {
48        match self {
49            Self::Sm => "badge--sm",
50            Self::Md => "badge--md",
51            Self::Lg => "badge--lg",
52        }
53    }
54
55    /// Returns inline CSS for label (with-content) mode.
56    ///
57    /// Sets `min-width`, `height`, `font-size`, and `padding`.
58    pub fn to_label_style(self) -> &'static str {
59        match self {
60            Self::Sm => "min-width: 16px; height: 16px; font-size: 10px; padding: 0 4px;",
61            Self::Md => "min-width: 20px; height: 20px; font-size: 11px; padding: 0 5px;",
62            Self::Lg => "min-width: 24px; height: 24px; font-size: 12px; padding: 0 6px;",
63        }
64    }
65
66    /// Returns inline CSS for dot (no-content) mode.
67    ///
68    /// Sets `width` and `height` only, no padding or min-width.
69    pub fn to_dot_style(self) -> &'static str {
70        match self {
71            Self::Sm => "width: 8px; height: 8px;",
72            Self::Md => "width: 10px; height: 10px;",
73            Self::Lg => "width: 12px; height: 12px;",
74        }
75    }
76}
77
78/// Color theme applied to the [`Badge`] component.
79///
80/// Each variant maps to a distinct palette across all three visual variants
81/// (`primary`, `secondary`, `soft`).
82///
83/// # Default
84///
85/// [`Color::Default`] is the default variant.
86///
87/// # Examples
88///
89/// ```rust
90/// use badges_rs::Color;
91///
92/// let style = Color::Danger.to_primary_style();
93/// assert!(style.contains("#dc2626"));
94///
95/// let cls = Color::Accent.to_class();
96/// assert_eq!(cls, "badge--accent");
97/// ```
98#[derive(Debug, Clone, PartialEq, Default, Copy)]
99pub enum Color {
100    /// Neutral gray.
101    #[default]
102    Default,
103
104    /// Accent purple: `#7c3aed`.
105    Accent,
106
107    /// Success green: `#16a34a`.
108    Success,
109
110    /// Warning amber: `#d97706`.
111    Warning,
112
113    /// Danger red: `#dc2626`.
114    Danger,
115}
116
117impl Color {
118    /// Returns the BEM modifier CSS class for this color.
119    ///
120    /// # Returns
121    ///
122    /// One of `"badge--default"`, `"badge--accent"`, `"badge--success"`,
123    /// `"badge--warning"`, or `"badge--danger"`.
124    pub fn to_class(self) -> &'static str {
125        match self {
126            Self::Default => "badge--default",
127            Self::Accent => "badge--accent",
128            Self::Success => "badge--success",
129            Self::Warning => "badge--warning",
130            Self::Danger => "badge--danger",
131        }
132    }
133
134    /// Returns the filled (primary variant) inline CSS for this color.
135    ///
136    /// Uses a saturated background with white text.
137    pub fn to_primary_style(self) -> &'static str {
138        match self {
139            Self::Default => "background-color: #4b5563; color: #ffffff;",
140            Self::Accent => "background-color: #7c3aed; color: #ffffff;",
141            Self::Success => "background-color: #16a34a; color: #ffffff;",
142            Self::Warning => "background-color: #d97706; color: #ffffff;",
143            Self::Danger => "background-color: #dc2626; color: #ffffff;",
144        }
145    }
146
147    /// Returns the outlined (secondary variant) inline CSS for this color.
148    ///
149    /// Uses a white background with a coloured border and matching text.
150    pub fn to_secondary_style(self) -> &'static str {
151        match self {
152            Self::Default => {
153                "background-color: #ffffff; color: #4b5563; border: 1px solid #4b5563;"
154            }
155            Self::Accent => "background-color: #ffffff; color: #7c3aed; border: 1px solid #7c3aed;",
156            Self::Success => {
157                "background-color: #ffffff; color: #16a34a; border: 1px solid #16a34a;"
158            }
159            Self::Warning => {
160                "background-color: #ffffff; color: #d97706; border: 1px solid #d97706;"
161            }
162            Self::Danger => "background-color: #ffffff; color: #dc2626; border: 1px solid #dc2626;",
163        }
164    }
165
166    /// Returns the tinted (soft variant) inline CSS for this color.
167    ///
168    /// Uses a lightly tinted background with the full-strength text color.
169    pub fn to_soft_style(self) -> &'static str {
170        match self {
171            Self::Default => "background-color: #f3f4f6; color: #4b5563;",
172            Self::Accent => "background-color: #ede9fe; color: #7c3aed;",
173            Self::Success => "background-color: #dcfce7; color: #16a34a;",
174            Self::Warning => "background-color: #fef3c7; color: #d97706;",
175            Self::Danger => "background-color: #fee2e2; color: #dc2626;",
176        }
177    }
178}
179
180/// Visual style variant of the [`Badge`] component.
181///
182/// Controls whether the badge appears filled, outlined, or softly tinted.
183///
184/// # Default
185///
186/// [`Variant::Primary`] is the default variant.
187#[derive(Debug, Clone, PartialEq, Default, Copy)]
188pub enum Variant {
189    /// Filled background, the most visually prominent style.
190    #[default]
191    Primary,
192
193    /// Outlined, white background with a coloured border.
194    Secondary,
195
196    /// Lightly tinted background with full-strength text.
197    Soft,
198}
199
200impl Variant {
201    /// Returns the BEM modifier CSS class for this variant.
202    ///
203    /// # Returns
204    ///
205    /// One of `"badge--primary"`, `"badge--secondary"`, or `"badge--soft"`.
206    pub fn to_class(self) -> &'static str {
207        match self {
208            Self::Primary => "badge--primary",
209            Self::Secondary => "badge--secondary",
210            Self::Soft => "badge--soft",
211        }
212    }
213}
214
215/// Absolute position of the [`Badge`] relative to its [`Anchor`].
216///
217/// Each variant maps to a combination of `top`/`bottom`/`left`/`right` and a
218/// CSS `transform` for precise corner placement.
219///
220/// # Default
221///
222/// [`Placement::TopRight`] is the default variant.
223///
224/// # Examples
225///
226/// ```rust
227/// use badges_rs::{Placement, Shape};
228///
229/// let cls = Placement::BottomLeft.to_class();
230/// assert_eq!(cls, "badge--bottom-left");
231///
232/// let style = Placement::TopRight.to_style(Shape::default());
233/// assert!(style.contains("translate(50%, -50%)"));
234/// ```
235#[derive(Debug, Clone, PartialEq, Default, Copy)]
236pub enum Placement {
237    /// Top-right corner, the most common placement. This is the default.
238    #[default]
239    TopRight,
240
241    /// Top-left corner.
242    TopLeft,
243
244    /// Bottom-right corner.
245    BottomRight,
246
247    /// Bottom-left corner.
248    BottomLeft,
249}
250
251impl Placement {
252    /// Returns the BEM modifier CSS class for this placement.
253    ///
254    /// # Returns
255    ///
256    /// One of `"badge--top-right"`, `"badge--top-left"`,
257    /// `"badge--bottom-right"`, or `"badge--bottom-left"`.
258    pub fn to_class(self) -> &'static str {
259        match self {
260            Self::TopRight => "badge--top-right",
261            Self::TopLeft => "badge--top-left",
262            Self::BottomRight => "badge--bottom-right",
263            Self::BottomLeft => "badge--bottom-left",
264        }
265    }
266
267    /// Returns the inline CSS positioning for this placement.
268    ///
269    /// Uses `top`/`bottom`/`left`/`right` anchors combined with `transform`
270    /// to centre the badge on the corner of its anchor element. Fits securely
271    /// on either a rectangle box or precisely curves onto a 45° arc for circles.
272    pub fn to_style(self, shape: Shape) -> &'static str {
273        match (self, shape) {
274            (Self::TopRight, Shape::Rectangle) => {
275                "top: 0; right: 0; transform: translate(50%, -50%);"
276            }
277            (Self::TopLeft, Shape::Rectangle) => {
278                "top: 0; left: 0; transform: translate(-50%, -50%);"
279            }
280            (Self::BottomRight, Shape::Rectangle) => {
281                "bottom: 0; right: 0; transform: translate(50%, 50%);"
282            }
283            (Self::BottomLeft, Shape::Rectangle) => {
284                "bottom: 0; left: 0; transform: translate(-50%, 50%);"
285            }
286
287            (Self::TopRight, Shape::Circle) => {
288                "top: 14.64%; right: 14.64%; transform: translate(50%, -50%);"
289            }
290            (Self::TopLeft, Shape::Circle) => {
291                "top: 14.64%; left: 14.64%; transform: translate(-50%, -50%);"
292            }
293            (Self::BottomRight, Shape::Circle) => {
294                "bottom: 14.64%; right: 14.64%; transform: translate(50%, 50%);"
295            }
296            (Self::BottomLeft, Shape::Circle) => {
297                "bottom: 14.64%; left: 14.64%; transform: translate(-50%, 50%);"
298            }
299        }
300    }
301}
302
303/// Boundary shape of the element this [`Badge`] anchors to.
304///
305/// Modifies the absolute offset to sit either precisely on a perfectly
306/// round corner (14.64% bounding box offset) or a standard rectangle corner.
307///
308/// # Default
309///
310/// [`Shape::Rectangle`] is the default.
311#[derive(Debug, Clone, PartialEq, Default, Copy)]
312pub enum Shape {
313    /// Bounding coordinates calculate to 0%.
314    #[default]
315    Rectangle,
316
317    /// Bounding coordinates calculate to ~14.64% (`1 - sin(45°)`).
318    Circle,
319}
320
321/// Returns the base inline CSS applied to every [`Badge`] span element.
322///
323/// Sets `position: absolute`, flex centering, full border-radius, font
324/// weight, and pointer-events suppression so the badge does not intercept
325/// mouse events on its anchor.
326pub fn base_badge_style() -> &'static str {
327    "position: absolute; display: inline-flex; align-items: center; justify-content: center; border-radius: 9999px; font-weight: 600; white-space: nowrap; pointer-events: none; line-height: 1; box-sizing: border-box;"
328}
329
330/// Returns the base inline CSS applied to the [`Anchor`] span element.
331///
332/// Sets `position: relative` so that the absolutely-positioned [`Badge`]
333/// resolves against this element, and `display: inline-flex` so the anchor
334/// hugs its child element.
335pub fn base_anchor_style() -> &'static str {
336    "position: relative; display: inline-flex; flex-shrink: 0;"
337}
338
339/// Returns the base inline CSS applied to the [`Label`] span element.
340///
341/// Inherits font settings from the parent [`Badge`] for consistent
342/// typography without re-declaring size values.
343pub fn base_label_style() -> &'static str {
344    "font-size: inherit; font-weight: inherit; line-height: 1;"
345}
346
347// Copyright 2026 Open SASS Core Maintainers.
348//
349// Licensed under the MIT license
350// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
351// option. This file may not be copied, modified, or distributed
352// except according to those terms.