feat: client-side tab caching for instant tab switching
Previously loaded tabs stay in the DOM and are shown/hidden via CSS. New tabs are fetched via fetch() + DOMParser on first visit, then cached. Switching back is instant with no network request. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d9bd7557fa
commit
abbfb552cc
|
|
@ -54,7 +54,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||||
<title>${escapeHtml(title)}</title>
|
<title>${escapeHtml(title)}</title>
|
||||||
<link rel="stylesheet" href="/shell.css?v=5">
|
<link rel="stylesheet" href="/shell.css?v=6">
|
||||||
<style>
|
<style>
|
||||||
/* When loaded inside an iframe (e.g. standalone domain redirecting back),
|
/* When loaded inside an iframe (e.g. standalone domain redirecting back),
|
||||||
hide the shell chrome — the parent rSpace page already provides it. */
|
hide the shell chrome — the parent rSpace page already provides it. */
|
||||||
|
|
@ -94,7 +94,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
${renderWelcomeOverlay()}
|
${renderWelcomeOverlay()}
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js?v=5';
|
import '/shell.js?v=6';
|
||||||
// Provide module list to app switcher
|
// Provide module list to app switcher
|
||||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||||
|
|
||||||
|
|
@ -206,31 +206,62 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tab cache: instant switching via show/hide DOM panes ──
|
||||||
|
let tabCache = null;
|
||||||
|
try {
|
||||||
|
const TC = window.__RSpaceTabCache;
|
||||||
|
if (TC) {
|
||||||
|
tabCache = new TC(spaceSlug, currentModuleId);
|
||||||
|
if (!tabCache.init()) tabCache = null;
|
||||||
|
}
|
||||||
|
} catch(e) { tabCache = null; }
|
||||||
|
|
||||||
// ── Tab events ──
|
// ── Tab events ──
|
||||||
tabBar.addEventListener('layer-switch', (e) => {
|
tabBar.addEventListener('layer-switch', (e) => {
|
||||||
const { moduleId } = e.detail;
|
const { moduleId } = e.detail;
|
||||||
saveTabs();
|
saveTabs();
|
||||||
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
if (tabCache) {
|
||||||
|
tabCache.switchTo(moduleId).then(ok => {
|
||||||
|
if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tabBar.addEventListener('layer-add', (e) => {
|
tabBar.addEventListener('layer-add', (e) => {
|
||||||
const { moduleId } = e.detail;
|
const { moduleId } = e.detail;
|
||||||
// Add the new module before navigating so the next page sees it
|
|
||||||
if (!layers.find(l => l.moduleId === moduleId)) {
|
if (!layers.find(l => l.moduleId === moduleId)) {
|
||||||
layers.push(makeLayer(moduleId, layers.length));
|
layers.push(makeLayer(moduleId, layers.length));
|
||||||
}
|
}
|
||||||
saveTabs();
|
saveTabs();
|
||||||
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
if (tabCache) {
|
||||||
|
tabCache.switchTo(moduleId).then(ok => {
|
||||||
|
if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tabBar.addEventListener('layer-close', (e) => {
|
tabBar.addEventListener('layer-close', (e) => {
|
||||||
const { layerId } = e.detail;
|
const { layerId } = e.detail;
|
||||||
|
const closedModuleId = layerId.replace('layer-', '');
|
||||||
tabBar.removeLayer(layerId);
|
tabBar.removeLayer(layerId);
|
||||||
layers = layers.filter(l => l.id !== layerId);
|
layers = layers.filter(l => l.id !== layerId);
|
||||||
saveTabs();
|
saveTabs();
|
||||||
|
// Remove cached pane from DOM
|
||||||
|
if (tabCache) tabCache.removePane(closedModuleId);
|
||||||
// If we closed the active tab, switch to the first remaining
|
// If we closed the active tab, switch to the first remaining
|
||||||
if (layerId === 'layer-' + currentModuleId && layers.length > 0) {
|
if (layerId === 'layer-' + currentModuleId && layers.length > 0) {
|
||||||
window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId);
|
const nextModuleId = layers[0].moduleId;
|
||||||
|
if (tabCache) {
|
||||||
|
tabCache.switchTo(nextModuleId).then(ok => {
|
||||||
|
if (!ok) window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.location.href = window.__rspaceNavUrl(spaceSlug, nextModuleId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -355,7 +386,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||||
<title>${escapeHtml(title)}</title>
|
<title>${escapeHtml(title)}</title>
|
||||||
<link rel="stylesheet" href="/shell.css?v=5">
|
<link rel="stylesheet" href="/shell.css?v=6">
|
||||||
<style>
|
<style>
|
||||||
html.rspace-embedded .rstack-header { display: none !important; }
|
html.rspace-embedded .rstack-header { display: none !important; }
|
||||||
html.rspace-embedded .rstack-tab-row { display: none !important; }
|
html.rspace-embedded .rstack-tab-row { display: none !important; }
|
||||||
|
|
@ -398,7 +429,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js?v=5';
|
import '/shell.js?v=6';
|
||||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||||
|
|
||||||
const tabBar = document.querySelector('rstack-tab-bar');
|
const tabBar = document.querySelector('rstack-tab-bar');
|
||||||
|
|
@ -581,7 +612,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>">
|
||||||
<title>${escapeHtml(mod.name)} — rSpace</title>
|
<title>${escapeHtml(mod.name)} — rSpace</title>
|
||||||
<link rel="stylesheet" href="/shell.css?v=5">
|
<link rel="stylesheet" href="/shell.css?v=6">
|
||||||
${cssBlock}
|
${cssBlock}
|
||||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -601,7 +632,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
||||||
</header>
|
</header>
|
||||||
${bodyContent}
|
${bodyContent}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js?v=5';
|
import '/shell.js?v=6';
|
||||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||||
try {
|
try {
|
||||||
var raw = localStorage.getItem('encryptid_session');
|
var raw = localStorage.getItem('encryptid_session');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
/**
|
||||||
|
* Client-side tab cache for instant tab switching.
|
||||||
|
*
|
||||||
|
* Instead of full page navigations, previously loaded tabs are kept in the DOM
|
||||||
|
* and shown/hidden via CSS. New tabs are fetched via fetch() + DOMParser on
|
||||||
|
* first visit, then stay in the DOM forever.
|
||||||
|
*
|
||||||
|
* Hiding preserves ALL component state: Shadow DOM, event listeners,
|
||||||
|
* WebSocket connections, timers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TabCache {
|
||||||
|
private currentModuleId: string;
|
||||||
|
private spaceSlug: string;
|
||||||
|
private panes = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
|
constructor(spaceSlug: string, moduleId: string) {
|
||||||
|
this.spaceSlug = spaceSlug;
|
||||||
|
this.currentModuleId = moduleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wrap the initial #app content in a pane div and set up popstate */
|
||||||
|
init(): boolean {
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
if (!app) return false;
|
||||||
|
|
||||||
|
// Wrap existing content in a pane
|
||||||
|
const pane = document.createElement("div");
|
||||||
|
pane.className = "rspace-tab-pane rspace-tab-pane--active";
|
||||||
|
pane.dataset.moduleId = this.currentModuleId;
|
||||||
|
pane.dataset.pageTitle = document.title;
|
||||||
|
|
||||||
|
while (app.firstChild) {
|
||||||
|
pane.appendChild(app.firstChild);
|
||||||
|
}
|
||||||
|
app.appendChild(pane);
|
||||||
|
this.panes.set(this.currentModuleId, pane);
|
||||||
|
|
||||||
|
// Push initial state into history
|
||||||
|
history.replaceState(
|
||||||
|
{ moduleId: this.currentModuleId, spaceSlug: this.spaceSlug },
|
||||||
|
"",
|
||||||
|
window.location.href,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle browser back/forward
|
||||||
|
window.addEventListener("popstate", (e) => {
|
||||||
|
const state = e.state;
|
||||||
|
if (state?.moduleId && this.panes.has(state.moduleId)) {
|
||||||
|
this.showPane(state.moduleId);
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Switch to a module tab. Returns true if handled client-side. */
|
||||||
|
async switchTo(moduleId: string): Promise<boolean> {
|
||||||
|
if (moduleId === this.currentModuleId) return true;
|
||||||
|
|
||||||
|
if (this.panes.has(moduleId)) {
|
||||||
|
this.showPane(moduleId);
|
||||||
|
this.updateUrl(moduleId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetchAndInject(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if a pane exists in the DOM cache */
|
||||||
|
has(moduleId: string): boolean {
|
||||||
|
return this.panes.has(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a cached pane from the DOM */
|
||||||
|
removePane(moduleId: string): void {
|
||||||
|
const pane = this.panes.get(moduleId);
|
||||||
|
if (pane) {
|
||||||
|
pane.remove();
|
||||||
|
this.panes.delete(moduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch a module page, extract content, and inject into a new pane */
|
||||||
|
private async fetchAndInject(moduleId: string): Promise<boolean> {
|
||||||
|
const navUrl = (window as any).__rspaceNavUrl;
|
||||||
|
if (!navUrl) return false;
|
||||||
|
const url: string = navUrl(this.spaceSlug, moduleId);
|
||||||
|
|
||||||
|
// Cross-origin URLs → fall back to full navigation
|
||||||
|
try {
|
||||||
|
const resolved = new URL(url, window.location.href);
|
||||||
|
if (resolved.origin !== window.location.origin) return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
if (!app) return false;
|
||||||
|
|
||||||
|
// Show loading spinner
|
||||||
|
const loadingPane = document.createElement("div");
|
||||||
|
loadingPane.className =
|
||||||
|
"rspace-tab-pane rspace-tab-pane--active rspace-tab-loading";
|
||||||
|
loadingPane.innerHTML = '<div class="rspace-tab-spinner"></div>';
|
||||||
|
this.hideAllPanes();
|
||||||
|
app.appendChild(loadingPane);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) {
|
||||||
|
loadingPane.remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await resp.text();
|
||||||
|
const content = this.extractContent(html);
|
||||||
|
if (!content) {
|
||||||
|
loadingPane.remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove loading spinner
|
||||||
|
loadingPane.remove();
|
||||||
|
|
||||||
|
// Create the real pane
|
||||||
|
const pane = document.createElement("div");
|
||||||
|
pane.className = "rspace-tab-pane rspace-tab-pane--active";
|
||||||
|
pane.dataset.moduleId = moduleId;
|
||||||
|
pane.dataset.pageTitle = content.title;
|
||||||
|
pane.innerHTML = content.body;
|
||||||
|
app.appendChild(pane);
|
||||||
|
this.panes.set(moduleId, pane);
|
||||||
|
|
||||||
|
// Load module-specific assets
|
||||||
|
this.loadAssets(content.scripts, content.styles);
|
||||||
|
|
||||||
|
// Update shell state
|
||||||
|
this.currentModuleId = moduleId;
|
||||||
|
document.title = content.title;
|
||||||
|
this.updateShellState(moduleId);
|
||||||
|
this.updateUrl(moduleId);
|
||||||
|
this.updateCanvasLayout(moduleId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
loadingPane.remove();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse full HTML response and extract module content */
|
||||||
|
private extractContent(
|
||||||
|
html: string,
|
||||||
|
): {
|
||||||
|
body: string;
|
||||||
|
title: string;
|
||||||
|
scripts: string[];
|
||||||
|
styles: string[];
|
||||||
|
} | null {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
|
|
||||||
|
// Extract #app content or .rspace-iframe-wrap (for external app modules)
|
||||||
|
const appEl = doc.getElementById("app");
|
||||||
|
const iframeWrap = doc.querySelector(".rspace-iframe-wrap");
|
||||||
|
|
||||||
|
let body: string;
|
||||||
|
if (appEl) {
|
||||||
|
body = appEl.innerHTML;
|
||||||
|
} else if (iframeWrap) {
|
||||||
|
body = iframeWrap.outerHTML;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = doc.title || "";
|
||||||
|
|
||||||
|
// Extract module-specific scripts (exclude shell.js and inline shell bootstrap)
|
||||||
|
const scripts: string[] = [];
|
||||||
|
doc.querySelectorAll('script[type="module"]').forEach((s) => {
|
||||||
|
const src = s.getAttribute("src") || "";
|
||||||
|
if (src.includes("shell.js")) return;
|
||||||
|
const text = s.textContent || "";
|
||||||
|
if (text.includes("import '/shell.js")) return;
|
||||||
|
if (text.includes('import "/shell.js')) return;
|
||||||
|
if (src) scripts.push(src);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract module-specific styles (exclude shell.css)
|
||||||
|
const styles: string[] = [];
|
||||||
|
doc.querySelectorAll('link[rel="stylesheet"]').forEach((l) => {
|
||||||
|
const href = l.getAttribute("href") || "";
|
||||||
|
if (href.includes("shell.css")) return;
|
||||||
|
styles.push(href);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { body, title, scripts, styles };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load script and style assets (idempotent — browsers deduplicate module scripts) */
|
||||||
|
private loadAssets(scripts: string[], styles: string[]): void {
|
||||||
|
for (const src of scripts) {
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.type = "module";
|
||||||
|
el.src = src;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const href of styles) {
|
||||||
|
if (document.querySelector(`link[href="${CSS.escape(href)}"]`))
|
||||||
|
continue;
|
||||||
|
const el = document.createElement("link");
|
||||||
|
el.rel = "stylesheet";
|
||||||
|
el.href = href;
|
||||||
|
document.head.appendChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show a specific pane and update shell state */
|
||||||
|
private showPane(moduleId: string): void {
|
||||||
|
this.hideAllPanes();
|
||||||
|
const pane = this.panes.get(moduleId);
|
||||||
|
if (pane) {
|
||||||
|
pane.classList.add("rspace-tab-pane--active");
|
||||||
|
const storedTitle = pane.dataset.pageTitle;
|
||||||
|
if (storedTitle) document.title = storedTitle;
|
||||||
|
}
|
||||||
|
this.currentModuleId = moduleId;
|
||||||
|
this.updateShellState(moduleId);
|
||||||
|
this.updateCanvasLayout(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide all panes */
|
||||||
|
private hideAllPanes(): void {
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
if (!app) return;
|
||||||
|
app.querySelectorAll(".rspace-tab-pane").forEach((p) => {
|
||||||
|
p.classList.remove("rspace-tab-pane--active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update app-switcher and tab-bar to reflect current module */
|
||||||
|
private updateShellState(moduleId: string): void {
|
||||||
|
const appSwitcher = document.querySelector("rstack-app-switcher");
|
||||||
|
if (appSwitcher) appSwitcher.setAttribute("current", moduleId);
|
||||||
|
|
||||||
|
const tabBar = (window as any).__rspaceTabBar;
|
||||||
|
if (tabBar) tabBar.setAttribute("active", "layer-" + moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle canvas-layout class on #app */
|
||||||
|
private updateCanvasLayout(moduleId: string): void {
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
if (!app) return;
|
||||||
|
if (moduleId === "canvas") {
|
||||||
|
app.classList.add("canvas-layout");
|
||||||
|
} else {
|
||||||
|
app.classList.remove("canvas-layout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Push new URL to browser history */
|
||||||
|
private updateUrl(moduleId: string): void {
|
||||||
|
const navUrl = (window as any).__rspaceNavUrl;
|
||||||
|
if (!navUrl) return;
|
||||||
|
const url: string = navUrl(this.spaceSlug, moduleId);
|
||||||
|
history.pushState(
|
||||||
|
{ moduleId, spaceSlug: this.spaceSlug },
|
||||||
|
"",
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -381,3 +381,29 @@ body[data-theme="light"] {
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Tab pane caching (show/hide) ── */
|
||||||
|
|
||||||
|
.rspace-tab-pane {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rspace-tab-pane--active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rspace-tab-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: calc(100vh - 92px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rspace-tab-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid rgba(255,255,255,0.1);
|
||||||
|
border-top-color: #14b8a6;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: rspace-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ import { RStackSpaceSwitcher } from "../shared/components/rstack-space-switcher"
|
||||||
import { RStackTabBar } from "../shared/components/rstack-tab-bar";
|
import { RStackTabBar } from "../shared/components/rstack-tab-bar";
|
||||||
import { RStackMi } from "../shared/components/rstack-mi";
|
import { RStackMi } from "../shared/components/rstack-mi";
|
||||||
import { rspaceNavUrl } from "../shared/url-helpers";
|
import { rspaceNavUrl } from "../shared/url-helpers";
|
||||||
|
import { TabCache } from "../shared/tab-cache";
|
||||||
|
|
||||||
// Expose URL helper globally (used by shell inline scripts + components)
|
// Expose URL helper globally (used by shell inline scripts + components)
|
||||||
(window as any).__rspaceNavUrl = rspaceNavUrl;
|
(window as any).__rspaceNavUrl = rspaceNavUrl;
|
||||||
|
|
||||||
|
// Expose TabCache for inline shell script to instantiate
|
||||||
|
(window as any).__RSpaceTabCache = TabCache;
|
||||||
|
|
||||||
// Register all header components
|
// Register all header components
|
||||||
RStackIdentity.define();
|
RStackIdentity.define();
|
||||||
RStackAppSwitcher.define();
|
RStackAppSwitcher.define();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue