Skip to main content

palette_demo/
palette_demo.rs

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