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");
}
}