/** * AppShell — Layer 1 layout scaffold * * ┌─────────────────────────────────┐ ← fixed banner (top, optional) * ├─────────────────────────────────┤ * │ message (optional, dismissible)│ * ├─────────────────────────────────┤ * │ │ * │ surface (fills remaining) │ * │ │ * ├─────────────────────────────────┤ * │ footer (optional) │ * ├─────────────────────────────────┤ * └─────────────────────────────────┘ ← fixed banner (bottom, same as top) * * Props: * banner — { text, variant } or null (one config → top AND bottom) * message — { text, variant } or null (dismissible) * footer — VNode children or null * children — Surface content */ const { html } = window; const { useState, useEffect, useRef, useReducer } = hooks; function ShellBanner({ position, text, variant = 'info' }) { const ref = useRef(null); const prop = position === 'top' ? '--banner-top-height' : '--banner-bottom-height'; useEffect(() => { if (ref.current) { const h = ref.current.offsetHeight + 'px'; document.documentElement.style.setProperty(prop, h); } return () => document.documentElement.style.setProperty(prop, '0px'); }, [text, prop]); return html`
${text}
`; } /** * Renders all components registered in a named slot. * Re-renders when sw.slots changes (via 'slots.changed' event). */ function SlotRenderer({ name }) { const [, tick] = useReducer(n => n + 1, 0); useEffect(() => { if (!window.sw) return; const off = window.sw.on('slots.changed', (e) => { if (e.slot === name) tick(); }); return () => { if (typeof off === 'function') off(); else window.sw.off('slots.changed', tick); }; }, [name]); const items = window.sw?.slots?.get(name) || []; if (!items.length) return null; return html`
${items.map(i => html`<${i.component} key=${i.id} />`)}
`; } export function AppShell({ banner, message, footer, children }) { const [msgDismissed, setMsgDismissed] = useState(false); const showMsg = message && !msgDismissed; return html`
${banner && html` <${ShellBanner} position="top" text=${banner.text} variant=${banner.variant} /> `}
${showMsg && html`
${message.text}
`} <${SlotRenderer} name="toolbar" /> <${SlotRenderer} name="surface.header" />
${children}
${footer && html` `} <${SlotRenderer} name="statusbar" />
${banner && html` <${ShellBanner} position="bottom" text=${banner.text} variant=${banner.variant} /> `}
`; }