Repository navigation
feat(web): graph legend and rich node hover cards - #88
Merged
Merged
Conversation
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>
mattevans
approved these changes
Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Why
The graph's vocabulary couldn't be self-taught: canonical and fork blocks are both labeled
VALIDwith 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
NODE_STYLEStable.2xlwhere 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.smup.Hover cards
Built on
@floating-ui/react(new direct dependency, pinned0.26.28to dedupe with the copy Headless UI v2 already bundles — Headless UI deliberately ships no tooltip, and its Popover is click-trigger by design):safePolygon(pointer can travel onto the card), keyboard-focus trigger,Escapedismiss, pointer-only (touch keeps opening the slide-over directly). Meets WCAG 1.4.13 (hoverable / dismissible / persistent).eschint.1199.33T) with the exact value on thetitleattribute.role="button", Enter/Space activates) and the truncation marker's nativetitletooltip is folded into the same card system.Refactor
NODE_STYLESwas duplicated verbatim inWeightedNodeandAggregatedNode; it now lives once inutils/nodeStyles.tsand 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
eslint-disable react-hooks/refscomments inHoverCard.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.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).2xldrop-down breakpoint assumes the playback dock keeps its current width; if fields are added toControl, the threshold may need a nudge.docs: screenshotscommit 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) andpnpm buildall green.🤖 Generated with Claude Code