From 88b29200d51ba90b3ba280114982034c15f77032 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:35:57 +0100 Subject: [PATCH] fix: avatar status dot size to match prototype cutout dimensions Reduced visible dot from 12px solid to 6px with 3px mask cutout gap, matching the prototype's 12px border-box dot with 3px border exactly. --- packages/web/src/components/ui/Avatar.tsx | 42 +++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/web/src/components/ui/Avatar.tsx b/packages/web/src/components/ui/Avatar.tsx index a28b0eec..026db272 100644 --- a/packages/web/src/components/ui/Avatar.tsx +++ b/packages/web/src/components/ui/Avatar.tsx @@ -21,6 +21,28 @@ const statusColors: Record = { offline: 'bg-status-offline', }; +/** + * Builds a CSS radial-gradient mask that punches a circular hole in the avatar + * where the status dot sits. The hole reveals the parent background, creating + * a true cutout effect on any surface — no border-color matching needed. + * + * Prototype reference (.m-dot): 12px box with 3px border (border-box) = 6px + * visible color, positioned at bottom:-2 right:-2 → center 4px from corner. + */ +function buildCutoutMask(size: number): string { + const { dot, gap, inset } = getDotMetrics(size); + const cx = size - inset; + const cy = size - inset; + const r = dot / 2 + gap; + return `radial-gradient(circle at ${cx}px ${cy}px, transparent ${r}px, black ${r + 0.5}px)`; +} + +/** Returns visible dot diameter, gap width, and center inset from avatar edge. */ +function getDotMetrics(size: number) { + if (size <= 24) return { dot: 5, gap: 2, inset: 3 }; + return { dot: 6, gap: 3, inset: 4 }; +} + export function Avatar({ src, name, size = 40, status, className = '', onClick, user, userId }: AvatarProps) { const openUserProfile = useUIStore((s) => s.openUserProfile); const initials = name.charAt(0).toUpperCase(); @@ -40,6 +62,14 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick, } }; + // Only compute mask when status dot is visible + const cutoutMask = status ? buildCutoutMask(size) : undefined; + const maskStyle: React.CSSProperties | undefined = cutoutMask + ? { maskImage: cutoutMask, WebkitMaskImage: cutoutMask } + : undefined; + + const { dot: dotDiameter, inset: dotInset } = getDotMetrics(size); + return (
{ (e.target as HTMLImageElement).style.display = 'none'; const parent = (e.target as HTMLImageElement).parentElement; @@ -63,7 +94,7 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick, ) : null}
{initials}
@@ -71,11 +102,10 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick,
)}