mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
b00ddfa16d
* Pin Mermaid SVG sizing attrs into markup so they survive re-injection Mermaid 11.x emits the root <svg> with style="max-width: <natural-px>;" and no preserveAspectRatio / height attributes. The component fixed this via imperative setAttribute in a useEffect, but React's repeated dangerouslySetInnerHTML re-injections (4-5 times during mount) wipe the imperative attrs because the useEffect's `svg` state dep does not change. Result: short flowcharts (e.g. `X --> Y --> Z`) render at their natural 85x66 px "dot" size, long flowcharts overflow vertically past their container (~1667 px), and drag feels broken because pan-on-clipped-svg has nowhere to go. Bake max-width:none, preserveAspectRatio="xMidYMid meet", and height="100%" into the SVG markup string itself before setSvg(). Since the attributes live in the markup, every re-injection carries them along. The imperative useEffect is left in place as defense-in-depth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Memoize MermaidBlock to preserve zoomed viewBox across pinpoint hover After zooming a Mermaid diagram and toggling pinpoint mode on, hovering the viewer would reset the visible viewport back to the full diagram. The zoom ref / badge stayed at e.g. 150%, but the SVG snapped back to its natural extents. Root cause: the pinpoint hook calls setHoverTarget on every mousemove, which re-renders the parent Viewer, which re-runs MermaidBlock's dangerouslySetInnerHTML and replaces the live SVG with the cached markup string. The prior commit's markup normalization pins three sizing attrs, but viewBox is not in the markup -- zoom mutates viewBox via setAttribute, so each re-injection drops the zoomed value. Wrap MermaidBlock with React.memo and a custom comparator on block.id + block.content (matching HtmlBlock's convention). Upstream blocks come from useMemo([markdown]) in the editor App, so referential equality holds whenever content is unchanged -- memo skips the re-render and the live SVG keeps its zoomed viewBox across pinpoint mousemove storms. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Skip pinpoint targeting inside Mermaid grab area to keep drag clean In pinpoint mode, releasing a Mermaid pan-drag created a whole-block annotation. mouseup is followed by a click event, and the pinpoint overlay's click handler caught it and called highlighter.fromRange() on the entire Mermaid block. Separate annoyance: the dashed hover border drawn by the pinpoint overlay was visually distracting while the user was clearly trying to drag the diagram. Add data-pinpoint-ignore="" to the grab container div. usePinpoint's existing SKIP_SELECTORS in blockTargeting.ts already matches that attribute, so this is a one-attribute opt-out using a public extension point -- no changes to the pinpoint hook itself. Trade-off: Mermaid blocks no longer support whole-block-annotation via pinpoint. Annotations on diagrams must go through standard selection. This is the deliberate behavior chosen for diagrams where drag/zoom ergonomics matter more than whole-block targeting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Extract normalizeMermaidSvgMarkup to a pure module The MermaidBlock test imported the component, which runs mermaid.initialize() at module load. That throws 'Unsupported color format: "#333"' in the headless CI environment, failing the test on import before any assertion runs. Move the pure SVG-normalization helper into mermaidSvg.ts (no React, no mermaid import) and have both the component and the test import it from there. App behavior is unchanged; the test no longer loads mermaid. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Michael Ramos <mdramos8@gmail.com>
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
// Pure SVG-markup helpers for MermaidBlock. Kept free of React and the
|
|
// mermaid library so they can be unit-tested without loading mermaid's
|
|
// browser-only `initialize()` (which throws in headless test environments).
|
|
|
|
// Bake sizing attrs into the SVG markup so they survive repeated
|
|
// dangerouslySetInnerHTML re-injection — imperative setAttribute gets wiped.
|
|
export function normalizeMermaidSvgMarkup(markup: string): string {
|
|
return markup.replace(/<svg\b([^>]*)>/i, (_match, attrs: string) => {
|
|
let next = attrs;
|
|
|
|
if (/\bstyle\s*=\s*"/i.test(next)) {
|
|
next = next.replace(/\bstyle\s*=\s*"([^"]*)"/i, (_m, styleVal: string) => {
|
|
const rules = styleVal
|
|
.split(';')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0 && !/^max-width\s*:/i.test(s));
|
|
rules.push('max-width: none');
|
|
return `style="${rules.join('; ')}"`;
|
|
});
|
|
} else {
|
|
next += ' style="max-width: none"';
|
|
}
|
|
|
|
if (!/\bpreserveAspectRatio\s*=/i.test(next)) {
|
|
next += ' preserveAspectRatio="xMidYMid meet"';
|
|
}
|
|
if (!/\bheight\s*=/i.test(next)) {
|
|
next += ' height="100%"';
|
|
}
|
|
|
|
return `<svg${next}>`;
|
|
});
|
|
}
|