Feat v0.2.3 sdk tasks (#7)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #7.
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
* children — Surface content
|
||||
*/
|
||||
const { html } = window;
|
||||
const { useState, useEffect, useRef } = hooks;
|
||||
const { useState, useEffect, useRef, useReducer } = hooks;
|
||||
|
||||
function ShellBanner({ position, text, variant = 'info' }) {
|
||||
const ref = useRef(null);
|
||||
@@ -41,6 +41,28 @@ function ShellBanner({ position, text, variant = 'info' }) {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -62,6 +84,9 @@ export function AppShell({ banner, message, footer, children }) {
|
||||
</div>
|
||||
`}
|
||||
|
||||
<${SlotRenderer} name="toolbar" />
|
||||
<${SlotRenderer} name="surface.header" />
|
||||
|
||||
<main class="sw-shell__surface">
|
||||
${children}
|
||||
</main>
|
||||
@@ -71,6 +96,8 @@ export function AppShell({ banner, message, footer, children }) {
|
||||
${footer}
|
||||
</footer>
|
||||
`}
|
||||
|
||||
<${SlotRenderer} name="statusbar" />
|
||||
</div>
|
||||
|
||||
${banner && html`
|
||||
|
||||
30
src/js/sw/shell/topbar.js
Normal file
30
src/js/sw/shell/topbar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Topbar — Standard navigation bar for surfaces
|
||||
*
|
||||
* Composes NotificationBell + UserMenu into a consistent bar.
|
||||
* Surfaces use: <${sw.shell.Topbar} title="My Surface">...slot...<//>
|
||||
*
|
||||
* Props:
|
||||
* title — Surface name (falls back to window.__MANIFEST__?.title)
|
||||
* children — Extension slot content (buttons, tabs, pickers)
|
||||
*/
|
||||
const { html } = window;
|
||||
|
||||
import { NotificationBell } from './notification-bell.js';
|
||||
import { UserMenu } from './user-menu.js';
|
||||
|
||||
export function Topbar({ title, children }) {
|
||||
const displayTitle = title || window.__MANIFEST__?.title || '';
|
||||
|
||||
return html`
|
||||
<div class="sw-topbar">
|
||||
<span class="sw-topbar__title">${displayTitle}</span>
|
||||
<div class="sw-topbar__spacer" />
|
||||
${children && html`<div class="sw-topbar__slot">${children}</div>`}
|
||||
<div class="sw-topbar__right">
|
||||
<${NotificationBell} />
|
||||
<${UserMenu} placement="down-right" />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -34,13 +34,15 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
|
||||
const user = sw?.auth?.user;
|
||||
const authenticated = sw?.auth?.isAuthenticated;
|
||||
|
||||
// Fetch extension surfaces once on mount
|
||||
// Fetch installed surfaces once on mount.
|
||||
// Filter out core surfaces that have dedicated menu entries below.
|
||||
const CORE_IDS = new Set(['admin', 'settings', 'team-admin', 'workflow', 'workflow-landing']);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sw?.api?.surfaces?.list) return;
|
||||
sw.api.surfaces.list().then(data => {
|
||||
const all = data || [];
|
||||
const CORE = new Set(['chat', 'admin', 'notes', 'settings', 'team-admin', 'projects', 'workflow', 'workflow-landing']);
|
||||
setExtSurfaces(all.filter(s => !CORE.has(s.id)));
|
||||
const raw = Array.isArray(data) ? data : data?.data || [];
|
||||
setExtSurfaces(raw.filter(s => !CORE_IDS.has(s.id)));
|
||||
}).catch(() => {});
|
||||
}, [authenticated]);
|
||||
|
||||
@@ -48,22 +50,11 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
|
||||
const current = _currentSurface();
|
||||
const list = [];
|
||||
|
||||
// ── Surface links (auto-filtered) ──────
|
||||
// Each surface that exists gets a menu item, except the current one.
|
||||
const surfaces = [
|
||||
{ key: 'chat', label: 'Chat', icon: '\ud83d\udcac' },
|
||||
{ key: 'notes', label: 'Notes', icon: '\ud83d\udcdd' },
|
||||
{ key: 'projects', label: 'Projects', icon: '\ud83d\udcc1' },
|
||||
];
|
||||
|
||||
// Add extension surfaces fetched from API
|
||||
for (const ext of extSurfaces) {
|
||||
surfaces.push({ key: ext.id, label: ext.title, icon: '\ud83e\udde9', route: ext.route });
|
||||
}
|
||||
|
||||
const surfaceItems = surfaces
|
||||
.filter(s => s.key !== current)
|
||||
.map(s => ({ label: s.label, action: s.key, icon: s.icon }));
|
||||
// ── Surface links (from API — only installed surfaces) ──────
|
||||
const DEFAULT_ICON = '\ud83e\udde9';
|
||||
const surfaceItems = extSurfaces
|
||||
.filter(s => s.id !== current)
|
||||
.map(s => ({ label: s.title, action: s.id, icon: s.icon || DEFAULT_ICON }));
|
||||
|
||||
if (surfaceItems.length) {
|
||||
list.push(...surfaceItems);
|
||||
@@ -119,11 +110,8 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
|
||||
case 'debug':
|
||||
window.dispatchEvent(new CustomEvent('debug:toggle'));
|
||||
break;
|
||||
case 'chat':
|
||||
location.href = BASE + '/';
|
||||
break;
|
||||
default: {
|
||||
// Extension surfaces use /s/{id} routes
|
||||
// Surfaces use their route from the API, or fall back to /{id}
|
||||
const ext = extSurfaces.find(s => s.id === action);
|
||||
location.href = BASE + (ext?.route || '/' + action);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user