fix(rsocials): proper canvas sizing, SVG zoom icons, category-styled nodes

- Fix height to account for shell header (92px not 60px)
- Add min-height:0 on flex children to prevent overflow
- Replace text zoom buttons with SVG icons matching rFlows pattern
- Add fit-to-view icon (corner brackets) with separators
- Add category class per node (trigger/delay/condition/action)
  with tinted backgrounds and category badge chips
- Add keyboard shortcuts: F=fit, +/-=zoom

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 00:20:01 -07:00
parent 78284e448c
commit 456d0de9c1
2 changed files with 96 additions and 17 deletions

View File

@ -1,7 +1,7 @@
/* rSocials Campaign Workflow — n8n-style workflow builder */ /* rSocials Campaign Workflow — n8n-style workflow builder */
folk-campaign-workflow { folk-campaign-workflow {
display: block; display: block;
height: calc(100vh - 60px); height: calc(100vh - 92px);
} }
.cw-root { .cw-root {
@ -22,6 +22,7 @@ folk-campaign-workflow {
border-bottom: 1px solid var(--rs-border, #2d2d44); border-bottom: 1px solid var(--rs-border, #2d2d44);
background: var(--rs-bg-surface, #1a1a2e); background: var(--rs-bg-surface, #1a1a2e);
z-index: 10; z-index: 10;
flex-shrink: 0;
} }
.cw-toolbar__title { .cw-toolbar__title {
@ -106,6 +107,7 @@ folk-campaign-workflow {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
min-height: 0;
} }
/* ── Left sidebar — node palette ── */ /* ── Left sidebar — node palette ── */
@ -172,6 +174,7 @@ folk-campaign-workflow {
overflow: hidden; overflow: hidden;
cursor: grab; cursor: grab;
background: var(--rs-canvas-bg, #0f0f23); background: var(--rs-canvas-bg, #0f0f23);
min-width: 0;
} }
.cw-canvas.grabbing { .cw-canvas.grabbing {
@ -330,42 +333,57 @@ folk-campaign-workflow {
/* ── Zoom controls ── */ /* ── Zoom controls ── */
.cw-zoom-controls { .cw-zoom-controls {
position: absolute; position: absolute;
bottom: 12px; bottom: 14px;
right: 12px; right: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 0;
background: var(--rs-bg-surface, #1a1a2e); background: var(--rs-bg-surface, #1a1a2e);
border: 1px solid var(--rs-border-strong, #3d3d5c); border: 1px solid var(--rs-border, #2d2d44);
border-radius: 8px; border-radius: 8px;
padding: 4px 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);
overflow: hidden;
z-index: 5; z-index: 5;
} }
.cw-zoom-btn { .cw-zoom-btn {
width: 28px; width: 32px;
height: 28px; height: 32px;
border: none; border: none;
border-radius: 6px; border-radius: 0;
background: transparent; background: transparent;
color: var(--rs-text-primary, #e2e8f0); color: var(--rs-text-primary, #e2e8f0);
font-size: 16px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.15s; transition: background 0.15s;
padding: 0;
} }
.cw-zoom-btn:hover { .cw-zoom-btn:hover {
background: var(--rs-bg-surface-raised, #252545); background: var(--rs-bg-surface-raised, #252545);
} }
.cw-zoom-btn svg {
width: 16px;
height: 16px;
}
.cw-zoom-sep {
width: 1px;
height: 18px;
background: var(--rs-border, #2d2d44);
margin: 0 2px;
}
.cw-zoom-level { .cw-zoom-level {
font-size: 11px; font-size: 11px;
color: var(--rs-text-muted, #94a3b8); font-weight: 600;
min-width: 36px; color: var(--rs-text-secondary, #94a3b8);
min-width: 40px;
text-align: center; text-align: center;
user-select: none;
} }
/* ── Node styles in SVG ── */ /* ── Node styles in SVG ── */
@ -388,6 +406,24 @@ folk-campaign-workflow {
border-color: #4f46e5 !important; border-color: #4f46e5 !important;
} }
/* Category-specific node backgrounds */
.cw-node--trigger foreignObject > div {
border-color: #3b82f644;
background: linear-gradient(135deg, #1a1a2e 0%, #1e2a3e 100%);
}
.cw-node--delay foreignObject > div {
border-color: #a855f744;
background: linear-gradient(135deg, #1a1a2e 0%, #251e3e 100%);
}
.cw-node--condition foreignObject > div {
border-color: #f59e0b44;
background: linear-gradient(135deg, #1a1a2e 0%, #2e2a1e 100%);
}
.cw-node--action foreignObject > div {
border-color: #10b98144;
background: linear-gradient(135deg, #1a1a2e 0%, #1e2e24 100%);
}
.cw-node-header { .cw-node-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -411,6 +447,20 @@ folk-campaign-workflow {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.cw-node-cat {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 5px;
border-radius: 3px;
flex-shrink: 0;
}
.cw-node-cat--trigger { color: #60a5fa; background: #3b82f622; }
.cw-node-cat--delay { color: #c084fc; background: #a855f722; }
.cw-node-cat--condition { color: #fbbf24; background: #f59e0b22; }
.cw-node-cat--action { color: #34d399; background: #10b98122; }
.cw-node-status { .cw-node-status {
width: 8px; width: 8px;
height: 8px; height: 8px;
@ -494,6 +544,10 @@ folk-campaign-workflow {
/* ── Mobile ── */ /* ── Mobile ── */
@media (max-width: 768px) { @media (max-width: 768px) {
folk-campaign-workflow {
height: calc(100vh - 56px);
}
.cw-palette { .cw-palette {
width: 160px; width: 160px;
min-width: 160px; min-width: 160px;

View File

@ -348,10 +348,19 @@ class FolkCampaignWorkflow extends HTMLElement {
</g> </g>
</svg> </svg>
<div class="cw-zoom-controls"> <div class="cw-zoom-controls">
<button class="cw-zoom-btn" id="zoom-out">-</button> <button class="cw-zoom-btn" id="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>
<span class="cw-zoom-sep"></span>
<span class="cw-zoom-level" id="zoom-level">${Math.round(this.canvasZoom * 100)}%</span> <span class="cw-zoom-level" id="zoom-level">${Math.round(this.canvasZoom * 100)}%</span>
<button class="cw-zoom-btn" id="zoom-in">+</button> <span class="cw-zoom-sep"></span>
<button class="cw-zoom-btn" id="zoom-fit" title="Fit view">&#8644;</button> <button class="cw-zoom-btn" id="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="cw-zoom-sep"></span>
<button class="cw-zoom-btn" id="zoom-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>
</div> </div>
@ -400,12 +409,13 @@ class FolkCampaignWorkflow extends HTMLElement {
} }
return ` return `
<g class="cw-node ${isSelected ? 'selected' : ''}" data-node-id="${node.id}"> <g class="cw-node cw-node--${def.category} ${isSelected ? 'selected' : ''}" data-node-id="${node.id}">
<foreignObject x="${node.position.x}" y="${node.position.y}" width="${NODE_WIDTH}" height="${NODE_HEIGHT}"> <foreignObject x="${node.position.x}" y="${node.position.y}" width="${NODE_WIDTH}" height="${NODE_HEIGHT}">
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%"> <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%">
<div class="cw-node-header" style="border-left: 3px solid ${catColor}"> <div class="cw-node-header" style="border-left: 3px solid ${catColor}">
<span class="cw-node-icon">${def.icon}</span> <span class="cw-node-icon">${def.icon}</span>
<span class="cw-node-label">${esc(node.label)}</span> <span class="cw-node-label">${esc(node.label)}</span>
<span class="cw-node-cat cw-node-cat--${def.category}">${def.category}</span>
<span class="cw-node-status ${status}"></span> <span class="cw-node-status ${status}"></span>
</div> </div>
<div class="cw-node-ports"> <div class="cw-node-ports">
@ -960,13 +970,28 @@ class FolkCampaignWorkflow extends HTMLElement {
} }
private handleKeyDown(e: KeyboardEvent) { private handleKeyDown(e: KeyboardEvent) {
const tag = (e.target as Element)?.tagName;
const isEditing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
if (e.key === 'Escape') { if (e.key === 'Escape') {
if (this.wiringActive) this.cancelWiring(); if (this.wiringActive) this.cancelWiring();
} }
if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedNodeId) { if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedNodeId) {
if ((e.target as Element)?.tagName === 'INPUT' || (e.target as Element)?.tagName === 'TEXTAREA') return; if (isEditing) return;
this.deleteNode(this.selectedNodeId); this.deleteNode(this.selectedNodeId);
} }
if (isEditing) return;
if (e.key === 'f' || e.key === 'F') {
this.fitView();
}
if (e.key === '=' || e.key === '+') {
const svg = this.shadow.getElementById('cw-svg');
if (svg) { const r = svg.getBoundingClientRect(); this.zoomAt(r.width / 2, r.height / 2, 1.2); }
}
if (e.key === '-') {
const svg = this.shadow.getElementById('cw-svg');
if (svg) { const r = svg.getBoundingClientRect(); this.zoomAt(r.width / 2, r.height / 2, 0.8); }
}
} }
} }