///|
priv enum ProgressState {
Indeterminate
Progressing
Complete
}
///|
const ProgressBaseStyle : String = "display:flex;width:100%;min-width:0;flex-wrap:wrap;align-items:center;gap:0.75rem"
///|
const ProgressTrackStyle : String = "position:relative;display:flex;width:100%;height:0.5rem;align-items:center;overflow-x:hidden;border-radius:9999px;background:color-mix(in oklab,var(--rui-primary,oklch(0.205 0 0)) 20%,transparent)"
///|
const ProgressIndicatorStyle : String = "height:100%;flex:none;background:var(--rui-primary,oklch(0.205 0 0));transition:width 150ms ease"
///|
const ProgressIndeterminateStyle : String = "position:absolute;inset-block:0;inset-inline-start:-40%;inline-size:40%;animation:rui-progress-indeterminate 1.4s ease-in-out infinite"
///|
const ProgressIndeterminateCss : String =
#|@keyframes rui-progress-indeterminate {
#| from { inset-inline-start:-40%; }
#| to { inset-inline-start:100%; }
#|}
#|@media (prefers-reduced-motion:reduce) {
#| [data-slot="progress-indicator"][data-state="indeterminate"] {
#| animation:none !important;
#| inset-inline-start:30%;
#| }
#|}
#|
///|
#warnings("-alert_xss_vulnerable")
fn progress_indeterminate_stylesheet() -> @html.Html {
// Static library CSS is required for keyframes; no caller input reaches it.
let attrs = @html.Attrs::build()
.data_set("rui-style", "progress-indeterminate")
.inner_html(ProgressIndeterminateCss)
@html.style_tag(attrs~, ([] : Array[@html.Html]))
}
///|
const ProgressLabelStyle : String = "font-size:0.875rem;line-height:1.25rem;font-weight:500"
///|
const ProgressValueStyle : String = "margin-inline-start:auto;color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:0.875rem;line-height:1.25rem;font-variant-numeric:tabular-nums"
///|
fn progress_effective_max(min : Int, max : Int) -> Int {
if max < min {
min
} else {
max
}
}
///|
fn progress_clamped_value(value : Int, min : Int, max : Int) -> Int {
let max = progress_effective_max(min, max)
if max == min {
min
} else if value < min {
min
} else if value > max {
max
} else {
value
}
}
///|
fn progress_percentage(value : Int, min : Int, max : Int) -> Int {
let max = progress_effective_max(min, max)
if max == min {
100
} else {
let clamped = progress_clamped_value(value, min, max)
(clamped - min) * 100 / (max - min)
}
}
///|
fn progress_state(value : Int?, min : Int, max : Int) -> ProgressState {
if value is Some(value) {
let max = progress_effective_max(min, max)
if progress_clamped_value(value, min, max) == max {
Complete
} else {
Progressing
}
} else {
Indeterminate
}
}
///|
fn progress_state_attrs(
attrs : @html.Attrs?,
state : ProgressState,
slot : String,
) -> @html.Attrs {
let element_attrs = ui_attrs(attrs).data_set("slot", slot)
match state {
Indeterminate =>
element_attrs
.data_set("state", "indeterminate")
.data_set("indeterminate", "")
Progressing =>
element_attrs.data_set("state", "progressing").data_set("progressing", "")
Complete =>
element_attrs.data_set("state", "complete").data_set("complete", "")
}
}
///|
fn progress_indicator_width(value : Int?, min : Int, max : Int) -> String {
if value is Some(value) {
"width:\{progress_percentage(value, min, max)}%"
} else {
ProgressIndeterminateStyle
}
}
///|
fn progress_aria_text(value : Int?, min : Int, max : Int) -> String {
if value is Some(value) {
"\{progress_percentage(value, min, max)}%"
} else {
"indeterminate progress"
}
}
///|
fn progress_display_text(value : Int?, min : Int, max : Int) -> String {
if value is Some(value) {
"\{progress_percentage(value, min, max)}%"
} else {
""
}
}
///|
/// Render an ARIA progress group and append Vega's track and
/// indicator automatically after optional label/value children.
///
/// Determinate values are clamped to `min...max`. A `max` below `min` is
/// normalized to `min`. `None` renders the indeterminate state and omits
/// `aria-valuenow`.
pub fn progress(
value? : Int,
min? : Int = 0,
max? : Int = 100,
aria_label? : String,
aria_value_text? : String,
labelledby? : String,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children? : Array[@html.Html] = [],
) -> @html.Html {
let state = progress_state(value, min, max)
let effective_max = progress_effective_max(min, max)
let element_attrs = progress_state_attrs(attrs, state, "progress")
.role("progressbar")
.aria_valuemin("\{min}")
.aria_valuemax("\{effective_max}")
if value is Some(value) {
ignore(
element_attrs.aria_valuenow("\{progress_clamped_value(value, min, max)}"),
)
}
if aria_label is Some(aria_label) {
ignore(element_attrs.aria_label(aria_label))
}
if labelledby is Some(labelledby) {
ignore(element_attrs.aria_labelledby(labelledby))
}
ignore(
element_attrs.aria_valuetext(
aria_value_text.unwrap_or(progress_aria_text(value, min, max)),
),
)
let root_children : Array[@html.Html] = []
for child in children {
root_children.push(child)
}
root_children.push(
progress_track(value?, min~, max~, [progress_indicator(value?, min~, max~)]),
)
@html.div(
style=ui_styles(
[UiBoxSizing, UiFontSans, UiTextRendering, ProgressBaseStyle],
style,
),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
root_children,
)
}
///|
pub fn[C : @html.IsChildren] progress_track(
value? : Int,
min? : Int = 0,
max? : Int = 100,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.div(
style=ui_styles([UiBoxSizing, ProgressTrackStyle], style),
id?,
class?,
title?,
hidden?,
attrs=progress_state_attrs(
attrs,
progress_state(value, min, max),
"progress-track",
),
children,
)
}
///|
pub fn progress_indicator(
value? : Int,
min? : Int = 0,
max? : Int = 100,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let indicator = @html.div(
style=ui_styles(
[
UiBoxSizing,
ProgressIndicatorStyle,
progress_indicator_width(value, min, max),
],
style,
),
id?,
class?,
title?,
hidden?,
attrs=progress_state_attrs(
attrs,
progress_state(value, min, max),
"progress-indicator",
),
@html.nothing,
)
if value is Some(_) {
indicator
} else {
@html.fragment([progress_indeterminate_stylesheet(), indicator])
}
}
///|
/// Render a progress label. Pass the same `id` to `progress(labelledby=...)`
/// because a stateless Rabbita function cannot register the label implicitly.
pub fn[C : @html.IsChildren] progress_label(
value? : Int,
min? : Int = 0,
max? : Int = 100,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_attrs = progress_state_attrs(
attrs,
progress_state(value, min, max),
"progress-label",
).role("presentation")
@html.span(
style=ui_styles([UiBoxSizing, ProgressLabelStyle], style),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
children,
)
}
///|
/// Render the formatted percentage, or an empty span for indeterminate state.
/// `text` overrides the default formatting while preserving `aria-hidden`.
pub fn progress_value(
value? : Int,
min? : Int = 0,
max? : Int = 100,
text? : String,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let element_attrs = progress_state_attrs(
attrs,
progress_state(value, min, max),
"progress-value",
).aria_hidden("true")
@html.span(
style=ui_styles([UiBoxSizing, ProgressValueStyle], style),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
text.unwrap_or(progress_display_text(value, min, max)),
)
}