islands-runtime 0.1.0

The shared WASM runtime for islands.rs: reactive Signal/Scope/effect primitives and idempotent island mounting, emitted as islands_core.{js,wasm}.
Documentation
//! View Transitions wrap for the morph+mount (Phase 3g, AC-V17).
//!
//! When `document.startViewTransition` exists AND `prefers-reduced-motion` is not
//! set, the morph + Suspense activation + mount (contract steps 4-6) run inside a
//! view transition so the change is animated. When the API is absent or
//! reduced-motion is requested, the exact same steps run synchronously, plain and
//! un-animated. **Nav correctness never depends on View Transitions** — the same
//! work happens either way; only the animation differs.
//!
//! The callback handed to `startViewTransition` must be **fully synchronous**
//! (return `undefined`, not a Promise): the morph and `mount_all` are synchronous
//! today, so running them in a sync callback captures island first-paint into the
//! transition's "after" snapshot. Step 7a (capture scroll + pushState) runs
//! BEFORE the transition; steps 7b (rAF scroll restore) + 7c (focus) run AFTER,
//! gated on the transition's `updateCallbackDone` — handled by the caller, which
//! awaits [`run_morph_with_optional_transition`].
//!
//! `startViewTransition` has no stable `web-sys` binding, so it is reached via a
//! tiny `inline_js` glue. The glue also folds in the feature-detect +
//! reduced-motion check so the guard lives in one place and needs no extra
//! `web-sys` media-query feature.

use std::cell::RefCell;
use std::rc::Rc;

use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;

/// Run `perform` (contract steps 4-6) wrapped in a view transition when one is
/// available and motion is allowed, otherwise run it directly. Returns once the
/// DOM update has been applied (the transition's `updateCallbackDone` has
/// resolved, or immediately in the plain path), so the caller can then run the
/// post-morph scroll-restore + focus (steps 7b/7c).
///
/// `perform` is invoked exactly once. Its `Result` is captured even when it runs
/// inside the transition callback, so a morph error still propagates to the
/// caller's full-load fallback (AC-V11) rather than being swallowed by the
/// animation machinery.
pub(crate) async fn run_morph_with_optional_transition(
    perform: impl FnOnce() -> Result<(), JsValue> + 'static,
) -> Result<(), JsValue> {
    if !should_use_view_transition() {
        // Plain path: VT unsupported or reduced-motion — run steps 4-6 inline.
        return perform();
    }

    // VT path: run steps 4-6 inside a synchronous transition callback. The
    // callback's Result is stashed into a shared cell so we can surface it after.
    // `startViewTransition` invokes the callback synchronously while creating the
    // transition, so the cell is populated by the time the glue returns. A shared
    // `Rc<RefCell<…>>` (not a `&mut` borrow) keeps the closure `'static`, as
    // `Closure::once_into_js` requires.
    let outcome: Rc<RefCell<Result<(), JsValue>>> = Rc::new(RefCell::new(Ok(())));
    let outcome_for_callback = outcome.clone();
    let mut perform_holder = Some(perform);
    let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
        if let Some(perform) = perform_holder.take() {
            *outcome_for_callback.borrow_mut() = perform();
        }
    });
    let update_callback_done = run_view_transition_glue(&callback);

    // Await the DOM-update phase so the caller's 7b/7c run after it (the callback
    // ran synchronously above, so the outcome is already set; awaiting just gates
    // the post-morph steps on the transition's update phase).
    let _ = JsFuture::from(update_callback_done).await;
    Rc::try_unwrap(outcome)
        .map(RefCell::into_inner)
        .unwrap_or_else(|shared| shared.borrow().clone())
}

/// Whether a view transition should wrap the morph: the API exists AND
/// `prefers-reduced-motion` is not set. Delegated to the JS glue so the
/// feature-detect and the media query live together and need no extra `web-sys`
/// feature.
fn should_use_view_transition() -> bool {
    should_view_transition_glue()
}

#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
export function __islands_should_view_transition() {
    if (typeof document === "undefined") return false;
    if (typeof document.startViewTransition !== "function") return false;
    try {
        if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
            return false;
        }
    } catch (_e) {
        // matchMedia unavailable — fall through to allowing the transition.
    }
    return true;
}
export function __islands_run_view_transition(callback) {
    // The callback is synchronous (returns undefined); startViewTransition
    // snapshots before, runs the callback to mutate the DOM, then animates.
    // Return updateCallbackDone so the caller can sequence post-morph work.
    const transition = document.startViewTransition(callback);
    return transition.updateCallbackDone;
}
"#)]
extern "C" {
    #[wasm_bindgen(js_name = __islands_should_view_transition)]
    fn should_view_transition_glue() -> bool;

    #[wasm_bindgen(js_name = __islands_run_view_transition)]
    fn run_view_transition_glue(callback: &JsValue) -> js_sys::Promise;
}