diff --git a/shared/components/rstack-tab-bar.ts b/shared/components/rstack-tab-bar.ts
index 7d83216e..376a5487 100644
--- a/shared/components/rstack-tab-bar.ts
+++ b/shared/components/rstack-tab-bar.ts
@@ -476,15 +476,11 @@ export class RStackTabBar extends HTMLElement {
#renderAddMenu(): string {
const existingModuleIds = new Set(this.#layers.map(l => l.moduleId));
- // Use server module list if available, fall back to MODULE_BADGES keys
- const allModules: Array<{ id: string; name: string; icon: string; description: string }> =
- this.#modules.length > 0
- ? this.#modules
- : Object.keys(MODULE_BADGES)
- .map(id => ({ id, name: id, icon: "", description: "" }));
+ // Only use server module list โ never fall back to MODULE_BADGES (leaks disabled modules)
+ const allModules = this.#modules;
if (allModules.length === 0) {
- return `
`;
+ return ``;
}
let html = "";
diff --git a/website/canvas.html b/website/canvas.html
index 4fc3b156..346dc859 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -153,6 +153,26 @@
cursor: default;
}
+ /* Disabled-module overlay on canvas shapes */
+ [data-module-disabled] {
+ position: relative;
+ opacity: 0.4;
+ pointer-events: none !important;
+ filter: grayscale(0.6);
+ }
+ [data-module-disabled]::after {
+ content: "Module disabled";
+ position: absolute;
+ top: 4px; right: 4px;
+ background: rgba(0,0,0,0.7);
+ color: #fff;
+ font-size: 11px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ pointer-events: none;
+ z-index: 9999;
+ }
+
/* Picker modal โ inline replacement for browser prompt() */
.picker-modal-overlay {
position: fixed; inset: 0; z-index: 9999;
@@ -1985,42 +2005,18 @@
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
@@ -2587,6 +2599,8 @@
const cls = customElements.get("folk-task-request");
if (cls?.setEnabledModules) cls.setEnabledModules(enabledIds);
});
+ // Overlay disabled-module shapes on canvas
+ syncCanvasShapeModuleStates(enabledIds);
}).catch(() => {});
// React to runtime module toggling from app switcher
@@ -2609,6 +2623,9 @@
const FolkTaskRequest = customElements.get("folk-task-request");
if (FolkTaskRequest?.setEnabledModules) FolkTaskRequest.setEnabledModules(enabledModules);
+ // Update disabled-module overlays on canvas shapes
+ syncCanvasShapeModuleStates(enabledModules);
+
document.querySelectorAll("[data-requires-module]").forEach(el => {
el.style.display = enabledSet.has(el.dataset.requiresModule) ? "" : "none";
});
@@ -3963,6 +3980,52 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
});
}
+ // Map shape tag โ required module (for gating disabled-module shapes)
+ const SHAPE_TO_MODULE = {
+ "folk-map": "rmaps",
+ "folk-calendar": "rcal",
+ "folk-social-post": "rsocials",
+ "folk-splat": "rsplat",
+ "folk-design-agent": "rdesign",
+ "folk-choice-vote": "rchoices",
+ "folk-choice-rank": "rchoices",
+ "folk-choice-spider": "rchoices",
+ "folk-spider-3d": "rchoices",
+ "folk-choice-conviction": "rchoices",
+ "folk-token-mint": "rwallet",
+ "folk-token-ledger": "rwallet",
+ "folk-transaction-builder": "rwallet",
+ "folk-commitment-pool": "rtime",
+ "folk-task-request": "rtime",
+ "folk-gov-binary": "rvote",
+ "folk-gov-threshold": "rvote",
+ "folk-itinerary": "rtrips",
+ "folk-destination": "rtrips",
+ "folk-budget": "rtrips",
+ "folk-packing-list": "rtrips",
+ "folk-booking": "rtrips",
+ "folk-video-chat": "rmeets",
+ // folk-rapp shapes are gated separately via FolkRApp.setEnabledModules
+ };
+
+ /** Toggle data-module-disabled on existing canvas shapes based on enabled module list */
+ function syncCanvasShapeModuleStates(enabledIds) {
+ if (!enabledIds) return;
+ const enabledSet = new Set(enabledIds);
+ const canvasEl = document.getElementById("canvas-content") || document.getElementById("canvas");
+ if (!canvasEl) return;
+ for (const shape of canvasEl.children) {
+ const tag = shape.tagName?.toLowerCase();
+ const reqMod = SHAPE_TO_MODULE[tag];
+ if (!reqMod) continue;
+ if (enabledSet.has(reqMod)) {
+ shape.removeAttribute("data-module-disabled");
+ } else {
+ shape.setAttribute("data-module-disabled", reqMod);
+ }
+ }
+ }
+
// Default dimensions for each shape type
const SHAPE_DEFAULTS = {
"folk-markdown": { width: 300, height: 200 },
@@ -4226,6 +4289,13 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
// atPosition: optional { x, y } in canvas coordinates โ places shape centered there exactly.
// Without atPosition, uses findFreePosition to auto-place without overlapping.
function newShape(tagName, props = {}, atPosition) {
+ // Block creating shapes for disabled modules
+ const _reqMod = SHAPE_TO_MODULE[tagName];
+ if (_reqMod && window.__rspaceEnabledModules &&
+ !new Set(window.__rspaceEnabledModules).has(_reqMod)) {
+ console.warn(`[Canvas] Blocked newShape("${tagName}") โ module "${_reqMod}" is disabled`);
+ return null;
+ }
const id = `shape-${Date.now()}-${++shapeCounter}`;
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
@@ -5071,7 +5141,10 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
// Auto-spawn shape from ?tool= URL param (e.g. ?tool=folk-zine-gen)
const toolParam = urlParams.get("tool");
- if (toolParam && shapeRegistry.has(toolParam)) {
+ const _toolMod = toolParam && SHAPE_TO_MODULE[toolParam];
+ const _toolAllowed = !_toolMod || !window.__rspaceEnabledModules ||
+ new Set(window.__rspaceEnabledModules).has(_toolMod);
+ if (toolParam && shapeRegistry.has(toolParam) && _toolAllowed) {
const spawnToolShape = () => {
newShape(toolParam);
const cleanUrl = new URL(window.location.href);