feat(web): NetX workbench, UME-only UI, i18n, and header status

- Replace sidebar app with workbench home and module tabs (openOrFocusModule)

- Remove legacy pages; add module registry, shared tab channel, WEB.md

- Add zh/en i18n, global ToastProvider, restore oclaw bridge pill in header

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-27 11:18:30 +08:00
parent 664775927b
commit 282dbe75af
33 changed files with 1631 additions and 1677 deletions

View file

@ -0,0 +1,64 @@
/** Shared cross-tab focus helpers (BroadcastChannel + sessionStorage ack). */
export const TAB_FOCUS_DELAY_MS = 200;
export function createFocusRequestId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
export function registerBroadcastListener<T extends { requestId: string }>(
channelName: string,
ackStorageKey: string,
onMessage: (data: T) => void,
): () => void {
if (typeof BroadcastChannel === "undefined") {
return () => undefined;
}
const channel = new BroadcastChannel(channelName);
channel.onmessage = (ev: MessageEvent<T>) => {
const data = ev.data;
if (!data?.requestId) return;
try {
sessionStorage.setItem(ackStorageKey, data.requestId);
} catch {
/* ignore */
}
onMessage(data);
};
return () => channel.close();
}
export function broadcastMessage<T>(channelName: string, message: T): void {
if (typeof BroadcastChannel === "undefined") return;
const channel = new BroadcastChannel(channelName);
channel.postMessage(message);
channel.close();
}
export function waitForFocusAck(
ackStorageKey: string,
requestId: string,
fallback: () => void,
delayMs: number = TAB_FOCUS_DELAY_MS,
): void {
window.setTimeout(() => {
try {
if (sessionStorage.getItem(ackStorageKey) === requestId) {
sessionStorage.removeItem(ackStorageKey);
return;
}
} catch {
/* ignore */
}
fallback();
}, delayMs);
}
export function focusWindowSafe(win: Window | null): void {
try {
win?.focus();
} catch {
/* ignore */
}
}