Avatar source
Inspect and copy the reviewed source included with this Free component.
Free source · 4 files
recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Avatar recipe — typed root axes and named image/fallback slots.
//!
//! A nested fallback follows the root Avatar's `data-size` through a named
//! group. Its legacy local size still provides sensible standalone rendering,
//! while the root remains authoritative when the parts are composed.
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
mod base {
use super::*;
pub const ROOT: &str = tw_join!(
"group/avatar",
"relative",
"flex",
"shrink-0",
"overflow-hidden"
);
pub const IMAGE: &str = tw_join!("aspect-square", "h-full", "w-full", "object-cover");
pub const FALLBACK: &str = tw_join!(
"flex",
"h-full",
"w-full",
"items-center",
"justify-center",
"bg-muted",
"text-muted-foreground",
"font-medium",
"group-data-[size=xs]/avatar:text-xs",
"group-data-[size=sm]/avatar:text-xs",
"group-data-[size=md]/avatar:text-sm",
"group-data-[size=lg]/avatar:text-base",
"group-data-[size=xl]/avatar:text-lg",
"group-data-[size=xxl]/avatar:text-2xl"
);
}
/// Size variants for the avatar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "size")]
pub enum AvatarSize {
Xs,
Sm,
#[default]
Md,
Lg,
Xl,
Xxl,
}
impl AvatarSize {
pub const fn classes(self) -> &'static str {
match self {
Self::Xs => "size-6",
Self::Sm => "size-8",
Self::Md => "size-10",
Self::Lg => "size-12",
Self::Xl => "size-16",
Self::Xxl => "size-24",
}
}
pub const fn text_classes(self) -> &'static str {
match self {
Self::Xs | Self::Sm => "text-xs",
Self::Md => "text-sm",
Self::Lg => "text-base",
Self::Xl => "text-lg",
Self::Xxl => "text-2xl",
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Xs => "xs",
Self::Sm => "sm",
Self::Md => "md",
Self::Lg => "lg",
Self::Xl => "xl",
Self::Xxl => "xxl",
}
}
}
/// Shape variants for the avatar.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, proa_macros::ProaVariant)]
#[variant(name = "shape")]
pub enum AvatarShape {
#[default]
Circle,
Square,
}
impl AvatarShape {
pub const fn classes(self) -> &'static str {
match self {
Self::Circle => "rounded-full",
Self::Square => "rounded-lg",
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Circle => "circle",
Self::Square => "square",
}
}
}
/// Optional inputs accepted by [`AvatarRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AvatarRecipeProps {
pub size: Option<AvatarSize>,
pub shape: Option<AvatarShape>,
}
/// Static named classes and defaults for Avatar.
#[derive(Debug, Clone, Copy)]
pub struct AvatarRecipe {
pub root: &'static str,
pub image: &'static str,
pub fallback: &'static str,
size_default: AvatarSize,
shape_default: AvatarShape,
}
impl AvatarRecipe {
pub const fn resolve(&'static self, props: AvatarRecipeProps) -> AvatarStyle {
let size = match props.size {
Some(size) => size,
None => self.size_default,
};
let shape = match props.shape {
Some(shape) => shape,
None => self.shape_default,
};
AvatarStyle {
recipe: self,
size,
shape,
}
}
/// Resolve a standalone fallback's local text size.
///
/// When nested, the root's named-group selectors have greater specificity
/// and make the root `data-size` authoritative.
pub const fn resolve_fallback(&'static self, size: AvatarSize) -> AvatarFallbackStyle {
AvatarFallbackStyle { recipe: self, size }
}
}
/// Fully resolved, streamable Avatar root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct AvatarStyle {
recipe: &'static AvatarRecipe,
size: AvatarSize,
shape: AvatarShape,
}
impl AvatarStyle {
pub const fn size(self) -> AvatarSize {
self.size
}
pub const fn shape(self) -> AvatarShape {
self.shape
}
}
impl AttrValue for AvatarStyle {
#[inline(always)]
fn should_render(&self) -> bool {
true
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.root.as_bytes())?;
cx.out.extend_static(b" ")?;
cx.out.extend_static(self.size.classes().as_bytes())?;
cx.out.extend_static(b" ")?;
cx.out.extend_static(self.shape.classes().as_bytes())
}
}
/// Streamable fallback slot plus its standalone size fallback.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct AvatarFallbackStyle {
recipe: &'static AvatarRecipe,
size: AvatarSize,
}
impl AttrValue for AvatarFallbackStyle {
#[inline(always)]
fn should_render(&self) -> bool {
true
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.fallback.as_bytes())?;
cx.out.extend_static(b" ")?;
cx.out.extend_static(self.size.text_classes().as_bytes())
}
}
pub const AVATAR_RECIPE: AvatarRecipe = AvatarRecipe {
root: base::ROOT,
image: base::IMAGE,
fallback: base::FALLBACK,
size_default: AvatarSize::Md,
shape_default: AvatarShape::Circle,
};
pub const fn avatar_recipe() -> &'static AvatarRecipe {
&AVATAR_RECIPE
}
/// Compatibility delegates for the former helpers.
pub const fn avatar_classes(size: AvatarSize, shape: AvatarShape) -> AvatarStyle {
AVATAR_RECIPE.resolve(AvatarRecipeProps {
size: Some(size),
shape: Some(shape),
})
}
pub const fn avatar_image_classes() -> &'static str {
AVATAR_RECIPE.image
}
pub const fn avatar_fallback_base_classes() -> &'static str {
AVATAR_RECIPE.fallback
}
pub const fn avatar_fallback_classes(size: AvatarSize) -> AvatarFallbackStyle {
AVATAR_RECIPE.resolve_fallback(size)
}
impl crate::variant_spec::ComponentSpec for AvatarRecipe {
const NAME: &'static str = "Avatar";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
vec![
crate::variant_spec::variant_metadata::<AvatarSize>(),
crate::variant_spec::variant_metadata::<AvatarShape>(),
]
}
fn anatomy(&self) -> Option<crate::Anatomy> {
Some(super::AVATAR_ANATOMY)
}
}
#[cfg(feature = "metadata-export")]
pub fn export_metadata() -> crate::variant_spec::ComponentMetadata {
crate::variant_spec::component_metadata(&AVATAR_RECIPE)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recipe_exposes_named_slots() {
let recipe = avatar_recipe();
assert!(recipe.root.contains("group/avatar"));
assert!(recipe.image.contains("object-cover"));
assert!(recipe
.fallback
.contains("group-data-[size=xl]/avatar:text-lg"));
}
#[test]
fn recipe_resolves_defaults_and_overrides() {
let default = AVATAR_RECIPE.resolve(AvatarRecipeProps::default());
assert_eq!(default.size(), AvatarSize::Md);
assert_eq!(default.shape(), AvatarShape::Circle);
let custom = AVATAR_RECIPE.resolve(AvatarRecipeProps {
size: Some(AvatarSize::Xl),
shape: Some(AvatarShape::Square),
});
assert_eq!(custom.size(), AvatarSize::Xl);
assert_eq!(custom.shape(), AvatarShape::Square);
assert!(custom.size().classes().contains("size-16"));
assert!(custom.shape().classes().contains("rounded-lg"));
}
#[test]
fn fallback_supports_standalone_and_root_owned_sizes() {
assert_eq!(AvatarSize::Xxl.text_classes(), "text-2xl");
assert!(AVATAR_RECIPE
.fallback
.contains("group-data-[size=xxl]/avatar:text-2xl"));
let standalone = AVATAR_RECIPE.resolve_fallback(AvatarSize::Xs);
assert_eq!(standalone.size, AvatarSize::Xs);
}
}
FreeAvatar
User identity display with fallback initials.
Install with the Proa CLI
proa ui add avatarInstalls the reviewed component source, dependencies, shared support files, and required legal notices.
CLI and registry setup