feat: render teammate avatars with initials fallback on live cursors so it is always clear who each pointer belongs to

This commit is contained in:
Matthew Meszaros
2026-06-30 10:34:14 +02:00
parent 7237a7500c
commit 18fd48bfcb
3 changed files with 29 additions and 9 deletions
@@ -14,7 +14,7 @@ export default function CanvasCursors({ cursors }: { cursors: RemoteCursor[] })
return (
<div className="pointer-events-none absolute inset-0 z-20 overflow-hidden">
{cursors.map((c) => (
<Cursor key={c.userId} color={c.color} name={c.name} left={x + c.x * zoom} top={y + c.y * zoom} />
<Cursor key={c.userId} color={c.color} name={c.name} avatar={c.avatar} left={x + c.x * zoom} top={y + c.y * zoom} />
))}
</div>
);
+27 -7
View File
@@ -1,17 +1,29 @@
// Cursor — one teammate's live pointer, positioned in screen space by the layer
// that renders it (canvas or page). A short CSS transition smooths the gaps
// between throttled frames so movement reads as continuous.
// that renders it (canvas or page). The label carries the person's real avatar
// (initials fallback) next to their name so it is always clear who it is. A
// short CSS transition smooths the gaps between throttled frames.
import React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
function initialsOf(name: string | null): string {
if (!name) return "?";
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
export default function Cursor({
color,
name,
avatar,
left,
top,
}: {
color: string;
name: string | null;
avatar?: string | null;
left: number;
top: number;
}) {
@@ -35,13 +47,21 @@ export default function Cursor({
strokeLinejoin="round"
/>
</svg>
{name ? (
<span
className="absolute left-3.5 top-3.5 max-w-[140px] truncate rounded-full px-1.5 py-0.5 text-[10.5px] font-medium text-white shadow-sm"
{name || avatar ? (
<div
className="absolute left-3 top-3.5 flex max-w-[160px] items-center gap-1 rounded-full py-0.5 pl-0.5 pr-2 shadow-sm"
style={{ backgroundColor: color }}
>
{name}
</span>
<Avatar className="size-3.5 shrink-0 ring-1 ring-white/80">
{avatar ? <AvatarImage src={avatar} alt={name ?? ""} /> : null}
<AvatarFallback className="bg-white/90 text-[7px] font-semibold text-slate-700">
{initialsOf(name)}
</AvatarFallback>
</Avatar>
{name ? (
<span className="truncate text-[10.5px] font-medium leading-none text-white">{name}</span>
) : null}
</div>
) : null}
</div>
);
@@ -123,7 +123,7 @@ function GlobalCursorsOverlay({ scrollRef }: { scrollRef: React.RefObject<HTMLEl
style={{ left: r.left, top: r.top, width: r.width, height: r.height }}
>
{live.cursors.map((c) => (
<Cursor key={c.userId} color={c.color} name={c.name} left={c.x - sl} top={c.y - st} />
<Cursor key={c.userId} color={c.color} name={c.name} avatar={c.avatar} left={c.x - sl} top={c.y - st} />
))}
</div>
);