rspace-online/website/shell.ts

135 lines
5.2 KiB
TypeScript

/**
* Shell entry point — loaded on every page.
*
* Registers the three header web components:
* <rstack-app-switcher>
* <rstack-space-switcher>
* <rstack-identity>
*/
import { RStackIdentity } from "../shared/components/rstack-identity";
import { RStackNotificationBell } from "../shared/components/rstack-notification-bell";
import { RStackAppSwitcher } from "../shared/components/rstack-app-switcher";
import { RStackSpaceSwitcher } from "../shared/components/rstack-space-switcher";
import { RStackTabBar } from "../shared/components/rstack-tab-bar";
import { RStackMi } from "../shared/components/rstack-mi";
import { RStackSpaceSettings } from "../shared/components/rstack-space-settings";
import { RStackModuleSetup } from "../shared/components/rstack-module-setup";
import { RStackHistoryPanel } from "../shared/components/rstack-history-panel";
import { RStackOfflineIndicator } from "../shared/components/rstack-offline-indicator";
import { RStackSharePanel } from "../shared/components/rstack-share-panel";
import { RStackCommentBell } from "../shared/components/rstack-comment-bell";
import { RStackCollabOverlay } from "../shared/components/rstack-collab-overlay";
import { RStackUserDashboard } from "../shared/components/rstack-user-dashboard";
import { rspaceNavUrl } from "../shared/url-helpers";
import { TabCache } from "../shared/tab-cache";
import { RSpaceOfflineRuntime } from "../shared/local-first/runtime";
import { CommunitySync } from "../lib/community-sync";
import { OfflineStore } from "../lib/offline-store";
// Expose URL helper globally (used by shell inline scripts + components)
(window as any).__rspaceNavUrl = rspaceNavUrl;
// Expose TabCache for inline shell script to instantiate
(window as any).__RSpaceTabCache = TabCache;
// Register all header components
RStackIdentity.define();
RStackNotificationBell.define();
RStackAppSwitcher.define();
RStackSpaceSwitcher.define();
RStackTabBar.define();
RStackMi.define();
RStackSpaceSettings.define();
RStackModuleSetup.define();
RStackHistoryPanel.define();
RStackOfflineIndicator.define();
RStackSharePanel.define();
RStackCommentBell.define();
RStackCollabOverlay.define();
RStackUserDashboard.define();
// ── Offline Runtime ──
// Instantiate the shared runtime from the space slug on the <body> tag.
// Components access it via window.__rspaceOfflineRuntime.
const spaceSlug = document.body?.getAttribute("data-space-slug");
if (spaceSlug && spaceSlug !== "demo") {
const runtime = new RSpaceOfflineRuntime(spaceSlug);
(window as any).__rspaceOfflineRuntime = runtime;
// Configure module scope resolution from server-rendered data
try {
const scopeJson = document.body?.getAttribute("data-scope-overrides");
const overrides: Record<string, string> = scopeJson ? JSON.parse(scopeJson) : {};
// Build scope config: merge module defaults with space overrides
const moduleList: Array<{ id: string; scoping?: { defaultScope: string } }> =
(window as any).__rspaceModuleList || [];
const scopes: Array<{ id: string; scope: 'global' | 'space' }> = moduleList.map(m => ({
id: m.id,
scope: (overrides[m.id] || m.scoping?.defaultScope || 'space') as 'global' | 'space',
}));
runtime.setModuleScopes(scopes);
} catch { /* scope config unavailable — defaults to space-scoped */ }
runtime.init().catch((e: unknown) => {
console.warn("[shell] Offline runtime init failed — REST fallback only:", e);
});
// Flush pending writes before the page unloads
window.addEventListener("beforeunload", () => {
runtime.flush();
});
// ── CommunitySync (tab list Automerge sync) ──
const offlineStore = new OfflineStore();
const communitySync = new CommunitySync(spaceSlug, offlineStore);
(window as any).__communitySync = communitySync;
(async () => {
try {
await offlineStore.open();
await communitySync.initFromCache();
} catch (e) {
console.warn("[shell] CommunitySync cache init failed:", e);
}
document.dispatchEvent(new CustomEvent("community-sync-ready", {
detail: { sync: communitySync, communitySlug: spaceSlug },
}));
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${proto}//${location.host}/ws/${spaceSlug}`;
communitySync.connect(wsUrl);
})();
window.addEventListener("beforeunload", () => {
communitySync.saveBeforeUnload();
});
}
// ── Track space visits for dashboard recency sorting ──
if (spaceSlug) {
try {
const RECENT_KEY = "rspace_recent_spaces";
const visits = JSON.parse(localStorage.getItem(RECENT_KEY) || "{}");
visits[spaceSlug] = Date.now();
localStorage.setItem(RECENT_KEY, JSON.stringify(visits));
} catch { /* localStorage unavailable */ }
}
// Reload space list when user signs in/out (to show/hide private spaces)
document.addEventListener("auth-change", (e) => {
const reason = (e as CustomEvent).detail?.reason;
// Token refreshes are invisible — no UI side-effects needed
if (reason === "refresh") return;
// Reload space switcher on state-changing events
const spaceSwitcher = document.querySelector("rstack-space-switcher") as any;
spaceSwitcher?.reload?.();
// Only redirect to homepage on genuine sign-out or server revocation
if (reason === "signout" || reason === "revoked") {
window.location.href = "/";
}
});