/** * — colored state icon + title + details (tooltip) + progress row. * * State icon: filled circle (FA fa-circle) colored by state: * idle — grey * running — blue (subtle pulse) * success — green * warning — orange (success with a non-fatal fallback/skip) * error — red * * Title is the brief "what's happening right now" line. Details live in a * hover tooltip on the icon — used for the longer narrative (which EP, * what error, etc.). Progress row shows a fractional bar with an optional * tile count to the right. * * Primary API: * sb.set({ title, state, details, progress, tileCount }) * - any subset; unspecified fields are left as-is * - state: 'idle' | 'running' | 'success' | 'warning' | 'error' * - progress: 0..1, -1 to hide, -2 indeterminate * - tileCount: { done, total } or null * * Convenience aliases preserved for incremental callers: * sb.message = '...' ≡ sb.set({ title }) * sb.showProgress(frac) ≡ sb.set({ progress: frac }) * sb.hideProgress() ≡ sb.set({ progress: -1, tileCount: null }) * sb.showIndeterminate() ≡ sb.set({ progress: -2 }) */ import { morph } from 'lib/morph'; const esc = (s) => String(s).replace(/&/g, '&').replace(//g, '>'); const STATES = ['idle', 'running', 'success', 'warning', 'error']; class StatusBar extends HTMLElement { #title = ''; #state = 'idle'; #details = ''; #progress = -1; #tileCount = null; connectedCallback() { this.classList.add('status-bar'); this.#render(); } set(fields) { if (fields.title !== undefined) this.#title = fields.title; if (fields.state !== undefined && STATES.includes(fields.state)) this.#state = fields.state; if (fields.details !== undefined) this.#details = fields.details; if (fields.progress !== undefined) this.#progress = fields.progress; if (fields.tileCount !== undefined) this.#tileCount = fields.tileCount; this.#render(); } set message(msg) { this.set({ title: msg }); } showProgress(frac) { this.set({ progress: frac }); } hideProgress() { this.set({ progress: -1, tileCount: null }); } showIndeterminate() { this.set({ progress: -2 }); } #render() { const showProgress = this.#progress !== -1; const indeterminate = this.#progress === -2; const fillWidth = indeterminate ? 100 : Math.max(0, Math.min(1, this.#progress)) * 100; const fillAnim = indeterminate ? 'animation: status-indeterminate 1.5s ease-in-out infinite;' : ''; const tc = this.#tileCount; const hasTileCount = tc && Number.isFinite(tc.done) && Number.isFinite(tc.total); const tileText = hasTileCount ? `${tc.done} / ${tc.total}` : ''; const ariaLabel = this.#details ? `${this.#title} — ${this.#details}` : this.#title; morph(this, `
${this.#details ? `${esc(this.#details)}` : ''}
${esc(this.#title)}
${showProgress ? ` ${hasTileCount ? `${esc(tileText)}` : ''} ` : ''}
`); } } customElements.define('status-bar', StatusBar);