diff --git a/modules/books/mod.ts b/modules/books/mod.ts index da357ca..c331011 100644 --- a/modules/books/mod.ts +++ b/modules/books/mod.ts @@ -10,7 +10,7 @@ import { resolve } from "node:path"; import { mkdir, readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { @@ -201,41 +201,17 @@ routes.get("/api/books/:id/pdf", async (c) => { }); }); -// ── Page: Library (book grid) ── -routes.get("/", async (c) => { +// ── Page: Library ── +routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "personal"; - - // Fetch books for the library page - const rows = await sql.unsafe( - `SELECT id, slug, title, author, description, pdf_size_bytes, page_count, tags, - cover_color, contributor_name, featured, view_count, created_at - FROM rbooks.books WHERE status = 'published' - ORDER BY featured DESC, created_at DESC LIMIT 50` - ); - - const booksJSON = JSON.stringify(rows); - - const html = renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Library | rSpace`, moduleId: "books", spaceSlug, - body: ` - - `, modules: getModuleInfoList(), theme: "dark", - head: ``, - scripts: ` - - `, - }); - - return c.html(html); + standaloneDomain: "rbooks.online", + })); }); // ── Page: Book reader ── diff --git a/modules/cal/mod.ts b/modules/cal/mod.ts index 358ca28..eeec1d7 100644 --- a/modules/cal/mod.ts +++ b/modules/cal/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -375,15 +375,13 @@ routes.get("/api/context/:tool", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Calendar | rSpace`, moduleId: "cal", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rcal.online", })); }); diff --git a/modules/cart/mod.ts b/modules/cart/mod.ts index 113925c..3d07441 100644 --- a/modules/cart/mod.ts +++ b/modules/cart/mod.ts @@ -10,7 +10,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import { depositOrderRevenue } from "./flow"; import type { RSpaceModule } from "../../shared/module"; @@ -441,15 +441,13 @@ routes.post("/api/fulfill/resolve", async (c) => { // ── Page route: shop ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `Shop | rSpace`, moduleId: "cart", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rcart.online", })); }); diff --git a/modules/choices/mod.ts b/modules/choices/mod.ts index 13d7558..ff26119 100644 --- a/modules/choices/mod.ts +++ b/modules/choices/mod.ts @@ -9,7 +9,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import type { RSpaceModule } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module"; import { getDocumentData } from "../../server/community-store"; @@ -48,15 +48,13 @@ routes.get("/api/choices", async (c) => { // GET / — choices page routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Choices | rSpace`, moduleId: "choices", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rchoices.online", })); }); diff --git a/modules/data/mod.ts b/modules/data/mod.ts index 3c1dc4d..e8c0aec 100644 --- a/modules/data/mod.ts +++ b/modules/data/mod.ts @@ -6,7 +6,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -120,15 +120,13 @@ routes.post("/api/collect", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Data | rSpace`, moduleId: "data", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rdata.online", })); }); diff --git a/modules/files/mod.ts b/modules/files/mod.ts index 4965f18..90a279a 100644 --- a/modules/files/mod.ts +++ b/modules/files/mod.ts @@ -9,7 +9,7 @@ import { resolve } from "node:path"; import { mkdir, writeFile, unlink } from "node:fs/promises"; import { createHash, randomBytes } from "node:crypto"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -366,15 +366,13 @@ routes.delete("/api/cards/:id", async (c) => { // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Files | rSpace`, moduleId: "files", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rfiles.online", })); }); diff --git a/modules/forum/mod.ts b/modules/forum/mod.ts index c2f6cc0..08c3314 100644 --- a/modules/forum/mod.ts +++ b/modules/forum/mod.ts @@ -7,7 +7,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import { provisionInstance, destroyInstance } from "./lib/provisioner"; import type { RSpaceModule } from "../../shared/module"; @@ -157,15 +157,13 @@ routes.get("/api/health", (c) => { // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Forum | rSpace`, moduleId: "forum", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rforum.online", })); }); diff --git a/modules/funds/mod.ts b/modules/funds/mod.ts index 9c11aa5..509dd95 100644 --- a/modules/funds/mod.ts +++ b/modules/funds/mod.ts @@ -8,7 +8,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import type { RSpaceModule } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -193,18 +193,16 @@ const fundsScripts = ` const fundsStyles = ``; -// Landing page — TBFF info + flow list +// Landing page — iframes the standalone rfunds.online app routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `rFunds — TBFF Flow Funding | rSpace`, moduleId: "funds", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: fundsStyles, - body: ``, - scripts: fundsScripts, + standaloneDomain: "rfunds.online", })); }); diff --git a/modules/inbox/mod.ts b/modules/inbox/mod.ts index 15d7c8f..4f97d33 100644 --- a/modules/inbox/mod.ts +++ b/modules/inbox/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -530,15 +530,13 @@ runSyncLoop(); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Inbox | rSpace`, moduleId: "inbox", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rinbox.online", })); }); diff --git a/modules/maps/mod.ts b/modules/maps/mod.ts index c230664..955c497 100644 --- a/modules/maps/mod.ts +++ b/modules/maps/mod.ts @@ -7,7 +7,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -133,15 +133,13 @@ routes.get("/api/c3nav/:event", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Maps | rSpace`, moduleId: "maps", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rmaps.online", })); }); diff --git a/modules/network/mod.ts b/modules/network/mod.ts index 0bd075d..890793c 100644 --- a/modules/network/mod.ts +++ b/modules/network/mod.ts @@ -7,7 +7,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -216,15 +216,13 @@ routes.get("/api/workspaces", (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Network | rSpace`, moduleId: "network", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rnetwork.online", })); }); diff --git a/modules/notes/mod.ts b/modules/notes/mod.ts index dd3860c..06b5e1c 100644 --- a/modules/notes/mod.ts +++ b/modules/notes/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -361,15 +361,13 @@ routes.delete("/api/notes/:id", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Notes | rSpace`, moduleId: "notes", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rnotes.online", })); }); diff --git a/modules/photos/mod.ts b/modules/photos/mod.ts index 4fd5f3b..5073588 100644 --- a/modules/photos/mod.ts +++ b/modules/photos/mod.ts @@ -7,7 +7,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -108,15 +108,13 @@ routes.get("/api/assets/:id/original", async (c) => { // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Photos | rSpace`, moduleId: "photos", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rphotos.online", })); }); diff --git a/modules/providers/mod.ts b/modules/providers/mod.ts index 79cf1a8..7a87734 100644 --- a/modules/providers/mod.ts +++ b/modules/providers/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -348,15 +348,13 @@ routes.delete("/api/providers/:id", async (c) => { // ── Page route: browse providers ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `Providers | rSpace`, moduleId: "providers", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "providers.mycofi.earth", })); }); diff --git a/modules/pubs/mod.ts b/modules/pubs/mod.ts index 4f7bccb..f9f7593 100644 --- a/modules/pubs/mod.ts +++ b/modules/pubs/mod.ts @@ -13,7 +13,7 @@ import { parseMarkdown } from "./parse-document"; import { compileDocument } from "./typst-compile"; import { getFormat, FORMATS, listFormats } from "./formats"; import type { BookFormat } from "./formats"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -319,32 +319,16 @@ routes.get("/api/artifact/:id/pdf", async (c) => { }); // ── Page: Editor ── -routes.get("/", async (c) => { +routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "personal"; - - const formatsJSON = JSON.stringify(listFormats()); - - const html = renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — rPubs Editor | rSpace`, moduleId: "pubs", spaceSlug, - body: ` - - `, modules: getModuleInfoList(), theme: "dark", - head: ``, - scripts: ` - - `, - }); - - return c.html(html); + standaloneDomain: "rpubs.online", + })); }); // ── Module export ── diff --git a/modules/trips/mod.ts b/modules/trips/mod.ts index 7fcee8e..6df7ad8 100644 --- a/modules/trips/mod.ts +++ b/modules/trips/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -253,15 +253,13 @@ routes.get("/routes", (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Trips | rSpace`, moduleId: "trips", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rtrips.online", })); }); diff --git a/modules/tube/mod.ts b/modules/tube/mod.ts index ca0de2b..94d8333 100644 --- a/modules/tube/mod.ts +++ b/modules/tube/mod.ts @@ -6,7 +6,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -191,15 +191,13 @@ routes.get("/api/health", (c) => c.json({ ok: true })); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Tube | rSpace`, moduleId: "tube", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rtube.online", })); }); diff --git a/modules/vote/mod.ts b/modules/vote/mod.ts index bff5ce9..35d705d 100644 --- a/modules/vote/mod.ts +++ b/modules/vote/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -327,15 +327,13 @@ routes.post("/api/proposals/:id/final-vote", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Vote | rSpace`, moduleId: "vote", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rvote.online", })); }); diff --git a/modules/wallet/mod.ts b/modules/wallet/mod.ts index 3aff1c9..1b9af12 100644 --- a/modules/wallet/mod.ts +++ b/modules/wallet/mod.ts @@ -6,7 +6,7 @@ */ import { Hono } from "hono"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; @@ -94,15 +94,13 @@ function getSafePrefix(chainId: string): string | null { // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${spaceSlug} — Wallet | rSpace`, moduleId: "wallet", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rwallet.online", })); }); diff --git a/modules/work/mod.ts b/modules/work/mod.ts index 0c055a3..1cbade7 100644 --- a/modules/work/mod.ts +++ b/modules/work/mod.ts @@ -9,7 +9,7 @@ import { Hono } from "hono"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { sql } from "../../shared/db/pool"; -import { renderShell } from "../../server/shell"; +import { renderShell, renderIframeShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; @@ -217,15 +217,13 @@ routes.get("/api/spaces/:slug/activity", async (c) => { // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; - return c.html(renderShell({ + return c.html(renderIframeShell({ title: `${space} — Work | rSpace`, moduleId: "work", spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: ``, - scripts: ``, + standaloneDomain: "rwork.online", })); }); diff --git a/server/shell.ts b/server/shell.ts index 79be254..9d6e2d9 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -411,6 +411,81 @@ const WELCOME_CSS = ` } `; +/** + * Shell that embeds a standalone app via iframe. + * + * Wraps the independent app's domain in the shared rSpace header/nav + * so users get the latest code from the standalone repo while preserving + * the unified space/identity experience. + */ +export interface IframeShellOptions extends Omit { + /** 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 @@ +
+ +

Loading...

@@ -654,6 +657,30 @@
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
+ @@ -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)