agent-ecosystem/src/shared/constants/memberColors.ts

138 lines
3.8 KiB
TypeScript

/**
* Pre-ordered color palette for team members.
* Colors are arranged so that consecutive entries are maximally distant
* on the hue wheel — the first N members always get visually distinct colors.
* Generated via greedy max-min-distance algorithm over hue angles.
* Intentionally excludes purple-family tones.
*/
export const MEMBER_COLOR_PALETTE = [
// ── First 12: intentionally distinct visual families for roster readability ──
'blue',
'saffron',
'turquoise',
'brick',
'indigo',
'forest',
'apricot',
'rose',
'cerulean',
'olive',
'copper',
'steel',
// ── Next 12: secondary accents after the core distinct set ──
'gold',
'emerald',
'cobalt',
'crimson',
'tangerine',
'denim',
'cyan',
'sage',
'tomato',
'rust',
'mustard',
'canary',
'teal',
'arctic',
'royal',
// ── Remaining: fill the hue gaps progressively ──
'green',
'pink',
'ruby', // 92°
'sienna', // 144°
'mint', // 216°
'sky', // 275°
'sapphire', // 321°
'yellow', // 13°
'red', // 26°
'orange', // 33°
'coral', // 52°
'scarlet', // 65°
'salmon', // 79°
'amber', // 98°
'peach', // 111°
'bronze', // 137°
'lemon', // 157°
'honey', // 170°
'marigold', // 183°
'sunflower', // 196°
'lime', // 209°
'jade', // 236°
'chartreuse', // 249°
'aqua', // 262°
'azure', // 281°
'seafoam', // 295°
'periwinkle', // 327°
'cornflower', // 353°
] as const;
export type MemberColorName = (typeof MEMBER_COLOR_PALETTE)[number];
/**
* Canonical runtime/member id for the team lead.
* UI surfaces should use this exact key when deriving the lead color so
* previews match the resolved team roster.
*/
export const TEAM_LEAD_MEMBER_COLOR_ID = 'team-lead' as const;
/**
* Fixed hue angle (0-359) for each palette color name.
* The first roster-assigned colors are intentionally spaced far apart so the
* first 10-12 teammates in a team remain visually distinct.
*/
const MEMBER_COLOR_HUES_BY_ORDER = [
240, 60, 180, 0, 120, 300, 30, 210, 330, 90, 150, 270, 15, 195, 105, 285, 45, 225, 135, 315, 75,
255, 165, 345, 7.5, 187.5, 97.5, 277.5, 37.5, 217.5, 127.5, 307.5, 67.5, 247.5, 157.5, 337.5,
22.5, 202.5, 112.5, 292.5, 52.5, 232.5, 142.5, 322.5, 82.5, 262.5, 172.5, 352.5, 11.25, 191.25,
101.25, 281.25, 41.25, 221.25, 131.25,
] as const;
export const MEMBER_COLOR_HUE: Record<string, number> = Object.fromEntries(
MEMBER_COLOR_PALETTE.map((colorName, index) => [colorName, MEMBER_COLOR_HUES_BY_ORDER[index]])
) as Record<string, number>;
const DISALLOWED_MEMBER_COLORS = new Set([
'purple',
'violet',
'plum',
'amethyst',
'lavender',
'orchid',
'magenta',
'fuchsia',
'berry',
]);
export function getMemberColor(index: number): string {
return MEMBER_COLOR_PALETTE[index % MEMBER_COLOR_PALETTE.length];
}
/**
* Simple deterministic hash for a string → non-negative integer.
* Uses djb2 algorithm for good distribution across the palette.
*/
function hashStringToIndex(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
}
return Math.abs(hash);
}
export function normalizeMemberColorName(colorName: string): string {
const normalized = colorName.trim().toLowerCase();
if (!normalized) return MEMBER_COLOR_PALETTE[0];
if (!DISALLOWED_MEMBER_COLORS.has(normalized)) return normalized;
return MEMBER_COLOR_PALETTE[hashStringToIndex(normalized) % MEMBER_COLOR_PALETTE.length];
}
/**
* Get a stable color for a member name.
* The color is deterministic — same name always maps to the same palette entry,
* regardless of member order or team size.
*/
export function getMemberColorByName(name: string): string {
return MEMBER_COLOR_PALETTE[hashStringToIndex(name) % MEMBER_COLOR_PALETTE.length];
}