184 lines
4.9 KiB
Rust
184 lines
4.9 KiB
Rust
//! Generic loading spinner component
|
|
|
|
use yew::prelude::*;
|
|
|
|
/// Size options for the loading spinner
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum SpinnerSize {
|
|
Small,
|
|
Medium,
|
|
Large,
|
|
}
|
|
|
|
impl SpinnerSize {
|
|
pub fn get_class(&self) -> &'static str {
|
|
match self {
|
|
SpinnerSize::Small => "spinner-border-sm",
|
|
SpinnerSize::Medium => "",
|
|
SpinnerSize::Large => "spinner-border-lg",
|
|
}
|
|
}
|
|
|
|
pub fn get_style(&self) -> &'static str {
|
|
match self {
|
|
SpinnerSize::Small => "width: 1rem; height: 1rem;",
|
|
SpinnerSize::Medium => "width: 1.5rem; height: 1.5rem;",
|
|
SpinnerSize::Large => "width: 2rem; height: 2rem;",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Color options for the loading spinner
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum SpinnerColor {
|
|
Primary,
|
|
Secondary,
|
|
Success,
|
|
Danger,
|
|
Warning,
|
|
Info,
|
|
Light,
|
|
Dark,
|
|
}
|
|
|
|
impl SpinnerColor {
|
|
pub fn get_class(&self) -> &'static str {
|
|
match self {
|
|
SpinnerColor::Primary => "text-primary",
|
|
SpinnerColor::Secondary => "text-secondary",
|
|
SpinnerColor::Success => "text-success",
|
|
SpinnerColor::Danger => "text-danger",
|
|
SpinnerColor::Warning => "text-warning",
|
|
SpinnerColor::Info => "text-info",
|
|
SpinnerColor::Light => "text-light",
|
|
SpinnerColor::Dark => "text-dark",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Properties for LoadingSpinner component
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct LoadingSpinnerProps {
|
|
/// Size of the spinner
|
|
#[prop_or(SpinnerSize::Medium)]
|
|
pub size: SpinnerSize,
|
|
|
|
/// Color of the spinner
|
|
#[prop_or(SpinnerColor::Primary)]
|
|
pub color: SpinnerColor,
|
|
|
|
/// Loading message to display
|
|
#[prop_or_default]
|
|
pub message: Option<AttrValue>,
|
|
|
|
/// Whether to center the spinner
|
|
#[prop_or(true)]
|
|
pub centered: bool,
|
|
|
|
/// Custom CSS class for container
|
|
#[prop_or_default]
|
|
pub container_class: Option<AttrValue>,
|
|
|
|
/// Whether to show as inline spinner
|
|
#[prop_or(false)]
|
|
pub inline: bool,
|
|
}
|
|
|
|
/// LoadingSpinner component
|
|
#[function_component(LoadingSpinner)]
|
|
pub fn loading_spinner(props: &LoadingSpinnerProps) -> Html {
|
|
let container_class = if props.inline {
|
|
"d-inline-flex align-items-center"
|
|
} else if props.centered {
|
|
"d-flex flex-column align-items-center justify-content-center"
|
|
} else {
|
|
"d-flex align-items-center"
|
|
};
|
|
|
|
let final_container_class = if let Some(custom_class) = &props.container_class {
|
|
format!("{} {}", container_class, custom_class.as_str())
|
|
} else {
|
|
container_class.to_string()
|
|
};
|
|
|
|
let spinner_classes = format!(
|
|
"spinner-border {} {}",
|
|
props.size.get_class(),
|
|
props.color.get_class()
|
|
);
|
|
|
|
html! {
|
|
<div class={final_container_class}>
|
|
<div
|
|
class={spinner_classes}
|
|
style={props.size.get_style()}
|
|
role="status"
|
|
aria-hidden="true"
|
|
>
|
|
<span class="visually-hidden">{"Loading..."}</span>
|
|
</div>
|
|
|
|
{if let Some(message) = &props.message {
|
|
let message_class = if props.inline {
|
|
"ms-2"
|
|
} else {
|
|
"mt-2"
|
|
};
|
|
|
|
html! {
|
|
<div class={message_class}>
|
|
{message.as_str()}
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// Convenience component for common loading scenarios
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct LoadingOverlayProps {
|
|
/// Loading message
|
|
#[prop_or("Loading...".to_string())]
|
|
pub message: String,
|
|
|
|
/// Whether the overlay is visible
|
|
#[prop_or(true)]
|
|
pub show: bool,
|
|
|
|
/// Background opacity (0.0 to 1.0)
|
|
#[prop_or(0.8)]
|
|
pub opacity: f64,
|
|
|
|
/// Spinner color
|
|
#[prop_or(SpinnerColor::Primary)]
|
|
pub spinner_color: SpinnerColor,
|
|
}
|
|
|
|
/// LoadingOverlay component for full-screen loading
|
|
#[function_component(LoadingOverlay)]
|
|
pub fn loading_overlay(props: &LoadingOverlayProps) -> Html {
|
|
if !props.show {
|
|
return html! {};
|
|
}
|
|
|
|
let background_style = format!(
|
|
"position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, {}); z-index: 9999;",
|
|
props.opacity
|
|
);
|
|
|
|
html! {
|
|
<div style={background_style}>
|
|
<div class="d-flex flex-column align-items-center justify-content-center h-100">
|
|
<LoadingSpinner
|
|
size={SpinnerSize::Large}
|
|
color={props.spinner_color.clone()}
|
|
message={props.message.clone()}
|
|
centered={true}
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
} |