Skip to content

feat(web): on-canvas playback controls + carrot-click inspect with multi-target highlight#93

Merged
naorsabag merged 10 commits into
masterfrom
feat/carrot-click-opens-inspect
May 9, 2026
Merged

feat(web): on-canvas playback controls + carrot-click inspect with multi-target highlight#93
naorsabag merged 10 commits into
masterfrom
feat/carrot-click-opens-inspect

Conversation

@naorsabag
Copy link
Copy Markdown
Owner

@naorsabag naorsabag commented May 8, 2026

A connected set of UX changes around the canvas: replaces the old click-to-pin popup with a richer inspect-panel highlight, adds Restart/Prev/Next controls next to Play, and fixes a handful of regressions surfaced while testing the order-flow demo. 8 commits, each scoped to one concern.

What's in

Commit Summary
5625163 Carrot click → opens inspect panel + highlights the clicked data slice; removes the on-canvas label and the click-to-pin DataPopup overlay
97b49d0 Carrot click also pauses autoplay; highlight visual cranked up (was too subtle to read)
e88ad17 On-canvas playback panel: ⏮ Restart, ⏪ Prev, ▶/⏸ Play, ⏩ Next; lifts header Play button into the canvas
6d0ada8 Sync parent playing state on Restart/Prev/Next + goToStep replays cumulative effects (create/destroy/progress)
013792b Reset drill-down memo on manual scrub so re-traversal of a drill-down step actually re-fires
4e5cec5 Play after scrub plays the current step (was skipping it because the resume path required pauseOffsetMs > 0)
bee1034 Drill-down effect gates on playing + cancels pending timer on pause; stops scrub from queuing accidental drills
cb11823 Inspect highlight matches by (from, to, data) triplet — fixes broadcast (shared data ref) lighting up all targets, and parallel sub-step inspect making siblings disappear

Manual test of example-order-flow covers

  • Pause mid-step, click each multi-data carrot → only that block highlighted
  • Pause on broadcast step (order-service → db|cache) → click each target's carrot, only that target's section highlights
  • Pause on parallel step (api / authz → order-service) → click one carrot, both sub-flows still render, only the clicked one highlights
  • Press Next/Prev/Restart while playing → button label flips to ▶, animation freezes (no brief flicker)
  • Scrub past drill-down step → Play (continues past, no accidental drill) → cycle ends → Prev back → Play → drill-down fires when reached
  • goToStep now applies cumulative create:/destroy: effects so a destroyed node reappears when scrubbing back across its destroy step

Verification

  • npm run typecheck — clean
  • npm test — 40/40
  • npm run lint — 0 errors

🤖 Generated with Claude Code

Three coupled UX changes that simplify the carrot-click flow.

1. Drop the on-canvas data label that floated next to each carrot.
   It overlapped roads, doubled the visual noise vs. the inspect
   panel, and went stale fast at any zoom. Aria label remains for
   screen readers; canvas ergonomics rely on the inspect panel
   instead.

2. Drop the click-to-pin DataPopup overlay (and its file). Pinned
   popups duplicated the panel's content over the canvas and forced
   a separate close gesture. Removed:
   - DataPopup component file
   - pinnedEdge / setPinnedEdge state in FlowCanvas
   - handlePinPopup callback
   - edgeStepsById lookup map (only used by the popup)
   - onPaneClick that cleared pinnedEdge

3. Carrot click now opens the inspect panel on that step AND
   highlights the specific FlowData entry the carrot was carrying.
   Multi-data steps emit one carrot per data entry; without the
   highlight you can't tell which one was clicked. Implementation:
   - DataPixel.onPixelClick signature → (step, focusData?)
   - FlowCanvas wires it to onInspectStep
   - App.tsx + AppFragment.tsx track inspectedFocusData and pass it
     down; both reset when the flow changes
   - DataInspectionPanel accepts focusData; the matching DataBlock
     gets a brand-cyan left bar + soft tint and scrolls into view
     via scrollIntoView({block:'nearest', behavior:'smooth'})
   - handleInspectStep also opens the inspector (was previously a
     no-op when closed) so a click reliably surfaces the data.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors the data inspection workflow, replacing a pinned-popup overlay with an inline highlighting and scrolling mechanism. App and AppFragment track focus data state and pass it to the inspection panel, which uses it to highlight and auto-scroll to a specific data block within the inspected step. The old DataPopup component is removed, and DataPixel and FlowCanvas are updated to emit and relay focus data instead of coordinates.

