commit afd26d008605a3f671353c96abb9374be14f5dae
parent 697e6d2d7993a78272781e05bf54695fbbf6decb
Author: triesap <triesap@radroots.dev>
Date: Thu, 22 Jan 2026 10:55:53 +0000
app: add list line component
- add list line wrapper with loading state support
- add border class resolver for top/bottom edges
- export list line from ui-components
- cover border class resolver with unit tests
Diffstat:
2 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -19,12 +19,14 @@ pub use icon::{
};
pub use list::{
radroots_app_ui_list_group_data_ui_value,
+ radroots_app_ui_list_border_classes,
radroots_app_ui_list_row_data_ui_value,
radroots_app_ui_list_row_leading_data_ui_value,
radroots_app_ui_list_row_trailing_data_ui_value,
radroots_app_ui_list_section_data_ui_value,
RadrootsAppUiListDefaultLabels,
RadrootsAppUiListGroup,
+ RadrootsAppUiListLine,
RadrootsAppUiListOffsetView,
RadrootsAppUiListRowDisplayValue,
RadrootsAppUiListRow,
diff --git a/crates/ui-components/src/list.rs b/crates/ui-components/src/list.rs
@@ -151,6 +151,72 @@ fn radroots_app_ui_list_class_merge(parts: &[Option<&str>]) -> String {
result
}
+pub fn radroots_app_ui_list_border_classes(
+ hide_border_top: bool,
+ hide_border_bottom: bool,
+) -> String {
+ let top = if hide_border_top {
+ "group-first:border-t-0"
+ } else {
+ "group-first:border-t-line"
+ };
+ let bottom = if hide_border_bottom {
+ "group-last:border-b-0"
+ } else {
+ "group-last:border-b-line"
+ };
+ format!("{top} {bottom}")
+}
+
+#[component]
+pub fn RadrootsAppUiListLine(
+ #[prop(optional)] loading: bool,
+ #[prop(optional)] hide_border_top: bool,
+ #[prop(optional)] hide_border_bottom: bool,
+ #[prop(optional)] on_click: Option<Callback<MouseEvent>>,
+ #[prop(optional)] end: Option<ChildrenFn>,
+ children: ChildrenFn,
+) -> impl IntoView {
+ let border_class = radroots_app_ui_list_border_classes(hide_border_top, hide_border_bottom);
+ let line_class = radroots_app_ui_list_class_merge(&[
+ Some("flex flex-row h-full w-full justify-center items-center border-t-line el-re"),
+ Some(border_class.as_str()),
+ ]);
+ let end_view = end.map(|slot| slot());
+ view! {
+ <button
+ type="button"
+ class="flex flex-row flex-grow overflow-hidden"
+ on:click=move |ev: MouseEvent| {
+ if let Some(callback) = &on_click {
+ callback.run(ev);
+ }
+ }
+ >
+ <div class=line_class data-ui="list-line">
+ {if loading {
+ view! {
+ <div class="flex flex-row h-full w-full justify-center items-center">
+ <RadrootsAppUiSpinner />
+ </div>
+ }
+ .into_any()
+ } else {
+ view! {
+ <div class="relative group flex flex-row h-line w-full pr-[2px] justify-between items-center el-re">
+ <div class="flex flex-row h-full w-trellis_display justify-between items-center">
+ {children()}
+ </div>
+ {end_view}
+ </div>
+ }
+ .into_any()
+ }}
+ </div>
+ </button>
+ }
+}
+
fn radroots_app_ui_list_title_padding_class(mod_value: Option<&RadrootsAppUiListOffsetMod>) -> Option<&'static str> {
match mod_value {
Some(RadrootsAppUiListOffsetMod::Small) => Some("pl-[16px]"),
@@ -591,6 +657,7 @@ pub fn RadrootsAppUiListDefaultLabels(
mod tests {
use super::{
radroots_app_ui_list_class_merge,
+ radroots_app_ui_list_border_classes,
radroots_app_ui_list_group_data_ui_value,
radroots_app_ui_list_row_data_ui_value,
radroots_app_ui_list_row_leading_data_ui_value,
@@ -629,6 +696,14 @@ mod tests {
}
#[test]
+ fn list_border_classes_match_flags() {
+ let classes = radroots_app_ui_list_border_classes(true, false);
+ assert_eq!(classes, "group-first:border-t-0 group-last:border-b-line");
+ let classes = radroots_app_ui_list_border_classes(false, true);
+ assert_eq!(classes, "group-first:border-t-line group-last:border-b-0");
+ }
+
+ #[test]
fn list_title_padding_matches_mod() {
assert_eq!(
radroots_app_ui_list_title_padding_class(Some(&RadrootsAppUiListOffsetMod::Small)),