diff --git a/modules/swag/mod.ts b/modules/swag/mod.ts index 0a36d64..1c6e29a 100644 --- a/modules/swag/mod.ts +++ b/modules/swag/mod.ts @@ -11,7 +11,7 @@ import { mkdir, writeFile, readFile, readdir, stat } from "node:fs/promises"; import { join } from "node:path"; import { getProduct, PRODUCTS } from "./products"; import { processImage } from "./process-image"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -228,15 +228,13 @@ routes.get("/api/artifact/:id", async (c) => { // ── Page route: swag designer ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `Swag Designer | rSpace`, moduleId: "swag", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "swag.mycofi.earth", })); }); diff --git a/server/shell.ts b/server/shell.ts index 9d6e2d9..0091008 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -128,132 +128,157 @@ export function renderShell(opts: ShellOptions): string { }; // ── Tab bar / Layer system initialization ── + // Tabs persist in localStorage so they survive full-page navigations. + // When a user opens a new rApp (via the app switcher or tab-add), + // the next page load reads the existing tabs and adds the new module. const tabBar = document.querySelector('rstack-tab-bar'); const spaceSlug = '${escapeAttr(spaceSlug)}'; const currentModuleId = '${escapeAttr(moduleId)}'; + const TABS_KEY = 'rspace_tabs_' + spaceSlug; + const moduleList = ${moduleListJSON}; if (tabBar) { - // Default layer: current module (bootstrap if no layers saved yet) - const defaultLayer = { - id: 'layer-' + currentModuleId, - moduleId: currentModuleId, - label: ${JSON.stringify(modules.find((m: any) => m.id === moduleId)?.name || moduleId)}, - order: 0, - color: '', - visible: true, - createdAt: Date.now(), - }; + // Helper: look up a module's display name + function getModuleLabel(id) { + const m = moduleList.find(mod => mod.id === id); + return m ? m.name : id; + } - // Set the current module as the active layer - tabBar.setLayers([defaultLayer]); - tabBar.setAttribute('active', defaultLayer.id); + // Helper: create a layer object + function makeLayer(id, order) { + return { + id: 'layer-' + id, + moduleId: id, + label: getModuleLabel(id), + order: order, + color: '', + visible: true, + createdAt: Date.now(), + }; + } - // Listen for tab events + // ── Restore tabs from localStorage ── + let layers; + try { + const saved = localStorage.getItem(TABS_KEY); + layers = saved ? JSON.parse(saved) : []; + if (!Array.isArray(layers)) layers = []; + } catch(e) { layers = []; } + + // Ensure the current module is in the tab list + if (!layers.find(l => l.moduleId === currentModuleId)) { + layers.push(makeLayer(currentModuleId, layers.length)); + } + + // Persist immediately (includes the newly-added tab) + localStorage.setItem(TABS_KEY, JSON.stringify(layers)); + + // Render all tabs with the current one active + tabBar.setLayers(layers); + tabBar.setAttribute('active', 'layer-' + currentModuleId); + + // Helper: save current tab list to localStorage + function saveTabs() { + localStorage.setItem(TABS_KEY, JSON.stringify(layers)); + } + + // ── Tab events ── tabBar.addEventListener('layer-switch', (e) => { const { moduleId } = e.detail; + saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); }); tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; - // Navigate to the new module (layer will be persisted when sync connects) + // Add the new module before navigating so the next page sees it + if (!layers.find(l => l.moduleId === moduleId)) { + layers.push(makeLayer(moduleId, layers.length)); + } + saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); }); tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); - // If we closed the active layer, switch to first remaining - const remaining = tabBar.querySelectorAll?.('[data-layer-id]'); - // The tab bar handles this internally + layers = layers.filter(l => l.id !== layerId); + saveTabs(); + // If we closed the active tab, switch to the first remaining + if (layerId === 'layer-' + currentModuleId && layers.length > 0) { + window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); + } }); tabBar.addEventListener('view-toggle', (e) => { const { mode } = e.detail; - // When switching to stack view, emit event for canvas to connect document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } })); }); // Expose tabBar for CommunitySync integration window.__rspaceTabBar = tabBar; - // If CommunitySync is available, wire up layer persistence + // ── CommunitySync: merge with Automerge once connected ── document.addEventListener('community-sync-ready', (e) => { const sync = e.detail?.sync; if (!sync) return; - // Load persisted layers - const layers = sync.getLayers(); - if (layers.length > 0) { + // Merge: Automerge layers win if they exist, otherwise seed from localStorage + const remoteLayers = sync.getLayers(); + if (remoteLayers.length > 0) { + // Ensure current module is also in the Automerge set + if (!remoteLayers.find(l => l.moduleId === currentModuleId)) { + const newLayer = makeLayer(currentModuleId, remoteLayers.length); + sync.addLayer(newLayer); + } + layers = sync.getLayers(); tabBar.setLayers(layers); const activeId = sync.doc.activeLayerId; if (activeId) tabBar.setAttribute('active', activeId); tabBar.setFlows(sync.getFlows()); } else { - // First visit: save the default layer - sync.addLayer(defaultLayer); - sync.setActiveLayer(defaultLayer.id); + // First connection: push all localStorage tabs into Automerge + for (const l of layers) { + sync.addLayer(l); + } + sync.setActiveLayer('layer-' + currentModuleId); } + // Keep localStorage in sync + saveTabs(); + // Sync layer changes back to Automerge tabBar.addEventListener('layer-switch', (e) => { sync.setActiveLayer(e.detail.layerId); }); - - // Layer add via tab bar (persist new layer) tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; - const newLayer = { - id: 'layer-' + moduleId, - moduleId, - label: moduleId, - order: sync.getLayers().length, - color: '', - visible: true, - createdAt: Date.now(), - }; + const newLayer = makeLayer(moduleId, sync.getLayers().length); sync.addLayer(newLayer); }); - - // Layer close (remove from Automerge) tabBar.addEventListener('layer-close', (e) => { sync.removeLayer(e.detail.layerId); }); - - // Layer reorder tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; sync.updateLayer(layerId, { order: newIndex }); - // Reindex all layers - const layers = sync.getLayers(); - layers.forEach((l, i) => { - if (l.order !== i) sync.updateLayer(l.id, { order: i }); - }); + const all = sync.getLayers(); + all.forEach((l, i) => { if (l.order !== i) sync.updateLayer(l.id, { order: i }); }); }); + tabBar.addEventListener('flow-create', (e) => { sync.addFlow(e.detail.flow); }); + tabBar.addEventListener('flow-remove', (e) => { sync.removeFlow(e.detail.flowId); }); + tabBar.addEventListener('view-toggle', (e) => { sync.setLayerViewMode(e.detail.mode); }); - // Flow creation from stack view drag-to-connect - tabBar.addEventListener('flow-create', (e) => { - sync.addFlow(e.detail.flow); - }); - - // Flow removal from stack view right-click - tabBar.addEventListener('flow-remove', (e) => { - sync.removeFlow(e.detail.flowId); - }); - - // View mode persistence - tabBar.addEventListener('view-toggle', (e) => { - sync.setLayerViewMode(e.detail.mode); - }); - - // Listen for remote layer/flow changes + // Listen for remote changes sync.addEventListener('change', () => { - tabBar.setLayers(sync.getLayers()); + layers = sync.getLayers(); + tabBar.setLayers(layers); tabBar.setFlows(sync.getFlows()); const activeId = sync.doc.activeLayerId; if (activeId) tabBar.setAttribute('active', activeId); const viewMode = sync.doc.layerViewMode; if (viewMode) tabBar.setAttribute('view-mode', viewMode); + saveTabs(); // keep localStorage in sync }); }); } @@ -429,13 +454,28 @@ export function renderIframeShell(opts: IframeShellOptions): string { const { standaloneDomain, path = "", ...shellOpts } = opts; const iframeSrc = `https://${standaloneDomain}${path}`; + const moduleName = shellOpts.modules.find(m => m.id === shellOpts.moduleId)?.name || shellOpts.moduleId; + return renderShell({ ...shellOpts, - body: ``, + body: `
+
+

Loading ${escapeHtml(moduleName)}...

+
+ + `, styles: ``, scripts: `