From 2264267ded4951eb0e480ac240367eaf6fb0a5cc Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Mar 2026 17:15:16 +0000 Subject: [PATCH] feat(rflows): floating play button, auto-start demo, minimizable panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add prominent floating Play/Pause FAB button (bottom center) with glow effect and pulse animation while running - Auto-start simulation for demo and sim-demo flows on load - Analytics panel now has a minimize button (◀/▶) to collapse to a narrow strip, preserving screen space - Keep existing toolbar Play button for discoverability Co-Authored-By: Claude Opus 4.6 --- modules/rflows/components/flows.css | 99 +++++++++++++++++++++ modules/rflows/components/folk-flows-app.ts | 30 ++++++- 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/modules/rflows/components/flows.css b/modules/rflows/components/flows.css index aa8e9a0..5dfe96f 100644 --- a/modules/rflows/components/flows.css +++ b/modules/rflows/components/flows.css @@ -1282,3 +1282,102 @@ color: var(--rs-text-muted, #64748b); font-style: italic; } + +/* ── Floating Play/Pause FAB ── */ +.flows-fab-play { + position: absolute; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + z-index: 30; + width: 56px; + height: 56px; + border-radius: 50%; + border: 2px solid rgba(6, 182, 212, 0.5); + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(139, 92, 246, 0.2)); + backdrop-filter: blur(12px); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(6, 182, 212, 0.3); + transition: all 0.2s ease; +} +.flows-fab-play:hover { + transform: translateX(-50%) scale(1.1); + box-shadow: 0 6px 28px rgba(6, 182, 212, 0.5); + border-color: rgba(6, 182, 212, 0.8); +} +.flows-fab-play.playing { + border-color: rgba(139, 92, 246, 0.6); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(236, 72, 153, 0.15)); + box-shadow: 0 4px 20px rgba(139, 92, 246, 0.3); + animation: fabPulse 2s ease-in-out infinite; +} +.flows-fab-play__icon { + font-size: 22px; + line-height: 1; +} +@keyframes fabPulse { + 0%, 100% { box-shadow: 0 4px 20px rgba(139, 92, 246, 0.3); } + 50% { box-shadow: 0 4px 28px rgba(139, 92, 246, 0.55); } +} + +/* ── Minimizable panels ── */ +.flows-analytics-panel .analytics-minimize { + background: none; + border: none; + color: var(--rs-text-secondary); + font-size: 16px; + cursor: pointer; + padding: 2px 6px; + margin-left: 4px; +} +.flows-analytics-panel .analytics-minimize:hover { color: var(--rs-text-primary); } +.flows-analytics-panel.minimized { + width: 44px; + min-width: 44px; + overflow: hidden; +} +.flows-analytics-panel.minimized .analytics-content, +.flows-analytics-panel.minimized .analytics-tabs, +.flows-analytics-panel.minimized .analytics-title, +.flows-analytics-panel.minimized .analytics-close { + display: none; +} +.flows-analytics-panel.minimized .analytics-header { + flex-direction: column; + padding: 8px 4px; + border-bottom: none; +} +.flows-analytics-panel.minimized .analytics-minimize { + writing-mode: vertical-rl; + text-orientation: mixed; + padding: 8px 2px; + font-size: 12px; +} + +/* Panel collapse tab — visible when panel is closed */ +.flows-panel-tab { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 19; + width: 24px; + height: 64px; + border-radius: 0 8px 8px 0; + background: var(--rs-bg-surface); + border: 1px solid var(--rs-border-strong); + border-left: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--rs-text-secondary); + font-size: 14px; + transition: background 0.15s; +} +.flows-panel-tab:hover { background: var(--rs-bg-surface-raised); color: var(--rs-text-primary); } +.flows-panel-tab--left { left: 0; } +.flows-panel-tab--left.panel-open { left: 380px; transition: left 0.25s ease; } +.flows-panel-tab--right { right: 0; border-radius: 8px 0 0 8px; border: 1px solid var(--rs-border-strong); border-right: none; } diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts index 9da40d8..6cfa126 100644 --- a/modules/rflows/components/folk-flows-app.ts +++ b/modules/rflows/components/folk-flows-app.ts @@ -1043,6 +1043,9 @@ class FolkFlowsApp extends HTMLElement { +
${this.simSpeedMs}ms @@ -1108,8 +1111,13 @@ 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")) { + // Auto-start simulation for demo flows + const isDemo = this.currentFlowId === 'demo' || this.currentFlowId === 'sim-demo' || this.isDemo; + if (isDemo && !this.isSimulating) { + setTimeout(() => this.toggleSimulation(), 600); + } + // Auto-start tour on first visit (skip for demos that auto-play) + else if (!localStorage.getItem("rflows_tour_done")) { setTimeout(() => this.startTour(), 1200); } } @@ -4654,7 +4662,13 @@ class FolkFlowsApp extends HTMLElement { private toggleSimulation() { this.isSimulating = !this.isSimulating; const btn = this.shadow.getElementById("sim-btn"); - if (btn) btn.textContent = this.isSimulating ? "Pause" : "Play"; + if (btn) btn.textContent = this.isSimulating ? "⏸ Pause" : "▶ Play"; + + // Update floating play button + const fabIcon = this.shadow.getElementById("fab-play-icon"); + if (fabIcon) fabIcon.textContent = this.isSimulating ? "⏸" : "▶"; + const fab = this.shadow.getElementById("fab-play"); + if (fab) fab.classList.toggle("playing", this.isSimulating); // Show/hide speed slider and timeline const speedContainer = this.shadow.getElementById("sim-speed-container"); @@ -4810,6 +4824,7 @@ class FolkFlowsApp extends HTMLElement {
+
@@ -4835,6 +4850,15 @@ class FolkFlowsApp extends HTMLElement { const closeBtn = this.shadow.querySelector("[data-analytics-close]"); closeBtn?.addEventListener("click", () => this.toggleAnalytics()); + const minimizeBtn = this.shadow.querySelector("[data-analytics-minimize]"); + minimizeBtn?.addEventListener("click", () => { + const panel = this.shadow.getElementById("analytics-panel"); + if (panel) { + const isMin = panel.classList.toggle("minimized"); + if (minimizeBtn) (minimizeBtn as HTMLElement).textContent = isMin ? "▶" : "◀"; + } + }); + this.shadow.querySelectorAll("[data-analytics-tab]").forEach((el) => { el.addEventListener("click", () => { const tab = (el as HTMLElement).dataset.analyticsTab as "overview" | "transactions";