From bf12996a406b04caed32946d9a8e74a4957cfd35 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Wed, 30 Sep 2026 22:29:35 +0530 Subject: [PATCH] =?UTF-8?q?feat(flowchart):=20visual=20polish=20=E2=80=94?= =?UTF-8?q?=20selection=20ring,=20hover,=20resize=20grip=20(C16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the visual feedback that was missing from the C10 resize + C2 selection commits. The canvas previously set the .selected class but no CSS rule styled it, so a selected node looked identical to an unselected one. Same for hover and the resize handle. CSS additions (src/flowchart-generator.html): - .flowchart-node: cursor:grab + 0.12s filter transition for the hover tint (drop-shadow blue glow). - .flowchart-node:hover .flowchart-node-shape: blue glow. - .flowchart-node.selected .flowchart-node-shape: 2.25px blue stroke — replaces the (previously invisible) .selected class. - .flowchart-edge:hover and .flowchart-edge.selected: blue stroke + 2.5px width for the same reason. - .flowchart-resize-handle-hit: 24px transparent rect for an enlarged hit area (the visible grip stays 10px). - .flowchart-resize-handle-grip: blue square with stroke, styled via CSS instead of inline fill/stroke (which were overriding the rest of the file's theme). - .flowchart-edge-label-bg: opaque white pill behind edge labels, so labels stay readable when an edge curves under them. - .fc-modal / .fc-modal-overlay: fade + scale-in keyframes (0.15s ease-out) so the promptInline / confirmInline modals animate in instead of popping in instantly. Canvas (src/flowchart/flowchart-canvas.js): the resize handle now emits two rects — a 24px transparent hit area (data-resize-node attribute preserved) and a smaller 10px visible grip styled via .flowchart-resize-handle-grip. Cursor stays nwse-resize via CSS. Tests: no new tests (visual); existing 14 canvas tests still pass. 1376 total. Amit Haridas --- src/flowchart-generator.html | 71 +++++++++++++++++++++++++++++++ src/flowchart/flowchart-canvas.js | 32 ++++++++------ tests/flowchart-align.test.js | 13 +----- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/flowchart-generator.html b/src/flowchart-generator.html index 1d06087..7684cba 100644 --- a/src/flowchart-generator.html +++ b/src/flowchart-generator.html @@ -185,6 +185,77 @@ .flowchart-edge { stroke: #1f2328 !important; } + /* v4.13.0 — visual polish. Adds a visible selection ring (was + previously only the .selected class without a distinct colour), + a hover tint, an enlarged resize-handle hit area, a smoother + modal dialog animation, and a proper opaque background behind + edge labels so they stay readable across curved connectors. */ + .flowchart-node { + cursor: grab; + transition: filter 0.12s ease-out; + } + .flowchart-node:hover .flowchart-node-shape { + filter: drop-shadow(0 0 2px rgba(37, 99, 235, 0.45)); + } + .flowchart-node.selected .flowchart-node-shape { + stroke: #2563eb !important; + stroke-width: 2.25; + } + .flowchart-edge { + cursor: pointer; + } + .flowchart-edge:hover { + stroke: #2563eb !important; + } + .flowchart-edge.selected { + stroke: #2563eb !important; + stroke-width: 2.5; + } + /* Resize handle — larger invisible hit area + visible square. */ + .flowchart-resize-handle { + cursor: nwse-resize; + } + .flowchart-resize-handle-hit { + fill: transparent; + stroke: none; + } + .flowchart-resize-handle-grip { + fill: #2563eb; + stroke: #1e40af; + stroke-width: 1; + } + /* Edge label background — keeps labels legible across curves by + giving them an opaque white pill behind the text. */ + .flowchart-edge-label-bg { + fill: #ffffff; + stroke: #d1d5db; + stroke-width: 0.5; + } + /* Modal dialog — fade + scale-in entrance. */ + .fc-modal { + animation: fc-modal-in 0.15s ease-out; + } + @keyframes fc-modal-in { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + } + .fc-modal-overlay { + animation: fc-overlay-in 0.12s ease-out; + } + @keyframes fc-overlay-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } /* v4.10.0 — preview-render info card explaining the layout */ .fc-preview-render-note { font-size: 0.75rem; diff --git a/src/flowchart/flowchart-canvas.js b/src/flowchart/flowchart-canvas.js index cfb4441..bfe201b 100644 --- a/src/flowchart/flowchart-canvas.js +++ b/src/flowchart/flowchart-canvas.js @@ -232,21 +232,29 @@ function createCanvas(container, store, opts = {}) { // v4.13.0 — bottom-right resize handle on the selected node. A // single square handle is enough for v1; multi-handle (4 corners // + 4 edges) is a follow-up. Width is the only mutable dimension. + // Two rects: a larger transparent hit area (24px) so the handle is + // easier to grab, plus a smaller visible grip (10px) styled via CSS. if (node.id === selectedNodeId) { - const handleSize = 10; - const handle = svgEl('rect', { - x: w - handleSize / 2, - y: DEFAULT_HEIGHT - handleSize / 2, - width: handleSize, - height: handleSize, - fill: '#e5461f', - stroke: '#ffffff', - 'stroke-width': 1, - class: 'flowchart-resize-handle', + const gripSize = 10; + const hitSize = 24; + const hit = svgEl('rect', { + x: w - hitSize / 2, + y: DEFAULT_HEIGHT - hitSize / 2, + width: hitSize, + height: hitSize, + class: 'flowchart-resize-handle flowchart-resize-handle-hit', 'data-resize-node': node.id, - cursor: 'nwse-resize', }); - g.appendChild(handle); + const grip = svgEl('rect', { + x: w - gripSize / 2, + y: DEFAULT_HEIGHT - gripSize / 2, + width: gripSize, + height: gripSize, + rx: 2, + class: 'flowchart-resize-handle flowchart-resize-handle-grip', + }); + g.appendChild(hit); + g.appendChild(grip); } nodesLayer.appendChild(g); diff --git a/tests/flowchart-align.test.js b/tests/flowchart-align.test.js index fb9fbbf..f51074e 100644 --- a/tests/flowchart-align.test.js +++ b/tests/flowchart-align.test.js @@ -91,11 +91,7 @@ describe('flowchart-align: distributeHorizontally', () => { expect(out.map((node) => node.x)).toEqual([0, 300, 600, 900]); }); test('handles unsorted input by sorting first', () => { - const out = distributeHorizontally([ - n('a', 500, 0), - n('b', 0, 0), - n('c', 900, 0), - ]); + const out = distributeHorizontally([n('a', 500, 0), n('b', 0, 0), n('c', 900, 0)]); // sorted: 0, 500, 900 → gap = 450 → x = [0, 450, 900] expect(out.map((node) => node.x)).toEqual([0, 450, 900]); }); @@ -109,12 +105,7 @@ describe('flowchart-align: distributeHorizontally', () => { describe('flowchart-align: distributeVertically', () => { test('spaces 4 nodes evenly between topmost and bottommost', () => { - const out = distributeVertically([ - n('a', 0, 0), - n('b', 0, 60), - n('c', 0, 300), - n('d', 0, 900), - ]); + const out = distributeVertically([n('a', 0, 0), n('b', 0, 60), n('c', 0, 300), n('d', 0, 900)]); // gap = 900 / 3 = 300 expect(out.map((node) => node.y)).toEqual([0, 300, 600, 900]); });