feat: añade paleta determinista por group_id y en TaskItem; añade tests
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>webui
parent
f0eb723020
commit
5f03caace6
@ -0,0 +1,53 @@
|
||||
export type GroupColor = {
|
||||
border: string;
|
||||
bg: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const PALETTE: GroupColor[] = [
|
||||
// 1) Blue
|
||||
{ border: '#2563EB', bg: '#DBEAFE', text: '#1E3A8A' },
|
||||
// 2) Indigo
|
||||
{ border: '#4F46E5', bg: '#E0E7FF', text: '#312E81' },
|
||||
// 3) Violet
|
||||
{ border: '#7C3AED', bg: '#EDE9FE', text: '#4C1D95' },
|
||||
// 4) Purple
|
||||
{ border: '#9333EA', bg: '#F3E8FF', text: '#581C87' },
|
||||
// 5) Fuchsia
|
||||
{ border: '#C026D3', bg: '#FAE8FF', text: '#701A75' },
|
||||
// 6) Pink
|
||||
{ border: '#DB2777', bg: '#FCE7F3', text: '#831843' },
|
||||
// 7) Rose
|
||||
{ border: '#E11D48', bg: '#FFE4E6', text: '#881337' },
|
||||
// 8) Red
|
||||
{ border: '#DC2626', bg: '#FEE2E2', text: '#7F1D1D' },
|
||||
// 9) Orange
|
||||
{ border: '#EA580C', bg: '#FFE7D1', text: '#7C2D12' },
|
||||
// 10) Amber
|
||||
{ border: '#D97706', bg: '#FEF3C7', text: '#78350F' },
|
||||
// 11) Green
|
||||
{ border: '#16A34A', bg: '#DCFCE7', text: '#14532D' },
|
||||
// 12) Teal
|
||||
{ border: '#0D9488', bg: '#CCFBF1', text: '#134E4A' }
|
||||
];
|
||||
|
||||
function hashString(input: string): number {
|
||||
// Hash sencillo y rápido (similar a multiplicador 31)
|
||||
let h = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
h = (h * 31 + input.charCodeAt(i)) | 0;
|
||||
}
|
||||
// Convertir a entero positivo de 32 bits
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve un esquema de color determinista para un groupId dado.
|
||||
* - Si groupId es falsy o vacío, devuelve null (usar estilos neutros por defecto).
|
||||
*/
|
||||
export function colorForGroup(groupId: string | null | undefined): GroupColor | null {
|
||||
const s = String(groupId || '').trim();
|
||||
if (!s) return null;
|
||||
const idx = hashString(s) % PALETTE.length;
|
||||
return PALETTE[idx];
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { colorForGroup } from '../../../apps/web/src/lib/utils/groupColor';
|
||||
|
||||
function isHexColor(s: string): boolean {
|
||||
return /^#[0-9A-Fa-f]{6}$/.test(s);
|
||||
}
|
||||
|
||||
describe('groupColor - colorForGroup', () => {
|
||||
it('devuelve null para groupId vacío o nulo', () => {
|
||||
expect(colorForGroup(null)).toBeNull();
|
||||
expect(colorForGroup(undefined)).toBeNull();
|
||||
expect(colorForGroup('')).toBeNull();
|
||||
expect(colorForGroup(' ')).toBeNull();
|
||||
});
|
||||
|
||||
it('es determinista: misma entrada → misma salida', () => {
|
||||
const a = colorForGroup('123@g.us');
|
||||
const b = colorForGroup('123@g.us');
|
||||
expect(a).not.toBeNull();
|
||||
expect(b).not.toBeNull();
|
||||
expect(a?.border).toBe(b?.border);
|
||||
expect(a?.bg).toBe(b?.bg);
|
||||
expect(a?.text).toBe(b?.text);
|
||||
});
|
||||
|
||||
it('devuelve colores hex válidos', () => {
|
||||
const c = colorForGroup('group-xyz@g.us');
|
||||
expect(c).not.toBeNull();
|
||||
expect(isHexColor(c!.border)).toBe(true);
|
||||
expect(isHexColor(c!.bg)).toBe(true);
|
||||
expect(isHexColor(c!.text)).toBe(true);
|
||||
});
|
||||
|
||||
it('tiene distribución razonable en distintos IDs', () => {
|
||||
const uniq = new Set<string>();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const c = colorForGroup(`group-${i}@g.us`);
|
||||
if (c) uniq.add(`${c.border}|${c.bg}|${c.text}`);
|
||||
}
|
||||
// Con 12 paletas, deberíamos cubrir bastantes índices con 30 IDs
|
||||
expect(uniq.size).toBeGreaterThan(8);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue