AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/ui/src/kit/button.rs

Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson

use crate::dom::El;
use crate::html;

/// Visual weight of a [`button`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Variant {
    Primary,
    Secondary,
    Outline,
    Ghost,
    Destructive,
    Link,
}

/// Height and padding of a [`button`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Size {
    Sm,
    Md,
    Lg,
    Icon,
}

const BASE: &str = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium select-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg disabled:pointer-events-none disabled:opacity-50";

fn variant_class(v: Variant) -> &'static str {
    match v {
        Variant::Primary => "bg-accent text-bg shadow-sm hover:bg-accent/90",
        Variant::Secondary => "bg-panel text-fg hover:bg-panel-2",
        Variant::Outline => "border border-border bg-transparent text-fg hover:bg-panel",
        Variant::Ghost => "text-fg hover:bg-panel",
        Variant::Destructive => "bg-danger text-bg shadow-sm hover:bg-danger/90",
        Variant::Link => "text-accent underline-offset-4 hover:underline",
    }
}

fn size_class(s: Size) -> &'static str {
    match s {
        Size::Sm => "h-8 px-3 text-xs",
        Size::Md => "h-9 px-4",
        Size::Lg => "h-10 px-6",
        Size::Icon => "size-9",
    }
}

/// A `<button type="button">`; set `.typ("submit")` inside forms.
pub fn button(variant: Variant, size: Size) -> El {
    html::button()
        .typ("button")
        .class(super::cn(&[BASE, variant_class(variant), size_class(size)]))
}

/// An icon-only button. `label` becomes its accessible name.
pub fn icon_button(variant: Variant, label: &str, icon: El) -> El {
    button(variant, Size::Icon)
        .attr("aria-label", label.to_string())
        .attr("title", label.to_string())
        .child(icon)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testing;

    #[test]
    fn button_has_type_and_classes() {
        testing::render(|| button(Variant::Primary, Size::Md).child("Save"));
        let b = testing::get("button:text(Save)");
        assert_eq!(testing::attr(b, "type").as_deref(), Some("button"));
        assert!(testing::attr(b, "class").unwrap().contains("bg-accent"));
    }

    #[test]
    fn icon_button_is_labelled() {
        testing::render(|| icon_button(Variant::Ghost, "Close", crate::kit::icons::x()));
        assert!(testing::find("button[aria-label=Close]").is_some());
    }
}