use leptos::prelude::*;
use orbital_base_components::{
AppBarDensity, AppBarInset, BaseLayout, BaseLayoutBody, LayoutPosition,
};
use orbital_macros::component_doc;
use super::context::LayoutSidebarOpen;
use super::main::LayoutMainShell;
use super::overlay::LayoutOverlayScroll;
use super::sidebar::LayoutSidebarShell;
use super::slots::{LayoutHeader, LayoutMain, LayoutSidebar};
use super::styles::layout_styles;
use crate::ScrollArea;
/// Application shell with optional overlay header and side navigation.
///
/// Compose with [`LayoutHeader`], [`LayoutSidebar`], and [`LayoutMain`] slots. Pair `overlay_header=true` with a Fixed or Sticky [`AppBar`](crate::AppBar) in the header slot.
///
/// # When to use
///
/// - Full-page application shells with header, optional side nav, and scrollable main content - Pinned sticky frost headers where page content scrolls beneath the bar - Opaque fixed headers where content and scrollbar start below the bar (inner scroll) - Inline header layouts without overlay chrome
///
/// # Usage
///
/// 1. Set `overlay_header=true` when the header uses Sticky or Fixed [`AppBar`]. 2. Use Sticky [`AppBar`] with frost material for the default pinned window-scroll shell. 3. Set `main_inset_scroll=true` for opaque fixed bars with inner main scroll below the bar. 4. Match `header_inset` density to the [`AppBar`] density tier. 5. Place navigation in [`LayoutSidebar`] and page content in [`LayoutMain`]. 6. [`LayoutSidebar`] stays pinned below the bar; only the page scrolls in pinned mode.
///
/// # Scroll modes
///
/// | Mode | Props | When |
/// |------|-------|------|
/// | Inline header | default | Header scrolls with page content |
/// | Overlay pinned | `overlay_header=true`, sticky frost [`AppBar`] | Content scrolls beneath the bar |
/// | Overlay inset | `overlay_header=true`, `main_inset_scroll=true`, opaque fixed [`AppBar`] | Main scrolls below the bar inside the shell |
///
/// # Best Practices
///
/// ## Do's
///
/// * Use [`MaterialElevation::Flat`] on shell chrome — borders separate regions, not shadows * Flat shell chrome in overlay layouts shares the layout canvas surface (`Background3`), not card fills * Use Sticky [`AppBar`] with Frost or Shell [`AppBarMaterial`] for pinned shells * Pair [`Navigation`] with `Solid` + `Flat` in [`LayoutSidebar`] for co-planar side nav * Use `main_inset_scroll=true` with Solid opaque fixed bars in bounded containers * Keep `header_inset` in sync with [`AppBar`] density * Put long scrolling content in [`LayoutMain`], not the layout root * Reserve `Resting` / `Raised` elevation for in-content cards and callouts, not shell regions
///
/// ## Don'ts
///
/// * Do not use Fixed AppBar with pinned overlay — use Sticky so the header row stays in document flow * Do not add manual top padding in main when the header sits above the body row * Do not use elevated Material tiers on AppBar or Navigation — use Flat with auto border separators
///
/// # Examples
///
/// ## Inline header
/// Header and main in normal document flow—the default stacked shell.
/// <!-- default -->
/// <!-- preview -->
/// ```rust
/// use crate::{DemoBox, Layout, LayoutHeader, LayoutMain, Title3};
/// view! {
/// <div data-testid="layout-preview" style="height: 200px; border: 1px solid var(--orb-color-border-subtle);">
/// <Layout>
/// <LayoutHeader slot>
/// <DemoBox data_testid="layout-header-demo"><Title3>"Workspace"</Title3></DemoBox>
/// </LayoutHeader>
/// <LayoutMain slot>
/// <DemoBox fill=true data_testid="layout-main-demo">"Main content"</DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## With sidebar
/// Side navigation beside the primary content column.
/// <!-- preview -->
/// ```rust
/// use crate::{DemoBox, Layout, LayoutMain, LayoutSidebar};
/// view! {
/// <div data-testid="layout-with-sidebar" style="height: 200px; border: 1px solid var(--orb-color-border-subtle);">
/// <Layout>
/// <LayoutSidebar slot>
/// <DemoBox fill=true data_testid="layout-sidebar-demo">"Nav"</DemoBox>
/// </LayoutSidebar>
/// <LayoutMain slot>
/// <DemoBox fill=true data_testid="layout-main-demo">"Main column"</DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## Overlay pinned header
/// Sticky frost header—content starts below the bar and scrolls beneath it via window scroll.
/// <!-- preview -->
/// ```rust
/// use crate::{
/// AppBar, AppBarLeading, AppBarMaterial, AppBarPosition, DemoBox, Layout, LayoutHeader, LayoutMain,
/// MaterialCorners, MaterialElevation, MaterialVariant, Title3,
/// };
/// view! {
/// <div data-testid="layout-overlay-header" style="height: 400px; border: 1px solid var(--orb-color-border-subtle); overflow: auto;">
/// <Layout overlay_header=true page_scrollport=false>
/// <LayoutHeader slot>
/// <AppBar position=AppBarPosition::Sticky>
/// <AppBarMaterial variant=MaterialVariant::Frost elevation=MaterialElevation::Flat corners=MaterialCorners::Square slot />
/// <AppBarLeading slot><Title3>"Overlay shell"</Title3></AppBarLeading>
/// </AppBar>
/// </LayoutHeader>
/// <LayoutMain slot>
/// <DemoBox height="1200px" data_testid="layout-scroll-content">
/// <p>"First line starts below the bar."</p>
/// <p>"Scroll to see content pass under the frosted header."</p>
/// </DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## Overlay inset header
/// Opaque fixed header—content and scrollbar start below the bar with no scroll-under.
/// <!-- preview -->
/// ```rust
/// use crate::{
/// AppBar, AppBarLeading, AppBarMaterial, AppBarPosition, DemoBox, Layout, LayoutHeader, LayoutMain,
/// MaterialCorners, MaterialElevation, MaterialVariant, Title3,
/// };
/// view! {
/// <div data-testid="layout-inset-header" style="height: 240px; border: 1px solid var(--orb-color-border-subtle); overflow: hidden;">
/// <Layout overlay_header=true main_inset_scroll=true>
/// <LayoutHeader slot>
/// <AppBar position=AppBarPosition::Fixed>
/// <AppBarMaterial variant=MaterialVariant::Solid elevation=MaterialElevation::Raised corners=MaterialCorners::Square slot />
/// <AppBarLeading slot><Title3>"Inset shell"</Title3></AppBarLeading>
/// </AppBar>
/// </LayoutHeader>
/// <LayoutMain slot>
/// <DemoBox height="400px" data_testid="layout-inset-content">
/// <p>"First line starts below the bar."</p>
/// <p>"Content stays below the opaque header when scrolling."</p>
/// </DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## App shell
/// Header, sidebar, and main—the catalog-style compound shell.
/// <!-- preview -->
/// ```rust
/// use crate::{
/// AppBar, AppBarLeading, AppBarMaterial, AppBarPosition, AppBarTrailing, Button,
/// ButtonAppearance, DemoBox, Layout, LayoutHeader, LayoutMain, LayoutSidebar, MaterialCorners,
/// MaterialElevation, MaterialVariant, Navigation, NavigationBody, NavigationConfig, NavigationItem,
/// Title3,
/// };
/// let selected = RwSignal::new(None::<String>);
/// let open = RwSignal::new(vec![] as Vec<String>);
/// view! {
/// <div data-testid="layout-app-shell" style="height: 260px; border: 1px solid var(--orb-color-border-subtle); overflow: auto;">
/// <Layout overlay_header=true page_scrollport=false>
/// <LayoutHeader slot>
/// <AppBar position=AppBarPosition::Sticky>
/// <AppBarMaterial variant=MaterialVariant::Frost elevation=MaterialElevation::Flat corners=MaterialCorners::Square slot />
/// <AppBarLeading slot><Title3>"Orbital Components"</Title3></AppBarLeading>
/// <AppBarTrailing slot>
/// <Button appearance=ButtonAppearance::Transparent icon=icondata::AiBulbOutlined />
/// </AppBarTrailing>
/// </AppBar>
/// </LayoutHeader>
/// <LayoutSidebar slot>
/// <Navigation config=NavigationConfig::new().with_selected_value(selected).with_open_categories(open)>
/// <NavigationBody slot>
/// <NavigationItem config="card" icon=icondata::AiAppstoreOutlined>"Card"</NavigationItem>
/// </NavigationBody>
/// </Navigation>
/// </LayoutSidebar>
/// <LayoutMain slot>
/// <DemoBox fill=true data_testid="layout-main-demo">"Preview outlet"</DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## Sidebar toggle
/// Coordinated sidebar open state via [`LayoutSidebarToggle`].
/// <!-- preview -->
/// ```rust
/// use crate::{
/// AppBar, AppBarLeading, AppBarMaterial, AppBarPosition, DemoBox, Layout, LayoutHeader, LayoutMain,
/// LayoutSidebar, LayoutSidebarToggle, MaterialCorners, MaterialElevation, MaterialVariant,
/// Navigation, NavigationBody, NavigationConfig, NavigationItem, Title3,
/// };
/// let sidebar_open = RwSignal::new(true);
/// let selected = RwSignal::new(None::<String>);
/// let open = RwSignal::new(vec![] as Vec<String>);
/// view! {
/// <div data-testid="layout-sidebar-toggle" style="height: 240px; border: 1px solid var(--orb-color-border-subtle); overflow: hidden;">
/// <Layout overlay_header=true sidebar_open=sidebar_open>
/// <LayoutHeader slot>
/// <AppBar position=AppBarPosition::Sticky>
/// <AppBarMaterial variant=MaterialVariant::Solid elevation=MaterialElevation::Flat corners=MaterialCorners::Square slot />
/// <AppBarLeading slot>
/// <LayoutSidebarToggle />
/// <Title3>"Shell"</Title3>
/// </AppBarLeading>
/// </AppBar>
/// </LayoutHeader>
/// <LayoutSidebar slot>
/// <Navigation config=NavigationConfig::new().with_open(Signal::derive(move || sidebar_open.get())).with_selected_value(selected).with_open_categories(open)>
/// <NavigationBody slot>
/// <NavigationItem config="home" icon=icondata::AiHomeOutlined>"Home"</NavigationItem>
/// </NavigationBody>
/// </Navigation>
/// </LayoutSidebar>
/// <LayoutMain slot>
/// <DemoBox fill=true data_testid="layout-main-demo">"Main content"</DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
///
/// ## Sidebar closed
/// Initial closed rail with expand affordance in the header.
/// <!-- preview -->
/// ```rust
/// use crate::{
/// AppBar, AppBarLeading, AppBarMaterial, AppBarPosition, DemoBox, Layout, LayoutHeader, LayoutMain,
/// LayoutSidebar, LayoutSidebarToggle, MaterialCorners, MaterialElevation, MaterialVariant,
/// Navigation, NavigationBody, NavigationConfig, NavigationItem, Title3,
/// };
/// let sidebar_open = RwSignal::new(false);
/// let selected = RwSignal::new(None::<String>);
/// let open = RwSignal::new(vec![] as Vec<String>);
/// view! {
/// <div data-testid="layout-sidebar-closed" style="height: 240px; border: 1px solid var(--orb-color-border-subtle); overflow: hidden;">
/// <Layout overlay_header=true sidebar_open=sidebar_open>
/// <LayoutHeader slot>
/// <AppBar position=AppBarPosition::Sticky>
/// <AppBarMaterial variant=MaterialVariant::Solid elevation=MaterialElevation::Flat corners=MaterialCorners::Square slot />
/// <AppBarLeading slot>
/// <LayoutSidebarToggle />
/// <Title3>"Collapsed rail"</Title3>
/// </AppBarLeading>
/// </AppBar>
/// </LayoutHeader>
/// <LayoutSidebar slot>
/// <Navigation config=NavigationConfig::new().with_open(Signal::derive(move || sidebar_open.get())).with_selected_value(selected).with_open_categories(open)>
/// <NavigationBody slot>
/// <NavigationItem config="home" icon=icondata::AiHomeOutlined>"Home"</NavigationItem>
/// </NavigationBody>
/// </Navigation>
/// </LayoutSidebar>
/// <LayoutMain slot>
/// <DemoBox fill=true data_testid="layout-main-demo">"Main content"</DemoBox>
/// </LayoutMain>
/// </Layout>
/// </div>
/// }
/// ```
#[component_doc(
category = "Shell",
preview_slug = "layout",
preview_label = "Layout",
preview_icon = icondata::AiLayoutOutlined,
)]
#[component]
pub fn Layout(
/// Extra CSS class names merged onto the shell root grid.
#[prop(optional, into)]
class: MaybeProp<String>,
/// Optional `data-testid` for E2E hooks on the layout root.
#[prop(optional, into)]
data_testid: MaybeProp<String>,
/// When true, the header overlays main content and provides [`AppBarInset`] context.
#[prop(default = false)]
overlay_header: bool,
/// When true with `overlay_header`, main content scrolls inside an inset below the header.
#[prop(default = false)]
main_inset_scroll: bool,
/// When true (default), pinned overlay shells scroll via a full-viewport [`ScrollArea`] so Chrome applies themed scrollbar chrome. Set false for bounded preview hosts.
#[prop(default = true)]
page_scrollport: bool,
/// Header height token used for overlay inset padding — match the [`AppBar`] density.
#[prop(default = AppBarDensity::Standard)]
header_inset: AppBarDensity,
/// Parent-owned sidebar open state; defaults to an internal open signal when omitted.
#[prop(optional, into)]
sidebar_open: Option<RwSignal<bool>>,
/// Header slot — typically an [`AppBar`] with leading and trailing regions.
#[prop(optional)]
layout_header: Option<LayoutHeader>,
/// Sidebar slot — typically a [`Navigation`] rail beside main content.
#[prop(optional)]
layout_sidebar: Option<LayoutSidebar>,
/// Main content slot — page body rendered in the scrollable region.
#[prop(optional)]
layout_main: Option<LayoutMain>,
) -> impl IntoView {
if overlay_header {
provide_context(AppBarInset {
height_px: header_inset.height_px(),
});
provide_context(LayoutOverlayScroll { main_inset_scroll });
}
let sidebar_open = sidebar_open.unwrap_or_else(|| RwSignal::new(true));
LayoutSidebarOpen::provide(sidebar_open);
let sidebar_open_signal = Signal::derive(move || sidebar_open.get());
let has_sidebar = layout_sidebar.is_some();
let inset_px = header_inset.height_px();
let root_style = Signal::derive(move || {
if overlay_header {
format!("--orbital-layout-header-inset: {inset_px}px;")
} else {
String::new()
}
});
let root_class = Signal::derive(move || {
let extra = class.get().unwrap_or_default();
let mut parts = Vec::new();
if has_sidebar {
parts.push("orbital-layout--has-sidebar".to_string());
if !sidebar_open_signal.get() {
parts.push("orbital-layout--sidebar-closed".to_string());
}
}
if overlay_header && main_inset_scroll {
parts.push("orbital-layout--inset-header".to_string());
}
if !extra.is_empty() {
parts.push(extra);
}
parts.join(" ")
});
let style_sheet = layout_styles();
let use_page_scrollport = overlay_header && !main_inset_scroll && page_scrollport;
let page_scroll_style = format!(
"display: block; width: 100%; height: 100%; box-sizing: border-box; \
--orbital-layout-header-inset: {inset_px}px; scroll-padding-top: {inset_px}px;"
);
let shell = view! {
<BaseLayout
class=root_class
style=root_style
data_testid=data_testid
overlay_header=overlay_header
position=LayoutPosition::Static
>
{layout_header.map(|slot| (slot.children)())}
<BaseLayoutBody>
{layout_sidebar.map(|slot| view! { <LayoutSidebarShell>{(slot.children)()}</LayoutSidebarShell> })}
{layout_main.map(|slot| view! { <LayoutMainShell>{(slot.children)()}</LayoutMainShell> })}
</BaseLayoutBody>
</BaseLayout>
};
view! {
<style>{style_sheet}</style>
{if use_page_scrollport {
view! {
<ScrollArea
class="orbital-layout__page-scroll"
style=page_scroll_style
>
{shell}
</ScrollArea>
}
.into_any()
} else {
shell.into_any()
}}
}
}