Changeset 0.37.4 (#216)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -18,6 +18,8 @@ import { createDomains } from './api-domains.js';
|
||||
import { createEvents } from './events.js';
|
||||
import { createPipe } from './pipe.js';
|
||||
import { createTheme } from './theme.js';
|
||||
import { confirm } from '../primitives/confirm.js';
|
||||
import { prompt } from '../primitives/prompt.js';
|
||||
|
||||
/**
|
||||
* Boot the SDK. Assembles modules, loads auth state, connects WebSocket.
|
||||
@@ -91,8 +93,20 @@ export async function boot() {
|
||||
sw.pipe = pipe;
|
||||
sw.theme = theme;
|
||||
|
||||
// Shell helpers — imperative confirm/prompt backed by DialogStack
|
||||
sw.confirm = confirm;
|
||||
sw.prompt = prompt;
|
||||
|
||||
// UserMenu render helper — surfaces call sw.userMenu(container, opts)
|
||||
sw.userMenu = function (container, opts = {}) {
|
||||
import('../shell/user-menu.js').then(({ UserMenu }) => {
|
||||
const { render } = preact;
|
||||
render(html`<${UserMenu} ...${opts} />`, container);
|
||||
});
|
||||
};
|
||||
|
||||
// Marker for idempotency
|
||||
sw._sdk = '0.37.3';
|
||||
sw._sdk = '0.37.4';
|
||||
|
||||
// 8. Expose globally
|
||||
window.sw = sw;
|
||||
@@ -114,7 +128,7 @@ export async function boot() {
|
||||
// 10. Signal ready
|
||||
events.emit('sdk.ready', {}, { localOnly: true });
|
||||
document.dispatchEvent(new CustomEvent('sw:ready', { detail: { sw } }));
|
||||
console.log('[sw] SDK v0.37.3 ready');
|
||||
console.log('[sw] SDK v0.37.4 ready');
|
||||
|
||||
return sw;
|
||||
}
|
||||
|
||||
36
src/js/sw/shell/app.js
Normal file
36
src/js/sw/shell/app.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* App — root component composing the full shell tree
|
||||
*
|
||||
* ┌───────────────────────────────┐
|
||||
* │ AppShell (banners, layout) │
|
||||
* │ └─ SurfaceViewport │
|
||||
* ├───────────────────────────────┤
|
||||
* │ ToastContainer (fixed z:10k) │
|
||||
* │ DialogStack (fixed z:5k) │
|
||||
* └───────────────────────────────┘
|
||||
*
|
||||
* ToastContainer and DialogStack live outside AppShell
|
||||
* so they float above all shell content.
|
||||
*
|
||||
* Props:
|
||||
* banner — { text, variant } or null
|
||||
* message — { text, variant } or null
|
||||
* footer — VNode or null
|
||||
* surface — VNode or null (active surface content)
|
||||
*/
|
||||
const { html } = window;
|
||||
|
||||
import { AppShell } from './app-shell.js';
|
||||
import { SurfaceViewport } from './surface-viewport.js';
|
||||
import { DialogStack } from './dialog-stack.js';
|
||||
import { ToastContainer } from '../primitives/toast.js';
|
||||
|
||||
export function App({ banner, message, footer, surface }) {
|
||||
return html`
|
||||
<${AppShell} banner=${banner} message=${message} footer=${footer}>
|
||||
<${SurfaceViewport} surface=${surface} />
|
||||
<//>
|
||||
<${ToastContainer} />
|
||||
<${DialogStack} />
|
||||
`;
|
||||
}
|
||||
17
src/js/sw/shell/dialog-stack.js
Normal file
17
src/js/sw/shell/dialog-stack.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* DialogStack — mounts imperative dialog hosts (ConfirmHost, PromptHost)
|
||||
*
|
||||
* Must be rendered once at the root level and never unmounted,
|
||||
* so sw.confirm() and sw.prompt() work globally.
|
||||
*/
|
||||
const { html } = window;
|
||||
|
||||
import { ConfirmHost } from '../primitives/confirm.js';
|
||||
import { PromptHost } from '../primitives/prompt.js';
|
||||
|
||||
export function DialogStack() {
|
||||
return html`
|
||||
<${ConfirmHost} />
|
||||
<${PromptHost} />
|
||||
`;
|
||||
}
|
||||
15
src/js/sw/shell/surface-viewport.js
Normal file
15
src/js/sw/shell/surface-viewport.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* SurfaceViewport — container where the active surface renders
|
||||
*
|
||||
* Deliberately thin for v0.37.4. Error boundaries and
|
||||
* surface transitions will be added in later versions.
|
||||
*/
|
||||
const { html } = window;
|
||||
|
||||
export function SurfaceViewport({ surface = null, className = '' }) {
|
||||
return html`
|
||||
<div class="sw-surface-viewport ${className}">
|
||||
${surface}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
79
src/js/sw/shell/user-menu.js
Normal file
79
src/js/sw/shell/user-menu.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* UserMenu — avatar button + flyout menu
|
||||
*
|
||||
* RBAC-gated items based on sw.can() / sw.isAdmin / sw.isTeamAdmin().
|
||||
* Surfaces mount this wherever they want; the shell does not own placement.
|
||||
*
|
||||
* Props:
|
||||
* placement — Menu direction: 'down-right' (default) | 'down-left'
|
||||
* onAction — Override handler; if omitted, uses default routing
|
||||
*/
|
||||
const { html } = window;
|
||||
const { useState, useRef, useMemo } = hooks;
|
||||
|
||||
import { Avatar } from '../primitives/avatar.js';
|
||||
import { Menu } from '../primitives/menu.js';
|
||||
|
||||
export function UserMenu({ placement = 'down-right', onAction }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
|
||||
const sw = window.sw;
|
||||
const user = sw?.auth?.user;
|
||||
const authenticated = sw?.auth?.isAuthenticated;
|
||||
|
||||
const items = useMemo(() => {
|
||||
const list = [
|
||||
{ label: 'Settings', action: 'settings', icon: '\u2699\ufe0f' },
|
||||
];
|
||||
|
||||
// Admin — requires admin role
|
||||
if (!authenticated || sw?.isAdmin) {
|
||||
list.push({ label: 'Admin', action: 'admin', icon: '\ud83d\udee1\ufe0f' });
|
||||
}
|
||||
|
||||
// Team Admin — requires admin role on at least one team
|
||||
const hasTeamAdmin = sw?.auth?.teams?.some(t => t.my_role === 'admin');
|
||||
if (!authenticated || hasTeamAdmin) {
|
||||
list.push({ label: 'Team Admin', action: 'team-admin', icon: '\ud83d\udc65' });
|
||||
}
|
||||
|
||||
// Debug — admin only
|
||||
if (!authenticated || sw?.isAdmin) {
|
||||
list.push({ label: 'Debug', action: 'debug', icon: '\ud83d\udc1b' });
|
||||
}
|
||||
|
||||
list.push({ divider: true });
|
||||
list.push({ label: 'Sign Out', action: 'sign-out' });
|
||||
|
||||
return list;
|
||||
}, [user, authenticated]);
|
||||
|
||||
function handleSelect(action) {
|
||||
setOpen(false);
|
||||
if (onAction) return onAction(action);
|
||||
if (action === 'sign-out') {
|
||||
sw?.auth?.logout();
|
||||
} else {
|
||||
sw?.emit('shell.navigate', { target: action });
|
||||
}
|
||||
}
|
||||
|
||||
const displayName = user?.display_name || user?.username || 'Demo User';
|
||||
const avatar = user?.avatar || null;
|
||||
|
||||
return html`
|
||||
<button class="sw-user-menu__trigger" ref=${btnRef}
|
||||
onClick=${() => setOpen(!open)} aria-label="User menu">
|
||||
<${Avatar} src=${avatar} name=${displayName} size="sm" />
|
||||
</button>
|
||||
<${Menu}
|
||||
open=${open}
|
||||
anchor=${btnRef.current}
|
||||
direction=${placement}
|
||||
items=${items}
|
||||
onSelect=${handleSelect}
|
||||
onClose=${() => setOpen(false)}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user