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:
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>
+ }
+}