This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / Avatar.svelte
2.5 kB 89 lines
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}