skill_web/components/run/
wizard_stepper.rs

1//! Wizard stepper component for step-by-step navigation
2//!
3//! Displays a horizontal progress indicator showing:
4//! - Current step (active/highlighted)
5//! - Completed steps (with checkmark)
6//! - Pending steps (grayed out)
7//!
8//! Users can click on accessible steps to navigate directly.
9
10use yew::prelude::*;
11use crate::hooks::WizardStep;
12
13#[derive(Properties, PartialEq)]
14pub struct WizardStepperProps {
15    pub current_step: WizardStep,
16    pub steps_completed: std::collections::HashMap<WizardStep, bool>,
17    pub on_step_click: Callback<WizardStep>,
18}
19
20#[function_component(WizardStepper)]
21pub fn wizard_stepper(props: &WizardStepperProps) -> Html {
22    let steps = vec![
23        WizardStep::SelectSkill,
24        WizardStep::SelectTool,
25        WizardStep::ConfigureParameters,
26        WizardStep::Execute,
27    ];
28
29    html! {
30        <div class="wizard-stepper">
31            // Desktop: Horizontal stepper
32            <div class="hidden md:flex items-center justify-between max-w-3xl mx-auto">
33                { for steps.iter().enumerate().map(|(idx, step)| {
34                    let is_current = props.current_step == *step;
35                    let is_completed = props.steps_completed.get(step).copied().unwrap_or(false);
36                    let is_pending = !is_current && !is_completed;
37
38                    let step_clone = *step;
39                    let on_click = {
40                        let on_step_click = props.on_step_click.clone();
41                        Callback::from(move |_| {
42                            on_step_click.emit(step_clone);
43                        })
44                    };
45
46                    html! {
47                        <>
48                            // Step circle with number/checkmark
49                            <div class="flex flex-col items-center">
50                                <button
51                                    onclick={on_click}
52                                    class={classes!(
53                                        "wizard-step-circle",
54                                        is_current.then(|| "active"),
55                                        is_completed.then(|| "completed"),
56                                        is_pending.then(|| "pending"),
57                                        "transition-all", "duration-200"
58                                    )}
59                                >
60                                    if is_completed {
61                                        // Checkmark icon
62                                        <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
63                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
64                                        </svg>
65                                    } else {
66                                        // Step number
67                                        { step.number() }
68                                    }
69                                </button>
70
71                                // Step label
72                                <span class={classes!(
73                                    "text-xs", "font-medium", "mt-2", "text-center", "max-w-[100px]",
74                                    if is_current {
75                                        "text-primary-600 dark:text-primary-400"
76                                    } else if is_completed {
77                                        "text-green-600 dark:text-green-400"
78                                    } else {
79                                        "text-gray-400 dark:text-gray-500"
80                                    }
81                                )}>
82                                    { step.label() }
83                                </span>
84                            </div>
85
86                            // Connecting line (except after last step)
87                            if idx < steps.len() - 1 {
88                                <div class={classes!(
89                                    "flex-1", "h-0.5", "mx-4", "transition-colors",
90                                    if is_completed {
91                                        "bg-green-500"
92                                    } else {
93                                        "bg-gray-300 dark:bg-gray-700"
94                                    }
95                                )}></div>
96                            }
97                        </>
98                    }
99                }) }
100            </div>
101
102            // Mobile: Vertical stepper
103            <div class="md:hidden space-y-4">
104                { for steps.iter().map(|step| {
105                    let is_current = props.current_step == *step;
106                    let is_completed = props.steps_completed.get(step).copied().unwrap_or(false);
107                    let is_pending = !is_current && !is_completed;
108
109                    let step_clone = *step;
110                    let on_click = {
111                        let on_step_click = props.on_step_click.clone();
112                        Callback::from(move |_| {
113                            on_step_click.emit(step_clone);
114                        })
115                    };
116
117                    html! {
118                        <button
119                            onclick={on_click}
120                            class={classes!(
121                                "flex", "items-center", "gap-3", "w-full", "text-left",
122                                "p-3", "rounded-lg", "transition-all",
123                                if is_current {
124                                    "bg-primary-50 dark:bg-primary-900/20 border-2 border-primary-500"
125                                } else if is_completed {
126                                    "bg-green-50 dark:bg-green-900/20 border-2 border-green-500"
127                                } else {
128                                    "bg-gray-50 dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-700"
129                                }
130                            )}
131                        >
132                            <div class={classes!(
133                                "wizard-step-circle",
134                                is_current.then(|| "active"),
135                                is_completed.then(|| "completed"),
136                                is_pending.then(|| "pending"),
137                                "flex-shrink-0"
138                            )}>
139                                if is_completed {
140                                    <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
141                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
142                                    </svg>
143                                } else {
144                                    { step.number() }
145                                }
146                            </div>
147
148                            <div class="flex-1">
149                                <div class={classes!(
150                                    "text-sm", "font-semibold",
151                                    if is_current {
152                                        "text-primary-600 dark:text-primary-400"
153                                    } else if is_completed {
154                                        "text-green-600 dark:text-green-400"
155                                    } else {
156                                        "text-gray-400 dark:text-gray-500"
157                                    }
158                                )}>
159                                    { step.label() }
160                                </div>
161
162                                if is_completed {
163                                    <div class="text-xs text-green-600 dark:text-green-400 mt-0.5">
164                                        { "Completed" }
165                                    </div>
166                                } else if is_current {
167                                    <div class="text-xs text-primary-600 dark:text-primary-400 mt-0.5">
168                                        { "In Progress" }
169                                    </div>
170                                }
171                            </div>
172
173                            // Arrow indicator for current step
174                            if is_current {
175                                <svg class="w-5 h-5 text-primary-600 dark:text-primary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
176                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
177                                </svg>
178                            }
179                        </button>
180                    }
181                }) }
182            </div>
183        </div>
184    }
185}