use leptos::prelude::*;
use leptos::tachys::view::any_view::{AnyView, IntoAny};
use orbital_macros::component_doc;
use turf::inline_style_sheet_values;
use super::{Body1, Caption1};
use crate::primitives::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StepStatus {
#[default]
Pending,
Active,
Done,
Failed,
Skipped,
}
#[slot]
pub struct Step {
#[prop(into)]
pub label: String,
pub status: StepStatus,
#[prop(optional, into)]
pub message: Option<String>,
}
#[component_doc(
category = "Feedback",
preview_slug = "stepper",
preview_label = "Stepper",
preview_icon = icondata::AiOrderedListOutlined,
)]
#[component]
pub fn Stepper(
#[prop(optional, default = Vec::new())]
steps: Vec<Step>,
#[prop(optional, default = true)]
vertical: bool,
#[prop(optional, into)]
progress: Option<Signal<u32>>,
) -> impl IntoView {
let total = steps.len();
let (style_sheet, class_names) = inline_style_sheet_values! {
.Wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.Vertical {
display: flex;
flex-direction: column;
}
.StepRowV {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px;
}
.IconColumnV {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 24px;
}
.ConnectorV {
width: 2px;
flex: 1;
min-height: 16px;
margin-top: 4px;
margin-bottom: 4px;
}
.ConnectorSolid {
border-left: 2px solid var(--orb-color-status-success-fg);
}
.ConnectorActive {
border-left: 2px solid var(--orb-color-brand-fg);
}
.ConnectorDashed {
border-left: 2px dashed var(--orb-color-border-default);
}
.ContentV {
display: flex;
flex-direction: column;
gap: 2px;
padding-bottom: 4px;
}
.Horizontal {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.StepItemH {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.IconRowH {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.ConnectorH {
height: 2px;
flex: 1;
}
.ConnectorHSolid {
border-top: 2px solid var(--orb-color-status-success-fg);
}
.ConnectorHActive {
border-top: 2px solid var(--orb-color-brand-fg);
}
.ConnectorHDashed {
border-top: 2px dashed var(--orb-color-border-default);
}
.ContentH {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
text-align: center;
max-width: 120px;
}
.IconWrap {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
}
.StatusDone {
color: var(--orb-color-status-success-fg);
}
.StatusActive {
color: var(--orb-color-brand-fg);
}
.StatusFailed {
color: var(--orb-color-status-danger-fg);
}
.StatusPending {
color: var(--orb-color-text-tertiary);
}
.StatusSkipped {
color: var(--orb-color-text-tertiary);
}
.Message {
color: var(--orb-color-text-tertiary);
}
};
let icon_class = |status: StepStatus| -> String {
let base = &class_names.icon_wrap;
let modifier = match status {
StepStatus::Done => &class_names.status_done,
StepStatus::Active => &class_names.status_active,
StepStatus::Failed => &class_names.status_failed,
StepStatus::Pending => &class_names.status_pending,
StepStatus::Skipped => &class_names.status_skipped,
};
format!("{base} {modifier}")
};
let connector_v_class = |status: StepStatus| -> String {
let base = &class_names.connector_v;
let modifier = match status {
StepStatus::Done => &class_names.connector_solid,
StepStatus::Active => &class_names.connector_active,
StepStatus::Pending | StepStatus::Failed | StepStatus::Skipped => {
&class_names.connector_dashed
}
};
format!("{base} {modifier}")
};
let connector_h_class = |status: StepStatus| -> String {
let base = &class_names.connector_h;
let modifier = match status {
StepStatus::Done => &class_names.connector_h_solid,
StepStatus::Active => &class_names.connector_h_active,
StepStatus::Pending | StepStatus::Failed | StepStatus::Skipped => {
&class_names.connector_h_dashed
}
};
format!("{base} {modifier}")
};
let step_icon = |status: StepStatus| -> AnyView {
match status {
StepStatus::Done => view! {
<Icon icon=icondata::AiCheckCircleOutlined width="20px" height="20px" />
}
.into_any(),
StepStatus::Active => view! {
<Spinner size=SpinnerSize::ExtraTiny />
}
.into_any(),
StepStatus::Failed => view! {
<Icon icon=icondata::AiCloseCircleOutlined width="20px" height="20px" />
}
.into_any(),
StepStatus::Pending => view! {
<Icon icon=icondata::AiClockCircleOutlined width="20px" height="20px" />
}
.into_any(),
StepStatus::Skipped => view! {
<Icon icon=icondata::BiSkipNextRegular width="20px" height="20px" />
}
.into_any(),
}
};
let steps_view = if vertical {
view! {
<div class=class_names.vertical>
{steps.into_iter().enumerate().map(|(i, step)| {
let is_last = i == total - 1;
let ic = icon_class(step.status);
let cc = connector_v_class(step.status);
view! {
<div class=class_names.step_row_v>
<div class=class_names.icon_column_v>
<div class=ic>{step_icon(step.status)}</div>
{if !is_last {
view! { <div class=cc></div> }.into_any()
} else {
().into_any()
}}
</div>
<div class=class_names.content_v>
<Body1>{step.label}</Body1>
{step.message.map(|msg| view! {
<Caption1 class=class_names.message>{msg}</Caption1>
})}
</div>
</div>
}
}).collect::<Vec<_>>()}
</div>
}
.into_any()
} else {
view! {
<div class=class_names.horizontal>
{steps.into_iter().enumerate().map(|(i, step)| {
let is_last = i == total - 1;
let ic = icon_class(step.status);
let cc = connector_h_class(step.status);
view! {
<div class=class_names.step_item_h style="flex: 1;">
<div class=class_names.icon_row_h>
<div class=ic>{step_icon(step.status)}</div>
{if !is_last {
view! { <div class=cc></div> }.into_any()
} else {
().into_any()
}}
</div>
<div class=class_names.content_h>
<Body1>{step.label}</Body1>
{step.message.map(|msg| view! {
<Caption1 class=class_names.message>{msg}</Caption1>
})}
</div>
</div>
}
}).collect::<Vec<_>>()}
</div>
}
.into_any()
};
view! {
<style>{style_sheet}</style>
<div class=class_names.wrapper>
{steps_view}
{progress.map(|p| view! {
<ProgressBar value=Signal::derive(move || p.get() as f64 / 100.0) />
})}
</div>
}
}