// Spaces tree (lazy-expand to projects) + global links section. // Drag-reorder is deferred — static order for now. import { api } from '../api.js'; import { el, mount, clear } from '../dom.js'; import { navigate, current } from '../router.js'; import { on } from '../state.js'; const expanded = new Set(); // space ids expanded in the tree function navItem(label, hash, opts = {}) { const active = current().hash === hash.replace(/^#/, ''); return el('a', { class: 'sb-item' + (active ? ' active' : ''), href: '#' + hash.replace(/^#/, ''), onclick: (e) => { if (opts.onclick) { e.preventDefault(); opts.onclick(e); return; } } }, opts.icon ? el('span', { class: 'caret' }, opts.icon) : null, el('span', { style: { flex: 1 } }, label), opts.badge !== undefined && opts.badge !== null ? el('span', { class: 'badge' }, String(opts.badge)) : null ); } async function loadProjects(space_id) { try { return await api.get(`/api/spaces/${space_id}/projects`); } catch { return []; } } async function renderSpaceTree(container) { let spaces; try { spaces = await api.get('/api/spaces'); } catch { spaces = []; } clear(container); if (!spaces.length) { container.appendChild(el('div', { class: 'sb-item muted' }, 'No spaces yet')); return; } for (const s of spaces) { const isOpen = expanded.has(s.id); const childWrap = el('div', { class: 'sb-children' }); const header = el('a', { class: 'sb-item', href: '#/space/' + s.id, onclick: async (e) => { // Click on caret toggles; click on label navigates. if (e.target.dataset.role === 'caret') { e.preventDefault(); if (expanded.has(s.id)) { expanded.delete(s.id); clear(childWrap); } else { expanded.add(s.id); const projects = await loadProjects(s.id); clear(childWrap); if (!projects.length) childWrap.appendChild(el('div', { class: 'sb-item muted' }, '(no projects)')); for (const p of projects) { childWrap.appendChild(el('a', { class: 'sb-item', href: '#/project/' + p.id }, p.name)); } } } } }, el('span', { class: 'caret', dataset: { role: 'caret' } }, isOpen ? '▾' : '▸'), el('span', { style: { flex: 1 } }, s.name) ); container.appendChild(header); if (isOpen) { loadProjects(s.id).then(projects => { clear(childWrap); if (!projects.length) childWrap.appendChild(el('div', { class: 'sb-item muted' }, '(no projects)')); for (const p of projects) { childWrap.appendChild(el('a', { class: 'sb-item', href: '#/project/' + p.id }, p.name)); } }); } container.appendChild(childWrap); } } export function renderSidebar(root) { const spacesContainer = el('div'); const inboxItem = navItem('Inbox', '/inbox'); mount(root, el('div', { class: 'sb-section' }, el('div', { class: 'sb-title' }, 'Spaces'), spacesContainer ), el('hr'), el('div', { class: 'sb-section' }, el('div', { class: 'sb-title' }, 'Navigate'), navItem('Sacred Valley', '/sacred-valley'), navItem('Search', '/search'), inboxItem, el('div', { class: 'sb-item muted', title: 'Ships post-Plan-2' }, 'Agents — later'), el('div', { class: 'sb-item muted', title: 'Ships post-Plan-2' }, 'Resources — later') ) ); renderSpaceTree(spacesContainer); // Pending-count badge wiring on('pending-count', (n) => { const old = inboxItem.querySelector('.badge'); if (old) old.remove(); if (n > 0) inboxItem.appendChild(el('span', { class: 'badge' }, String(n))); }); // Refresh tree on hashchange (active highlight) and on space creation. window.addEventListener('hashchange', () => renderSpaceTree(spacesContainer)); }