(function (root, factory) { 'use strict'; const foundation = typeof module === 'object' && module.exports ? require('./foundation.js') : root.HermesHuxFoundation; const api = factory(foundation); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxShell = api; }(typeof globalThis === 'object' ? globalThis : this, function (foundation) { 'use strict'; function element(doc, tag, attributes, text) { const node = doc.createElement(tag); Object.entries(attributes || {}).forEach(([name, value]) => node.setAttribute(name, value)); if (text) node.textContent = text; return node; } function createShell(options) { const settings = options || {}; const client = settings.client; const doc = settings.document || (typeof document === 'object' ? document : null); if (!client || !doc) throw new TypeError('HUX shell requires a client and document'); const extensions = new Map(); const instanceId = /^[a-z][a-z0-9-]{0,30}$/.test(settings.instanceId || '') ? settings.instanceId : 'main'; let root = null; let unsubscribe = null; let selected = null; function register(extension) { if (!extension || !/^[a-z][a-z0-9-]{2,48}$/.test(extension.id || '')) { throw new TypeError('Extension id must be stable kebab-case'); } if (!foundation.FLAGS.includes(extension.flag) || extension.flag === 'hux.foundation') { throw new TypeError('Extension requires a registered feature flag'); } if (typeof extension.render !== 'function' || !String(extension.label || '').trim()) { throw new TypeError('Extension requires a label and renderer'); } if (extensions.has(extension.id)) throw new TypeError('Extension id already registered'); extensions.set(extension.id, Object.freeze({ id: extension.id, flag: extension.flag, label: String(extension.label).slice(0, 80), order: Number.isFinite(extension.order) ? extension.order : 100, render: extension.render, })); if (root) render(client.getState()); return function remove() { extensions.delete(extension.id); if (selected === extension.id) selected = null; if (root) render(client.getState()); }; } function status(message, retry) { const box = element(doc, 'div', {'class': 'hux-shell__status', 'role': 'status', 'aria-live': 'polite'}, message); if (retry) { const button = element(doc, 'button', {'class': 'hux-shell__retry', 'type': 'button'}, 'Try again'); button.addEventListener('click', () => client.negotiate()); box.appendChild(button); } root.appendChild(box); } function showExtension(extension, panels, tabs) { selected = extension.id; tabs.forEach((tab) => { const active = tab.dataset.extensionId === selected; tab.setAttribute('aria-selected', active ? 'true' : 'false'); tab.setAttribute('tabindex', active ? '0' : '-1'); }); panels.forEach((panel) => { panel.hidden = panel.dataset.extensionId !== selected; }); } function moveTab(event, tabs, index) { const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End']; if (!keys.includes(event.key)) return; event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length; tabs[next].focus(); tabs[next].click(); } function renderReady() { const active = Array.from(extensions.values()) .filter((extension) => client.enabled(extension.flag)) .sort((left, right) => left.order - right.order || left.id.localeCompare(right.id)); if (!active.length) { root.hidden = true; return; } root.hidden = false; const heading = element(doc, 'h2', {'class': 'hux-shell__title'}, 'Hermes workspace'); const tabsNode = element(doc, 'div', {'class': 'hux-shell__tabs', 'role': 'tablist', 'aria-label': 'Hermes workspace'}); const body = element(doc, 'div', {'class': 'hux-shell__body'}); const tabs = []; const panels = []; active.forEach((extension) => { const tabId = `hux-${instanceId}-tab-${extension.id}`; const panelId = `hux-${instanceId}-panel-${extension.id}`; const tab = element(doc, 'button', { 'class': 'hux-shell__tab', 'type': 'button', 'role': 'tab', 'id': tabId, 'aria-controls': panelId, 'data-extension-id': extension.id, }, extension.label); const panel = element(doc, 'section', { 'class': 'hux-shell__panel', 'role': 'tabpanel', 'id': panelId, 'aria-labelledby': tabId, 'data-extension-id': extension.id, }); try { extension.render(Object.freeze({client, container: panel})); } catch (_) { panel.replaceChildren(element(doc, 'div', { 'class': 'hux-shell__status', 'role': 'status', 'aria-live': 'polite', }, 'This workspace panel is temporarily unavailable.')); } tab.addEventListener('click', () => showExtension(extension, panels, tabs)); tab.addEventListener('keydown', (event) => moveTab(event, tabs, tabs.indexOf(tab))); tabsNode.appendChild(tab); body.appendChild(panel); tabs.push(tab); panels.push(panel); }); root.appendChild(heading); root.appendChild(tabsNode); root.appendChild(body); const choice = active.find((extension) => extension.id === selected) || active[0]; showExtension(choice, panels, tabs); } function render(state) { if (!root) return; root.replaceChildren(); root.setAttribute('data-hux-state', state.phase); if (state.phase === 'loading') { root.hidden = false; status('Loading Hermes workspace…', false); } else if (state.phase === 'error') { root.hidden = false; status(state.error || 'Hermes workspace is unavailable.', true); } else if (state.phase === 'ready') renderReady(); else root.hidden = true; } function mount(target) { if (root) throw new Error('HUX shell is already mounted'); root = element(doc, 'aside', { 'class': 'hux-shell', 'aria-label': 'Hermes workspace', 'data-hux-version': foundation.API_VERSION, }); root.hidden = true; target.appendChild(root); unsubscribe = client.subscribe(render); return root; } function destroy() { if (unsubscribe) unsubscribe(); if (root && root.parentNode) root.parentNode.removeChild(root); root = null; unsubscribe = null; } return Object.freeze({destroy, mount, register}); } return Object.freeze({createShell}); }));