///|
pub(all) enum AvatarSize {
Default
Sm
Lg
} derive(Debug, Eq)
///|
pub(all) enum AvatarImageStatus {
AvatarLoading
AvatarLoaded
AvatarError
} derive(Debug, Eq)
///|
#cfg(target="js")
fn bind_avatar_image(id : String, notify : (Int) -> Unit) -> Unit {
guard ui_element_by_id(id) is Some(element) else { return }
guard element.to_html_image_element() is Some(image) else { return }
image.add_event_listener("load", _ => notify(1))
image.add_event_listener("error", _ => notify(2))
if image.get_complete() {
notify(if image.get_natural_width() > 0.0 { 1 } else { 2 })
}
}
///|
#cfg(target="js")
fn bind_avatar_image_cmd(
id : String,
emit : @cmd.Emit[AvatarImageStatus],
) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
ui_after_mount(
id,
() => {
bind_avatar_image(id, status => {
scheduler.add(
emit(if status == 1 { AvatarLoaded } else { AvatarError }),
)
})
},
purpose="avatar-image-bind",
)
})
}
///|
const AvatarBaseStyle : String = "position:relative;isolation:isolate;display:inline-flex;width:var(--rui-avatar-size,2rem);height:var(--rui-avatar-size,2rem);flex-shrink:0;overflow:hidden;margin-inline-start:var(--rui-avatar-overlap,0);border-radius:9999px;box-shadow:var(--rui-avatar-ring,0 0 0 0 transparent);vertical-align:middle;user-select:none"
///|
const AvatarDefaultSizeStyle : String = "--rui-avatar-size:2rem;--rui-avatar-fallback-font-size:0.875rem;--rui-avatar-badge-size:0.625rem;--rui-avatar-badge-icon-size:0.5rem;--rui-avatar-group-count-icon-size:1rem"
///|
const AvatarSmSizeStyle : String = "--rui-avatar-size:1.5rem;--rui-avatar-fallback-font-size:0.75rem;--rui-avatar-badge-size:0.5rem;--rui-avatar-badge-icon-size:0;--rui-avatar-group-count-icon-size:0.75rem"
///|
const AvatarLgSizeStyle : String = "--rui-avatar-size:2.5rem;--rui-avatar-fallback-font-size:0.875rem;--rui-avatar-badge-size:0.75rem;--rui-avatar-badge-icon-size:0.5rem;--rui-avatar-group-count-icon-size:1.25rem"
///|
const AvatarImageStyle : String = "position:absolute;inset:0;z-index:1;width:100%;height:100%;border-radius:9999px;color:transparent;font-size:0;object-fit:cover"
///|
const AvatarFallbackStyle : String = "display:flex;width:100%;height:100%;align-items:center;justify-content:center;border-radius:9999px;background:var(--rui-muted,oklch(0.97 0 0));color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:var(--rui-avatar-fallback-font-size,0.875rem);line-height:1;font-weight:500"
///|
const AvatarBadgeStyle : String = "position:absolute;inset-inline-end:0;bottom:0;z-index:2;display:inline-flex;width:var(--rui-avatar-badge-size,0.625rem);height:var(--rui-avatar-badge-size,0.625rem);align-items:center;justify-content:center;border-radius:9999px;background:var(--rui-primary,oklch(0.205 0 0));color:var(--rui-primary-foreground,oklch(0.985 0 0));font-size:var(--rui-avatar-badge-icon-size,0.5rem);line-height:1;box-shadow:0 0 0 2px var(--rui-background,oklch(1 0 0));user-select:none"
///|
const AvatarGroupStyle : String = "--rui-avatar-overlap:-0.5rem;--rui-avatar-ring:0 0 0 2px var(--rui-background,oklch(1 0 0));display:flex;align-items:center;padding-inline-start:0.5rem"
///|
const AvatarGroupCountStyle : String = "position:relative;display:flex;width:var(--rui-avatar-size,2rem);height:var(--rui-avatar-size,2rem);flex-shrink:0;align-items:center;justify-content:center;margin-inline-start:var(--rui-avatar-overlap,-0.5rem);border-radius:9999px;background:var(--rui-muted,oklch(0.97 0 0));color:var(--rui-muted-foreground,oklch(0.556 0 0));font-size:var(--rui-avatar-fallback-font-size,0.875rem);line-height:1;font-weight:500;box-shadow:var(--rui-avatar-ring,0 0 0 2px var(--rui-background,oklch(1 0 0)));--rui-icon-size:var(--rui-avatar-group-count-icon-size,1rem)"
///|
fn avatar_size_style(size : AvatarSize) -> String {
match size {
Default => AvatarDefaultSizeStyle
Sm => AvatarSmSizeStyle
Lg => AvatarLgSizeStyle
}
}
///|
fn avatar_size_value(size : AvatarSize) -> String {
match size {
Default => "default"
Sm => "sm"
Lg => "lg"
}
}
///|
fn avatar_part_attrs(attrs : @html.Attrs?, slot : String) -> @html.Attrs {
ui_attrs(attrs).data_set("slot", slot)
}
///|
/// Render the composable Vega avatar root as a native `span` element.
///
/// Size is shared with fallback and badge parts through inherited inline custom
/// properties. The root does not own image-loading state; omit `avatar_image`
/// while showing a fallback, or control that choice in the parent TEA model.
pub fn[C : @html.IsChildren] avatar(
size? : AvatarSize = Default,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_attrs = avatar_part_attrs(attrs, "avatar").data_set(
"size",
avatar_size_value(size),
)
@html.span(
style=ui_styles(
[
UiBoxSizing,
UiFontSans,
UiTextRendering,
AvatarBaseStyle,
avatar_size_style(size),
],
style,
),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
children,
)
}
///|
/// Render the avatar image as a native `img` layered above the fallback.
pub fn avatar_image(
src? : String,
alt? : String,
width? : Int,
height? : Int,
srcset? : String,
sizes? : String,
loading? : String,
decoding? : String,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
@html.img(
style=ui_styles([UiBoxSizing, AvatarImageStyle], style),
id?,
class?,
hidden?,
src?,
alt?,
title?,
width?,
height?,
srcset?,
sizes?,
loading?,
decoding?,
attrs=avatar_part_attrs(attrs, "avatar-image"),
)
}
///|
pub fn[C : @html.IsChildren] avatar_fallback(
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.span(
style=ui_styles([UiBoxSizing, AvatarFallbackStyle], style),
id?,
class?,
title?,
hidden?,
attrs=avatar_part_attrs(attrs, "avatar-fallback"),
children,
)
}
///|
pub fn[C : @html.IsChildren] avatar_badge(
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.span(
style=ui_styles([UiBoxSizing, AvatarBadgeStyle], style),
id?,
class?,
title?,
hidden?,
attrs=avatar_part_attrs(attrs, "avatar-badge"),
children,
)
}
///|
pub fn[C : @html.IsChildren] avatar_group(
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
@html.div(
style=ui_styles(
[UiBoxSizing, UiFontSans, UiTextRendering, AvatarGroupStyle],
style,
),
id?,
class?,
title?,
hidden?,
attrs=avatar_part_attrs(attrs, "avatar-group"),
children,
)
}
///|
pub fn[C : @html.IsChildren] avatar_group_count(
size? : AvatarSize = Default,
id? : String,
class? : String,
title? : String,
hidden? : Bool,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_attrs = avatar_part_attrs(attrs, "avatar-group-count").data_set(
"size",
avatar_size_value(size),
)
@html.div(
style=ui_styles(
[UiBoxSizing, avatar_size_style(size), AvatarGroupCountStyle],
style,
),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
children,
)
}
///|
fn[C : @html.IsChildren] render_avatar_with_fallback(
status : AvatarImageStatus,
id : String,
src : String,
alt : String?,
size : AvatarSize,
class : String?,
title : String?,
attrs : @html.Attrs?,
style : Array[String],
image_attrs : @html.Attrs?,
image_style : Array[String],
fallback_attrs : @html.Attrs?,
fallback_style : Array[String],
fallback : C,
) -> @html.Html {
let status_value = match status {
AvatarLoading => "loading"
AvatarLoaded => "loaded"
AvatarError => "error"
}
let root_attrs = ui_attrs(attrs).data_set("image-state", status_value)
avatar(size~, id~, class?, title?, attrs=root_attrs, style~, [
avatar_fallback(
hidden=status is AvatarLoaded,
attrs?=fallback_attrs,
style=fallback_style,
fallback,
),
avatar_image(
id=id + "-image",
src~,
alt?,
hidden=!(status is AvatarLoaded),
attrs?=image_attrs,
style=image_style,
),
])
}
///|
/// Build a black-box avatar that owns image loading and failure state.
///
/// The fallback remains visible until the browser reports a successfully
/// decoded image resource. Use the pure `avatar`, `avatar_image`, and
/// `avatar_fallback` parts when the application already owns that state.
#cfg(target="js")
pub fn[C : @html.IsChildren] avatar_with_fallback(
id~ : String,
src~ : String,
alt? : String,
size? : AvatarSize = Default,
on_status_change? : @cmd.Emit[AvatarImageStatus],
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
image_attrs? : @html.Attrs,
image_style? : Array[String] = [],
fallback_attrs? : @html.Attrs,
fallback_style? : Array[String] = [],
fallback : C,
) -> @rabbita.Val[@html.Html] {
let initial = if src == "" { AvatarError } else { AvatarLoading }
let (status, _) = @rabbita.create_state_with_init(
init=emit => {
(
initial,
if src == "" {
@cmd.none
} else {
bind_avatar_image_cmd(id + "-image", emit)
},
)
},
update=(_, next, _) => {
(
next,
if on_status_change is Some(notify) {
notify(next)
} else {
@cmd.none
},
)
},
)
status.view(status => {
render_avatar_with_fallback(
status, id, src, alt, size, class, title, attrs, style, image_attrs, image_style,
fallback_attrs, fallback_style, fallback,
)
})
}
///|
#cfg(not(target="js"))
pub fn[C : @html.IsChildren] avatar_with_fallback(
id~ : String,
src~ : String,
alt? : String,
size? : AvatarSize = Default,
on_status_change? : @cmd.Emit[AvatarImageStatus],
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
image_attrs? : @html.Attrs,
image_style? : Array[String] = [],
fallback_attrs? : @html.Attrs,
fallback_style? : Array[String] = [],
fallback : C,
) -> @rabbita.Val[@html.Html] {
ignore(on_status_change)
let status = if src == "" { AvatarError } else { AvatarLoading }
@rabbita.Val::constant(
render_avatar_with_fallback(
status, id, src, alt, size, class, title, attrs, style, image_attrs, image_style,
fallback_attrs, fallback_style, fallback,
),
)
}