1use 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}