Merge branch 'dev'
This commit is contained in:
commit
4f49af905a
462
lib/folk-rapp.ts
462
lib/folk-rapp.ts
|
|
@ -42,14 +42,23 @@ const MODULE_META: Record<string, { badge: string; color: string; name: string;
|
|||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1e293b;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
min-width: 320px;
|
||||
min-height: 240px;
|
||||
min-width: 200px;
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rapp-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rapp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -62,6 +71,7 @@ const styles = css`
|
|||
cursor: move;
|
||||
user-select: none;
|
||||
border-radius: 10px 10px 0 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rapp-header-left {
|
||||
|
|
@ -117,10 +127,23 @@ const styles = css`
|
|||
}
|
||||
|
||||
.rapp-content {
|
||||
width: 100%;
|
||||
height: calc(100% - 34px);
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #0f172a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Transparent overlay intercepts wheel/pointer events so canvas zoom + selection work */
|
||||
.rapp-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
/* In editing mode, hide overlay so iframe becomes interactive */
|
||||
:host(:state(editing)) .rapp-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rapp-iframe {
|
||||
|
|
@ -289,6 +312,98 @@ const styles = css`
|
|||
.rapp-status.connected {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* Widget mode styles */
|
||||
.rapp-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 10px 12px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rapp-widget:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.rapp-widget-stat {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.rapp-widget-divider {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.rapp-widget-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rapp-widget-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rapp-widget-row .bullet {
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rapp-widget-row .label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rapp-widget-row .value {
|
||||
margin-left: auto;
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rapp-widget-empty {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.rapp-widget-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rapp-widget-thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
/* Mode toggle button active state */
|
||||
.rapp-actions button.active {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
`;
|
||||
|
||||
declare global {
|
||||
|
|
@ -297,6 +412,120 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
// API endpoint config per module for widget mode
|
||||
const WIDGET_API: Record<string, { path: string; transform: (data: any) => WidgetData }> = {
|
||||
rinbox: {
|
||||
path: "/api/mailboxes",
|
||||
transform: (data) => {
|
||||
const mailboxes = data?.mailboxes || [];
|
||||
const totalThreads = mailboxes.reduce((sum: number, m: any) => sum + (m.threadCount || 0), 0);
|
||||
return {
|
||||
stat: `${totalThreads} thread${totalThreads !== 1 ? "s" : ""}`,
|
||||
rows: mailboxes.slice(0, 3).map((m: any) => ({
|
||||
label: m.name || m.slug,
|
||||
value: `${m.threadCount || 0}`,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rphotos: {
|
||||
path: "/api/assets",
|
||||
transform: (data) => {
|
||||
const assets = data?.assets || [];
|
||||
return {
|
||||
stat: `${assets.length} photo${assets.length !== 1 ? "s" : ""}`,
|
||||
thumbs: assets.slice(0, 4).map((a: any) => a.id),
|
||||
};
|
||||
},
|
||||
},
|
||||
rcal: {
|
||||
path: "/api/events?upcoming=true&limit=3",
|
||||
transform: (data) => {
|
||||
const events = data?.results || [];
|
||||
return {
|
||||
stat: `${data?.count ?? events.length} event${(data?.count ?? events.length) !== 1 ? "s" : ""}`,
|
||||
rows: events.slice(0, 3).map((ev: any) => ({
|
||||
label: ev.title || ev.summary || "Untitled",
|
||||
value: ev.start ? new Date(ev.start).toLocaleDateString(undefined, { month: "short", day: "numeric" }) : "",
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rwork: {
|
||||
path: "/api/spaces",
|
||||
transform: (data) => {
|
||||
const spaces = Array.isArray(data) ? data : [];
|
||||
const totalTasks = spaces.reduce((sum: number, s: any) => sum + (s.taskCount || 0), 0);
|
||||
return {
|
||||
stat: `${totalTasks} task${totalTasks !== 1 ? "s" : ""}`,
|
||||
rows: spaces.slice(0, 3).map((s: any) => ({
|
||||
label: s.name || s.slug,
|
||||
value: `${s.taskCount || 0}`,
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rnotes: {
|
||||
path: "/api/notebooks",
|
||||
transform: (data) => {
|
||||
const notebooks = data?.notebooks || [];
|
||||
return {
|
||||
stat: `${notebooks.length} notebook${notebooks.length !== 1 ? "s" : ""}`,
|
||||
rows: notebooks.slice(0, 3).map((n: any) => ({
|
||||
label: n.title || n.name || "Untitled",
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rfunds: {
|
||||
path: "/api/flows",
|
||||
transform: (data) => {
|
||||
const flows = Array.isArray(data) ? data : [];
|
||||
const active = flows.filter((f: any) => f.status === "active").length;
|
||||
return {
|
||||
stat: `${active} active flow${active !== 1 ? "s" : ""}`,
|
||||
rows: flows.slice(0, 3).map((f: any) => ({
|
||||
label: f.name || f.id,
|
||||
value: f.status || "",
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rschedule: {
|
||||
path: "/api/jobs",
|
||||
transform: (data) => {
|
||||
const jobs = data?.results || [];
|
||||
return {
|
||||
stat: `${data?.count ?? jobs.length} job${(data?.count ?? jobs.length) !== 1 ? "s" : ""}`,
|
||||
rows: jobs.slice(0, 3).map((j: any) => ({
|
||||
label: j.name || j.id,
|
||||
value: j.enabled === false ? "paused" : "active",
|
||||
})),
|
||||
};
|
||||
},
|
||||
},
|
||||
rnetwork: {
|
||||
path: "/api/graph",
|
||||
transform: (data) => {
|
||||
const nodes = data?.nodes?.length ?? 0;
|
||||
const edges = data?.edges?.length ?? 0;
|
||||
return {
|
||||
stat: `${nodes} node${nodes !== 1 ? "s" : ""}`,
|
||||
rows: [
|
||||
{ label: "Nodes", value: `${nodes}` },
|
||||
{ label: "Edges", value: `${edges}` },
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface WidgetData {
|
||||
stat: string;
|
||||
rows?: { label: string; value?: string }[];
|
||||
thumbs?: string[]; // rphotos asset IDs for thumbnail grid
|
||||
}
|
||||
|
||||
export class FolkRApp extends FolkShape {
|
||||
static override tagName = "folk-rapp";
|
||||
|
||||
|
|
@ -314,10 +543,13 @@ export class FolkRApp extends FolkShape {
|
|||
|
||||
#moduleId: string = "";
|
||||
#spaceSlug: string = "";
|
||||
#mode: "widget" | "iframe" = "widget";
|
||||
#iframe: HTMLIFrameElement | null = null;
|
||||
#contentEl: HTMLElement | null = null;
|
||||
#messageHandler: ((e: MessageEvent) => void) | null = null;
|
||||
#statusEl: HTMLElement | null = null;
|
||||
#refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
#modeToggleBtn: HTMLButtonElement | null = null;
|
||||
|
||||
get moduleId() { return this.#moduleId; }
|
||||
set moduleId(value: string) {
|
||||
|
|
@ -325,7 +557,7 @@ export class FolkRApp extends FolkShape {
|
|||
this.#moduleId = value;
|
||||
this.requestUpdate("moduleId");
|
||||
this.dispatchEvent(new CustomEvent("content-change"));
|
||||
this.#loadModule();
|
||||
this.#renderContent();
|
||||
}
|
||||
|
||||
get spaceSlug() { return this.#spaceSlug; }
|
||||
|
|
@ -334,7 +566,16 @@ export class FolkRApp extends FolkShape {
|
|||
this.#spaceSlug = value;
|
||||
this.requestUpdate("spaceSlug");
|
||||
this.dispatchEvent(new CustomEvent("content-change"));
|
||||
this.#loadModule();
|
||||
this.#renderContent();
|
||||
}
|
||||
|
||||
get mode() { return this.#mode; }
|
||||
set mode(value: "widget" | "iframe") {
|
||||
if (this.#mode === value) return;
|
||||
this.#mode = value;
|
||||
this.dispatchEvent(new CustomEvent("content-change"));
|
||||
this.#updateModeToggle();
|
||||
this.#renderContent();
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
|
|
@ -343,6 +584,8 @@ export class FolkRApp extends FolkShape {
|
|||
// Prefer JS-set properties (from newShape props); fall back to HTML attributes
|
||||
if (!this.#moduleId) this.#moduleId = this.getAttribute("module-id") || "";
|
||||
if (!this.#spaceSlug) this.#spaceSlug = this.getAttribute("space-slug") || "";
|
||||
const attrMode = this.getAttribute("mode");
|
||||
if (attrMode === "iframe" || attrMode === "widget") this.#mode = attrMode;
|
||||
|
||||
// Auto-derive spaceSlug from current URL if not explicitly provided (/{space}/canvas → space)
|
||||
if (!this.#spaceSlug) {
|
||||
|
|
@ -357,6 +600,7 @@ export class FolkRApp extends FolkShape {
|
|||
const headerIcon = meta?.icon || "📱";
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "rapp-wrapper";
|
||||
wrapper.innerHTML = html`
|
||||
<div class="header rapp-header" data-drag style="--rapp-color: ${headerColor}">
|
||||
<div class="rapp-header-left">
|
||||
|
|
@ -367,6 +611,7 @@ export class FolkRApp extends FolkShape {
|
|||
<div class="rapp-switcher"></div>
|
||||
</div>
|
||||
<div class="rapp-actions">
|
||||
<button class="mode-toggle-btn" title="Toggle widget/iframe mode">${this.#mode === "widget" ? "□" : "▪"}</button>
|
||||
<button class="switch-btn" title="Switch module">⇄</button>
|
||||
<button class="open-tab-btn" title="Open in tab">↗</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
|
|
@ -383,11 +628,18 @@ export class FolkRApp extends FolkShape {
|
|||
|
||||
this.#contentEl = wrapper.querySelector(".rapp-content") as HTMLElement;
|
||||
this.#statusEl = wrapper.querySelector(".rapp-status") as HTMLElement;
|
||||
this.#modeToggleBtn = wrapper.querySelector(".mode-toggle-btn") as HTMLButtonElement;
|
||||
const switchBtn = wrapper.querySelector(".switch-btn") as HTMLButtonElement;
|
||||
const openTabBtn = wrapper.querySelector(".open-tab-btn") as HTMLButtonElement;
|
||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||
const switcherEl = wrapper.querySelector(".rapp-switcher") as HTMLElement;
|
||||
|
||||
// Mode toggle: widget ↔ iframe
|
||||
this.#modeToggleBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.mode = this.#mode === "widget" ? "iframe" : "widget";
|
||||
});
|
||||
|
||||
// Module switcher dropdown
|
||||
this.#buildSwitcher(switcherEl);
|
||||
switchBtn.addEventListener("click", (e) => {
|
||||
|
|
@ -419,7 +671,7 @@ export class FolkRApp extends FolkShape {
|
|||
|
||||
// Load content
|
||||
if (this.#moduleId) {
|
||||
this.#loadModule();
|
||||
this.#renderContent();
|
||||
} else {
|
||||
this.#showPicker();
|
||||
}
|
||||
|
|
@ -433,6 +685,10 @@ export class FolkRApp extends FolkShape {
|
|||
window.removeEventListener("message", this.#messageHandler);
|
||||
this.#messageHandler = null;
|
||||
}
|
||||
if (this.#refreshTimer) {
|
||||
clearInterval(this.#refreshTimer);
|
||||
this.#refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
#buildSwitcher(switcherEl: HTMLElement) {
|
||||
|
|
@ -507,10 +763,46 @@ export class FolkRApp extends FolkShape {
|
|||
}
|
||||
}
|
||||
|
||||
#loadModule() {
|
||||
#updateModeToggle() {
|
||||
if (this.#modeToggleBtn) {
|
||||
this.#modeToggleBtn.textContent = this.#mode === "widget" ? "□" : "▪";
|
||||
this.#modeToggleBtn.title = this.#mode === "widget" ? "Expand to iframe" : "Collapse to widget";
|
||||
}
|
||||
}
|
||||
|
||||
/** Derive the base module URL path, accounting for subdomain routing */
|
||||
#getModulePath(): string {
|
||||
if (!this.#spaceSlug) {
|
||||
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
||||
if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
|
||||
}
|
||||
const space = this.#spaceSlug || "demo";
|
||||
const hostname = window.location.hostname;
|
||||
const onSubdomain = hostname.split(".").length >= 3 && hostname.startsWith(space + ".");
|
||||
return onSubdomain ? `/${this.#moduleId}` : `/${space}/${this.#moduleId}`;
|
||||
}
|
||||
|
||||
/** Route to the right render method based on current mode */
|
||||
#renderContent() {
|
||||
if (!this.#contentEl || !this.#moduleId) return;
|
||||
|
||||
// Clear refresh timer
|
||||
if (this.#refreshTimer) {
|
||||
clearInterval(this.#refreshTimer);
|
||||
this.#refreshTimer = null;
|
||||
}
|
||||
|
||||
// Update header
|
||||
this.#updateHeader();
|
||||
|
||||
if (this.#mode === "widget") {
|
||||
this.#loadWidget();
|
||||
} else {
|
||||
this.#loadIframe();
|
||||
}
|
||||
}
|
||||
|
||||
#updateHeader() {
|
||||
const meta = MODULE_META[this.#moduleId];
|
||||
const header = this.shadowRoot?.querySelector(".rapp-header") as HTMLElement;
|
||||
if (header && meta) {
|
||||
|
|
@ -522,6 +814,11 @@ export class FolkRApp extends FolkShape {
|
|||
if (name) name.textContent = meta.name;
|
||||
if (icon) icon.textContent = meta.icon;
|
||||
}
|
||||
}
|
||||
|
||||
/** Iframe mode: load the full rApp in an iframe with overlay for event capture */
|
||||
#loadIframe() {
|
||||
if (!this.#contentEl) return;
|
||||
|
||||
// Reset connection status
|
||||
if (this.#statusEl) {
|
||||
|
|
@ -529,26 +826,18 @@ export class FolkRApp extends FolkShape {
|
|||
this.#statusEl.title = "Loading...";
|
||||
}
|
||||
|
||||
const meta = MODULE_META[this.#moduleId];
|
||||
|
||||
// Show loading state
|
||||
this.#contentEl.innerHTML = `
|
||||
<div class="rapp-overlay"></div>
|
||||
<div class="rapp-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading ${meta?.name || this.#moduleId}...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Auto-derive space from URL if still missing
|
||||
if (!this.#spaceSlug) {
|
||||
const pathParts = window.location.pathname.split("/").filter(Boolean);
|
||||
if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
|
||||
}
|
||||
const space = this.#spaceSlug || "demo";
|
||||
// On subdomain URLs (jeff.rspace.online), the server prepends /{space}/ automatically,
|
||||
// so the iframe should load /{moduleId} directly. Using /{space}/{moduleId} would
|
||||
// double-prefix to /{space}/{space}/{moduleId} → 404.
|
||||
const hostname = window.location.hostname;
|
||||
const onSubdomain = hostname.split(".").length >= 3 && hostname.startsWith(space + ".");
|
||||
const iframeUrl = onSubdomain ? `/${this.#moduleId}` : `/${space}/${this.#moduleId}`;
|
||||
const iframeUrl = this.#getModulePath();
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.className = "rapp-iframe";
|
||||
|
|
@ -557,17 +846,15 @@ export class FolkRApp extends FolkShape {
|
|||
iframe.allow = "clipboard-write";
|
||||
|
||||
iframe.addEventListener("load", () => {
|
||||
// Remove loading indicator
|
||||
const loading = this.#contentEl?.querySelector(".rapp-loading");
|
||||
if (loading) loading.remove();
|
||||
|
||||
// Send context to the newly loaded iframe
|
||||
this.#sendContext();
|
||||
});
|
||||
|
||||
iframe.addEventListener("error", () => {
|
||||
if (this.#contentEl) {
|
||||
this.#contentEl.innerHTML = `
|
||||
<div class="rapp-overlay"></div>
|
||||
<div class="rapp-error">
|
||||
<span>Failed to load ${meta?.name || this.#moduleId}</span>
|
||||
<button class="rapp-picker-item" style="justify-content: center; color: #94a3b8;">
|
||||
|
|
@ -575,7 +862,7 @@ export class FolkRApp extends FolkShape {
|
|||
</button>
|
||||
</div>
|
||||
`;
|
||||
this.#contentEl.querySelector("button")?.addEventListener("click", () => this.#loadModule());
|
||||
this.#contentEl.querySelector("button")?.addEventListener("click", () => this.#loadIframe());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -583,6 +870,130 @@ export class FolkRApp extends FolkShape {
|
|||
this.#iframe = iframe;
|
||||
}
|
||||
|
||||
/** Widget mode: fetch data from module API and render a compact card */
|
||||
#loadWidget() {
|
||||
if (!this.#contentEl) return;
|
||||
|
||||
this.#iframe = null;
|
||||
|
||||
// Reset status — widget mode doesn't use postMessage connection
|
||||
if (this.#statusEl) {
|
||||
this.#statusEl.classList.remove("connected");
|
||||
this.#statusEl.title = "Widget mode";
|
||||
}
|
||||
|
||||
const meta = MODULE_META[this.#moduleId];
|
||||
|
||||
// Show loading
|
||||
this.#contentEl.innerHTML = `
|
||||
<div class="rapp-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading ${meta?.name || this.#moduleId}...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch and render
|
||||
this.#fetchAndRenderWidget();
|
||||
|
||||
// Auto-refresh every 60 seconds
|
||||
this.#refreshTimer = setInterval(() => this.#fetchAndRenderWidget(), 60_000);
|
||||
}
|
||||
|
||||
async #fetchAndRenderWidget() {
|
||||
if (!this.#contentEl || this.#mode !== "widget") return;
|
||||
|
||||
const apiConfig = WIDGET_API[this.#moduleId];
|
||||
if (!apiConfig) {
|
||||
this.#renderWidgetFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const basePath = this.#getModulePath();
|
||||
const url = `${basePath}${apiConfig.path}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
const data = await res.json();
|
||||
const widgetData = apiConfig.transform(data);
|
||||
this.#renderWidgetCard(widgetData);
|
||||
} catch {
|
||||
this.#renderWidgetFallback();
|
||||
}
|
||||
}
|
||||
|
||||
#renderWidgetCard(data: WidgetData) {
|
||||
if (!this.#contentEl) return;
|
||||
|
||||
const meta = MODULE_META[this.#moduleId];
|
||||
const basePath = this.#getModulePath();
|
||||
|
||||
let listHtml = "";
|
||||
|
||||
if (data.thumbs && data.thumbs.length > 0) {
|
||||
// Photo grid mode
|
||||
const thumbItems = data.thumbs
|
||||
.map((id) => `<img class="rapp-widget-thumb" src="${basePath}/api/assets/${id}/thumbnail" alt="" loading="lazy" />`)
|
||||
.join("");
|
||||
listHtml = `<div class="rapp-widget-grid">${thumbItems}</div>`;
|
||||
} else if (data.rows && data.rows.length > 0) {
|
||||
const rowItems = data.rows
|
||||
.map((r) => `
|
||||
<div class="rapp-widget-row">
|
||||
<span class="bullet">•</span>
|
||||
<span class="label">${this.#escapeHtml(r.label)}</span>
|
||||
${r.value ? `<span class="value">${this.#escapeHtml(r.value)}</span>` : ""}
|
||||
</div>
|
||||
`)
|
||||
.join("");
|
||||
listHtml = `
|
||||
<hr class="rapp-widget-divider" />
|
||||
<div class="rapp-widget-list">${rowItems}</div>
|
||||
`;
|
||||
} else {
|
||||
listHtml = `<span class="rapp-widget-empty">No data yet</span>`;
|
||||
}
|
||||
|
||||
this.#contentEl.innerHTML = `
|
||||
<div class="rapp-widget" title="Click to open ${meta?.name || this.#moduleId}">
|
||||
<div class="rapp-widget-stat">${this.#escapeHtml(data.stat)}</div>
|
||||
${listHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Click widget → navigate to full module
|
||||
this.#contentEl.querySelector(".rapp-widget")?.addEventListener("click", () => {
|
||||
if (this.#moduleId && this.#spaceSlug) {
|
||||
window.location.href = rspaceNavUrl(this.#spaceSlug, this.#moduleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Fallback widget when no API config exists or fetch fails */
|
||||
#renderWidgetFallback() {
|
||||
if (!this.#contentEl) return;
|
||||
|
||||
const meta = MODULE_META[this.#moduleId];
|
||||
this.#contentEl.innerHTML = `
|
||||
<div class="rapp-widget" title="Click to open ${meta?.name || this.#moduleId}">
|
||||
<div class="rapp-widget-stat">${meta?.icon || "📱"} ${meta?.name || this.#moduleId}</div>
|
||||
<span class="rapp-widget-empty">Could not load data</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.#contentEl.querySelector(".rapp-widget")?.addEventListener("click", () => {
|
||||
if (this.#moduleId && this.#spaceSlug) {
|
||||
window.location.href = rspaceNavUrl(this.#spaceSlug, this.#moduleId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#escapeHtml(str: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
#showPicker() {
|
||||
if (!this.#contentEl) return;
|
||||
|
||||
|
|
@ -619,6 +1030,7 @@ export class FolkRApp extends FolkShape {
|
|||
type: "folk-rapp",
|
||||
moduleId: this.#moduleId,
|
||||
spaceSlug: this.#spaceSlug,
|
||||
mode: this.#mode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3084,6 +3084,7 @@
|
|||
shape = document.createElement("folk-rapp");
|
||||
if (data.moduleId) shape.moduleId = data.moduleId;
|
||||
shape.spaceSlug = data.spaceSlug || communitySlug;
|
||||
if (data.mode) shape.mode = data.mode;
|
||||
break;
|
||||
case "folk-feed":
|
||||
shape = document.createElement("folk-feed");
|
||||
|
|
|
|||
Loading…
Reference in New Issue