tailwind_rs_core/
dark_mode.rs

1//! Dark mode variant support for tailwind-rs
2//!
3//! This module provides support for Tailwind CSS dark mode variants,
4//! allowing users to specify styles that apply only in dark mode.
5//! Examples: dark:bg-gray-800, dark:text-white, dark:hover:bg-gray-700, etc.
6
7use crate::classes::ClassBuilder;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11/// Represents a dark mode variant in Tailwind CSS
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct DarkModeVariant {
14    /// The base class name (e.g., "bg-gray-800", "text-white")
15    pub class: String,
16    /// Whether this is a hover variant (dark:hover:bg-gray-700)
17    pub is_hover: bool,
18    /// Whether this is a focus variant (dark:focus:bg-gray-700)
19    pub is_focus: bool,
20    /// Whether this is an active variant (dark:active:bg-gray-700)
21    pub is_active: bool,
22    /// Whether this is a disabled variant (dark:disabled:bg-gray-700)
23    pub is_disabled: bool,
24    /// Whether this is a checked variant (dark:checked:bg-gray-700)
25    pub is_checked: bool,
26    /// Whether this is a group hover variant (dark:group-hover:bg-gray-700)
27    pub is_group_hover: bool,
28    /// Whether this is a group focus variant (dark:group-focus:bg-gray-700)
29    pub is_group_focus: bool,
30}
31
32impl DarkModeVariant {
33    /// Create a new dark mode variant
34    pub fn new(class: impl Into<String>) -> Self {
35        Self {
36            class: class.into(),
37            is_hover: false,
38            is_focus: false,
39            is_active: false,
40            is_disabled: false,
41            is_checked: false,
42            is_group_hover: false,
43            is_group_focus: false,
44        }
45    }
46
47    /// Create a dark mode hover variant
48    pub fn hover(class: impl Into<String>) -> Self {
49        Self {
50            class: class.into(),
51            is_hover: true,
52            is_focus: false,
53            is_active: false,
54            is_disabled: false,
55            is_checked: false,
56            is_group_hover: false,
57            is_group_focus: false,
58        }
59    }
60
61    /// Create a dark mode focus variant
62    pub fn focus(class: impl Into<String>) -> Self {
63        Self {
64            class: class.into(),
65            is_hover: false,
66            is_focus: true,
67            is_active: false,
68            is_disabled: false,
69            is_checked: false,
70            is_group_hover: false,
71            is_group_focus: false,
72        }
73    }
74
75    /// Create a dark mode active variant
76    pub fn active(class: impl Into<String>) -> Self {
77        Self {
78            class: class.into(),
79            is_hover: false,
80            is_focus: false,
81            is_active: true,
82            is_disabled: false,
83            is_checked: false,
84            is_group_hover: false,
85            is_group_focus: false,
86        }
87    }
88
89    /// Create a dark mode disabled variant
90    pub fn disabled(class: impl Into<String>) -> Self {
91        Self {
92            class: class.into(),
93            is_hover: false,
94            is_focus: false,
95            is_active: false,
96            is_disabled: true,
97            is_checked: false,
98            is_group_hover: false,
99            is_group_focus: false,
100        }
101    }
102
103    /// Create a dark mode checked variant
104    pub fn checked(class: impl Into<String>) -> Self {
105        Self {
106            class: class.into(),
107            is_hover: false,
108            is_focus: false,
109            is_active: false,
110            is_disabled: false,
111            is_checked: true,
112            is_group_hover: false,
113            is_group_focus: false,
114        }
115    }
116
117    /// Create a dark mode group hover variant
118    pub fn group_hover(class: impl Into<String>) -> Self {
119        Self {
120            class: class.into(),
121            is_hover: false,
122            is_focus: false,
123            is_active: false,
124            is_disabled: false,
125            is_checked: false,
126            is_group_hover: true,
127            is_group_focus: false,
128        }
129    }
130
131    /// Create a dark mode group focus variant
132    pub fn group_focus(class: impl Into<String>) -> Self {
133        Self {
134            class: class.into(),
135            is_hover: false,
136            is_focus: false,
137            is_active: false,
138            is_disabled: false,
139            is_checked: false,
140            is_group_hover: false,
141            is_group_focus: true,
142        }
143    }
144
145    /// Convert to Tailwind CSS class name
146    pub fn to_class_name(&self) -> String {
147        let mut prefix = "dark:".to_string();
148        
149        if self.is_group_hover {
150            prefix.push_str("group-hover:");
151        } else if self.is_group_focus {
152            prefix.push_str("group-focus:");
153        } else if self.is_hover {
154            prefix.push_str("hover:");
155        } else if self.is_focus {
156            prefix.push_str("focus:");
157        } else if self.is_active {
158            prefix.push_str("active:");
159        } else if self.is_disabled {
160            prefix.push_str("disabled:");
161        } else if self.is_checked {
162            prefix.push_str("checked:");
163        }
164        
165        format!("{}{}", prefix, self.class)
166    }
167
168    /// Validate the dark mode variant
169    pub fn validate(&self) -> Result<(), DarkModeVariantError> {
170        if self.class.is_empty() {
171            return Err(DarkModeVariantError::EmptyClass);
172        }
173
174        // Check for conflicting states
175        let state_count = [
176            self.is_hover,
177            self.is_focus,
178            self.is_active,
179            self.is_disabled,
180            self.is_checked,
181            self.is_group_hover,
182            self.is_group_focus,
183        ]
184        .iter()
185        .filter(|&&state| state)
186        .count();
187
188        if state_count > 1 {
189            return Err(DarkModeVariantError::ConflictingStates);
190        }
191
192        Ok(())
193    }
194}
195
196/// Errors that can occur when working with dark mode variants
197#[derive(Debug, thiserror::Error)]
198pub enum DarkModeVariantError {
199    #[error("Empty class name")]
200    EmptyClass,
201    
202    #[error("Conflicting states: only one state modifier can be used at a time")]
203    ConflictingStates,
204}
205
206impl fmt::Display for DarkModeVariant {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        write!(f, "{}", self.to_class_name())
209    }
210}
211
212/// Trait for adding dark mode variant utilities to a class builder
213pub trait DarkModeVariantUtilities {
214    /// Add a dark mode variant
215    fn dark_mode(self, class: impl Into<String>) -> Self;
216    
217    /// Add a dark mode hover variant
218    fn dark_hover(self, class: impl Into<String>) -> Self;
219    
220    /// Add a dark mode focus variant
221    fn dark_focus(self, class: impl Into<String>) -> Self;
222    
223    /// Add a dark mode active variant
224    fn dark_active(self, class: impl Into<String>) -> Self;
225    
226    /// Add a dark mode disabled variant
227    fn dark_disabled(self, class: impl Into<String>) -> Self;
228    
229    /// Add a dark mode checked variant
230    fn dark_checked(self, class: impl Into<String>) -> Self;
231    
232    /// Add a dark mode group hover variant
233    fn dark_group_hover(self, class: impl Into<String>) -> Self;
234    
235    /// Add a dark mode group focus variant
236    fn dark_group_focus(self, class: impl Into<String>) -> Self;
237    
238    /// Add a dark mode background color
239    fn dark_bg(self, color: impl Into<String>) -> Self;
240    
241    /// Add a dark mode text color
242    fn dark_text(self, color: impl Into<String>) -> Self;
243    
244    /// Add a dark mode border color
245    fn dark_border(self, color: impl Into<String>) -> Self;
246    
247    /// Add a dark mode hover background color
248    fn dark_hover_bg(self, color: impl Into<String>) -> Self;
249    
250    /// Add a dark mode hover text color
251    fn dark_hover_text(self, color: impl Into<String>) -> Self;
252    
253    /// Add a dark mode focus background color
254    fn dark_focus_bg(self, color: impl Into<String>) -> Self;
255    
256    /// Add a dark mode focus text color
257    fn dark_focus_text(self, color: impl Into<String>) -> Self;
258}
259
260impl DarkModeVariantUtilities for ClassBuilder {
261    fn dark_mode(self, class: impl Into<String>) -> Self {
262        let variant = DarkModeVariant::new(class);
263        self.class(variant.to_class_name())
264    }
265    
266    fn dark_hover(self, class: impl Into<String>) -> Self {
267        let variant = DarkModeVariant::hover(class);
268        self.class(variant.to_class_name())
269    }
270    
271    fn dark_focus(self, class: impl Into<String>) -> Self {
272        let variant = DarkModeVariant::focus(class);
273        self.class(variant.to_class_name())
274    }
275    
276    fn dark_active(self, class: impl Into<String>) -> Self {
277        let variant = DarkModeVariant::active(class);
278        self.class(variant.to_class_name())
279    }
280    
281    fn dark_disabled(self, class: impl Into<String>) -> Self {
282        let variant = DarkModeVariant::disabled(class);
283        self.class(variant.to_class_name())
284    }
285    
286    fn dark_checked(self, class: impl Into<String>) -> Self {
287        let variant = DarkModeVariant::checked(class);
288        self.class(variant.to_class_name())
289    }
290    
291    fn dark_group_hover(self, class: impl Into<String>) -> Self {
292        let variant = DarkModeVariant::group_hover(class);
293        self.class(variant.to_class_name())
294    }
295    
296    fn dark_group_focus(self, class: impl Into<String>) -> Self {
297        let variant = DarkModeVariant::group_focus(class);
298        self.class(variant.to_class_name())
299    }
300    
301    fn dark_bg(self, color: impl Into<String>) -> Self {
302        self.dark_mode(format!("bg-{}", color.into()))
303    }
304    
305    fn dark_text(self, color: impl Into<String>) -> Self {
306        self.dark_mode(format!("text-{}", color.into()))
307    }
308    
309    fn dark_border(self, color: impl Into<String>) -> Self {
310        self.dark_mode(format!("border-{}", color.into()))
311    }
312    
313    fn dark_hover_bg(self, color: impl Into<String>) -> Self {
314        self.dark_hover(format!("bg-{}", color.into()))
315    }
316    
317    fn dark_hover_text(self, color: impl Into<String>) -> Self {
318        self.dark_hover(format!("text-{}", color.into()))
319    }
320    
321    fn dark_focus_bg(self, color: impl Into<String>) -> Self {
322        self.dark_focus(format!("bg-{}", color.into()))
323    }
324    
325    fn dark_focus_text(self, color: impl Into<String>) -> Self {
326        self.dark_focus(format!("text-{}", color.into()))
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    
334    #[test]
335    fn test_dark_mode_variant_creation() {
336        let variant = DarkModeVariant::new("bg-gray-800");
337        assert_eq!(variant.to_class_name(), "dark:bg-gray-800");
338    }
339    
340    #[test]
341    fn test_dark_mode_hover_variant() {
342        let variant = DarkModeVariant::hover("bg-gray-700");
343        assert_eq!(variant.to_class_name(), "dark:hover:bg-gray-700");
344    }
345    
346    #[test]
347    fn test_dark_mode_focus_variant() {
348        let variant = DarkModeVariant::focus("bg-gray-700");
349        assert_eq!(variant.to_class_name(), "dark:focus:bg-gray-700");
350    }
351    
352    #[test]
353    fn test_dark_mode_active_variant() {
354        let variant = DarkModeVariant::active("bg-gray-700");
355        assert_eq!(variant.to_class_name(), "dark:active:bg-gray-700");
356    }
357    
358    #[test]
359    fn test_dark_mode_disabled_variant() {
360        let variant = DarkModeVariant::disabled("bg-gray-700");
361        assert_eq!(variant.to_class_name(), "dark:disabled:bg-gray-700");
362    }
363    
364    #[test]
365    fn test_dark_mode_checked_variant() {
366        let variant = DarkModeVariant::checked("bg-gray-700");
367        assert_eq!(variant.to_class_name(), "dark:checked:bg-gray-700");
368    }
369    
370    #[test]
371    fn test_dark_mode_group_hover_variant() {
372        let variant = DarkModeVariant::group_hover("bg-gray-700");
373        assert_eq!(variant.to_class_name(), "dark:group-hover:bg-gray-700");
374    }
375    
376    #[test]
377    fn test_dark_mode_group_focus_variant() {
378        let variant = DarkModeVariant::group_focus("bg-gray-700");
379        assert_eq!(variant.to_class_name(), "dark:group-focus:bg-gray-700");
380    }
381    
382    #[test]
383    fn test_dark_mode_variant_validation() {
384        // Valid variants
385        assert!(DarkModeVariant::new("bg-gray-800").validate().is_ok());
386        assert!(DarkModeVariant::hover("bg-gray-700").validate().is_ok());
387        assert!(DarkModeVariant::focus("bg-gray-700").validate().is_ok());
388        
389        // Invalid variants
390        assert!(DarkModeVariant::new("").validate().is_err());
391    }
392    
393    #[test]
394    fn test_dark_mode_variant_display() {
395        let variant = DarkModeVariant::new("bg-gray-800");
396        assert_eq!(format!("{}", variant), "dark:bg-gray-800");
397    }
398    
399    #[test]
400    fn test_dark_mode_variant_utilities() {
401        let classes = ClassBuilder::new()
402            .dark_bg("gray-800")
403            .dark_text("white")
404            .dark_border("gray-700")
405            .dark_hover_bg("gray-700")
406            .dark_hover_text("gray-100")
407            .dark_focus_bg("gray-600")
408            .dark_focus_text("gray-50")
409            .build();
410        
411        let css_classes = classes.to_css_classes();
412        assert!(css_classes.contains("dark:bg-gray-800"));
413        assert!(css_classes.contains("dark:text-white"));
414        assert!(css_classes.contains("dark:border-gray-700"));
415        assert!(css_classes.contains("dark:hover:bg-gray-700"));
416        assert!(css_classes.contains("dark:hover:text-gray-100"));
417        assert!(css_classes.contains("dark:focus:bg-gray-600"));
418        assert!(css_classes.contains("dark:focus:text-gray-50"));
419    }
420    
421    #[test]
422    fn test_dark_mode_variant_utilities_advanced() {
423        let classes = ClassBuilder::new()
424            .dark_mode("bg-gray-800")
425            .dark_hover("bg-gray-700")
426            .dark_focus("bg-gray-600")
427            .dark_active("bg-gray-500")
428            .dark_disabled("bg-gray-400")
429            .dark_checked("bg-gray-300")
430            .dark_group_hover("bg-gray-200")
431            .dark_group_focus("bg-gray-100")
432            .build();
433        
434        let css_classes = classes.to_css_classes();
435        assert!(css_classes.contains("dark:bg-gray-800"));
436        assert!(css_classes.contains("dark:hover:bg-gray-700"));
437        assert!(css_classes.contains("dark:focus:bg-gray-600"));
438        assert!(css_classes.contains("dark:active:bg-gray-500"));
439        assert!(css_classes.contains("dark:disabled:bg-gray-400"));
440        assert!(css_classes.contains("dark:checked:bg-gray-300"));
441        assert!(css_classes.contains("dark:group-hover:bg-gray-200"));
442        assert!(css_classes.contains("dark:group-focus:bg-gray-100"));
443    }
444}