commit cb862446796985476f03edaee9580bb44e146d82
parent 80ee42a89d11524b196180066026df1f546ca900
Author: triesap <triesap@radroots.dev>
Date: Thu, 22 Jan 2026 04:50:01 +0000
app: add list title and default label views
- render list title with offset padding and optional link
- render default label rows with callbacks and fallback text
- add list helpers for title padding and default labels
- cover title padding and default label fallback in tests
Diffstat:
2 files changed, 190 insertions(+), 0 deletions(-)
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -22,6 +22,7 @@ pub use list::{
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,
RadrootsAppUiListRowDisplayValue,
RadrootsAppUiListRow,
@@ -29,6 +30,7 @@ pub use list::{
RadrootsAppUiListRowLeading,
RadrootsAppUiListRowTrailing,
RadrootsAppUiListSection,
+ RadrootsAppUiListTitleView,
};
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
@@ -8,9 +8,13 @@ use crate::{
RadrootsAppUiIcon,
RadrootsAppUiListDisplay,
RadrootsAppUiListDisplayValue,
+ RadrootsAppUiListDefaultLabel,
RadrootsAppUiListLabel,
RadrootsAppUiListLabelValue,
RadrootsAppUiListLabelValueKind,
+ RadrootsAppUiListOffsetMod,
+ RadrootsAppUiListTitle,
+ RadrootsAppUiListTitleValue,
};
pub fn radroots_app_ui_list_group_data_ui_value() -> &'static str {
@@ -144,6 +148,31 @@ fn radroots_app_ui_list_class_merge(parts: &[Option<&str>]) -> String {
result
}
+fn radroots_app_ui_list_title_padding_class(mod_value: Option<&RadrootsAppUiListOffsetMod>) -> Option<&'static str> {
+ match mod_value {
+ Some(RadrootsAppUiListOffsetMod::Small) => Some("pl-[16px]"),
+ Some(RadrootsAppUiListOffsetMod::Glyph)
+ | Some(RadrootsAppUiListOffsetMod::Icon { .. })
+ | Some(RadrootsAppUiListOffsetMod::IconCircle { .. }) => Some("pl-[36px]"),
+ None => None,
+ }
+}
+
+fn radroots_app_ui_list_default_labels(
+ labels: Option<&[RadrootsAppUiListDefaultLabel]>,
+) -> Vec<RadrootsAppUiListDefaultLabel> {
+ labels.map_or_else(
+ || {
+ vec![RadrootsAppUiListDefaultLabel {
+ label: "No items to display.".to_string(),
+ classes: None,
+ on_click: None,
+ }]
+ },
+ |labels| labels.to_vec(),
+ )
+}
+
fn radroots_app_ui_list_label_value_view(
value: RadrootsAppUiListLabelValue,
is_right: bool,
@@ -261,6 +290,142 @@ pub fn RadrootsAppUiListRowDisplayValue(
}
}
+#[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"),
+ basis.classes.as_deref(),
+ ]);
+ let padding_class = radroots_app_ui_list_title_padding_class(basis.mod_value.as_ref());
+ let button_class = radroots_app_ui_list_class_merge(&[
+ Some("flex flex-row h-full w-max items-center gap-1"),
+ padding_class,
+ ]);
+ let on_click = basis.on_click;
+ let title_value = match basis.value {
+ RadrootsAppUiListTitleValue::Spacer => {
+ view! { <div class="flex-fluid"></div> }.into_any()
+ }
+ RadrootsAppUiListTitleValue::Text(value) => {
+ view! { <p class="text-trellis_ti uppercase ui-text-secondary">{value}</p> }.into_any()
+ }
+ };
+ let link_view = basis.link.map(|link| {
+ let label_view = link.label.map(|label| match label.value {
+ RadrootsAppUiListLabelValueKind::Text(text) => {
+ let class = radroots_app_ui_list_class_merge(&[
+ Some("text-trellis_ti uppercase fade-in"),
+ text.classes.as_deref(),
+ ]);
+ view! { <p class=class>{text.value}</p> }.into_any()
+ }
+ RadrootsAppUiListLabelValueKind::Icon(icon) => {
+ let icon_key = radroots_app_ui_list_icon_key(&icon);
+ let icon_class = radroots_app_ui_list_class_merge(&[
+ Some("fade-in"),
+ icon.class.as_deref(),
+ ]);
+ 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()
+ }
+ }
+ });
+ let icon_view = link.icon.and_then(|icon| {
+ radroots_app_ui_list_icon_key(&icon).map(|icon_key| {
+ let icon_class = radroots_app_ui_list_class_merge(&[
+ Some("fade-in"),
+ icon.class.as_deref(),
+ ]);
+ view! { <RadrootsAppUiIcon key=icon_key class=icon_class size=16 /> }.into_any()
+ })
+ });
+ let link_class = radroots_app_ui_list_class_merge(&[
+ Some("group flex flex-row h-full w-max items-center"),
+ link.classes.as_deref(),
+ ]);
+ let on_click = link.on_click;
+ view! {
+ <button
+ type="button"
+ class=link_class
+ on:click=move |_| {
+ if let Some(callback) = &on_click {
+ callback.run(());
+ }
+ }
+ >
+ {label_view}
+ {icon_view}
+ </button>
+ }
+ .into_any()
+ });
+ view! {
+ <div class=title_class>
+ <button
+ type="button"
+ class=button_class
+ on:click=move |_| {
+ if let Some(callback) = &on_click {
+ callback.run(());
+ }
+ }
+ >
+ {title_value}
+ </button>
+ {link_view}
+ </div>
+ }
+}
+
+#[component]
+pub fn RadrootsAppUiListDefaultLabels(
+ labels: Option<Vec<RadrootsAppUiListDefaultLabel>>,
+ #[prop(optional)] class: Option<String>,
+) -> impl IntoView {
+ let labels = radroots_app_ui_list_default_labels(labels.as_deref());
+ let wrap_class = radroots_app_ui_list_class_merge(&[
+ Some("flex flex-row"),
+ class.as_deref(),
+ ]);
+ let items = labels
+ .into_iter()
+ .map(|label| {
+ let inner_class = radroots_app_ui_list_class_merge(&[
+ Some("text-trellis_ti"),
+ label.classes.as_deref(),
+ ]);
+ let on_click = label.on_click;
+ if on_click.is_some() {
+ view! {
+ <button
+ type="button"
+ class=inner_class
+ on:click=move |_| {
+ if let Some(callback) = &on_click {
+ callback.run(());
+ }
+ }
+ >
+ {label.label}
+ </button>
+ }
+ .into_any()
+ } else {
+ view! { <span class=inner_class>{label.label}</span> }.into_any()
+ }
+ })
+ .collect_view();
+ view! {
+ <div class=wrap_class>
+ <p class="text-trellis_ti ui-text-secondary">{items}</p>
+ </div>
+ }
+}
+
#[cfg(test)]
mod tests {
use super::{
@@ -270,7 +435,10 @@ mod tests {
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,
+ radroots_app_ui_list_default_labels,
+ radroots_app_ui_list_title_padding_class,
};
+ use crate::RadrootsAppUiListOffsetMod;
#[test]
fn list_data_ui_values() {
@@ -297,4 +465,24 @@ mod tests {
]);
assert_eq!(merged, "alpha beta");
}
+
+ #[test]
+ fn list_title_padding_matches_mod() {
+ assert_eq!(
+ radroots_app_ui_list_title_padding_class(Some(&RadrootsAppUiListOffsetMod::Small)),
+ Some("pl-[16px]")
+ );
+ assert_eq!(
+ radroots_app_ui_list_title_padding_class(Some(&RadrootsAppUiListOffsetMod::Glyph)),
+ Some("pl-[36px]")
+ );
+ assert_eq!(radroots_app_ui_list_title_padding_class(None), None);
+ }
+
+ #[test]
+ fn list_default_labels_fallbacks() {
+ let labels = radroots_app_ui_list_default_labels(None);
+ assert_eq!(labels.len(), 1);
+ assert_eq!(labels[0].label, "No items to display.");
+ }
}