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)}...
+
+
+
⚠
+
${escapeHtml(moduleName)} is not available
+
The standalone app at ${escapeHtml(standaloneDomain)} didn't respond.
+
+
+ `,
styles: ``,
scripts: `