1pub mod builder;
7pub mod predefined;
8
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12
13use crate::canvas_types::{
14 CanvasOgParams, GradientType, OgImageLayer, TextGradient, TextOutline, TextShadow,
15};
16use crate::error::{ErrorKind, MetadataError};
17
18pub type ThemeId = String;
20
21pub type ThemeVersion = String;
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub enum ThemeCategory {
27 Business,
28 Technology,
29 Creative,
30 Minimalist,
31 Bold,
32 Elegant,
33 Modern,
34 Classic,
35 Custom,
36}
37
38impl fmt::Display for ThemeCategory {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 ThemeCategory::Business => write!(f, "Business"),
42 ThemeCategory::Technology => write!(f, "Technology"),
43 ThemeCategory::Creative => write!(f, "Creative"),
44 ThemeCategory::Minimalist => write!(f, "Minimalist"),
45 ThemeCategory::Bold => write!(f, "Bold"),
46 ThemeCategory::Elegant => write!(f, "Elegant"),
47 ThemeCategory::Modern => write!(f, "Modern"),
48 ThemeCategory::Classic => write!(f, "Classic"),
49 ThemeCategory::Custom => write!(f, "Custom"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ColorPalette {
57 pub primary: String,
59 pub secondary: String,
61 pub background: String,
63 pub text: String,
65 pub accent: String,
67 pub additional: Vec<String>,
69}
70
71impl Default for ColorPalette {
72 fn default() -> Self {
73 Self {
74 primary: "#4f46e5".to_string(),
75 secondary: "#7c3aed".to_string(),
76 background: "#ffffff".to_string(),
77 text: "#1f2937".to_string(),
78 accent: "#f59e0b".to_string(),
79 additional: vec!["#10b981".to_string(), "#ef4444".to_string()],
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Typography {
87 pub primary_font: String,
89 pub secondary_font: String,
91 pub title_size: u32,
93 pub description_size: u32,
95 pub weights: FontWeights,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct FontWeights {
102 pub light: u32,
103 pub normal: u32,
104 pub medium: u32,
105 pub bold: u32,
106}
107
108impl Default for FontWeights {
109 fn default() -> Self {
110 Self {
111 light: 300,
112 normal: 400,
113 medium: 500,
114 bold: 700,
115 }
116 }
117}
118
119impl Default for Typography {
120 fn default() -> Self {
121 Self {
122 primary_font: "Inter, sans-serif".to_string(),
123 secondary_font: "Georgia, serif".to_string(),
124 title_size: 48,
125 description_size: 24,
126 weights: FontWeights::default(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Layout {
134 pub padding: u32,
136 pub spacing: u32,
138 pub alignment: ContentAlignment,
140 pub logo_position: LogoPosition,
142 pub text_alignment: TextAlignment,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub enum ContentAlignment {
149 Left,
150 Center,
151 Right,
152 Justify,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub enum LogoPosition {
158 TopLeft,
159 TopRight,
160 BottomLeft,
161 BottomRight,
162 Center,
163 None,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub enum TextAlignment {
169 Left,
170 Center,
171 Right,
172}
173
174impl Default for Layout {
175 fn default() -> Self {
176 Self {
177 padding: 60,
178 spacing: 30,
179 alignment: ContentAlignment::Center,
180 logo_position: LogoPosition::None,
181 text_alignment: TextAlignment::Center,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct VisualEffects {
189 pub background: BackgroundEffects,
191 pub text: TextEffects,
193 pub border: BorderEffects,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BackgroundEffects {
200 pub background_type: BackgroundType,
202 pub gradient: Option<GradientConfig>,
204 pub pattern: Option<PatternConfig>,
206 pub blur: Option<f64>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub enum BackgroundType {
213 Solid,
214 Gradient,
215 Pattern,
216 Image,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct GradientConfig {
222 pub gradient_type: GradientType,
223 pub colors: Vec<String>,
224 pub angle: f64,
225 pub stops: Vec<f64>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct PatternConfig {
231 pub pattern_type: PatternType,
232 pub color: String,
233 pub opacity: f64,
234 pub size: u32,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum PatternType {
240 Dots,
241 Lines,
242 Grid,
243 Waves,
244 Geometric,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TextEffects {
250 pub shadow: Option<TextShadow>,
252 pub outline: Option<TextOutline>,
254 pub gradient: Option<TextGradient>,
256 pub glow: Option<GlowEffect>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct GlowEffect {
263 pub color: String,
264 pub intensity: f64,
265 pub radius: f64,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct BorderEffects {
271 pub width: f64,
273 pub color: String,
275 pub radius: f64,
277 pub style: BorderStyle,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub enum BorderStyle {
284 Solid,
285 Dashed,
286 Dotted,
287 Double,
288}
289
290impl Default for VisualEffects {
291 fn default() -> Self {
292 Self {
293 background: BackgroundEffects {
294 background_type: BackgroundType::Solid,
295 gradient: None,
296 pattern: None,
297 blur: None,
298 },
299 text: TextEffects {
300 shadow: None,
301 outline: None,
302 gradient: None,
303 glow: None,
304 },
305 border: BorderEffects {
306 width: 0.0,
307 color: "#000000".to_string(),
308 radius: 0.0,
309 style: BorderStyle::Solid,
310 },
311 }
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct Theme {
318 pub id: ThemeId,
320 pub name: String,
322 pub description: String,
324 pub version: ThemeVersion,
326 pub category: ThemeCategory,
328 pub colors: ColorPalette,
330 pub typography: Typography,
332 pub layout: Layout,
334 pub effects: VisualEffects,
336 pub custom_layers: Vec<OgImageLayer>,
338 pub metadata: ThemeMetadata,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ThemeMetadata {
345 pub author: String,
347 pub created_at: String,
349 pub modified_at: String,
351 pub tags: Vec<String>,
353 pub license: String,
355 pub preview_url: Option<String>,
357}
358
359impl Default for ThemeMetadata {
360 fn default() -> Self {
361 Self {
362 author: "leptos-next-metadata".to_string(),
363 created_at: chrono::Utc::now().to_rfc3339(),
364 modified_at: chrono::Utc::now().to_rfc3339(),
365 tags: vec![],
366 license: "MIT".to_string(),
367 preview_url: None,
368 }
369 }
370}
371
372impl Theme {
373 pub fn new(id: ThemeId, name: String, description: String, category: ThemeCategory) -> Self {
375 Self {
376 id,
377 name,
378 description,
379 version: "1.0.0".to_string(),
380 category,
381 colors: ColorPalette::default(),
382 typography: Typography::default(),
383 layout: Layout::default(),
384 effects: VisualEffects::default(),
385 custom_layers: vec![],
386 metadata: ThemeMetadata::default(),
387 }
388 }
389
390 pub fn apply_to_canvas_params(&self, mut params: CanvasOgParams) -> CanvasOgParams {
392 params.background_color = Some(self.colors.background.clone());
394 params.text_color = Some(self.colors.text.clone());
395
396 params.font_family = Some(self.typography.primary_font.clone());
398 params.title_font_size = Some(self.typography.title_size);
399 params.description_font_size = Some(self.typography.description_size);
400
401 params.padding = Some(self.layout.padding);
403 params.text_align = Some(match self.layout.text_alignment {
404 TextAlignment::Left => crate::canvas_types::TextAlign::Left,
405 TextAlignment::Center => crate::canvas_types::TextAlign::Center,
406 TextAlignment::Right => crate::canvas_types::TextAlign::Right,
407 });
408
409 if let Some(gradient) = &self.effects.text.gradient {
411 params.text_gradient = Some(gradient.clone());
412 }
413 if let Some(shadow) = &self.effects.text.shadow {
414 params.text_shadow = Some(shadow.clone());
415 }
416 if let Some(outline) = &self.effects.text.outline {
417 params.text_outline = Some(outline.clone());
418 }
419
420 if !self.custom_layers.is_empty() {
422 params.layers = Some(self.custom_layers.clone());
423 }
424
425 params
426 }
427
428 pub fn generate_preview(&self) -> Result<String, MetadataError> {
430 let _preview_params = CanvasOgParams {
431 title: format!("{} Theme Preview", self.name),
432 description: Some(self.description.clone()),
433 width: Some(1200),
434 height: Some(630),
435 background_color: Some(self.colors.background.clone()),
436 text_color: Some(self.colors.text.clone()),
437 font_family: Some(self.typography.primary_font.clone()),
438 title_font_size: Some(self.typography.title_size),
439 description_font_size: Some(self.typography.description_size),
440 logo_url: None,
441 font_urls: None,
442 default_font_family: None,
443 layers: Some(self.custom_layers.clone()),
444 background_image_url: None,
445 background_image_opacity: None,
446 text_gradient: self.effects.text.gradient.clone(),
447 text_shadow: self.effects.text.shadow.clone(),
448 text_outline: self.effects.text.outline.clone(),
449 logo_position: Some(match self.layout.logo_position {
450 LogoPosition::TopLeft => crate::canvas_types::LogoPosition::TopLeft,
451 LogoPosition::TopRight => crate::canvas_types::LogoPosition::TopRight,
452 LogoPosition::BottomLeft => crate::canvas_types::LogoPosition::BottomLeft,
453 LogoPosition::BottomRight => crate::canvas_types::LogoPosition::BottomRight,
454 LogoPosition::Center => crate::canvas_types::LogoPosition::Center,
455 LogoPosition::None => crate::canvas_types::LogoPosition::TopLeft,
456 }),
457 text_align: Some(match self.layout.text_alignment {
458 TextAlignment::Left => crate::canvas_types::TextAlign::Left,
459 TextAlignment::Center => crate::canvas_types::TextAlign::Center,
460 TextAlignment::Right => crate::canvas_types::TextAlign::Right,
461 }),
462 padding: Some(self.layout.padding),
463 };
464
465 let svg_content = format!(
468 r#"<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
469 <rect width="100%" height="100%" fill="{}"/>
470 <text x="50%" y="50%" text-anchor="middle" dy=".3em" font-family="{}" font-size="{}" fill="{}">
471 {} Theme Preview
472 </text>
473 </svg>"#,
474 self.colors.background,
475 self.typography.primary_font,
476 self.typography.title_size,
477 self.colors.text,
478 self.name
479 );
480
481 Ok(format!(
482 "data:image/svg+xml;base64,{}",
483 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, svg_content)
484 ))
485 }
486
487 pub fn validate(&self) -> Result<(), MetadataError> {
489 if self.id.is_empty() {
490 return Err(MetadataError::new(
491 ErrorKind::Validation,
492 "Theme ID cannot be empty".to_string(),
493 ));
494 }
495
496 if self.name.is_empty() {
497 return Err(MetadataError::new(
498 ErrorKind::Validation,
499 "Theme name cannot be empty".to_string(),
500 ));
501 }
502
503 if !self.is_valid_hex_color(&self.colors.primary) {
505 return Err(MetadataError::new(
506 ErrorKind::Validation,
507 "Invalid primary color format".to_string(),
508 ));
509 }
510
511 if !self.is_valid_hex_color(&self.colors.background) {
512 return Err(MetadataError::new(
513 ErrorKind::Validation,
514 "Invalid background color format".to_string(),
515 ));
516 }
517
518 if self.typography.title_size < 12 || self.typography.title_size > 120 {
520 return Err(MetadataError::new(
521 ErrorKind::Validation,
522 "Title font size must be between 12 and 120".to_string(),
523 ));
524 }
525
526 Ok(())
527 }
528
529 fn is_valid_hex_color(&self, color: &str) -> bool {
531 if color.starts_with('#') && color.len() == 7 {
532 color[1..].chars().all(|c| c.is_ascii_hexdigit())
533 } else {
534 false
535 }
536 }
537
538 pub fn clone_with_id(&self, new_id: ThemeId, new_name: String) -> Self {
540 let mut cloned = self.clone();
541 cloned.id = new_id;
542 cloned.name = new_name;
543 cloned.metadata.modified_at = chrono::Utc::now().to_rfc3339();
544 cloned
545 }
546
547 pub fn update_version(&mut self, new_version: ThemeVersion) {
549 self.version = new_version;
550 self.metadata.modified_at = chrono::Utc::now().to_rfc3339();
551 }
552}
553
554#[derive(Debug, Clone)]
556pub struct ThemeManager {
557 themes: HashMap<ThemeId, Theme>,
559 default_theme_id: Option<ThemeId>,
561}
562
563impl ThemeManager {
564 pub fn new() -> Self {
566 Self {
567 themes: HashMap::new(),
568 default_theme_id: None,
569 }
570 }
571
572 pub fn add_theme(&mut self, theme: Theme) -> Result<(), MetadataError> {
574 theme.validate()?;
575 let theme_id = theme.id.clone();
576 self.themes.insert(theme_id, theme);
577 Ok(())
578 }
579
580 pub fn get_theme(&self, theme_id: &ThemeId) -> Option<&Theme> {
582 self.themes.get(theme_id)
583 }
584
585 pub fn get_all_themes(&self) -> Vec<&Theme> {
587 self.themes.values().collect()
588 }
589
590 pub fn get_themes_by_category(&self, category: &ThemeCategory) -> Vec<&Theme> {
592 self.themes
593 .values()
594 .filter(|theme| &theme.category == category)
595 .collect()
596 }
597
598 pub fn set_default_theme(&mut self, theme_id: ThemeId) -> Result<(), MetadataError> {
600 if self.themes.contains_key(&theme_id) {
601 self.default_theme_id = Some(theme_id);
602 Ok(())
603 } else {
604 Err(MetadataError::new(
605 ErrorKind::Unknown,
606 format!("Theme with ID '{}' not found", theme_id),
607 ))
608 }
609 }
610
611 pub fn get_default_theme(&self) -> Option<&Theme> {
613 if let Some(ref default_id) = self.default_theme_id {
614 self.get_theme(default_id)
615 } else {
616 None
617 }
618 }
619
620 pub fn remove_theme(&mut self, theme_id: &ThemeId) -> Option<Theme> {
622 if let Some(ref default_id) = self.default_theme_id {
623 if default_id == theme_id {
624 self.default_theme_id = None;
625 }
626 }
627 self.themes.remove(theme_id)
628 }
629
630 pub fn search_themes(&self, query: &str) -> Vec<&Theme> {
632 let query_lower = query.to_lowercase();
633 self.themes
634 .values()
635 .filter(|theme| {
636 theme.name.to_lowercase().contains(&query_lower)
637 || theme.description.to_lowercase().contains(&query_lower)
638 || theme
639 .metadata
640 .tags
641 .iter()
642 .any(|tag| tag.to_lowercase().contains(&query_lower))
643 })
644 .collect()
645 }
646
647 pub fn theme_count(&self) -> usize {
649 self.themes.len()
650 }
651
652 pub fn clear(&mut self) {
654 self.themes.clear();
655 self.default_theme_id = None;
656 }
657}
658
659impl Default for ThemeManager {
660 fn default() -> Self {
661 Self::new()
662 }
663}
664
665pub use builder::{QuickThemes, ThemeBuilder, ThemeHelpers};
667pub use predefined::create_predefined_themes;