From 81dd6bf6b6f29ed947d493f635ae0e8ce43069f1 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:59:28 -0400 Subject: [PATCH] fix(book): text-frame ports were eating resize handles + drag/select events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three reported bugs, one root cause: the in/out ports sat at the top-left and bottom-right CORNERS of every text-frame layer — exactly where the lh-nw and lh-se resize handles live — AND their pointerdown bubbled to the freeLayer's startLayerDrag handler. Symptoms: - Couldn't resize a text frame (corner handles hidden under the larger-z-index ports). - Couldn't click an in/out port to thread (the port's pointerdown started a layer drag, the click never fired). - Couldn't 'Place chapter into selected frame' (the chapter button needs a selected frame, and selection requires a click that wasn't being shadowed by drag). Fixes: - Move ports OUTSIDE the layer's bounding box on the top edge (in port) and bottom edge (out port), centered. New tab shape with a dashed connector to the frame edge so the affordance reads. Both well clear of the 8 corner/edge/rotation handles. - Bump the frameId badge to sit just above the layer (top-right, -16px) so it doesn't visually conflict with the in port. - Add pointerdown + mousedown handlers on both ports AND on the source-chapter chip + its X clear button that stopPropagation. Drag handler no longer kidnaps the click. Resize, select, link, and place-chapter all flow correctly now. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 55 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/index.html b/src/index.html index b782601..26e2ed1 100644 --- a/src/index.html +++ b/src/index.html @@ -2050,24 +2050,38 @@ .bookStarterCardDesc { font-size: .76rem; color: var(--ink); margin-top: 8px; line-height: 1.35; } /* Phase J3: in / out ports for threaded text frames. The ports - are small square handles at the corners of every text-frame - layer; clicking out → in links the frames so chapter text - flows from one to the next. Colour signals state: + sit OUTSIDE the layer's bounding box on the top and bottom + edges (centered) so they don't collide with the 8 corner / + edge / rotation resize handles or the per-target controls + layered on the frame. Click out → click another in to thread. + Colour signals state: empty (slate) → unlinked linked (green) → part of a thread overset (red) → end of thread with overflow text */ .bookFramePort { position: absolute; - width: 12px; height: 12px; + width: 14px; height: 14px; background: #fff; border: 1.5px solid #475569; box-sizing: border-box; cursor: crosshair; - z-index: 50; + z-index: 60; pointer-events: auto; + left: 50%; + transform: translateX(-50%); } - .bookFramePort.in { top: -7px; left: -7px; border-radius: 0; } - .bookFramePort.out { bottom: -7px; right: -7px; border-radius: 0; } + .bookFramePort.in { top: -22px; border-radius: 0 0 7px 7px; } + .bookFramePort.out { bottom: -22px; border-radius: 7px 7px 0 0; } + .bookFramePort::before { + content: ''; + position: absolute; + left: 50%; + width: 0; height: 8px; + border-left: 1.5px dashed #475569; + transform: translateX(-50%); + } + .bookFramePort.in::before { top: 100%; } + .bookFramePort.out::before { bottom: 100%; } .bookFramePort.linked { background: #16a34a; border-color: #15803d; } .bookFramePort.overset { background: #dc2626; border-color: #991b1b; } .bookFramePort:hover { box-shadow: 0 0 0 2px rgba(96,165,250,.6); } @@ -2103,13 +2117,14 @@ 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; - color: rgba(71,85,105,.65); - background: rgba(255,255,255,.7); - padding: 1px 4px; border-radius: 2px; + position: absolute; top: -16px; right: -2px; + font-size: .56rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + color: rgba(71,85,105,.55); + background: rgba(255,255,255,.85); + padding: 0 4px; border-radius: 2px; pointer-events: none; user-select: none; + z-index: 7; } /* J5: source-chapter chip on threaded text frames. Shows which @@ -23076,6 +23091,11 @@ const linkedIn = !!incoming; const isLoadingSource = _bookPortLoading?.layerId === layer.id; + // J3.b fix: stop pointerdown on the port from bubbling to the + // freeLayer's startLayerDrag handler — otherwise the drag + // starts and the click never fires. + const stopPointerDown = (ev) => { ev.stopPropagation(); }; + const inPort = document.createElement('div'); inPort.className = 'bookFramePort in' + (linkedIn ? ' linked' : ''); inPort.title = linkedIn @@ -23085,6 +23105,8 @@ : 'In port — empty. Click an "out" port elsewhere first to load a thread, then click this.'); inPort.dataset.framePort = 'in'; inPort.dataset.frameId = myFrameId; + inPort.addEventListener('pointerdown', stopPointerDown); + inPort.addEventListener('mousedown', stopPointerDown); inPort.addEventListener('click', (ev) => _bookPortOnClick(layer, 'in', ev)); if (linkedIn || linkedOut) { inPort.addEventListener('mouseenter', () => _bookHighlightThread(layer)); @@ -23103,6 +23125,8 @@ : 'Out port — text ends here. Click to load and then click another frame\'s in port to link.'); outPort.dataset.framePort = 'out'; outPort.dataset.frameId = myFrameId; + outPort.addEventListener('pointerdown', stopPointerDown); + outPort.addEventListener('mousedown', stopPointerDown); outPort.addEventListener('click', (ev) => _bookPortOnClick(layer, 'out', ev)); if (linkedIn || linkedOut) { outPort.addEventListener('mouseenter', () => _bookHighlightThread(layer)); @@ -23140,6 +23164,11 @@ lab.textContent = `📄 ${title}${positionTag}`; lab.title = `Chapter placed on this thread's head frame. ${chain.length > 1 ? `This is frame ${myIndex + 1} of ${chain.length} in the chain.` : 'Solo frame — link more frames with the out port to extend the thread.'}`; chip.appendChild(lab); + // J3.b fix: same pointerdown shield as ports — the chip + // sits on top of the freeLayer, so its events would + // otherwise start a drag. + chip.addEventListener('pointerdown', (ev) => ev.stopPropagation()); + chip.addEventListener('mousedown', (ev) => ev.stopPropagation()); // Only the head shows the clear (X) — clearing on a // mid-chain frame would be ambiguous. if (layer === head) { @@ -23147,6 +23176,8 @@ clearBtn.className = 'bookFrameSourceClear'; clearBtn.textContent = '×'; clearBtn.title = 'Unplace this chapter from the thread.'; + clearBtn.addEventListener('pointerdown', (ev) => ev.stopPropagation()); + clearBtn.addEventListener('mousedown', (ev) => ev.stopPropagation()); clearBtn.addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation();