Skip to main content

palette_demo/
palette_demo.rs

1//! palette_demo — token palette comparison fixture.
2//!
3//! Run:
4//! `cargo run -p damascene-core --example palette_demo`
5
6use 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}