Skip to content

feat(panels): improve drag UX and add close buttons to live panels#1447

Closed
rayanhabib07 wants to merge 2 commits intokoala73:mainfrom
rayanhabib07:main
Closed

feat(panels): improve drag UX and add close buttons to live panels#1447
rayanhabib07 wants to merge 2 commits intokoala73:mainfrom
rayanhabib07:main

Conversation

@rayanhabib07
Copy link
Contributor

Improves the drag-and-drop system and adds close buttons to live panels.

Changes:

  • Dragging a panel no longer shuffles adjacent panels until you drop it
  • Ghost panel follows cursor straight and translucent, no tilt
  • Hovered drop target gets highlighted so you know where the panel will land
  • Added X close buttons to Live News and Live Webcams panels (fixes Issue: No X buttons on Live News or Live Webcam Blocks #1395)
  • Pressing Cmd+Z / Ctrl+Z restores the last closed panel

@vercel
Copy link

vercel bot commented Mar 11, 2026

@rayanhabib07 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@koala73 koala73 requested a review from SebastienMelki March 11, 2026 19:29
@koala73
Copy link
Owner

koala73 commented Mar 12, 2026

Thanks for this @rayanhabib07! The ghost-based drag is a clear improvement over live DOM shuffling, and the close buttons for live panels are a clean fix for #1395. A few things need fixing before merge:

Blocking:

  1. Cmd+Z hijacks browser undo globally (event-handlers.ts:321-326) — e.preventDefault() fires on every Cmd+Z, even when the user is typing in search, text inputs, or contenteditable fields. Add a guard:

    const tag = (e.target as HTMLElement).tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return;
  2. cloneNode(true) on panels with iframes (panel-layout.ts:1298) — LiveNews and LiveWebcams contain YouTube iframes. Cloning them triggers new network requests and duplicate postMessage handlers. Either use a placeholder ghost (colored rectangle with the panel title) or strip iframe children before cloning.

  3. Drop position uses ghost center instead of cursor (panel-layout.ts:1468-1469) — ghostCenter is offset from the cursor by dragOffsetY. For tall panels grabbed near the bottom, the panel drops in the wrong position. Use lastY (actual cursor position) instead of ghostCenter for the before/after comparison.

Should fix:

  1. Undo stack has no size limit (event-handlers.ts:89) — closedPanelStack grows unbounded. Cap it (e.g., 20 entries).

  2. Undo doesn't re-fetch panel data (event-handlers.ts:113-121) — performUndo() re-enables the setting but doesn't trigger fetchData() on the restored panel. It may show stale/empty content.

  3. Dead CSS for .panel-drop-indicator (main.css:1306-1312) — Inline styles from createDropIndicator() override everything in this class. Either remove the CSS class or move the styles out of JS.

  4. Redundant pointerEvents lines (panel-layout.ts:1341-1342) — Ghost already has pointerEvents: 'none' from creation. Both lines set the same value; the second was probably meant to restore it.

  5. Remove docs/DRAG_SYSTEM_IMPROVEMENTS.mddocs/ is the Mintlify public docs directory. This internal implementation doc doesn't belong there. The PR description is sufficient.

Minor:

  1. Extra blank line at event-handlers.ts:262.
  2. Consider adding max-items: 20 comment near the undo stack for future maintainers.

Fix items 1-3 and this is good to go!

Copy link
Collaborator

@SebastienMelki SebastienMelki left a comment

Choose a reason for hiding this comment

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

Nice work @rayanhabib07 — the ghost-based drag is a significant UX upgrade and the code is well-structured. Koala covered the main issues thoroughly; here are a few additional things I spotted:

Bug — ghost & indicator leak in the DOM

In onMouseUp (panel-layout.ts), the fade-out cleanup has a closure bug:

if (ghostEl) {
    ghostEl.style.opacity = '0';
    setTimeout(() => ghostEl?.remove(), 200);  // ← closure captures the variable
    ghostEl = null;                             // ← variable is null before timeout fires
}

The arrow function closes over the ghostEl variable, not its current value. By the time the 200ms timeout fires, ghostEl is already null, so ?.remove() is a no-op. The invisible DOM node stays in the document forever — one orphan per drag. Same issue with dropIndicator right below it.

Fix: capture the reference before nulling:

if (ghostEl) {
    ghostEl.style.opacity = '0';
    const g = ghostEl;
    setTimeout(() => g.remove(), 200);
    ghostEl = null;
}

Missing Escape-to-cancel

There's no way to abort a drag mid-flight. If a user starts dragging and wants to bail, they have to drop it somewhere and then undo. Adding an Escape keydown listener during drag that cleans up the ghost/indicator and restores the original position would be a nice touch (non-blocking, but worth considering).

Drag on panels with panel-content click targets

onMouseDown correctly bails if target.closest('button, a, input, select, textarea, .panel-content') — but the drag handle is the panel header area. Just double-checking: this means users can only grab panels by their header bar, right? That's fine UX-wise, just want to confirm it's intentional since the older code may have allowed dragging from anywhere on the panel.

Everything else looks clean — the close button change is minimal and correct (closable: true), the event listener cleanup in destroy() is solid, and the CSS animations are tasteful. Great first contribution, looking forward to seeing the fixes! 🤘

@vercel
Copy link

vercel bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Mar 14, 2026 4:13am

Request Review

Copy link
Collaborator

@SebastienMelki SebastienMelki left a comment

Choose a reason for hiding this comment

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

Reviewed the fix commit (71a1ce7) against all feedback from both reviews — everything has been addressed:

Koala's blocking items (1–3): all fixed

  • Cmd+Z now guards against INPUT/TEXTAREA/contentEditable before calling preventDefault
  • cloneNode strips iframes from the ghost to avoid duplicate network requests
  • Drop logic replaced with swapElements() which avoids the ghost-center offset bug entirely

Koala's should-fix items (4–8): all fixed

  • Undo stack capped at 20 entries
  • performUndo() now calls fetchData() on the restored panel
  • .panel-drop-indicator styles moved from inline JS to CSS class
  • Redundant pointerEvents assignment replaced with save/restore pattern
  • docs/DRAG_SYSTEM_IMPROVEMENTS.md deleted

Koala's minor items (9–10): fixed (blank line removed, max-items comment added)

Seb's items: all fixed

  • Ghost/indicator DOM leak fixed — reference captured before nulling (const g = ghostEl; setTimeout(() => g.remove(), 200))
  • Escape-to-cancel added with full cleanup (ghost, indicator, target highlight, original position restore, listener removal, RAF cancellation)

⚠️ Merge conflict — the PR currently conflicts with main, likely from the recent webcam panel refactor (#1561, #1562). @rayanhabib07 please rebase on latest main to resolve before merging.

LGTM on the code side — nice work on the fixes!

@koala73
Copy link
Owner

koala73 commented Mar 14, 2026

This was replaced by #1550

@koala73 koala73 closed this Mar 14, 2026
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.

Issue: No X buttons on Live News or Live Webcam Blocks

3 participants