class Sidebar { constructor(onSelectSurface) { this.onSelectSurface = onSelectSurface; this.sidebar = document.getElementById('sidebar'); this.overlay = document.getElementById('sidebar-overlay'); this.tree = document.getElementById('sidebar-tree'); this.menuBtn = document.getElementById('menu-btn'); this.closeBtn = document.getElementById('sidebar-close'); this.workspaces = []; this.activeSurface = null; this._loaded = false; this.menuBtn.addEventListener('click', () => this.isOpen() ? this.close() : this.open()); this.closeBtn.addEventListener('click', () => this.close()); this.overlay.addEventListener('click', () => this.close()); // On desktop, sidebar is always visible via CSS (position: relative) // Check on resize to close overlay if switching to desktop this._onResize = () => { if (window.innerWidth >= 1024) { this.overlay.classList.remove('visible'); } }; window.addEventListener('resize', this._onResize); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen() && !this.isDesktop()) this.close(); }); this.render(); } open() { if (this.isDesktop()) { this.sidebar.classList.remove('desktop-hidden'); } else { this.sidebar.classList.add('open'); this.overlay.classList.add('visible'); } } close() { if (this.isDesktop()) { this.sidebar.classList.add('desktop-hidden'); } else { this.sidebar.classList.remove('open'); this.overlay.classList.remove('visible'); } } isDesktop() { return window.innerWidth >= 1024; } isOpen() { if (this.isDesktop()) return !this.sidebar.classList.contains('desktop-hidden'); return this.sidebar.classList.contains('open'); } setWorkspaces(workspaces) { this.workspaces = workspaces; this._loaded = true; this.render(); } setActiveSurface(surfaceRef) { this.activeSurface = surfaceRef; this.render(); } render() { this.tree.innerHTML = ''; if (this.workspaces.length === 0) { const empty = document.createElement('div'); empty.className = 'sidebar-empty'; empty.textContent = this._loaded ? 'No workspaces' : 'Loading...'; this.tree.appendChild(empty); return; } for (const ws of this.workspaces) { // Workspace group label const groupLabel = document.createElement('div'); groupLabel.className = 'ws-group-label'; groupLabel.textContent = ws.name || ws.ref; this.tree.appendChild(groupLabel); // Surfaces within workspace for (const pane of ws.panes) { for (const surface of pane.surfaces) { const item = document.createElement('div'); item.className = 'surface-item'; if (surface.ref === this.activeSurface) { item.classList.add('active'); } const titleRaw = surface.title || surface.ref; const status = Sidebar.parseStatus(titleRaw); const dot = document.createElement('span'); dot.className = 'surface-status'; if (status.state === 'working') dot.classList.add('working'); else if (status.state === 'idle') dot.classList.add('idle'); const label = document.createElement('span'); label.className = 'ws-label'; label.textContent = status.title; item.appendChild(dot); item.appendChild(label); item.addEventListener('click', () => { this.onSelectSurface(ws.ref, surface.ref); this.setActiveSurface(surface.ref); if (!this.isDesktop()) this.close(); }); this.tree.appendChild(item); } } } } static parseStatus(title) { // Working: Braille spinner (U+2800-U+28FF), ⏺ (U+23FA), ● (U+25CF), ⬤ (U+2B24) const workingRe = /^[\u2800-\u28FF\u23FA\u25CF\u2B24]\s*/; // Idle/waiting: dingbats range — ✳ ✻ ✢ ✦ etc. const idleRe = /^[\u2700-\u27BF]\s*/; if (workingRe.test(title)) { return { state: 'working', title: title.replace(workingRe, '') }; } if (idleRe.test(title)) { return { state: 'idle', title: title.replace(idleRe, '') }; } return { state: null, title }; } } window.Sidebar = Sidebar;