From 8116e2432c0abeca50eb1251184df9cb8c73a503 Mon Sep 17 00:00:00 2001 From: Andrew Mikofalvy <5668128+amikofalvy@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:13:45 +0000 Subject: [PATCH] fix(visimer): hold the hero headline in place while mermaid renders (PRD-8003) (#3467) * Hold the hero headline in place while mermaid renders The visimer.com headline is a live mermaid diagram, so on a cold load the page went through three states: a plain text headline from index.html for about 1.3s, then the fully drawn page with a 300px hole where the h1 belongs for another 600ms, then the diagram popping in. The middle state reads as broken, because the hint line points at an empty band. The 600ms hole is mermaid pulling its flowchart renderer over two dynamic imports after React mounts, then laying the diagram out. Rather than chase that down, close the window: draw a static stand-in headline that matches the real render and crossfade it out when the real one lands. src/hero-diagram.ts is the single definition of both. Its coordinates are mermaid's own output for the hero source, so the stand-in and the real diagram share a viewBox and are fitted into the same box by the same preserveAspectRatio. Measured over the band, the two differ by 0.01% of pixels at 1440px and 0% at 400px, and the 220ms crossfade covers the rest. The same module generates the first-paint markup that a small Vite plugin inlines into index.html, replacing the hand-maintained copy that used to live there. That copy carried its own geometry and sat the headline 150px from the top while the real hero puts it at 189px, so mounting moved the page under the visitor. The hero's layout values now live in site.css and both renderers read them. Note that mermaid measures its labels exactly once, and emits a layout about 2% tighter whenever it happens to render before Inter has loaded. The committed coordinates are the Inter-loaded ones, which is what a real visitor gets: the font stylesheet is preconnected and lands well before the bundle that starts the render. * Address local review on the hero stand-in Six items from the review panel, none blocking. Pass the boot HTML to String.replace as a function rather than a string. A string replacement gives $&, $' and friends their special meaning, so the day a hero string gains a dollar sign the served document would be silently corrupted, and only in the pre-React copy that nobody looks at during normal development. Move the last shared strings into hero-diagram.ts: the license label and the two hint lines were still written out in both renderers, which is the drift the module exists to prevent. licenseLabel now reads from it, so the badge and the footer follow one definition. Link the boot spacer to the real header through a --site-header-h custom property, with the header enforcing it as a floor. It stays a floor rather than a fixed height so taller content grows the header instead of being clipped by it. Correct the section comment in site.css that still described the old .mw-svg-host scope. The rules now reach the stand-in too, and a new rule written to the old scope would style only one of the pair, which is exactly how the crossfade stops being invisible. Unexport the two orientation constants, which are only composed into HERO_PLACEHOLDER_HTML in the same file. Tighten the comments: drop the point-in-time bundle size and the count of mermaid's internal imports, name all four inputs the coordinates depend on so a maintainer tuning heroConfig knows to re-derive them, and use mermaid's own arrowMarkerPath class on the arrowhead instead of claiming attribute-level fidelity while diverging on the class name. Re-verified after the changes: stand-in against the settled render still differs by 0.01% of band pixels at 1440px and 0% at 400px, hero geometry is byte-identical, in-place editing works, no console errors. * Address cloud review on the hero stand-in Four inline findings and two scope notes from pullfrog, none blocking. Compose HERO_HEADLINE from the three label constants rather than restating the sentence. It was the one string in the module still written twice, so the accessible headline and the visible diagram could drift apart, which is the duplication the module exists to remove. Escape the values heroBootHtml interpolates. The JSX consumers of those same constants get React's escaping; the raw-HTML consumer got none, so the first hero string to contain a <, an & or a quote would render correctly in the app and malformed in the served document. Same asymmetry as the dollar-sign fix, one layer up. Static entities in the template are markup rather than values and stay as authored. Stop letting two predicates decide one crossfade. The stand-in's orientation came from a media query while the diagram's came from a single mount-time read of window.innerWidth that is deliberately never re-picked, so resizing across 760px could leave a stacked stand-in fading into a left-to-right diagram in the same box. The app now renders one stand-in chosen from the source the editor actually got; the media-query pair stays, but only in the served document, which has no better option. Drop the boot block on non-landing routes. index.html is the only entry, so the landing hero also painted over /playground and /hero-loop until the bundle routed away from it. Before this change that was a line of text; a full hero reproduction is worse. An inline synchronous script removes it during parse, matching main.tsx's own pathname normalization. Correct the index.html comment that still called this a render-failure fallback. A failed parse emits render with ok false, which resolves the gate, fades the stand-in out and reveals the canvas error badge. Add a dev-only viewBox comparison in the render handler. Nothing else notices when the hand-measured coordinates stop matching mermaid, and the only symptom is a stand-in that shifts as it fades on a cold load. It checks the first render only, since an edited headline is legitimately a different shape, and import.meta.env.DEV keeps it out of the bundle. Verified: 0.01% band-pixel difference at 1440px and 0% at 400px unchanged; / keeps the stand-in while /playground and /hero-loop drop it; a 1000-to-400 resize after render leaves exactly one stand-in matching the rendered viewBox; the warning is absent from the production bundle; in-place editing works with no console errors. GitOrigin-RevId: c668267a3aef9067cd0392776a7ad61cf3fae633 --- apps/site/index.html | 14 +-- apps/site/src/App.tsx | 151 +++++++++++------------ apps/site/src/hero-diagram.ts | 223 ++++++++++++++++++++++++++++++++++ apps/site/src/site.css | 207 +++++++++++++++++++++++++------ apps/site/vite.config.ts | 30 ++++- 5 files changed, 504 insertions(+), 121 deletions(-) create mode 100644 apps/site/src/hero-diagram.ts diff --git a/apps/site/index.html b/apps/site/index.html index 1491e8e..31c4f74 100644 --- a/apps/site/index.html +++ b/apps/site/index.html @@ -81,15 +81,15 @@ empty div, so anything that does not execute JavaScript sees no headline at all, and the first paint is blank until the bundle has run. - Since the visible headline is drawn by mermaid at runtime, this is also - what shows if that render ever fails while the rest of the page works. + It is not a failure fallback: a render that throws still emits `render`, + which resolves the gate in App.tsx, fades this out and reveals the + canvas's own error badge. This covers the wait, not the failure. - Keep this copy in sync with the hero in App.tsx. + The markup is generated from src/hero-diagram.ts by the visimer-hero-boot + plugin in vite.config.ts, so it cannot drift from the hero it stands in + for. Edit that module, not the output. --> -
Click a node to edit it. Perfect for polishing AI-generated diagrams.
-