Skip to content

fix(grunt/tui): #N ticket-hover — shadcn-svelte HoverCard parity (anchored, delayed, sticky)#16

Merged
terrxo merged 1 commit into
devfrom
grunt-fix/ticket-hover-shadcn-style
May 27, 2026
Merged

fix(grunt/tui): #N ticket-hover — shadcn-svelte HoverCard parity (anchored, delayed, sticky)#16
terrxo merged 1 commit into
devfrom
grunt-fix/ticket-hover-shadcn-style

Conversation

@terrxo

@terrxo terrxo commented May 27, 2026

Copy link
Copy Markdown

Closes hivemind anomalyco#233 properly. Nik feedback after PR #15: 'works but I'd much rather want the thing that works like shadcn-svelte's hovercard'.

What changed vs PR #15

PR #15 used a fixed top-right screen anchor to dodge flicker. That gave reliable behavior but wrong UX. shadcn HoverCard parity = card pops up NEAR the trigger after a small open delay, STAYS open if cursor moves into the card body, closes after a brief leave delay.

Implementation

Hivemind context owns the state machine:

  • hoveredTicket signal: { id, anchorX, anchorY } | null
  • triggerHoverEnter(id, x, y) — schedules open after 150ms at anchor coords
  • triggerHoverLeave() — schedules close after 200ms (cursor needs time to traverse gap)
  • setCardHovered(bool) — called by the card's own onMouseOver/Out. true cancels close-timer; false restarts it.

Sticky behavior: trigger leave → close-timer starts → cursor reaches card → setCardHovered(true) cancels timer → card stays. Cursor leaves card → setCardHovered(false) restarts timer → close.

TicketHoverCard

  • Anchors at left=anchorX+1, top=anchorY+1 (offset clears the trigger column → no flicker)
  • Right-edge overflow guard: flips so right edge aligns with cursor if card would push off
  • Bottom-edge guard: flips above cursor if near terminal bottom
  • Own onMouseOver/Out wired to setCardHovered

Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.f86e6640f.dirty.

…hored near trigger, open/close delays, sticky)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #15: 'works but I'd much
rather want the thing that works like shadcn-svelte's hovercard'.

## What changed vs PR #15

PR #15 moved the tooltip to a fixed top-right screen anchor to fix the
flicker. That worked but produced wrong UX — Nik wanted shadcn HoverCard
ergonomics: card pops up NEAR the trigger after a small open delay,
STAYS open if cursor moves into the card body (close delay gives time to
traverse the gap), closes after a brief delay so a small detour doesn't
dismiss it.

## How shadcn-style is achieved

Hivemind context now owns:
- hoveredTicket signal: { id, anchorX, anchorY } | null
- triggerHoverEnter(id, x, y) — clears any pending close-timer, schedules
  open after OPEN_DELAY_MS (150ms) at the anchor coords
- triggerHoverLeave() — cancels pending open if still scheduled; otherwise
  schedules close after CLOSE_DELAY_MS (200ms) so cursor can traverse to
  the card
- setCardHovered(bool) — called by the card's own onMouseOver/Out. true
  cancels any pending close timer; false starts one.

Sticky behavior emerges naturally: trigger leave fires close-timer →
within 200ms cursor reaches card body → setCardHovered(true) cancels the
timer → card stays. When cursor leaves card → setCardHovered(false)
restarts the timer → card closes after 200ms.

## TicketRef changes

onMouseOver now passes evt.x, evt.y to hive.triggerHoverEnter() so the
card knows where to anchor. onMouseOut calls hive.triggerHoverLeave().
Click handler unchanged.

## TicketHoverCard changes

- Reads anchor coords from hive.hoveredTicket()
- Positions just below + slightly right of the cursor (left=anchorX+1, top=anchorY+1)
- Right-edge overflow guard: if card would push off the right, flips so its right
  edge aligns at the cursor.
- Bottom-edge guard: flips above the cursor if near terminal bottom.
- Adds onMouseOver={hive.setCardHovered(true)} + onMouseOut={hive.setCardHovered(false)}
  for the sticky behavior.

## Why no flicker even though card is near trigger

PR #15 was right that a per-ref tooltip overlapping its own text caused flicker.
This PR keeps the single-instance shared-card pattern but moves the position from
fixed top-right back to anchored-near-cursor. The flicker risk only re-emerges if
the card overlapped the trigger text — but we offset by +1 in both axes, so the
trigger column is always clear. And the sticky-hover state machine handles the
gap traversal cleanly.

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.f86e6640f.dirty.
@terrxo terrxo merged commit f9cad9d into dev May 27, 2026
@terrxo terrxo deleted the grunt-fix/ticket-hover-shadcn-style branch May 27, 2026 00:49
@github-actions

Copy link
Copy Markdown

Hey! Your PR title fix(grunt/tui): #N ticket-hover — shadcn-svelte HoverCard parity (anchored, delayed, sticky) doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

