1use aetna_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::aetna_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("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}