anydrop/web/src/components/PeerAvatar.tsx
ordinarthur c18d995c3f feat(web): Paper & Envelope design system
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.

Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).

Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 10:49:15 +02:00

91 lines
2.3 KiB
TypeScript

import type { DeviceType } from "@anydrop/shared";
interface PeerAvatarProps {
displayName: string;
deviceType: DeviceType;
avatar?: string;
online?: boolean;
onClick?: () => void;
isSelected?: boolean;
size?: "sm" | "md" | "lg";
}
const DEVICE_GLYPH: Record<DeviceType, string> = {
phone: "phone",
tablet: "tablet",
laptop: "laptop",
desktop: "desk",
};
const sizeClasses = {
sm: { container: "w-12 h-12", label: "text-[10px]" },
md: { container: "w-16 h-16", label: "text-[11px]" },
lg: { container: "w-20 h-20", label: "text-xs" },
};
export default function PeerAvatar({
displayName,
deviceType,
avatar,
online = true,
onClick,
isSelected,
size = "md",
}: PeerAvatarProps) {
const s = sizeClasses[size];
const isOffline = !online;
return (
<button
onClick={onClick}
className={`
group flex flex-col items-center gap-2.5
transition-transform duration-fast ease-crisp
${isOffline ? "opacity-60" : ""}
`}
>
<div className="relative">
<div
className={`
${s.container}
rounded-full flex items-center justify-center overflow-hidden
border transition-all duration-fast ease-crisp
${isSelected
? "border-signal ring-1 ring-signal"
: "border-paper-edge group-hover:border-ink"
}
${avatar ? "" : "bg-paper"}
`}
>
{avatar ? (
<img
src={avatar}
alt=""
className={`w-full h-full object-cover ${isOffline ? "grayscale" : ""}`}
/>
) : (
<span className="font-mono text-[10px] uppercase tracking-widest text-ink-muted">
{DEVICE_GLYPH[deviceType]}
</span>
)}
</div>
<span
className={`
absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border border-paper
${online ? "bg-ok" : "bg-ink-faint"}
`}
aria-label={online ? "online" : "offline"}
/>
</div>
<span
className={`
${s.label} max-w-[88px] truncate transition-colors
${isSelected ? "text-ink" : isOffline ? "text-ink-faint" : "text-ink-muted group-hover:text-ink"}
`}
>
{displayName}
</span>
</button>
);
}