feat(rflows): floating play button, auto-start demo, minimizable panels

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-15 17:15:16 +00:00
parent 99131df914
commit 2264267ded
2 changed files with 126 additions and 3 deletions

View File

@ -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; }

View File

@ -1043,6 +1043,9 @@ class FolkFlowsApp extends HTMLElement {
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 6V3a1 1 0 011-1h3M10 2h3a1 1 0 011 1v3M14 10v3a1 1 0 01-1 1h-3M6 14H3a1 1 0 01-1-1v-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<button class="flows-fab-play" id="fab-play" data-canvas-action="sim" title="Play / Pause simulation">
<span class="flows-fab-play__icon" id="fab-play-icon">${this.isSimulating ? "⏸" : "▶"}</span>
</button>
<div class="flows-sim-speed" id="sim-speed-container" style="display:${this.isSimulating ? "flex" : "none"}">
<input type="range" class="flows-speed-slider" id="sim-speed-slider" min="20" max="1000" value="${this.simSpeedMs}" step="10"/>
<span class="flows-speed-label" id="sim-speed-label">${this.simSpeedMs}ms</span>
@ -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 {
<button class="analytics-tab ${this.analyticsTab === "overview" ? "analytics-tab--active" : ""}" data-analytics-tab="overview">Overview</button>
<button class="analytics-tab ${this.analyticsTab === "transactions" ? "analytics-tab--active" : ""}" data-analytics-tab="transactions">Transactions</button>
</div>
<button class="analytics-minimize" data-analytics-minimize title="Minimize panel"></button>
<button class="analytics-close" data-analytics-close>&times;</button>
</div>
<div class="analytics-content">
@ -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";