Skip to main content

orbital_base_components/layout/
base.rs

1use leptos::prelude::*;
2
3use super::types::LayoutPosition;
4
5/// Headless application shell root — structure only, no theme styling.
6#[component]
7pub fn BaseLayout(
8    #[prop(optional, into)] class: MaybeProp<String>,
9    #[prop(optional, into)] style: MaybeProp<String>,
10    #[prop(optional, into)] data_testid: MaybeProp<String>,
11    #[prop(default = false)] overlay_header: bool,
12    #[prop(default = LayoutPosition::Static)] position: LayoutPosition,
13    children: Children,
14) -> impl IntoView {
15    let root_class = Memo::new(move |_| {
16        let extra = class.get().unwrap_or_default();
17        let mut parts = vec!["orbital-layout".to_string()];
18        if overlay_header {
19            parts.push("orbital-layout--overlay-header".to_string());
20        }
21        parts.push(position.modifier_class().to_string());
22        if !extra.is_empty() {
23            parts.push(extra);
24        }
25        parts.join(" ")
26    });
27
28    let root_style = move || style.get().unwrap_or_default();
29    let overlay_header_attr = move || overlay_header.to_string();
30
31    if let Some(test_id) = data_testid.get() {
32        view! {
33            <div
34                class=root_class
35                style=root_style
36                data-testid=test_id
37                data-layout-overlay-header=overlay_header_attr
38                data-layout-position=position.as_data()
39            >
40                {children()}
41            </div>
42        }
43        .into_any()
44    } else {
45        view! {
46            <div
47                class=root_class
48                style=root_style
49                data-layout-overlay-header=overlay_header_attr
50                data-layout-position=position.as_data()
51            >
52                {children()}
53            </div>
54        }
55        .into_any()
56    }
57}
58
59/// Headless flex row body beneath the layout header region.
60#[component]
61pub fn BaseLayoutBody(
62    #[prop(optional, into)] class: MaybeProp<String>,
63    #[prop(optional, into)] style: MaybeProp<String>,
64    children: Children,
65) -> impl IntoView {
66    view! {
67        <div
68            class=move || {
69                let extra = class.get().unwrap_or_default();
70                if extra.is_empty() {
71                    "orbital-layout__body".to_string()
72                } else {
73                    format!("orbital-layout__body {extra}")
74                }
75            }
76            style=move || style.get().unwrap_or_default()
77        >
78            {children()}
79        </div>
80    }
81}
82
83/// Headless side navigation region.
84#[component]
85pub fn BaseLayoutSidebar(
86    #[prop(optional, into)] class: MaybeProp<String>,
87    children: Children,
88) -> impl IntoView {
89    view! {
90        <aside class=move || {
91            let extra = class.get().unwrap_or_default();
92            if extra.is_empty() {
93                "orbital-layout__sidebar".to_string()
94            } else {
95                format!("orbital-layout__sidebar {extra}")
96            }
97        }>
98            {children()}
99        </aside>
100    }
101}
102
103/// Headless scrollable main content region.
104#[component]
105pub fn BaseLayoutMain(
106    #[prop(optional, into)] class: MaybeProp<String>,
107    children: Children,
108) -> impl IntoView {
109    view! {
110        <main class=move || {
111            let extra = class.get().unwrap_or_default();
112            if extra.is_empty() {
113                "orbital-layout__main".to_string()
114            } else {
115                format!("orbital-layout__main {extra}")
116            }
117        }>
118            {children()}
119        </main>
120    }
121}
122
123/// Headless scrollable spacer matching overlay header height.
124#[component]
125pub fn BaseLayoutHeaderInset(
126    #[prop(optional, into)] class: MaybeProp<String>,
127    #[prop(default = 56)] height_px: u16,
128) -> impl IntoView {
129    view! {
130        <div
131            class=move || {
132                let extra = class.get().unwrap_or_default();
133                if extra.is_empty() {
134                    "orbital-layout-header-inset".to_string()
135                } else {
136                    format!("orbital-layout-header-inset {extra}")
137                }
138            }
139            role="presentation"
140            aria-hidden="true"
141            style=move || format!("height: {height_px}px; min-height: {height_px}px;")
142        ></div>
143    }
144}