dd-timer/src/presentation/components/Sidebar.ts
POL Mickaël 61bbac0adc perf: optimisations RAM et rendu + fix logo sidebar en production
- Fix logo sidebar : chemin relatif au lieu d'absolu (cassé en prod)
- Compression icone_sidebar.png : 448KB → 24KB (resize 128x128)
- Cache DOM EnclosView : élimine ~360 querySelector/sec dans le RAF
- Throttle RAF à 4fps quand aucun timer actif (idle)
- Cache version Sidebar : un seul appel IPC au lieu d'un par update()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 11:51:23 +02:00

134 lines
4.6 KiB
TypeScript
Executable File

import type { UIState } from '@presentation/state/UIState';
import type { QueryBus } from '@application/handlers/QueryBus';
import type { DashboardResult } from '@application/queries/GetDashboard';
import { esc } from '@presentation/helpers/format';
export class Sidebar {
private el: HTMLElement | null = null;
private cachedVersion: string | null = null;
constructor(
private uiState: UIState,
private queryBus: QueryBus,
) {}
render(container: HTMLElement): void {
this.el = document.createElement('aside');
this.el.className = 'sidebar-new';
container.appendChild(this.el);
this.update();
this.fetchAndInjectVersion();
}
update(): void {
if (!this.el) return;
const data = this.queryBus.execute<DashboardResult>({ type: 'get-dashboard' });
const enclosList = data.enclosSummaries;
const activeView = this.uiState.activeView;
let html = '';
// ── Header ──
html += `
<div class="sb-header">
<div class="sb-logo-wrap">
<img src="icone_sidebar.png" alt="logo" class="sb-logo-img" />
</div>
<div class="sb-brand">
<span class="sb-brand-name">Obsidienne</span>
<span class="sb-brand-sub">Gestion d'&eacute;levage</span>
</div>
</div>
`;
// ── Nav ──
html += `<div class="sb-nav">`;
// Section Principal
html += `<div class="sb-section">`;
html += `<div class="sb-section-head"><span class="sb-section-label">Principal</span></div>`;
html += this.item('dashboard', 'dashboard', 'Tableau de bord', activeView === 'dashboard');
html += this.item('statistiques', 'bar_chart', 'Statistiques', activeView === 'statistiques');
html += `</div>`;
// Section Enclos
html += `<div class="sb-section">`;
html += `<div class="sb-section-head"><span class="sb-section-label">Enclos</span></div>`;
enclosList.forEach(enc => {
const isActive = activeView === enc.id;
const dotClass = enc.running ? 'running' : 'idle';
html += `<button class="sb-item${isActive ? ' active' : ''}" data-view="${enc.id}">`;
html += `<span class="sb-item-icon material-symbols-outlined">pentagon</span>`;
html += `<span class="sb-item-text">${esc(enc.name)}</span>`;
html += `<span class="sb-dot ${dotClass}"></span>`;
html += `</button>`;
});
html += `</div>`;
// Section Outils
html += `<div class="sb-section">`;
html += `<div class="sb-section-head"><span class="sb-section-label">Outils</span></div>`;
html += this.item('accouplement', 'favorite', 'Accouplement', activeView === 'accouplement');
html += this.item('appro', 'science', 'R\u00e9appro', activeView === 'appro');
html += this.item('inventaire', 'inventory_2', 'Inventaire', activeView === 'inventaire');
html += this.item('workflows', 'account_tree', 'Workflows', activeView === 'workflows');
html += `</div>`;
html += `</div>`; // end sb-nav
// ── Footer ──
html += `
<div class="sb-footer">
${this.item('parametres', 'settings', 'Param\u00e8tres', activeView === 'parametres')}
<div class="sb-version">
<span class="material-symbols-outlined">info</span>
<span id="sb-ver">v\u2014</span>
</div>
</div>
`;
this.el.innerHTML = html;
this.bindEvents();
this.fetchAndInjectVersion();
}
private item(viewId: string, icon: string, label: string, active: boolean): string {
return `<button class="sb-item${active ? ' active' : ''}" data-view="${viewId}">` +
`<span class="sb-item-icon material-symbols-outlined">${icon}</span>` +
`<span class="sb-item-text">${esc(label)}</span>` +
`</button>`;
}
private bindEvents(): void {
if (!this.el) return;
this.el.querySelectorAll<HTMLElement>('.sb-item[data-view]').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset['view']!;
const viewValue: string | number = /^\d+$/.test(view) ? Number(view) : view;
this.uiState.setActiveView(viewValue);
});
});
}
private fetchAndInjectVersion(): void {
if (this.cachedVersion) {
const verEl = this.el?.querySelector('#sb-ver');
if (verEl) verEl.textContent = `v${this.cachedVersion}`;
return;
}
const api = (window as any).electronAPI;
if (!api?.getVersion) return;
api.getVersion().then((v: string) => {
this.cachedVersion = v;
const verEl = this.el?.querySelector('#sb-ver');
if (verEl) verEl.textContent = `v${v}`;
}).catch(() => {});
}
destroy(): void {
this.el?.remove();
this.el = null;
}
}