feat: hashColor utility with FNV-1a for deterministic badge/avatar colors
This commit is contained in:
50
src/design-system/utils/hashColor.test.ts
Normal file
50
src/design-system/utils/hashColor.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { hashColor, fnv1a } from './hashColor'
|
||||||
|
|
||||||
|
describe('fnv1a', () => {
|
||||||
|
it('returns a number for any string', () => {
|
||||||
|
expect(typeof fnv1a('test')).toBe('number')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns consistent hash for same input', () => {
|
||||||
|
expect(fnv1a('order-service')).toBe(fnv1a('order-service'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns different hashes for different inputs', () => {
|
||||||
|
expect(fnv1a('order-service')).not.toBe(fnv1a('payment-service'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('hashColor', () => {
|
||||||
|
it('returns bg, text, and border properties', () => {
|
||||||
|
const result = hashColor('test')
|
||||||
|
expect(result).toHaveProperty('bg')
|
||||||
|
expect(result).toHaveProperty('text')
|
||||||
|
expect(result).toHaveProperty('border')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns HSL color strings', () => {
|
||||||
|
const result = hashColor('order-service')
|
||||||
|
expect(result.bg).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
|
||||||
|
expect(result.text).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
|
||||||
|
expect(result.border).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns consistent colors for same name', () => {
|
||||||
|
const a = hashColor('VIEWER')
|
||||||
|
const b = hashColor('VIEWER')
|
||||||
|
expect(a).toEqual(b)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns different hues for different names', () => {
|
||||||
|
const a = hashColor('VIEWER')
|
||||||
|
const b = hashColor('OPERATOR')
|
||||||
|
expect(a.bg).not.toBe(b.bg)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts dark mode parameter', () => {
|
||||||
|
const light = hashColor('test', 'light')
|
||||||
|
const dark = hashColor('test', 'dark')
|
||||||
|
expect(light.bg).not.toBe(dark.bg)
|
||||||
|
})
|
||||||
|
})
|
||||||
32
src/design-system/utils/hashColor.ts
Normal file
32
src/design-system/utils/hashColor.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const FNV_OFFSET = 2166136261
|
||||||
|
const FNV_PRIME = 16777619
|
||||||
|
|
||||||
|
export function fnv1a(str: string): number {
|
||||||
|
let hash = FNV_OFFSET
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash ^= str.charCodeAt(i)
|
||||||
|
hash = Math.imul(hash, FNV_PRIME)
|
||||||
|
}
|
||||||
|
return hash >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashColor(
|
||||||
|
name: string,
|
||||||
|
theme: 'light' | 'dark' = 'light',
|
||||||
|
): { bg: string; text: string; border: string } {
|
||||||
|
const hue = fnv1a(name) % 360
|
||||||
|
|
||||||
|
if (theme === 'dark') {
|
||||||
|
return {
|
||||||
|
bg: `hsl(${hue}, 35%, 20%)`,
|
||||||
|
text: `hsl(${hue}, 45%, 75%)`,
|
||||||
|
border: `hsl(${hue}, 30%, 30%)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bg: `hsl(${hue}, 45%, 92%)`,
|
||||||
|
text: `hsl(${hue}, 55%, 35%)`,
|
||||||
|
border: `hsl(${hue}, 35%, 82%)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user