{
+ /** The standalone app domain, e.g. "rvote.online" */
+ standaloneDomain: string;
+ /** Extra path to append after the domain root (default: "") */
+ path?: string;
+}
+
+export function renderIframeShell(opts: IframeShellOptions): string {
+ const { standaloneDomain, path = "", ...shellOpts } = opts;
+ const iframeSrc = `https://${standaloneDomain}${path}`;
+
+ return renderShell({
+ ...shellOpts,
+ body: ``,
+ styles: ``,
+ scripts: ``,
+ });
+}
+
function escapeHtml(s: string): string {
return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
diff --git a/website/canvas.html b/website/canvas.html
index 132118f..a439d52 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -577,6 +577,9 @@
+
+
+
+
+
@@ -724,6 +751,7 @@
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 { rspaceNavUrl } from "@shared/url-helpers";
// Register shell header components
RStackIdentity.define();
@@ -736,6 +764,55 @@
document.querySelector("rstack-app-switcher")?.setModules(data.modules || []);
}).catch(() => {});
+ // ── Tab bar / Layer system initialization ──
+ const tabBar = document.querySelector("rstack-tab-bar");
+ if (tabBar) {
+ const canvasDefaultLayer = {
+ id: "layer-canvas",
+ moduleId: "canvas",
+ label: "rSpace",
+ order: 0,
+ color: "",
+ visible: true,
+ createdAt: Date.now(),
+ };
+
+ tabBar.setLayers([canvasDefaultLayer]);
+ tabBar.setAttribute("active", canvasDefaultLayer.id);
+
+ // Tab switching: navigate to the selected module's page
+ tabBar.addEventListener("layer-switch", (e) => {
+ const { moduleId } = e.detail;
+ if (moduleId === "canvas") return; // already on canvas
+ window.location.href = rspaceNavUrl(
+ document.querySelector("rstack-space-switcher")?.getAttribute("current") || "demo",
+ moduleId
+ );
+ });
+
+ // Adding a new tab: navigate to that module
+ tabBar.addEventListener("layer-add", (e) => {
+ const { moduleId } = e.detail;
+ window.location.href = rspaceNavUrl(
+ document.querySelector("rstack-space-switcher")?.getAttribute("current") || "demo",
+ moduleId
+ );
+ });
+
+ // Closing a tab
+ tabBar.addEventListener("layer-close", (e) => {
+ tabBar.removeLayer(e.detail.layerId);
+ });
+
+ // View mode toggle
+ tabBar.addEventListener("view-toggle", (e) => {
+ document.dispatchEvent(new CustomEvent("layer-view-mode", { detail: { mode: e.detail.mode } }));
+ });
+
+ // Expose for CommunitySync wiring
+ window.__rspaceTabBar = tabBar;
+ }
+
// Register service worker for offline support
if ("serviceWorker" in navigator && window.location.hostname !== "localhost") {
navigator.serviceWorker.register("/sw.js").then((reg) => {
@@ -807,6 +884,11 @@
spaceSwitcher.setAttribute("name", communitySlug);
}
+ // Update tab bar with resolved space slug
+ if (tabBar) {
+ tabBar.setAttribute("space", communitySlug);
+ }
+
const canvas = document.getElementById("canvas");
const canvasContent = document.getElementById("canvas-content");
const status = document.getElementById("status");
@@ -844,6 +926,94 @@
detail: { sync, communitySlug }
}));
+ // Wire tab bar to CommunitySync for layer persistence
+ if (tabBar && sync) {
+ const canvasDefaultLayer = {
+ id: "layer-canvas",
+ moduleId: "canvas",
+ label: "rSpace",
+ order: 0,
+ color: "",
+ visible: true,
+ createdAt: Date.now(),
+ };
+
+ // Load persisted layers from Automerge
+ const layers = sync.getLayers?.() || [];
+ if (layers.length > 0) {
+ tabBar.setLayers(layers);
+ const activeId = sync.doc?.activeLayerId;
+ if (activeId) tabBar.setAttribute("active", activeId);
+ if (sync.getFlows) tabBar.setFlows(sync.getFlows());
+ } else {
+ // First visit: persist the canvas layer
+ sync.addLayer?.(canvasDefaultLayer);
+ sync.setActiveLayer?.(canvasDefaultLayer.id);
+ }
+
+ // Persist layer switch
+ tabBar.addEventListener("layer-switch", (e) => {
+ sync.setActiveLayer?.(e.detail.layerId);
+ });
+
+ // Persist new layer
+ tabBar.addEventListener("layer-add", (e) => {
+ const { moduleId } = e.detail;
+ sync.addLayer?.({
+ id: "layer-" + moduleId,
+ moduleId,
+ label: moduleId,
+ order: (sync.getLayers?.() || []).length,
+ color: "",
+ visible: true,
+ createdAt: Date.now(),
+ });
+ });
+
+ // Persist layer close
+ tabBar.addEventListener("layer-close", (e) => {
+ sync.removeLayer?.(e.detail.layerId);
+ });
+
+ // Persist layer reorder
+ tabBar.addEventListener("layer-reorder", (e) => {
+ const { layerId, newIndex } = e.detail;
+ sync.updateLayer?.(layerId, { order: newIndex });
+ const allLayers = sync.getLayers?.() || [];
+ allLayers.forEach((l, i) => {
+ if (l.order !== i) sync.updateLayer?.(l.id, { order: i });
+ });
+ });
+
+ // Flow creation from stack view
+ tabBar.addEventListener("flow-create", (e) => {
+ sync.addFlow?.(e.detail.flow);
+ });
+
+ // Flow removal from stack view
+ tabBar.addEventListener("flow-remove", (e) => {
+ sync.removeFlow?.(e.detail.flowId);
+ });
+
+ // View mode persistence
+ tabBar.addEventListener("view-toggle", (e) => {
+ sync.setLayerViewMode?.(e.detail.mode);
+ });
+
+ // Sync remote layer/flow changes back to tab bar
+ sync.addEventListener("change", () => {
+ const updatedLayers = sync.getLayers?.() || [];
+ if (updatedLayers.length > 0) {
+ tabBar.setLayers(updatedLayers);
+ if (sync.getFlows) 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);
+ }
+ });
+ }
+
// Initialize Presence for real-time cursors
const peerId = generatePeerId();
const storedUsername = localStorage.getItem("rspace-username") || `User ${peerId.slice(0, 4)}`;
@@ -1468,6 +1638,38 @@
});
});
+ // rApp embed buttons — embed any module as an interactive iframe on the canvas
+ const rAppModules = [
+ { btnId: "embed-notes", moduleId: "notes", icon: "📝", name: "rNotes" },
+ { btnId: "embed-photos", moduleId: "photos", icon: "📸", name: "rPhotos" },
+ { btnId: "embed-books", moduleId: "books", icon: "📚", name: "rBooks" },
+ { btnId: "embed-pubs", moduleId: "pubs", icon: "📖", name: "rPubs" },
+ { btnId: "embed-files", moduleId: "files", icon: "📁", name: "rFiles" },
+ { btnId: "embed-work", moduleId: "work", icon: "📋", name: "rWork" },
+ { btnId: "embed-forum", moduleId: "forum", icon: "💬", name: "rForum" },
+ { btnId: "embed-inbox", moduleId: "inbox", icon: "📧", name: "rInbox" },
+ { btnId: "embed-tube", moduleId: "tube", icon: "🎬", name: "rTube" },
+ { btnId: "embed-funds", moduleId: "funds", icon: "🌊", name: "rFunds" },
+ { btnId: "embed-wallet", moduleId: "wallet", icon: "💰", name: "rWallet" },
+ { btnId: "embed-vote", moduleId: "vote", icon: "🗳️", name: "rVote" },
+ { btnId: "embed-cart", moduleId: "cart", icon: "🛒", name: "rCart" },
+ { btnId: "embed-data", moduleId: "data", icon: "📊", name: "rData" },
+ { btnId: "embed-network", moduleId: "network", icon: "🌍", name: "rNetwork" },
+ { btnId: "embed-splat", moduleId: "splat", icon: "🔮", name: "rSplat" },
+ { btnId: "embed-providers", moduleId: "providers", icon: "🏭", name: "rProviders" },
+ { btnId: "embed-swag", moduleId: "swag", icon: "🎨", name: "rSwag" },
+ ];
+
+ for (const app of rAppModules) {
+ const btn = document.getElementById(app.btnId);
+ if (btn) {
+ btn.addEventListener("click", () => {
+ const moduleUrl = rspaceNavUrl(communitySlug, app.moduleId);
+ newShape("folk-embed", { url: moduleUrl });
+ });
+ }
+ }
+
// Feed shape — pull live data from another layer/module
document.getElementById("new-feed").addEventListener("click", () => {
// Prompt for source module (simple for now — will get a proper UI)