fix(book): text-frame ports were eating resize handles + drag/select events

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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-06-04 20:59:28 -04:00
1 parent b6b00c09f1
commit 81dd6bf6b6
1 file changed
+43 -12
+43 -12
View File
@@ -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();