This repository has no description
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 // the sizes the design system names, from Figma's User component (node 723:254).
5 // Size=Header is not a rung here: it draws the same 26px circle as Size=Large
6 // and differs only in the text style beside it.
7 export const avatar = tv({
8 base: "shrink-0 rounded-full border border-border-default",
9 variants: {
10 size: {
11 mini: "size-3.25",
12 small: "size-4.25",
13 regular: "size-5.25",
14 large: "size-6.5",
15 // for a size off the scale: `full` fills the parent, and a size class
16 // passed by the caller replaces it, since tv() merges the two rather
17 // than leaving both to fight in the class string
18 full: "size-full"
19 }
20 },
21 defaultVariants: {
22 size: "regular"
23 }
24 });
25
26 export type AvatarSize = NonNullable<VariantProps<typeof avatar>["size"]>;
27
28 // what each rung above renders at, in css pixels — the spacing unit is 4px, so
29 // these are the class numbers times four. the avatar is fetched at twice this so
30 // that it stays sharp at 2x. `full` has no size of its own, the layout decides
31 // it, so there is nothing to fetch at.
32 const sizePx = {
33 mini: 13,
34 small: 17,
35 regular: 21,
36 large: 26,
37 full: undefined
38 } satisfies Record<AvatarSize, number | undefined>;
39
40 export const avatarSizeNames = Object.keys(sizePx) as AvatarSize[];
41</script>
42
43<script lang="ts">
44 import UserRound from "$icon/user-round";
45 import { avatarUrl } from "$lib/avatar";
46
47 interface Props {
48 did?: string;
49 src?: string;
50 handle?: string;
51 size?: AvatarSize;
52 class?: string;
53 }
54
55 let { did, src, handle, size = "regular", class: className = "" }: Props = $props();
56
57 const px = $derived(sizePx[size]);
58 const source = $derived(
59 src ?? (did ? avatarUrl(did, px === undefined ? undefined : px * 2) : undefined)
60 );
61
62 // an unconfigured avatar service 404s, same as a broken image
63 let failed = $state(false);
64
65 $effect(() => {
66 if (source) failed = false;
67 });
68</script>
69
70{#if source && !failed}
71 <img
72 src={source}
73 alt={handle ? `${handle}'s avatar` : "avatar"}
74 class={avatar({ size, class: `object-cover ${className}` })}
75 onerror={() => (failed = true)}
76 />
77{:else}
78 <span
79 class={avatar({
80 size,
81 class: `flex items-center justify-center bg-background-canvas text-foreground-subtle ${className}`
82 })}
83 role={handle ? "img" : undefined}
84 aria-label={handle ? `${handle}'s avatar` : undefined}
85 aria-hidden={handle ? undefined : "true"}
86 >
87 <UserRound class="size-1/2" />
88 </span>
89{/if}