///|
const SliderTrackStyle : String = "display:block;width:100%;height:12px;margin:0;padding:0;border:none;outline:none"
///|
const SliderValueStyle : String = "flex:none;min-width:28px;text-align:right;font-size:12px;line-height:20px;font-weight:500;color:var(--sumi-text,#ffffff);font-variant-numeric:tabular-nums"
///|
fn slider_percent(value : Int, min : Int, max : Int) -> Int {
if max <= min {
0
} else {
let clamped = if value < min {
min
} else if value > max {
max
} else {
value
}
(clamped - min) * 100 / (max - min)
}
}
///|
/// A single-value slider built on the native range input; track fill and
/// thumb styling live in the interaction sheet emitted by `theme`.
///
/// `value_tooltip=true` adds the design's value bubble above the thumb,
/// visible while hovering, dragging, or keyboard-focusing the slider.
/// `on_input` fires per tick (keep it cheap); `on_commit` fires once on
/// release for expensive reactions.
///
/// ```moonbit nocheck
/// slider(
/// value=35,
/// value_tooltip=true,
/// on_input=set_strength.map(v => _ => v),
/// on_commit=emit(CommitStrength),
/// suffix="°",
/// )
/// ```
pub fn slider(
value~ : Int,
min? : Int = 0,
max? : Int = 100,
step? : Int = 1,
on_input? : @internal.Emit[Int],
on_commit? : @internal.Emit[Int],
disabled? : Bool = false,
show_value? : Bool = false,
value_tooltip? : Bool = false,
suffix? : String = "",
id? : String,
class? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let percent = slider_percent(value, min, max)
let input_attrs = @internal.sumi_attrs(attrs)
.data_set("slot", "slider")
.style("--sumi-slider-percent", "\{percent}%")
if disabled {
ignore(input_attrs.disabled(true))
}
let parse = fn(s : String) -> Int { @string.parse_int(s) catch { _ => min } }
let input = @html.input(
input_type=Range,
value=value.to_string(),
min~,
max~,
step~,
on_input?=on_input.map(emit => emit.map(parse)),
on_change?=on_commit.map(emit => emit.map(parse)),
style=@internal.sumi_styles(
[
@internal.SumiBoxSizing,
@internal.SumiFontNum,
@internal.SumiTextRendering,
SliderTrackStyle,
],
[],
),
id?,
class?,
attrs=input_attrs,
)
let bubble : @html.Html = if value_tooltip {
@html.span(
style=[
"position:absolute;bottom:calc(100% + 12px);left:\{percent}%;transform:translateX(-50%);z-index:50;display:flex;align-items:center;justify-content:center;padding:10px 16px;border-radius:var(--sumi-radius,8px);background:var(--sumi-tooltip-bg,#333333);box-shadow:0px 8px 28px var(--sumi-shadow,rgba(0,0,0,0.24));font-size:12px;line-height:20px;font-weight:500;color:var(--sumi-tooltip-fg,#ffffff);font-variant-numeric:tabular-nums;pointer-events:none;opacity:0;transition:opacity 100ms ease",
],
attrs=@html.Attrs::build()
.data_set("slot", "slider-bubble")
.aria_hidden("true"),
"\{value}\{suffix}",
)
} else {
@html.nothing
}
let track = @html.div(
style=["position:relative;display:flex;align-items:center;width:100%"],
attrs=@html.Attrs::build().data_set("slot", "slider-track"),
[input, bubble],
)
if show_value {
@html.div(
style=@internal.sumi_styles(
[
@internal.SumiBoxSizing,
@internal.SumiFontSans,
"display:flex;align-items:center;gap:8px;width:100%",
],
style,
),
attrs=@html.Attrs::build().data_set("slot", "slider-root"),
[
track,
@html.span(
style=[
@internal.SumiFontNum,
@internal.SumiTextRendering,
SliderValueStyle,
],
attrs=@html.Attrs::build().data_set("slot", "slider-value"),
"\{value}\{suffix}",
),
],
)
} else {
track
}
}