From e10ab44dc7df7b0f3fcb45ecc3c004fc1e7b980d Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Mon, 21 Sep 2026 14:29:22 +0200 Subject: [PATCH] feat(frontend): composant ev-icon en SVG inline pour les types d'alerte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cinq tracés (spike, threshold, anomaly, outage, sensor) dessinés en trait sur currentColor, dimensionnés par font-size comme ev-brand. Décoratif par défaut (aria-hidden), rôle image et libellé accessible quand `label` est fourni. Aucune bibliothèque : la CSP du reverse proxy interdit les scripts tiers, pas le SVG inline. --- .../app/shared/components/ui/icon/icon.html | 37 ++++++++++++++ .../app/shared/components/ui/icon/icon.scss | 12 +++++ .../shared/components/ui/icon/icon.spec.ts | 49 +++++++++++++++++++ .../src/app/shared/components/ui/icon/icon.ts | 14 ++++++ 4 files changed, 112 insertions(+) create mode 100644 apps/frontend/src/app/shared/components/ui/icon/icon.html create mode 100644 apps/frontend/src/app/shared/components/ui/icon/icon.scss create mode 100644 apps/frontend/src/app/shared/components/ui/icon/icon.spec.ts create mode 100644 apps/frontend/src/app/shared/components/ui/icon/icon.ts diff --git a/apps/frontend/src/app/shared/components/ui/icon/icon.html b/apps/frontend/src/app/shared/components/ui/icon/icon.html new file mode 100644 index 0000000..869ccd1 --- /dev/null +++ b/apps/frontend/src/app/shared/components/ui/icon/icon.html @@ -0,0 +1,37 @@ + + @switch (name()) { + @case ('spike') { + + + } + @case ('threshold') { + + + } + @case ('anomaly') { + + } + @case ('outage') { + + + } + @case ('sensor') { + + + + + + } + } + diff --git a/apps/frontend/src/app/shared/components/ui/icon/icon.scss b/apps/frontend/src/app/shared/components/ui/icon/icon.scss new file mode 100644 index 0000000..c90a9ce --- /dev/null +++ b/apps/frontend/src/app/shared/components/ui/icon/icon.scss @@ -0,0 +1,12 @@ +:host { + display: inline-flex; + flex-shrink: 0; + width: 1em; + height: 1em; + vertical-align: -0.125em; +} + +svg { + width: 100%; + height: 100%; +} diff --git a/apps/frontend/src/app/shared/components/ui/icon/icon.spec.ts b/apps/frontend/src/app/shared/components/ui/icon/icon.spec.ts new file mode 100644 index 0000000..ce3dedc --- /dev/null +++ b/apps/frontend/src/app/shared/components/ui/icon/icon.spec.ts @@ -0,0 +1,49 @@ +import { TestBed } from '@angular/core/testing'; +import { Icon, IconName } from './icon'; + +const NOMS: IconName[] = ['spike', 'threshold', 'anomaly', 'outage', 'sensor']; + +function rendre(name: IconName, label: string | null = null) { + const fixture = TestBed.createComponent(Icon); + fixture.componentRef.setInput('name', name); + fixture.componentRef.setInput('label', label); + fixture.detectChanges(); + return fixture.nativeElement as HTMLElement; +} + +describe('Icon', () => { + beforeEach(() => { + TestBed.configureTestingModule({ imports: [Icon] }); + }); + + it('dessine un tracé distinct pour chacun des cinq types', () => { + const traces = NOMS.map((name) => rendre(name).querySelector('svg')?.innerHTML.trim()); + + for (const trace of traces) { + expect(trace).toBeTruthy(); + } + expect(new Set(traces).size).toBe(NOMS.length); + }); + + it('reste décoratif sans libellé', () => { + const svg = rendre('spike').querySelector('svg'); + + expect(svg?.getAttribute('aria-hidden')).toBe('true'); + expect(svg?.hasAttribute('role')).toBe(false); + }); + + it('expose un rôle image et un libellé accessible quand on lui en donne un', () => { + const svg = rendre('outage', 'Coupure').querySelector('svg'); + + expect(svg?.getAttribute('role')).toBe('img'); + expect(svg?.getAttribute('aria-label')).toBe('Coupure'); + expect(svg?.hasAttribute('aria-hidden')).toBe(false); + }); + + it('hérite de la couleur du parent via currentColor', () => { + const svg = rendre('sensor').querySelector('svg'); + + expect(svg?.getAttribute('stroke')).toBe('currentColor'); + expect(svg?.getAttribute('fill')).toBe('none'); + }); +}); diff --git a/apps/frontend/src/app/shared/components/ui/icon/icon.ts b/apps/frontend/src/app/shared/components/ui/icon/icon.ts new file mode 100644 index 0000000..dc302e9 --- /dev/null +++ b/apps/frontend/src/app/shared/components/ui/icon/icon.ts @@ -0,0 +1,14 @@ +import { Component, input } from '@angular/core'; + +export type IconName = 'spike' | 'threshold' | 'anomaly' | 'outage' | 'sensor'; + +@Component({ + selector: 'ev-icon', + standalone: true, + templateUrl: './icon.html', + styleUrl: './icon.scss', +}) +export class Icon { + name = input.required(); + label = input(null); +}