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/shell/app-shell.js
Jeffrey Smith 239a5fb732
Some checks failed
CI/CD / detect-changes (push) Successful in 23s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Failing after 2m27s
CI/CD / test-sqlite (push) Successful in 2m34s
CI/CD / build-and-deploy (push) Has been skipped
Feat v0.2.3 sdk tasks (#7)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-27 11:06:40 +00:00

109 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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`
<div class="sw-shell__banner sw-shell__banner--${position} sw-shell__banner--${variant}" ref=${ref}>
<span class="sw-shell__banner-text">${text}</span>
</div>
`;
}
/**
* 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`<div class="sw-slot sw-slot--${name}">
${items.map(i => html`<${i.component} key=${i.id} />`)}
</div>`;
}
export function AppShell({ banner, message, footer, children }) {
const [msgDismissed, setMsgDismissed] = useState(false);
const showMsg = message && !msgDismissed;
return html`
<div class="sw-shell">
${banner && html`
<${ShellBanner} position="top" text=${banner.text} variant=${banner.variant} />
`}
<div class="sw-shell__body">
${showMsg && html`
<div class="sw-shell__announcement">
<div class="sw-shell__announcement-inner sw-shell__announcement--${message.variant || 'info'}">
<span class="sw-shell__announcement-text">${message.text}</span>
<button class="sw-shell__banner-close" onClick=${() => setMsgDismissed(true)} aria-label="Dismiss">×</button>
</div>
</div>
`}
<${SlotRenderer} name="toolbar" />
<${SlotRenderer} name="surface.header" />
<main class="sw-shell__surface">
${children}
</main>
${footer && html`
<footer class="sw-shell__footer">
${footer}
</footer>
`}
<${SlotRenderer} name="statusbar" />
</div>
${banner && html`
<${ShellBanner} position="bottom" text=${banner.text} variant=${banner.variant} />
`}
</div>
`;
}