app

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

commit 854d6350257d8a02f9dc71677bab4a5fd2c311b5
parent a92ba8c76703060371beaf10bbd5b35a10542ba2
Author: triesap <triesap@radroots.dev>
Date:   Thu, 22 Jan 2026 11:13:55 +0000

app: add list container view

- render list view with title, default, and items

- compute row wrapper classes from list styles

- support optional offsets and hidden rows

- add tests for row class and title visibility

Diffstat:
Mcrates/ui-components/src/lib.rs | 1+
Mcrates/ui-components/src/list.rs | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 221 insertions(+), 0 deletions(-)

diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs @@ -39,6 +39,7 @@ pub use list::{ RadrootsAppUiListTitleView, RadrootsAppUiListTouchEndView, RadrootsAppUiListTouchRow, + RadrootsAppUiListView, }; pub use list_types::{ radroots_app_ui_list_icon_key, diff --git a/crates/ui-components/src/list.rs b/crates/ui-components/src/list.rs @@ -7,19 +7,25 @@ use leptos::prelude::*; use crate::{ radroots_app_ui_list_icon_key, + radroots_app_ui_list_styles_resolve, RadrootsAppUiIcon, RadrootsAppUiIconKey, + RadrootsAppUiList, RadrootsAppUiListDisplay, RadrootsAppUiListDisplayValue, + RadrootsAppUiListDefault, RadrootsAppUiListDefaultLabel, RadrootsAppUiListInput, RadrootsAppUiListInputAction, + RadrootsAppUiListItem, + RadrootsAppUiListItemKind, RadrootsAppUiListLabel, RadrootsAppUiListLabelValue, RadrootsAppUiListLabelValueKind, RadrootsAppUiListOffset, RadrootsAppUiListOffsetMod, RadrootsAppUiListSelect, + RadrootsAppUiListStylesResolved, RadrootsAppUiListTitle, RadrootsAppUiListTitleValue, RadrootsAppUiListTouch, @@ -273,6 +279,34 @@ fn radroots_app_ui_list_display_loading(display: Option<&RadrootsAppUiListDispla display.map(|value| value.loading).unwrap_or(false) } +fn radroots_app_ui_list_title_visible( + title: Option<&RadrootsAppUiListTitle>, + default_state: Option<&RadrootsAppUiListDefault>, +) -> bool { + match title { + None => false, + Some(_) => default_state.map(|value| value.show_title).unwrap_or(true), + } +} + +fn radroots_app_ui_list_row_class( + item: &RadrootsAppUiListItem, + styles: &RadrootsAppUiListStylesResolved, +) -> String { + let active_class = radroots_app_ui_list_active_class(item.hide_active); + radroots_app_ui_list_class_merge(&[ + Some("group flex flex-row h-full w-full justify-end items-center el-re"), + if item.hide_field { Some("hidden") } else { None }, + if item.full_rounded { Some("rounded-touch") } else { None }, + if styles.hide_rounded { + None + } else { + Some("first:rounded-t-2xl last:rounded-b-2xl") + }, + active_class, + ]) +} + fn radroots_app_ui_list_label_value_view( value: RadrootsAppUiListLabelValue, is_right: bool, @@ -896,6 +930,127 @@ pub fn RadrootsAppUiListDefaultLabels( } } +#[component] +pub fn RadrootsAppUiListView(basis: RadrootsAppUiList) -> impl IntoView { + let RadrootsAppUiList { + id, + view, + classes, + title, + default_state, + list, + hide_offset, + styles, + } = basis; + let resolved_styles = radroots_app_ui_list_styles_resolve(styles.as_ref()); + let wrap_class = radroots_app_ui_list_class_merge(&[ + Some("flex flex-col"), + classes.as_deref(), + ]); + let group_class = radroots_app_ui_list_class_merge(&[ + Some("relative flex flex-col h-auto w-full gap-[3px]"), + if resolved_styles.set_title_background { + Some("ui-surface") + } else { + None + }, + ]); + let view_value = view.unwrap_or_default(); + let title_view = if radroots_app_ui_list_title_visible(title.as_ref(), default_state.as_ref()) + { + title.map(|title| view! { <RadrootsAppUiListTitleView basis=title /> }.into_any()) + } else { + None + }; + let content_view = if let Some(default_state) = default_state { + let default_class = radroots_app_ui_list_class_merge(&[ + Some("flex flex-col h-auto w-full justify-center items-center"), + if resolved_styles.set_default_background { + Some("ui-surface") + } else { + None + }, + default_state.classes.as_deref(), + ]); + Some( + view! { + <div class=default_class> + <RadrootsAppUiListDefaultLabels labels=default_state.labels /> + </div> + } + .into_any(), + ) + } else if let Some(list) = list { + let items = list + .into_iter() + .filter_map(|item| item) + .map(|item| { + let row_class = radroots_app_ui_list_row_class(&item, &resolved_styles); + let offset_view = if hide_offset { + None + } else { + Some( + view! { <RadrootsAppUiListOffsetView basis=item.offset.clone() /> } + .into_any(), + ) + }; + let row_view = match item.kind { + RadrootsAppUiListItemKind::Touch(touch) => view! { + <RadrootsAppUiListTouchRow + basis=touch + loading=item.loading + hide_active=item.hide_active + hide_border_top=resolved_styles.hide_border_top + hide_border_bottom=resolved_styles.hide_border_bottom + /> + } + .into_any(), + RadrootsAppUiListItemKind::Input(input) => view! { + <RadrootsAppUiListInputRow + basis=input + hide_border_top=resolved_styles.hide_border_top + hide_border_bottom=resolved_styles.hide_border_bottom + /> + } + .into_any(), + RadrootsAppUiListItemKind::Select(select) => view! { + <RadrootsAppUiListSelectRow + basis=select + hide_active=item.hide_active + hide_border_top=resolved_styles.hide_border_top + hide_border_bottom=resolved_styles.hide_border_bottom + /> + } + .into_any(), + }; + view! { + <div class=row_class> + <div class="flex flex-row h-full w-full gap-1 items-center overflow-y-hidden"> + {offset_view} + {row_view} + </div> + </div> + } + .into_any() + }) + .collect_view(); + Some( + view! { <div class="flex flex-col w-full justify-center items-center">{items}</div> } + .into_any(), + ) + } else { + None + }; + view! { + <div id=id class=wrap_class data-view=view_value> + <div class=group_class> + {title_view} + {content_view} + </div> + </div> + } +} + #[cfg(test)] mod tests { use super::{ @@ -911,12 +1066,21 @@ mod tests { radroots_app_ui_list_offset_mod, radroots_app_ui_list_input_action_icon_key, radroots_app_ui_list_display_loading, + radroots_app_ui_list_row_class, + radroots_app_ui_list_title_visible, radroots_app_ui_list_title_padding_class, }; use crate::{ RadrootsAppUiIconKey, RadrootsAppUiListInputAction, + RadrootsAppUiListInputField, + RadrootsAppUiListInput, + RadrootsAppUiListItem, + RadrootsAppUiListItemKind, RadrootsAppUiListOffsetMod, + RadrootsAppUiListStylesResolved, + RadrootsAppUiListTitle, + RadrootsAppUiListTitleValue, }; #[test] @@ -1003,4 +1167,60 @@ mod tests { fn list_display_loading_defaults_false() { assert!(!radroots_app_ui_list_display_loading(None)); } + + #[test] + fn list_title_visible_requires_title() { + assert!(!radroots_app_ui_list_title_visible(None, None)); + let title = RadrootsAppUiListTitle { + value: RadrootsAppUiListTitleValue::Text("Title".to_string()), + classes: None, + mod_value: None, + link: None, + on_click: None, + }; + assert!(radroots_app_ui_list_title_visible(Some(&title), None)); + let default_state = crate::RadrootsAppUiListDefault { + labels: None, + show_title: false, + classes: None, + }; + assert!(!radroots_app_ui_list_title_visible( + Some(&title), + Some(&default_state) + )); + } + + #[test] + fn list_row_class_flags_hidden_and_rounding() { + let item = RadrootsAppUiListItem { + kind: RadrootsAppUiListItemKind::Input(RadrootsAppUiListInput { + field: RadrootsAppUiListInputField { + value: String::new(), + placeholder: None, + disabled: false, + classes: None, + id: None, + on_input: None, + }, + line_label: None, + action: None, + }), + loading: false, + hide_active: true, + hide_field: true, + full_rounded: true, + offset: None, + }; + let styles = RadrootsAppUiListStylesResolved { + hide_border_top: false, + hide_border_bottom: false, + hide_rounded: false, + set_title_background: false, + set_default_background: false, + }; + let class = radroots_app_ui_list_row_class(&item, &styles); + assert!(class.contains("hidden")); + assert!(class.contains("rounded-touch")); + assert!(class.contains("first:rounded-t-2xl")); + } }