app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit a9b2462795521e0f4151fe9926b8539afbfd3e3d
parent 23a018160e76e3d88adb0c5c01a8d491506fe4ce
Author: triesap <tyson@radroots.org>
Date:   Fri,  6 Feb 2026 13:56:09 +0000

ui: add nav tabs component

- add nav tabs component with auto-hide behavior
- wire tabs to scroll context velocity thresholds
- add ios-style tabs tray styles
- import nav tabs styles into app css

Diffstat:
Mapp/app.css | 1+
Acrates/ui-components/assets/nav_tabs.css | 38++++++++++++++++++++++++++++++++++++++
Mcrates/ui-components/src/lib.rs | 2++
Acrates/ui-components/src/nav_tabs.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 96 insertions(+), 0 deletions(-)

diff --git a/app/app.css b/app/app.css @@ -11,6 +11,7 @@ @import "./stylesheets/styles-superellipse.css"; @import "../crates/ui-components/assets/list.css"; @import "../crates/ui-components/assets/nav.css"; +@import "../crates/ui-components/assets/nav_tabs.css"; @custom-variant h-compact "@media (max-height: 540px)"; @custom-variant se-compact "@media (max-width: 375px) and (max-height: 540px)"; diff --git a/crates/ui-components/assets/nav_tabs.css b/crates/ui-components/assets/nav_tabs.css @@ -0,0 +1,38 @@ +.nav-tabs { + position: fixed; + bottom: 0; + left: 0; + z-index: 20; + width: 100%; + height: var(--nav-tabs-height); + padding: 8px 16px calc(var(--safe-b) + 8px); + display: flex; + align-items: flex-start; + justify-content: center; + transition: transform var(--dur-2) var(--ease-ios), opacity var(--dur-2) var(--ease-ios); + will-change: transform, opacity; +} + +.nav-tabs__tray { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 20px; + padding: 8px 20px; + border-radius: 999px; + background: var(--material-regular); + backdrop-filter: blur(18px) saturate(180%); + box-shadow: var(--shadow-1); +} + +.nav-tabs[data-hidden="true"] { + transform: translateY(calc(100% + 12px)); + opacity: 0; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + .nav-tabs { + transition: none; + } +} diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs @@ -12,6 +12,7 @@ mod dialog; mod sheet; mod scroll; mod nav_header; +mod nav_tabs; pub use button::RadrootsAppUiButton; pub use button_layout::{ @@ -125,3 +126,4 @@ pub use nav_header::{ RadrootsAppUiNavHeaderBgMode, RadrootsAppUiNavHeaderCollapseMode, }; +pub use nav_tabs::RadrootsAppUiNavTabs; diff --git a/crates/ui-components/src/nav_tabs.rs b/crates/ui-components/src/nav_tabs.rs @@ -0,0 +1,55 @@ +#![forbid(unsafe_code)] + +use leptos::prelude::*; + +const NAV_TABS_HIDE_VELOCITY: f64 = 220.0; +const NAV_TABS_SHOW_VELOCITY: f64 = 120.0; +const NAV_TABS_HIDE_SCROLL: f64 = 40.0; + +#[component] +pub fn RadrootsAppUiNavTabs( + #[prop(optional)] id: Option<String>, + #[prop(optional)] class: Option<String>, + #[prop(optional)] auto_hide: Option<bool>, + children: Children, +) -> impl IntoView { + let auto_hide = auto_hide.unwrap_or(true); + let scroll_context = use_context::<crate::RadrootsAppUiScrollContext>(); + let hidden = RwSignal::new(false); + Effect::new(move || { + if !auto_hide { + hidden.set(false); + return; + } + let Some(context) = scroll_context.as_ref() else { + hidden.set(false); + return; + }; + let scroll_top = context.scroll_top.get(); + if scroll_top <= NAV_TABS_HIDE_SCROLL { + hidden.set(false); + return; + } + let velocity = context.scroll_velocity.get(); + if velocity > NAV_TABS_HIDE_VELOCITY { + hidden.set(true); + } else if velocity < -NAV_TABS_SHOW_VELOCITY { + hidden.set(false); + } + }); + let class_value = match class { + Some(value) => format!("nav-tabs {value}"), + None => "nav-tabs".to_string(), + }; + view! { + <nav + id=id + class=class_value + attr:data-hidden=move || if hidden.get() { "true" } else { "false" } + > + <div class="nav-tabs__tray"> + {children()} + </div> + </nav> + } +}