use std::fmt::{Display, Formatter};
use leptos::{ev::MouseEvent, *};
use leptos_router::{State, ToHref, A};
use leptos_use::on_click_outside;
use crate::{icon::Icon, prelude::Consumer, OptionalMaybeSignal};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ButtonVariant {
Flat,
Outlined,
#[default]
Filled,
}
impl ButtonVariant {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Flat => "flat",
Self::Outlined => "outlined",
Self::Filled => "filled",
}
}
}
impl Display for ButtonVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ButtonColor {
#[default]
Primary,
Secondary,
Success,
Info,
Warn,
Danger,
}
impl ButtonColor {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Primary => "primary",
Self::Secondary => "secondary",
Self::Success => "success",
Self::Info => "info",
Self::Warn => "warn",
Self::Danger => "danger",
}
}
}
impl Display for ButtonColor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ButtonSize {
Small,
#[default]
Normal,
Big,
}
impl ButtonSize {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Small => "small",
Self::Normal => "normal",
Self::Big => "big",
}
}
}
impl Display for ButtonSize {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[component]
pub fn Button(
#[prop(into)] on_click: Consumer<MouseEvent>,
#[prop(into, optional)] variant: OptionalMaybeSignal<ButtonVariant>,
#[prop(into, optional)] color: OptionalMaybeSignal<ButtonColor>,
#[prop(into, optional)] size: OptionalMaybeSignal<ButtonSize>,
#[prop(into, optional)] disabled: OptionalMaybeSignal<bool>,
#[prop(into, optional)] active: OptionalMaybeSignal<bool>,
#[prop(into, optional)] variations: OptionalMaybeSignal<View>,
#[prop(into, optional)] id: Option<AttributeValue>,
#[prop(into, optional)] class: OptionalMaybeSignal<String>,
#[prop(into, optional)] style: Option<AttributeValue>,
children: Children,
) -> impl IntoView {
let has_variations = variations.0.as_ref().is_some();
let variations = move || {
if has_variations {
let (dropdown_open, set_dropdown_open): (ReadSignal<bool>, WriteSignal<bool>) =
create_signal(false);
let dropdown_trigger = create_node_ref::<html::Div>();
let _ = on_click_outside(dropdown_trigger, move |_| {
set_dropdown_open.set(false);
});
Some(
view! {
<div class="dropdown-trigger" node_ref=dropdown_trigger on:click=move |e| {
if !disabled.get_untracked() {
set_dropdown_open.update(|it| *it = !*it);
e.stop_propagation();
}
}>
{ move || {
let icon = match dropdown_open.get() {
true => icondata::BsCaretUp,
false => icondata::BsCaretDown,
};
view! {
<Icon icon=icon/>
}
}}
</div>
<div class="dropdown" class:active=move || dropdown_open.get() && !disabled.get()>
{ variations.get() }
</div>
}
.into_view(),
)
} else {
None
}
};
view! {
<button
id=id
class=move || class.0.as_ref().map(|it| format!("{} leptonic-btn", it.get())).unwrap_or_else(|| "leptonic-btn".to_string())
class:has-variations=has_variations
class:active=move || active.get()
data-variant=move || variant.get().as_str()
data-color=move || color.get().as_str()
data-size=move || size.get().as_str()
style=style
aria-disabled=move || match disabled.get() {
true => "true",
false => "false",
}
on:click=move |e| {
if !disabled.get_untracked() {
e.stop_propagation();
on_click.consume(e);
}
}
>
<div class="name">
{ children() }
</div>
{ variations }
</button>
}
}
#[component]
pub fn ButtonGroup(children: Children) -> impl IntoView {
view! {
<leptonic-btn-group>
{ children() }
</leptonic-btn-group>
}
}
#[component]
pub fn ButtonWrapper(children: Children) -> impl IntoView {
view! {
<leptonic-btn-wrapper>
{ children() }
</leptonic-btn-wrapper>
}
}
#[component]
#[allow(clippy::needless_pass_by_value)] pub fn LinkButton<H>(
href: H,
#[prop(into, optional)] variant: OptionalMaybeSignal<ButtonVariant>,
#[prop(into, optional)] color: OptionalMaybeSignal<ButtonColor>,
#[prop(into, optional)] size: OptionalMaybeSignal<ButtonSize>,
#[prop(into, optional)] disabled: OptionalMaybeSignal<bool>,
#[prop(into, optional)] active: OptionalMaybeSignal<bool>,
#[prop(into, optional)] id: Option<AttributeValue>,
#[prop(into, optional)] class: OptionalMaybeSignal<String>,
#[prop(into, optional)] style: Option<AttributeValue>,
#[allow(unused)] #[prop(into, optional)]
title: Option<AttributeValue>, #[prop(optional)]
exact: bool,
#[prop(optional)]
state: Option<State>,
#[prop(optional)]
replace: bool,
children: Children,
) -> impl IntoView
where
H: ToHref + 'static,
{
view! {
<leptonic-link
id=id
class=move || {
let user = class.get();
let active = active.get().then_some("active").unwrap_or_default();
format!("leptonic-btn {user} {active}")
}
data-variant=move || variant.get().as_str()
data-color=move || color.get().as_str()
data-size=move || size.get().as_str()
aria-disabled=move || match disabled.get() {
true => "true",
false => "false",
}
style=style
>
<A href=href exact=exact state=state.unwrap_or_default() replace=replace>
<div class="name">
{ children() }
</div>
</A>
</leptonic-link>
}
}