From 7dc4e9b3e94a06532ce199e519f918308aa75f0b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 1 Apr 2026 11:55:13 -0700 Subject: [PATCH] Enforce module disabling on canvas folk-rapp shapes Disabled rApp modules now show a grayed lock overlay on the canvas instead of loading their iframe/widget. Uses a static instance registry so setEnabledModules() broadcasts to all live shapes immediately. Re-enabling a module reloads its content. Co-Authored-By: Claude Opus 4.6 --- lib/folk-rapp.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/lib/folk-rapp.ts b/lib/folk-rapp.ts index 8debb31..01d5239 100644 --- a/lib/folk-rapp.ts +++ b/lib/folk-rapp.ts @@ -195,6 +195,29 @@ const styles = css` text-align: center; } + .rapp-disabled { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 8px; + color: var(--rs-text-muted); + font-size: 13px; + padding: 16px; + text-align: center; + } + + .rapp-disabled-icon { + font-size: 28px; + opacity: 0.4; + } + + :host([data-module-disabled]) .rapp-header { + filter: grayscale(0.6); + opacity: 0.6; + } + /* Module picker (shown when no moduleId set) */ .rapp-picker { display: flex; @@ -598,9 +621,13 @@ export class FolkRApp extends FolkShape { /** Enabled module IDs for picker/switcher filtering (null = all enabled) */ static enabledModuleIds: Set | null = null; + /** Live instance registry for broadcasting enabled-state changes */ + static #instances = new Set(); + /** Update which modules appear in the picker and switcher dropdowns */ static setEnabledModules(ids: string[] | null) { FolkRApp.enabledModuleIds = ids ? new Set(ids) : null; + for (const inst of FolkRApp.#instances) inst.#syncDisabledState(); } /** Port descriptors for data pipe integration (AC#3) */ @@ -813,6 +840,9 @@ export class FolkRApp extends FolkShape { } catch { /* iframe not ready */ } }) as EventListener); + // Register for enabled-state broadcasts + FolkRApp.#instances.add(this); + // Load content if (this.#moduleId) { this.#renderContent(); @@ -825,6 +855,7 @@ export class FolkRApp extends FolkShape { disconnectedCallback() { super.disconnectedCallback?.(); + FolkRApp.#instances.delete(this); if (this.#messageHandler) { window.removeEventListener("message", this.#messageHandler); this.#messageHandler = null; @@ -964,10 +995,56 @@ export class FolkRApp extends FolkShape { return onSubdomain ? `/${this.#moduleId}` : `/${space}/${this.#moduleId}`; } + /** Check if this shape's module is currently disabled */ + #isModuleDisabled(): boolean { + const enabled = FolkRApp.enabledModuleIds; + if (!enabled) return false; // null = all enabled + return this.#moduleId !== "" && !enabled.has(this.#moduleId); + } + + /** Show the disabled overlay and tear down any active iframe/timer */ + #showDisabled() { + if (!this.#contentEl) return; + + // Clear iframe and timer + this.#iframe = null; + if (this.#refreshTimer) { + clearInterval(this.#refreshTimer); + this.#refreshTimer = null; + } + + this.setAttribute("data-module-disabled", ""); + + const meta = MODULE_META[this.#moduleId]; + this.#contentEl.innerHTML = ` +
+ 🔒 + ${meta?.name || this.#moduleId} is disabled +
+ `; + } + + /** Sync this instance's visual state with the current enabled set */ + #syncDisabledState() { + if (!this.#contentEl || !this.#moduleId) return; + const disabled = this.#isModuleDisabled(); + const wasDisabled = this.hasAttribute("data-module-disabled"); + + if (disabled && !wasDisabled) { + this.#showDisabled(); + } else if (!disabled && wasDisabled) { + this.removeAttribute("data-module-disabled"); + this.#renderContent(); + } + } + /** Route to the right render method based on current mode */ #renderContent() { if (!this.#contentEl || !this.#moduleId) return; + // Block rendering if module is disabled + if (this.#isModuleDisabled()) { this.#showDisabled(); return; } + // Clear refresh timer if (this.#refreshTimer) { clearInterval(this.#refreshTimer);