app

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

commit 697e6d2d7993a78272781e05bf54695fbbf6decb
parent 7ca2fc3f3a81f4c6ccfb81dd574c86be3f17a244
Author: triesap <triesap@radroots.dev>
Date:   Thu, 22 Jan 2026 04:54:16 +0000

app: add list offset and end views

- render list offset variants for spacing and icon actions
- add touch end icon button with callback support
- add offset mod resolver with default fallback
- cover offset default resolution in tests

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

diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs @@ -25,6 +25,7 @@ pub use list::{ radroots_app_ui_list_section_data_ui_value, RadrootsAppUiListDefaultLabels, RadrootsAppUiListGroup, + RadrootsAppUiListOffsetView, RadrootsAppUiListRowDisplayValue, RadrootsAppUiListRow, RadrootsAppUiListRowLabel, @@ -32,6 +33,7 @@ pub use list::{ RadrootsAppUiListRowTrailing, RadrootsAppUiListSection, RadrootsAppUiListTitleView, + RadrootsAppUiListTouchEndView, }; 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 @@ -12,9 +12,12 @@ use crate::{ RadrootsAppUiListLabel, RadrootsAppUiListLabelValue, RadrootsAppUiListLabelValueKind, + RadrootsAppUiListOffset, RadrootsAppUiListOffsetMod, RadrootsAppUiListTitle, RadrootsAppUiListTitleValue, + RadrootsAppUiListTouchEnd, + RadrootsAppUiSpinner, }; pub fn radroots_app_ui_list_group_data_ui_value() -> &'static str { @@ -173,6 +176,12 @@ fn radroots_app_ui_list_default_labels( ) } +fn radroots_app_ui_list_offset_mod( + mod_value: Option<&RadrootsAppUiListOffsetMod>, +) -> RadrootsAppUiListOffsetMod { + mod_value.cloned().unwrap_or(RadrootsAppUiListOffsetMod::Small) +} + fn radroots_app_ui_list_label_value_view( value: RadrootsAppUiListLabelValue, is_right: bool, @@ -291,6 +300,158 @@ pub fn RadrootsAppUiListRowDisplayValue( } #[component] +pub fn RadrootsAppUiListOffsetView( + basis: Option<RadrootsAppUiListOffset>, + #[prop(optional)] class: Option<String>, +) -> impl IntoView { + let basis = basis.unwrap_or(RadrootsAppUiListOffset { + mod_value: None, + classes: None, + hide_space: false, + hide_offset: false, + on_click: None, + }); + if basis.hide_offset { + return view! { <div></div> }.into_any(); + } + let mod_value = radroots_app_ui_list_offset_mod(basis.mod_value.as_ref()); + let wrap_class = radroots_app_ui_list_class_merge(&[ + Some("flex flex-row h-full"), + class.as_deref(), + basis.classes.as_deref(), + ]); + let on_click = basis.on_click; + match mod_value { + RadrootsAppUiListOffsetMod::Small => view! { + <div class=wrap_class> + <div class="flex flex-row h-full w-[22px]"> + <div class="flex-fluid"></div> + </div> + </div> + } + .into_any(), + RadrootsAppUiListOffsetMod::Glyph => view! { + <div class=wrap_class> + <div class="flex flex-row pr-[2px]"> + <div class="flex flex-row h-full w-trellisOffset"> + <div class="flex-fluid"></div> + </div> + </div> + </div> + } + .into_any(), + RadrootsAppUiListOffsetMod::Icon { icon, loading } => { + let icon_key = radroots_app_ui_list_icon_key(&icon); + let icon_class = radroots_app_ui_list_class_merge(&[ + Some("ui-text-secondary"), + icon.class.as_deref(), + ]); + let button_class = radroots_app_ui_list_class_merge(&[ + Some("fade-in pl-2 translate-x-[3px] translate-y-[1px]"), + ]); + let icon_view = if loading { + view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }.into_any() + } else if let Some(icon_key) = icon_key { + view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any() + } else { + view! { <div></div> }.into_any() + }; + view! { + <div class=wrap_class> + <div class="flex flex-row h-full min-w-[20px] w-trellisOffset justify-center items-center pr-3"> + <button + type="button" + class=button_class + on:click=move |ev: MouseEvent| { + if loading { + return; + } + if let Some(callback) = &on_click { + callback.run(ev); + } + } + > + {icon_view} + </button> + </div> + </div> + } + .into_any() + } + RadrootsAppUiListOffsetMod::IconCircle { icon, loading } => { + let icon_key = radroots_app_ui_list_icon_key(&icon); + let icon_class = radroots_app_ui_list_class_merge(&[ + Some("ui-text-secondary"), + icon.class.as_deref(), + ]); + let button_class = radroots_app_ui_list_class_merge(&[ + Some("fade-in pl-2 translate-x-[3px] translate-y-[1px] rounded-full"), + ]); + let icon_view = if loading { + view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> }.into_any() + } else if let Some(icon_key) = icon_key { + view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any() + } else { + view! { <div></div> }.into_any() + }; + view! { + <div class=wrap_class> + <div class="flex flex-row h-full min-w-[20px] w-trellisOffset justify-center items-center pr-3"> + <button + type="button" + class=button_class + on:click=move |ev: MouseEvent| { + if loading { + return; + } + if let Some(callback) = &on_click { + callback.run(ev); + } + } + > + {icon_view} + </button> + </div> + </div> + } + .into_any() + } + } +} + +#[component] +pub fn RadrootsAppUiListTouchEndView( + basis: RadrootsAppUiListTouchEnd, + #[prop(optional)] hide_active: bool, +) -> impl IntoView { + let icon_key = radroots_app_ui_list_icon_key(&basis.icon); + let icon_class = radroots_app_ui_list_class_merge(&[ + Some("ui-text-secondary opacity-70 translate-y-[1px]"), + if hide_active { None } else { Some("opacity-active") }, + basis.icon.class.as_deref(), + ]); + let on_click = basis.on_click; + let icon_view = icon_key.map(|icon_key| { + view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=14 /> }.into_any() + }); + view! { + <div class="absolute top-0 right-0 h-full w-max flex flex-row justify-center items-center"> + <button + type="button" + class="flex pr-3" + on:click=move |ev: MouseEvent| { + if let Some(callback) = &on_click { + callback.run(ev); + } + } + > + {icon_view} + </button> + </div> + } +} + +#[component] pub fn RadrootsAppUiListTitleView(basis: RadrootsAppUiListTitle) -> impl IntoView { let title_class = radroots_app_ui_list_class_merge(&[ Some("flex flex-row h-[24px] w-full pl-[2px] gap-1 items-center"), @@ -436,6 +597,7 @@ mod tests { radroots_app_ui_list_row_trailing_data_ui_value, radroots_app_ui_list_section_data_ui_value, radroots_app_ui_list_default_labels, + radroots_app_ui_list_offset_mod, radroots_app_ui_list_title_padding_class, }; use crate::RadrootsAppUiListOffsetMod; @@ -485,4 +647,10 @@ mod tests { assert_eq!(labels.len(), 1); assert_eq!(labels[0].label, "No items to display."); } + + #[test] + fn list_offset_defaults_to_small() { + let resolved = radroots_app_ui_list_offset_mod(None); + assert!(matches!(resolved, RadrootsAppUiListOffsetMod::Small)); + } }