Skip to main content

arc_lang/
themes.rs

1/// Arc themes — opinionated color palettes for architecture diagrams.
2/// No CSS, no custom colors. Just pick a theme and get a beautiful diagram.
3
4use crate::ast::NodeType;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
8pub struct Theme {
9    pub name: String,
10    pub background: String,
11    pub canvas_padding: f64,
12    pub node_styles: HashMap<NodeType, NodeStyle>,
13    pub group_style: GroupStyle,
14    pub connection_style: ConnectionStyle,
15    pub font: FontConfig,
16}
17
18#[derive(Debug, Clone)]
19pub struct NodeStyle {
20    pub fill: String,
21    pub stroke: String,
22    pub text_color: String,
23    pub type_color: String,
24    pub tag_bg: String,
25    pub tag_text: String,
26}
27
28#[derive(Debug, Clone)]
29pub struct GroupStyle {
30    pub fills: Vec<String>,  // Different fills for nesting depth
31    pub strokes: Vec<String>,
32    pub text_color: String,
33    pub corner_radius: f64,
34    pub padding: f64,
35    pub label_size: f64,
36}
37
38#[derive(Debug, Clone)]
39pub struct ConnectionStyle {
40    pub stroke: String,
41    pub stroke_width: f64,
42    pub dashed_stroke: String,
43    pub blocked_stroke: String,
44    pub text_color: String,
45    pub text_bg: String,
46    pub arrow_size: f64,
47    pub label_size: f64,
48    pub tag_bg: String,
49    pub tag_text: String,
50}
51
52#[derive(Debug, Clone)]
53pub struct FontConfig {
54    pub family: String,
55    pub node_label_size: f64,
56    pub node_type_size: f64,
57    pub tag_size: f64,
58}
59
60impl Theme {
61    pub fn node_style(&self, node_type: &NodeType) -> &NodeStyle {
62        self.node_styles.get(node_type).unwrap_or_else(|| {
63            self.node_styles.get(&NodeType::Service).unwrap()
64        })
65    }
66
67    pub fn group_fill(&self, depth: usize) -> &str {
68        let idx = depth.min(self.group_style.fills.len() - 1);
69        &self.group_style.fills[idx]
70    }
71
72    pub fn group_stroke(&self, depth: usize) -> &str {
73        let idx = depth.min(self.group_style.strokes.len() - 1);
74        &self.group_style.strokes[idx]
75    }
76}
77
78pub fn get_theme(name: &str) -> Theme {
79    match name {
80        "dark" => dark_theme(),
81        "blueprint" => blueprint_theme(),
82        "mono" => mono_theme(),
83        "sketch" => light_theme(), // sketch uses light colors + different rendering
84        _ => light_theme(),
85    }
86}
87
88fn make_node_styles(entries: Vec<(NodeType, &str, &str, &str, &str, &str, &str)>) -> HashMap<NodeType, NodeStyle> {
89    entries.into_iter().map(|(nt, fill, stroke, text, type_c, tag_bg, tag_text)| {
90        (nt, NodeStyle {
91            fill: fill.into(),
92            stroke: stroke.into(),
93            text_color: text.into(),
94            type_color: type_c.into(),
95            tag_bg: tag_bg.into(),
96            tag_text: tag_text.into(),
97        })
98    }).collect()
99}
100
101fn light_theme() -> Theme {
102    Theme {
103        name: "light".into(),
104        background: "#FAFBFC".into(),
105        canvas_padding: 40.0,
106        node_styles: make_node_styles(vec![
107            (NodeType::Service,  "#4A90D9", "#2E6AB0", "#FFFFFF", "#B8D4F0", "#EBF2FA", "#2E6AB0"),
108            (NodeType::Db,       "#E8913A", "#C47425", "#FFFFFF", "#F5D5B0", "#FDF0E2", "#C47425"),
109            (NodeType::Cache,    "#50B88E", "#3A9171", "#FFFFFF", "#B8E4D0", "#E8F6F0", "#3A9171"),
110            (NodeType::Queue,    "#8B6CC1", "#6B4FA0", "#FFFFFF", "#CFC0E5", "#F0EBF7", "#6B4FA0"),
111            (NodeType::Gateway,  "#3AAFA9", "#2B8A85", "#FFFFFF", "#B0DCD9", "#E4F4F3", "#2B8A85"),
112            (NodeType::User,     "#6B7B8D", "#4F5D6B", "#FFFFFF", "#BCC5CE", "#E8ECF0", "#4F5D6B"),
113            (NodeType::Store,    "#D4884A", "#B06E35", "#FFFFFF", "#EDD0B3", "#FAF0E3", "#B06E35"),
114            (NodeType::Fn,       "#C75C9B", "#A44580", "#FFFFFF", "#E7B8D3", "#F8EAF2", "#A44580"),
115            (NodeType::Worker,   "#5A8F6A", "#437352", "#FFFFFF", "#B5D4BD", "#E6F0E9", "#437352"),
116            (NodeType::External, "#95A5B6", "#6E8091", "#FFFFFF", "#CDD5DC", "#EDF0F3", "#6E8091"),
117        ]),
118        group_style: GroupStyle {
119            fills: vec![
120                "rgba(0,0,0,0.03)".into(),
121                "rgba(0,0,0,0.02)".into(),
122                "rgba(0,0,0,0.01)".into(),
123            ],
124            strokes: vec![
125                "#CBD5E0".into(),
126                "#E2E8F0".into(),
127                "#EDF2F7".into(),
128            ],
129            text_color: "#4A5568".into(),
130            corner_radius: 12.0,
131            padding: 24.0,
132            label_size: 13.0,
133        },
134        connection_style: ConnectionStyle {
135            stroke: "#8896A4".into(),
136            stroke_width: 1.5,
137            dashed_stroke: "#A0AEC0".into(),
138            blocked_stroke: "#E53E3E".into(),
139            text_color: "#4A5568".into(),
140            text_bg: "#FFFFFF".into(),
141            arrow_size: 8.0,
142            label_size: 11.0,
143            tag_bg: "#EDF2F7".into(),
144            tag_text: "#4A5568".into(),
145        },
146        font: FontConfig {
147            family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif".into(),
148            node_label_size: 14.0,
149            node_type_size: 10.0,
150            tag_size: 9.0,
151        },
152    }
153}
154
155fn dark_theme() -> Theme {
156    Theme {
157        name: "dark".into(),
158        background: "#1A202C".into(),
159        canvas_padding: 40.0,
160        node_styles: make_node_styles(vec![
161            (NodeType::Service,  "#2B6CB0", "#3182CE", "#E2E8F0", "#90CDF4", "#1A365D", "#90CDF4"),
162            (NodeType::Db,       "#C05621", "#DD6B20", "#E2E8F0", "#FBD38D", "#652B19", "#FBD38D"),
163            (NodeType::Cache,    "#276749", "#38A169", "#E2E8F0", "#9AE6B4", "#1C4532", "#9AE6B4"),
164            (NodeType::Queue,    "#553C9A", "#805AD5", "#E2E8F0", "#D6BCFA", "#322659", "#D6BCFA"),
165            (NodeType::Gateway,  "#234E52", "#319795", "#E2E8F0", "#81E6D9", "#1D4044", "#81E6D9"),
166            (NodeType::User,     "#4A5568", "#718096", "#E2E8F0", "#CBD5E0", "#2D3748", "#CBD5E0"),
167            (NodeType::Store,    "#9C4221", "#C05621", "#E2E8F0", "#FBD38D", "#652B19", "#FBD38D"),
168            (NodeType::Fn,       "#702459", "#B83280", "#E2E8F0", "#FBB6CE", "#521B41", "#FBB6CE"),
169            (NodeType::Worker,   "#22543D", "#38A169", "#E2E8F0", "#9AE6B4", "#1C4532", "#9AE6B4"),
170            (NodeType::External, "#2D3748", "#4A5568", "#CBD5E0", "#A0AEC0", "#1A202C", "#A0AEC0"),
171        ]),
172        group_style: GroupStyle {
173            fills: vec![
174                "rgba(255,255,255,0.04)".into(),
175                "rgba(255,255,255,0.03)".into(),
176                "rgba(255,255,255,0.02)".into(),
177            ],
178            strokes: vec![
179                "#4A5568".into(),
180                "#2D3748".into(),
181                "#1A202C".into(),
182            ],
183            text_color: "#A0AEC0".into(),
184            corner_radius: 12.0,
185            padding: 24.0,
186            label_size: 13.0,
187        },
188        connection_style: ConnectionStyle {
189            stroke: "#718096".into(),
190            stroke_width: 1.5,
191            dashed_stroke: "#4A5568".into(),
192            blocked_stroke: "#FC8181".into(),
193            text_color: "#A0AEC0".into(),
194            text_bg: "#2D3748".into(),
195            arrow_size: 8.0,
196            label_size: 11.0,
197            tag_bg: "#2D3748".into(),
198            tag_text: "#A0AEC0".into(),
199        },
200        font: FontConfig {
201            family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif".into(),
202            node_label_size: 14.0,
203            node_type_size: 10.0,
204            tag_size: 9.0,
205        },
206    }
207}
208
209fn blueprint_theme() -> Theme {
210    Theme {
211        name: "blueprint".into(),
212        background: "#0D2137".into(),
213        canvas_padding: 40.0,
214        node_styles: make_node_styles(vec![
215            (NodeType::Service,  "#133D6B", "#2980B9", "#D6EAF8", "#85C1E9", "#0B2545", "#85C1E9"),
216            (NodeType::Db,       "#1B4D3E", "#27AE60", "#D5F5E3", "#82E0AA", "#0B3D2E", "#82E0AA"),
217            (NodeType::Cache,    "#4A235A", "#8E44AD", "#E8DAEF", "#BB8FCE", "#2C1338", "#BB8FCE"),
218            (NodeType::Queue,    "#1B4F72", "#2E86C1", "#D6EAF8", "#85C1E9", "#0B3D5C", "#85C1E9"),
219            (NodeType::Gateway,  "#0E4D4D", "#17A589", "#D1F2EB", "#76D7C4", "#0A3D3D", "#76D7C4"),
220            (NodeType::User,     "#1C2833", "#5D6D7E", "#D6DBDF", "#AEB6BF", "#0E1A25", "#AEB6BF"),
221            (NodeType::Store,    "#1B3A4B", "#2E86C1", "#D6EAF8", "#85C1E9", "#0B2A3B", "#85C1E9"),
222            (NodeType::Fn,       "#4A235A", "#AF7AC5", "#E8DAEF", "#D2B4DE", "#2C1338", "#D2B4DE"),
223            (NodeType::Worker,   "#1B4D3E", "#2ECC71", "#D5F5E3", "#82E0AA", "#0B3D2E", "#82E0AA"),
224            (NodeType::External, "#1C2833", "#5D6D7E", "#D6DBDF", "#AEB6BF", "#0E1A25", "#AEB6BF"),
225        ]),
226        group_style: GroupStyle {
227            fills: vec![
228                "rgba(41,128,185,0.08)".into(),
229                "rgba(41,128,185,0.05)".into(),
230                "rgba(41,128,185,0.03)".into(),
231            ],
232            strokes: vec![
233                "#2980B9".into(),
234                "#1F6FA3".into(),
235                "#155A8A".into(),
236            ],
237            text_color: "#85C1E9".into(),
238            corner_radius: 4.0,
239            padding: 24.0,
240            label_size: 13.0,
241        },
242        connection_style: ConnectionStyle {
243            stroke: "#5DADE2".into(),
244            stroke_width: 1.0,
245            dashed_stroke: "#3498DB".into(),
246            blocked_stroke: "#E74C3C".into(),
247            text_color: "#85C1E9".into(),
248            text_bg: "#0D2137".into(),
249            arrow_size: 8.0,
250            label_size: 11.0,
251            tag_bg: "#133D6B".into(),
252            tag_text: "#85C1E9".into(),
253        },
254        font: FontConfig {
255            family: "'SF Mono', 'Fira Code', 'Consolas', monospace".into(),
256            node_label_size: 13.0,
257            node_type_size: 9.0,
258            tag_size: 9.0,
259        },
260    }
261}
262
263fn mono_theme() -> Theme {
264    Theme {
265        name: "mono".into(),
266        background: "#FFFFFF".into(),
267        canvas_padding: 40.0,
268        node_styles: make_node_styles(vec![
269            (NodeType::Service,  "#F7F7F7", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
270            (NodeType::Db,       "#F0F0F0", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
271            (NodeType::Cache,    "#F7F7F7", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
272            (NodeType::Queue,    "#F0F0F0", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
273            (NodeType::Gateway,  "#F7F7F7", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
274            (NodeType::User,     "#F0F0F0", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
275            (NodeType::Store,    "#F7F7F7", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
276            (NodeType::Fn,       "#F0F0F0", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
277            (NodeType::Worker,   "#F7F7F7", "#333333", "#333333", "#888888", "#EEEEEE", "#555555"),
278            (NodeType::External, "#F0F0F0", "#555555", "#333333", "#888888", "#EEEEEE", "#555555"),
279        ]),
280        group_style: GroupStyle {
281            fills: vec![
282                "rgba(0,0,0,0.02)".into(),
283                "rgba(0,0,0,0.01)".into(),
284                "rgba(0,0,0,0.005)".into(),
285            ],
286            strokes: vec![
287                "#CCCCCC".into(),
288                "#DDDDDD".into(),
289                "#EEEEEE".into(),
290            ],
291            text_color: "#666666".into(),
292            corner_radius: 8.0,
293            padding: 24.0,
294            label_size: 13.0,
295        },
296        connection_style: ConnectionStyle {
297            stroke: "#999999".into(),
298            stroke_width: 1.0,
299            dashed_stroke: "#BBBBBB".into(),
300            blocked_stroke: "#CC3333".into(),
301            text_color: "#666666".into(),
302            text_bg: "#FFFFFF".into(),
303            arrow_size: 8.0,
304            label_size: 11.0,
305            tag_bg: "#F0F0F0".into(),
306            tag_text: "#666666".into(),
307        },
308        font: FontConfig {
309            family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif".into(),
310            node_label_size: 14.0,
311            node_type_size: 10.0,
312            tag_size: 9.0,
313        },
314    }
315}