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
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
118 lines
3.8 KiB
JavaScript
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();
|
|
},
|
|
};
|
|
}
|