app

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

commit ba19560a50ee1ccd64c9f4c6fd6d5d9c4c7ddeef
parent b48fc9d496e474df01d5cc3ac20ffa40b8c7aa62
Author: triesap <triesap@radroots.dev>
Date:   Thu, 22 Jan 2026 11:00:55 +0000

app: add list input row view

- render input row with line label and field
- wire input callbacks and disabled state
- render action icon or spinner for submit
- cover input action icon defaults in tests

Diffstat:
Mcrates/ui-components/src/lib.rs | 1+
Mcrates/ui-components/src/list.rs | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 133 insertions(+), 1 deletion(-)

diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs @@ -27,6 +27,7 @@ pub use list::{ RadrootsAppUiListDefaultLabels, RadrootsAppUiListGroup, RadrootsAppUiListLine, + RadrootsAppUiListInputRow, RadrootsAppUiListOffsetView, RadrootsAppUiListRowDisplayValue, RadrootsAppUiListRow, diff --git a/crates/ui-components/src/list.rs b/crates/ui-components/src/list.rs @@ -8,9 +8,12 @@ use leptos::prelude::*; use crate::{ radroots_app_ui_list_icon_key, RadrootsAppUiIcon, + RadrootsAppUiIconKey, RadrootsAppUiListDisplay, RadrootsAppUiListDisplayValue, RadrootsAppUiListDefaultLabel, + RadrootsAppUiListInput, + RadrootsAppUiListInputAction, RadrootsAppUiListLabel, RadrootsAppUiListLabelValue, RadrootsAppUiListLabelValueKind, @@ -255,6 +258,16 @@ fn radroots_app_ui_list_offset_mod( mod_value.cloned().unwrap_or(RadrootsAppUiListOffsetMod::Small) } +fn radroots_app_ui_list_input_action_icon_key( + action: &RadrootsAppUiListInputAction, +) -> RadrootsAppUiIconKey { + action + .icon + .as_ref() + .and_then(radroots_app_ui_list_icon_key) + .unwrap_or(RadrootsAppUiIconKey::Plus) +} + fn radroots_app_ui_list_label_value_view( value: RadrootsAppUiListLabelValue, is_right: bool, @@ -564,6 +577,105 @@ pub fn RadrootsAppUiListTouchRow( } #[component] +pub fn RadrootsAppUiListInputRow( + basis: RadrootsAppUiListInput, + #[prop(optional)] hide_border_top: bool, + #[prop(optional)] hide_border_bottom: bool, +) -> impl IntoView { + let RadrootsAppUiListInput { + field, + line_label, + action, + } = basis; + let border_class = radroots_app_ui_list_border_classes(hide_border_top, hide_border_bottom); + let wrap_class = radroots_app_ui_list_class_merge(&[ + Some("flex flex-row h-line w-full justify-start items-center border-t-line overflow-hidden"), + Some(border_class.as_str()), + ]); + let line_label_view = line_label.map(|line_label| { + let label_class = radroots_app_ui_list_class_merge(&[ + Some("text-form_base ui-text-secondary"), + line_label.classes.as_deref(), + ]); + view! { + <div class="flex flex-row h-full justify-start items-center overflow-x-hidden"> + <p class=label_class>{line_label.value}</p> + </div> + } + .into_any() + }); + let input_class = radroots_app_ui_list_class_merge(&[ + Some("el-input"), + field.classes.as_deref(), + ]); + let input_id = field.id; + let input_value = field.value; + let input_placeholder = field.placeholder; + let input_disabled = field.disabled; + let on_input = field.on_input; + let action_view = action.and_then(|action| { + if !action.visible { + return None; + } + let action_loading = action.loading; + let action_icon_key = radroots_app_ui_list_input_action_icon_key(&action); + let action_icon_class = radroots_app_ui_list_class_merge(&[ + Some("ui-text-secondary"), + action.icon.as_ref().and_then(|icon| icon.class.as_deref()), + ]); + let on_click = action.on_click; + Some( + view! { + <div class="absolute top-0 right-0 flex flex-row h-full w-12 pr-4 justify-end items-center fade-in"> + {if action_loading { + view! { <RadrootsAppUiSpinner class="text-[12px]".to_string() /> } + .into_any() + } else { + view! { + <button + type="button" + class="group fade-in-long" + on:click=move |ev: MouseEvent| { + if let Some(callback) = &on_click { + callback.run(ev); + } + } + > + <RadrootsAppUiIcon key=action_icon_key class=action_icon_class size=18 /> + </button> + } + .into_any() + }} + </div> + } + .into_any(), + ) + }); + view! { + <div class="flex flex-row flex-grow h-full w-full" data-ui="list-input"> + <div class=wrap_class> + {line_label_view} + <div class="relative flex flex-row flex-grow h-full pr-12 justify-start items-center"> + <input + id=input_id + class=input_class + disabled=input_disabled + placeholder=input_placeholder + prop:value=input_value + on:input=move |ev| { + if let Some(callback) = &on_input { + callback.run(event_target_value(&ev)); + } + } + /> + {action_view} + </div> + </div> + </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"), @@ -712,9 +824,14 @@ mod tests { radroots_app_ui_list_section_data_ui_value, radroots_app_ui_list_default_labels, radroots_app_ui_list_offset_mod, + radroots_app_ui_list_input_action_icon_key, radroots_app_ui_list_title_padding_class, }; - use crate::RadrootsAppUiListOffsetMod; + use crate::{ + RadrootsAppUiIconKey, + RadrootsAppUiListInputAction, + RadrootsAppUiListOffsetMod, + }; #[test] fn list_data_ui_values() { @@ -781,4 +898,18 @@ mod tests { let resolved = radroots_app_ui_list_offset_mod(None); assert!(matches!(resolved, RadrootsAppUiListOffsetMod::Small)); } + + #[test] + fn list_input_action_defaults_to_plus() { + let action = RadrootsAppUiListInputAction { + visible: true, + loading: false, + icon: None, + on_click: None, + }; + assert_eq!( + radroots_app_ui_list_input_action_icon_key(&action), + RadrootsAppUiIconKey::Plus + ); + } }