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 = "settings_calibration";
14 let theme = Theme::damascene_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(key, value, &Selection::caret(key, value.len())).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("compact-navigation", true),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch("email-summaries", false),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox("approval-required", true),
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("interface-scale", 0.66).width(Size::Fill(1.0)),
315 ]),
316 ])
317 .width(Size::Fill(1.0))
318}
319
320fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
321 row([
322 column([
323 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
324 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
325 ])
326 .gap(2.0)
327 .width(Size::Fill(1.0))
328 .height(Size::Hug),
329 control,
330 ])
331 .gap(tokens::SPACE_2)
332 .height(Size::Fixed(44.0))
333 .align(Align::Center)
334}
335
336fn section_label(label: &'static str) -> El {
337 text(label)
338 .caption()
339 .height(Size::Fixed(22.0))
340 .padding(Sides::xy(tokens::SPACE_2, 0.0))
341}
342
343fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
344 let mut item = row([
345 icon(icon_name)
346 .color(tokens::MUTED_FOREGROUND)
347 .icon_size(tokens::ICON_SM)
348 .width(Size::Fixed(tokens::ICON_SM)),
349 text(label)
350 .font_weight(FontWeight::Medium)
351 .ellipsis()
352 .width(Size::Fill(1.0)),
353 ])
354 .key(if selected {
355 "metric:sidebar.nav.row".to_string()
356 } else {
357 format!("side-item-{label}")
358 })
359 .metrics_role(MetricsRole::ListItem)
360 .gap(tokens::SPACE_2)
361 .padding(Sides::xy(tokens::SPACE_2, 0.0))
362 .height(Size::Fixed(32.0))
363 .align(Align::Center)
364 .focusable();
365
366 if selected {
367 item = item.current();
368 } else {
369 item = item.color(tokens::MUTED_FOREGROUND);
370 }
371
372 item
373}
374
375fn icon_slot(icon_name: &'static str) -> El {
376 El::new(Kind::Custom("icon_cell"))
377 .style_profile(StyleProfile::Surface)
378 .child(
379 icon(icon_name)
380 .color(tokens::FOREGROUND)
381 .icon_size(tokens::ICON_XS),
382 )
383 .align(Align::Center)
384 .justify(Justify::Center)
385 .fill(tokens::MUTED)
386 .stroke(tokens::BORDER)
387 .radius(tokens::RADIUS_SM)
388 .width(Size::Fixed(30.0))
389 .height(Size::Fixed(30.0))
390}