feat(rgov): standalone n8n-style GovMod circuit canvas

Add <folk-gov-circuit> component with interactive SVG canvas pre-loaded
with 3 demo governance circuits. 8 node types (signoff, threshold, knob,
project, quadratic, conviction, multisig, sankey) with pan/zoom, node
dragging, Bezier wiring, palette sidebar, and detail panel. Compatible
with rspace canvas shape types for rapplet integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-04 01:55:52 +00:00 committed by Jeff Emmett
parent 996f9ec465
commit cc12f7a936
3 changed files with 1580 additions and 49 deletions

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@
*/ */
import { Hono } from "hono"; import { Hono } from "hono";
import { resolve } from "path";
import { renderShell } from "../../server/shell"; import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module"; import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module";
@ -17,53 +16,10 @@ import { addShapes, getDocumentData } from "../../server/community-store";
const routes = new Hono(); const routes = new Hono();
// ── Canvas content loader (same approach as rspace module) ── // ── Module page — renders standalone GovMod circuit canvas ──
const DIST_DIR = resolve(import.meta.dir, "../../dist"); routes.get("/", (c) => {
let canvasCache: { body: string; styles: string; scripts: string } | null = null;
function extractCanvasContent(html: string) {
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
const styleMatches = [...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)];
const scriptMatches = [...html.matchAll(/<script[^>]*>[\s\S]*?<\/script>/gi)];
return {
body: bodyMatch?.[1] || "",
styles: styleMatches.map(m => m[0]).join("\n"),
scripts: scriptMatches.map(m => m[0]).join("\n"),
};
}
async function getCanvasContent() {
if (canvasCache) return canvasCache;
const moduleFile = Bun.file(resolve(DIST_DIR, "canvas-module.html"));
if (await moduleFile.exists()) {
canvasCache = {
body: await moduleFile.text(),
styles: "",
scripts: `<script type="module" src="/canvas-module.js"></script>`,
};
return canvasCache;
}
const fullFile = Bun.file(resolve(DIST_DIR, "canvas.html"));
if (await fullFile.exists()) {
canvasCache = extractCanvasContent(await fullFile.text());
return canvasCache;
}
return {
body: `<div style="padding:2rem;text-align:center;color:#64748b;">Canvas loading...</div>`,
styles: "",
scripts: "",
};
}
// ── Module page (within a space) — renders canvas directly ──
routes.get("/", async (c) => {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
const canvas = await getCanvasContent();
return c.html(renderShell({ return c.html(renderShell({
title: `${space} — rGov | rSpace`, title: `${space} — rGov | rSpace`,
@ -71,9 +27,8 @@ routes.get("/", async (c) => {
spaceSlug: space, spaceSlug: space,
modules: getModuleInfoList(), modules: getModuleInfoList(),
theme: "dark", theme: "dark",
body: canvas.body, body: `<folk-gov-circuit space="${space}" style="width:100%;height:100%;display:block;"></folk-gov-circuit>`,
styles: canvas.styles, scripts: `<script type="module" src="/modules/rgov/folk-gov-circuit.js"></script>`,
scripts: canvas.scripts,
})); }));
}); });

View File

@ -1394,6 +1394,27 @@ export default defineConfig({
} }
} }
// ── Build rGov circuit canvas component ──
mkdirSync(resolve(__dirname, "dist/modules/rgov"), { recursive: true });
await wasmBuild({
configFile: false,
root: resolve(__dirname, "modules/rgov/components"),
build: {
emptyOutDir: false,
outDir: resolve(__dirname, "dist/modules/rgov"),
lib: {
entry: resolve(__dirname, "modules/rgov/components/folk-gov-circuit.ts"),
formats: ["es"],
fileName: () => "folk-gov-circuit.js",
},
rollupOptions: {
output: {
entryFileNames: "folk-gov-circuit.js",
},
},
},
});
// ── Generate content hashes for cache-busting ── // ── Generate content hashes for cache-busting ──
const { readdirSync, readFileSync, writeFileSync, statSync: statSync2 } = await import("node:fs"); const { readdirSync, readFileSync, writeFileSync, statSync: statSync2 } = await import("node:fs");
const { createHash } = await import("node:crypto"); const { createHash } = await import("node:crypto");