tailwind_rs_core/
gradients.rs

1//! Gradient support for tailwind-rs
2//!
3//! This module provides comprehensive support for Tailwind CSS gradients,
4//! including background gradients, gradient directions, and gradient stops.
5//! Examples: bg-gradient-to-r, from-blue-500, via-purple-500, to-pink-500, etc.
6
7use crate::classes::ClassBuilder;
8use crate::utilities::colors::{Color, ColorPalette, ColorShade};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// Represents gradient directions in Tailwind CSS
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum GradientDirection {
15    /// To right
16    ToRight,
17    /// To left
18    ToLeft,
19    /// To top
20    ToTop,
21    /// To bottom
22    ToBottom,
23    /// To top right
24    ToTopRight,
25    /// To top left
26    ToTopLeft,
27    /// To bottom right
28    ToBottomRight,
29    /// To bottom left
30    ToBottomLeft,
31}
32
33impl GradientDirection {
34    /// Convert to Tailwind CSS class name
35    pub fn to_class_name(&self) -> String {
36        match self {
37            GradientDirection::ToRight => "to-r".to_string(),
38            GradientDirection::ToLeft => "to-l".to_string(),
39            GradientDirection::ToTop => "to-t".to_string(),
40            GradientDirection::ToBottom => "to-b".to_string(),
41            GradientDirection::ToTopRight => "to-tr".to_string(),
42            GradientDirection::ToTopLeft => "to-tl".to_string(),
43            GradientDirection::ToBottomRight => "to-br".to_string(),
44            GradientDirection::ToBottomLeft => "to-bl".to_string(),
45        }
46    }
47
48    /// Convert to CSS value
49    pub fn to_css_value(&self) -> String {
50        match self {
51            GradientDirection::ToRight => "to right".to_string(),
52            GradientDirection::ToLeft => "to left".to_string(),
53            GradientDirection::ToTop => "to top".to_string(),
54            GradientDirection::ToBottom => "to bottom".to_string(),
55            GradientDirection::ToTopRight => "to top right".to_string(),
56            GradientDirection::ToTopLeft => "to top left".to_string(),
57            GradientDirection::ToBottomRight => "to bottom right".to_string(),
58            GradientDirection::ToBottomLeft => "to bottom left".to_string(),
59        }
60    }
61}
62
63impl fmt::Display for GradientDirection {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.to_class_name())
66    }
67}
68
69/// Represents gradient stops in Tailwind CSS
70#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub enum GradientStop {
72    /// From stop (starting color)
73    From(Color),
74    /// Via stop (middle color)
75    Via(Color),
76    /// To stop (ending color)
77    To(Color),
78}
79
80impl GradientStop {
81    /// Create a from stop
82    pub fn from(color: Color) -> Self {
83        Self::From(color)
84    }
85
86    /// Create a via stop
87    pub fn via(color: Color) -> Self {
88        Self::Via(color)
89    }
90
91    /// Create a to stop
92    pub fn to(color: Color) -> Self {
93        Self::To(color)
94    }
95
96    /// Convert to Tailwind CSS class name
97    pub fn to_class_name(&self) -> String {
98        match self {
99            GradientStop::From(color) => format!("from-{}", color.to_class_name()),
100            GradientStop::Via(color) => format!("via-{}", color.to_class_name()),
101            GradientStop::To(color) => format!("to-{}", color.to_class_name()),
102        }
103    }
104
105    /// Convert to CSS value
106    pub fn to_css_value(&self) -> String {
107        match self {
108            GradientStop::From(color) => color.to_css_value(),
109            GradientStop::Via(color) => color.to_css_value(),
110            GradientStop::To(color) => color.to_css_value(),
111        }
112    }
113}
114
115impl fmt::Display for GradientStop {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "{}", self.to_class_name())
118    }
119}
120
121/// Represents a complete gradient configuration
122#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
123pub struct Gradient {
124    /// Gradient direction
125    pub direction: GradientDirection,
126    /// Gradient stops
127    pub stops: Vec<GradientStop>,
128}
129
130impl Gradient {
131    /// Create a new gradient
132    pub fn new(direction: GradientDirection) -> Self {
133        Self {
134            direction,
135            stops: Vec::new(),
136        }
137    }
138
139    /// Add a from stop
140    pub fn from(mut self, color: Color) -> Self {
141        self.stops.push(GradientStop::from(color));
142        self
143    }
144
145    /// Add a via stop
146    pub fn via(mut self, color: Color) -> Self {
147        self.stops.push(GradientStop::via(color));
148        self
149    }
150
151    /// Add a to stop
152    pub fn to(mut self, color: Color) -> Self {
153        self.stops.push(GradientStop::to(color));
154        self
155    }
156
157    /// Convert to Tailwind CSS class names
158    pub fn to_class_names(&self) -> Vec<String> {
159        let mut classes = vec![format!("bg-gradient-{}", self.direction.to_class_name())];
160        
161        for stop in &self.stops {
162            classes.push(stop.to_class_name());
163        }
164        
165        classes
166    }
167
168    /// Convert to CSS value
169    pub fn to_css_value(&self) -> String {
170        let mut css = format!("linear-gradient({}, ", self.direction.to_css_value());
171        
172        let stop_values: Vec<String> = self.stops.iter()
173            .map(|stop| stop.to_css_value())
174            .collect();
175        
176        css.push_str(&stop_values.join(", "));
177        css.push(')');
178        css
179    }
180
181    /// Validate the gradient
182    pub fn validate(&self) -> Result<(), GradientError> {
183        if self.stops.is_empty() {
184            return Err(GradientError::NoStops);
185        }
186
187        // Check for at least one from and one to stop
188        let has_from = self.stops.iter().any(|stop| matches!(stop, GradientStop::From(_)));
189        let has_to = self.stops.iter().any(|stop| matches!(stop, GradientStop::To(_)));
190
191        if !has_from {
192            return Err(GradientError::MissingFromStop);
193        }
194
195        if !has_to {
196            return Err(GradientError::MissingToStop);
197        }
198
199        Ok(())
200    }
201}
202
203/// Errors that can occur when working with gradients
204#[derive(Debug, thiserror::Error)]
205pub enum GradientError {
206    #[error("No gradient stops defined")]
207    NoStops,
208    
209    #[error("Missing 'from' stop")]
210    MissingFromStop,
211    
212    #[error("Missing 'to' stop")]
213    MissingToStop,
214}
215
216impl fmt::Display for Gradient {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.to_class_names().join(" "))
219    }
220}
221
222/// Trait for adding gradient utilities to a class builder
223pub trait GradientUtilities {
224    /// Add a gradient direction
225    fn gradient_direction(self, direction: GradientDirection) -> Self;
226    
227    /// Add a gradient from stop
228    fn gradient_from(self, color: Color) -> Self;
229    
230    /// Add a gradient via stop
231    fn gradient_via(self, color: Color) -> Self;
232    
233    /// Add a gradient to stop
234    fn gradient_to(self, color: Color) -> Self;
235    
236    /// Add a complete gradient
237    fn gradient(self, gradient: Gradient) -> Self;
238    
239    /// Add a simple two-color gradient
240    fn gradient_simple(self, direction: GradientDirection, from: Color, to: Color) -> Self;
241    
242    /// Add a three-color gradient
243    fn gradient_three(self, direction: GradientDirection, from: Color, via: Color, to: Color) -> Self;
244    
245    /// Add a gradient from color names
246    fn gradient_from_names(self, direction: GradientDirection, from: &str, to: &str) -> Self;
247    
248    /// Add a gradient from color names with via
249    fn gradient_from_names_with_via(self, direction: GradientDirection, from: &str, via: &str, to: &str) -> Self;
250}
251
252impl GradientUtilities for ClassBuilder {
253    fn gradient_direction(self, direction: GradientDirection) -> Self {
254        self.class(format!("bg-gradient-{}", direction.to_class_name()))
255    }
256    
257    fn gradient_from(self, color: Color) -> Self {
258        self.class(format!("from-{}", color.to_class_name()))
259    }
260    
261    fn gradient_via(self, color: Color) -> Self {
262        self.class(format!("via-{}", color.to_class_name()))
263    }
264    
265    fn gradient_to(self, color: Color) -> Self {
266        self.class(format!("to-{}", color.to_class_name()))
267    }
268    
269    fn gradient(self, gradient: Gradient) -> Self {
270        let mut builder = self;
271        let class_names = gradient.to_class_names();
272        
273        for class_name in class_names {
274            builder = builder.class(class_name);
275        }
276        
277        builder
278    }
279    
280    fn gradient_simple(self, direction: GradientDirection, from: Color, to: Color) -> Self {
281        let gradient = Gradient::new(direction)
282            .from(from)
283            .to(to);
284        
285        self.gradient(gradient)
286    }
287    
288    fn gradient_three(self, direction: GradientDirection, from: Color, via: Color, to: Color) -> Self {
289        let gradient = Gradient::new(direction)
290            .from(from)
291            .via(via)
292            .to(to);
293        
294        self.gradient(gradient)
295    }
296    
297    fn gradient_from_names(self, direction: GradientDirection, from: &str, to: &str) -> Self {
298        // Parse color names (simplified - in real implementation, you'd want more robust parsing)
299        let from_color = parse_color_name(from);
300        let to_color = parse_color_name(to);
301        
302        self.gradient_simple(direction, from_color, to_color)
303    }
304    
305    fn gradient_from_names_with_via(self, direction: GradientDirection, from: &str, via: &str, to: &str) -> Self {
306        // Parse color names (simplified - in real implementation, you'd want more robust parsing)
307        let from_color = parse_color_name(from);
308        let via_color = parse_color_name(via);
309        let to_color = parse_color_name(to);
310        
311        self.gradient_three(direction, from_color, via_color, to_color)
312    }
313}
314
315/// Parse a color name string into a Color struct
316/// This is a simplified implementation - in production, you'd want more robust parsing
317fn parse_color_name(color_name: &str) -> Color {
318    // Handle common color patterns like "blue-500", "red-600", etc.
319    let parts: Vec<&str> = color_name.split('-').collect();
320    
321    if parts.len() == 2 {
322        let palette = match parts[0] {
323            "slate" => ColorPalette::Slate,
324            "gray" => ColorPalette::Gray,
325            "zinc" => ColorPalette::Zinc,
326            "neutral" => ColorPalette::Neutral,
327            "stone" => ColorPalette::Stone,
328            "red" => ColorPalette::Red,
329            "orange" => ColorPalette::Orange,
330            "amber" => ColorPalette::Amber,
331            "yellow" => ColorPalette::Yellow,
332            "lime" => ColorPalette::Lime,
333            "green" => ColorPalette::Green,
334            "emerald" => ColorPalette::Emerald,
335            "teal" => ColorPalette::Teal,
336            "cyan" => ColorPalette::Cyan,
337            "sky" => ColorPalette::Sky,
338            "blue" => ColorPalette::Blue,
339            "indigo" => ColorPalette::Indigo,
340            "violet" => ColorPalette::Violet,
341            "purple" => ColorPalette::Purple,
342            "fuchsia" => ColorPalette::Fuchsia,
343            "pink" => ColorPalette::Pink,
344            "rose" => ColorPalette::Rose,
345            _ => ColorPalette::Blue, // Default fallback
346        };
347        
348        let shade = match parts[1] {
349            "50" => ColorShade::Shade50,
350            "100" => ColorShade::Shade100,
351            "200" => ColorShade::Shade200,
352            "300" => ColorShade::Shade300,
353            "400" => ColorShade::Shade400,
354            "500" => ColorShade::Shade500,
355            "600" => ColorShade::Shade600,
356            "700" => ColorShade::Shade700,
357            "800" => ColorShade::Shade800,
358            "900" => ColorShade::Shade900,
359            "950" => ColorShade::Shade950,
360            _ => ColorShade::Shade500, // Default fallback
361        };
362        
363        Color::new(palette, shade)
364    } else {
365        // Default to blue-500 if parsing fails
366        Color::new(ColorPalette::Blue, ColorShade::Shade500)
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    
374    #[test]
375    fn test_gradient_direction() {
376        assert_eq!(GradientDirection::ToRight.to_class_name(), "to-r");
377        assert_eq!(GradientDirection::ToLeft.to_class_name(), "to-l");
378        assert_eq!(GradientDirection::ToTop.to_class_name(), "to-t");
379        assert_eq!(GradientDirection::ToBottom.to_class_name(), "to-b");
380        assert_eq!(GradientDirection::ToTopRight.to_class_name(), "to-tr");
381        assert_eq!(GradientDirection::ToTopLeft.to_class_name(), "to-tl");
382        assert_eq!(GradientDirection::ToBottomRight.to_class_name(), "to-br");
383        assert_eq!(GradientDirection::ToBottomLeft.to_class_name(), "to-bl");
384    }
385    
386    #[test]
387    fn test_gradient_direction_css() {
388        assert_eq!(GradientDirection::ToRight.to_css_value(), "to right");
389        assert_eq!(GradientDirection::ToLeft.to_css_value(), "to left");
390        assert_eq!(GradientDirection::ToTop.to_css_value(), "to top");
391        assert_eq!(GradientDirection::ToBottom.to_css_value(), "to bottom");
392    }
393    
394    #[test]
395    fn test_gradient_stop() {
396        let from_stop = GradientStop::from(Color::new(ColorPalette::Blue, ColorShade::Shade500));
397        assert_eq!(from_stop.to_class_name(), "from-blue-500");
398        
399        let via_stop = GradientStop::via(Color::new(ColorPalette::Purple, ColorShade::Shade500));
400        assert_eq!(via_stop.to_class_name(), "via-purple-500");
401        
402        let to_stop = GradientStop::to(Color::new(ColorPalette::Pink, ColorShade::Shade500));
403        assert_eq!(to_stop.to_class_name(), "to-pink-500");
404    }
405    
406    #[test]
407    fn test_gradient_creation() {
408        let gradient = Gradient::new(GradientDirection::ToRight)
409            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
410            .to(Color::new(ColorPalette::Red, ColorShade::Shade500));
411        
412        let class_names = gradient.to_class_names();
413        assert!(class_names.contains(&"bg-gradient-to-r".to_string()));
414        assert!(class_names.contains(&"from-blue-500".to_string()));
415        assert!(class_names.contains(&"to-red-500".to_string()));
416    }
417    
418    #[test]
419    fn test_gradient_three_colors() {
420        let gradient = Gradient::new(GradientDirection::ToRight)
421            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
422            .via(Color::new(ColorPalette::Purple, ColorShade::Shade500))
423            .to(Color::new(ColorPalette::Pink, ColorShade::Shade500));
424        
425        let class_names = gradient.to_class_names();
426        assert!(class_names.contains(&"bg-gradient-to-r".to_string()));
427        assert!(class_names.contains(&"from-blue-500".to_string()));
428        assert!(class_names.contains(&"via-purple-500".to_string()));
429        assert!(class_names.contains(&"to-pink-500".to_string()));
430    }
431    
432    #[test]
433    fn test_gradient_validation() {
434        // Valid gradient
435        let valid_gradient = Gradient::new(GradientDirection::ToRight)
436            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
437            .to(Color::new(ColorPalette::Red, ColorShade::Shade500));
438        assert!(valid_gradient.validate().is_ok());
439        
440        // Invalid gradient - no stops
441        let invalid_gradient = Gradient::new(GradientDirection::ToRight);
442        assert!(invalid_gradient.validate().is_err());
443        
444        // Invalid gradient - missing from
445        let invalid_gradient2 = Gradient::new(GradientDirection::ToRight)
446            .to(Color::new(ColorPalette::Red, ColorShade::Shade500));
447        assert!(invalid_gradient2.validate().is_err());
448        
449        // Invalid gradient - missing to
450        let invalid_gradient3 = Gradient::new(GradientDirection::ToRight)
451            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500));
452        assert!(invalid_gradient3.validate().is_err());
453    }
454    
455    #[test]
456    fn test_gradient_utilities() {
457        let classes = ClassBuilder::new()
458            .gradient_direction(GradientDirection::ToRight)
459            .gradient_from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
460            .gradient_to(Color::new(ColorPalette::Red, ColorShade::Shade500))
461            .build();
462        
463        let css_classes = classes.to_css_classes();
464        assert!(css_classes.contains("bg-gradient-to-r"));
465        assert!(css_classes.contains("from-blue-500"));
466        assert!(css_classes.contains("to-red-500"));
467    }
468    
469    #[test]
470    fn test_gradient_simple() {
471        let classes = ClassBuilder::new()
472            .gradient_simple(
473                GradientDirection::ToRight,
474                Color::new(ColorPalette::Blue, ColorShade::Shade500),
475                Color::new(ColorPalette::Red, ColorShade::Shade500)
476            )
477            .build();
478        
479        let css_classes = classes.to_css_classes();
480        assert!(css_classes.contains("bg-gradient-to-r"));
481        assert!(css_classes.contains("from-blue-500"));
482        assert!(css_classes.contains("to-red-500"));
483    }
484    
485    #[test]
486    fn test_gradient_three_colors_utility() {
487        let classes = ClassBuilder::new()
488            .gradient_three(
489                GradientDirection::ToRight,
490                Color::new(ColorPalette::Blue, ColorShade::Shade500),
491                Color::new(ColorPalette::Purple, ColorShade::Shade500),
492                Color::new(ColorPalette::Pink, ColorShade::Shade500)
493            )
494            .build();
495        
496        let css_classes = classes.to_css_classes();
497        assert!(css_classes.contains("bg-gradient-to-r"));
498        assert!(css_classes.contains("from-blue-500"));
499        assert!(css_classes.contains("via-purple-500"));
500        assert!(css_classes.contains("to-pink-500"));
501    }
502    
503    #[test]
504    fn test_gradient_from_names() {
505        let classes = ClassBuilder::new()
506            .gradient_from_names(GradientDirection::ToRight, "blue-500", "red-500")
507            .build();
508        
509        let css_classes = classes.to_css_classes();
510        assert!(css_classes.contains("bg-gradient-to-r"));
511        assert!(css_classes.contains("from-blue-500"));
512        assert!(css_classes.contains("to-red-500"));
513    }
514    
515    #[test]
516    fn test_gradient_from_names_with_via() {
517        let classes = ClassBuilder::new()
518            .gradient_from_names_with_via(GradientDirection::ToRight, "blue-500", "purple-500", "pink-500")
519            .build();
520        
521        let css_classes = classes.to_css_classes();
522        assert!(css_classes.contains("bg-gradient-to-r"));
523        assert!(css_classes.contains("from-blue-500"));
524        assert!(css_classes.contains("via-purple-500"));
525        assert!(css_classes.contains("to-pink-500"));
526    }
527    
528    #[test]
529    fn test_gradient_display() {
530        let gradient = Gradient::new(GradientDirection::ToRight)
531            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
532            .to(Color::new(ColorPalette::Red, ColorShade::Shade500));
533        
534        let display = format!("{}", gradient);
535        assert!(display.contains("bg-gradient-to-r"));
536        assert!(display.contains("from-blue-500"));
537        assert!(display.contains("to-red-500"));
538    }
539    
540    #[test]
541    fn test_gradient_css_value() {
542        let gradient = Gradient::new(GradientDirection::ToRight)
543            .from(Color::new(ColorPalette::Blue, ColorShade::Shade500))
544            .to(Color::new(ColorPalette::Red, ColorShade::Shade500));
545        
546        let css_value = gradient.to_css_value();
547        assert!(css_value.contains("linear-gradient(to right"));
548        assert!(css_value.contains("#3b82f6")); // blue-500
549        assert!(css_value.contains("#ef4444")); // red-500
550    }
551}