Skip to main content

leptos_shadcn_progress/
default.rs

1use leptos::prelude::*;
2use leptos_style::Style;
3
4pub const PROGRESS_CLASS: &str = "relative w-full overflow-hidden rounded-full bg-secondary";
5pub const PROGRESS_INDICATOR_CLASS: &str = "h-full w-full flex-1 bg-primary transition-all";
6
7#[derive(Clone, Copy, PartialEq, Debug)]
8pub enum ProgressVariant {
9    Default,
10    Success,
11    Warning,
12    Destructive,
13    Info,
14}
15
16impl Default for ProgressVariant {
17    fn default() -> Self {
18        ProgressVariant::Default
19    }
20}
21
22impl From<String> for ProgressVariant {
23    fn from(s: String) -> Self {
24        match s.as_str() {
25            "success" => ProgressVariant::Success,
26            "warning" => ProgressVariant::Warning,
27            "destructive" => ProgressVariant::Destructive,
28            "info" => ProgressVariant::Info,
29            _ => ProgressVariant::Default,
30        }
31    }
32}
33
34impl ProgressVariant {
35    pub fn indicator_class(&self) -> &'static str {
36        match self {
37            ProgressVariant::Default => "bg-primary",
38            ProgressVariant::Success => "bg-green-500",
39            ProgressVariant::Warning => "bg-yellow-500",
40            ProgressVariant::Destructive => "bg-red-500",
41            ProgressVariant::Info => "bg-blue-500",
42        }
43    }
44}
45
46#[component]
47pub fn Progress(
48    #[prop(into, optional)] value: Signal<f64>,
49    #[prop(into, optional)] max: MaybeProp<f64>,
50    #[prop(into, optional)] variant: MaybeProp<ProgressVariant>,
51    #[prop(into, optional)] animated: Signal<bool>,
52    #[prop(into, optional)] show_label: Signal<bool>,
53    #[prop(into, optional)] size: MaybeProp<String>,
54    #[prop(into, optional)] class: MaybeProp<String>,
55    #[prop(into, optional)] id: MaybeProp<String>,
56    #[prop(into, optional)] style: Signal<Style>,
57) -> impl IntoView {
58    let max_value = max.get().unwrap_or(100.0);
59    let progress_variant = variant.get().unwrap_or_default();
60    let size_class = match size.get().unwrap_or_default().as_str() {
61        "sm" => "h-2",
62        "lg" => "h-4",
63        "xl" => "h-6",
64        _ => "h-3",
65    };
66    
67    let progress_percentage = Signal::derive(move || {
68        let val = value.get();
69        let max_val = max_value;
70        if max_val <= 0.0 { 0.0 } else { (val / max_val * 100.0).clamp(0.0, 100.0) }
71    });
72
73    let indicator_class = Signal::derive(move || {
74        let base_class = PROGRESS_INDICATOR_CLASS;
75        let variant_class = progress_variant.indicator_class();
76        let animation_class = if animated.get() { "animate-pulse" } else { "" };
77        format!("{} {} {}", base_class, variant_class, animation_class)
78    });
79
80    let computed_class = Signal::derive(move || {
81        format!("{} {} {}", PROGRESS_CLASS, size_class, class.get().unwrap_or_default())
82    });
83
84    view! {
85        <div class="w-full space-y-2">
86            <div
87                class=move || computed_class.get()
88                id=move || id.get().unwrap_or_default()
89                style=move || style.get().to_string()
90                role="progressbar"
91                aria-valuenow={move || value.get()}
92                aria-valuemin="0"
93                aria-valuemax={max_value}
94            >
95                <div
96                    class=indicator_class
97                    style={move || format!("width: {}%", progress_percentage.get())}
98                />
99            </div>
100            <Show
101                when=move || show_label.get()
102                fallback=|| view! { <div class="hidden"></div> }
103            >
104                <div class="flex justify-between text-sm text-muted-foreground">
105                    <span>"Progress"</span>
106                    <span>{move || format!("{:.0}%", progress_percentage.get())}</span>
107                </div>
108            </Show>
109        </div>
110    }
111}
112
113// Progress Root with Context
114#[derive(Clone, Copy)]
115pub struct ProgressContextValue {
116    pub value: RwSignal<f64>,
117    pub max: RwSignal<f64>,
118    pub variant: RwSignal<ProgressVariant>,
119    pub animated: RwSignal<bool>,
120    pub show_label: RwSignal<bool>,
121}
122
123#[component]
124pub fn ProgressRoot(
125    #[prop(into, optional)] value: Signal<f64>,
126    #[prop(into, optional)] max: MaybeProp<f64>,
127    #[prop(into, optional)] variant: MaybeProp<ProgressVariant>,
128    #[prop(into, optional)] animated: Signal<bool>,
129    #[prop(into, optional)] show_label: Signal<bool>,
130    #[prop(optional)] children: Option<Children>,
131) -> impl IntoView {
132    let value_signal = RwSignal::new(value.get());
133    let max_signal = RwSignal::new(max.get().unwrap_or(100.0));
134    let variant_signal = RwSignal::new(variant.get().unwrap_or_default());
135    let animated_signal = RwSignal::new(animated.get());
136    let show_label_signal = RwSignal::new(show_label.get());
137
138    // Update signals when props change
139    Effect::new(move |_| {
140        value_signal.set(value.get());
141    });
142    Effect::new(move |_| {
143        max_signal.set(max.get().unwrap_or(100.0));
144    });
145    Effect::new(move |_| {
146        variant_signal.set(variant.get().unwrap_or_default());
147    });
148    Effect::new(move |_| {
149        animated_signal.set(animated.get());
150    });
151    Effect::new(move |_| {
152        show_label_signal.set(show_label.get());
153    });
154
155    let context_value = ProgressContextValue {
156        value: value_signal,
157        max: max_signal,
158        variant: variant_signal,
159        animated: animated_signal,
160        show_label: show_label_signal,
161    };
162
163    provide_context(context_value);
164
165    view! {
166        <div class="w-full">
167            {children.map(|c| c())}
168        </div>
169    }
170}
171
172// Progress Indicator (uses context)
173#[component]
174pub fn ProgressIndicator(
175    #[prop(into, optional)] class: MaybeProp<String>,
176    #[prop(into, optional)] id: MaybeProp<String>,
177    #[prop(into, optional)] style: Signal<Style>,
178) -> impl IntoView {
179    let ctx = expect_context::<ProgressContextValue>();
180    
181    let progress_percentage = Signal::derive(move || {
182        let val = ctx.value.get();
183        let max_val = ctx.max.get();
184        if max_val <= 0.0 { 0.0 } else { (val / max_val * 100.0).clamp(0.0, 100.0) }
185    });
186
187    let indicator_class = Signal::derive(move || {
188        let base_class = PROGRESS_INDICATOR_CLASS;
189        let variant_class = ctx.variant.get().indicator_class();
190        let animation_class = if ctx.animated.get() { "animate-pulse" } else { "" };
191        format!("{} {} {} {}", base_class, variant_class, animation_class, class.get().unwrap_or_default())
192    });
193
194    view! {
195        <div
196            class=indicator_class
197            id=move || id.get().unwrap_or_default()
198            style={move || format!("width: {}%; {}", progress_percentage.get(), style.get().to_string())}
199        />
200    }
201}
202
203// Progress Label (uses context)
204#[component]
205pub fn ProgressLabel(
206    #[prop(into, optional)] class: MaybeProp<String>,
207    #[prop(into, optional)] id: MaybeProp<String>,
208    #[prop(into, optional)] style: Signal<Style>,
209) -> impl IntoView {
210    let ctx = expect_context::<ProgressContextValue>();
211    
212    let progress_percentage = Signal::derive(move || {
213        let val = ctx.value.get();
214        let max_val = ctx.max.get();
215        if max_val <= 0.0 { 0.0 } else { (val / max_val * 100.0).clamp(0.0, 100.0) }
216    });
217
218    let computed_class = Signal::derive(move || {
219        format!("flex justify-between text-sm text-muted-foreground {}", class.get().unwrap_or_default())
220    });
221
222    view! {
223        <div
224            class=move || computed_class.get()
225            id=move || id.get().unwrap_or_default()
226            style=move || style.get().to_string()
227        >
228            <span>"Progress"</span>
229            <span>{move || format!("{:.0}%", progress_percentage.get())}</span>
230        </div>
231    }
232}