dyn_into runs 'instanceof TiptapEditorModule', but the IIFE TiptapEditor
is a plain object literal — not an instance of wasm-bindgen's registered
constructor — so the check always fails with a panic.
unchecked_into does only the compile-time type annotation; Reflect::get
already guarantees we have the right object.
The extern 'fn get_module() -> TiptapEditorModule' was compiled by
wasm-bindgen to 'window.TiptapEditor()' — a function call. But
TiptapEditor is the IIFE module object, not a function, so this threw
'window.TiptapEditor is not a function'.
Replace with js_sys::Reflect::get(&window, "TiptapEditor") +
dyn_into, which reads the property without invoking it.
This bug predated the Vite 8 upgrade but only surfaced after the
EditorOptions fix let initialization progress further.
EditorOptions was a TS interface (erased at runtime), but the Rust
wasm-bindgen extern uses #[wasm_bindgen(constructor)] which generates
'new EditorOptions()' — requiring a global constructor.
Vite 5/Rollup accidentally leaked the name into IIFE global scope,
masking this. Vite 8/Rolldown strictly erases interfaces, exposing it
as 'EditorOptions is not defined'.
Fix: make EditorOptions a class (retains runtime constructor) and mount
it on window, since the IIFE bundle name is taken by TiptapEditor.
overlay inherited opacity:1 from .lightbox-overlay CSS, so the moment
it was appended to the DOM it painted a full-opacity black background.
this showed for ~24ms (the time until the image loaded and start() ran
the first frame that sets opacity 0), visible as a black flash before
the fade-in.
set overlay.style.opacity = '0' right after creating it, so it is
transparent when appended; start()'s transition then fades it to 1 as
intended.
several bugs found via in-browser debugging, all in lightbox.js + the
blur-up CSS:
1. DOMRect attribute mismatch: getBoundingClientRect returns .width/
.height, but code read .w/.h -> NaN transforms. map rectOf() to a
uniform {x,y,w,h}.
2. zoom scale base was the huge original natural size (2942px), so the
centered state collapsed to a tiny scale and the open animation ran
backwards (shrink). base the scale on originRect instead: first
frame is the in-article image at scale 1, centered state scales to
target.w/originRect.w. open is always grow, close always shrink.
3. scroll-to-close fired on a phantom scroll jump: the oversized
original img (layout size = naturalW) expanded the document's
scrollable area. set img layout size explicitly and add
overflow:hidden on the (position:fixed) overlay so the img cannot
expand the body.
4. first open animation jumped from the wrong position: a single rAF
was not enough to commit the no-transition first frame. use a forced
reflow (offsetHeight) + double-rAF before starting the transition.
5. blur-up full layer stuck at opacity 0 until a forced reflow (opening
the lightbox) repainted it. drop the opacity transition on .blur-img-
full and set opacity:1 from JS on load (deterministic, no repaint
race). mark the container .is-loaded and hide the placeholder via
the container class.
6. a 16px grey gap above in-article images: .md-content img { margin:
1rem 0 } applied to the absolute .blur-img-full too, pushing it
down. reset margin:0 / max-width:none on the blur-up img layers.
remove the duplicated .image-viewer-* and .md-image-lightbox-* rules
(~93 lines); add a single .lightbox-* set sized/positioned by JS
transform. entry-cover and post-card-cover now target the blur-img
structure (4:3 ratio on cards, cover-fill on entry cover).
both call sites (PostCover, PostCard) now render raw .blur-img, so the
Dioxus ImageViewer component — with its leaky closure.forget() and dead
js_sys::eval delay — is no longer needed. remove the file and its mod
declaration.
card cover is display-only (click goes to the card link), so it uses
a bare .blur-img with ?w=20 placeholder + ?thumb=400x300 display, no
lightbox-single class. card pages are outside .post-content/.entry-cover
so lightbox.js never scans them.
drop ImageViewer; render .blur-img with a lightbox-single class (the
class, not a data attribute, because Dioxus' typed span elements reject
arbitrary data-* attrs). lightbox.js now keys single-mode off the
lightbox-single class instead of data-single. cover uses ?w=20
placeholder + ?w=1200 display; SSR writes --ar from real dimensions.
eval both post-content.js (copy) and lightbox.js (lightbox + lazy-load),
then call __initLightbox over ['.post-content', '.entry-cover'] so both
in-content images (gallery) and the cover (single) are wired up.
add prev/next arrow buttons (only in gallery mode with >1 image) and
ArrowLeft/ArrowRight keyboard handling. switching uses a 150ms
fade-out -> swap src -> fade-in (no fly, matching Medium). originNode
is updated to the new image so close/scroll-close flies back to the
right place; openScrollY is reset to keep scroll-close baseline valid.
bindInteractions() was only called from start(), which waits for the
image load event. during the load window Esc/scroll/backdrop-click did
nothing. bind right after appending the overlay so the lightbox is
closable throughout the load.