Skip to main content

dashboard_01_calibration/
dashboard_01_calibration.rs

1//! dashboard_01_calibration — Damascene fixture paired with the shadcn
2//! dashboard-01-style reference.
3//!
4//! Run:
5//! `cargo run -p damascene-core --example dashboard_01_calibration`
6
7use damascene_core::prelude::*;
8
9fn main() -> std::io::Result<()> {
10    let viewport = Rect::new(0.0, 0.0, 1180.0, 780.0);
11    let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("out");
12
13    let name = "dashboard_01_calibration";
14    let theme = Theme::damascene_dark();
15    let mut root = dashboard_01_calibration();
16    let bundle = render_bundle_themed(&mut root, viewport, &theme);
17    let written = write_bundle(&bundle, &out_dir, name)?;
18    for p in &written {
19        println!("wrote {}", p.display());
20    }
21    if !bundle.lint.findings.is_empty() {
22        eprintln!(
23            "\nlint findings for {name} ({}):",
24            bundle.lint.findings.len()
25        );
26        eprint!("{}", bundle.lint.text());
27    }
28
29    Ok(())
30}
31
32fn dashboard_01_calibration() -> El {
33    row([dashboard_sidebar(), dashboard_main()])
34        .key("metric:root")
35        .gap(0.0)
36        .fill_size()
37        .align(Align::Stretch)
38        .fill(tokens::BACKGROUND)
39}
40
41fn dashboard_sidebar() -> El {
42    column([
43        row([
44            icon_cell("A"),
45            column([
46                text("Acme Inc.")
47                    .semibold()
48                    .ellipsis()
49                    .width(Size::Fill(1.0)),
50                text("Enterprise")
51                    .caption()
52                    .ellipsis()
53                    .width(Size::Fill(1.0)),
54            ])
55            .gap(2.0)
56            .width(Size::Fill(1.0))
57            .height(Size::Hug),
58        ])
59        .gap(tokens::SPACE_2)
60        .height(Size::Fixed(44.0))
61        .align(Align::Center),
62        section_label("Platform"),
63        side_item("layout-dashboard", "Dashboard", true),
64        side_item("activity", "Lifecycle", false),
65        side_item("bar-chart", "Analytics", false),
66        side_item("folder", "Projects", false),
67        spacer().height(Size::Fixed(tokens::SPACE_4)),
68        section_label("Documents"),
69        side_item("file-text", "Data library", false),
70        side_item("download", "Reports", false),
71        side_item("users", "Team", false),
72        spacer(),
73        row([
74            icon_cell("AK"),
75            column([
76                text("Alicia Koch")
77                    .semibold()
78                    .ellipsis()
79                    .width(Size::Fill(1.0)),
80                text("alicia@example.com")
81                    .caption()
82                    .ellipsis()
83                    .width(Size::Fill(1.0)),
84            ])
85            .gap(2.0)
86            .width(Size::Fill(1.0))
87            .height(Size::Hug),
88        ])
89        .gap(tokens::SPACE_2)
90        .height(Size::Fixed(50.0))
91        .align(Align::Center),
92    ])
93    .gap(tokens::SPACE_2)
94    .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95    .key("metric:sidebar")
96    .width(Size::Fixed(244.0))
97    .height(Size::Fill(1.0))
98    .fill(tokens::CARD)
99    .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103    text(label)
104        .caption()
105        .height(Size::Fixed(22.0))
106        .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110    let mut item = row([
111        icon(icon_name)
112            .color(tokens::MUTED_FOREGROUND)
113            .icon_size(tokens::ICON_SM)
114            .width(Size::Fixed(tokens::ICON_SM)),
115        text(label)
116            .font_weight(FontWeight::Medium)
117            .ellipsis()
118            .width(Size::Fill(1.0)),
119    ])
120    .key(if selected {
121        "metric:sidebar.nav.row".to_string()
122    } else {
123        format!("side-item-{label}")
124    })
125    .metrics_role(MetricsRole::ListItem)
126    .gap(tokens::SPACE_2)
127    .padding(Sides::xy(tokens::SPACE_2, 0.0))
128    .height(Size::Fixed(32.0))
129    .align(Align::Center)
130    .focusable();
131
132    if selected {
133        item = item.current();
134    } else {
135        item = item.color(tokens::MUTED_FOREGROUND);
136    }
137
138    item
139}
140
141fn dashboard_main() -> El {
142    column([
143        dashboard_header(),
144        column([
145            row([
146                metric_card(
147                    "bar-chart",
148                    "Total Revenue",
149                    "$1,250.00",
150                    "+12.5%",
151                    "Trending up this month",
152                    true,
153                ),
154                metric_card(
155                    "users",
156                    "New Customers",
157                    "1,234",
158                    "-20%",
159                    "Acquisition needs attention",
160                    false,
161                ),
162                metric_card(
163                    "folder",
164                    "Active Accounts",
165                    "45,678",
166                    "+12.5%",
167                    "Strong user retention",
168                    true,
169                ),
170                metric_card(
171                    "activity",
172                    "Growth Rate",
173                    "4.5%",
174                    "+4.5%",
175                    "Meets growth projections",
176                    true,
177                ),
178            ])
179            .gap(tokens::SPACE_4),
180            row([chart_card(), sales_card()])
181                .gap(tokens::SPACE_4)
182                .height(Size::Fixed(306.0))
183                .align(Align::Stretch),
184            documents_card(),
185        ])
186        .gap(tokens::SPACE_4)
187        .padding(tokens::SPACE_7)
188        .height(Size::Fill(1.0)),
189    ])
190    .width(Size::Fill(1.0))
191    .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195    row([
196        icon_button("menu").ghost(),
197        divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198        h3("Documents").key("metric:page.title"),
199        spacer(),
200        text_input_with(
201            "dashboard-search",
202            "",
203            &Selection::default(),
204            TextInputOpts::default().placeholder("Search..."),
205        )
206        .key("metric:command.input")
207        .width(Size::Fixed(260.0)),
208        icon_button("plus").ghost(),
209        icon_button("bell").ghost(),
210    ])
211    .key("metric:header")
212    .gap(tokens::SPACE_3)
213    .height(Size::Fixed(56.0))
214    .padding(Sides::xy(tokens::SPACE_4, 0.0))
215    .align(Align::Center)
216    .stroke(tokens::BORDER)
217}
218
219fn metric_card(
220    icon_name: &'static str,
221    title: &'static str,
222    value: &'static str,
223    delta: &'static str,
224    note: &'static str,
225    positive: bool,
226) -> El {
227    let badge = if positive {
228        badge(delta).success()
229    } else {
230        badge(delta).warning()
231    };
232    let badge = if title == "Total Revenue" {
233        badge.key("metric:kpi.badge")
234    } else {
235        badge
236    };
237    let value = if title == "Total Revenue" {
238        h2(value).ellipsis().key("metric:kpi.value")
239    } else {
240        h2(value).ellipsis()
241    };
242    card([card_content([
243        row([
244            row([
245                icon(icon_name)
246                    .color(tokens::MUTED_FOREGROUND)
247                    .icon_size(tokens::ICON_XS),
248                text(title).muted().ellipsis().width(Size::Fill(1.0)),
249            ])
250            .gap(tokens::SPACE_1)
251            .width(Size::Fill(1.0))
252            .align(Align::Center),
253            badge,
254        ])
255        .gap(tokens::SPACE_2)
256        .align(Align::Center),
257        value,
258        text(note).caption().ellipsis().width(Size::Fill(1.0)),
259    ])
260    .padding(tokens::SPACE_4)
261    .gap(tokens::SPACE_2)])
262    .key(if title == "Total Revenue" {
263        "metric:kpi.card"
264    } else {
265        title
266    })
267    .width(Size::Fill(1.0))
268}
269
270fn chart_card() -> El {
271    card([
272        card_header([
273            card_title("Visitors for the last 6 months"),
274            card_description("Total visitors by channel."),
275        ])
276        .padding(tokens::SPACE_4),
277        card_content([row(chart_bars())
278            .gap(2.0)
279            .height(Size::Fill(1.0))
280            .align(Align::End)])
281        .padding(Sides {
282            left: tokens::SPACE_4,
283            right: tokens::SPACE_4,
284            top: 0.0,
285            bottom: tokens::SPACE_4,
286        })
287        .height(Size::Fill(1.0)),
288    ])
289    .key("metric:chart.card")
290    .width(Size::Fill(1.0))
291    .height(Size::Fill(1.0))
292}
293
294fn chart_bars() -> Vec<El> {
295    [
296        48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
297    ]
298    .into_iter()
299    .flat_map(|height| {
300        [
301            bar(height, tokens::MUTED_FOREGROUND),
302            bar((height - 28.0_f32).max(24.0), tokens::INPUT),
303        ]
304    })
305    .collect()
306}
307
308fn bar(height: f32, color: Color) -> El {
309    El::new(Kind::Custom("chart_bar"))
310        .fill(color)
311        .radius(tokens::RADIUS_SM)
312        .width(Size::Fill(1.0))
313        .height(Size::Fixed(height))
314}
315
316fn sales_card() -> El {
317    card([
318        card_header([
319            card_title("Recent Sales"),
320            card_description("You made 265 sales this month."),
321        ])
322        .padding(tokens::SPACE_4),
323        card_content([
324            sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
325            sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
326            sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
327            sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
328        ])
329        .gap(tokens::SPACE_2)
330        .padding(Sides {
331            left: tokens::SPACE_4,
332            right: tokens::SPACE_4,
333            top: 0.0,
334            bottom: tokens::SPACE_4,
335        }),
336    ])
337    .key("metric:sales.card")
338    .width(Size::Fixed(330.0))
339    .height(Size::Fill(1.0))
340}
341
342fn sale_row(
343    initials: &'static str,
344    name: &'static str,
345    email: &'static str,
346    amount: &'static str,
347) -> El {
348    row([
349        icon_cell(initials),
350        column([
351            text(name).semibold().ellipsis().width(Size::Fill(1.0)),
352            text(email).caption().ellipsis().width(Size::Fill(1.0)),
353        ])
354        .gap(2.0)
355        .height(Size::Hug)
356        .width(Size::Fill(1.0)),
357        text(amount).label().small(),
358    ])
359    .gap(tokens::SPACE_2)
360    .height(Size::Fixed(42.0))
361    .align(Align::Center)
362}
363
364fn documents_card() -> El {
365    card([
366        card_header([card_title("Documents")]).padding(tokens::SPACE_4),
367        card_content([scroll([table([
368            table_header([table_row([
369                table_head("").width(Size::Fixed(35.0)),
370                table_head("Header").width(Size::Fill(1.8)),
371                table_head("Section Type").width(Size::Fill(1.0)),
372                table_head("Status").width(Size::Fixed(104.0)),
373                table_head("Target").width(Size::Fixed(64.0)),
374                table_head("Limit").width(Size::Fixed(64.0)),
375                table_head("Reviewer").width(Size::Fixed(128.0)),
376                table_head("").width(Size::Fixed(32.0)),
377            ])
378            .padding(Sides::xy(tokens::SPACE_4, 0.0))
379            .key("metric:table.header")]),
380            divider(),
381            table_body([
382                document_row(
383                    "Cover page",
384                    "Cover page",
385                    "In Process",
386                    "18",
387                    "5",
388                    "Eddie Lake",
389                    "info",
390                ),
391                document_row(
392                    "Table of contents",
393                    "Table of contents",
394                    "Done",
395                    "29",
396                    "24",
397                    "Eddie Lake",
398                    "success",
399                ),
400            ]),
401        ])])
402        .height(Size::Fill(1.0))])
403        .gap(0.0)
404        .padding(0.0)
405        .height(Size::Fill(1.0)),
406    ])
407    .key("metric:table.card")
408    .height(Size::Fill(1.0))
409}
410
411fn document_row(
412    header: &'static str,
413    section: &'static str,
414    status: &'static str,
415    target: &'static str,
416    limit: &'static str,
417    reviewer: &'static str,
418    tone: &'static str,
419) -> El {
420    let status_badge = match tone {
421        "success" => badge(status).success(),
422        _ => badge(status).info(),
423    };
424    table_row([
425        table_utility_cell("::"),
426        table_cell(text(header).label().small()).width(Size::Fill(1.8)),
427        table_cell(text(section).muted()).width(Size::Fill(1.0)),
428        table_cell(status_badge).width(Size::Fixed(104.0)),
429        table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
430        table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
431        table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
432        table_action_cell(),
433    ])
434    .padding(Sides::xy(tokens::SPACE_4, 0.0))
435    .key(if header == "Cover page" {
436        "metric:table.row"
437    } else {
438        header
439    })
440}
441
442fn table_utility_cell(label: &'static str) -> El {
443    table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
444}
445
446fn table_action_cell() -> El {
447    stack([icon("more-horizontal")
448        .icon_size(tokens::ICON_SM)
449        .color(tokens::MUTED_FOREGROUND)])
450    .align(Align::Center)
451    .justify(Justify::Center)
452    .width(Size::Fixed(32.0))
453    .height(Size::Hug)
454}
455
456fn icon_cell(label: &'static str) -> El {
457    El::new(Kind::Custom("icon_cell"))
458        .style_profile(StyleProfile::Surface)
459        .text(label)
460        .text_align(TextAlign::Center)
461        .caption()
462        .font_weight(FontWeight::Semibold)
463        .fill(tokens::MUTED)
464        .radius(tokens::RADIUS_SM)
465        .width(Size::Fixed(30.0))
466        .height(Size::Fixed(30.0))
467}