/** * 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`
`; } /** * 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`