SIGN IN SIGN UP

fix(ui): lazy-load the diagram engine so a document without diagrams ships none of it; canvas controls never open a comment; ui 0.41.1 (#1562)

* refactor(ui): give svgContentSize its own dependency-free module

The document's fence block needs the rendered diagram's intrinsic size to
size its inline box, and nothing else of the canvas. Importing it from
DiagramCanvas dragged the zoom/pan surface, its viewport hook and lucide
into every closure that touched a diagram block. DiagramCanvas re-exports
it, so components/diagram/DiagramCanvas and the barrel still resolve it.

* perf(ui): load the diagram Source pane only when it opens

The pane is CodeMirror, and a viewer that can never open one -- every
fence in a Plannotator document today, since no host passes onSave --
carried the whole editor anyway. React.lazy behind the existing
hasPane && sourceOpen condition; the Suspense fallback is a box with the
pane's own class list rather than null, so the split it opens into is
already the right size and never collapses back onto the canvas.

A host that imports DiagramViewer directly still gets a working pane; it
arrives one chunk later.

* perf(ui): load the diagram popout only when it opens

The full-size popout is only ever reached by pressing Expand. React.lazy
with a null fallback: the overlay has simply not opened yet, so nothing
in the document flow moves.

The block's pending state (the source fence under "Rendering diagram…",
data-mermaid-pending and all) moves to its own dependency-free module,
because it is about to become the Suspense fallback the document shows
while the block's own chunk loads -- one pending state for both waits.

* perf(ui): load the diagram engine only for documents that have a diagram

Viewer imported MermaidBlock and GraphvizBlock statically, so every host
that reads a document shipped the renderer slot, the canvas, the comment
overlay, the finders and the projection whether or not the document had
a fence. Both blocks are now React.lazy, one chunk each over a shared
DiagramBlock chunk.

The Suspense fallback is the block's own pending state in the block's own
boxes (DiagramBlockPending), so the source fence paints with the document
and the two waits -- the block chunk, then the engine -- read as one.

* test(ui): pin the document-read closure of the Viewer entry

Bundles Viewer with the chunking bundler the portal build uses and walks
the entry chunk's static imports only (rollup reports imports and
dynamicImports separately, so a lazy edge is a chunk boundary by
construction). Asserts the entry closure reaches no CodeMirror, Source
pane, popout, canvas or projection, that each of those is still reachable
off-entry, and that the fence's pending state stays in the entry chunk.

Single-file builds inline everything and can never show this regression,
which is why it needs its own check.

* fix(ui): a press on the diagram canvas's own controls never comments on the part behind it

The zoom strip, the composer and the popout header are painted over the
canvas and are not in the svg, so the elementsFromPoint walk (node, then
edge, then cluster) stepped straight past them to whatever part sat
underneath: pressing Zoom out over a node opened the composer on that
node, and a press on the strip could start a pan.

A pointer event whose composed path contains a control surface now
resolves no target, opens no composer and starts no pan. Controls are
marked with data-diagram-control; buttons, toolbars, inputs, the
composer and the source pane count without marking.

Released over a control after a press that began on the canvas is
handled too: targetUnder answering null would otherwise read as
'comment on the whole diagram'.

* chore(ui): @plannotator/ui 0.41.1

Lazy diagram engine and the canvas-control fix. Core stays at 0.25.4:
nothing under packages/core moved, so 0.41.1 publishes alone.

Every export named in the 0.41.0 notes still resolves from the same path;
the diagram barrel additionally exports the new pending components and
the control predicate.

* test(ui): pin the diagram comment restored before its diagram mounts

Making MermaidBlock / GraphvizBlock lazy opens a window in which the
document has painted and the draft has restored but no diagram exists in
the DOM. A comment on a diagram part has no text to fall back on, so
anything that drops or mis-reports it in that window is data loss.

The test mounts Viewer (real lazy edges) beside AnnotationPanel with the
row already seeded, holds the engine open on a gated runtime loader, and
asserts the panel lists the row, nothing reports it unanchored while the
block is still pending, and the badge and mark appear once the engine
arrives -- no second restore pass, no reload.

Negative control: making Viewer's "no diagram in this document" report key
on what has MOUNTED rather than on the parse fails it on the unanchored
assertion.

* test(ui): pin the highlighter skip for a quoteless, block-less diagram row

A comment on a whole diagram (or a label-less part) carries an empty
originalText, and one posted by an agent that named no fence carries
blockId ''. That is exactly the shape the text restore pass treats as
unrestorable: findTextInDOM('') matches nothing and the blockId names
nothing. The diagramAnchor skip has to come first, or such a comment
returns from a reload wearing the "Unanchored" chip while the diagram
overlay is showing it perfectly well.

Negative control: dropping the skip reports both diagram rows as attempted.

* docs(ui): record the lazy-mount restore window in the 0.41.1 notes

A report of diagram comments lost across a reload did not hold -- the probe
behind it never answered the "Draft Recovered" modal and then counted an
un-restored session -- but the window it pointed at is real and is widened
by the lazy block wrappers. Name the three properties that keep it safe and
the tests that now pin each of them.
M
Michael Ramos committed
3a497cfb489075f59b89a417239cb226ce71117e
Parent: 144857f
Committed by GitHub <noreply@github.com> on 9/17/2026, 11:26:25 PM