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#[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 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#[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#[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}