Skip to content

feat(studio): off-screen element indicators + unclipped overlay#1360

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/kf-offscreen-indicators
Jun 12, 2026
Merged

feat(studio): off-screen element indicators + unclipped overlay#1360
miguel-heygen merged 1 commit into
mainfrom
fix/kf-offscreen-indicators

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add useOffScreenIndicators hook: RAF-driven detection of GSAP elements positioned outside composition bounds
  • Render dotted indicators at composition edges showing direction to off-screen elements
  • Click indicator to select the element, drag indicator to move and select
  • Split NLE layout: inner overflow-hidden clips iframe, outer container leaves overlay unclipped for handle interaction beyond composition bounds
  • Add onSelectElementById prop to DomEditOverlay for programmatic selection
  • Intentional flag flip: STUDIO_GSAP_DRAG_INTERCEPT_ENABLED set to true — this stack implements and tests the drag intercept system that was gated behind false in fix(studio): disable preview gsap-element dragging behaviour by default #1341

Review feedback addressed (from R2)

Test plan

  • Build passes
  • Typecheck clean
  • GSAP-animated elements positioned outside bounds show dotted indicators
  • Clicking an indicator selects the element
  • Dragging an indicator commits a position move
  • Iframe content doesn't bleed outside preview area at any zoom level
  • Delete All Keyframes removes all animations atomically in one click
  • Context menus appear at cursor position
  • Drag on GSAP element creates/updates keyframes correctly

Stack: 7/7 — depends on #1359

@miguel-heygen miguel-heygen force-pushed the fix/kf-gesture-recording branch from 9dbe446 to da4a238 Compare June 11, 2026 23:54
@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch 2 times, most recently from 3a4d951 to 29c439e Compare June 12, 2026 00:06
@miguel-heygen miguel-heygen changed the title feat(studio): off-screen indicators + unclipped overlay feat(studio): off-screen element indicators + unclipped overlay Jun 12, 2026
@miguel-heygen miguel-heygen marked this pull request as ready for review June 12, 2026 00:18

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 🚨 BLOCKER — off-screen indicator click handler can't find the elements it surfaces.

Blocker — broken selection for unidentified elements

  • useOffScreenIndicators.ts:181 sets elementId = el.id || base where base falls back to data-hf-id || tagName.
  • StudioPreviewArea.tsx:285 click handler resolves via iframe.contentDocument.getElementById(id) — but getElementById only matches the id attribute, NOT data-hf-id.
  • Elements without an HTML id (most authored HF elements, per htmlParser.ts:204 which preserves data-hf-id as canonical) will show indicators that silently fail on click.

Fix: either select by [data-hf-id="..."] in the click handler (querySelector instead of getElementById), or restrict indicators to elements with a real id.

Other concerns

  • Docstring says "edge-clamped" but code doesn't clampuseOffScreenIndicators.ts:1-4 claims "edge-clamped indicator positions" but the hook pushes raw elLeft/elTop/elW/elH — no clamp to comp edges. The "indicators at edges" UX described in the PR body relies entirely on the NLELayout overflow-hidden move letting the off-screen rect render in the unclipped region. A very-far-off element won't render an edge indicator at all — it'll render somewhere far outside the outer container or be clipped by it. Either add the clamp, or update the docstring + PR body to match what's actually built.
  • RAF loop runs foreveruseOffScreenIndicators.ts:113-167 walks every GSAP target, calls getBoundingClientRect() per element, every frame, regardless of whether anything is animating or any element is actually off-screen. With many elements + studio panel re-renders, this is layout thrash on the 16ms budget. Suggest gating on selection change + transform-commit events, or throttling to e.g. 10Hz, or short-circuiting when compRect.width hasn't changed AND targets array hash hasn't changed.
  • Pointer-handler leak riskDomEditOverlay.tsx:560-572 attaches pointermove/pointerup via el.addEventListener but no pointercancel listener. Touch-cancel / contextmenu / scroll-while-dragging will leak listeners + leave setPointerCapture un-released + leave el.style.transform mutated. Add a pointercancel cleanup path mirroring onUp.
  • Outside-comp click guard changes deselection behaviorDomEditOverlay.tsx:263-278 early-returns on clicks outside comp bounds. Comment says "would clear the selection" — but the previous "click empty space to deselect" behavior is now broken in the unclipped overlay region. Worth a UX check: if you select an element then click in the new unclipped margin, does selection persist forever? May be intentional, but it's a behavior change beyond the documented fix.

Determinism

collectGsapTargetElements:36-58 reads iframe.contentWindow.__timelines — wrapped in try/catch for cross-origin, good. No render-time fetch(). HF determinism rule not violated.

Test coverage

No tests added — 196 LOC of coordinate math + RAF loop + DOM mutation with zero coverage. The coordinate-math path (elLeft = iframeRect.left - overlayRect.left + elRect.left * rootScaleX) is exactly the kind of thing that breaks silently on zoom/pan changes.

