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.