1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct ThemeContract {
16 pub canvas_bg: String,
19 pub grid_color: String,
21
22 pub selection_accent: String,
25 pub smart_guide_color: String,
27
28 pub panel_bg: String,
31 pub panel_border: String,
33
34 pub text_primary: String,
37 pub text_secondary: String,
39
40 pub accent: String,
43
44 pub placeholder_border: String,
47 pub placeholder_bg: String,
49 pub placeholder_text: String,
51
52 pub font_family: String,
55 pub font_size_base: f32,
57 pub border_radius: f32,
59}
60
61impl ThemeContract {
62 pub fn light() -> Self {
64 Self {
65 canvas_bg: "#F5F5F7".into(),
66 grid_color: "rgba(0, 0, 0, 0.05)".into(),
67 selection_accent: "#007AFF".into(),
68 smart_guide_color: "#FF3B30".into(),
69 panel_bg: "rgba(255, 255, 255, 0.8)".into(),
70 panel_border: "rgba(0, 0, 0, 0.06)".into(),
71 text_primary: "#1D1D1F".into(),
72 text_secondary: "#86868B".into(),
73 accent: "#007AFF".into(),
74 placeholder_border: "#86868B".into(),
75 placeholder_bg: "rgba(142, 142, 147, 0.06)".into(),
76 placeholder_text: "#86868B".into(),
77 font_family: "Inter, SF Pro, system-ui, sans-serif".into(),
78 font_size_base: 13.0,
79 border_radius: 8.0,
80 }
81 }
82
83 pub fn dark() -> Self {
85 Self {
86 canvas_bg: "#1C1C1E".into(),
87 grid_color: "rgba(255, 255, 255, 0.04)".into(),
88 selection_accent: "#0A84FF".into(),
89 smart_guide_color: "#FF453A".into(),
90 panel_bg: "rgba(44, 44, 46, 0.8)".into(),
91 panel_border: "rgba(255, 255, 255, 0.08)".into(),
92 text_primary: "#F5F5F7".into(),
93 text_secondary: "#98989D".into(),
94 accent: "#0A84FF".into(),
95 placeholder_border: "#636366".into(),
96 placeholder_bg: "rgba(99, 99, 102, 0.08)".into(),
97 placeholder_text: "#98989D".into(),
98 font_family: "Inter, SF Pro, system-ui, sans-serif".into(),
99 font_size_base: 13.0,
100 border_radius: 8.0,
101 }
102 }
103
104 pub fn to_json(&self) -> String {
106 serde_json::to_string(self).unwrap_or_else(|_| "{}".into())
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn theme_light_fields_non_empty() {
116 let t = ThemeContract::light();
117 assert!(!t.canvas_bg.is_empty());
118 assert!(!t.grid_color.is_empty());
119 assert!(!t.selection_accent.is_empty());
120 assert!(!t.panel_bg.is_empty());
121 assert!(!t.text_primary.is_empty());
122 assert!(!t.accent.is_empty());
123 assert!(!t.font_family.is_empty());
124 assert!(t.font_size_base > 0.0);
125 assert!(t.border_radius >= 0.0);
126 }
127
128 #[test]
129 fn theme_dark_fields_non_empty() {
130 let t = ThemeContract::dark();
131 assert!(!t.canvas_bg.is_empty());
132 assert!(!t.grid_color.is_empty());
133 assert!(!t.selection_accent.is_empty());
134 assert!(!t.panel_bg.is_empty());
135 assert!(!t.text_primary.is_empty());
136 assert!(!t.accent.is_empty());
137 }
138
139 #[test]
140 fn theme_light_dark_differ() {
141 let l = ThemeContract::light();
142 let d = ThemeContract::dark();
143 assert_ne!(l.canvas_bg, d.canvas_bg, "light and dark bg should differ");
144 assert_ne!(
145 l.text_primary, d.text_primary,
146 "light and dark text should differ"
147 );
148 }
149
150 #[test]
151 fn theme_to_json_roundtrip() {
152 let original = ThemeContract::light();
153 let json = original.to_json();
154 let parsed: ThemeContract = serde_json::from_str(&json).unwrap();
155 assert_eq!(original, parsed);
156 }
157
158 #[test]
159 fn canvas_theme_from_contract() {
160 let contract = ThemeContract::light();
161 assert_eq!(contract.placeholder_border, "#86868B");
163 assert_eq!(contract.placeholder_bg, "rgba(142, 142, 147, 0.06)");
164 assert_eq!(contract.placeholder_text, "#86868B");
165 }
166}