1use crate::{assets, html};
2use askama::Template;
3
4#[derive(Debug, Template)]
5#[template(path = "layouts/app_shell.html")]
6pub struct AppShell<'a> {
7 pub title: &'a str,
8 pub app_name: &'a str,
9 pub mode: &'a str,
10 pub density_class: &'a str,
11 pub asset_base_path: &'a str,
12 pub nav_html: &'a str,
13 pub actions_html: &'a str,
14 pub content_html: &'a str,
15 pub status_left: &'a str,
16 pub status_right: &'a str,
17}
18
19impl<'a> AppShell<'a> {
20 pub const fn new(title: &'a str, app_name: &'a str, content_html: &'a str) -> Self {
21 Self {
22 title,
23 app_name,
24 mode: "dark",
25 density_class: "density-dense",
26 asset_base_path: assets::DEFAULT_BASE_PATH,
27 nav_html: "",
28 actions_html: "",
29 content_html,
30 status_left: app_name,
31 status_right: "",
32 }
33 }
34
35 pub const fn with_mode(mut self, mode: &'a str) -> Self {
36 self.mode = mode;
37 self
38 }
39
40 pub const fn light(self) -> Self {
41 self.with_mode("light")
42 }
43
44 pub const fn dark(self) -> Self {
45 self.with_mode("dark")
46 }
47
48 pub const fn dense(mut self) -> Self {
49 self.density_class = "density-dense";
50 self
51 }
52
53 pub const fn default_density(mut self) -> Self {
54 self.density_class = "";
55 self
56 }
57
58 pub const fn with_asset_base_path(mut self, asset_base_path: &'a str) -> Self {
59 self.asset_base_path = asset_base_path;
60 self
61 }
62
63 pub const fn with_nav(mut self, nav_html: &'a str) -> Self {
64 self.nav_html = nav_html;
65 self
66 }
67
68 pub const fn with_actions(mut self, actions_html: &'a str) -> Self {
69 self.actions_html = actions_html;
70 self
71 }
72
73 pub const fn with_status(mut self, status_left: &'a str, status_right: &'a str) -> Self {
74 self.status_left = status_left;
75 self.status_right = status_right;
76 self
77 }
78
79 pub fn stylesheet_link(&self) -> String {
80 html::stylesheet_link(self.asset_base_path)
81 }
82
83 pub fn htmx_script_link(&self) -> String {
84 html::htmx_script_link(self.asset_base_path)
85 }
86
87 pub fn script_link(&self) -> String {
88 html::script_link(self.asset_base_path)
89 }
90}
91
92impl<'a> askama::filters::HtmlSafe for AppShell<'a> {}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn app_shell_builders_render_variants() {
100 let html = AppShell::new("Title", "Wave Funk", "<section>Content</section>")
101 .light()
102 .default_density()
103 .with_nav(r#"<a class="wf-nav-item" href="/">Home</a>"#)
104 .with_actions(r#"<button class="wf-btn">Save</button>"#)
105 .with_status("Ready", "v0.1")
106 .render()
107 .unwrap();
108
109 assert!(html.contains(r#"data-mode="light""#));
110 assert!(html.contains(r#"class="wf-app ""#));
111 assert!(html.contains(r#"<a class="wf-nav-item" href="/">Home</a>"#));
112 assert!(html.contains(r#"<button class="wf-btn">Save</button>"#));
113 assert!(html.contains(">Ready<"));
114 assert!(html.contains(">v0.1<"));
115 }
116}