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:
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),
}