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 = "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}