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 type { TeamAgent } from '../../domain/teamAgent'
|
||||||
import { AgentStatus } from '../../domain/teamAgent'
|
import { AgentStatus } from '../../domain/teamAgent'
|
||||||
import { AVATAR_TONE_COLOR } from './teamTokens'
|
import { AVATAR_TONE_COLOR } from './teamTokens'
|
||||||
|
import { AGENT_PHOTOS } from './agentPhotos'
|
||||||
import { AGENT_STATUS_LABELS } from '../../lib/constants'
|
import { AGENT_STATUS_LABELS } from '../../lib/constants'
|
||||||
import { DS_BG, DS_TEXT } from '../../lib/ds'
|
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 isActive = agent.status === AgentStatus.ACTIVE
|
||||||
const statusLabel = AGENT_STATUS_LABELS[agent.status] ?? agent.status
|
const statusLabel = AGENT_STATUS_LABELS[agent.status] ?? agent.status
|
||||||
const badgePx = size === 'large' ? 20 : 14
|
const badgePx = size === 'large' ? 20 : 14
|
||||||
|
const photo = AGENT_PHOTOS[agent.id]
|
||||||
|
const label = `${agent.name}, ${agent.role}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'relative', width: px, height: px, flexShrink: 0 }}>
|
<Box sx={{ position: 'relative', width: px, height: px, flexShrink: 0 }}>
|
||||||
{/* `alt` reicht MUI ausschliesslich an den img-Slot weiter, und ohne `src`
|
{/* Der zugängliche Name hängt an genau einem Element, nie an zweien:
|
||||||
rendert Avatar gar kein <img> — die Beschriftung landete damit nirgends
|
mit Porträt trägt ihn das von MUI erzeugte <img> über `alt`, ohne
|
||||||
im DOM. Der zugängliche Name muss deshalb direkt auf das Element. */}
|
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
|
<Avatar
|
||||||
role="img"
|
src={photo}
|
||||||
aria-label={`${agent.name}, ${agent.role}`}
|
alt={photo ? label : undefined}
|
||||||
|
{...(photo ? {} : { role: 'img', 'aria-label': label })}
|
||||||
sx={{
|
sx={{
|
||||||
width: px,
|
width: px,
|
||||||
height: px,
|
height: px,
|
||||||
@@ -57,6 +63,7 @@ export const AgentAvatar = memo(function AgentAvatar({ agent, size = 'medium', s
|
|||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Fällt das Bild aus, zeigt MUI wieder die Initialen statt eines Lochs. */}
|
||||||
{initialsOf(agent.name)}
|
{initialsOf(agent.name)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
|
|||||||
@@ -139,9 +139,20 @@ describe('AgentCard', () => {
|
|||||||
expect(within(card).getByText(/Zuletzt aktiv|Noch nicht gelaufen/)).toBeInTheDocument()
|
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()} />)
|
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')
|
const card = screen.getByRole('article')
|
||||||
expect(within(card).getByText(ferdi.name.slice(0, 2).toUpperCase())).toBeInTheDocument()
|
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', () => {
|
it('gibt dem Avatar einen zugänglichen Namen aus Name und Rolle', () => {
|
||||||
renderWithProviders(<AgentCard agent={ferdi} onOpen={vi.fn()} />)
|
renderWithProviders(<AgentCard agent={ferdi} onOpen={vi.fn()} />)
|
||||||
|
|
||||||
// `alt` allein genügt bei MUI nicht: ohne `src` rendert Avatar kein <img>,
|
// Der Name hängt an genau einem Element: mit Porträt am <img> über `alt`,
|
||||||
// und der Wert landet nirgends im DOM. Der Name muss deshalb über
|
// ohne Porträt am Container über role="img" + aria-label (§18).
|
||||||
// role="img" + aria-label direkt am Element hängen (§18).
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('img', { name: `${ferdi.name}, ${ferdi.role}` }),
|
screen.getByRole('img', { name: `${ferdi.name}, ${ferdi.role}` }),
|
||||||
).toBeInTheDocument()
|
).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.
|
||||||