ee8797c142
Co-authored-by: Copilot <copilot@github.com>
170 lines
4.8 KiB
Markdown
170 lines
4.8 KiB
Markdown
# Avatar
|
||
|
||
A circular user avatar. Shows an image when a `src` URL is provided; falls back to a text/initials span otherwise.
|
||
|
||
---
|
||
|
||
## HTML structure
|
||
|
||
```
|
||
span.relative.flex.{size classes}.shrink-0.overflow-hidden.rounded-full
|
||
img[src, alt, class] ← when src is provided
|
||
span.flex.items-center... ← fallback when no src
|
||
{fallback text}
|
||
```
|
||
|
||
---
|
||
|
||
## CSS mechanics
|
||
|
||
| Class | Effect |
|
||
|---|---|
|
||
| `rounded-full overflow-hidden` | Clips content to a circle |
|
||
| `aspect-square h-full w-full object-cover` | Image fills the circle without distortion |
|
||
| `bg-muted text-muted-foreground` | Neutral background for the initials fallback |
|
||
| Size `h-8 w-8` / `h-10 w-10` / `h-14 w-14` / `h-20 w-20` | sm / default / lg / xl |
|
||
|
||
---
|
||
|
||
## Constructor signature
|
||
|
||
```csharp
|
||
public Avatar(
|
||
string fallback,
|
||
string? src = null,
|
||
string size = "default")
|
||
```
|
||
|
||
| Parameter | Description |
|
||
|---|---|
|
||
| `fallback` | Text shown when no `src` is given; also used as `alt` text on the image |
|
||
| `src` | Optional image URL |
|
||
| `size` | `"sm"` / `"default"` / `"lg"` / `"xl"` |
|
||
|
||
---
|
||
|
||
## Usage examples
|
||
|
||
### Initials avatar
|
||
|
||
```csharp
|
||
new Avatar(fallback: "JD")
|
||
new Avatar(fallback: "JD", size: "lg")
|
||
```
|
||
|
||
### Image avatar with fallback
|
||
|
||
```csharp
|
||
new Avatar(fallback: "Jane Doe", src: "/avatars/jane.jpg", size: "default")
|
||
```
|
||
|
||
### Sizes
|
||
|
||
```csharp
|
||
new Avatar(fallback: "SM", size: "sm") // 32×32
|
||
new Avatar(fallback: "DF", size: "default") // 40×40
|
||
new Avatar(fallback: "LG", size: "lg") // 56×56
|
||
new Avatar(fallback: "XL", size: "xl") // 80×80
|
||
```
|
||
|
||
### Inside a user card
|
||
|
||
```csharp
|
||
var avatar = new Avatar(fallback: user.Initials, src: user.AvatarUrl, size: "lg");
|
||
|
||
// In a page's RenderUserCard override:
|
||
protected override void RenderUserAvatar(HtmxRenderContext ctx)
|
||
=> avatar.Render(ctx.Next());
|
||
```
|
||
|
||
---
|
||
|
||
## Tips and tricks
|
||
|
||
- Compute initials before constructing the Avatar — the component does not extract them from a full name. See `MainLayout`'s `GetInitials` helper for a reference implementation.
|
||
- Always provide `fallback` even when you also provide `src` — it serves as the `alt` attribute for accessibility.
|
||
- The Avatar does not handle image load errors. If you need a graceful image fallback on 404, add an `onerror="this.style.display='none'"` attribute by embedding it in the `src` or use `hxAttrs` in a subclassed version.
|
||
- For a group of overlapping avatars (avatar stack), wrap several Avatars in a flex container with negative margin: `<div class="flex -space-x-2">`.
|
||
|
||
---
|
||
|
||
## Complete page example
|
||
|
||
**`Templates/ProfilePage.htmx`**
|
||
```html
|
||
<div class="max-w-lg mx-auto py-10">
|
||
<div class="flex items-center gap-4 mb-6">
|
||
$$UserAvatar$$
|
||
<div>
|
||
<h1 class="text-xl font-bold">$$DisplayName$$</h1>
|
||
<p class="text-sm text-muted-foreground">$$Email$$</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-sm">Member since $$JoinDate$$</p>
|
||
</div>
|
||
```
|
||
|
||
**`Templates/ProfilePage.htmx.cs`**
|
||
```csharp
|
||
namespace Htmx.ApiDemo.Templates;
|
||
|
||
public sealed class ProfilePage : ProfilePageBase
|
||
{
|
||
private readonly IHtmxComponent _avatar;
|
||
private readonly byte[] _displayName;
|
||
private readonly byte[] _email;
|
||
private readonly byte[] _joinDate;
|
||
|
||
public ProfilePage(AppUser user)
|
||
{
|
||
_avatar = new Components.Avatar(
|
||
fallback: GetInitials(user.DisplayName),
|
||
size: "lg");
|
||
|
||
_displayName = (user.DisplayName ?? "Unknown").ToUtf8Bytes();
|
||
_email = user.Email.ToUtf8Bytes();
|
||
_joinDate = user.CreatedAt.ToString("MMMM yyyy").ToUtf8Bytes();
|
||
}
|
||
|
||
private static string GetInitials(string? name)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(name)) return "?";
|
||
var parts = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||
return parts.Length >= 2
|
||
? $"{parts[0][0]}{parts[^1][0]}"
|
||
: name[..1].ToUpperInvariant();
|
||
}
|
||
|
||
protected override void RenderUserAvatar(HtmxRenderContext ctx)
|
||
=> _avatar.Render(ctx.Next());
|
||
protected override void RenderDisplayName(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_displayName);
|
||
protected override void RenderEmail(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_email);
|
||
protected override void RenderJoinDate(HtmxRenderContext ctx)
|
||
=> ctx.Writer.WriteUtf8(_joinDate);
|
||
}
|
||
```
|
||
|
||
**GET handler**
|
||
```csharp
|
||
[Handler]
|
||
[MapGet("/profile")]
|
||
public static partial class GetProfileHandler
|
||
{
|
||
public record Query();
|
||
|
||
private static async Task<IResult> HandleAsync(
|
||
Query _,
|
||
HttpContext ctx,
|
||
MongoDbService db,
|
||
CancellationToken ct)
|
||
{
|
||
var email = ctx.User.FindFirst(ClaimTypes.Email)?.Value ?? "";
|
||
var user = await db.FindByNormalizedEmailAsync(email.ToUpperInvariant(), ct);
|
||
if (user is null) return Results.Redirect("/login");
|
||
return await ctx.WriteHtmxPage(new ProfilePage(user), title: "Profile");
|
||
}
|
||
}
|
||
```
|