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:
parent
99131df914
commit
2264267ded
|
|
@ -1282,3 +1282,102 @@
|
||||||
color: var(--rs-text-muted, #64748b);
|
color: var(--rs-text-muted, #64748b);
|
||||||
font-style: italic;
|
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; }
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<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>
|
</button>
|
||||||
</div>
|
</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"}">
|
<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"/>
|
<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>
|
<span class="flows-speed-label" id="sim-speed-label">${this.simSpeedMs}ms</span>
|
||||||
|
|
@ -1108,8 +1111,13 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
if (!this.canvasInitialized) {
|
if (!this.canvasInitialized) {
|
||||||
this.canvasInitialized = true;
|
this.canvasInitialized = true;
|
||||||
requestAnimationFrame(() => this.fitView());
|
requestAnimationFrame(() => this.fitView());
|
||||||
// Auto-start tour on first visit
|
// Auto-start simulation for demo flows
|
||||||
if (!localStorage.getItem("rflows_tour_done")) {
|
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);
|
setTimeout(() => this.startTour(), 1200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4654,7 +4662,13 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
private toggleSimulation() {
|
private toggleSimulation() {
|
||||||
this.isSimulating = !this.isSimulating;
|
this.isSimulating = !this.isSimulating;
|
||||||
const btn = this.shadow.getElementById("sim-btn");
|
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
|
// Show/hide speed slider and timeline
|
||||||
const speedContainer = this.shadow.getElementById("sim-speed-container");
|
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 === "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>
|
<button class="analytics-tab ${this.analyticsTab === "transactions" ? "analytics-tab--active" : ""}" data-analytics-tab="transactions">Transactions</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="analytics-minimize" data-analytics-minimize title="Minimize panel">◀</button>
|
||||||
<button class="analytics-close" data-analytics-close>×</button>
|
<button class="analytics-close" data-analytics-close>×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="analytics-content">
|
<div class="analytics-content">
|
||||||
|
|
@ -4835,6 +4850,15 @@ class FolkFlowsApp extends HTMLElement {
|
||||||
const closeBtn = this.shadow.querySelector("[data-analytics-close]");
|
const closeBtn = this.shadow.querySelector("[data-analytics-close]");
|
||||||
closeBtn?.addEventListener("click", () => this.toggleAnalytics());
|
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) => {
|
this.shadow.querySelectorAll("[data-analytics-tab]").forEach((el) => {
|
||||||
el.addEventListener("click", () => {
|
el.addEventListener("click", () => {
|
||||||
const tab = (el as HTMLElement).dataset.analyticsTab as "overview" | "transactions";
|
const tab = (el as HTMLElement).dataset.analyticsTab as "overview" | "transactions";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue