AkurAI Build
Menu

Public repository / master

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

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

use super::cn;
use crate::dom::{El, IntoView};
use crate::html::{div, h3, p, span};

/// Semantic colour of a badge or alert.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tone {
    Neutral,
    Accent,
    Ok,
    Warn,
    Danger,
}

fn tone_class(t: Tone) -> &'static str {
    match t {
        Tone::Neutral => "border-border bg-panel text-fg",
        Tone::Accent => "border-accent/40 bg-accent/15 text-accent",
        Tone::Ok => "border-ok/40 bg-ok/15 text-ok",
        Tone::Warn => "border-warn/40 bg-warn/15 text-warn",
        Tone::Danger => "border-danger/40 bg-danger/15 text-danger",
    }
}

/// A small status label.
pub fn badge(tone: Tone) -> El {
    span().class(cn(&[
        "inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 text-xs font-medium",
        tone_class(tone),
    ]))
}

/// A callout box with an optional title.
pub fn alert(tone: Tone, title: Option<&str>, body: impl IntoView) -> El {
    div()
        .role(if tone == Tone::Danger {
            "alert"
        } else {
            "status"
        })
        .class(cn(&[
            "grid gap-1 rounded-md border p-3 text-sm",
            tone_class(tone),
        ]))
        .child((
            title.map(|t| p().class("font-medium").child(t.to_string())),
            div().class("text-fg/80").child(body),
        ))
}

/// Initials on a coloured square; `color` is any CSS colour.
pub fn avatar(name: &str, color: &str) -> El {
    let initials: String = name
        .split_whitespace()
        .filter_map(|w| w.chars().next())
        .take(2)
        .collect::<String>()
        .to_uppercase();
    span()
        .attr("aria-hidden", "true")
        .class("inline-flex size-8 shrink-0 select-none items-center justify-center rounded-md text-xs font-semibold text-white")
        .style(format!("background-color:{color}"))
        .child(initials)
}

/// A 1px rule (`role="separator"`).
pub fn separator() -> El {
    div()
        .role("separator")
        .class("h-px w-full shrink-0 bg-border")
}

/// A loading placeholder block; size it with classes.
pub fn skeleton() -> El {
    div().class("animate-pulse rounded-md bg-panel")
}

/// A spinning indicator with an accessible label.
pub fn spinner(label: &str) -> El {
    span()
        .role("status")
        .attr("aria-label", label.to_string())
        .class(
        "inline-block size-4 animate-spin rounded-sm border-2 border-current border-t-transparent",
    )
}

/// A centred placeholder for empty lists.
pub fn empty_state(icon: El, title: &str, text: &str) -> El {
    div()
        .class("flex flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border p-8 text-center")
        .child((
            div().class("text-muted").child(icon.class("size-6")),
            h3().class("text-sm font-medium").child(title.to_string()),
            p().class("max-w-xs text-sm text-muted").child(text.to_string()),
        ))
}

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

    #[test]
    fn avatar_shows_initials() {
        testing::render(|| avatar("anna maria", "#c0392b"));
        assert_eq!(testing::text(testing::get("span")), "AM");
    }
}