@@ -1026,6 +1038,10 @@ class FolkFlowsApp extends HTMLElement {
if (!this.canvasInitialized) {
this.canvasInitialized = true;
requestAnimationFrame(() => this.fitView());
+ // Auto-start tour on first visit
+ if (!localStorage.getItem("rflows_tour_done")) {
+ setTimeout(() => this.startTour(), 1200);
+ }
}
this.loadFromHash();
}
@@ -1424,6 +1440,7 @@ class FolkFlowsApp extends HTMLElement {
else if (action === "analytics") this.toggleAnalytics();
else if (action === "quick-fund") this.quickFund();
else if (action === "share") this.shareState();
+ else if (action === "tour") this.startTour();
else if (action === "zoom-in") { this.canvasZoom = Math.min(4, this.canvasZoom * 1.2); this.updateCanvasTransform(); }
else if (action === "zoom-out") { this.canvasZoom = Math.max(0.1, this.canvasZoom * 0.8); this.updateCanvasTransform(); }
else if (action === "flow-picker") this.toggleFlowDropdown();
@@ -4953,6 +4970,110 @@ class FolkFlowsApp extends HTMLElement {
this.render();
}
+ // ── Guided Tour ──
+
+ startTour() {
+ this.tourActive = true;
+ this.tourStep = 0;
+ this.renderTourOverlay();
+ }
+
+ private advanceTour() {
+ this.tourStep++;
+ if (this.tourStep >= FolkFlowsApp.TOUR_STEPS.length) {
+ this.endTour();
+ } else {
+ this.renderTourOverlay();
+ }
+ }
+
+ private endTour() {
+ this.tourActive = false;
+ this.tourStep = 0;
+ localStorage.setItem("rflows_tour_done", "1");
+ const overlay = this.shadow.getElementById("flows-tour-overlay");
+ if (overlay) overlay.remove();
+ }
+
+ private renderTourOverlay() {
+ // Remove existing overlay
+ let overlay = this.shadow.getElementById("flows-tour-overlay");
+ if (!overlay) {
+ overlay = document.createElement("div");
+ overlay.id = "flows-tour-overlay";
+ overlay.className = "flows-tour-overlay";
+ const container = this.shadow.getElementById("canvas-container");
+ if (container) container.appendChild(overlay);
+ else this.shadow.appendChild(overlay);
+ }
+
+ const step = FolkFlowsApp.TOUR_STEPS[this.tourStep];
+ const targetEl = this.shadow.querySelector(step.target) as HTMLElement | null;
+
+ // Compute spotlight position
+ let spotX = 0, spotY = 0, spotW = 120, spotH = 40;
+ if (targetEl) {
+ const containerEl = this.shadow.getElementById("canvas-container") || this.shadow.host as HTMLElement;
+ const containerRect = containerEl.getBoundingClientRect();
+ const rect = targetEl.getBoundingClientRect();
+ spotX = rect.left - containerRect.left - 6;
+ spotY = rect.top - containerRect.top - 6;
+ spotW = rect.width + 12;
+ spotH = rect.height + 12;
+ }
+
+ const isLast = this.tourStep >= FolkFlowsApp.TOUR_STEPS.length - 1;
+ const stepNum = this.tourStep + 1;
+ const totalSteps = FolkFlowsApp.TOUR_STEPS.length;
+
+ // Position tooltip below target
+ const tooltipTop = spotY + spotH + 12;
+ const tooltipLeft = Math.max(8, spotX);
+
+ overlay.innerHTML = `
+
+
+
+ `;
+
+ // Wire tour navigation
+ overlay.querySelectorAll("[data-tour]").forEach(btn => {
+ btn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ const action = (btn as HTMLElement).dataset.tour;
+ if (action === "next") this.advanceTour();
+ else if (action === "prev") { this.tourStep = Math.max(0, this.tourStep - 1); this.renderTourOverlay(); }
+ else if (action === "skip") this.endTour();
+ });
+ });
+
+ // For advanceOnClick steps, listen for the target button click
+ if (step.advanceOnClick && targetEl) {
+ const handler = () => {
+ targetEl.removeEventListener("click", handler);
+ // Delay slightly so the action completes first
+ setTimeout(() => this.advanceTour(), 300);
+ };
+ targetEl.addEventListener("click", handler);
+ }
+ }
+
private esc(s: string): string {
return s
.replace(/&/g, "&")
diff --git a/modules/rflows/landing.ts b/modules/rflows/landing.ts
index d5257b5..96563c1 100644
--- a/modules/rflows/landing.ts
+++ b/modules/rflows/landing.ts
@@ -176,6 +176,11 @@ export function renderLanding(): string {
Build your flow in the demo, then sign in to save it to your own space.
+
+
+ Start Guided Tour →
+
+
diff --git a/server/index.ts b/server/index.ts
index 8f3d026..9e47487 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -693,6 +693,15 @@ app.get("/api/modules", (c) => {
return c.json({ modules: getModuleInfoList() });
});
+// ── Module landing HTML API (for info popover) ──
+app.get("/api/modules/:moduleId/landing", (c) => {
+ const moduleId = c.req.param("moduleId");
+ const mod = getModule(moduleId);
+ if (!mod) return c.json({ error: "Module not found" }, 404);
+ const html = mod.landingPage ? mod.landingPage() : `${mod.description || "No description available."}
`;
+ return c.json({ html });
+});
+
// ── x402 test endpoint (no auth, payment-gated only) ──
import { setupX402FromEnv } from "../shared/x402/hono-middleware";
const x402Test = setupX402FromEnv({ description: "x402 test endpoint", resource: "/api/x402-test" });
diff --git a/server/shell.ts b/server/shell.ts
index 2e369b1..79c6a10 100644
--- a/server/shell.ts
+++ b/server/shell.ts
@@ -128,6 +128,7 @@ export function renderShell(opts: ShellOptions): string {
${head}
+