Fix hero-canvas navigation race; style the prosekit editor and add a toolbar
Deploy / deploy (push) Successful in 28s

- Hero: gate RasterizedYES construction on a NodeRef resolving, not
  just "the Effect ran". Root cause of the reported "loading..." stall
  + "RefCell already borrowed" panic - navigating back to / client-side
  could run the Effect before the new <canvas> was actually in the
  DOM, yes.js did an unchecked getElementById(...).getContext() on
  null and threw mid-reactive-update, corrupting wasm_bindgen_futures'
  executor badly enough to panic on the next tick. Same NodeRef-gating
  pattern the prosekit editor's own mount already used.
- style/main.css: .prosekit-wrap/.prosekit-toolbar/.prosekit-editor to
  match the existing input/textarea look (border, background, focus
  ring).
- prosekit-editor.js: a real toolbar (Bold, Italic, H1, H2, Link, and
  a dedicated Gitea-repo-embed button reusing the paste rule's own
  insert logic) with active-state highlighting via the standard
  ProseMirror markActive/nodeActive idiom.
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-05 14:52:08 +02:00
parent 3242482c8e
commit 00d2a95625
3 changed files with 239 additions and 22 deletions
+21 -7
View File
@@ -232,11 +232,11 @@ fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView {
// Only the landing page gets the full interactive piece - it's the
// one page this is actually "the" hero for; other pages (thank-you
// pages, /review) get the plain dark header below. Starts the
// animation once the canvas elements exist (post-mount, via
// Effect), and explicitly stops the requestAnimationFrame loop on
// unmount - the original page-owning script never needed this since
// navigating away meant a full document unload, which doesn't
// happen in an SPA.
// animation once the canvas elements exist, and explicitly stops
// the requestAnimationFrame loop on unmount - the original
// page-owning script never needed this since navigating away meant
// a full document unload, which doesn't happen in an SPA.
let raster_ref: NodeRef<leptos::html::Canvas> = NodeRef::new();
#[cfg(feature = "hydrate")]
if show_yes {
// `on_cleanup` requires Send + Sync (even single-threaded, wasm),
@@ -245,7 +245,21 @@ fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView {
// cnats' WebRTC call state uses for its own non-Send peer map).
let instance: StoredValue<Option<yes::RasterizedYes>, LocalStorage> =
StoredValue::new_local(None);
// Gated on the canvas NodeRef resolving, not just "the Effect
// ran" - on a fresh page load the DOM is already there by the
// time this fires, but navigating back to `/` client-side hit a
// real race: the Effect ran before the new view's <canvas> was
// actually inserted, RasterizedYES::new() (yes.js) did an
// unchecked `document.getElementById(...).getContext(...)` on
// null and threw mid-reactive-update - which then corrupted
// wasm_bindgen_futures' single-threaded executor state badly
// enough to panic ("RefCell already borrowed") on the next
// tick. Waiting for the NodeRef itself guarantees DOM presence
// the same way the prosekit editor's own mount Effect does.
Effect::new(move |_| {
if raster_ref.get().is_none() {
return;
}
instance.set_value(Some(yes::RasterizedYes::new()));
});
on_cleanup(move || {
@@ -264,7 +278,7 @@ fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView {
view! {
<div class="hero-canvas" aria-hidden="true">
<canvas id="lineCanvas"></canvas>
<canvas id="rasterCanvas"></canvas>
<canvas id="rasterCanvas" node_ref=raster_ref></canvas>
</div>
}
})}
@@ -502,7 +516,7 @@ fn AlternativeCard(
prop:value=move || sig.get()
on:input=move |ev| sig.set(event_target_value(&ev))
/>
<div class="prosekit-editor" node_ref=container_ref></div>
<div class="prosekit-wrap" node_ref=container_ref></div>
</label>
}
.into_any();