- 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>
134 lines
4.6 KiB
TypeScript
Executable File
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'é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;
|
|
}
|
|
}
|