feat: canvas tab bar + rApps toolbar + iframe shell for all modules
Add the missing tab bar to the canvas page so users can switch between rApp layers (with full CommunitySync persistence). Add an "rApps" toolbar group that embeds any of the 18 remaining modules as interactive iframes directly on the canvas. Switch all module page routes to renderIframeShell, loading standalone domains inside the unified shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fcf350d40a
commit
0f7d4eb7bb
|
|
@ -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: `
|
||||
<folk-book-shelf id="shelf"></folk-book-shelf>
|
||||
`,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
head: `<link rel="stylesheet" href="/modules/books/books.css">`,
|
||||
scripts: `
|
||||
<script type="module">
|
||||
import { FolkBookShelf } from '/modules/books/folk-book-shelf.js';
|
||||
const shelf = document.getElementById('shelf');
|
||||
shelf.books = ${booksJSON};
|
||||
shelf.spaceSlug = '${spaceSlug}';
|
||||
</script>
|
||||
`,
|
||||
});
|
||||
|
||||
return c.html(html);
|
||||
standaloneDomain: "rbooks.online",
|
||||
}));
|
||||
});
|
||||
|
||||
// ── Page: Book reader ──
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/cal/cal.css">`,
|
||||
body: `<folk-calendar-view space="${space}"></folk-calendar-view>`,
|
||||
scripts: `<script type="module" src="/modules/cal/folk-calendar-view.js"></script>`,
|
||||
standaloneDomain: "rcal.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/cart/cart.css">`,
|
||||
body: `<folk-cart-shop></folk-cart-shop>`,
|
||||
scripts: `<script type="module" src="/modules/cart/folk-cart-shop.js"></script>`,
|
||||
standaloneDomain: "rcart.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/choices/choices.css">`,
|
||||
body: `<folk-choices-dashboard space="${spaceSlug}"></folk-choices-dashboard>`,
|
||||
scripts: `<script type="module" src="/modules/choices/folk-choices-dashboard.js"></script>`,
|
||||
standaloneDomain: "rchoices.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/data/data.css">`,
|
||||
body: `<folk-analytics-view space="${space}"></folk-analytics-view>`,
|
||||
scripts: `<script type="module" src="/modules/data/folk-analytics-view.js"></script>`,
|
||||
standaloneDomain: "rdata.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/files/files.css">`,
|
||||
body: `<folk-file-browser space="${spaceSlug}"></folk-file-browser>`,
|
||||
scripts: `<script type="module" src="/modules/files/folk-file-browser.js"></script>`,
|
||||
standaloneDomain: "rfiles.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/forum/forum.css">`,
|
||||
body: `<folk-forum-dashboard space="${spaceSlug}"></folk-forum-dashboard>`,
|
||||
scripts: `<script type="module" src="/modules/forum/folk-forum-dashboard.js"></script>`,
|
||||
standaloneDomain: "rforum.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `<link rel="stylesheet" href="/modules/funds/funds.css">`;
|
||||
|
||||
// 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: `<folk-funds-app space="${spaceSlug}"></folk-funds-app>`,
|
||||
scripts: fundsScripts,
|
||||
standaloneDomain: "rfunds.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/inbox/inbox.css">`,
|
||||
body: `<folk-inbox-client space="${space}"></folk-inbox-client>`,
|
||||
scripts: `<script type="module" src="/modules/inbox/folk-inbox-client.js"></script>`,
|
||||
standaloneDomain: "rinbox.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/maps/maps.css">`,
|
||||
body: `<folk-map-viewer space="${space}"></folk-map-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/maps/folk-map-viewer.js"></script>`,
|
||||
standaloneDomain: "rmaps.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/network/network.css">`,
|
||||
body: `<folk-graph-viewer space="${space}"></folk-graph-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/network/folk-graph-viewer.js"></script>`,
|
||||
standaloneDomain: "rnetwork.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/notes/notes.css">`,
|
||||
body: `<folk-notes-app space="${space}"></folk-notes-app>`,
|
||||
scripts: `<script type="module" src="/modules/notes/folk-notes-app.js"></script>`,
|
||||
standaloneDomain: "rnotes.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/photos/photos.css">`,
|
||||
body: `<folk-photo-gallery space="${spaceSlug}"></folk-photo-gallery>`,
|
||||
scripts: `<script type="module" src="/modules/photos/folk-photo-gallery.js"></script>`,
|
||||
standaloneDomain: "rphotos.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/providers/providers.css">`,
|
||||
body: `<folk-provider-directory></folk-provider-directory>`,
|
||||
scripts: `<script type="module" src="/modules/providers/folk-provider-directory.js"></script>`,
|
||||
standaloneDomain: "providers.mycofi.earth",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `
|
||||
<folk-pubs-editor id="editor"></folk-pubs-editor>
|
||||
`,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
head: `<link rel="stylesheet" href="/modules/pubs/pubs.css">`,
|
||||
scripts: `
|
||||
<script type="module">
|
||||
import { FolkPubsEditor } from '/modules/pubs/folk-pubs-editor.js';
|
||||
const editor = document.getElementById('editor');
|
||||
editor.formats = ${formatsJSON};
|
||||
editor.spaceSlug = '${escapeAttr(spaceSlug)}';
|
||||
</script>
|
||||
`,
|
||||
});
|
||||
|
||||
return c.html(html);
|
||||
standaloneDomain: "rpubs.online",
|
||||
}));
|
||||
});
|
||||
|
||||
// ── Module export ──
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/trips/trips.css">`,
|
||||
body: `<folk-trips-planner space="${space}"></folk-trips-planner>`,
|
||||
scripts: `<script type="module" src="/modules/trips/folk-trips-planner.js"></script>`,
|
||||
standaloneDomain: "rtrips.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/tube/tube.css">`,
|
||||
body: `<folk-video-player space="${space}"></folk-video-player>`,
|
||||
scripts: `<script type="module" src="/modules/tube/folk-video-player.js"></script>`,
|
||||
standaloneDomain: "rtube.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/vote/vote.css">`,
|
||||
body: `<folk-vote-dashboard space="${space}"></folk-vote-dashboard>`,
|
||||
scripts: `<script type="module" src="/modules/vote/folk-vote-dashboard.js"></script>`,
|
||||
standaloneDomain: "rvote.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/wallet/wallet.css">`,
|
||||
body: `<folk-wallet-viewer space="${spaceSlug}"></folk-wallet-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/wallet/folk-wallet-viewer.js"></script>`,
|
||||
standaloneDomain: "rwallet.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: `<link rel="stylesheet" href="/modules/work/work.css">`,
|
||||
body: `<folk-work-board space="${space}"></folk-work-board>`,
|
||||
scripts: `<script type="module" src="/modules/work/folk-work-board.js"></script>`,
|
||||
standaloneDomain: "rwork.online",
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ShellOptions, "body" | "scripts" | "styles"> {
|
||||
/** 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: `<iframe id="rspace-module-frame"
|
||||
src="${escapeAttr(iframeSrc)}"
|
||||
class="rspace-iframe"
|
||||
allow="camera;microphone;fullscreen;autoplay;clipboard-write;web-share"
|
||||
loading="lazy"></iframe>`,
|
||||
styles: `<style>
|
||||
#app.iframe-layout {
|
||||
padding-top: 92px;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.rspace-iframe {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
</style>`,
|
||||
scripts: `<script type="module">
|
||||
document.getElementById('app')?.classList.add('iframe-layout');
|
||||
|
||||
// Identity bridge: forward EncryptID session to the embedded app
|
||||
const frame = document.getElementById('rspace-module-frame');
|
||||
if (frame) {
|
||||
frame.addEventListener('load', () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('encryptid_session');
|
||||
if (raw) {
|
||||
frame.contentWindow?.postMessage({
|
||||
type: 'rspace:identity',
|
||||
session: JSON.parse(raw),
|
||||
space: '${escapeAttr(shellOpts.spaceSlug)}',
|
||||
module: '${escapeAttr(shellOpts.moduleId)}',
|
||||
}, 'https://${escapeAttr(standaloneDomain)}');
|
||||
}
|
||||
} catch(e) {}
|
||||
});
|
||||
|
||||
// Listen for navigation messages from the iframe
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.origin !== 'https://${escapeAttr(standaloneDomain)}') return;
|
||||
if (e.data?.type === 'rspace:navigate') {
|
||||
const { space, module } = e.data;
|
||||
if (space && module && window.__rspaceNavUrl) {
|
||||
window.location.href = window.__rspaceNavUrl(space, module);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>`,
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -577,6 +577,9 @@
|
|||
<rstack-identity></rstack-identity>
|
||||
</div>
|
||||
</header>
|
||||
<div class="rstack-tab-row" data-theme="light">
|
||||
<rstack-tab-bar space="" active="" view-mode="flat"></rstack-tab-bar>
|
||||
</div>
|
||||
<div id="community-info">
|
||||
<h2 id="community-name">Loading...</h2>
|
||||
<p id="community-slug"></p>
|
||||
|
|
@ -654,6 +657,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button class="toolbar-group-toggle">📱 rApps</button>
|
||||
<div class="toolbar-dropdown">
|
||||
<button id="embed-notes" title="Embed rNotes">📝 rNotes</button>
|
||||
<button id="embed-photos" title="Embed rPhotos">📸 rPhotos</button>
|
||||
<button id="embed-books" title="Embed rBooks">📚 rBooks</button>
|
||||
<button id="embed-pubs" title="Embed rPubs">📖 rPubs</button>
|
||||
<button id="embed-files" title="Embed rFiles">📁 rFiles</button>
|
||||
<button id="embed-work" title="Embed rWork">📋 rWork</button>
|
||||
<button id="embed-forum" title="Embed rForum">💬 rForum</button>
|
||||
<button id="embed-inbox" title="Embed rInbox">📧 rInbox</button>
|
||||
<button id="embed-tube" title="Embed rTube">🎬 rTube</button>
|
||||
<button id="embed-funds" title="Embed rFunds">🌊 rFunds</button>
|
||||
<button id="embed-wallet" title="Embed rWallet">💰 rWallet</button>
|
||||
<button id="embed-vote" title="Embed rVote">🗳️ rVote</button>
|
||||
<button id="embed-cart" title="Embed rCart">🛒 rCart</button>
|
||||
<button id="embed-data" title="Embed rData">📊 rData</button>
|
||||
<button id="embed-network" title="Embed rNetwork">🌍 rNetwork</button>
|
||||
<button id="embed-splat" title="Embed rSplat">🔮 rSplat</button>
|
||||
<button id="embed-providers" title="Embed rProviders">🏭 rProviders</button>
|
||||
<button id="embed-swag" title="Embed rSwag">🎨 rSwag</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="toolbar-sep"></span>
|
||||
|
||||
<button id="new-arrow" title="Connect rSpaces">↗️ Connect</button>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue