1use std::collections::HashMap;
10
11#[derive(Default)]
13pub struct TokenExport {
14 colors: HashMap<String, [f32; 4]>,
16 spacing: HashMap<String, f32>,
18 radius: HashMap<String, f32>,
20 typography: HashMap<String, f32>,
22}
23
24impl TokenExport {
25 pub fn new() -> Self {
27 let mut colors = HashMap::new();
28 colors.insert("background".to_string(), [1.0, 1.0, 1.0, 1.0]);
30 colors.insert("surface".to_string(), [0.98, 0.98, 0.98, 1.0]);
31 colors.insert("surface_elevated".to_string(), [0.95, 0.95, 0.95, 1.0]);
32 colors.insert("surface_overlay".to_string(), [0.92, 0.92, 0.94, 1.0]);
33 colors.insert("text".to_string(), [0.1, 0.1, 0.12, 1.0]);
34 colors.insert("text_muted".to_string(), [0.4, 0.4, 0.45, 1.0]);
35 colors.insert("text_dim".to_string(), [0.55, 0.55, 0.6, 1.0]);
36 colors.insert("primary".to_string(), [0.2, 0.2, 0.25, 1.0]);
37 colors.insert("secondary".to_string(), [0.5, 0.5, 0.55, 1.0]);
38 colors.insert("accent".to_string(), [0.0, 0.8, 1.0, 1.0]);
39 colors.insert("accent_hover".to_string(), [0.2, 0.85, 1.0, 1.0]);
40 colors.insert("border".to_string(), [0.85, 0.85, 0.88, 1.0]);
41 colors.insert("border_strong".to_string(), [0.7, 0.7, 0.75, 1.0]);
42 colors.insert("hover".to_string(), [0.95, 0.95, 0.97, 1.0]);
43 colors.insert("active".to_string(), [0.9, 0.9, 0.93, 1.0]);
44 colors.insert("disabled".to_string(), [0.92, 0.92, 0.94, 1.0]);
45 colors.insert("disabled_text".to_string(), [0.65, 0.65, 0.7, 1.0]);
46 colors.insert("success".to_string(), [0.2, 0.8, 0.4, 1.0]);
47 colors.insert("warning".to_string(), [1.0, 0.7, 0.0, 1.0]);
48 colors.insert("error".to_string(), [0.95, 0.2, 0.3, 1.0]);
49 colors.insert("info".to_string(), [0.2, 0.6, 1.0, 1.0]);
50 colors.insert("focus_ring".to_string(), [0.0, 0.8, 1.0, 0.8]);
51 colors.insert("shadow".to_string(), [0.0, 0.0, 0.0, 0.15]);
52 colors.insert("code_bg".to_string(), [0.96, 0.96, 0.98, 1.0]);
53
54 let mut dark_colors = HashMap::new();
56 dark_colors.insert("background".to_string(), [0.05, 0.05, 0.08, 1.0]);
57 dark_colors.insert("surface".to_string(), [0.1, 0.1, 0.14, 1.0]);
58 dark_colors.insert("surface_elevated".to_string(), [0.15, 0.15, 0.2, 1.0]);
59 dark_colors.insert("surface_overlay".to_string(), [0.18, 0.18, 0.24, 1.0]);
60 dark_colors.insert("text".to_string(), [0.95, 0.95, 0.97, 1.0]);
61 dark_colors.insert("text_muted".to_string(), [0.6, 0.6, 0.65, 1.0]);
62 dark_colors.insert("text_dim".to_string(), [0.45, 0.45, 0.5, 1.0]);
63 dark_colors.insert("border".to_string(), [0.25, 0.25, 0.3, 1.0]);
64 dark_colors.insert("border_strong".to_string(), [0.35, 0.35, 0.4, 1.0]);
65 dark_colors.insert("hover".to_string(), [0.15, 0.15, 0.2, 1.0]);
66 dark_colors.insert("active".to_string(), [0.2, 0.2, 0.28, 1.0]);
67 dark_colors.insert("disabled".to_string(), [0.12, 0.12, 0.16, 1.0]);
68 dark_colors.insert("disabled_text".to_string(), [0.35, 0.35, 0.4, 1.0]);
69 dark_colors.insert("shadow".to_string(), [0.0, 0.0, 0.0, 0.4]);
70
71 let mut spacing = HashMap::new();
72 spacing.insert("xs".to_string(), 4.0);
73 spacing.insert("sm".to_string(), 8.0);
74 spacing.insert("md".to_string(), 16.0);
75 spacing.insert("lg".to_string(), 24.0);
76 spacing.insert("xl".to_string(), 32.0);
77 spacing.insert("2xl".to_string(), 48.0);
78 spacing.insert("3xl".to_string(), 64.0);
79
80 let mut radius = HashMap::new();
81 radius.insert("xs".to_string(), 2.0);
82 radius.insert("sm".to_string(), 4.0);
83 radius.insert("md".to_string(), 6.0);
84 radius.insert("lg".to_string(), 8.0);
85 radius.insert("xl".to_string(), 12.0);
86 radius.insert("2xl".to_string(), 16.0);
87 radius.insert("full".to_string(), 9999.0);
88
89 let mut typography = HashMap::new();
90 typography.insert("footnote".to_string(), 10.0);
91 typography.insert("caption".to_string(), 12.0);
92 typography.insert("body".to_string(), 14.0);
93 typography.insert("body_large".to_string(), 16.0);
94 typography.insert("heading3".to_string(), 20.0);
95 typography.insert("heading2".to_string(), 24.0);
96 typography.insert("heading1".to_string(), 32.0);
97 typography.insert("display".to_string(), 48.0);
98
99 Self {
100 colors,
101 spacing,
102 radius,
103 typography,
104 }
105 }
106
107 pub fn generate(&self, format: &str) -> Result<String, String> {
109 match format {
110 "figma" => Ok(self.generate_figma()),
111 "css" => Ok(self.generate_css()),
112 "swift" => Ok(self.generate_swift()),
113 "json" => Ok(self.generate_json()),
114 other => Err(format!("Unknown format: {}", other)),
115 }
116 }
117
118 fn generate_figma(&self) -> String {
120 let mut parts = vec!["{".to_string()];
121
122 parts.push(" \"colors\": {".to_string());
124 let color_entries: Vec<String> = self
125 .colors
126 .iter()
127 .map(|(name, rgba)| {
128 let r = (rgba[0] * 255.0).round() as u32;
129 let g = (rgba[1] * 255.0).round() as u32;
130 let b = (rgba[2] * 255.0).round() as u32;
131 format!(
132 " \"{}\": {{\"r\": {}, \"g\": {}, \"b\": {}, \"a\": {:.2}}}",
133 name, r, g, b, rgba[3]
134 )
135 })
136 .collect();
137 parts.push(color_entries.join(",\n"));
138 parts.push(" },".to_string());
139
140 parts.push(" \"spacing\": {".to_string());
142 let spacing_entries: Vec<String> = self
143 .spacing
144 .iter()
145 .map(|(name, val)| format!(" \"{}\": {}", name, val))
146 .collect();
147 parts.push(spacing_entries.join(",\n"));
148 parts.push(" },".to_string());
149
150 parts.push(" \"radius\": {".to_string());
152 let radius_entries: Vec<String> = self
153 .radius
154 .iter()
155 .map(|(name, val)| format!(" \"{}\": {}", name, val))
156 .collect();
157 parts.push(radius_entries.join(",\n"));
158 parts.push(" },".to_string());
159
160 parts.push(" \"typography\": {".to_string());
162 let type_entries: Vec<String> = self
163 .typography
164 .iter()
165 .map(|(name, val)| format!(" \"{}\": {}", name, val))
166 .collect();
167 parts.push(type_entries.join(",\n"));
168 parts.push(" }".to_string());
169
170 parts.push("}".to_string());
171 parts.join("\n")
172 }
173
174 fn generate_css(&self) -> String {
176 let mut lines = vec![":root {".to_string()];
177
178 for (name, rgba) in &self.colors {
180 let css_name = format!("--color-{}", name.replace('_', "-"));
181 let r = (rgba[0] * 255.0).round() as u32;
182 let g = (rgba[1] * 255.0).round() as u32;
183 let b = (rgba[2] * 255.0).round() as u32;
184 if rgba[3] < 1.0 {
185 lines.push(format!(
186 " {}: rgba({}, {}, {}, {:.2});",
187 css_name, r, g, b, rgba[3]
188 ));
189 } else {
190 lines.push(format!(" {}: rgb({}, {}, {});", css_name, r, g, b));
191 }
192 }
193
194 for (name, val) in &self.spacing {
196 lines.push(format!(
197 " --spacing-{}: {}px;",
198 name.replace('_', "-"),
199 val
200 ));
201 }
202
203 for (name, val) in &self.radius {
205 lines.push(format!(" --radius-{}: {}px;", name.replace('_', "-"), val));
206 }
207
208 for (name, val) in &self.typography {
210 lines.push(format!(
211 " --font-size-{}: {}px;",
212 name.replace('_', "-"),
213 val
214 ));
215 }
216
217 lines.push("}".to_string());
218 lines.join("\n")
219 }
220
221 fn generate_swift(&self) -> String {
223 let mut lines = vec![
224 "// CVKG Design Tokens — SwiftUI".to_string(),
225 "// Auto-generated by cvkg tokens export --format swift".to_string(),
226 "".to_string(),
227 "import SwiftUI".to_string(),
228 "".to_string(),
229 "struct CVKGTheme {".to_string(),
230 ];
231
232 lines.append(&mut vec![
234 " // MARK: - Colors".to_string(),
235 " struct Colors {".to_string(),
236 ]);
237 for (name, rgba) in &self.colors {
238 let swift_name = Self::to_camel_case(name);
239 lines.push(format!(
240 " static let {} = Color(red: {:.3}, green: {:.3}, blue: {:.3}, opacity: {:.2})",
241 swift_name, rgba[0], rgba[1], rgba[2], rgba[3]
242 ));
243 }
244 lines.push(" }".to_string());
245 lines.push("".to_string());
246
247 lines.append(&mut vec![
249 " // MARK: - Spacing".to_string(),
250 " struct Spacing {".to_string(),
251 ]);
252 for (name, val) in &self.spacing {
253 let swift_name = Self::to_camel_case(name);
254 lines.push(format!(
255 " static let {}: CGFloat = {}",
256 swift_name, val
257 ));
258 }
259 lines.push(" }".to_string());
260 lines.push("".to_string());
261
262 lines.append(&mut vec![
264 " // MARK: - Corner Radius".to_string(),
265 " struct Radius {".to_string(),
266 ]);
267 for (name, val) in &self.radius {
268 let swift_name = Self::to_camel_case(name);
269 lines.push(format!(
270 " static let {}: CGFloat = {}",
271 swift_name, val
272 ));
273 }
274 lines.push(" }".to_string());
275 lines.push("".to_string());
276
277 lines.append(&mut vec![
279 " // MARK: - Typography".to_string(),
280 " struct Typography {".to_string(),
281 ]);
282 for (name, val) in &self.typography {
283 let swift_name = Self::to_camel_case(name);
284 lines.push(format!(
285 " static let {}: CGFloat = {}",
286 swift_name, val
287 ));
288 }
289 lines.push(" }".to_string());
290
291 lines.push("}".to_string());
292 lines.join("\n")
293 }
294
295 fn generate_json(&self) -> String {
297 let mut parts = vec!["{".to_string()];
298
299 parts.push(" \"colors\": {".to_string());
300 let color_entries: Vec<String> = self
301 .colors
302 .iter()
303 .map(|(name, rgba)| {
304 format!(
305 " \"{}\": [{:.3}, {:.3}, {:.3}, {:.2}]",
306 name, rgba[0], rgba[1], rgba[2], rgba[3]
307 )
308 })
309 .collect();
310 parts.push(color_entries.join(",\n"));
311 parts.push(" },".to_string());
312
313 parts.push(" \"spacing\": {".to_string());
314 let spacing_entries: Vec<String> = self
315 .spacing
316 .iter()
317 .map(|(name, val)| format!(" \"{}\": {}", name, val))
318 .collect();
319 parts.push(spacing_entries.join(",\n"));
320 parts.push(" },".to_string());
321
322 parts.push(" \"radius\": {".to_string());
323 let radius_entries: Vec<String> = self
324 .radius
325 .iter()
326 .map(|(name, val)| format!(" \"{}\": {}", name, val))
327 .collect();
328 parts.push(radius_entries.join(",\n"));
329 parts.push(" },".to_string());
330
331 parts.push(" \"typography\": {".to_string());
332 let type_entries: Vec<String> = self
333 .typography
334 .iter()
335 .map(|(name, val)| format!(" \"{}\": {}", name, val))
336 .collect();
337 parts.push(type_entries.join(",\n"));
338 parts.push(" }".to_string());
339
340 parts.push("}".to_string());
341 parts.join("\n")
342 }
343
344 fn to_camel_case(s: &str) -> String {
346 let mut result = String::new();
347 let mut capitalize = false;
348 for c in s.chars() {
349 if c == '_' {
350 capitalize = true;
351 } else if capitalize {
352 result.push(c.to_ascii_uppercase());
353 capitalize = false;
354 } else {
355 result.push(c);
356 }
357 }
358 if let Some(first) = result.chars().next() {
360 result = first.to_ascii_uppercase().to_string() + &result[1..];
361 }
362 result
363 }
364}