feat(ui): deterministic avatar initials + hue helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/client-detail-redesign
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…
Reference in New Issue