Changes

Data Inspection Focus Refactor

Layer / File(s) Summary
Type and Prop Contracts
packages/web/src/components/DataPixel.tsx, packages/web/src/components/FlowCanvas.tsx, packages/web/src/App.tsx
DataPixel.onPixelClick and FlowCanvas.onInspectStep now accept optional focusData?: FlowData. App.tsx imports FlowData for new inspector state.
Removed Popup Component
packages/web/src/components/DataPopup.tsx
Entire DataPopup component, DataPopupProps interface, and navBtnStyle constant removed (144 lines).
Focus State Management
packages/web/src/App.tsx, packages/web/src/AppFragment.tsx
Both files add inspectedFocusData state and clear/reset it on step or flow/hash changes; handleInspectStep accepts focusData and stores/passes it to the panel.
Inspection Panel Highlighting
packages/web/src/components/DataInspectionPanel.tsx
Panel accepts focusData prop and forwards to StepBody. StepBody marks matching DataBlock entries with highlighted flag. DataBlock uses a ref and scrollIntoView() for smooth auto-scroll and renders highlighted styling (left border, tinted background).
Canvas Integration and Click Handling
packages/web/src/components/FlowCanvas.tsx, packages/web/src/components/DataPixel.tsx
FlowCanvas removes pinned-popup state and DataPopup render; both pixel overlay handlers now call onInspectStep(step, focusData). DataPixel computes focusData from its data context, removes on-canvas label element, preserves tooltip, sets aria-label, and updates click handler signature.
Playback Controls & Animation API
packages/web/src/components/FlowCanvas.tsx, packages/web/src/hooks/useFlowAnimation.ts
New on-canvas playback UI (restart/prev/play-pause/next) wired to restart, goToStep, and existing playback controls; useFlowAnimation now returns restart() and goToStep(idx) for manual scrubbing.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Pixel as DataPixel
  participant Canvas as FlowCanvas
  participant App
  participant Panel as DataInspectionPanel
  User->>Pixel: click visual pixel
  Pixel->>Pixel: derive focusData from dataOverride/step.data
  Pixel->>Canvas: onPixelClick(step, focusData)
  Canvas->>App: onInspectStep(step, focusData)
  App->>Panel: render DataInspectionPanel with focusData
  Panel->>StepBody: highlight matching DataBlock
  StepBody->>DataBlock: scrollIntoView() highlighted block
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • naorsabag/OpenHop#60: Both PRs modify the same components (packages/web/src/components/DataPixel.tsx and FlowCanvas.tsx) and their props/signatures (DataPixel props and FlowCanvas handlers).
  • naorsabag/openhop#90: Both PRs modify the same animation/interaction stack—DataPixel, FlowCanvas, and the flow animation hook—so focus-data and play/pause control changes are related.
  • naorsabag/openhop#77: Related fragment-mode changes to AppFragment/App.tsx inspection plumbing that this PR extends to carry a focused FlowData.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main changes: on-canvas playback controls and carrot-click inspection with data highlighting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/carrot-click-opens-inspect

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/components/DataPixel.tsx`:
- Around line 181-201: The interactive carrot is currently a non-focusable <div>
with an onClick handler (see the pixelRef element and its onClick that calls
onPixelClick), which prevents keyboard users from triggering the inspect flow;
make the element keyboard-accessible by adding tabIndex={0} and a semantic role
(e.g., role="button"), and wire a keyboard handler (onKeyDown or onKeyUp) that
invokes the exact same click logic when Enter or Space is pressed (reusing the
focusData calculation and onPixelClick call) and keep existing mouse handlers
(onClick, onMouseEnter/onMouseLeave) and aria-label intact so behavior is
identical across input methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bc332c7-7d96-472c-b003-e4a3bce5504e

📥 Commits

Reviewing files that changed from the base of the PR and between c46f181 and 5625163.

📒 Files selected for processing (6)
  • packages/web/src/App.tsx
  • packages/web/src/AppFragment.tsx
  • packages/web/src/components/DataInspectionPanel.tsx
  • packages/web/src/components/DataPixel.tsx
  • packages/web/src/components/DataPopup.tsx
  • packages/web/src/components/FlowCanvas.tsx
💤 Files with no reviewable changes (1)
  • packages/web/src/components/DataPopup.tsx

Comment thread packages/web/src/components/DataPixel.tsx
naorsabag added 3 commits May 8, 2026 10:02
Two follow-ups for the carrot-click → inspect-panel flow:

1. Pause autoplay when the click provides focusData. The user could
   click a carrot during autoplay and the focus highlight would land
   for ~1 frame before handleStepChange's setInspectedFocusData(null)
   nuked it on the next step advance. Result: clicks felt like they
   did nothing. Now a click both surfaces the data AND freezes the
   canvas so the user can read it.

2. Crank up the highlighted DataBlock's visual weight: 4px cyan bar
   (was 3px), 0.18 background opacity (was 0.08), and a soft cyan
   glow via box-shadow. Was too subtle to read against the panel's
   dark theme; users couldn't tell what changed.
… Next)

Move Play/Pause off the header and into a vertical panel on the
canvas's top-right edge, alongside three new controls:

- ⏮ Restart  — clears step pointer + node progress + active/destroyed
                node sets, snaps back to step -1, sets playing = false
- ⏪ Prev     — snap visualization to step (current - 1)
- ▶/⏸ Play  — toggle (lifted from the header)
- ⏩ Next    — snap to step (current + 1)

Implementation:
- useFlowAnimation gains `restart()` and `goToStep(idx)`. Both clear
  the timer chain, the phase ref, and the pause-offset ref before
  mutating state, so a scrub during autoplay won't get clobbered by
  a pending tick. `goToStep` is a snapshot — it doesn't replay
  cumulative create/destroy effects from earlier steps; pressing Play
  resumes correctly from there.
- FlowCanvas mounts a <Panel position="top-right"> with a small
  PlaybackButton helper. Disabled state for Prev/Next at the
  boundaries.
- Header Play button removed in App.tsx and AppFragment.tsx; both
  pass `onTogglePlay={() => setPlaying(p => \!p)}` to FlowCanvas.
…lative effects

Four issues from manual test of example-order-flow:

1. Play button stayed in Pause state after Restart/Prev/Next.
2. Pressing Prev/Next while paused briefly resumed the current step
   before snapping to the new one.

   Both same root cause: `goToStep`/`restart` only flipped the hook's
   internal `playing` to false; the parent App still had `playing =
   true`, so DataPixel received `paused={\!playing}` = false and kept
   animating. Fix: thread `onPause: () => setPlaying(false)` from
   App + AppFragment → FlowCanvas, and wrap each panel button's
   onClick to call onPause first. Now parent state matches the
   scrubbed visualization, button label flips correctly, and pixel
   freezes at the new step.

3. Drill-down step intermittently failed after using Prev/Next.
4. Create/destroy steps weren't undone when scrubbing — destroyed
   nodes stayed gone, created nodes stayed visible.

   Both same root cause: `goToStep` was a snapshot — it only updated
   activeStep/activeEdges, leaving nodeProgress/activeNodes/
   destroyedNodes from the previous run-through. Drill-down detection
   reads node state, so stale state broke it. Fix: `goToStep` now
   replays steps 0..idx CUMULATIVELY — resets all three refs, then
   loops applying create/destroy/progress effects so the state
   matches what autoplay would produce if you'd watched up to that
   step.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/web/src/components/FlowCanvas.tsx (1)

619-622: 💤 Low value

Comment contradicts the implementation.

The comment states "without replaying cumulative create/destroy effects" but goToStep in useFlowAnimation.ts explicitly replays all effects from step 0 to the target index:

for (let i = 0; i <= idx; i++) applyStepEffects(i)

This could mislead future maintainers. The cumulative replay is actually the correct behavior as it ensures node visibility state matches what autoplay would have produced.

📝 Suggested fix
-        {/* Playback controls — right side, vertical column. Restart rewinds
-            to step -1 with all progress cleared; Prev/Next snap the
-            visualization to the adjacent step (without replaying cumulative
-            create/destroy effects — press Play to advance from there). */}
+        {/* Playback controls — right side, vertical column. Restart rewinds
+            to step -1 with all progress cleared; Prev/Next snap the
+            visualization to the adjacent step, replaying cumulative
+            create/destroy effects so node visibility stays consistent. */}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/components/FlowCanvas.tsx` around lines 619 - 622, The
comment in FlowCanvas.tsx is incorrect about goToStep's behavior; update the
comment to state that goToStep (in useFlowAnimation.ts) replays cumulative
create/destroy effects by iterating applyStepEffects from 0 to idx so the
node/edge visibility matches what autoplay would produce, rather than snapping
without replaying effects—mention goToStep and applyStepEffects by name so
future maintainers can find the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/web/src/components/FlowCanvas.tsx`:
- Around line 619-622: The comment in FlowCanvas.tsx is incorrect about
goToStep's behavior; update the comment to state that goToStep (in
useFlowAnimation.ts) replays cumulative create/destroy effects by iterating
applyStepEffects from 0 to idx so the node/edge visibility matches what autoplay
would produce, rather than snapping without replaying effects—mention goToStep
and applyStepEffects by name so future maintainers can find the implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 02fe92a5-056d-4c75-8668-e227e0957bfc

📥 Commits

Reviewing files that changed from the base of the PR and between e88ad17 and 6d0ada8.

📒 Files selected for processing (4)
  • packages/web/src/App.tsx
  • packages/web/src/AppFragment.tsx
  • packages/web/src/components/FlowCanvas.tsx
  • packages/web/src/hooks/useFlowAnimation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/AppFragment.tsx

naorsabag added 4 commits May 8, 2026 14:53
After auto-firing the drill-down callback for step N, FlowCanvas
remembered N in lastDrilldownStepRef so the same step wouldn't fire
twice. But this memo lived across user-driven scrubbing too — Prev
back over a drill-down step then Next forward would land on N
again, the effect would see the memo == currentStepIndex, and
silently skip the drill on the second pass.

Reset the memo to -1 inside each panel button's onClick (Restart,
Prev, Next), so a re-traversal of a drill-down step always re-fires.
When Prev/Next/Restart called goToStep, phaseRef was reset to null
and pauseOffsetMs to 0. The next Play hit the useEffect's
fresh-start branch, which fires advanceStep() — and advanceStep
increments stepIndexRef before applying state, so the step the
user just scrubbed to was skipped (step N+1 → advanceStep → N+2).

Two changes:
- goToStep now sets phaseRef = 'pixel-active' (we're showing the
  step's pixel-active visualization, paused at progress 0).
- The Play useEffect's resume guard drops the `offset > 0` check.
  We re-enter the captured phase whenever phaseRef is set —
  offset = 0 just means "start of phase", which is the right thing
  for both pause-mid-step (offset captured) and post-scrub plays.
The drill-down useEffect didn't gate on `playing`. Two failure
modes:

1. Scrubbing past a drill-down step queued a 1500ms timer for that
   step. The next Next-click changed the activeStep, the effect
   re-ran, but the new step's branch returned early WITHOUT
   canceling the pending timer — so the drill fired anyway, ~1.5s
   later, in the middle of scrubbing.

2. After that accidental drill, lastDrilldownStepRef was set to the
   drill-down index. When the user later did Prev back to before
   the drill and pressed Play, autoplay reached the drill step but
   the memo matched and the effect skipped scheduling.

Fix:
- Effect now early-returns when \!playing, and explicitly
  clearTimeout on the pending drill-down timer in that branch (so
  pause + scrub kills any in-flight queued drill).
- `playing` added to the effect deps so flipping play/pause
  re-runs the gate.

Verified end-to-end: scrub past drill → Play → cycle end → Prev
back → Play → drill-down fires when the step is reached.
Two related carrot-click bugs:

1. Broadcast steps (e.g. `from: order-service, to: [db, cache], data: {…}`)
   share one data object reference across all targets. Clicking the
   carrot to `db` would highlight BOTH the db and cache sections
   because `d === focusData` matched both — same object, both targets.

2. Parallel steps (`parallel: [{from: api, to: order-service, data: …},
   {from: authz, to: order-service, data: …}]`) inspected the clicked
   sub-step rather than the parent. Result: clicking one carrot made
   the sibling sub-flow disappear from the panel entirely instead of
   leaving it visible with the clicked one highlighted.

Both fixed by replacing the `focusData: FlowData` argument with a
`focus: { from?, to?, data? }` triplet:

- DataPixel.onPixelClick simplified to (focusData?: FlowData) — the
  pixel only knows its own data slice; FlowCanvas layers in the
  edge identity (fromId, toId of edgeFlow) so the panel can match
  the click to a specific (target, data) pair.
- FlowCanvas now passes `animState.activeStep` (the parent) as the
  inspect step + the click's edge from/to. Manual click-to-fire
  pixels store `inspectStep / fromId / toId` on the ManualPixel so
  they preserve the parent context after the autoplay state moves.
- DataInspectionPanel highlights a DataBlock only if the section's
  (from, to) matches focus AND the data ref matches focus.data.

Verified end-to-end:
- Multi-data on a single edge (charge request / order context) —
  click highlights exactly one
- Broadcast (persist order → db|cache) — click highlights only the
  clicked target's section
- Parallel (api / authz → order-service) — both sub-flows remain
  visible, only the clicked one highlighted
@naorsabag naorsabag changed the title feat(web): carrot click opens inspect panel + highlights its data slice feat(web): on-canvas playback controls + carrot-click inspect with multi-target highlight May 9, 2026
naorsabag added 2 commits May 9, 2026 18:09
Adds role="button", tabIndex=0, and onKeyDown(Enter/Space) so
keyboard users can fire the inspect-panel flow that the new
mouse-only onClick introduced. Extracts the click logic into
`emitPixelClick` so mouse + keyboard share one path.

Per CodeRabbit review on PR #93.
CI on f24e09a hit two failures:

1. Prettier formatcheck — DataInspectionPanel.tsx and FlowCanvas.tsx
   had local style drift from the multi-prop callback edits in
   cb11823. Ran `prettier --write` on both.

2. npm audit (prod tree) — fast-uri <=3.1.1 had a pair of newly
   disclosed CVEs (path traversal via percent-encoded dot segments,
   host confusion via percent-encoded authority delimiters). Pulled
   in transitively from fastify@5.8.5 / @fastify/swagger / ajv.
   `npm audit fix` bumped to 3.1.2 cleanly — package-lock-only
   change, no direct deps touched.
@naorsabag naorsabag merged commit 8a270bd into master May 9, 2026
7 checks passed
naorsabag added a commit that referenced this pull request May 9, 2026
* chore: bump @openhop/web 0.1.0-beta.1 → 0.1.0-beta.2 and openhop CLI 0.1.0-beta.3 → 0.1.0-beta.4

Web bumps for the canvas/UI work since beta.1:
- pause behavior fix + label overflow + back-edge corridor (#90)
- carrot click opens inspect with multi-target highlight + on-canvas
  playback controls (#93)
- bookmark-style toggle tabs for sidebar + inspector (#95)
- preserve node progress on Play resume + drill-back (#96)
- shrink sprite SVGs by 46% (3.7MB → 2.0MB) (#97)

CLI bumps to ship the new web bundle (its only change is the pinned
@openhop/web dependency moving to 0.1.0-beta.2). @openhop/server
stays at 0.1.0-beta.1 — no changes since last publish.

* fix(cli): bump hardcoded --version to 0.1.0-beta.4

Test contract.test.ts asserts `openhop --version` matches the
package.json version. The bump in 9b373aa missed the duplicate
hardcoded string in src/index.ts:53. Sync them up so CI passes.
@naorsabag naorsabag deleted the feat/carrot-click-opens-inspect branch May 10, 2026 17:38
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.

1 participant