"""
A user avatar with image support, initials fallback, and status indicator.
"""
from duck.html.components.container import Container
from duck.html.components.image import Image
from duck.html.components.label import Label
from quirl.theme import Theme
# Pixel diameter for each named avatar size
SIZE_MAP = {
"xs": "24px",
"sm": "32px",
"md": "40px",
"lg": "48px",
"xl": "64px",
}
# Theme token backing each status dot color
STATUS_TOKEN_MAP = {
"online": "success_color",
"away": "warning_color",
"busy": "error_color",
"offline": "border_color",
}
[docs]
class Avatar(Container):
"""
A circular user avatar with image support, initials fallback, and an
optional presence status dot.
Displays a profile image when available, otherwise renders initials
centered in a themed circle. The image fades in smoothly once loaded.
Usage:
```python
Avatar(src="/static/user.jpg", alt="Jane Doe", initials="JD", status="online")
```
Required Props:
- initials: Fallback text when no image is provided
Optional Props:
- src: Image source URL
- alt: Alt text for the image
- size: One of xs, sm, md, lg, xl. Defaults to md.
- status: One of online, away, busy, offline. Adds a presence dot.
- bg_color: Circle background, defaults to var(--theme-accent-color)
- color: Initials color, defaults to var(--theme-surface-color)
"""
docs_preview_kwargs = {
"initials": "JD",
"size": "lg",
"status": "online",
}
[docs]
def on_create(self):
"""
Build the avatar circle plus an optional status dot.
"""
super().on_create()
size = self.kwargs.get("size", "md")
dim = SIZE_MAP.get(size, SIZE_MAP["md"])
status = self.kwargs.get("status")
# Wrapper stays overflow-visible so the status dot can sit on the edge
self.style.update({
"position": "relative",
"display": "inline-flex",
"width": dim,
"height": dim,
"flex-shrink": "0",
})
# Assemble the circle and optional presence dot
children = [self.build_circle(dim)]
if status:
children.append(self.build_status_dot(status, dim))
self.add_children(children)
[docs]
def build_circle(self, dim: str) -> Container:
"""
Build the clipped circle holding the image or initials.
Args:
dim: Pixel diameter for this avatar's size.
Returns:
A circular Container with image or initials content.
"""
src = self.kwargs.get("src")
alt = self.kwargs.get("alt", "")
if src:
content = [Image(
source=src,
alt=alt,
style={
"width": "100%",
"height": "100%",
"object-fit": "cover",
"animation": "quirl-fade-in 0.3s ease-out",
},
)]
else:
content = [Label(
text=self.kwargs.get("initials", ""),
style={
"color": "inherit",
"font-size": "inherit",
"font-weight": "inherit",
},
)]
return Container(
style={
"display": "flex",
"align-items": "center",
"justify-content": "center",
"width": "100%",
"height": "100%",
"border-radius": "50%",
"overflow": "hidden",
"background": self.kwargs.get("bg_color", Theme.current.accent_color),
"color": self.kwargs.get("color", Theme.current.surface_color),
"font-weight": "600",
"font-size": f"calc({dim} * 0.4)",
"box-shadow": Theme.current.shadow_sm,
},
children=content,
)
[docs]
def build_status_dot(self, status: str, dim: str) -> Container:
"""
Build the small presence indicator anchored to the avatar's edge.
Args:
status: One of online, away, busy, offline.
dim: Pixel diameter of the parent avatar.
Returns:
A positioned Container rendering the status dot.
"""
token = STATUS_TOKEN_MAP.get(status, STATUS_TOKEN_MAP["offline"])
dot_size = f"calc({dim} * 0.28)"
return Container(
props={"aria-label": f"Status: {status}"},
style={
"position": "absolute",
"bottom": "0",
"right": "0",
"width": dot_size,
"height": dot_size,
"min-width": "8px",
"min-height": "8px",
"border-radius": "50%",
"background": getattr(Theme.current, token),
"border": f"2px solid {Theme.current.surface_color}",
},
)