app

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

commit 3821b4ff14060740ad3ddcade961c17a1b8301ea
parent 9ba7ed427d539732a18b1e541b6bc448352416ce
Author: triesap <tyson@radroots.org>
Date:   Wed, 28 Jan 2026 13:51:30 +0000

ui: Refine setup layout and button interactions

- Center key choice layout and profile title
- Normalize view sizing and transitions for auth screens
- Update button pair behavior and back offset
- Adjust compact breakpoint and bold button sizing

Diffstat:
Mapp/app.css | 28++++++++++++++++++----------
Mapp/src/app.rs | 175+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/ui-components/src/button_layout.rs | 22++++++++++------------
Mcrates/ui-tokens/assets/themes/layout.css | 10+++++-----
4 files changed, 115 insertions(+), 120 deletions(-)

diff --git a/app/app.css b/app/app.css @@ -11,6 +11,9 @@ @import "./stylesheets/styles-superellipse.css"; @import "../crates/ui-components/assets/list.css"; +@custom-variant h-compact "@media (max-height: 540px)"; +@custom-variant se-compact "@media (max-width: 375px) and (max-height: 540px)"; + @source "./index.html"; @source "./src/**/*.rs"; @source "../crates/**/*.rs"; @@ -56,6 +59,11 @@ min-height: var(--size-line-button); } +@utility h-bold_button { + height: var(--height-bold_button); + min-height: var(--height-bold_button); +} + @layer base { html { font-family: var(--font-sans); @@ -128,6 +136,8 @@ flex-direction: column; flex: 1 1 auto; min-height: 0; + height: 100dvh; + min-height: 100dvh; overscroll-behavior: none; } @@ -149,11 +159,15 @@ } .app-view { - will-change: transform, opacity; + will-change: opacity; + position: relative; + width: 100%; + flex: 1 1 auto; + min-height: 0; } .app-view-enter { - animation: app-view-enter 360ms var(--ease-ios) both; + animation: app-view-enter 200ms cubic-bezier(.2,.8,.2,1) both; } @media (prefers-reduced-motion: reduce) { @@ -290,16 +304,10 @@ } @keyframes app-view-enter { - 0% { + from { opacity: 0; - transform: translateY(10px) scale(0.98); - } - 60% { - opacity: 1; - transform: translateY(-2px) scale(1.005); } - 100% { + to { opacity: 1; - transform: translateY(0) scale(1); } } diff --git a/app/src/app.rs b/app/src/app.rs @@ -12,17 +12,6 @@ use radroots_app_ui_components::{ RadrootsAppUiButtonLayoutAction, RadrootsAppUiButtonLayoutBackAction, RadrootsAppUiButtonLayoutPair, - RadrootsAppUiList, - RadrootsAppUiListIcon, - RadrootsAppUiListItem, - RadrootsAppUiListItemKind, - RadrootsAppUiListLabel, - RadrootsAppUiListLabelText, - RadrootsAppUiListLabelValue, - RadrootsAppUiListLabelValueKind, - RadrootsAppUiListTouch, - RadrootsAppUiListTouchEnd, - RadrootsAppUiListView, }; use crate::{ @@ -78,23 +67,18 @@ fn health_result_label(result: &RadrootsAppHealthCheckResult) -> String { } } -fn setup_label(value: &str) -> RadrootsAppUiListLabelValue { - RadrootsAppUiListLabelValue { - classes_wrap: None, - hide_truncate: false, - value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText { - value: value.to_string(), - classes: Some("capitalize".to_string()), - }), - } -} - fn setup_touch_callback(action: &'static str) -> Callback<MouseEvent> { Callback::new(move |_| { let _ = app_log_debug_emit("log.app.setup.choice", action, None); }) } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RadrootsAppSetupKeyChoice { + Generate, + AddExisting, +} + fn active_key_label(value: Option<String>) -> String { let Some(value) = value else { return "missing".to_string(); @@ -224,61 +208,9 @@ fn SetupPage() -> impl IntoView { let navigate_guard = navigate.clone(); let navigate_home = navigate.clone(); let setup_step = RwSignal::new_local(app_setup_step_default()); - let key_choice_list = RadrootsAppUiList { - id: Some("setup-key-choice".to_string()), - view: Some("setup".to_string()), - classes: None, - title: None, - default_state: None, - list: Some(vec![ - Some(RadrootsAppUiListItem { - kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch { - label: RadrootsAppUiListLabel { - left: vec![setup_label("generate new key")], - right: Vec::new(), - }, - display: None, - end: Some(RadrootsAppUiListTouchEnd { - icon: RadrootsAppUiListIcon { - key: "caret-right".to_string(), - class: None, - }, - on_click: None, - }), - on_click: Some(setup_touch_callback("generate_key")), - }), - loading: false, - hide_active: false, - hide_field: false, - full_rounded: false, - offset: None, - }), - Some(RadrootsAppUiListItem { - kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch { - label: RadrootsAppUiListLabel { - left: vec![setup_label("add existing key")], - right: Vec::new(), - }, - display: None, - end: Some(RadrootsAppUiListTouchEnd { - icon: RadrootsAppUiListIcon { - key: "caret-right".to_string(), - class: None, - }, - on_click: None, - }), - on_click: Some(setup_touch_callback("add_key")), - }), - loading: false, - hide_active: false, - hide_field: false, - full_rounded: false, - offset: None, - }), - ]), - hide_offset: false, - styles: None, - }; + let setup_key_choice = RwSignal::new_local(None::<RadrootsAppSetupKeyChoice>); + let on_generate_key = setup_touch_callback("generate_key"); + let on_add_key = setup_touch_callback("add_key"); Effect::new(move || { if setup_required.get() == Some(false) { navigate_guard("/", Default::default()); @@ -294,12 +226,16 @@ fn SetupPage() -> impl IntoView { }; let rewind_step: Callback<MouseEvent> = { let setup_step = setup_step.clone(); + let setup_key_choice = setup_key_choice.clone(); Callback::new(move |_| { setup_step.update(|step| { *step = step.prev(); }); + setup_key_choice.set(None); }) }; + let on_generate_key = on_generate_key.clone(); + let on_add_key = on_add_key.clone(); view! { <main id="app-setup" @@ -371,34 +307,87 @@ fn SetupPage() -> impl IntoView { RadrootsAppSetupStep::KeyChoice => view! { <section id="app-setup-key-choice" - class="app-view app-view-enter flex flex-col w-full gap-4 px-6 pt-10 pb-16" + class="app-view app-view-enter flex flex-col w-full px-6 pt-10 pb-16" + on:click=move |_| { + setup_key_choice.set(None); + } > - <header id="app-setup-key-choice-header" class="flex flex-col gap-2"> - <p class="font-sans text-sm uppercase tracking-[0.14em] text-ly0-gl-label"> - "Setup" - </p> - <h2 class="font-sans font-[600] text-2xl text-ly0-gl"> - "Choose your key" - </h2> - <p class="font-sans text-line_d_e text-ly0-gl-label"> - "Select how you want to add your Nostr key." - </p> - </header> - <div id="app-setup-key-choice-list" class="w-full"> - <RadrootsAppUiListView basis=key_choice_list.clone() /> + <div + id="app-setup-key-choice-body" + class="flex flex-1 w-full flex-col justify-center items-center gap-8" + > + <div + id="app-setup-key-choice-title" + class="flex flex-row w-full justify-center items-center" + > + <p class="font-sans font-[600] text-ly0-gl text-3xl"> + "Configure Device" + </p> + </div> + <div + id="app-setup-key-choice-actions" + class="flex flex-col w-full gap-6 justify-center items-center" + > + <button + id="app-setup-key-choice-generate" + type="button" + class=move || { + if setup_key_choice.get() + == Some(RadrootsAppSetupKeyChoice::Generate) + { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-apply-active ly1-raise-apply ly1-ring-apply el-re" + } else { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re" + } + } + on:click=move |ev| { + ev.stop_propagation(); + setup_key_choice.set(Some(RadrootsAppSetupKeyChoice::Generate)); + on_generate_key.run(ev); + } + > + <span class="font-sans font-[600] text-ly0-gl text-xl"> + "Create new keypair" + </span> + </button> + <button + id="app-setup-key-choice-add" + type="button" + class=move || { + if setup_key_choice.get() + == Some(RadrootsAppSetupKeyChoice::AddExisting) + { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch ly1-apply-active ly1-raise-apply ly1-ring-apply el-re" + } else { + "flex flex-col h-bold_button w-lo_ios0 ios1:w-lo_ios1 justify-center items-center rounded-touch bg-ly1 el-re" + } + } + on:click=move |ev| { + ev.stop_propagation(); + setup_key_choice.set(Some(RadrootsAppSetupKeyChoice::AddExisting)); + on_add_key.run(ev); + } + > + <span class="font-sans font-[600] text-ly0-gl text-xl"> + "Use existing keypair" + </span> + </button> + </div> </div> </section> }.into_any(), }} <footer id="app-setup-actions" - class="z-10 absolute bottom-4 left-0 flex flex-col w-full justify-center items-center" + class="z-10 absolute bottom-4 left-0 flex flex-col w-full justify-center items-center se-compact:bottom-0" > {move || { let step = setup_step.get(); + let continue_disabled = matches!(step, RadrootsAppSetupStep::KeyChoice) + && setup_key_choice.get().is_none(); let continue_action = RadrootsAppUiButtonLayoutAction { label: "Continue".to_string(), - disabled: step.is_terminal(), + disabled: continue_disabled, loading: false, on_click: advance_step.clone(), }; diff --git a/crates/ui-components/src/button_layout.rs b/crates/ui-components/src/button_layout.rs @@ -97,8 +97,7 @@ pub fn RadrootsAppUiButtonLayoutPair( .map(|value| value.visible) .unwrap_or(false); let wrapper_class = radroots_app_ui_button_class_merge(&[ - Some("flex flex-col gap-1 justify-center items-center el-re"), - if back_visible { Some("-translate-y-8") } else { None }, + Some("flex flex-col gap-1 justify-center items-center"), class.as_deref(), ]); view! { @@ -111,19 +110,24 @@ pub fn RadrootsAppUiButtonLayoutPair( /> {back.map(|back_action| { view! { - <div class="flex flex-col justify-center items-center el-re"> - {if back_action.visible { + <div class="flex flex-col justify-center items-center"> + {{ let back_label = back_action.label.clone().unwrap_or_default(); let back_disabled = back_action.disabled; let back_on_click = back_action.on_click.clone(); + let back_visible = back_action.visible; let back_text_class = radroots_app_ui_button_class_merge(&[ - Some("font-sans font-[600] tracking-wide text-ly1-gl-shade el-re"), + Some("font-sans font-[600] tracking-wide text-ly1-gl-shade"), if back_disabled { None } else { Some("group-active:text-ly1-gl/40") }, ]); + let back_button_class = radroots_app_ui_button_class_merge(&[ + Some("group flex flex-row h-12 w-lo_ios0 ios1:w-lo_ios1 justify-center items-center -translate-y-[2px] transition-opacity duration-[160ms] ease-[cubic-bezier(.2,.8,.2,1)]"), + if back_visible { Some("opacity-100") } else { Some("opacity-0 pointer-events-none") }, + ]); view! { <button type="button" - class="group flex flex-row h-12 w-lo_ios0 ios1:w-lo_ios1 justify-center items-center fade-in el-re" + class=back_button_class disabled=back_disabled on:click=move |ev| { ev.stop_propagation(); @@ -136,12 +140,6 @@ pub fn RadrootsAppUiButtonLayoutPair( <span class=back_text_class>{back_label}</span> </button> }.into_any() - } else { - view! { - <div class="flex flex-row h-4 w-full justify-start items-center"> - <div class="flex-fluid"></div> - </div> - }.into_any() }} </div> } diff --git a/crates/ui-tokens/assets/themes/layout.css b/crates/ui-tokens/assets/themes/layout.css @@ -1,5 +1,5 @@ @theme { - --height-bold_button: 4.25rem; + --height-bold_button: 3.75rem; --height-entry_line: 48px; --height-ios0: 340px; --height-ios1: 345px; @@ -16,7 +16,7 @@ --height-nav_tabs_ios0: 80px; --height-nav_tabs_ios1: 120px; --height-touch_guide: 3.4rem; - --max-height-bold_button: 4.25rem; + --max-height-bold_button: 3.75rem; --max-height-entry_line: 48px; --max-height-ios0: 340px; --max-height-ios1: 345px; @@ -43,7 +43,7 @@ --max-width-lo_textdesc_ios1: 312px; --max-width-trellis_display: 286px; --max-width-trellis_value: 180px; - --min-height-bold_button: 4.25rem; + --min-height-bold_button: 3.75rem; --min-height-entry_line: 48px; --min-height-ios0: 340px; --min-height-ios1: 345px; @@ -72,7 +72,7 @@ --min-width-trellis_value: 180px; --padding-dim_ios0: 340px; --padding-dim_ios1: 345px; - --padding-h_bold_button: 4.25rem; + --padding-h_bold_button: 3.75rem; --padding-h_entry_line: 48px; --padding-h_ios0: 340px; --padding-h_ios1: 345px; @@ -103,7 +103,7 @@ --spacing-dim_ios1: 345px; --spacing-edge: 2px; --spacing-line: 1px; - --translate-h_bold_button: 4.25rem; + --translate-h_bold_button: 3.75rem; --translate-h_entry_line: 48px; --translate-h_ios0: 340px; --translate-h_ios1: 345px;