commit 7ca2fc3f3a81f4c6ccfb81dd574c86be3f17a244
parent cb862446796985476f03edaee9580bb44e146d82
Author: triesap <triesap@radroots.dev>
Date: Thu, 22 Jan 2026 04:51:09 +0000
app: add spinner component for ui states
- add spinner component with blade rendering
- support white and centered variants via classes
- export spinner from ui-components
- cover blade count constant with unit test
Diffstat:
2 files changed, 61 insertions(+), 0 deletions(-)
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -6,6 +6,7 @@ mod label;
mod list;
mod list_types;
mod separator;
+mod spinner;
mod dialog;
mod sheet;
@@ -70,6 +71,7 @@ pub use separator::{
RadrootsAppUiSeparator,
RadrootsAppUiSeparatorOrientation,
};
+pub use spinner::RadrootsAppUiSpinner;
pub use dialog::{
radroots_app_ui_dialog_state_value,
RadrootsAppUiDialogClose,
diff --git a/crates/ui-components/src/spinner.rs b/crates/ui-components/src/spinner.rs
@@ -0,0 +1,59 @@
+#![forbid(unsafe_code)]
+
+use leptos::prelude::*;
+
+const RADROOTS_APP_UI_SPINNER_BLADE_COUNT: usize = 8;
+
+fn radroots_app_ui_spinner_class_merge(parts: &[Option<&str>]) -> String {
+ let mut result = String::new();
+ for part in parts {
+ if let Some(value) = part {
+ if value.is_empty() {
+ continue;
+ }
+ if !result.is_empty() {
+ result.push(' ');
+ }
+ result.push_str(value);
+ }
+ }
+ result
+}
+
+#[component]
+pub fn RadrootsAppUiSpinner(
+ #[prop(optional)] class: Option<String>,
+ #[prop(optional)] id: Option<String>,
+ #[prop(optional)] style: Option<String>,
+ #[prop(optional)] white: bool,
+ #[prop(optional)] center: bool,
+) -> impl IntoView {
+ let class_value = radroots_app_ui_spinner_class_merge(&[
+ Some("spinner8"),
+ if white { Some("spinner8-white") } else { None },
+ if center { Some("center") } else { None },
+ class.as_deref(),
+ ]);
+ let blade_class = radroots_app_ui_spinner_class_merge(&[
+ Some("spinner8-blade"),
+ if white { Some("spinner8-blade-white") } else { None },
+ ]);
+ let blades = (0..RADROOTS_APP_UI_SPINNER_BLADE_COUNT)
+ .map(|_| view! { <span class=blade_class.clone()></span> })
+ .collect_view();
+ view! {
+ <span id=id class=class_value style=style>
+ {blades}
+ </span>
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::RADROOTS_APP_UI_SPINNER_BLADE_COUNT;
+
+ #[test]
+ fn spinner_blade_count_is_expected() {
+ assert_eq!(RADROOTS_APP_UI_SPINNER_BLADE_COUNT, 8);
+ }
+}