feat(rflows): redesign canvas toolbar + zoom controls, remove sufficiency badge

Move toolbar from horizontal top-right strip to vertical left-side panel
with larger, more prominent buttons and emoji icons. Replace basic +/-
zoom buttons with a pill-shaped widget showing live zoom percentage and a
fit-to-view button. Remove the "0% Enough" sufficiency badge overlay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-10 12:39:04 -07:00
parent cf7fab25f3
commit e37044f599
2 changed files with 72 additions and 55 deletions

View File

@ -234,8 +234,8 @@
.flows-nav-overlay .rapp-nav__title { color: var(--rs-text-primary); font-size: 14px; font-weight: 600; }
.flows-nav-overlay .rapp-nav__badge { font-size: 10px; color: var(--rs-warning); background: rgba(251,191,36,0.15); padding: 2px 8px; border-radius: 4px; }
/* Badge offset when nav overlay present */
.flows-canvas-container--fullpage .flows-canvas-badge { top: 54px; }
/* Toolbar offset when nav overlay present */
.flows-canvas-container--fullpage .flows-canvas-toolbar { top: 54px; }
.flows-canvas-svg {
width: 100%; height: 100%; display: block;
@ -247,26 +247,32 @@
.flows-canvas-svg.panning { cursor: grabbing; }
.flows-canvas-svg.dragging { cursor: move; }
/* Toolbar — top-right overlay */
/* Toolbar — left-side vertical panel */
.flows-canvas-toolbar {
position: absolute; top: 10px; right: 10px; z-index: 10;
display: flex; gap: 4px; flex-wrap: wrap; align-items: center;
position: absolute; top: 10px; left: 10px; z-index: 10;
display: flex; flex-direction: column; gap: 4px; align-items: stretch;
background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
border-radius: 10px; padding: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);
width: 150px;
}
.flows-canvas-btn {
padding: 5px 10px; border: 1px solid var(--rs-bg-surface-raised); border-radius: 6px;
background: var(--rs-bg-surface); color: var(--rs-text-primary); font-size: 11px; font-weight: 500;
.flows-toolbar-btn {
padding: 7px 10px; border: 1px solid transparent; border-radius: 6px;
background: transparent; color: var(--rs-text-primary); font-size: 12px; font-weight: 600;
cursor: pointer; white-space: nowrap; transition: background 0.15s, border-color 0.15s;
text-align: left;
}
.flows-canvas-btn:hover { background: var(--rs-border-strong); border-color: var(--rs-text-muted); }
.flows-canvas-btn--source { border-color: #10b981; color: var(--rflows-source-text, #6ee7b7); }
.flows-canvas-btn--source:hover { background: var(--rflows-source-hover-bg, #064e3b); }
.flows-canvas-btn--funnel { border-color: #3b82f6; color: var(--rflows-funnel-text, #93c5fd); }
.flows-canvas-btn--funnel:hover { background: var(--rflows-funnel-hover-bg, #1e3a5f); }
.flows-canvas-btn--outcome { border-color: #ec4899; color: var(--rflows-outcome-text, #f9a8d4); }
.flows-canvas-btn--outcome:hover { background: var(--rflows-outcome-hover-bg, #4a1942); }
.flows-canvas-btn--active { background: var(--rs-primary); border-color: var(--rs-primary-hover); color: #fff; }
.flows-canvas-sep {
width: 1px; height: 20px; background: var(--rs-border-strong); margin: 0 4px;
.flows-toolbar-btn:hover { background: var(--rs-bg-hover); }
.flows-toolbar-btn--fund { background: var(--rs-primary); color: #fff; border-color: var(--rs-primary); text-align: center; }
.flows-toolbar-btn--fund:hover { background: var(--rs-primary-hover); }
.flows-toolbar-btn--source { color: var(--rflows-source-text, #6ee7b7); }
.flows-toolbar-btn--source:hover { background: var(--rflows-source-hover-bg, #064e3b); }
.flows-toolbar-btn--funnel { color: var(--rflows-funnel-text, #93c5fd); }
.flows-toolbar-btn--funnel:hover { background: var(--rflows-funnel-hover-bg, #1e3a5f); }
.flows-toolbar-btn--outcome { color: var(--rflows-outcome-text, #f9a8d4); }
.flows-toolbar-btn--outcome:hover { background: var(--rflows-outcome-hover-bg, #4a1942); }
.flows-toolbar-btn--active { background: var(--rs-primary); border-color: var(--rs-primary-hover); color: #fff; }
.flows-toolbar-sep {
height: 1px; background: var(--rs-border); margin: 2px 0;
}
/* SVG node styles */
@ -389,20 +395,27 @@
.flows-canvas-legend-item { display: flex; align-items: center; gap: 4px; }
.flows-canvas-legend-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
/* Zoom controls — bottom-right */
/* Zoom controls — bottom-right pill */
.flows-canvas-zoom {
position: absolute; bottom: 10px; right: 10px; z-index: 10;
display: flex; gap: 4px;
position: absolute; bottom: 14px; right: 14px; z-index: 10;
display: flex; align-items: center; gap: 0;
background: var(--rs-bg-surface); border: 1px solid var(--rs-border);
border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);
overflow: hidden;
}
/* Sufficiency badge — top-left */
.flows-canvas-badge {
position: absolute; top: 10px; left: 10px; z-index: 10;
background: var(--rs-glass-bg); border-radius: 8px; padding: 8px 14px;
display: flex; align-items: center; gap: 8px;
.flows-zoom-btn {
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border: none; background: transparent;
color: var(--rs-text-primary); cursor: pointer; transition: background 0.15s;
}
.flows-zoom-btn:hover { background: var(--rs-bg-hover); }
.flows-zoom-level {
font-size: 11px; font-weight: 600; color: var(--rs-text-secondary);
min-width: 40px; text-align: center; user-select: none;
}
.flows-zoom-sep {
width: 1px; height: 18px; background: var(--rs-border); margin: 0 2px;
}
.flows-canvas-badge__score { font-size: 20px; font-weight: 700; }
.flows-canvas-badge__label { font-size: 10px; color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
/* Legacy diagram (kept for compat) */
.flows-diagram { overflow-x: auto; }
@ -972,11 +985,11 @@
/* ── Flow dropdown (toolbar) ──────────────────────── */
.flows-dropdown {
position: relative; display: inline-block;
position: relative; display: block;
}
.flows-dropdown__trigger {
display: flex; align-items: center; gap: 4px;
max-width: 180px;
display: flex; align-items: center; gap: 4px; width: 100%;
max-width: none;
}
.flows-dropdown__name {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@ -1147,8 +1160,8 @@
.flows-cards { grid-template-columns: 1fr; }
.flows-canvas-container { height: 50vh; min-height: 300px; }
.flows-canvas-container--fullpage { height: 100%; min-height: unset; }
.flows-canvas-toolbar { flex-wrap: wrap; gap: 3px; top: 6px; right: 6px; }
.flows-canvas-btn { padding: 6px 10px; font-size: 11px; min-height: 44px; min-width: 44px; }
.flows-canvas-toolbar { width: 130px; top: 6px; left: 6px; padding: 4px; }
.flows-toolbar-btn { padding: 8px 8px; font-size: 12px; min-height: 40px; }
.flows-editor-panel { width: 100%; }
.flows-analytics-panel { width: 100%; }
.flows-canvas-legend { font-size: 10px; gap: 8px; }

View File

@ -891,15 +891,9 @@ class FolkFlowsApp extends HTMLElement {
<span class="rapp-nav__title">${this.esc(this.flowName || "Flow Detail")}</span>
${this.isDemo ? '<span class="rapp-nav__badge">Demo</span>' : ""}
</div>
<div class="flows-canvas-badge" id="canvas-badge">
<div>
<div class="flows-canvas-badge__score" id="badge-score" style="color:${scoreColor}">${scorePct}%</div>
<div class="flows-canvas-badge__label">ENOUGH</div>
</div>
</div>
<div class="flows-canvas-toolbar">
<div class="flows-dropdown" id="flow-dropdown">
<button class="flows-canvas-btn flows-dropdown__trigger" data-canvas-action="flow-picker">
<button class="flows-toolbar-btn flows-dropdown__trigger" data-canvas-action="flow-picker">
<span class="flows-dropdown__name">${this.esc(this.flowName || 'Untitled')}</span>
<span class="flows-dropdown__chevron">&#x25BE;</span>
</button>
@ -910,17 +904,16 @@ class FolkFlowsApp extends HTMLElement {
<button class="flows-dropdown__item" data-flow-action="manage-flows">Manage Flows...</button>
</div>
</div>
<div class="flows-canvas-sep"></div>
<button class="flows-canvas-btn flows-canvas-btn--fund" data-canvas-action="quick-fund" style="background:var(--rs-primary);color:white;font-weight:600">Fund</button>
<div class="flows-canvas-sep"></div>
<button class="flows-canvas-btn flows-canvas-btn--source" data-canvas-action="add-source">+ Source</button>
<button class="flows-canvas-btn flows-canvas-btn--funnel" data-canvas-action="add-funnel">+ Funnel</button>
<button class="flows-canvas-btn flows-canvas-btn--outcome" data-canvas-action="add-outcome">+ Outcome</button>
<div class="flows-canvas-sep"></div>
<button class="flows-canvas-btn" data-canvas-action="sim" id="sim-btn">${this.isSimulating ? "Pause" : "Play"}</button>
<button class="flows-canvas-btn" data-canvas-action="fit">Fit</button>
<button class="flows-canvas-btn ${this.analyticsOpen ? "flows-canvas-btn--active" : ""}" data-canvas-action="analytics">Analytics</button>
<button class="flows-canvas-btn" data-canvas-action="share">Share</button>
<div class="flows-toolbar-sep"></div>
<button class="flows-toolbar-btn flows-toolbar-btn--fund" data-canvas-action="quick-fund">💰 Fund</button>
<div class="flows-toolbar-sep"></div>
<button class="flows-toolbar-btn flows-toolbar-btn--source" data-canvas-action="add-source">+ Source</button>
<button class="flows-toolbar-btn flows-toolbar-btn--funnel" data-canvas-action="add-funnel">+ Funnel</button>
<button class="flows-toolbar-btn flows-toolbar-btn--outcome" data-canvas-action="add-outcome">+ Outcome</button>
<div class="flows-toolbar-sep"></div>
<button class="flows-toolbar-btn" data-canvas-action="sim" id="sim-btn">${this.isSimulating ? "⏸ Pause" : "▶ Play"}</button>
<button class="flows-toolbar-btn ${this.analyticsOpen ? "flows-toolbar-btn--active" : ""}" data-canvas-action="analytics">📊 Analytics</button>
<button class="flows-toolbar-btn" data-canvas-action="share">🔗 Share</button>
</div>
<svg class="flows-canvas-svg" id="flow-canvas">
<defs>
@ -956,8 +949,17 @@ class FolkFlowsApp extends HTMLElement {
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:var(--rflows-status-thriving);border-radius:50%"></span>Thriving</span>
</div>
<div class="flows-canvas-zoom">
<button class="flows-canvas-btn" data-canvas-action="zoom-in">+</button>
<button class="flows-canvas-btn" data-canvas-action="zoom-out">&minus;</button>
<button class="flows-zoom-btn" data-canvas-action="zoom-in" title="Zoom in">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
<span class="flows-zoom-level" id="zoom-level">${Math.round(this.canvasZoom * 100)}%</span>
<button class="flows-zoom-btn" data-canvas-action="zoom-out" title="Zoom out">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
<div class="flows-zoom-sep"></div>
<button class="flows-zoom-btn" data-canvas-action="fit" title="Fit to view">
<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>
<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"/>
@ -1040,6 +1042,8 @@ class FolkFlowsApp extends HTMLElement {
const g = this.shadow.getElementById("canvas-transform");
if (g) g.setAttribute("transform", `translate(${this.canvasPanX},${this.canvasPanY}) scale(${this.canvasZoom})`);
this.saveViewport();
const zl = this.shadow.getElementById("zoom-level");
if (zl) zl.textContent = `${Math.round(this.canvasZoom * 100)}%`;
}
private fitView() {
@ -4640,7 +4644,7 @@ class FolkFlowsApp extends HTMLElement {
panel.classList.toggle("open", this.analyticsOpen);
}
const btn = this.shadow.querySelector('[data-canvas-action="analytics"]');
if (btn) btn.classList.toggle("flows-canvas-btn--active", this.analyticsOpen);
if (btn) btn.classList.toggle("flows-toolbar-btn--active", this.analyticsOpen);
}
private attachAnalyticsListeners() {