Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

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

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 subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant