From a419160b3af07736c273a2c848f07771bf78e586 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:09:44 -0400 Subject: [PATCH] =?UTF-8?q?feat(book):=20J3.c=20=E2=80=94=20show-thread=20?= =?UTF-8?q?overlay=20on=20port=20hover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hovering any in / out port of a threaded frame highlights every frame in the same chain. The head of the thread (first frame, no incoming link) gets a blue outline; the rest get green. Hover off clears the highlight. _bookCollectThreadChain walks backwards along incoming nextFrame refs to the head, then forward along outgoing refs to the tail. Cycle-safe and depth-capped. Makes long threads (e.g., a chapter flowing across many designed pages) immediately legible — useful in J5+ when threads start spanning more than two frames. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/index.html b/src/index.html index ca7b41b..8fc1d24 100644 --- a/src/index.html +++ b/src/index.html @@ -2088,6 +2088,20 @@ 0%,100% { box-shadow: 0 0 0 3px rgba(59,130,246,.9); } 50% { box-shadow: 0 0 0 6px rgba(59,130,246,.35); } } + + /* J3.c: thread-hover highlight. When the user hovers any port on + a threaded frame, every frame in the same chain glows so the + flow path is visible at a glance. Implemented by tagging + chain-member freeLayers with `.threadHighlight` from a JS + hover handler. */ + .freeLayer.threadHighlight { + outline: 2px solid rgba(34,197,94,.85); + outline-offset: 2px; + transition: outline-color .12s; + } + .freeLayer.threadHighlight.threadHighlightHead { + outline-color: rgba(59,130,246,.85); + } .bookFrameIdBadge { position: absolute; bottom: 4px; left: 4px; font-size: .58rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; @@ -22662,6 +22676,54 @@ layer.nextFrame = null; render(); } + // J3.c — walk forward + backward along nextFrame refs from + // `layer` and tag every chain member's freeLayer wrapper. The + // head of the thread (first frame, no incoming link) gets a + // distinct blue outline so direction is readable. + function _bookHighlightThread(layer) { + _bookClearThreadHighlight(); + const chain = _bookCollectThreadChain(layer); + if (!chain.length) return; + chain.forEach((l, i) => { + const el = document.querySelector(`.freeLayer[data-layer-id="${CSS.escape(l.id)}"]`); + if (!el) return; + el.classList.add('threadHighlight'); + if (i === 0) el.classList.add('threadHighlightHead'); + }); + } + function _bookClearThreadHighlight() { + document.querySelectorAll('.freeLayer.threadHighlight').forEach(el => { + el.classList.remove('threadHighlight', 'threadHighlightHead'); + }); + } + function _bookCollectThreadChain(layer) { + // Walk backwards to the head, then forward to the tail. + // Returns layers in flow order (head → ... → tail). + const seen = new Set(); + // Backwards: find the layer whose nextFrame.frameId equals + // current.frameId; loop until none exists. + let head = layer; + let guard = 0; + while (guard++ < 1000) { + const prev = _findBookFrameLayer(o => o !== head && o.nextFrame?.frameId === head.frameId); + if (!prev || seen.has(prev.id)) break; + seen.add(prev.id); + head = prev; + } + const chain = [head]; + seen.add(head.id); + let cursor = head; + guard = 0; + while (cursor.nextFrame?.frameId && guard++ < 1000) { + const nxt = _findBookFrameLayer(o => o.frameId === cursor.nextFrame.frameId); + if (!nxt || seen.has(nxt.id)) break; + seen.add(nxt.id); + chain.push(nxt); + cursor = nxt; + } + return chain; + } + function _bookPortOnClick(layer, portKind, ev) { ev.preventDefault(); ev.stopPropagation(); @@ -22713,6 +22775,10 @@ inPort.dataset.framePort = 'in'; inPort.dataset.frameId = myFrameId; inPort.addEventListener('click', (ev) => _bookPortOnClick(layer, 'in', ev)); + if (linkedIn || linkedOut) { + inPort.addEventListener('mouseenter', () => _bookHighlightThread(layer)); + inPort.addEventListener('mouseleave', _bookClearThreadHighlight); + } el.appendChild(inPort); const outPort = document.createElement('div'); @@ -22727,6 +22793,10 @@ outPort.dataset.framePort = 'out'; outPort.dataset.frameId = myFrameId; outPort.addEventListener('click', (ev) => _bookPortOnClick(layer, 'out', ev)); + if (linkedIn || linkedOut) { + outPort.addEventListener('mouseenter', () => _bookHighlightThread(layer)); + outPort.addEventListener('mouseleave', _bookClearThreadHighlight); + } el.appendChild(outPort); const badge = document.createElement('div');