1use super::display::Tooltip;
3use super::icons::{Icon, IconName};
4use leptos::ev::MouseEvent;
5use leptos::prelude::*;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum ButtonVariant {
10 #[default]
12 Primary,
13 Secondary,
15 Ghost,
17 Outline,
19 Danger,
21 DangerSoft,
23}
24
25impl ButtonVariant {
26 const fn class(self) -> &'static str {
27 match self {
28 ButtonVariant::Primary => "btn-primary",
29 ButtonVariant::Secondary => "btn-secondary",
30 ButtonVariant::Ghost => "btn-ghost",
31 ButtonVariant::Outline => "btn-outline",
32 ButtonVariant::Danger => "btn-danger",
33 ButtonVariant::DangerSoft => "btn-danger-soft",
34 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum ButtonSize {
41 Xs,
42 Sm,
43 #[default]
44 Md,
45 Lg,
46}
47
48impl ButtonSize {
49 const fn class(self) -> &'static str {
50 match self {
51 ButtonSize::Xs => "btn-xs",
52 ButtonSize::Sm => "btn-sm",
53 ButtonSize::Md => "",
54 ButtonSize::Lg => "btn-lg",
55 }
56 }
57
58 const fn icon_px(self) -> u32 {
59 match self {
60 ButtonSize::Xs => 14,
61 ButtonSize::Sm | ButtonSize::Md => 16,
62 ButtonSize::Lg => 18,
63 }
64 }
65}
66
67pub fn button_class(variant: ButtonVariant, size: ButtonSize) -> String {
69 format!("btn {} {}", size.class(), variant.class())
70}
71
72#[component]
80pub fn Button(
81 #[prop(optional, into)] variant: Signal<ButtonVariant>,
82 #[prop(optional)] size: ButtonSize,
83 #[prop(optional, into)] icon: MaybeProp<IconName>,
84 #[prop(optional, into)] loading: Signal<bool>,
85 #[prop(optional, into)] disabled: Signal<bool>,
86 #[prop(default = "button")] r#type: &'static str,
87 #[prop(optional, into)] class: Signal<String>,
88 #[prop(optional, into)] onclick: Option<Callback<MouseEvent>>,
89 children: Children,
90) -> impl IntoView {
91 let px = size.icon_px();
92 view! {
93 <button
94 class=move || format!("{} {}", button_class(variant.get(), size), class.get())
95 type=r#type
96 disabled=move || disabled.get() || loading.get()
97 aria-busy=move || loading.get().then_some("true")
98 on:click=move |e| {
99 if let Some(handler) = onclick {
100 handler.run(e);
101 }
102 }
103 >
104 <span class="btn-label inline-flex items-center gap-[inherit]">
105 {move || icon.get().map(|icon| view! { <Icon name=icon size=px/> })}
106 {children()}
107 </span>
108 <Show when=move || loading.get()>
109 <span class="btn-spinner">
110 <Icon name=IconName::LoaderCircle size=px/>
111 </span>
112 </Show>
113 </button>
114 }
115}
116
117#[component]
120pub fn IconButton(
121 #[prop(into)] icon: Signal<IconName>,
122 #[prop(into)] label: Signal<String>,
123 #[prop(default = ButtonVariant::Ghost.into(), into)] variant: Signal<ButtonVariant>,
124 #[prop(optional)] size: ButtonSize,
125 #[prop(optional, into)] disabled: Signal<bool>,
126 #[prop(optional, into)] pressed: Signal<Option<bool>>,
127 #[prop(optional, into)] shortcut: MaybeProp<&'static str>,
128 #[prop(default = "top".into(), into)] tooltip_placement: Signal<String>,
129 #[prop(default = "center".into(), into)] tooltip_align: Signal<String>,
130 #[prop(optional, into)] class: Signal<String>,
131 #[prop(optional, into)] onclick: Option<Callback<MouseEvent>>,
132) -> impl IntoView {
133 let px = size.icon_px();
134 view! {
135 <Tooltip tip=label shortcut=shortcut placement=tooltip_placement align=tooltip_align>
136 <button
137 class=move || {
138 format!("{} btn-icon {}", button_class(variant.get(), size), class.get())
139 }
140 type="button"
141 disabled=move || disabled.get()
142 aria-label=move || label.get()
143 aria-pressed=move || pressed.get().map(|p| if p { "true" } else { "false" })
144 on:click=move |e| {
145 if let Some(handler) = onclick {
146 handler.run(e);
147 }
148 }
149 >
150 {move || view! { <Icon name=icon.get() size=px/> }}
151 </button>
152 </Tooltip>
153 }
154}
155
156#[component]
172pub fn Checkbox(
173 #[prop(into)] checked: Signal<bool>,
174 #[prop(into)] onchange: Callback<bool>,
175 #[prop(optional, into)] danger: Signal<bool>,
176 #[prop(optional, into)] label: Option<String>,
177) -> impl IntoView {
178 view! {
179 <span class=move || {
180 if danger.get() { "ygg-cb ygg-cb-danger" } else { "ygg-cb" }
181 }>
182 <input
183 type="checkbox"
184 prop:checked=move || checked.get()
185 aria-label=label
186 on:change=move |e| onchange.run(event_target_checked(&e))
187 />
188 <svg class="ygg-cb-mark" viewBox="0 0 16 16">
189 <path class="ygg-cb-check" d="M3.5 8.5l3 3 6-6.5"></path>
190 </svg>
191 </span>
192 }
193}
194
195#[component]
200pub fn Radio(
201 #[prop(into)] name: String,
202 #[prop(into)] value: String,
203 #[prop(into)] checked: Signal<bool>,
204 #[prop(into)] onchange: Callback<String>,
205 #[prop(optional, into)] disabled: Signal<bool>,
206) -> impl IntoView {
207 let current = value.clone();
208 view! {
209 <input
210 class="ygg-radio"
211 type="radio"
212 name=name
213 value=value
214 prop:checked=move || checked.get()
215 disabled=move || disabled.get()
216 on:change=move |_| onchange.run(current.clone())
217 />
218 }
219}