1import React from "react";
2
3type AvatarSize = "sm" | "md" | "lg";
4
5interface AvatarProps {
6 src?: string;
7 alt?: string;
8 name?: string;
9 size?: AvatarSize;
10 className?: string;
11}
12
13const sizeStyles: Record<AvatarSize, string> = {
14 sm: "w-7 h-7 text-[10px]",
15 md: "w-9 h-9 text-[12px]",
16 lg: "w-12 h-12 text-[14px]",
17};
18
19function getInitials(name: string): string {
20 return name
21 .split(" ")
22 .map((w) => w[0])
23 .slice(0, 2)
24 .join("")
25 .toLowerCase();
26}
27
28export function Avatar({
29 src,
30 alt = "",
31 name,
32 size = "md",
33 className = "",
34}: AvatarProps) {
35 const initials = name ? getInitials(name) : "?";
36
37 return (
38 <div
39 className={[
40 "relative shrink-0 rounded-full overflow-hidden flex items-center justify-center font-semibold text-neutral-600 select-none",
41 sizeStyles[size],
42 className,
43 ]
44 .filter(Boolean)
45 .join(" ")}
46 style={{ background: "var(--face-light)" }}
47 >
48 {src ? (
49 // eslint-disable-next-line @next/next/no-img-element
50 <img src={src} alt={alt || name || ""} loading="lazy" className="w-full h-full object-cover" />
51 ) : (
52 <span role="img" aria-label={name ?? "Unknown"}>{initials}</span>
53 )}
54 <span
55 aria-hidden
56 className="pointer-events-none absolute inset-0 rounded-full"
57 style={{
58 boxShadow: "inset 0 2px 3px -2px rgba(255,255,255,0.45), inset 0 0 2px rgba(17,17,17,0.12), inset 0 -2px 3px -2px rgba(17,17,17,0.04)",
59 }}
60 />
61 </div>
62 );
63}
64
65interface AvatarGroupProps {
66 children: React.ReactNode;
67 max?: number;
68 size?: AvatarSize;
69 className?: string;
70}
71
72const overlapStyles: Record<AvatarSize, string> = {
73 sm: "-ml-2",
74 md: "-ml-2.5",
75 lg: "-ml-3",
76};
77
78export function AvatarGroup({
79 children,
80 max = 5,
81 size = "md",
82 className = "",
83}: AvatarGroupProps) {
84 const items = React.Children.toArray(children);
85 const visible = items.slice(0, max);
86 const overflow = items.length - max;
87
88 return (
89 <div className={["flex items-center", className].join(" ")}>
90 {visible.map((child, i) => (
91 <div
92 key={i}
93 className={[
94 "ring-2 ring-white rounded-full",
95 i > 0 ? overlapStyles[size] : "",
96 ].join(" ")}
97 >
98 {child}
99 </div>
100 ))}
101 {overflow > 0 && (
102 <div
103 className={[
104 "ring-2 ring-white rounded-full flex items-center justify-center font-semibold text-neutral-600 select-none",
105 overlapStyles[size],
106 sizeStyles[size],
107 ].join(" ")}
108 style={{ background: "var(--well)", boxShadow: "var(--well-shadow)" }}
109 aria-label={`${overflow} more`}
110 >
111 +{overflow}
112 </div>
113 )}
114 </div>
115 );
116}