Cross-stack

Introduces new onSelectElementById prop on DomEditOverlay; depends on buildDomSelectionFromTarget from useDomSelection.ts:206 (already in stack). NLELayout overflow restructure is isolated to this PR.

Review by Rames D Jusso

@miguel-heygen miguel-heygen force-pushed the fix/kf-gesture-recording branch from da4a238 to b4906c9 Compare June 12, 2026 01:03
@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch 3 times, most recently from f7b33b8 to 0dede9b Compare June 12, 2026 03:17

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3 verification — blocker. R2 click-resolution blocker unchanged, plus a stack-internal drive-by flag flip.

🚨 StudioPreviewArea.tsx:289 / useOffScreenIndicators.ts:464getElementById vs data-hf-id mismatch unchanged. The click handler is still iframe?.contentDocument?.getElementById(id); elementId = el.id || base still falls back to data-hf-id/tagName. Authored HF elements with no id attribute will still render an indicator (keyed off data-hf-id) and silently fail to select on click. Fix: swap click handler to querySelector('[data-hf-id="..."], #...') with a discriminator, OR guard elementId so we only emit indicators for elements with a real id.

🚨 manualEditingAvailability.ts:93 — stack-internal drive-by flag flip. STUDIO_GSAP_DRAG_INTERCEPT_ENABLED default flips from falsetrue in this PR — and #1356 (one below in the stack) correctly ships false. Not in the PR body, not in scope of "off-screen element indicators." Either an accidental ride-along (the same accidental flip I caught on monolithic #1344 R2) or an intentional flip that needs body/commit-message coverage. Same flag, same shape, same stack. Please confirm intent.

RAF loop still per-frame at useOffScreenIndicators.ts:387-480. requestAnimationFrame(update) runs unconditionally every frame, calling getBoundingClientRect on every GSAP target. No throttling, no early-out on no-iframe-change. Layout thrash risk on big compositions.

No pointercancel listener at DomEditOverlay.tsx:225-260. onPointerDown adds pointermove/pointerup, no pointercancel. OS-cancelled gestures (touch interrupt, alt-tab mid-drag) leak listeners + leave el.style.transform stuck.

Zero tests. 196 LOC of coordinate math (declared-vs-iframe scaling, overlay vs comp-rect transforms) shipped untested.

PR body vs implementation. Body says "dotted indicators at composition edges"; what ships is border: 1.5px dashed; opacity: 0.5 at ind.left/ind.top — the element's actual off-screen position, clipped by the unclipped overlay. A translucent dashed ghost at real position, not an edge indicator. Update body or implementation.

Review by Rames D Jusso

@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch 2 times, most recently from 7abbbb0 to 7e8f788 Compare June 12, 2026 03:45

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification pass — R3 items checked against current HEAD (7e8f788).

1. getElementById vs data-hf-idStudioPreviewArea.tsx:290

FIXED. useOffScreenIndicators.ts now has a guard at line 172:

// Only elements with a real id attribute can be selected via getElementById
if (!el.id) continue;

All surfaced indicators have elementId = el.id (a real DOM id). The getElementById(id) call in StudioPreviewArea.tsx:290 correctly resolves them. Comment explains the contract.

2. STUDIO_GSAP_DRAG_INTERCEPT_ENABLED false→true flip — manualEditingAvailability.ts:93

INTENTIONAL and explained. PR body states: "Intentional flag flip: STUDIO_GSAP_DRAG_INTERCEPT_ENABLED set to true — this stack implements and tests the drag intercept system that was gated behind false in #1341." The explanation is in the PR body. Sufficient.

Both R3 items addressed. Stamp-ready (with the same non-blocking R3 concerns Rames noted: RAF throttling, pointercancel, zero tests).

— Vai (verification pass)

@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch from 7e8f788 to ef27848 Compare June 12, 2026 03:53
vanceingalls
vanceingalls previously approved these changes Jun 12, 2026

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — Via reviewed, stamp applied.

— Vai

@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch from ef27848 to f82b1d7 Compare June 12, 2026 03:57
Base automatically changed from fix/kf-gesture-recording to main June 12, 2026 04:19
@miguel-heygen miguel-heygen dismissed vanceingalls’s stale review June 12, 2026 04:19

The base branch was changed.

Gesture recording uses replace-with-keyframes mutation to replace existing
position-group tween. Fix N1 sign inversion and N9 wheel startPointer
with pointerElementOffset subtraction.
@miguel-heygen miguel-heygen force-pushed the fix/kf-offscreen-indicators branch from f82b1d7 to 7f93f44 Compare June 12, 2026 04:20
@miguel-heygen miguel-heygen merged commit d49ee41 into main Jun 12, 2026
1 check passed
@miguel-heygen miguel-heygen deleted the fix/kf-offscreen-indicators branch June 12, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants