leptos_shadcn_switch/
default.rs

1use leptos::{ev::MouseEvent, prelude::*};
2use leptos_style::Style;
3
4const SWITCH_CLASS: &str = "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input";
5const SWITCH_THUMB_CLASS: &str = "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0";
6
7#[derive(Clone, Copy, PartialEq, Debug)]
8pub enum SwitchVariant {
9    Default,
10    Success,
11    Warning,
12    Destructive,
13    Info,
14}
15
16impl Default for SwitchVariant {
17    fn default() -> Self {
18        SwitchVariant::Default
19    }
20}
21
22impl From<String> for SwitchVariant {
23    fn from(s: String) -> Self {
24        match s.as_str() {
25            "success" => SwitchVariant::Success,
26            "warning" => SwitchVariant::Warning,
27            "destructive" => SwitchVariant::Destructive,
28            "info" => SwitchVariant::Info,
29            _ => SwitchVariant::Default,
30        }
31    }
32}
33
34impl SwitchVariant {
35    fn checked_class(&self) -> &'static str {
36        match self {
37            SwitchVariant::Default => "data-[state=checked]:bg-primary",
38            SwitchVariant::Success => "data-[state=checked]:bg-green-500",
39            SwitchVariant::Warning => "data-[state=checked]:bg-yellow-500",
40            SwitchVariant::Destructive => "data-[state=checked]:bg-red-500",
41            SwitchVariant::Info => "data-[state=checked]:bg-blue-500",
42        }
43    }
44}
45
46#[derive(Clone, Copy, PartialEq, Debug)]
47pub enum SwitchSize {
48    Sm,
49    Md,
50    Lg,
51}
52
53impl Default for SwitchSize {
54    fn default() -> Self {
55        SwitchSize::Md
56    }
57}
58
59impl From<String> for SwitchSize {
60    fn from(s: String) -> Self {
61        match s.as_str() {
62            "sm" => SwitchSize::Sm,
63            "lg" => SwitchSize::Lg,
64            _ => SwitchSize::Md,
65        }
66    }
67}
68
69impl SwitchSize {
70    fn switch_class(&self) -> &'static str {
71        match self {
72            SwitchSize::Sm => "h-4 w-7",
73            SwitchSize::Md => "h-6 w-11",
74            SwitchSize::Lg => "h-8 w-14",
75        }
76    }
77    
78    fn thumb_class(&self) -> &'static str {
79        match self {
80            SwitchSize::Sm => "h-3 w-3 data-[state=checked]:translate-x-3",
81            SwitchSize::Md => "h-5 w-5 data-[state=checked]:translate-x-5",
82            SwitchSize::Lg => "h-6 w-6 data-[state=checked]:translate-x-6",
83        }
84    }
85}
86
87#[component]
88pub fn Switch(
89    #[prop(into, optional)] checked: Signal<bool>,
90    #[prop(into, optional)] on_change: Option<Callback<bool>>,
91    #[prop(into, optional)] variant: MaybeProp<SwitchVariant>,
92    #[prop(into, optional)] size: MaybeProp<SwitchSize>,
93    #[prop(into, optional)] disabled: Signal<bool>,
94    #[prop(into, optional)] animated: Signal<bool>,
95    #[prop(into, optional)] class: MaybeProp<String>,
96    #[prop(into, optional)] id: MaybeProp<String>,
97    #[prop(into, optional)] style: Signal<Style>,
98) -> impl IntoView {
99    let switch_variant = variant.get().unwrap_or_default();
100    let switch_size = size.get().unwrap_or_default();
101    
102    let handle_change = {
103        let on_change = on_change.clone();
104        move |_event: MouseEvent| {
105            if let Some(callback) = &on_change {
106                let new_value = !checked.get();
107                callback.run(new_value);
108            }
109        }
110    };
111
112    let switch_class = switch_size.switch_class();
113    let thumb_class = switch_size.thumb_class();
114    let variant_class = switch_variant.checked_class();
115    let animation_class = if animated.get() { "transition-all duration-200" } else { "transition-colors" };
116    
117    let computed_class = Signal::derive(move || {
118        format!("{} {} {} {} {}", SWITCH_CLASS, switch_class, variant_class, animation_class, class.get().unwrap_or_default())
119    });
120
121    let computed_thumb_class = Signal::derive(move || {
122        format!("{} {} {}", SWITCH_THUMB_CLASS, thumb_class, animation_class)
123    });
124
125    let state_attr = Signal::derive(move || {
126        if checked.get() { "checked" } else { "unchecked" }
127    });
128
129    view! {
130        <button
131            r#type="button"
132            role="switch"
133            aria-checked=move || checked.get()
134            data-state=move || state_attr.get()
135            disabled=move || disabled.get()
136            class=move || computed_class.get()
137            id=move || id.get().unwrap_or_default()
138            style=move || style.get().to_string()
139            on:click=handle_change
140        >
141            <span class=move || computed_thumb_class.get() data-state=move || state_attr.get() />
142        </button>
143    }
144}
145
146// Switch Root with Context
147#[derive(Clone, Copy)]
148pub struct SwitchContextValue {
149    pub checked: RwSignal<bool>,
150    pub disabled: RwSignal<bool>,
151    pub variant: RwSignal<SwitchVariant>,
152    pub size: RwSignal<SwitchSize>,
153    pub animated: RwSignal<bool>,
154}
155
156#[component]
157pub fn SwitchRoot(
158    #[prop(into, optional)] checked: Signal<bool>,
159    #[prop(into, optional)] disabled: Signal<bool>,
160    #[prop(into, optional)] variant: MaybeProp<SwitchVariant>,
161    #[prop(into, optional)] size: MaybeProp<SwitchSize>,
162    #[prop(into, optional)] animated: Signal<bool>,
163    #[prop(optional)] children: Option<Children>,
164) -> impl IntoView {
165    let checked_signal = RwSignal::new(checked.get());
166    let disabled_signal = RwSignal::new(disabled.get());
167    let variant_signal = RwSignal::new(variant.get().unwrap_or_default());
168    let size_signal = RwSignal::new(size.get().unwrap_or_default());
169    let animated_signal = RwSignal::new(animated.get());
170
171    // Update signals when props change
172    Effect::new(move |_| {
173        checked_signal.set(checked.get());
174    });
175    Effect::new(move |_| {
176        disabled_signal.set(disabled.get());
177    });
178    Effect::new(move |_| {
179        variant_signal.set(variant.get().unwrap_or_default());
180    });
181    Effect::new(move |_| {
182        size_signal.set(size.get().unwrap_or_default());
183    });
184    Effect::new(move |_| {
185        animated_signal.set(animated.get());
186    });
187
188    let context_value = SwitchContextValue {
189        checked: checked_signal,
190        disabled: disabled_signal,
191        variant: variant_signal,
192        size: size_signal,
193        animated: animated_signal,
194    };
195
196    provide_context(context_value);
197
198    view! {
199        <div class="flex items-center space-x-2">
200            {children.map(|c| c())}
201        </div>
202    }
203}
204
205// Switch Thumb (uses context)
206#[component]
207pub fn SwitchThumb(
208    #[prop(into, optional)] class: MaybeProp<String>,
209    #[prop(into, optional)] id: MaybeProp<String>,
210    #[prop(into, optional)] style: Signal<Style>,
211) -> impl IntoView {
212    let ctx = expect_context::<SwitchContextValue>();
213    
214    let thumb_class = ctx.size.get().thumb_class();
215    let animation_class = if ctx.animated.get() { "transition-all duration-200" } else { "transition-transform" };
216    let state_attr = Signal::derive(move || {
217        if ctx.checked.get() { "checked" } else { "unchecked" }
218    });
219    
220    let computed_class = Signal::derive(move || {
221        format!("{} {} {} {}", SWITCH_THUMB_CLASS, thumb_class, animation_class, class.get().unwrap_or_default())
222    });
223
224    view! {
225        <span
226            class=move || computed_class.get()
227            id=move || id.get().unwrap_or_default()
228            style=move || style.get().to_string()
229            data-state={state_attr}
230        />
231    }
232}
233
234// Switch Label
235#[component]
236pub fn SwitchLabel(
237    #[prop(into, optional)] class: MaybeProp<String>,
238    #[prop(into, optional)] id: MaybeProp<String>,
239    #[prop(into, optional)] style: Signal<Style>,
240    #[prop(optional)] children: Option<Children>,
241) -> impl IntoView {
242    let computed_class = Signal::derive(move || {
243        format!("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 {}", class.get().unwrap_or_default())
244    });
245
246    view! {
247        <label
248            class=move || computed_class.get()
249            id=move || id.get().unwrap_or_default()
250            style=move || style.get().to_string()
251        >
252            {children.map(|c| c())}
253        </label>
254    }
255}