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 @@
+
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);
+}