agent-ecosystem/src/renderer/components/team/MemberBadge.tsx
2026-05-04 09:12:53 +03:00

121 lines
3.6 KiB
TypeScript

import { memo, useMemo } from 'react';
import {
getTeamColorSet,
getThemedBadge,
getThemedBorder,
getThemedText,
} from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import {
agentAvatarUrl,
buildMemberAvatarMap,
displayMemberName,
} from '@renderer/utils/memberHelpers';
import { MemberHoverCard } from './members/MemberHoverCard';
interface MemberBadgeProps {
name: string;
color?: string;
/** Owning team context for hover-card store lookups. */
teamName?: string;
/** Avatar + badge size variant */
size?: 'xs' | 'sm' | 'md';
/** Hide the avatar icon, show only the name badge */
hideAvatar?: boolean;
onClick?: (name: string) => void;
/** Disable the hover card (e.g. inside MemberHoverCard itself to avoid nesting) */
disableHoverCard?: boolean;
}
/**
* Reusable member avatar + colored name badge.
* Avatar is rendered OUTSIDE the badge, to the left.
* When onClick is provided, both avatar and badge are clickable as one unit.
* Wrapped in MemberHoverCard to show member info on hover.
*/
export const MemberBadge = memo(
({
name,
color,
teamName,
size = 'sm',
hideAvatar,
onClick,
disableHoverCard,
}: MemberBadgeProps): React.JSX.Element => {
const colors = getTeamColorSet(color ?? '');
const { isLight } = useTheme();
const selectedTeamName = useStore((s) => s.selectedTeamName);
const effectiveTeamName = teamName ?? selectedTeamName;
const teamMembers = useStore((s) =>
effectiveTeamName ? selectResolvedMembersForTeamName(s, effectiveTeamName) : []
);
const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]);
const avatarSize = size === 'md' ? 32 : size === 'sm' ? 24 : 18;
const avatarClass = size === 'md' ? 'size-6' : size === 'sm' ? 'size-5' : 'size-4';
const textClass = size === 'md' ? 'text-xs' : size === 'sm' ? 'text-[10px]' : 'text-[9px]';
const paddingClass = size === 'xs' ? 'px-1 py-0.5' : 'px-1.5 py-0.5';
const badgeStyle = {
backgroundColor: getThemedBadge(colors, isLight),
color: getThemedText(colors, isLight),
border: `1px solid ${getThemedBorder(colors, isLight)}40`,
};
const avatar = (
<img
src={avatarMap.get(name) ?? agentAvatarUrl(name, avatarSize)}
alt=""
className={`${avatarClass} shrink-0 rounded-full bg-[var(--color-surface-raised)]`}
loading="lazy"
/>
);
const badge = (
<span
className={`rounded ${paddingClass} ${textClass} font-medium tracking-wide`}
style={badgeStyle}
>
{displayMemberName(name)}
</span>
);
// Skip hover card for "user" and "system" pseudo-members
const skipHoverCard = disableHoverCard || name === 'user' || name === 'system';
const content = onClick ? (
<button
type="button"
className="inline-flex items-center gap-1 rounded transition-opacity hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--color-border)]"
onClick={(e) => {
e.stopPropagation();
onClick(name);
}}
>
{!hideAvatar && avatar}
{badge}
</button>
) : (
<span className="inline-flex items-center gap-1">
{!hideAvatar && avatar}
{badge}
</span>
);
if (skipHoverCard) {
return content;
}
return (
<MemberHoverCard name={name} color={color} teamName={teamName}>
{content}
</MemberHoverCard>
);
}
);
MemberBadge.displayName = 'MemberBadge';