feat: folk-rapp shape — embed live rApp modules on the canvas
POC for cross-app embedding (TASK-46). New folk-rapp shape type that embeds any rApp module as a live iframe inside a canvas shape. Features: - Module picker dropdown when no module selected - Colored header with module badge/icon - Open-in-tab action button - Syncs moduleId + spaceSlug via Automerge CRDT - Toolbar rApps section now creates folk-rapp (not generic folk-embed) - Fixed stale "canvas" moduleId refs → "rspace" in canvas.html Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
69cb105758
commit
0a795006d3
|
|
@ -769,6 +769,13 @@ export class CommunitySync extends EventTarget {
|
||||||
if (data.scores !== undefined) spider.scores = data.scores;
|
if (data.scores !== undefined) spider.scores = data.scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update rApp embed properties
|
||||||
|
if (data.type === "folk-rapp") {
|
||||||
|
const rapp = shape as any;
|
||||||
|
if (data.moduleId !== undefined && rapp.moduleId !== data.moduleId) rapp.moduleId = data.moduleId;
|
||||||
|
if (data.spaceSlug !== undefined && rapp.spaceSlug !== data.spaceSlug) rapp.spaceSlug = data.spaceSlug;
|
||||||
|
}
|
||||||
|
|
||||||
// Update feed shape properties
|
// Update feed shape properties
|
||||||
if (data.type === "folk-feed") {
|
if (data.type === "folk-feed") {
|
||||||
const feed = shape as any;
|
const feed = shape as any;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,416 @@
|
||||||
|
import { FolkShape } from "./folk-shape";
|
||||||
|
import { css, html } from "./tags";
|
||||||
|
import { rspaceNavUrl } from "../shared/url-helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <folk-rapp> — Embeds a live rApp module as a shape on the canvas.
|
||||||
|
*
|
||||||
|
* Unlike folk-embed (generic URL iframe), folk-rapp understands the module
|
||||||
|
* system: it stores moduleId + spaceSlug, derives the iframe URL, shows
|
||||||
|
* the module's icon/badge in the header, and can switch modules in-place.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Module metadata for header display (subset of rstack-app-switcher badges)
|
||||||
|
const MODULE_META: Record<string, { badge: string; color: string; name: string; icon: string }> = {
|
||||||
|
rnotes: { badge: "rN", color: "#fcd34d", name: "rNotes", icon: "📝" },
|
||||||
|
rphotos: { badge: "rPh", color: "#f9a8d4", name: "rPhotos", icon: "📸" },
|
||||||
|
rbooks: { badge: "rB", color: "#fda4af", name: "rBooks", icon: "📚" },
|
||||||
|
rpubs: { badge: "rP", color: "#fda4af", name: "rPubs", icon: "📖" },
|
||||||
|
rfiles: { badge: "rFi", color: "#67e8f9", name: "rFiles", icon: "📁" },
|
||||||
|
rwork: { badge: "rWo", color: "#cbd5e1", name: "rWork", icon: "📋" },
|
||||||
|
rforum: { badge: "rFo", color: "#fcd34d", name: "rForum", icon: "💬" },
|
||||||
|
rinbox: { badge: "rI", color: "#a5b4fc", name: "rInbox", icon: "📧" },
|
||||||
|
rtube: { badge: "rTu", color: "#f9a8d4", name: "rTube", icon: "🎬" },
|
||||||
|
rfunds: { badge: "rF", color: "#bef264", name: "rFunds", icon: "🌊" },
|
||||||
|
rwallet: { badge: "rW", color: "#fde047", name: "rWallet", icon: "💰" },
|
||||||
|
rvote: { badge: "rV", color: "#c4b5fd", name: "rVote", icon: "🗳️" },
|
||||||
|
rcart: { badge: "rCt", color: "#fdba74", name: "rCart", icon: "🛒" },
|
||||||
|
rdata: { badge: "rD", color: "#d8b4fe", name: "rData", icon: "📊" },
|
||||||
|
rnetwork: { badge: "rNe", color: "#93c5fd", name: "rNetwork", icon: "🌍" },
|
||||||
|
rsplat: { badge: "r3", color: "#d8b4fe", name: "rSplat", icon: "🔮" },
|
||||||
|
rproviders: { badge: "rPr", color: "#fdba74", name: "rProviders", icon: "🏭" },
|
||||||
|
rswag: { badge: "rSw", color: "#fda4af", name: "rSwag", icon: "🎨" },
|
||||||
|
rchoices: { badge: "rCo", color: "#f0abfc", name: "rChoices", icon: "🤔" },
|
||||||
|
rcal: { badge: "rC", color: "#7dd3fc", name: "rCal", icon: "📅" },
|
||||||
|
rtrips: { badge: "rT", color: "#6ee7b7", name: "rTrips", icon: "✈️" },
|
||||||
|
rmaps: { badge: "rM", color: "#86efac", name: "rMaps", icon: "🗺️" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = css`
|
||||||
|
:host {
|
||||||
|
background: #1e293b;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--rapp-color, #334155);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
font-size: 0.55rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-actions button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #0f172a;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-actions button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-content {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 34px);
|
||||||
|
position: relative;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-loading .spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid rgba(100, 116, 139, 0.3);
|
||||||
|
border-top-color: #64748b;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Module picker (shown when no moduleId set) */
|
||||||
|
.rapp-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-picker-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-picker-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-picker-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rapp-picker-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #0f172a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"folk-rapp": FolkRApp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FolkRApp extends FolkShape {
|
||||||
|
static override tagName = "folk-rapp";
|
||||||
|
|
||||||
|
static {
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
const parentRules = Array.from(FolkShape.styles.cssRules)
|
||||||
|
.map((r) => r.cssText)
|
||||||
|
.join("\n");
|
||||||
|
const childRules = Array.from(styles.cssRules)
|
||||||
|
.map((r) => r.cssText)
|
||||||
|
.join("\n");
|
||||||
|
sheet.replaceSync(`${parentRules}\n${childRules}`);
|
||||||
|
this.styles = sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
#moduleId: string = "";
|
||||||
|
#spaceSlug: string = "";
|
||||||
|
#iframe: HTMLIFrameElement | null = null;
|
||||||
|
#contentEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
get moduleId() { return this.#moduleId; }
|
||||||
|
set moduleId(value: string) {
|
||||||
|
this.#moduleId = value;
|
||||||
|
this.requestUpdate("moduleId");
|
||||||
|
this.dispatchEvent(new CustomEvent("content-change"));
|
||||||
|
this.#loadModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
get spaceSlug() { return this.#spaceSlug; }
|
||||||
|
set spaceSlug(value: string) {
|
||||||
|
this.#spaceSlug = value;
|
||||||
|
this.requestUpdate("spaceSlug");
|
||||||
|
this.dispatchEvent(new CustomEvent("content-change"));
|
||||||
|
this.#loadModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
override createRenderRoot() {
|
||||||
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
|
this.#moduleId = this.getAttribute("module-id") || "";
|
||||||
|
this.#spaceSlug = this.getAttribute("space-slug") || "";
|
||||||
|
|
||||||
|
const meta = MODULE_META[this.#moduleId];
|
||||||
|
const headerColor = meta?.color || "#475569";
|
||||||
|
const headerName = meta?.name || this.#moduleId || "rApp";
|
||||||
|
const headerBadge = meta?.badge || "r?";
|
||||||
|
const headerIcon = meta?.icon || "📱";
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.innerHTML = html`
|
||||||
|
<div class="rapp-header" style="--rapp-color: ${headerColor}">
|
||||||
|
<div class="rapp-header-left">
|
||||||
|
<span class="rapp-badge">${headerBadge}</span>
|
||||||
|
<span class="rapp-name">${headerName}</span>
|
||||||
|
<span class="rapp-icon">${headerIcon}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rapp-actions">
|
||||||
|
<button class="open-tab-btn" title="Open in tab">↗</button>
|
||||||
|
<button class="close-btn" title="Close">\u00D7</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rapp-content"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const slot = root.querySelector("slot");
|
||||||
|
const containerDiv = slot?.parentElement as HTMLElement;
|
||||||
|
if (containerDiv) {
|
||||||
|
containerDiv.replaceWith(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#contentEl = wrapper.querySelector(".rapp-content") as HTMLElement;
|
||||||
|
const openTabBtn = wrapper.querySelector(".open-tab-btn") as HTMLButtonElement;
|
||||||
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||||
|
|
||||||
|
// Open in tab navigates to the module's page
|
||||||
|
openTabBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (this.#moduleId && this.#spaceSlug) {
|
||||||
|
window.open(rspaceNavUrl(this.#spaceSlug, this.#moduleId), "_blank");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
closeBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load content
|
||||||
|
if (this.#moduleId) {
|
||||||
|
this.#loadModule();
|
||||||
|
} else {
|
||||||
|
this.#showPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loadModule() {
|
||||||
|
if (!this.#contentEl || !this.#moduleId) return;
|
||||||
|
|
||||||
|
// Update header
|
||||||
|
const meta = MODULE_META[this.#moduleId];
|
||||||
|
const header = this.shadowRoot?.querySelector(".rapp-header") as HTMLElement;
|
||||||
|
if (header && meta) {
|
||||||
|
header.style.setProperty("--rapp-color", meta.color);
|
||||||
|
const badge = header.querySelector(".rapp-badge");
|
||||||
|
const name = header.querySelector(".rapp-name");
|
||||||
|
const icon = header.querySelector(".rapp-icon");
|
||||||
|
if (badge) badge.textContent = meta.badge;
|
||||||
|
if (name) name.textContent = meta.name;
|
||||||
|
if (icon) icon.textContent = meta.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
this.#contentEl.innerHTML = `
|
||||||
|
<div class="rapp-loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>Loading ${meta?.name || this.#moduleId}...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create iframe
|
||||||
|
const space = this.#spaceSlug || "demo";
|
||||||
|
const iframeUrl = `/${space}/${this.#moduleId}`;
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
iframe.className = "rapp-iframe";
|
||||||
|
iframe.src = iframeUrl;
|
||||||
|
iframe.loading = "lazy";
|
||||||
|
iframe.allow = "clipboard-write";
|
||||||
|
|
||||||
|
iframe.addEventListener("load", () => {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loading = this.#contentEl?.querySelector(".rapp-loading");
|
||||||
|
if (loading) loading.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
iframe.addEventListener("error", () => {
|
||||||
|
if (this.#contentEl) {
|
||||||
|
this.#contentEl.innerHTML = `
|
||||||
|
<div class="rapp-error">
|
||||||
|
<span>Failed to load ${meta?.name || this.#moduleId}</span>
|
||||||
|
<button class="rapp-picker-item" style="justify-content: center; color: #94a3b8;">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.#contentEl.querySelector("button")?.addEventListener("click", () => this.#loadModule());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#contentEl.appendChild(iframe);
|
||||||
|
this.#iframe = iframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#showPicker() {
|
||||||
|
if (!this.#contentEl) return;
|
||||||
|
|
||||||
|
const items = Object.entries(MODULE_META)
|
||||||
|
.map(([id, meta]) => `
|
||||||
|
<button class="rapp-picker-item" data-module="${id}">
|
||||||
|
<span class="rapp-picker-badge" style="background: ${meta.color}">${meta.badge}</span>
|
||||||
|
<span>${meta.name}</span>
|
||||||
|
<span>${meta.icon}</span>
|
||||||
|
</button>
|
||||||
|
`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
this.#contentEl.innerHTML = `
|
||||||
|
<div class="rapp-picker">
|
||||||
|
<span class="rapp-picker-title">Choose an rApp to embed</span>
|
||||||
|
${items}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.#contentEl.querySelectorAll(".rapp-picker-item").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const modId = (btn as HTMLElement).dataset.module;
|
||||||
|
if (modId) {
|
||||||
|
this.moduleId = modId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override toJSON() {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
type: "folk-rapp",
|
||||||
|
moduleId: this.#moduleId,
|
||||||
|
spaceSlug: this.#spaceSlug,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,9 @@ export * from "./folk-choice-spider";
|
||||||
// Nested Space Shape
|
// Nested Space Shape
|
||||||
export * from "./folk-canvas";
|
export * from "./folk-canvas";
|
||||||
|
|
||||||
|
// rApp Embed Shape (cross-app embedding)
|
||||||
|
export * from "./folk-rapp";
|
||||||
|
|
||||||
// Feed Shape (inter-layer data flow)
|
// Feed Shape (inter-layer data flow)
|
||||||
export * from "./folk-feed";
|
export * from "./folk-feed";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,8 @@
|
||||||
folk-choice-vote,
|
folk-choice-vote,
|
||||||
folk-choice-rank,
|
folk-choice-rank,
|
||||||
folk-choice-spider,
|
folk-choice-spider,
|
||||||
folk-social-post {
|
folk-social-post,
|
||||||
|
folk-rapp {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +414,7 @@
|
||||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
folk-booking, folk-token-mint, folk-token-ledger,
|
folk-booking, folk-token-mint, folk-token-ledger,
|
||||||
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
||||||
folk-social-post) {
|
folk-social-post, folk-rapp) {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +425,7 @@
|
||||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
folk-booking, folk-token-mint, folk-token-ledger,
|
folk-booking, folk-token-mint, folk-token-ledger,
|
||||||
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
||||||
folk-social-post):hover {
|
folk-social-post, folk-rapp):hover {
|
||||||
outline: 2px dashed #3b82f6;
|
outline: 2px dashed #3b82f6;
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -741,6 +742,7 @@
|
||||||
FolkChoiceSpider,
|
FolkChoiceSpider,
|
||||||
FolkSocialPost,
|
FolkSocialPost,
|
||||||
FolkCanvas,
|
FolkCanvas,
|
||||||
|
FolkRApp,
|
||||||
FolkFeed,
|
FolkFeed,
|
||||||
CommunitySync,
|
CommunitySync,
|
||||||
PresenceManager,
|
PresenceManager,
|
||||||
|
|
@ -769,7 +771,7 @@
|
||||||
if (tabBar) {
|
if (tabBar) {
|
||||||
const canvasDefaultLayer = {
|
const canvasDefaultLayer = {
|
||||||
id: "layer-canvas",
|
id: "layer-canvas",
|
||||||
moduleId: "canvas",
|
moduleId: "rspace",
|
||||||
label: "rSpace",
|
label: "rSpace",
|
||||||
order: 0,
|
order: 0,
|
||||||
color: "",
|
color: "",
|
||||||
|
|
@ -783,7 +785,7 @@
|
||||||
// Tab switching: navigate to the selected module's page
|
// Tab switching: navigate to the selected module's page
|
||||||
tabBar.addEventListener("layer-switch", (e) => {
|
tabBar.addEventListener("layer-switch", (e) => {
|
||||||
const { moduleId } = e.detail;
|
const { moduleId } = e.detail;
|
||||||
if (moduleId === "canvas") return; // already on canvas
|
if (moduleId === "rspace") return; // already on canvas
|
||||||
window.location.href = rspaceNavUrl(
|
window.location.href = rspaceNavUrl(
|
||||||
document.querySelector("rstack-space-switcher")?.getAttribute("current") || "demo",
|
document.querySelector("rstack-space-switcher")?.getAttribute("current") || "demo",
|
||||||
moduleId
|
moduleId
|
||||||
|
|
@ -853,6 +855,7 @@
|
||||||
FolkChoiceSpider.define();
|
FolkChoiceSpider.define();
|
||||||
FolkSocialPost.define();
|
FolkSocialPost.define();
|
||||||
FolkCanvas.define();
|
FolkCanvas.define();
|
||||||
|
FolkRApp.define();
|
||||||
FolkFeed.define();
|
FolkFeed.define();
|
||||||
|
|
||||||
// Get community info from URL
|
// Get community info from URL
|
||||||
|
|
@ -863,7 +866,7 @@
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
const pathSegments = window.location.pathname.split("/").filter(Boolean);
|
const pathSegments = window.location.pathname.split("/").filter(Boolean);
|
||||||
const ignorePaths = ["canvas", "settings", "api"];
|
const ignorePaths = ["rspace", "canvas", "settings", "api"];
|
||||||
const cleanSegments = pathSegments.filter(s => !ignorePaths.includes(s));
|
const cleanSegments = pathSegments.filter(s => !ignorePaths.includes(s));
|
||||||
|
|
||||||
let communitySlug = urlParams.get("space");
|
let communitySlug = urlParams.get("space");
|
||||||
|
|
@ -906,6 +909,7 @@
|
||||||
"folk-token-mint", "folk-token-ledger",
|
"folk-token-mint", "folk-token-ledger",
|
||||||
"folk-choice-vote", "folk-choice-rank", "folk-choice-spider",
|
"folk-choice-vote", "folk-choice-rank", "folk-choice-spider",
|
||||||
"folk-social-post",
|
"folk-social-post",
|
||||||
|
"folk-rapp",
|
||||||
"folk-feed"
|
"folk-feed"
|
||||||
].join(", ");
|
].join(", ");
|
||||||
|
|
||||||
|
|
@ -930,7 +934,7 @@
|
||||||
if (tabBar && sync) {
|
if (tabBar && sync) {
|
||||||
const canvasDefaultLayer = {
|
const canvasDefaultLayer = {
|
||||||
id: "layer-canvas",
|
id: "layer-canvas",
|
||||||
moduleId: "canvas",
|
moduleId: "rspace",
|
||||||
label: "rSpace",
|
label: "rSpace",
|
||||||
order: 0,
|
order: 0,
|
||||||
color: "",
|
color: "",
|
||||||
|
|
@ -1307,6 +1311,11 @@
|
||||||
if (data.collapsed != null) shape.collapsed = data.collapsed;
|
if (data.collapsed != null) shape.collapsed = data.collapsed;
|
||||||
if (data.label) shape.label = data.label;
|
if (data.label) shape.label = data.label;
|
||||||
break;
|
break;
|
||||||
|
case "folk-rapp":
|
||||||
|
shape = document.createElement("folk-rapp");
|
||||||
|
if (data.moduleId) shape.moduleId = data.moduleId;
|
||||||
|
if (data.spaceSlug) shape.spaceSlug = data.spaceSlug;
|
||||||
|
break;
|
||||||
case "folk-feed":
|
case "folk-feed":
|
||||||
shape = document.createElement("folk-feed");
|
shape = document.createElement("folk-feed");
|
||||||
if (data.sourceLayer) shape.sourceLayer = data.sourceLayer;
|
if (data.sourceLayer) shape.sourceLayer = data.sourceLayer;
|
||||||
|
|
@ -1387,6 +1396,7 @@
|
||||||
"folk-choice-spider": { width: 440, height: 540 },
|
"folk-choice-spider": { width: 440, height: 540 },
|
||||||
"folk-social-post": { width: 300, height: 380 },
|
"folk-social-post": { width: 300, height: 380 },
|
||||||
"folk-canvas": { width: 600, height: 400 },
|
"folk-canvas": { width: 600, height: 400 },
|
||||||
|
"folk-rapp": { width: 500, height: 400 },
|
||||||
"folk-feed": { width: 280, height: 360 },
|
"folk-feed": { width: 280, height: 360 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1638,34 +1648,33 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// rApp embed buttons — embed any module as an interactive iframe on the canvas
|
// rApp embed buttons — embed any module as a folk-rapp shape on the canvas
|
||||||
const rAppModules = [
|
const rAppModules = [
|
||||||
{ btnId: "embed-notes", moduleId: "notes", icon: "📝", name: "rNotes" },
|
{ btnId: "embed-notes", moduleId: "rnotes" },
|
||||||
{ btnId: "embed-photos", moduleId: "photos", icon: "📸", name: "rPhotos" },
|
{ btnId: "embed-photos", moduleId: "rphotos" },
|
||||||
{ btnId: "embed-books", moduleId: "books", icon: "📚", name: "rBooks" },
|
{ btnId: "embed-books", moduleId: "rbooks" },
|
||||||
{ btnId: "embed-pubs", moduleId: "pubs", icon: "📖", name: "rPubs" },
|
{ btnId: "embed-pubs", moduleId: "rpubs" },
|
||||||
{ btnId: "embed-files", moduleId: "files", icon: "📁", name: "rFiles" },
|
{ btnId: "embed-files", moduleId: "rfiles" },
|
||||||
{ btnId: "embed-work", moduleId: "work", icon: "📋", name: "rWork" },
|
{ btnId: "embed-work", moduleId: "rwork" },
|
||||||
{ btnId: "embed-forum", moduleId: "forum", icon: "💬", name: "rForum" },
|
{ btnId: "embed-forum", moduleId: "rforum" },
|
||||||
{ btnId: "embed-inbox", moduleId: "inbox", icon: "📧", name: "rInbox" },
|
{ btnId: "embed-inbox", moduleId: "rinbox" },
|
||||||
{ btnId: "embed-tube", moduleId: "tube", icon: "🎬", name: "rTube" },
|
{ btnId: "embed-tube", moduleId: "rtube" },
|
||||||
{ btnId: "embed-funds", moduleId: "funds", icon: "🌊", name: "rFunds" },
|
{ btnId: "embed-funds", moduleId: "rfunds" },
|
||||||
{ btnId: "embed-wallet", moduleId: "wallet", icon: "💰", name: "rWallet" },
|
{ btnId: "embed-wallet", moduleId: "rwallet" },
|
||||||
{ btnId: "embed-vote", moduleId: "vote", icon: "🗳️", name: "rVote" },
|
{ btnId: "embed-vote", moduleId: "rvote" },
|
||||||
{ btnId: "embed-cart", moduleId: "cart", icon: "🛒", name: "rCart" },
|
{ btnId: "embed-cart", moduleId: "rcart" },
|
||||||
{ btnId: "embed-data", moduleId: "data", icon: "📊", name: "rData" },
|
{ btnId: "embed-data", moduleId: "rdata" },
|
||||||
{ btnId: "embed-network", moduleId: "network", icon: "🌍", name: "rNetwork" },
|
{ btnId: "embed-network", moduleId: "rnetwork" },
|
||||||
{ btnId: "embed-splat", moduleId: "splat", icon: "🔮", name: "rSplat" },
|
{ btnId: "embed-splat", moduleId: "rsplat" },
|
||||||
{ btnId: "embed-providers", moduleId: "providers", icon: "🏭", name: "rProviders" },
|
{ btnId: "embed-providers", moduleId: "rproviders" },
|
||||||
{ btnId: "embed-swag", moduleId: "swag", icon: "🎨", name: "rSwag" },
|
{ btnId: "embed-swag", moduleId: "rswag" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const app of rAppModules) {
|
for (const app of rAppModules) {
|
||||||
const btn = document.getElementById(app.btnId);
|
const btn = document.getElementById(app.btnId);
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
const moduleUrl = rspaceNavUrl(communitySlug, app.moduleId);
|
newShape("folk-rapp", { moduleId: app.moduleId, spaceSlug: communitySlug });
|
||||||
newShape("folk-embed", { url: moduleUrl });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1699,7 +1708,7 @@
|
||||||
// Auto-register a LayerFlow in Automerge if layers exist
|
// Auto-register a LayerFlow in Automerge if layers exist
|
||||||
if (shape && sync.getLayers) {
|
if (shape && sync.getLayers) {
|
||||||
const layers = sync.getLayers();
|
const layers = sync.getLayers();
|
||||||
const currentLayer = layers.find(l => l.moduleId === "canvas") || layers[0];
|
const currentLayer = layers.find(l => l.moduleId === "rspace") || layers[0];
|
||||||
const sourceLayer = layers.find(l => l.moduleId === sourceModule);
|
const sourceLayer = layers.find(l => l.moduleId === sourceModule);
|
||||||
|
|
||||||
if (currentLayer && sourceLayer) {
|
if (currentLayer && sourceLayer) {
|
||||||
|
|
@ -1800,7 +1809,7 @@
|
||||||
"folk-token-mint": "🪙", "folk-token-ledger": "📒",
|
"folk-token-mint": "🪙", "folk-token-ledger": "📒",
|
||||||
"folk-choice-vote": "☑", "folk-choice-rank": "📊",
|
"folk-choice-vote": "☑", "folk-choice-rank": "📊",
|
||||||
"folk-choice-spider": "🕸", "folk-social-post": "📱",
|
"folk-choice-spider": "🕸", "folk-social-post": "📱",
|
||||||
"folk-feed": "🔄", "folk-arrow": "↗️",
|
"folk-rapp": "📱", "folk-feed": "🔄", "folk-arrow": "↗️",
|
||||||
};
|
};
|
||||||
|
|
||||||
function getShapeLabel(data) {
|
function getShapeLabel(data) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue