orbital-core-components 0.1.1

Themed Orbital UI components for Leptos applications
Documentation
use leptos::prelude::*;
use orbital_base_components::{AppBarDensity, AppBarPosition, BaseAppBar};
use orbital_macros::component_doc;

use crate::{
    material::material_flat_outline_modifier, material::material_modifier_classes,
    material::MaterialOutlineEdge, Material,
};
use orbital_base_components::AppBarInset;

use super::slots::{default_app_bar_material, AppBarLeading, AppBarMaterial, AppBarTrailing};
use super::styles::app_bar_styles;

/// Application shell header composed from [`Material`] and region slots.
///
/// The top application header surfaces screen context, wayfinding, and actions tied to the current view. Content is host-composed via [`AppBarLeading`], default `children` (center), and [`AppBarTrailing`]. Configure surface treatment through the [`AppBarMaterial`] slot (`variant`, `elevation`, `corners`).
///
/// # When to use
///
/// - Application shell chrome above primary content - Screen titles, breadcrumbs, and workspace branding - Inline search, filters, or section navigation in the center band - Account, theme, and utility controls in the trailing region
///
/// # Usage
///
/// 1. Configure surface via [`AppBarMaterial`] slot (`variant`, `elevation`, `corners`). 2. Place branding and wayfinding in [`AppBarLeading`]. 3. Put search, filters, or inline nav in default `children` (center region). 4. Place account and utility controls in [`AppBarTrailing`]. 5. Adjust `position` and `density` for scroll behavior and vertical budget. 6. Pair Sticky bars with [`Layout`](crate::Layout) `overlay_header=true` for pinned frost shells; use `main_inset_scroll=true` with Fixed opaque bars for inset inner scroll.
///
/// # Best Practices
///
/// ## Do's
///
/// * Prefer `Flat` elevation for shell chrome — a bottom border separates the bar from content * Prefer Frost or Shell + `Flat` for pinned scroll-under shells * Prefer `Shell` + `Flat` for catalog and workspace shells when opaque chrome is needed * Put search fields and inline nav in default children (center) * Use `ButtonAppearance::Transparent` for icon utilities in trailing * Wrap E2E hooks in a native `div` with `data-testid`
///
/// ## Don'ts
///
/// * Do not use `Resting` or `Raised` on shell AppBar — reserve elevated tiers for in-content surfaces * Do not duplicate Material shadow CSS on AppBar — elevation belongs on [`Material`] * Do not bake Button variants into AppBar — compose controls in slots * Do not use icon-only controls without an accessible name on a wrapper
///
/// # Examples
///
/// ## Standard shell
/// Workspace title in leading and a sign-in action in trailing—the default application header.
/// <!-- default -->
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarTrailing, Button, ButtonAppearance, Title3};
/// view! {
///     <div data-testid="app-bar-preview">
///         <AppBar>
///             <AppBarLeading slot>
///                 <Title3>"Orbital Components"</Title3>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Primary>"Sign in"</Button>
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Navigation affordance
/// Menu control beside the workspace title; sign-out action in trailing.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarTrailing, Button, ButtonAppearance, Title3};
/// view! {
///     <div data-testid="app-bar-with-menu">
///         <AppBar>
///             <AppBarLeading slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiMenuOutlined />
///                 <Title3>"Workspace"</Title3>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Secondary>"Sign out"</Button>
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Inline wayfinding
/// Product mark in leading, section links in center, account control in trailing.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarTrailing, Button, ButtonAppearance, Flex, FlexAlign, FlexGap, Title3};
/// view! {
///     <div data-testid="app-bar-with-nav">
///         <AppBar>
///             <AppBarLeading slot>
///                 <Title3>"Orbital"</Title3>
///             </AppBarLeading>
///             <Flex gap=FlexGap::Large align=FlexAlign::Center>
///                 <span>"Components"</span>
///                 <span>"Patterns"</span>
///                 <span>"Tokens"</span>
///             </Flex>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiUserOutlined />
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Centered search
/// Title in leading, scoped search field in center, actions in trailing.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarTrailing, Button, ButtonAppearance, SearchBox, SearchBoxAppearance, Title3};
/// let search = RwSignal::new(String::new());
/// view! {
///     <div data-testid="app-bar-with-search">
///         <AppBar>
///             <AppBarLeading slot>
///                 <Title3>"Catalog"</Title3>
///             </AppBarLeading>
///             <SearchBox bind=search appearance=SearchBoxAppearance::with_placeholder("Search components…") />
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiSettingOutlined />
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Search-first
/// Minimal leading label; the search field owns the center band.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarTrailing, Button, ButtonAppearance, SearchBox, SearchBoxAppearance};
/// let search = RwSignal::new(String::new());
/// view! {
///     <div data-testid="app-bar-search-first">
///         <AppBar>
///             <AppBarLeading slot>
///                 <span>"Find"</span>
///             </AppBarLeading>
///             <SearchBox bind=search appearance=SearchBoxAppearance::with_placeholder("Search workspace…") />
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Primary>"Go"</Button>
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Compact density
/// 48px compact bar for secondary tool rows.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarDensity, AppBarLeading, AppBarTrailing, Button, ButtonAppearance};
/// view! {
///     <div data-testid="app-bar-compact">
///         <AppBar density=AppBarDensity::Compact>
///             <AppBarLeading slot>
///                 <span>"Tools"</span>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiFilterOutlined />
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Expanded density
/// 96px expanded band for hero title or branding in leading.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarDensity, AppBarLeading};
/// view! {
///     <div data-testid="app-bar-expanded">
///         <AppBar density=AppBarDensity::Expanded>
///             <AppBarLeading slot>
///                 <span style="font-size: 28px; font-weight: 600;">"Design System"</span>
///             </AppBarLeading>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Shell surface
/// Shell material with flat elevation—catalog-style frosted shell chrome.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarMaterial, AppBarTrailing, Button, ButtonAppearance, MaterialCorners, MaterialElevation, MaterialVariant, Title3};
/// view! {
///     <div data-testid="app-bar-shell-flat">
///         <AppBar>
///             <AppBarMaterial variant=MaterialVariant::Shell 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>
///     </div>
/// }
/// ```
///
/// ## Raised over content
/// Raised elevation separates the header from scrolling body content below.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarMaterial, AppBarTrailing, Button, ButtonAppearance, MaterialElevation, MaterialVariant, Title3};
/// view! {
///     <div data-testid="app-bar-raised">
///         <AppBar>
///             <AppBarMaterial variant=MaterialVariant::Solid elevation=MaterialElevation::Raised slot />
///             <AppBarLeading slot>
///                 <Title3>"Dashboard"</Title3>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiBellOutlined />
///             </AppBarTrailing>
///         </AppBar>
///     </div>
/// }
/// ```
///
/// ## Sticky while scrolling
/// Bar pins to the top of its scroll container while content moves beneath.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarPosition, AppBarTrailing, Button, ButtonAppearance, Title3};
/// view! {
///     <div data-testid="app-bar-sticky" style="height: 200px; overflow: auto; border: 1px solid var(--orb-color-border-subtle);">
///         <AppBar position=AppBarPosition::Sticky>
///             <AppBarLeading slot>
///                 <Title3>"Sticky header"</Title3>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiMoreOutlined />
///             </AppBarTrailing>
///         </AppBar>
///         <div style="height: 400px; padding: 16px;">"Scroll to see the bar stay pinned."</div>
///     </div>
/// }
/// ```
///
/// ## Fixed with scroll body
/// Fixed bar with scrollable content below—the host must offset body content.
/// <!-- preview -->
/// ```rust
/// use crate::{AppBar, AppBarLeading, AppBarPosition, AppBarTrailing, Button, ButtonAppearance, Title3};
/// view! {
///     <div data-testid="app-bar-fixed" style="position: relative; height: 200px; overflow: hidden; border: 1px solid var(--orb-color-border-subtle);">
///         <AppBar position=AppBarPosition::Fixed>
///             <AppBarLeading slot>
///                 <Title3>"Fixed header"</Title3>
///             </AppBarLeading>
///             <AppBarTrailing slot>
///                 <Button appearance=ButtonAppearance::Transparent icon=icondata::AiCloseOutlined />
///             </AppBarTrailing>
///         </AppBar>
///         <div style="padding-top: 56px; height: 100%; overflow: auto; box-sizing: border-box;">
///             <div style="height: 400px; padding: 16px;">"Content scrolls beneath the fixed bar."</div>
///         </div>
///     </div>
/// }
/// ```
#[component_doc(
    category = "Shell",
    preview_slug = "app-bar",
    preview_label = "App Bar",
    preview_icon = icondata::AiLayoutOutlined,
)]
#[component]
pub fn AppBar(
    /// Extra CSS class names merged onto the app bar root.
    #[prop(optional, into)]
    class: MaybeProp<String>,
    /// Positioning mode — `Fixed` and `Sticky` provide [`AppBarInset`] context for [`Layout`].
    #[prop(default = AppBarPosition::Static)]
    position: AppBarPosition,
    /// Bar height and row spacing — `Standard` (56px) or `Compact` (48px).
    #[prop(default = AppBarDensity::Standard)]
    density: AppBarDensity,
    /// Optional [`Material`] treatment override for the bar surface.
    #[prop(optional)]
    app_bar_material: Option<AppBarMaterial>,
    /// Leading slot — menu button, back affordance, or title cluster on the start edge.
    #[prop(optional)]
    app_bar_leading: Option<AppBarLeading>,
    /// Trailing slot — search, actions, or account controls on the end edge.
    #[prop(optional)]
    app_bar_trailing: Option<AppBarTrailing>,
    /// Center title or custom bar content between leading and trailing slots.
    #[prop(optional)]
    children: Option<Children>,
) -> impl IntoView {
    let (variant, elevation, corners) = app_bar_material
        .map(|m| (m.variant, m.elevation, m.corners))
        .unwrap_or_else(default_app_bar_material);

    if matches!(position, AppBarPosition::Fixed | AppBarPosition::Sticky) {
        provide_context(AppBarInset {
            height_px: density.height_px(),
        });
    }

    let style_sheet = app_bar_styles();
    let material_modifiers = material_modifier_classes(variant, elevation, corners);
    let outline = material_flat_outline_modifier(variant, elevation, MaterialOutlineEdge::Bottom)
        .map(|class| format!(" {class}"))
        .unwrap_or_default();
    let material_class =
        format!("orbital-app-bar__material orbital-material {material_modifiers}{outline}");

    view! {
        <style>{style_sheet}</style>
        <BaseAppBar
            class=class
            position=position
            density=density
        >
            <Material class=material_class variant=variant elevation=elevation corners=corners>
                <div class="orbital-app-bar__row">
                    {app_bar_leading.map(|slot| {
                        view! {
                            <div class="orbital-app-bar__leading">
                                {(slot.children)()}
                            </div>
                        }
                    })}
                    <div class="orbital-app-bar__center">
                        {children.map(|c| c())}
                    </div>
                    {app_bar_trailing.map(|slot| {
                        view! {
                            <div class="orbital-app-bar__trailing">
                                {(slot.children)()}
                            </div>
                        }
                    })}
                </div>
            </Material>
        </BaseAppBar>
    }
}