<script module lang="ts">
import { tv, type VariantProps } from "tailwind-variants";
export const avatarSizeClass = {
mini: "size-3.25",
small: "size-4.25",
regular: "size-5.25",
large: "size-6.5",
full: "size-full"
};
export const avatar = tv({
base: "relative flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-default bg-background-canvas text-foreground-subtle",
variants: {
size: avatarSizeClass
},
defaultVariants: {
size: "regular"
}
});
export type AvatarSize = NonNullable<VariantProps<typeof avatar>["size"]>;
export const avatarSizePx = {
mini: 13,
small: 17,
regular: 21,
large: 26,
full: undefined
} satisfies Record<AvatarSize, number | undefined>;
export const avatarSizeNames = Object.keys(avatarSizePx) as AvatarSize[];
</script>
<script lang="ts">
import UserRound from "$icon/user-round";
import { avatarUrl, avatarWidthFor } from "#lib/avatar.ts";
import { gradientCss } from "#lib/avatarGradient.ts";
import type { Moderation } from "#lib/api/moderationLabels.ts";
interface Props {
did?: string;
// null means the tangled profile has no avatar. the worker still tries bluesky
avatarCid?: string | null;
src?: string;
handle?: string;
size?: AvatarSize;
pixels?: number;
loading?: "lazy" | "eager";
class?: string;
moderation?: Moderation;
}
let {
did,
avatarCid,
src,
handle,
size = "regular",
pixels,
loading = "lazy",
class: className = "",
moderation
}: Props = $props();
const px = $derived(pixels ?? avatarSizePx[size]);
const source = $derived(
moderation?.level === "hide"
? undefined
: (src ??
(did
? avatarUrl(
did,
px === undefined ? undefined : avatarWidthFor(px),
undefined,
avatarCid ?? undefined
)
: undefined))
);
// the gradient shows until the image loads and stays when there is none
const placeholder = $derived(
did && moderation?.level !== "hide" ? gradientCss(did) : undefined
);
// hidden with a style, not state, a page full of 404s would write state mid-render
const hide = (event: Event) => {
(event.currentTarget as HTMLImageElement).style.display = "none";
};
// a reused element has to recover from a previous failure
const show = (event: Event) => {
(event.currentTarget as HTMLImageElement).style.display = "";
};
</script>
<span
class={avatar({ size, class: className })}
role={handle ? "img" : undefined}
aria-label={handle ? `${handle}'s avatar` : undefined}
aria-hidden={handle ? undefined : "true"}
>
<span class="absolute inset-px rounded-full" style:background-image={placeholder}></span>
{#if !placeholder}
<UserRound class="size-1/2" />
{/if}
{#if source}
<img
src={source}
alt=""
width={px}
height={px}
{loading}
decoding="async"
class={[
"absolute inset-0 size-full object-cover",
moderation?.level === "warn" && "scale-125 blur-md"
]}
onerror={hide}
onload={show}
/>
{/if}
</span>