terrxo added a commit that referenced this pull request May 27, 2026
… no character bleed (#17)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #16: 'there is some
gibberish being added in to text' with a screenshot showing the title
rendered as '##nNikrdirective...' (jumbled) and the scope as
'clicktto,open inrhivemind-ui closed the session before' (with extra
letters and the footer-line text bleeding into the scope).

## Root cause

Three compounding issues in the card body:

1. **Multi-line scope text with wrapMode='word'** — opentui's text renderable
   joins adjacent <text> siblings when wrap-word is enabled, producing
   character bleed across line boundaries. The actual title was correct
   in the api response; the renderer was conflating it with the scope's
   first line plus the footer ('click to open in hivemind-ui') text.

2. **Markdown chars in scope** — full scope text contains markdown
   ('## Nik directive 2026-05-26T23:01Z (recovered from ses_...)') which
   the plain-text renderable was passing through verbatim, then word-wrap
   ate the spaces around '#' and '##'.

3. **<>Fragment</> wrapping the inner content** — Fragments don't
   establish a layout box; the renderable was free to flow children in
   weird ways. The position-absolute outer box wasn't enough to constrain
   line breaks per-text.

## Fix

- Wrap inner content in <box flexDirection='column'> so each <text> is
  one explicit line.
- Drop wrapMode='word' from the body; let opentui's natural per-text
  layout handle line bounds.
- Pre-flatten the scope: strip code fences, headings, emphasis markers,
  and collapse whitespace to single spaces. This makes the preview a
  proper one-paragraph blurb instead of attempting to render markdown
  inside a 60-col tooltip.
- Compute title / meta / scope-preview as plain strings before render.
- Drop the <b> wrapper around title (use attributes={1} on <text> instead,
  matching the pattern in routes/session/sidebar.tsx).

## Verified

bun typecheck clean. Local install reports
1.15.10-grunt.7+local.f9cad9dea.dirty.
terrxo added a commit that referenced this pull request May 28, 2026
…hored near trigger, open/close delays, sticky) (#16)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #15: 'works but I'd much
rather want the thing that works like shadcn-svelte's hovercard'.

## What changed vs PR #15

PR #15 moved the tooltip to a fixed top-right screen anchor to fix the
flicker. That worked but produced wrong UX — Nik wanted shadcn HoverCard
ergonomics: card pops up NEAR the trigger after a small open delay,
STAYS open if cursor moves into the card body (close delay gives time to
traverse the gap), closes after a brief delay so a small detour doesn't
dismiss it.

## How shadcn-style is achieved

Hivemind context now owns:
- hoveredTicket signal: { id, anchorX, anchorY } | null
- triggerHoverEnter(id, x, y) — clears any pending close-timer, schedules
  open after OPEN_DELAY_MS (150ms) at the anchor coords
- triggerHoverLeave() — cancels pending open if still scheduled; otherwise
  schedules close after CLOSE_DELAY_MS (200ms) so cursor can traverse to
  the card
- setCardHovered(bool) — called by the card's own onMouseOver/Out. true
  cancels any pending close timer; false starts one.

Sticky behavior emerges naturally: trigger leave fires close-timer →
within 200ms cursor reaches card body → setCardHovered(true) cancels the
timer → card stays. When cursor leaves card → setCardHovered(false)
restarts the timer → card closes after 200ms.

## TicketRef changes

onMouseOver now passes evt.x, evt.y to hive.triggerHoverEnter() so the
card knows where to anchor. onMouseOut calls hive.triggerHoverLeave().
Click handler unchanged.

## TicketHoverCard changes

- Reads anchor coords from hive.hoveredTicket()
- Positions just below + slightly right of the cursor (left=anchorX+1, top=anchorY+1)
- Right-edge overflow guard: if card would push off the right, flips so its right
  edge aligns at the cursor.
- Bottom-edge guard: flips above the cursor if near terminal bottom.
- Adds onMouseOver={hive.setCardHovered(true)} + onMouseOut={hive.setCardHovered(false)}
  for the sticky behavior.

## Why no flicker even though card is near trigger

PR #15 was right that a per-ref tooltip overlapping its own text caused flicker.
This PR keeps the single-instance shared-card pattern but moves the position from
fixed top-right back to anchored-near-cursor. The flicker risk only re-emerges if
the card overlapped the trigger text — but we offset by +1 in both axes, so the
trigger column is always clear. And the sticky-hover state machine handles the
gap traversal cleanly.

## Verified

bun typecheck clean. Local install reports 1.15.10-grunt.7+local.f86e6640f.dirty.
terrxo added a commit that referenced this pull request May 28, 2026
… no character bleed (#17)

Refs hivemind anomalyco#233. Closes Nik feedback after PR #16: 'there is some
gibberish being added in to text' with a screenshot showing the title
rendered as '##nNikrdirective...' (jumbled) and the scope as
'clicktto,open inrhivemind-ui closed the session before' (with extra
letters and the footer-line text bleeding into the scope).

## Root cause

Three compounding issues in the card body:

1. **Multi-line scope text with wrapMode='word'** — opentui's text renderable
   joins adjacent <text> siblings when wrap-word is enabled, producing
   character bleed across line boundaries. The actual title was correct
   in the api response; the renderer was conflating it with the scope's
   first line plus the footer ('click to open in hivemind-ui') text.

2. **Markdown chars in scope** — full scope text contains markdown
   ('## Nik directive 2026-05-26T23:01Z (recovered from ses_...)') which
   the plain-text renderable was passing through verbatim, then word-wrap
   ate the spaces around '#' and '##'.

3. **<>Fragment</> wrapping the inner content** — Fragments don't
   establish a layout box; the renderable was free to flow children in
   weird ways. The position-absolute outer box wasn't enough to constrain
   line breaks per-text.

## Fix

- Wrap inner content in <box flexDirection='column'> so each <text> is
  one explicit line.
- Drop wrapMode='word' from the body; let opentui's natural per-text
  layout handle line bounds.
- Pre-flatten the scope: strip code fences, headings, emphasis markers,
  and collapse whitespace to single spaces. This makes the preview a
  proper one-paragraph blurb instead of attempting to render markdown
  inside a 60-col tooltip.
- Compute title / meta / scope-preview as plain strings before render.
- Drop the <b> wrapper around title (use attributes={1} on <text> instead,
  matching the pattern in routes/session/sidebar.tsx).

## Verified

bun typecheck clean. Local install reports
1.15.10-grunt.7+local.f9cad9dea.dirty.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant