Skip to content

feat(board): preview the dragged card at its insertion point#3532

Merged
Sayt-0 merged 2 commits into
docker:mainfrom
dgageot:feat/board-drag-ghost-preview
Jul 8, 2026
Merged

feat(board): preview the dragged card at its insertion point#3532
Sayt-0 merged 2 commits into
docker:mainfrom
dgageot:feat/board-drag-ghost-preview

Conversation

@dgageot

@dgageot dgageot commented Jul 8, 2026

Copy link
Copy Markdown
Member

When dragging a card in the board TUI, there was no visual indication of where the card would land. Drop targets highlight the destination column with a green border, but the exact insertion point — always the tail of the column — was invisible.

While a drag is in progress and a column is highlighted as the drop target, the column now renders a faded "ghost" clone of the dragged card at the tail position, exactly where the card will be appended on drop. The column's scroll window shifts temporarily to reveal that tail slot, giving the user a clear preview of the outcome. If the drag is cancelled the scroll is restored, leaving the column untouched. The ghost uses faded title and project text with a border in faded success green, matching the existing green column-highlight treatment.

A second commit hardens the feature against edge cases: terminals too small to show even one real card get a … N more line instead of a ghost-only view, the ghost is suppressed when the dragged card is already in the target column to handle mid-drag refresh races, and stale drag state is cleared at the top of handleClick to recover from lost release events. Tests cover tail-scroll behaviour, the tiny-terminal fallback, and duplicate-card suppression.

dgageot added 2 commits July 8, 2026 13:30
…target column

Renders a faded ghost clone of the dragged card at the tail of the drop-target
column so users can see where the card will land before releasing.

Assisted-By: claude-opus-4-5
@dgageot dgageot requested a review from a team as a code owner July 8, 2026 11:38

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟢 APPROVE

The ghost-preview feature is well-structured: the containsCard guard prevents double-rendering, the scroll-to-tail logic is correct for the common case, and the resetDrag refactor in tui.go is a clean improvement. Two minor edge cases in view.go are worth a quick look before merging.

Comment thread pkg/board/tui/view.go
if dropTarget {
if card := m.cardByID(m.dragCardID); card != nil {
ghost = m.renderGhost(card, innerWidth)
slots--

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Ghost preview hides all real cards when terminal fits exactly one card slot

When visibleSlots() returns 1 (the minimum, enforced by the max(..., 1) guard), slots-- drives it to 0. The subsequent computation then becomes:

scroll = max(len(cards)-0, 0)  // = len(cards)
end    = min(len(cards)+0, len(cards))  // = len(cards)

The render loop (for i := scroll; i < end) is empty, so every real card disappears and the column shows only header + rule + "… N more" + ghost. On a tall-enough terminal this never triggers (slots ≥ 2), but on a constrained terminal where only one card row fits, a drag-over causes the entire card list to vanish except the ghost.

A simple guard avoids the underflow:

if slots > 1 {
    slots--
    scroll = max(len(cards)-slots, 0)
}

That way a one-slot terminal still renders its single real card and skips the ghost, matching the PR description's intent ("terminals too small to show even one real card get a … N more line").

Comment thread pkg/board/tui/view.go
borderColor := darken(styles.BorderSecondary, 0.35)
switch {
case m.dragging && idx == m.dragCol && idx != m.selCol:
case dropTarget:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] Dropping idx != m.selCol lets dropTarget override the selected column's white border with green

The old condition was:

case m.dragging && idx == m.dragCol && idx != m.selCol:

The new dropTarget is:

m.dragging && idx == m.dragCol && !containsCard(cards, m.dragCardID)

There is no longer a guard preventing dropTarget from being true when the destination column is also the selected column (m.dragCol == m.selCol). Because case dropTarget: appears first in the border-color switch, a drag toward a column the user previously selected will show a green border instead of the white one they'd normally see.

This may well be intentional — the green ghost+border is more informative during a drag — but since the removal is silent and the old guard was explicit, it's worth a quick confirmation that this is the desired UX before merging.

@Sayt-0 Sayt-0 merged commit 1514ecb into docker:main Jul 8, 2026
11 of 13 checks passed
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