Skip to content

feat(web): graph legend and rich node hover cards - #88

Merged
Savid merged 2 commits into
masterfrom
feat/graph-legend-hover-cards
Jun 11, 2026
Merged

Savid merged 2 commits into
masterfrom
feat/graph-legend-hover-cards

Conversation

@Savid

@Savid Savid commented Jun 11, 2026

Copy link
Copy Markdown
Member

What

Two new affordances for reading the fork-choice graph's visual vocabulary, both driven by a single shared style table so they can never drift from the nodes:

  1. Stage legend — a collapsible glass pill bottom-left of the stage that expands into a panel explaining every node state, the progress-ring semantics and (in aggregated view) the counter glyphs.
  2. Node hover cards — rich, animated hover/focus cards on every graph node and truncation marker, filling the gap between "hover = brightness bump" and "click = full slide-over dialog".

Why

The graph's vocabulary couldn't be self-taught: canonical and fork blocks are both labeled VALID with only hue distinguishing them, detached shares invalid's red, and the progress ring means weight share in single-node view but source agreement in aggregated view. And the only way to inspect a block was the heavyweight slide-over.

Legend

legend desktop

  • Collapsed pill (six status dots) by default; expands to a panel matching the Sources-panel chrome. Swatches are miniature real nodes rendered from the shared NODE_STYLES table.
  • Mode-aware: weighted vs aggregated row descriptions, ring-meaning footnote, and the ✓/⚑/👁 counter explanations only in aggregated view. Plus a row for the dashed truncation marker.
  • Anchoring: clears the floating playback dock (97px strip + ~66px dock; 138px strip at xl with the epoch dial) and flows down beside the dock from 2xl where the corner is wide enough (the dock is a constant ~886px centered, so its left edge clears the panel at ≥1542px). BYO/snapshot footers are flat strips, so the legend sits low there at all widths.
  • Mobile: compact swatch+name-only panel — prose rows appear from sm up.

legend mobile

Hover cards

Aggregated view (light) Single-node view (dark)
aggregated hover card weighted hover card

Built on @floating-ui/react (new direct dependency, pinned 0.26.28 to dedupe with the copy Headless UI v2 already bundles — Headless UI deliberately ships no tooltip, and its Popover is click-trigger by design):

  • Interaction: 150ms-delay hover with safePolygon (pointer can travel onto the card), keyboard-focus trigger, Escape dismiss, pointer-only (touch keeps opening the slide-over directly). Meets WCAG 1.4.13 (hoverable / dismissible / persistent).
  • Rendering: portal-rendered so the card never scales with the graph zoom, re-anchored every animation frame so it follows its node while panning; 180ms ease-out fade/rise entrance with an arrow tying the card to its node.
  • Chrome: hairline top bar + soft halo in the node's status ring color.
  • Content: status header with slot/epoch chips, full block root with a copy button, stat tiles (weight + branch-share meter, or canonical/seen source counts), validity chip, checkpoint votes and not-valid/optimistic/detached counts, parent root, and an esc hint.
  • Mainnet-scale numbers: slot/epoch chips wrap instead of overflowing, and 15+ digit weights display compact notation (1199.33T) with the exact value on the title attribute.
  • Graph nodes are now keyboard-focusable (role="button", Enter/Space activates) and the truncation marker's native title tooltip is folded into the same card system.

Refactor

NODE_STYLES was duplicated verbatim in WeightedNode and AggregatedNode; it now lives once in utils/nodeStyles.ts and feeds the nodes, the legend swatches and the card accents.

Storybook

  • Components/Legend: both modes expanded, collapsed pill, expand/collapse play test, aggregated-counters assertions, iPhone-viewport stories.
  • Components/Graph Primitives: static card galleries for every weighted/aggregated state (rendered in the real card chrome) including a mainnet-scale variant, plus pointer-interaction stories (hover opens, Escape dismisses) for nodes and truncation markers.
  • Parts/Graph: legend expand/collapse in both view modes and a hover-card test against the live graph head.

Notes for reviewers

  • Three targeted eslint-disable react-hooks/refs comments in HoverCard.tsx: the rule's heuristic misfires on floating-ui's stable callback refs (refs.setReference/setFloating/arrow({element})), which are designed for render-time attachment.
  • The cards keep role="tooltip" despite the copy button; strict ARIA would call an interactive hover surface a dialog, but full focus management felt disproportionate for a progressive-enhancement button (keyboard users have the slide-over).
  • The legend's 2xl drop-down breakpoint assumes the playback dock keeps its current width; if fields are added to Control, the threshold may need a nudge.
  • The docs: screenshots commit only adds the images referenced above — drop it before merge if you don't want them in history.

pnpm lint, pnpm typecheck, pnpm test (125 unit + 98 storybook incl. the new interaction tests) and pnpm build all green.

🤖 Generated with Claude Code

Savid and others added 2 commits June 11, 2026 13:33
Add the two missing affordances for reading the fork-choice graph's
visual vocabulary: a stage legend and rich hover cards on graph nodes.

Legend (components/Legend.tsx):
- Collapsed glass pill (six status dots) bottom-left of the stage;
  expands into a panel matching the Sources panel chrome.
- Mode-aware rows: weighted vs aggregated descriptions, ring-meaning
  footnote (weight share vs source agreement) and the check/flag/eye
  counter glyph explanations (aggregated only), plus the dashed
  truncation marker row.
- Mobile: compact swatch+name-only panel; prose appears from sm up.
- Anchoring clears the floating playback dock (97px strip + ~66px dock,
  138px strip at xl) and drops down beside it from 2xl where the
  bottom-left corner is wide enough; BYO/snapshot footers are flat so
  the legend sits low there at all widths.

Hover cards (components/HoverCard.tsx, components/NodeCards.tsx):
- New @floating-ui/react primitive (pinned 0.26.28 to dedupe with the
  copy Headless UI v2 already ships): 150ms-delay hover with
  safePolygon, keyboard-focus trigger, Escape dismiss, pointer-only
  (touch keeps opening the slide-over), portal-rendered so the card
  never scales with graph zoom, re-anchored every animation frame so it
  follows nodes while panning, 180ms ease-out fade/rise entrance and an
  arrow tying the card to its node.
- Card content per primitive: weighted (weight + branch-share meter,
  validity chip, parent root), aggregated (canonical/seen stat tiles,
  checkpoint votes, not-valid/optimistic/detached counts) and the
  truncation marker (replaces its native title tooltip).
- Status-accented chrome: hairline top bar + soft halo in the node's
  status ring color.
- Mainnet-scale numbers stay clean: slot/epoch render as wrap-friendly
  chips and 15+ digit weights display compact notation (393.23T) with
  the exact value on the title attribute.
- Graph nodes are now keyboard-focusable (role=button, Enter/Space
  activates, focus shows the card).

Shared plumbing:
- utils/nodeStyles.ts: the NODE_STYLES table (previously duplicated in
  WeightedNode and AggregatedNode) is now the single source of truth
  for status visuals; legend swatches and card accents render from it.
- Graph passes slot/epoch/parentRoot through to the cards.

Storybook: new Legend stories (modes, expand/collapse, mobile
viewports), static card galleries for every node state incl. a
mainnet-scale variant, and pointer-interaction stories (hover to open,
Escape to dismiss) for nodes and truncation markers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Referenced from the PR description; safe to drop before merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Savid
Savid merged commit f39f935 into master Jun 11, 2026
5 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.

2 participants