commit da9067f22962ea9fed09c33249250ea77b65084e
parent a9b2462795521e0f4151fe9926b8539afbfd3e3d
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Feb 2026 14:34:03 +0000
ui: migrate pages to chrome layout
- wrap routes with app page chrome and nav tabs
- adjust app page padding and body layout for header + tabs
- extend nav tabs styling and icon keys for home/test
- refactor logs actions to avoid rc in children
Diffstat:
10 files changed, 384 insertions(+), 243 deletions(-)
diff --git a/app/app.css b/app/app.css
@@ -171,10 +171,17 @@
}
.app-page-chrome {
- padding-top: calc(var(--nav-header-height) + var(--safe-t));
padding-bottom: calc(var(--nav-tabs-height) + var(--safe-b));
}
+ .app-page-body {
+ padding: 0 16px 24px;
+ }
+
+ .app-page-shell {
+ display: contents;
+ }
+
.app-view {
will-change: opacity;
position: relative;
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -2,7 +2,7 @@ use leptos::ev::{KeyboardEvent, MouseEvent};
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos_router::components::{A, Route, Router, Routes};
-use leptos_router::hooks::use_navigate;
+use leptos_router::hooks::{use_location, use_navigate};
use leptos_router::path;
use web_sys::HtmlElement;
@@ -19,6 +19,12 @@ use radroots_app_ui_components::{
RadrootsAppUiButtonLayoutPair,
RadrootsAppUiIcon,
RadrootsAppUiIconKey,
+ RadrootsAppUiNavHeader,
+ RadrootsAppUiNavHeaderBgMode,
+ RadrootsAppUiNavHeaderCollapseMode,
+ RadrootsAppUiNavTabs,
+ RadrootsAppUiScrollContainer,
+ RadrootsAppUiScrollContext,
RadrootsAppUiSpinner,
};
use uuid::Uuid;
@@ -95,6 +101,84 @@ impl HomeView {
}
}
+#[component]
+pub(crate) fn AppPageChrome(
+ title: String,
+ #[prop(optional)] header_right: Option<ChildrenFn>,
+ #[prop(optional)] show_tabs: Option<bool>,
+ #[prop(optional)] bg_mode: Option<RadrootsAppUiNavHeaderBgMode>,
+ #[prop(optional)] collapse_mode: Option<RadrootsAppUiNavHeaderCollapseMode>,
+ children: Children,
+) -> impl IntoView {
+ let scroll_context = RadrootsAppUiScrollContext::new();
+ provide_context(scroll_context.clone());
+ let show_tabs = show_tabs.unwrap_or(true);
+ let bg_mode = bg_mode.unwrap_or(RadrootsAppUiNavHeaderBgMode::AutoBlur);
+ let collapse_mode = collapse_mode.unwrap_or(RadrootsAppUiNavHeaderCollapseMode::Scroll);
+ let location = use_location();
+ let is_home = move || location.pathname.get() == "/";
+ let is_test = move || location.pathname.get().starts_with("/test");
+ let is_settings = move || location.pathname.get().starts_with("/settings");
+ view! {
+ <div class="app-page-shell">
+ <RadrootsAppUiScrollContainer
+ id=None
+ classes=Some("app-page app-page-scroll app-page-chrome".to_string())
+ collapse_range=None
+ context=Some(scroll_context.clone())
+ >
+ <RadrootsAppUiNavHeader
+ label=title
+ on_label_click=None
+ bg_mode=Some(bg_mode)
+ collapse_mode=Some(collapse_mode)
+ right=header_right
+ id=None
+ class=None
+ />
+ <div class="app-page-body">
+ {children()}
+ </div>
+ </RadrootsAppUiScrollContainer>
+ {move || {
+ if show_tabs {
+ view! {
+ <RadrootsAppUiNavTabs>
+ <A
+ href="/"
+ attr:class="nav-tabs__item"
+ attr:data-active=move || if is_home() { "true" } else { "false" }
+ attr:aria-label=t!("app.nav.home")
+ >
+ <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Home size=22 />
+ </A>
+ <A
+ href="/test"
+ attr:class="nav-tabs__item"
+ attr:data-active=move || if is_test() { "true" } else { "false" }
+ attr:aria-label=t!("app.nav.ui")
+ >
+ <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Beaker size=22 />
+ </A>
+ <A
+ href="/settings"
+ attr:class="nav-tabs__item"
+ attr:data-active=move || if is_settings() { "true" } else { "false" }
+ attr:aria-label=t!("app.nav.settings")
+ >
+ <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Settings size=22 />
+ </A>
+ </RadrootsAppUiNavTabs>
+ }
+ .into_any()
+ } else {
+ view! { <></> }.into_any()
+ }
+ }}
+ </div>
+ }
+}
+
fn error_label(key: &str) -> Option<String> {
let label = match key {
"error.app.init.idb" => t!("error.app.init.idb"),
@@ -1435,43 +1519,48 @@ fn HomePage() -> impl IntoView {
let is_activity = move || matches!(current_view.get(), HomeView::Activity);
let is_profile = move || matches!(current_view.get(), HomeView::Profile);
view! {
- <main id="app-home" class="app-page app-page-fixed flex flex-col items-center justify-start gap-6 pt-10">
- <div
- id="app-home-toggle"
- class="home-toggle"
- class:home-toggle--left=is_activity
- class:home-toggle--right=is_profile
- role="tablist"
- aria-label="Home view"
+ <AppPageChrome title=t!("app.nav.home")>
+ <section
+ id="app-home"
+ class="flex flex-col items-center justify-start gap-6 pt-6"
>
- <div class="home-toggle__indicator" aria-hidden="true"></div>
- <button
- id="app-home-toggle-activity"
- class="home-toggle__button"
- class:is-active=is_activity
- type="button"
- role="tab"
- aria-selected=move || if is_activity() { "true" } else { "false" }
- on:click=move |_| current_view.set(HomeView::Activity)
- >
- {"Activity"}
- </button>
- <button
- id="app-home-toggle-profile"
- class="home-toggle__button"
- class:is-active=is_profile
- type="button"
- role="tab"
- aria-selected=move || if is_profile() { "true" } else { "false" }
- on:click=move |_| current_view.set(HomeView::Profile)
+ <div
+ id="app-home-toggle"
+ class="home-toggle"
+ class:home-toggle--left=is_activity
+ class:home-toggle--right=is_profile
+ role="tablist"
+ aria-label="Home view"
>
- {"Profile"}
- </button>
- </div>
- <p id="app-home-view-title" class="home-toggle__title">
- {move || current_view.get().label()}
- </p>
- </main>
+ <div class="home-toggle__indicator" aria-hidden="true"></div>
+ <button
+ id="app-home-toggle-activity"
+ class="home-toggle__button"
+ class:is-active=is_activity
+ type="button"
+ role="tab"
+ aria-selected=move || if is_activity() { "true" } else { "false" }
+ on:click=move |_| current_view.set(HomeView::Activity)
+ >
+ {"Activity"}
+ </button>
+ <button
+ id="app-home-toggle-profile"
+ class="home-toggle__button"
+ class:is-active=is_profile
+ type="button"
+ role="tab"
+ aria-selected=move || if is_profile() { "true" } else { "false" }
+ on:click=move |_| current_view.set(HomeView::Profile)
+ >
+ {"Profile"}
+ </button>
+ </div>
+ <p id="app-home-view-title" class="home-toggle__title">
+ {move || current_view.get().label()}
+ </p>
+ </section>
+ </AppPageChrome>
}
}
@@ -1618,18 +1707,6 @@ fn AppShell() -> impl IntoView {
if gate.show_app {
return view! {
<div id="app-shell">
- <div
- id="app-topbar"
- class="flex items-center justify-end px-4 pt-4"
- >
- <A
- href="/settings"
- attr:aria-label=t!("app.nav.settings")
- attr:class="inline-flex h-9 w-9 items-center justify-center rounded-full"
- >
- <RadrootsAppUiIcon key=RadrootsAppUiIconKey::Settings size=20 />
- </A>
- </div>
<Routes
fallback=|| view! {
<main id="app-not-found" class="app-page app-page-fixed">
diff --git a/app/src/logs.rs b/app/src/logs.rs
@@ -12,18 +12,20 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;
-use std::rc::Rc;
use radroots_app_core::datastore::RadrootsClientWebDatastore;
use radroots_app_core::idb::IDB_CONFIG_LOGS;
use crate::{
+ app::AppPageChrome,
app_context,
app_log_buffer_flush_no_prune,
app_log_entries_clear,
app_log_entries_dump,
app_log_entries_load,
app_log_metadata,
+ RadrootsAppContext,
+ RadrootsAppKeyMapConfig,
RadrootsAppLogEntry,
RadrootsAppLogLevel,
};
@@ -52,6 +54,79 @@ fn logs_datastore() -> RadrootsClientWebDatastore {
RadrootsClientWebDatastore::new(Some(IDB_CONFIG_LOGS))
}
+fn logs_resolve_backends(
+ context: &Option<RadrootsAppContext>,
+) -> Option<(RadrootsClientWebDatastore, RadrootsAppKeyMapConfig)> {
+ context.as_ref().and_then(|context| {
+ context.backends.with_untracked(|value| {
+ value.as_ref().map(|backends| {
+ (logs_datastore(), backends.config.datastore.key_maps.clone())
+ })
+ })
+ })
+}
+
+fn logs_refresh(
+ context: &Option<RadrootsAppContext>,
+ entries: RwSignal<Vec<RadrootsAppLogEntry>, LocalStorage>,
+ loading: RwSignal<bool, LocalStorage>,
+ dump_error: RwSignal<Option<String>, LocalStorage>,
+) {
+ let Some((datastore, key_maps)) = logs_resolve_backends(context) else {
+ entries.set(Vec::new());
+ dump_error.set(None);
+ return;
+ };
+ loading.set(true);
+ spawn_local(async move {
+ let _ = app_log_buffer_flush_no_prune(&datastore, &key_maps).await;
+ let result = app_log_entries_load(&datastore, &key_maps).await;
+ match result {
+ Ok(mut items) => {
+ items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
+ dump_error.set(None);
+ entries.set(items);
+ }
+ Err(err) => {
+ dump_error.set(Some(format!("error: {err}")));
+ entries.set(Vec::new());
+ }
+ }
+ loading.set(false);
+ });
+}
+
+fn logs_clear(
+ context: &Option<RadrootsAppContext>,
+ entries: RwSignal<Vec<RadrootsAppLogEntry>, LocalStorage>,
+ loading: RwSignal<bool, LocalStorage>,
+ dump_error: RwSignal<Option<String>, LocalStorage>,
+) {
+ let Some((datastore, key_maps)) = logs_resolve_backends(context) else {
+ entries.set(Vec::new());
+ dump_error.set(None);
+ return;
+ };
+ loading.set(true);
+ spawn_local(async move {
+ let _ = app_log_entries_clear(&datastore, &key_maps).await;
+ let _ = app_log_buffer_flush_no_prune(&datastore, &key_maps).await;
+ let result = app_log_entries_load(&datastore, &key_maps).await;
+ match result {
+ Ok(mut items) => {
+ items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
+ dump_error.set(None);
+ entries.set(items);
+ }
+ Err(err) => {
+ dump_error.set(Some(format!("error: {err}")));
+ entries.set(Vec::new());
+ }
+ }
+ loading.set(false);
+ });
+}
+
fn log_level_color(level: RadrootsAppLogLevel) -> &'static str {
match level {
RadrootsAppLogLevel::Debug => "#6b7280",
@@ -324,7 +399,7 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
let page_size = RwSignal::new_local(logs_page_size_default());
let page_index = RwSignal::new_local(0usize);
let dump_config = RwSignal::new_local(None::<RadrootsAppLogDumpConfig>);
- let context = Rc::new(app_context());
+ let context = app_context();
let filtered_entries = Memo::new(move |_| {
let level_filter = filter_level.get();
let query = filter_query.get();
@@ -394,72 +469,21 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
let header = log_dump_header_with_context(context);
log_dump_with_context(&items, header)
});
- let resolve_backends = {
- let context = Rc::clone(&context);
- Rc::new(move || {
- context.as_ref().as_ref().and_then(|context| {
- context
- .backends
- .with_untracked(|value| {
- value.as_ref().map(|backends| {
- (
- Rc::new(logs_datastore()),
- backends.config.datastore.key_maps.clone(),
- )
- })
- })
- })
- })
- };
- let refresh = {
- let resolve_backends = Rc::clone(&resolve_backends);
- Rc::new(move || {
- let Some((datastore, key_maps)) = resolve_backends() else {
- entries.set(Vec::new());
- dump_error.set(None);
- return;
- };
- loading.set(true);
- let entries_signal = entries;
- let loading_signal = loading;
- spawn_local(async move {
- let _ = app_log_buffer_flush_no_prune(datastore.as_ref(), &key_maps).await;
- let result = app_log_entries_load(datastore.as_ref(), &key_maps).await;
- match result {
- Ok(mut items) => {
- items.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
- dump_error.set(None);
- entries_signal.set(items);
- }
- Err(err) => {
- dump_error.set(Some(format!("error: {err}")));
- entries_signal.set(Vec::new());
- }
- }
- loading_signal.set(false);
- });
- })
+ let refresh_action = {
+ let context = context.clone();
+ move || {
+ logs_refresh(&context, entries, loading, dump_error);
+ }
};
- let clear = {
- let resolve_backends = Rc::clone(&resolve_backends);
- let refresh = Rc::clone(&refresh);
- Rc::new(move || {
- let Some((datastore, key_maps)) = resolve_backends() else {
- entries.set(Vec::new());
- dump_error.set(None);
- return;
- };
- loading.set(true);
- let refresh = Rc::clone(&refresh);
- spawn_local(async move {
- let _ = app_log_entries_clear(datastore.as_ref(), &key_maps).await;
- refresh();
- });
- })
+ let clear_action = {
+ let context = context.clone();
+ move || {
+ logs_clear(&context, entries, loading, dump_error);
+ }
};
let copy_dump = {
let dump_text = dump_text.clone();
- Rc::new(move || {
+ move || {
let text = dump_text.get();
if text.is_empty() {
dump_status.set(Some(String::from("dump_empty")));
@@ -474,11 +498,11 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
dump_status.set(Some(status));
dump_action_running.set(false);
});
- })
+ }
};
let download_dump = {
let dump_text = dump_text.clone();
- Rc::new(move || {
+ move || {
let text = dump_text.get();
if text.is_empty() {
dump_status.set(Some(String::from("dump_empty")));
@@ -493,25 +517,23 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
dump_status.set(Some(status));
dump_action_running.set(false);
});
- })
+ }
};
- let copy_support = {
- Rc::new(move || {
- let text = support_instructions_text();
- support_running.set(true);
- spawn_local(async move {
- let status = match log_dump_copy(text).await {
- Ok(()) => String::from("support_copy_ok"),
- Err(err) => err,
- };
- support_status.set(Some(status));
- support_running.set(false);
- });
- })
+ let copy_support = move || {
+ let text = support_instructions_text();
+ support_running.set(true);
+ spawn_local(async move {
+ let status = match log_dump_copy(text).await {
+ Ok(()) => String::from("support_copy_ok"),
+ Err(err) => err,
+ };
+ support_status.set(Some(status));
+ support_running.set(false);
+ });
};
let support_bundle = {
let dump_text = dump_text.clone();
- Rc::new(move || {
+ move || {
let text = dump_text.get();
if text.is_empty() {
support_status.set(Some(String::from("dump_empty")));
@@ -530,56 +552,64 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
support_status.set(Some(status));
support_running.set(false);
});
- })
- };
- let refresh_effect = Rc::clone(&refresh);
- let context_effect = Rc::clone(&context);
- Effect::new(move || {
- let Some(context) = context_effect.as_ref() else {
- return;
- };
- if did_load.get() {
- return;
}
- let has_backends = context.backends.with(|value| value.is_some());
- if !has_backends {
- return;
+ };
+ Effect::new({
+ let context = context.clone();
+ move || {
+ let Some(context_ref) = context.as_ref() else {
+ return;
+ };
+ if did_load.get() {
+ return;
+ }
+ let has_backends = context_ref.backends.with(|value| value.is_some());
+ if !has_backends {
+ return;
+ }
+ did_load.set(true);
+ logs_refresh(&context, entries, loading, dump_error);
}
- did_load.set(true);
- refresh_effect();
});
- let config_effect = Rc::clone(&context);
- Effect::new(move || {
- let Some(context) = config_effect.as_ref() else {
- return;
- };
- let config = context
- .backends
- .with(|value| value.as_ref().map(|backends| backends.config.clone()));
- let Some(config) = config else {
- return;
- };
- dump_config.set(Some(log_dump_config_from_app(&config)));
+ Effect::new({
+ let context = context.clone();
+ move || {
+ let Some(context) = context.as_ref() else {
+ return;
+ };
+ let config = context
+ .backends
+ .with(|value| value.as_ref().map(|backends| backends.config.clone()));
+ let Some(config) = config else {
+ return;
+ };
+ dump_config.set(Some(log_dump_config_from_app(&config)));
+ }
});
- let interval_effect = Rc::clone(&refresh);
- Effect::new(move || {
- if interval_started.get() {
- return;
+ Effect::new({
+ let context = context.clone();
+ move || {
+ if interval_started.get() {
+ return;
+ }
+ interval_started.set(true);
+ let context = context.clone();
+ let entries = entries;
+ let loading = loading;
+ let dump_error = dump_error;
+ let (abort_handle, abort_reg) = AbortHandle::new_pair();
+ let abort_handle_cleanup = abort_handle.clone();
+ spawn_local(async move {
+ let mut ticks = IntervalStream::new(logs_auto_refresh_ms());
+ let task = async move {
+ while ticks.next().await.is_some() {
+ logs_refresh(&context, entries, loading, dump_error);
+ }
+ };
+ let _ = Abortable::new(task, abort_reg).await;
+ });
+ on_cleanup(move || abort_handle_cleanup.abort());
}
- interval_started.set(true);
- let refresh = Rc::clone(&interval_effect);
- let (abort_handle, abort_reg) = AbortHandle::new_pair();
- let abort_handle_cleanup = abort_handle.clone();
- spawn_local(async move {
- let mut ticks = IntervalStream::new(logs_auto_refresh_ms());
- let task = async move {
- while ticks.next().await.is_some() {
- refresh();
- }
- };
- let _ = Abortable::new(task, abort_reg).await;
- });
- on_cleanup(move || abort_handle_cleanup.abort());
});
let status_label = move || {
if loading.get() {
@@ -610,11 +640,10 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
total == 0 || page_index.get() + 1 >= total
};
view! {
- <main id="app-logs" class="app-page app-page-scroll">
+ <AppPageChrome title=t!("app.logs.title")>
<header id="app-logs-header" style="display:flex;align-items:center;gap:12px;">
- <h1 id="app-logs-title" style="font-size:18px;font-weight:600;">{t!("app.logs.title")}</h1>
- <button on:click=move |_| refresh()>{t!("app.logs.action.refresh")}</button>
- <button on:click=move |_| clear()>{t!("app.logs.action.clear")}</button>
+ <button on:click=move |_| refresh_action()>{t!("app.logs.action.refresh")}</button>
+ <button on:click=move |_| clear_action()>{t!("app.logs.action.clear")}</button>
<button on:click=move |_| copy_dump() disabled=dump_action_disabled>{t!("app.logs.action.copy_dump")}</button>
<button on:click=move |_| download_dump() disabled=dump_action_disabled>{t!("app.logs.action.download_dump")}</button>
<div id="app-logs-status" style="font-size:12px;color:#6b7280;">{status_label}</div>
@@ -787,7 +816,7 @@ pub fn RadrootsAppLogsPage() -> impl IntoView {
></textarea>
</section>
</section>
- </main>
+ </AppPageChrome>
}
}
diff --git a/app/src/settings.rs b/app/src/settings.rs
@@ -5,6 +5,7 @@ use leptos::prelude::*;
use leptos_router::hooks::use_navigate;
use crate::{
+ app::AppPageChrome,
app_theme_apply_mode,
app_theme_mode_from_value,
app_theme_read_mode,
@@ -285,18 +286,12 @@ pub fn RadrootsAppSettingsPage() -> impl IntoView {
styles: None,
};
view! {
- <main id="app-settings" class="app-page app-page-scroll" style="padding: 16px;">
- <header
- id="app-settings-header"
- style="font-family: var(--font-sans); font-size: 34px; line-height: 41px; font-weight: 600; letter-spacing: -0.01em; margin-bottom: 12px;"
- >
- <h1 id="app-settings-title" class="capitalize">{t!("app.settings.title")}</h1>
- </header>
- <section id="app-settings-content" style="display:flex;flex-direction:column;gap:16px;">
+ <AppPageChrome title=t!("app.settings.title")>
+ <section id="app-settings-content" class="flex flex-col gap-4">
<RadrootsAppUiListView basis=appearance_list />
<RadrootsAppUiListView basis=actions_list />
<RadrootsAppUiListView basis=system_list />
</section>
- </main>
+ </AppPageChrome>
}
}
diff --git a/app/src/settings_status.rs b/app/src/settings_status.rs
@@ -5,6 +5,7 @@ use leptos::prelude::*;
use leptos::task::spawn_local;
use crate::{
+ app::AppPageChrome,
active_key_label,
app_context,
app_health_check_delay_ms,
@@ -148,44 +149,43 @@ pub fn RadrootsAppSettingsStatusPage() -> impl IntoView {
value.unwrap_or_else(|| t!("app.common.unknown"))
};
view! {
- <main id="app-settings-status" class="app-page app-page-scroll" style="padding: 16px;">
- <header id="app-settings-status-header" style="display:flex;align-items:center;gap:12px;">
- <h1 id="app-settings-status-title" style="font: var(--type-title2);">
- {t!("app.settings.status.title")}
- </h1>
- <button
- on:click=move |_| {
- let config = backends.with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
- let Some(config) = config else {
- return;
- };
- let setup_required_value =
- !matches!(setup_status.get(), RadrootsAppSetupStatus::Configured);
- spawn_health_checks(
- config,
- setup_required_value,
- health_report,
- health_running,
- active_key,
- notifications_status,
- last_run,
- );
- }
- disabled=health_disabled
- >
- {move || {
- if health_running.get() {
- t!("app.home.health.button.checking")
- } else {
- t!("app.home.health.button.run")
+ <AppPageChrome title=t!("app.settings.status.title")>
+ <header id="app-settings-status-header" class="flex flex-col gap-2">
+ <div class="flex flex-row items-center gap-4">
+ <button
+ on:click=move |_| {
+ let config = backends.with_untracked(|value| value.as_ref().map(|backends| backends.config.clone()));
+ let Some(config) = config else {
+ return;
+ };
+ let setup_required_value =
+ !matches!(setup_status.get(), RadrootsAppSetupStatus::Configured);
+ spawn_health_checks(
+ config,
+ setup_required_value,
+ health_report,
+ health_running,
+ active_key,
+ notifications_status,
+ last_run,
+ );
}
- }}
- </button>
- <div id="app-settings-status-updated" style="font-size:12px;color:var(--text-secondary);">
- {move || format!("{}: {}", t!("app.settings.status.updated"), last_updated_label())}
+ disabled=health_disabled
+ >
+ {move || {
+ if health_running.get() {
+ t!("app.home.health.button.checking")
+ } else {
+ t!("app.home.health.button.run")
+ }
+ }}
+ </button>
+ <div id="app-settings-status-updated" class="text-xs text-[var(--text-secondary)]">
+ {move || format!("{}: {}", t!("app.settings.status.updated"), last_updated_label())}
+ </div>
</div>
</header>
- <section id="app-settings-status-content" style="display:flex;flex-direction:column;gap:16px;margin-top:12px;">
+ <section id="app-settings-status-content" class="flex flex-col gap-4 mt-3">
{move || {
let report = health_report.get();
let active = active_key_label(active_key.get());
@@ -244,6 +244,6 @@ pub fn RadrootsAppSettingsStatusPage() -> impl IntoView {
view! { <RadrootsAppUiListView basis=list /> }.into_any()
}}
</section>
- </main>
+ </AppPageChrome>
}
}
diff --git a/app/src/ui_demo.rs b/app/src/ui_demo.rs
@@ -1,6 +1,6 @@
use leptos::prelude::*;
-use crate::t;
+use crate::{app::AppPageChrome, t};
use radroots_app_ui_components::{
RadrootsAppUiList,
RadrootsAppUiListDisplay,
@@ -182,10 +182,7 @@ pub fn RadrootsAppUiDemoPage() -> impl IntoView {
}),
};
view! {
- <main id="app-ui-demo" class="app-page app-page-scroll" style="padding: 16px;">
- <header id="app-ui-demo-header" style="font: var(--type-title2); margin-bottom: 12px;">
- <h1 id="app-ui-demo-title">{t!("app.ui_demo.title")}</h1>
- </header>
+ <AppPageChrome title=t!("app.ui_demo.title")>
<section id="app-ui-demo-content">
<RadrootsAppUiListView basis=list />
@@ -242,6 +239,6 @@ pub fn RadrootsAppUiDemoPage() -> impl IntoView {
</RadrootsAppUiSheetPortal>
</RadrootsAppUiSheetRoot>
</section>
- </main>
+ </AppPageChrome>
}
}
diff --git a/crates/ui-components/assets/nav_tabs.css b/crates/ui-components/assets/nav_tabs.css
@@ -25,6 +25,21 @@
box-shadow: var(--shadow-1);
}
+.nav-tabs__item {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ width: 40px;
+ border-radius: 999px;
+ color: var(--text-secondary);
+ transition: color var(--dur-1) var(--ease-ios);
+}
+
+.nav-tabs__item[data-active="true"] {
+ color: var(--text-primary);
+}
+
.nav-tabs[data-hidden="true"] {
transform: translateY(calc(100% + 12px));
opacity: 0;
diff --git a/crates/ui-components/src/icon.rs b/crates/ui-components/src/icon.rs
@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
-use icondata::{Icon, LuChevronRight, LuChevronsUpDown, LuPlus, LuSettings};
+use icondata::{Icon, LuBeaker, LuChevronRight, LuChevronsUpDown, LuHome, LuPlus, LuSettings};
use leptos::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -9,6 +9,8 @@ pub enum RadrootsAppUiIconKey {
CaretUpDown,
Plus,
Settings,
+ Home,
+ Beaker,
}
impl RadrootsAppUiIconKey {
@@ -18,6 +20,8 @@ impl RadrootsAppUiIconKey {
RadrootsAppUiIconKey::CaretUpDown => "caret-up-down",
RadrootsAppUiIconKey::Plus => "plus",
RadrootsAppUiIconKey::Settings => "settings",
+ RadrootsAppUiIconKey::Home => "home",
+ RadrootsAppUiIconKey::Beaker => "beaker",
}
}
}
@@ -28,6 +32,8 @@ pub fn radroots_app_ui_icon_key_from_name(name: &str) -> Option<RadrootsAppUiIco
"caret-up-down" | "chevrons-up-down" => Some(RadrootsAppUiIconKey::CaretUpDown),
"plus" => Some(RadrootsAppUiIconKey::Plus),
"settings" | "gear" => Some(RadrootsAppUiIconKey::Settings),
+ "home" => Some(RadrootsAppUiIconKey::Home),
+ "beaker" | "test" => Some(RadrootsAppUiIconKey::Beaker),
_ => None,
}
}
@@ -38,6 +44,8 @@ pub fn radroots_app_ui_icon_data(key: RadrootsAppUiIconKey) -> Icon {
RadrootsAppUiIconKey::CaretUpDown => LuChevronsUpDown,
RadrootsAppUiIconKey::Plus => LuPlus,
RadrootsAppUiIconKey::Settings => LuSettings,
+ RadrootsAppUiIconKey::Home => LuHome,
+ RadrootsAppUiIconKey::Beaker => LuBeaker,
}
}
@@ -116,6 +124,18 @@ mod tests {
radroots_app_ui_icon_key_from_name("gear"),
Some(RadrootsAppUiIconKey::Settings)
);
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("home"),
+ Some(RadrootsAppUiIconKey::Home)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("beaker"),
+ Some(RadrootsAppUiIconKey::Beaker)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("test"),
+ Some(RadrootsAppUiIconKey::Beaker)
+ );
assert_eq!(radroots_app_ui_icon_key_from_name("unknown"), None);
}
diff --git a/crates/ui-components/src/nav_header.rs b/crates/ui-components/src/nav_header.rs
@@ -40,12 +40,12 @@ pub enum RadrootsAppUiNavHeaderCollapseMode {
#[component]
pub fn RadrootsAppUiNavHeader(
label: String,
- #[prop(optional)] on_label_click: Option<Callback<MouseEvent>>,
- #[prop(optional)] bg_mode: Option<RadrootsAppUiNavHeaderBgMode>,
- #[prop(optional)] collapse_mode: Option<RadrootsAppUiNavHeaderCollapseMode>,
- #[prop(optional)] right: Option<ChildrenFn>,
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] class: Option<String>,
+ on_label_click: Option<Callback<MouseEvent>>,
+ bg_mode: Option<RadrootsAppUiNavHeaderBgMode>,
+ collapse_mode: Option<RadrootsAppUiNavHeaderCollapseMode>,
+ right: Option<ChildrenFn>,
+ id: Option<String>,
+ class: Option<String>,
) -> impl IntoView {
let bg_mode = bg_mode.unwrap_or(RadrootsAppUiNavHeaderBgMode::AutoBlur);
let collapse_mode = collapse_mode.unwrap_or(RadrootsAppUiNavHeaderCollapseMode::Scroll);
diff --git a/crates/ui-components/src/scroll.rs b/crates/ui-components/src/scroll.rs
@@ -54,17 +54,18 @@ pub fn radroots_app_ui_scroll_velocity(prev_top: f64, next_top: f64, dt_ms: f64)
#[component]
pub fn RadrootsAppUiScrollContainer(
- #[prop(optional)] id: Option<String>,
- #[prop(optional)] class: Option<String>,
- #[prop(optional)] collapse_range: Option<f64>,
- #[prop(optional)] context: Option<RadrootsAppUiScrollContext>,
+ id: Option<String>,
+ classes: Option<String>,
+ collapse_range: Option<f64>,
+ context: Option<RadrootsAppUiScrollContext>,
children: Children,
) -> impl IntoView {
let context = context.unwrap_or_else(RadrootsAppUiScrollContext::new);
provide_context(context.clone());
let last_sample = RwSignal::new_local(RadrootsAppUiScrollSample::default());
let collapse_range_value = collapse_range.unwrap_or(DEFAULT_COLLAPSE_RANGE);
- let class_value = class.unwrap_or_else(|| "app-page app-page-scroll app-page-chrome".to_string());
+ let class_value =
+ classes.unwrap_or_else(|| "app-page app-page-scroll app-page-chrome".to_string());
let on_scroll = move |ev: Event| {
let target = event_target::<HtmlElement>(&ev);
let scroll_top = target.scroll_top() as f64;