/** * 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 = Object.fromEntries( MEMBER_COLOR_PALETTE.map((colorName, index) => [colorName, MEMBER_COLOR_HUES_BY_ORDER[index]]) ) as Record; 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]; }