agent-ecosystem/src/shared/utils/teamMemberColors.ts

107 lines
3.1 KiB
TypeScript

import {
getMemberColorByName,
MEMBER_COLOR_PALETTE,
normalizeMemberColorName,
TEAM_LEAD_MEMBER_COLOR_ID,
} from '@shared/constants/memberColors';
import { isLeadMember } from '@shared/utils/leadDetection';
export interface TeamMemberColorInput {
name: string;
color?: string;
removedAt?: number | string | null;
agentType?: string;
}
interface BuildTeamMemberColorMapOptions {
preferProvidedColors?: boolean;
}
/**
* Build a deterministic roster color map that optimizes contrast inside a team.
* Leads reserve their own color but do not consume the teammate palette order.
*/
export function buildTeamMemberColorMap(
members: readonly TeamMemberColorInput[],
options: BuildTeamMemberColorMapOptions = {}
): Map<string, string> {
const preferProvidedColors = options.preferProvidedColors ?? true;
const map = new Map<string, string>();
const active = members.filter((member) => !member.removedAt);
const removed = members.filter((member) => member.removedAt);
const activeLeads = active.filter((member) => isLeadMember(member));
const activeTeammates = active.filter((member) => !isLeadMember(member));
const usedColors = new Set<string>();
let nextPaletteIdx = 0;
for (const member of activeLeads) {
const color =
preferProvidedColors && member.color
? normalizeMemberColorName(member.color)
: getMemberColorByName(member.name);
map.set(member.name, color);
usedColors.add(color);
}
for (const member of activeTeammates) {
let color =
preferProvidedColors && member.color ? normalizeMemberColorName(member.color) : undefined;
if (!color || usedColors.has(color)) {
while (
nextPaletteIdx < MEMBER_COLOR_PALETTE.length &&
usedColors.has(MEMBER_COLOR_PALETTE[nextPaletteIdx])
) {
nextPaletteIdx += 1;
}
color =
nextPaletteIdx < MEMBER_COLOR_PALETTE.length
? MEMBER_COLOR_PALETTE[nextPaletteIdx]
: MEMBER_COLOR_PALETTE[activeTeammates.indexOf(member) % MEMBER_COLOR_PALETTE.length];
nextPaletteIdx += 1;
}
map.set(member.name, color);
usedColors.add(color);
}
for (const member of removed) {
const color =
preferProvidedColors && member.color
? normalizeMemberColorName(member.color)
: getMemberColorByName(member.name);
map.set(member.name, color);
}
map.set('user', 'user');
return map;
}
/**
* Resolve the visual color for a standalone member preview by reusing the same
* roster color pipeline that powers the team screen.
*/
export function resolveTeamMemberColorName(
member: TeamMemberColorInput,
options: BuildTeamMemberColorMapOptions = {}
): string {
const color = buildTeamMemberColorMap([member], options).get(member.name);
if (color) {
return color;
}
if (options.preferProvidedColors !== false && member.color) {
return normalizeMemberColorName(member.color);
}
return getMemberColorByName(member.name);
}
export function resolveTeamLeadColorName(): string {
return resolveTeamMemberColorName(
{
name: TEAM_LEAD_MEMBER_COLOR_ID,
agentType: 'team-lead',
},
{ preferProvidedColors: false }
);
}