// Theme-aware color palette for projects // Uses oklch for better perceptual uniformity and theme compatibility // These are base hues that work in both light and dark modes import { isValidProjectColor } from "./color"; // Default color for unassigned/fallback - neutral gray that works in both themes export const UNASSIGNED_COLOR = "oklch(0.55 0.02 260)"; // Golden angle distribution for visually distinct hues (based on Fibonacci) const GOLDEN_ANGLE = 137.508; // Base hues distributed using golden angle for maximum visual distinction const BASE_HUES = [ 0, // red/rose 34, // orange 60, // yellow-gold 145, // teal/cyan 210, // blue 265, // purple/violet 300, // pink/magenta 90, // lime/green 180, // cyan 240, // indigo ] as const; let colorIndex = 0; /** * Generate a simple hash from a string */ function hashString(str: string): number { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); } /** * Generate a deterministic oklch color from a string * Uses oklch for better perceptual uniformity across light/dark themes * Returns an oklch string that works with CSS */ export function getColorForName(name: string): string { const hash = hashString(name); // Use golden angle distribution for better hue spread const hueIndex = hash % BASE_HUES.length; const baseHue = BASE_HUES[hueIndex]; // Add small variation based on hash for uniqueness within same hue family const hueVariation = ((hash >> 8) % 30) - 15; // -15 to +15 degrees const hue = (baseHue + hueVariation + 360) % 360; // oklch values tuned for both light and dark mode visibility // L: 0.6 works in both modes (not too light, not too dark) // C: 0.15 is vibrant but not overwhelming const lightness = 0.55 + ((hash >> 4) % 10) / 100; // 0.55-0.65 const chroma = 0.12 + ((hash >> 6) % 6) / 100; // 0.12-0.18 return `oklch(${lightness} ${chroma} ${hue})`; } /** * Get the next color using golden angle distribution */ export function getNextColor(): string { const hue = (colorIndex * GOLDEN_ANGLE) % 360; colorIndex++; return `oklch(0.6 0.15 ${hue})`; } /** * Reset the color index (useful when starting fresh) */ export function resetColorIndex(): void { colorIndex = 0; } /** * Get a color at a specific index using golden angle distribution */ export function getColorAt(index: number): string { const hue = (index * GOLDEN_ANGLE) % 360; return `oklch(0.6 0.15 ${hue})`; } /** * Deterministic default accent color for a team, derived from its stable id so * a team that has never chosen an explicit themeColor still gets a distinct, * theme-aware color. Same hashing as project colors for a consistent palette. */ export function getDefaultTeamColor(teamId: string): string { return getColorForName(teamId); } /** * Resolve a team's accent color: the stored `themeColor` when it is a valid * persistable color (strict hex / numeric oklch), otherwise the deterministic * per-team default. Used by app chrome that inherits the active team's color. */ export function resolveTeamColor( teamId: string, themeColor?: string | null, ): string { return typeof themeColor === "string" && isValidProjectColor(themeColor) ? themeColor.trim() : getDefaultTeamColor(teamId); }