app

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

commit 31003eb397a250ddfe3e1394e4cf52f190f9fb0e
parent 4968368f7bc2c62732ad2271a074da01d75e7874
Author: triesap <tyson@radroots.org>
Date:   Sat,  7 Feb 2026 01:51:05 +0000

ui: use list toggles in config

- add list toggle item type and row renderer
- export list toggle types from ui-components
- refactor config preferences to use list views
- render individual summary inside list component

Diffstat:
Mapp/src/app.rs | 284++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/ui-components/src/lib.rs | 1+
Mcrates/ui-components/src/list.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/ui-components/src/list_types.rs | 9+++++++++
4 files changed, 248 insertions(+), 111 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -22,6 +22,20 @@ use radroots_app_ui_components::{ RadrootsAppUiFormField, RadrootsAppUiIcon, RadrootsAppUiIconKey, + RadrootsAppUiList, + RadrootsAppUiListIcon, + RadrootsAppUiListItem, + RadrootsAppUiListItemKind, + RadrootsAppUiListLabel, + RadrootsAppUiListLabelText, + RadrootsAppUiListLabelValue, + RadrootsAppUiListLabelValueKind, + RadrootsAppUiListTitle, + RadrootsAppUiListTitleValue, + RadrootsAppUiListTouch, + RadrootsAppUiListTouchEnd, + RadrootsAppUiListToggle, + RadrootsAppUiListView, RadrootsAppUiNavHeader, RadrootsAppUiNavHeaderBgMode, RadrootsAppUiNavHeaderCollapseMode, @@ -1577,6 +1591,14 @@ fn ConfigPage() -> impl IntoView { notifications_orders: notifications_orders.get(), notifications_messages: notifications_messages.get(), }; + let list_label = |value: String, classes: Option<&str>| RadrootsAppUiListLabelValue { + classes_wrap: None, + hide_truncate: false, + value: RadrootsAppUiListLabelValueKind::Text(RadrootsAppUiListLabelText { + value, + classes: classes.map(str::to_string), + }), + }; let config_validation = move || app_config_flow_validate(&config_flow()); let advance_step = { let backends = backends.clone(); @@ -1965,123 +1987,163 @@ fn ConfigPage() -> impl IntoView { > {move || { if role.get() == Some(RadrootsAppRole::Individual) { - view! { - <div - id="app-config-summary" - class="flex flex-col gap-3" - > - <p class="text-xs font-semibold uppercase tracking-[0.18em] text-ly1-gl-label/70"> - {"Summary"} - </p> - <div class="flex flex-col rounded-touch border border-ly1-edge/60 bg-ly1 overflow-hidden"> - <button - id="app-config-summary-profile" - type="button" - class="flex items-center justify-between gap-4 px-4 py-3 text-left" - on:click=move |_| { + let profile_summary = { + let name = profile_name.get(); + let location = profile_location.get(); + let name = name.trim().to_string(); + let location = location.trim().to_string(); + if name.is_empty() && location.is_empty() { + "Add name and location".to_string() + } else if location.is_empty() { + name + } else if name.is_empty() { + location + } else { + format!("{name} • {location}") + } + }; + let products_summary = { + let items = individual_products.get(); + if items.is_empty() { + "Add products".to_string() + } else { + items.join(", ") + } + }; + let summary_list = RadrootsAppUiList { + id: Some("app-config-summary-list".to_string()), + view: Some("app-config-summary".to_string()), + classes: None, + title: Some(RadrootsAppUiListTitle { + value: RadrootsAppUiListTitleValue::Text("Summary".to_string()), + classes: None, + mod_value: None, + link: None, + on_click: None, + }), + default_state: None, + list: Some(vec![ + Some(RadrootsAppUiListItem { + kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch { + label: RadrootsAppUiListLabel { + left: vec![list_label("Profile".to_string(), None)], + right: vec![list_label(profile_summary, Some("text-xs"))], + }, + display: None, + end: Some(RadrootsAppUiListTouchEnd { + icon: RadrootsAppUiListIcon { + key: "caret-right".to_string(), + class: None, + }, + on_click: None, + }), + on_click: Some(Callback::new(move |_| { config_step.set(RadrootsAppConfigStep::Profile); - } - > - <div class="flex flex-col gap-1"> - <span class="text-sm font-semibold text-ly1-gl"> - {"Profile"} - </span> - <span class="text-xs text-ly1-gl-label/80 line-clamp-2"> - {move || { - let name = profile_name.get(); - let location = profile_location.get(); - let name = name.trim().to_string(); - let location = location.trim().to_string(); - if name.is_empty() && location.is_empty() { - "Add name and location".to_string() - } else if location.is_empty() { - name - } else if name.is_empty() { - location - } else { - format!("{name} • {location}") - } - }} - </span> - </div> - <RadrootsAppUiIcon key=RadrootsAppUiIconKey::CaretRight size=18 /> - </button> - <button - id="app-config-summary-products" - type="button" - class="flex items-center justify-between gap-4 border-t border-ly1-edge/50 px-4 py-3 text-left" - on:click=move |_| { + })), + }), + loading: false, + hide_active: false, + hide_field: false, + full_rounded: false, + offset: None, + }), + Some(RadrootsAppUiListItem { + kind: RadrootsAppUiListItemKind::Touch(RadrootsAppUiListTouch { + label: RadrootsAppUiListLabel { + left: vec![list_label("Products interested in".to_string(), None)], + right: vec![list_label(products_summary, Some("text-xs"))], + }, + display: None, + end: Some(RadrootsAppUiListTouchEnd { + icon: RadrootsAppUiListIcon { + key: "caret-right".to_string(), + class: None, + }, + on_click: None, + }), + on_click: Some(Callback::new(move |_| { config_step.set(RadrootsAppConfigStep::Role); - } - > - <div class="flex flex-col gap-1"> - <span class="text-sm font-semibold text-ly1-gl"> - {"Products interested in"} - </span> - <span class="text-xs text-ly1-gl-label/80 line-clamp-2"> - {move || { - let items = individual_products.get(); - if items.is_empty() { - "Add products".to_string() - } else { - items.join(", ") - } - }} - </span> - </div> - <RadrootsAppUiIcon key=RadrootsAppUiIconKey::CaretRight size=18 /> - </button> - </div> - </div> - } - .into_any() + })), + }), + loading: false, + hide_active: false, + hide_field: false, + full_rounded: false, + offset: None, + }), + ]), + hide_offset: false, + styles: None, + }; + view! { <RadrootsAppUiListView basis=summary_list /> }.into_any() } else { view! { <></> }.into_any() } }} - <RadrootsAppUiFormField - label="Notifications".to_string() - id="app-config-preferences-notifications".to_string() - > - <div class="flex flex-col rounded-touch border border-ly1-edge/60 bg-ly1 overflow-hidden"> - <button - id="app-config-notifications-orders" - type="button" - class="flex w-full items-center justify-between gap-4 px-4 py-3 text-left text-sm text-ly1-gl" - role="switch" - aria-checked=move || if notifications_orders.get() { "true" } else { "false" } - on:click=move |_| { - notifications_orders.update(|value| *value = !*value); - } - > - <span>{"Order updates"}</span> - <span - class="ios-switch" - class:ios-switch--checked=move || notifications_orders.get() - > - <span class="ios-switch__thumb"></span> - </span> - </button> - <button - id="app-config-notifications-messages" - type="button" - class="flex w-full items-center justify-between gap-4 border-t border-ly1-edge/50 px-4 py-3 text-left text-sm text-ly1-gl" - role="switch" - aria-checked=move || if notifications_messages.get() { "true" } else { "false" } - on:click=move |_| { - notifications_messages.update(|value| *value = !*value); - } - > - <span>{"Messages"}</span> - <span - class="ios-switch" - class:ios-switch--checked=move || notifications_messages.get() - > - <span class="ios-switch__thumb"></span> - </span> - </button> - </div> - </RadrootsAppUiFormField> + {move || { + let toggle_orders = { + let notifications_orders = notifications_orders.clone(); + Callback::new(move |value: bool| { + notifications_orders.set(value); + }) + }; + let toggle_messages = { + let notifications_messages = notifications_messages.clone(); + Callback::new(move |value: bool| { + notifications_messages.set(value); + }) + }; + let notifications_list = RadrootsAppUiList { + id: Some("app-config-notifications-list".to_string()), + view: Some("app-config-notifications".to_string()), + classes: None, + title: Some(RadrootsAppUiListTitle { + value: RadrootsAppUiListTitleValue::Text("Notifications".to_string()), + classes: None, + mod_value: None, + link: None, + on_click: None, + }), + default_state: None, + list: Some(vec![ + Some(RadrootsAppUiListItem { + kind: RadrootsAppUiListItemKind::Toggle(RadrootsAppUiListToggle { + label: RadrootsAppUiListLabel { + left: vec![list_label("Order updates".to_string(), None)], + right: Vec::new(), + }, + checked: notifications_orders.get(), + disabled: false, + on_toggle: Some(toggle_orders), + }), + loading: false, + hide_active: true, + hide_field: false, + full_rounded: false, + offset: None, + }), + Some(RadrootsAppUiListItem { + kind: RadrootsAppUiListItemKind::Toggle(RadrootsAppUiListToggle { + label: RadrootsAppUiListLabel { + left: vec![list_label("Messages".to_string(), None)], + right: Vec::new(), + }, + checked: notifications_messages.get(), + disabled: false, + on_toggle: Some(toggle_messages), + }), + loading: false, + hide_active: true, + hide_field: false, + full_rounded: false, + offset: None, + }), + ]), + hide_offset: false, + styles: None, + }; + view! { <RadrootsAppUiListView basis=notifications_list /> } + }} </section> }.into_any(), }} diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs @@ -85,6 +85,7 @@ pub use list_types::{ RadrootsAppUiListTitleValue, RadrootsAppUiListTouch, RadrootsAppUiListTouchEnd, + RadrootsAppUiListToggle, }; pub use label::RadrootsAppUiLabel; pub use separator::{ diff --git a/crates/ui-components/src/list.rs b/crates/ui-components/src/list.rs @@ -30,6 +30,7 @@ use crate::{ RadrootsAppUiListTitleValue, RadrootsAppUiListTouch, RadrootsAppUiListTouchEnd, + RadrootsAppUiListToggle, RadrootsAppUiSpinner, }; @@ -676,6 +677,59 @@ pub fn RadrootsAppUiListTouchRow( } #[component] +pub fn RadrootsAppUiListToggleRow( + basis: RadrootsAppUiListToggle, + #[prop(optional)] line_id: String, + #[prop(optional)] hide_active: bool, + #[prop(optional)] hide_border_top: bool, + #[prop(optional)] hide_border_bottom: bool, + #[prop(optional)] loading: bool, +) -> impl IntoView { + let label = basis.label; + let checked = basis.checked; + let disabled = basis.disabled; + let on_toggle = basis.on_toggle; + let switch_class = if checked { + "ios-switch ios-switch--checked" + } else { + "ios-switch" + }; + let end_slot = Arc::new(move || { + view! { + <span class="flex flex-row h-full items-center pr-3"> + <span class=switch_class aria-hidden="true"> + <span class="ios-switch__thumb"></span> + </span> + </span> + } + .into_any() + }) as ChildrenFn; + let on_click = if disabled { + None + } else { + let on_toggle = on_toggle.clone(); + Some(Callback::new(move |_ev: MouseEvent| { + if let Some(callback) = &on_toggle { + callback.run(!checked); + } + })) + }; + view! { + <RadrootsAppUiListLine + id=line_id + as_button=true + loading=loading + hide_border_top=hide_border_top + hide_border_bottom=hide_border_bottom + on_click=on_click + end=Some(end_slot) + > + <RadrootsAppUiListRowLabel basis=label.clone() hide_active=hide_active /> + </RadrootsAppUiListLine> + } +} + +#[component] pub fn RadrootsAppUiListInputRow( basis: RadrootsAppUiListInput, #[prop(optional)] line_id: String, @@ -1169,6 +1223,17 @@ pub fn RadrootsAppUiListView(basis: RadrootsAppUiList) -> impl IntoView { /> } .into_any(), + RadrootsAppUiListItemKind::Toggle(toggle) => view! { + <RadrootsAppUiListToggleRow + basis=toggle + loading=item.loading + hide_active=item.hide_active + hide_border_top=resolved_styles.hide_border_top + hide_border_bottom=resolved_styles.hide_border_bottom + line_id=line_id.clone() + /> + } + .into_any(), RadrootsAppUiListItemKind::Input(input) => view! { <RadrootsAppUiListInputRow basis=input diff --git a/crates/ui-components/src/list_types.rs b/crates/ui-components/src/list_types.rs @@ -159,6 +159,14 @@ pub struct RadrootsAppUiListTouch { } #[derive(Debug, Clone)] +pub struct RadrootsAppUiListToggle { + pub label: RadrootsAppUiListLabel, + pub checked: bool, + pub disabled: bool, + pub on_toggle: Option<Callback<bool>>, +} + +#[derive(Debug, Clone)] pub struct RadrootsAppUiListInputAction { pub visible: bool, pub loading: bool, @@ -242,6 +250,7 @@ pub struct RadrootsAppUiListOffset { #[derive(Debug, Clone)] pub enum RadrootsAppUiListItemKind { Touch(RadrootsAppUiListTouch), + Toggle(RadrootsAppUiListToggle), Input(RadrootsAppUiListInput), Select(RadrootsAppUiListSelect), }