// ========================================== // Armature — SDK: Panel System (v0.10.0) // ========================================== // Composable companion views that surfaces can // pull in from other packages. // // Factory: createPanels(emitFn) // ========================================== /** * Create the panels registry and lifecycle manager. * * @param {Function} emitFn — events.emit for change notifications * @returns {object} panels */ export function createPanels(emitFn) { /** @type {Map} panelId → manifest meta */ const _registry = new Map(); /** @type {Map} panelId → active state */ const _active = new Map(); /** @type {Map} panelId → cached JS module */ const _modules = new Map(); return { /** * Register a panel from resolved manifest data. Called by the * kernel during surface boot — not by package code directly. * * @param {string} panelId — e.g. 'notes.reference' * @param {object} meta — PanelMeta from window.__PANELS__ */ _register(panelId, meta) { _registry.set(panelId, meta); }, /** * Open a panel. Lazy-loads the JS entry if not yet loaded, * creates an unstyled container, and calls mount(). * * @param {string} panelId — e.g. 'notes.reference' * @param {object} [opts] * @param {string} [opts.mode] — presentation hint (ignored in v0.10.0) * @param {object} [opts.params] — passed to mount(el, ctx) as ctx.params * @returns {Promise} — false if panel not available */ async open(panelId, opts = {}) { if (!_registry.has(panelId)) return false; if (_active.has(panelId)) return true; // already open const meta = _registry.get(panelId); // Lazy-load the panel JS module if (!_modules.has(panelId)) { try { const base = window.__BASE__ || ''; const url = `${base}/surfaces/${meta.package_id}/${meta.entry}`; const mod = await import(url); _modules.set(panelId, mod); } catch (err) { console.error(`[sw.panels] Failed to load ${panelId}:`, err); return false; } } const mod = _modules.get(panelId); if (typeof mod.mount !== 'function') { console.error(`[sw.panels] ${panelId} does not export mount()`); return false; } // Create unstyled container (v0.10.0 — no chrome) const el = document.createElement('div'); el.className = 'sw-panel-container'; el.dataset.panelId = panelId; el.style.cssText = 'position:fixed;top:80px;right:16px;width:400px;height:350px;background:var(--bg-surface,#fff);border:1px solid var(--border,#ccc);border-radius:8px;overflow:auto;z-index:100;'; document.body.appendChild(el); // Mount const ctx = { sw: window.sw, params: opts.params || {}, panelId, close: () => this.close(panelId), resize: () => {}, // no-op in v0.10.0 }; let cleanup; try { cleanup = mod.mount(el, ctx); } catch (err) { console.error(`[sw.panels] mount() failed for ${panelId}:`, err); el.remove(); return false; } _active.set(panelId, { el, cleanup: typeof cleanup === 'function' ? cleanup : () => {} }); emitFn('panels.opened', { panelId, mode: opts.mode || 'plain' }, { localOnly: true }); return true; }, /** * Close a panel. Calls cleanup, removes container from DOM. * * @param {string} panelId */ close(panelId) { const entry = _active.get(panelId); if (!entry) return; try { entry.cleanup(); } catch (err) { console.warn(`[sw.panels] cleanup error for ${panelId}:`, err); } entry.el.remove(); _active.delete(panelId); emitFn('panels.closed', { panelId }, { localOnly: true }); }, /** * Toggle a panel open/closed. * * @param {string} panelId * @param {object} [opts] — passed to open() if opening * @returns {Promise} */ async toggle(panelId, opts = {}) { if (_active.has(panelId)) { this.close(panelId); return true; } return this.open(panelId, opts); }, /** * Check if a panel is currently open. * @param {string} panelId * @returns {boolean} */ isOpen(panelId) { return _active.has(panelId); }, /** * Check if a panel is available (provider installed + enabled). * @param {string} panelId * @returns {boolean} */ isAvailable(panelId) { return _registry.has(panelId); }, /** * List available panel IDs for the current surface. * @returns {string[]} */ list() { return [..._registry.keys()]; }, /** * List currently open panel IDs. * @returns {string[]} */ active() { return [..._active.keys()]; }, }; }