Current section
Files
Jump to
Current section
Files
src/ui/spinner.gleam
// MIT License
// Copyright (c) 2026 Koncreate
// See LICENSE for details
import lustre/attribute.{type Attribute, attribute, class, style}
import lustre/element.{type Element}
import lustre/element/html.{div, span}
import ui/size as size_helpers
pub type Variant {
Default
Muted
}
pub type Size {
Small
Medium
Large
}
/// Create a spinner with optional size and variant attributes.
/// The spinner includes inline styles for dimensions to work without CSS.
pub fn spinner(attributes: List(Attribute(a))) -> Element(a) {
div(
[
class(
"animate-spin inline-block rounded-full border-2 border-primary border-t-transparent",
),
attribute("role", "status"),
attribute("aria-live", "polite"),
attribute("aria-busy", "true"),
style("width", "24px"),
style("height", "24px"),
..attributes
],
[
span(
[
class("block h-full w-full rounded-full bg-primary/10"),
attribute("aria-hidden", "true"),
],
[],
),
],
)
}
/// Create a spinner with explicit size control.
/// Includes inline styles for dimensions to work without CSS.
pub fn spinner_with_size(
size: Size,
attributes: List(Attribute(a)),
) -> Element(a) {
let dimensions = size_to_inline_style(size)
div(
[
class(
"animate-spin inline-block rounded-full border-2 border-primary border-t-transparent",
),
attribute("role", "status"),
attribute("aria-live", "polite"),
attribute("aria-busy", "true"),
style("width", dimensions),
style("height", dimensions),
..attributes
],
[
span(
[
class("block h-full w-full rounded-full bg-primary/10"),
attribute("aria-hidden", "true"),
],
[],
),
],
)
}
fn size_to_inline_style(size: Size) -> String {
case size {
Small -> "16px"
Medium -> "24px"
Large -> "32px"
}
}
pub fn variant(v: Variant) -> Attribute(a) {
case v {
Default -> class("border-primary")
Muted -> class("border-muted-foreground")
}
}
pub fn size(s: Size) -> Attribute(a) {
case s {
Small -> class(size_helpers.icon_class(size_helpers.Small))
Medium -> class(size_helpers.icon_class(size_helpers.Medium))
Large -> class("h-8 w-8 border-[3px]")
}
}
pub fn border() -> Attribute(a) {
class("rounded-full border-current border-t-transparent")
}
pub fn aria_label(label: String) -> Attribute(a) {
attribute("aria-label", label)
}
pub fn aria_busy(busy: Bool) -> Attribute(a) {
attribute("aria-busy", case busy {
True -> "true"
False -> "false"
})
}
pub fn loading_text(text: String) -> Attribute(a) {
attribute("aria-valuetext", text)
}