// ========================================== // Armature — SDK: Slot System // ========================================== // Named shell regions where extensions inject UI. // // Factory: createSlots(emitFn) // ========================================== /** * Create the slot registry. * * @param {Function} emitFn — events.emit for change notifications * @returns {object} slots */ export function createSlots(emitFn) { /** @type {Map>} */ const _registry = new Map(); /** @type {Map|undefined} */ let _declarations; function _sort(arr) { arr.sort((a, b) => a.priority - b.priority); } return { /** * Register a component into a named slot. * @param {string} name — slot name (e.g. 'toolbar', 'statusbar') * @param {object} entry — { id, component, priority? } * @returns {Function} unregister */ register(name, { id, component, priority = 100 }) { if (!_registry.has(name)) _registry.set(name, []); const list = _registry.get(name); // Prevent duplicate ids in same slot const idx = list.findIndex(e => e.id === id); if (idx !== -1) list.splice(idx, 1); list.push({ id, component, priority }); _sort(list); emitFn('slots.changed', { slot: name, action: 'register', id }, { localOnly: true }); return () => this.unregister(name, id); }, /** * Remove a component from a slot. */ unregister(name, id) { const list = _registry.get(name); if (!list) return; const idx = list.findIndex(e => e.id === id); if (idx !== -1) { list.splice(idx, 1); if (list.length === 0) _registry.delete(name); emitFn('slots.changed', { slot: name, action: 'unregister', id }, { localOnly: true }); } }, /** * Get sorted entries for a slot. * @param {string} name * @returns {Array<{id:string, component:Function, priority:number}>} */ get(name) { return _registry.get(name) || []; }, /** * List all slot names that have content. * @returns {string[]} */ names() { return [..._registry.keys()]; }, /** * Render all components registered in a slot. * Each component is called with the context object. Errors are * caught so one broken contributor doesn't break the host surface. * @param {string} name — slot name * @param {object} context — props passed to each component * @returns {Array} — array of rendered vnodes (nulls filtered) */ renderAll(name, context = {}) { return this.get(name).map(e => { try { return e.component(context); } catch (err) { console.error(`[sw.slots] Error rendering "${e.id}" in slot "${name}":`, err); return null; } }).filter(Boolean); }, /** * Declare a slot for discoverability. Runtime-only metadata — * does not affect registration or rendering. * @param {string} name — slot name * @param {string} description — human-readable purpose */ declare(name, description) { if (!_declarations) _declarations = new Map(); _declarations.set(name, description); }, /** * List declared slot metadata (from declare()). * @returns {Map} */ declarations() { return _declarations ? new Map(_declarations) : new Map(); }, }; }