feat(team): Porträts der digitalen Mitarbeiter aus dem Konzept übernehmen
Bisher zeigten die Avatare Initialen auf einer Tonfarbe. Ich hatte das bewusst so gebaut, weil erfundene Gesichter in einer Kundendemo irreführend wären — das Konzeptdokument «Agenten_Bewirtschaftung_v2.html» trifft diese Entscheidung jedoch bereits: dort hängt an jedem der 36 Agenten ein Porträt als eingebettetes JPEG. Die sieben Kernagenten sind daraus extrahiert. - src/assets/team/*.jpg — sieben Porträts, je rund 6 KB, Zuordnung über die Mailadresse aus dem Konzept, die exakt unseren Agenten-IDs entspricht - agentPhotos.ts hält die Zuordnung in der Darstellungsschicht. Ein Domain-Typ, der Binärdateien importiert, wäre an den Bundler gekoppelt und weder im Test noch später gegen ein echtes Backend sauber verwendbar. - Fehlt zu einer ID ein Bild, zeigt der Avatar weiterhin die Initialen; dasselbe greift, wenn das Laden fehlschlägt Der zugängliche Name hängt weiterhin an genau einem Element, nie an zweien: mit Porträt am erzeugten <img> über `alt`, ohne Porträt am Container über role="img" und aria-label. vite-env.d.ts ergänzt — die Referenz auf vite/client fehlte, ohne sie kennt TypeScript die Modul-Deklarationen für Assets nicht. 415 Tests grün, Build grün, die sieben JPEGs werden korrekt als Assets emittiert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -4,6 +4,7 @@ import { Check } from 'lucide-react'
|
||||
import type { TeamAgent } from '../../domain/teamAgent'
|
||||
import { AgentStatus } from '../../domain/teamAgent'
|
||||
import { AVATAR_TONE_COLOR } from './teamTokens'
|
||||
import { AGENT_PHOTOS } from './agentPhotos'
|
||||
import { AGENT_STATUS_LABELS } from '../../lib/constants'
|
||||
import { DS_BG, DS_TEXT } from '../../lib/ds'
|
||||
|
||||
@@ -39,15 +40,20 @@ export const AgentAvatar = memo(function AgentAvatar({ agent, size = 'medium', s
|
||||
const isActive = agent.status === AgentStatus.ACTIVE
|
||||
const statusLabel = AGENT_STATUS_LABELS[agent.status] ?? agent.status
|
||||
const badgePx = size === 'large' ? 20 : 14
|
||||
const photo = AGENT_PHOTOS[agent.id]
|
||||
const label = `${agent.name}, ${agent.role}`
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative', width: px, height: px, flexShrink: 0 }}>
|
||||
{/* `alt` reicht MUI ausschliesslich an den img-Slot weiter, und ohne `src`
|
||||
rendert Avatar gar kein <img> — die Beschriftung landete damit nirgends
|
||||
im DOM. Der zugängliche Name muss deshalb direkt auf das Element. */}
|
||||
{/* Der zugängliche Name hängt an genau einem Element, nie an zweien:
|
||||
mit Porträt trägt ihn das von MUI erzeugte <img> über `alt`, ohne
|
||||
Porträt der Container über role="img" und aria-label. `alt` allein
|
||||
genügt nämlich nicht — ohne `src` rendert MUI gar kein <img>, und der
|
||||
Wert landet nirgends im DOM. */}
|
||||
<Avatar
|
||||
role="img"
|
||||
aria-label={`${agent.name}, ${agent.role}`}
|
||||
src={photo}
|
||||
alt={photo ? label : undefined}
|
||||
{...(photo ? {} : { role: 'img', 'aria-label': label })}
|
||||
sx={{
|
||||
width: px,
|
||||
height: px,
|
||||
@@ -57,6 +63,7 @@ export const AgentAvatar = memo(function AgentAvatar({ agent, size = 'medium', s
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{/* Fällt das Bild aus, zeigt MUI wieder die Initialen statt eines Lochs. */}
|
||||
{initialsOf(agent.name)}
|
||||
</Avatar>
|
||||
|
||||
|
||||
@@ -139,9 +139,20 @@ describe('AgentCard', () => {
|
||||
expect(within(card).getByText(/Zuletzt aktiv|Noch nicht gelaufen/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('rendert den Avatar mit den Initialen des Mitarbeiters', () => {
|
||||
it('rendert das Porträt des Mitarbeiters', () => {
|
||||
renderWithProviders(<AgentCard agent={ferdi} onOpen={vi.fn()} />)
|
||||
|
||||
const portrait = screen.getByRole('img', { name: `${ferdi.name}, ${ferdi.role}` })
|
||||
expect(portrait).toHaveAttribute('src')
|
||||
expect(portrait.getAttribute('src')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('fällt ohne hinterlegtes Porträt auf die Initialen zurück', () => {
|
||||
// Ein Mitarbeiter, zu dem in `agentPhotos.ts` kein Bild liegt — so verhält
|
||||
// sich die Karte, falls der Katalog später um jemanden ohne Foto wächst.
|
||||
const ohneFoto = { ...ferdi, id: 'unbekannt' }
|
||||
renderWithProviders(<AgentCard agent={ohneFoto} onOpen={vi.fn()} />)
|
||||
|
||||
const card = screen.getByRole('article')
|
||||
expect(within(card).getByText(ferdi.name.slice(0, 2).toUpperCase())).toBeInTheDocument()
|
||||
})
|
||||
@@ -149,9 +160,8 @@ describe('AgentCard', () => {
|
||||
it('gibt dem Avatar einen zugänglichen Namen aus Name und Rolle', () => {
|
||||
renderWithProviders(<AgentCard agent={ferdi} onOpen={vi.fn()} />)
|
||||
|
||||
// `alt` allein genügt bei MUI nicht: ohne `src` rendert Avatar kein <img>,
|
||||
// und der Wert landet nirgends im DOM. Der Name muss deshalb über
|
||||
// role="img" + aria-label direkt am Element hängen (§18).
|
||||
// Der Name hängt an genau einem Element: mit Porträt am <img> über `alt`,
|
||||
// ohne Porträt am Container über role="img" + aria-label (§18).
|
||||
expect(
|
||||
screen.getByRole('img', { name: `${ferdi.name}, ${ferdi.role}` }),
|
||||
).toBeInTheDocument()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Property On — Porträts der digitalen Mitarbeiter.
|
||||
*
|
||||
* Die Bilder stammen aus dem Konzeptdokument «Agenten_Bewirtschaftung_v2.html»,
|
||||
* wo sie als eingebettete JPEGs am jeweiligen Agenten hängen. Sie sind damit
|
||||
* Teil der fachlichen Vorgabe, nicht eine gestalterische Zutat: das digitale
|
||||
* Personalbüro zeigt Mitarbeiter mit Gesicht, nicht abstrakte Werkzeuge.
|
||||
*
|
||||
* Die Zuordnung liegt bewusst in der Darstellungsschicht und nicht in
|
||||
* `domain/teamAgent.ts` oder den Mockdaten. Ein Domain-Typ, der Binärdateien
|
||||
* importiert, wäre an den Bundler gekoppelt und liesse sich weder in einem Test
|
||||
* noch später gegen ein echtes Backend sauber verwenden.
|
||||
*
|
||||
* Fehlt zu einer ID ein Bild, fällt `AgentAvatar` auf die Initialen zurück.
|
||||
*/
|
||||
|
||||
import bruno from '../../assets/team/bruno.jpg'
|
||||
import ferdi from '../../assets/team/ferdi.jpg'
|
||||
import lea from '../../assets/team/lea.jpg'
|
||||
import livia from '../../assets/team/livia.jpg'
|
||||
import nora from '../../assets/team/nora.jpg'
|
||||
import reto from '../../assets/team/reto.jpg'
|
||||
import sina from '../../assets/team/sina.jpg'
|
||||
|
||||
export const AGENT_PHOTOS: Record<string, string | undefined> = {
|
||||
bruno,
|
||||
ferdi,
|
||||
lea,
|
||||
livia,
|
||||
nora,
|
||||
reto,
|
||||
sina,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// Ohne diese Referenz kennt TypeScript die von Vite bereitgestellten
|
||||
// Modul-Deklarationen für Assets nicht — ein `import portrait from './x.jpg'`
|
||||
// würde als Fehler gemeldet, obwohl der Bundler es korrekt auflöst.
|
||||