Skip to main content

settings_calibration/
settings_calibration.rs

1//! settings_calibration — Aetna fixture paired with the shadcn
2//! settings/form reference.
3//!
4//! Run:
5//! `cargo run -p aetna-core --example settings_calibration`
6
7use 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 = "settings_calibration";
14    let theme = Theme::aetna_dark();
15    let mut root = settings_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 settings_calibration() -> El {
33    row([settings_sidebar(), settings_main()])
34        .key("metric:root")
35        .gap(0.0)
36        .fill_size()
37        .align(Align::Stretch)
38        .fill(tokens::BACKGROUND)
39}
40
41fn settings_sidebar() -> El {
42    column([
43        row([
44            icon_slot("settings"),
45            column([
46                text("Workspace")
47                    .semibold()
48                    .ellipsis()
49                    .width(Size::Fill(1.0)),
50                text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51            ])
52            .gap(2.0)
53            .width(Size::Fill(1.0))
54            .height(Size::Hug),
55        ])
56        .gap(tokens::SPACE_2)
57        .height(Size::Fixed(44.0))
58        .align(Align::Center),
59        section_label("Personal"),
60        side_item("users", "Profile", false),
61        side_item("settings", "Account", true),
62        side_item("alert-circle", "Security", false),
63        side_item("bell", "Notifications", false),
64        spacer().height(Size::Fixed(tokens::SPACE_4)),
65        section_label("Workspace"),
66        side_item("file-text", "Billing", false),
67        side_item("bar-chart", "Appearance", false),
68        side_item("activity", "Integrations", false),
69        spacer(),
70        column([text("Changes sync after save.").caption().wrap_text()])
71            .padding(tokens::SPACE_2)
72            .fill(tokens::MUTED)
73            .radius(tokens::RADIUS_MD),
74    ])
75    .gap(tokens::SPACE_2)
76    .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77    .key("metric:sidebar")
78    .width(Size::Fixed(244.0))
79    .height(Size::Fill(1.0))
80    .fill(tokens::CARD)
81    .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85    column([
86        settings_header(),
87        row([settings_nav_card(), settings_body(), settings_aside()])
88            .gap(tokens::SPACE_4)
89            .padding(tokens::SPACE_4)
90            .height(Size::Fill(1.0))
91            .align(Align::Stretch),
92    ])
93    .width(Size::Fill(1.0))
94    .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98    row([
99        icon_button("menu").ghost(),
100        divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101        h3("Settings").key("metric:page.title"),
102        spacer(),
103        button("Reset").secondary(),
104        button("Save changes").primary(),
105    ])
106    .key("metric:header")
107    .gap(tokens::SPACE_3)
108    .height(Size::Fixed(56.0))
109    .padding(Sides::xy(tokens::SPACE_4, 0.0))
110    .align(Align::Center)
111    .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115    column([
116        settings_nav_item("Account", true),
117        settings_nav_item("Security", false),
118        settings_nav_item("Notifications", false),
119        settings_nav_item("Appearance", false),
120        settings_nav_item("Billing", false),
121    ])
122    .gap(tokens::SPACE_1)
123    .padding(tokens::SPACE_1)
124    .width(Size::Fixed(220.0))
125    .height(Size::Fill(1.0))
126    .style_profile(StyleProfile::Surface)
127    .surface_role(SurfaceRole::Panel)
128    .fill(tokens::CARD)
129    .stroke(tokens::BORDER)
130    .radius(tokens::RADIUS_MD)
131    .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135    let mut item = row([
136        El::new(Kind::Custom("nav-dot"))
137            .fill(tokens::MUTED_FOREGROUND)
138            .radius(tokens::RADIUS_PILL)
139            .width(Size::Fixed(6.0))
140            .height(Size::Fixed(6.0)),
141        text(label)
142            .font_weight(FontWeight::Medium)
143            .ellipsis()
144            .width(Size::Fill(1.0)),
145    ])
146    .key(if selected {
147        "metric:settings.nav.row".to_string()
148    } else {
149        format!("settings-nav-{label}")
150    })
151    .metrics_role(MetricsRole::ListItem)
152    .align(Align::Center)
153    .focusable();
154
155    if selected {
156        item = item.current();
157    } else {
158        item = item.color(tokens::MUTED_FOREGROUND);
159    }
160
161    item
162}
163
164fn settings_body() -> El {
165    column([
166        column([
167            h1("Account").heading().key("metric:section.title"),
168            text("Manage identity, workspace defaults, and security preferences.")
169                .muted()
170                .wrap_text()
171                .key("metric:page.subtitle"),
172        ])
173        .gap(tokens::SPACE_1)
174        .height(Size::Hug),
175        scroll([profile_card(), preferences_card()])
176            .key("settings-body-scroll")
177            .gap(tokens::SPACE_4)
178            .width(Size::Fill(1.0))
179            .height(Size::Fill(1.0)),
180    ])
181    .gap(tokens::SPACE_4)
182    .width(Size::Fill(1.0))
183    .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187    card([
188        card_header([
189            card_title("Profile"),
190            card_description("This information appears in audit logs and shared documents."),
191        ]),
192        card_content([form([
193            row([
194                setting_field("Display name", "Alicia Koch", "display-name"),
195                setting_field("Email", "alicia@acme.co", "email"),
196            ])
197            .gap(tokens::SPACE_3),
198            row([
199                setting_select("Role", "Workspace admin", "role"),
200                setting_select("Region", "US East", "region"),
201            ])
202            .gap(tokens::SPACE_3),
203        ])]),
204    ])
205    .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209    form_item([
210        form_label(label),
211        form_control(
212            text_input(value, &Selection::caret(key, value.len()), key).key(
213                if key == "display-name" {
214                    "metric:form.input"
215                } else {
216                    key
217                },
218            ),
219        ),
220    ])
221    .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225    form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229    card([
230        card_header([
231            card_title("Preferences"),
232            card_description("Defaults used when creating new dashboards and exports."),
233        ]),
234        card_content([column([
235            preference_row(
236                "Compact navigation",
237                "Use tighter rows in the sidebar and command menus.",
238                switch(true).key("compact-navigation"),
239            ),
240            divider(),
241            preference_row(
242                "Email summaries",
243                "Send a daily digest when documents change.",
244                switch(false).key("email-summaries"),
245            ),
246            divider(),
247            preference_row(
248                "Require approval",
249                "Route external sharing through an owner review.",
250                checkbox(true).key("approval-required"),
251            ),
252        ])
253        .gap(0.0)
254        .width(Size::Fill(1.0))])
255        .padding(0.0),
256    ])
257    .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261    row([
262        column([
263            text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264            text(description)
265                .caption()
266                .ellipsis()
267                .width(Size::Fill(1.0)),
268        ])
269        .gap(2.0)
270        .width(Size::Fill(1.0))
271        .height(Size::Hug),
272        control,
273    ])
274    .key(if title == "Compact navigation" {
275        "metric:preference.row".to_string()
276    } else {
277        format!("preference-{title}")
278    })
279    .metrics_role(MetricsRole::PreferenceRow)
280    .gap(tokens::SPACE_4)
281    .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282    .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286    column([security_card(), scale_card()])
287        .gap(tokens::SPACE_4)
288        .width(Size::Fixed(300.0))
289        .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293    card([
294        card_header([
295            card_title("Security"),
296            card_description("Two-factor authentication is enabled for all privileged users."),
297        ]),
298        card_content([
299            compact_stat("Passkeys", "2 registered", badge("On").success()),
300            compact_stat("Sessions", "3 active", button("Review").secondary()),
301        ]),
302    ])
303    .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307    card([
308        card_header([
309            card_title("Interface scale"),
310            card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311        ]),
312        card_content([
313            row([text("Dense").caption(), spacer(), text("Default").caption()]),
314            slider(0.66, tokens::PRIMARY)
315                .key("interface-scale")
316                .width(Size::Fill(1.0)),
317        ]),
318    ])
319    .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323    row([
324        column([
325            text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326            text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327        ])
328        .gap(2.0)
329        .width(Size::Fill(1.0))
330        .height(Size::Hug),
331        control,
332    ])
333    .gap(tokens::SPACE_2)
334    .height(Size::Fixed(44.0))
335    .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339    text(label)
340        .caption()
341        .height(Size::Fixed(22.0))
342        .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346    let mut item = row([
347        icon(icon_name)
348            .color(tokens::MUTED_FOREGROUND)
349            .icon_size(tokens::ICON_SM)
350            .width(Size::Fixed(tokens::ICON_SM)),
351        text(label)
352            .font_weight(FontWeight::Medium)
353            .ellipsis()
354            .width(Size::Fill(1.0)),
355    ])
356    .key(if selected {
357        "metric:sidebar.nav.row".to_string()
358    } else {
359        format!("side-item-{label}")
360    })
361    .metrics_role(MetricsRole::ListItem)
362    .gap(tokens::SPACE_2)
363    .padding(Sides::xy(tokens::SPACE_2, 0.0))
364    .height(Size::Fixed(32.0))
365    .align(Align::Center)
366    .focusable();
367
368    if selected {
369        item = item.current();
370    } else {
371        item = item.color(tokens::MUTED_FOREGROUND);
372    }
373
374    item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378    El::new(Kind::Custom("icon_cell"))
379        .style_profile(StyleProfile::Surface)
380        .child(
381            icon(icon_name)
382                .color(tokens::FOREGROUND)
383                .icon_size(tokens::ICON_XS),
384        )
385        .align(Align::Center)
386        .justify(Justify::Center)
387        .fill(tokens::MUTED)
388        .stroke(tokens::BORDER)
389        .radius(tokens::RADIUS_SM)
390        .width(Size::Fixed(30.0))
391        .height(Size::Fixed(30.0))
392}