1use damascene_core::prelude::*;
7
8#[derive(Clone, Copy)]
9struct TokenDef {
10 name: &'static str,
11 color: Color,
12}
13
14fn main() -> std::io::Result<()> {
15 let viewport = Rect::new(0.0, 0.0, 1220.0, 1040.0);
16 let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("out");
17
18 let variants = [
19 (
20 "palette_demo.damascene_dark",
21 "Damascene dark",
22 Theme::damascene_dark(),
23 ),
24 (
25 "palette_demo.damascene_light",
26 "Damascene light",
27 Theme::damascene_light(),
28 ),
29 (
30 "palette_demo.radix_slate_blue_dark",
31 "Radix slate + blue dark",
32 Theme::radix_slate_blue_dark(),
33 ),
34 (
35 "palette_demo.radix_slate_blue_light",
36 "Radix slate + blue light",
37 Theme::radix_slate_blue_light(),
38 ),
39 (
40 "palette_demo.radix_sand_amber_dark",
41 "Radix sand + amber dark",
42 Theme::radix_sand_amber_dark(),
43 ),
44 (
45 "palette_demo.radix_sand_amber_light",
46 "Radix sand + amber light",
47 Theme::radix_sand_amber_light(),
48 ),
49 (
50 "palette_demo.radix_mauve_violet_dark",
51 "Radix mauve + violet dark",
52 Theme::radix_mauve_violet_dark(),
53 ),
54 (
55 "palette_demo.radix_mauve_violet_light",
56 "Radix mauve + violet light",
57 Theme::radix_mauve_violet_light(),
58 ),
59 ];
60
61 for (file_name, label, theme) in variants {
62 let mut root = palette_demo(label, theme.palette());
63 let bundle = render_bundle_themed(&mut root, viewport, &theme);
64 let written = write_bundle(&bundle, &out_dir, file_name)?;
65 for p in &written {
66 println!("wrote {}", p.display());
67 }
68
69 if !bundle.lint.findings.is_empty() {
70 eprintln!(
71 "\nlint findings for {file_name} ({}):",
72 bundle.lint.findings.len()
73 );
74 eprint!("{}", bundle.lint.text());
75 }
76 }
77
78 Ok(())
79}
80
81fn palette_demo(label: &'static str, palette: &Palette) -> El {
82 column([
83 row([
84 column([
85 h1("Palette demo"),
86 text("Damascene and Radix palettes rendered through Damascene tokens.").muted(),
87 ])
88 .gap(tokens::SPACE_2)
89 .height(Size::Hug),
90 spacer(),
91 badge(label).muted(),
92 ])
93 .align(Align::Start)
94 .height(Size::Hug),
95 row([
96 token_section(
97 "Core tokens",
98 "The shadcn-shaped semantic vocabulary.",
99 &CORE_TOKENS,
100 palette,
101 )
102 .width(Size::Fill(1.25)),
103 column([
104 token_section(
105 "Damascene extensions",
106 "Component and status tokens layered on top.",
107 &EXTENSION_TOKENS,
108 palette,
109 ),
110 component_section(),
111 ])
112 .gap(tokens::SPACE_4)
113 .width(Size::Fill(1.0))
114 .height(Size::Fill(1.0)),
115 ])
116 .gap(tokens::SPACE_4)
117 .align(Align::Stretch)
118 .height(Size::Fill(1.0)),
119 ])
120 .padding(tokens::SPACE_8)
121 .gap(tokens::SPACE_6)
122 .fill_size()
123 .fill(tokens::BACKGROUND)
124}
125
126const CORE_TOKENS: [TokenDef; 19] = [
127 TokenDef {
128 name: "background",
129 color: tokens::BACKGROUND,
130 },
131 TokenDef {
132 name: "foreground",
133 color: tokens::FOREGROUND,
134 },
135 TokenDef {
136 name: "card",
137 color: tokens::CARD,
138 },
139 TokenDef {
140 name: "card-foreground",
141 color: tokens::CARD_FOREGROUND,
142 },
143 TokenDef {
144 name: "popover",
145 color: tokens::POPOVER,
146 },
147 TokenDef {
148 name: "popover-foreground",
149 color: tokens::POPOVER_FOREGROUND,
150 },
151 TokenDef {
152 name: "primary",
153 color: tokens::PRIMARY,
154 },
155 TokenDef {
156 name: "primary-foreground",
157 color: tokens::PRIMARY_FOREGROUND,
158 },
159 TokenDef {
160 name: "secondary",
161 color: tokens::SECONDARY,
162 },
163 TokenDef {
164 name: "secondary-foreground",
165 color: tokens::SECONDARY_FOREGROUND,
166 },
167 TokenDef {
168 name: "muted",
169 color: tokens::MUTED,
170 },
171 TokenDef {
172 name: "muted-foreground",
173 color: tokens::MUTED_FOREGROUND,
174 },
175 TokenDef {
176 name: "accent",
177 color: tokens::ACCENT,
178 },
179 TokenDef {
180 name: "accent-foreground",
181 color: tokens::ACCENT_FOREGROUND,
182 },
183 TokenDef {
184 name: "destructive",
185 color: tokens::DESTRUCTIVE,
186 },
187 TokenDef {
188 name: "destructive-foreground",
189 color: tokens::DESTRUCTIVE_FOREGROUND,
190 },
191 TokenDef {
192 name: "border",
193 color: tokens::BORDER,
194 },
195 TokenDef {
196 name: "input",
197 color: tokens::INPUT,
198 },
199 TokenDef {
200 name: "ring",
201 color: tokens::RING,
202 },
203];
204
205const EXTENSION_TOKENS: [TokenDef; 12] = [
206 TokenDef {
207 name: "success",
208 color: tokens::SUCCESS,
209 },
210 TokenDef {
211 name: "success-foreground",
212 color: tokens::SUCCESS_FOREGROUND,
213 },
214 TokenDef {
215 name: "warning",
216 color: tokens::WARNING,
217 },
218 TokenDef {
219 name: "warning-foreground",
220 color: tokens::WARNING_FOREGROUND,
221 },
222 TokenDef {
223 name: "info",
224 color: tokens::INFO,
225 },
226 TokenDef {
227 name: "info-foreground",
228 color: tokens::INFO_FOREGROUND,
229 },
230 TokenDef {
231 name: "link-foreground",
232 color: tokens::LINK_FOREGROUND,
233 },
234 TokenDef {
235 name: "overlay-scrim",
236 color: tokens::OVERLAY_SCRIM,
237 },
238 TokenDef {
239 name: "scrollbar-thumb",
240 color: tokens::SCROLLBAR_THUMB_FILL,
241 },
242 TokenDef {
243 name: "scrollbar-thumb-active",
244 color: tokens::SCROLLBAR_THUMB_FILL_ACTIVE,
245 },
246 TokenDef {
247 name: "selection-bg",
248 color: tokens::SELECTION_BG,
249 },
250 TokenDef {
251 name: "selection-bg-unfocused",
252 color: tokens::SELECTION_BG_UNFOCUSED,
253 },
254];
255
256fn token_section(
257 title: &'static str,
258 description: &'static str,
259 tokens: &'static [TokenDef],
260 palette: &Palette,
261) -> El {
262 card([
263 card_header([card_title(title), card_description(description)]),
264 card_content([swatch_grid(tokens, palette)]),
265 ])
266 .height(Size::Fill(1.0))
267}
268
269fn swatch_grid(defs: &'static [TokenDef], palette: &Palette) -> El {
270 let rows = defs
271 .chunks(2)
272 .map(|chunk| {
273 row(chunk.iter().map(|token| token_chip(*token, palette)))
274 .gap(tokens::SPACE_3)
275 .align(Align::Center)
276 .width(Size::Fill(1.0))
277 })
278 .collect::<Vec<_>>();
279
280 column(rows)
281 .gap(tokens::SPACE_3)
282 .width(Size::Fill(1.0))
283 .height(Size::Hug)
284}
285
286fn token_chip(token: TokenDef, palette: &Palette) -> El {
287 let resolved = palette.resolve(token.color);
288 row([
289 El::new(Kind::Custom("palette-swatch"))
290 .at(file!(), line!())
291 .fill(token.color)
292 .stroke(tokens::BORDER)
293 .radius(tokens::RADIUS_SM)
294 .width(Size::Fixed(42.0))
295 .height(Size::Fixed(34.0)),
296 column([
297 text(token.name)
298 .label()
299 .ellipsis()
300 .nowrap_text()
301 .width(Size::Fill(1.0)),
302 mono(rgba_label(resolved)).caption().muted(),
303 ])
304 .gap(0.0)
305 .width(Size::Fill(1.0))
306 .height(Size::Hug),
307 ])
308 .gap(tokens::SPACE_2)
309 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
310 .align(Align::Center)
311 .fill(tokens::CARD)
312 .stroke(tokens::BORDER)
313 .radius(tokens::RADIUS_MD)
314 .width(Size::Fill(1.0))
315 .height(Size::Fixed(54.0))
316}
317
318fn component_section() -> El {
319 card([
320 card_header([
321 card_title("Stock widgets"),
322 card_description("The same palette applied to regular component constructors."),
323 ]),
324 card_content([
325 row([
326 button("Primary").primary(),
327 button("Secondary").secondary(),
328 button("Outline").outline(),
329 button("Ghost").ghost(),
330 ])
331 .gap(tokens::SPACE_2)
332 .align(Align::Center),
333 row([
334 badge("success").success(),
335 badge("warning").warning(),
336 badge("destructive").destructive(),
337 badge("info").info(),
338 badge("muted").muted(),
339 ])
340 .gap(tokens::SPACE_2)
341 .align(Align::Center),
342 row([
343 text_input("palette search", &Selection::default(), "palette:search")
344 .width(Size::Fill(1.0)),
345 button_with_icon("settings", "Tune").secondary(),
346 ])
347 .gap(tokens::SPACE_2)
348 .align(Align::Center),
349 row([
350 surface_sample("Card", tokens::CARD),
351 surface_sample("Muted", tokens::MUTED),
352 surface_sample("Popover", tokens::POPOVER),
353 ])
354 .gap(tokens::SPACE_3)
355 .align(Align::Stretch),
356 ])
357 .gap(tokens::SPACE_4),
358 ])
359 .height(Size::Hug)
360}
361
362fn surface_sample(title: &'static str, fill: Color) -> El {
363 column([
364 text(title).label(),
365 text("surface sample").caption().muted(),
366 ])
367 .gap(tokens::SPACE_1)
368 .padding(tokens::SPACE_3)
369 .fill(fill)
370 .stroke(tokens::BORDER)
371 .radius(tokens::RADIUS_MD)
372 .width(Size::Fill(1.0))
373 .height(Size::Fixed(76.0))
374}
375
376fn rgba_label(c: Color) -> String {
377 let [r, g, b, a] = c.to_srgb_u8a();
378 if a == 255 {
379 format!("#{r:02x}{g:02x}{b:02x}")
380 } else {
381 format!("#{r:02x}{g:02x}{b:02x}/{a:03}")
382 }
383}