feat(ui): deterministic avatar initials + hue helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent fccc189b2c
commit b767a0c191

@ -0,0 +1,28 @@
/** Deterministic identity visuals: initials + a stable hue from the name. */
export function initials(name: string): string {
const words = name.trim().split(/\s+/).filter((w) => w !== '')
if (words.length === 0) return '?'
const first = words[0]!.charAt(0)
const second = words.length > 1 ? words[1]!.charAt(0) : ''
return (first + second).toUpperCase()
}
/** FNV-1a over the name → hue bucket; same name always gets the same colour. */
export function avatarHue(name: string): number {
let h = 0x811c9dc5
for (let i = 0; i < name.length; i++) {
h ^= name.charCodeAt(i)
h = Math.imul(h, 0x01000193)
}
return (h >>> 0) % 360
}
/** Soft tinted background + readable foreground for both themes. */
export function avatarColors(name: string): { bg: string; fg: string } {
const h = avatarHue(name)
return {
bg: `hsl(${h} 42% 50% / 0.16)`,
fg: `hsl(${h} 45% 38%)`,
}
}

@ -1,3 +1,4 @@
export * from './avatar'
export * from './components' export * from './components'
export * from './theme' export * from './theme'
export * from './ThemeSwitcher' export * from './ThemeSwitcher'

@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import { avatarColors, avatarHue, initials } from '../src/avatar'
describe('initials', () => {
it('takes the first letter of the first two words', () => {
expect(initials('Acme UK Compliance Group')).toBe('AU')
expect(initials('hamiltel')).toBe('H')
})
it('falls back to ? for empty input', () => {
expect(initials('')).toBe('?')
expect(initials(' ')).toBe('?')
})
})
describe('avatarHue', () => {
it('is deterministic', () => {
expect(avatarHue('Hamiltel')).toBe(avatarHue('Hamiltel'))
})
it('stays in [0, 360)', () => {
for (const n of ['a', 'Acme', 'Zed Systems', '৪২']) {
const h = avatarHue(n)
expect(h).toBeGreaterThanOrEqual(0)
expect(h).toBeLessThan(360)
}
})
})
describe('avatarColors', () => {
it('returns hsl strings derived from the hue', () => {
const { bg, fg } = avatarColors('Hamiltel')
const h = avatarHue('Hamiltel')
expect(bg).toContain(`hsl(${h}`)
expect(fg).toContain(`hsl(${h}`)
})
})
Loading…
Cancel
Save