This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/sdk/slots.js
Jeffrey Smith 98fd3eb3e6
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 52s
Feat v0.8.5 extension composability (#72)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 11:33:47 +00:00

118 lines
3.8 KiB
JavaScript

// ==========================================
// 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<string, Array<{id:string, component:Function, priority:number}>>} */
const _registry = new Map();
/** @type {Map<string, string>|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<string, string>}
*/
declarations() {
return _declarations ? new Map(_declarations) : new Map();
},
};
}