feat(ui): copy the flow or the map as an image, for a PR comment or a README (CG-55)

"Copy image" and "Download SVG" on the Flow strip's header and in the Map's
side panel. The image is the distribution loop: a flow pasted into a review, a
map pasted into a README, read by somebody with no viewer open.

The exporter serialises the LAYOUT OBJECT rather than scraping the DOM — no
html-to-image, no foreignObject, no new dependency. buildFlowLayout and
buildMapLayout already compute every rectangle, port and curve before a
component renders, so the image and the screen come from one piece of
arithmetic and cannot drift apart, and the whole exporter is a pure function a
test runs with no browser. Output is presentation-only SVG (rect, line, path,
polygon, text, tspan, clipPath) — no script, no external reference, no data:
URL — which is what GitHub's sanitiser accepts in a README.

Light theme is forced whatever the viewer is set to: a dark strip on GitHub's
white comment background reads as a mistake, not a preference. 24px of paper
around the drawing, a caption naming the path or the root at the bottom left,
a CodeGraph mark at the bottom right.

Fonts travel as family stacks, not bytes (spec). An SVG loaded as an image may
not fetch a webfont, so a raster falls back to the platform's own monospace —
every fallback in the stack advances at ~0.6em like IBM Plex Mono, so the code
grid survives and only the letterforms change. Text is truncated
arithmetically with an ellipsis and clipped as well, so a wider fallback
cannot spill a source line out of a card.

`scale` multiplies only the root width/height while the viewBox stays in CSS
pixels, so the raster draws an image whose intrinsic size is already 2x
instead of upscaling a 1x bitmap. The clipboard write uses the ClipboardItem
promise form (Safari discards the gesture across an await) and falls back to
downloading the PNG, saying which happened rather than claiming a copy it did
not make.

Measured on this repo: execute -> rowToFileRecord (8 hops) exports 3690x253
CSS px, 491 kB PNG at 2x / 38 kB SVG; the 16-module map reproduces the canvas
exactly — 16 boxes, 52 links, 9 layer rules, both band labels, and with
src/index.ts selected 15 links and 4 dimmed boxes.
This commit is contained in:
Colby McHenry
2026-08-27 05:41:34 -05:00
parent 94f4e287e6
commit 8ac0138940
12 changed files with 1837 additions and 1 deletions
+4
View File
@@ -46,6 +46,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Nothing polls: the viewer watches for these two things and is told about them. If it loses touch with the server it retries a few times with a growing delay, then stops and says "Not live" in the top bar rather than hammering a port that isn't answering.
- **Take a flow or a map with you: copy it as an image, or save it as an SVG.** The Flow strip and the Map both gained **Copy image** and **Download SVG**. Copy image puts a PNG on your clipboard, ready to paste into a pull-request comment or a chat — the fastest way to say "here is what your change actually touches" without asking anyone to install anything. Download SVG saves a file for a README: it is real text rather than a bitmap, so it stays sharp at any size and the symbol names in it are selectable.
Both render the light theme whichever one you are reading in, because the image is going to be read on somebody else's screen, and both carry a caption saying what the picture is. What comes out is exactly what is on screen — the same hops, the same dashed dynamic-dispatch links with their wiring sites, the same "where the graph stops" block, the same modules dimmed or brought forward by your selection — because the image is drawn from the same measurements the screen is, not photographed off it. An eight-hop strip comes out around half a megabyte, well inside what GitHub takes inline.
- **"Where does anything start?" has a screen now.** The **Entry points** tab in `codegraph ui` (or press `e`) is the first thing worth opening on a codebase you have never seen. Every route with the symbol that serves it and the `file:line` you will find it at, grouped by the file the URL is registered in — your router, not your handlers — and headed with the framework CodeGraph detected it from. Under that, the files that actually *do* something when they load (a CLI, a worker entry, a build script), the tests ranked by how much of the project each one exercises, and the symbols the most code depends on.
None of it is guessed from a filename: a file "runs something" because the graph recorded a call from the file itself, and a project with fewer than three routes simply has no Routes section rather than an empty one. Every list says how much of itself it is showing, and says "at least" wherever the real total can only be a floor.