/** * Issue #208 — Minimal inline SPA-navigation helper. * Issue #220 — body-swap observability + fallback + script re-execution. * Issue #222 — hash-anchor preservation across SPA swaps. * * Self-contained IIFE injected into the SSR `
` that upgrades plain * full-page navigations into client-side `history.pushState` + * `fetch` + DOM-swap transitions, without loading any JS bundle. * * Motivating use case: docs / blog / marketing sites that build with * `hydration: "none"` (no islands). Under Issue #193 the opt-out SPA * router lives in `@mandujs/core/client` (`router.ts`), which only ships * inside a hydration bundle. Zero-JS pages therefore lost the "feels * like a SPA" behavior that `spa: true` (the framework default) promises. * * This helper fills the gap: ~2.8 KB of inline JavaScript that the * browser parses and runs immediately, no module graph, no network * round-trip. Paired with the `@view-transition { navigation: auto }` * style block (#192) the result is a visually-animated pushState * navigation on every internal link click. * * Design constraints (locked — changing any of these needs an explicit * rationale in the PR): * * 1. **Exclusion parity with the full router** (`router.ts` * `handleLinkClick`): every browser-owned escape hatch — modifier * keys, non-left click, `target` other than `_self`, `download`, * `mailto:` / `tel:` / `javascript:` / …, cross-origin, hash-only, * no `href`, `data-no-spa`, and `event.defaultPrevented` — is * checked here too. Regression matrix lives at * `tests/client/spa-nav-helper-exclusions.test.ts`. * * 2. **Co-existence with the full router**: both handlers listen * on `document` `click`. The helper bails out early when * `window.__MANDU_ROUTER_STATE__` is present — that global is * installed by `initializeRouter()` before it calls * `addEventListener`, so on hydrated pages the full router wins. * On pure-SSR pages the state global is missing and the helper * is authoritative. * * 3. **View Transitions API** — we call * `document.startViewTransition(cb)` when available, mirroring the * `@view-transition` at-rule we already inject. Browsers without * the API (Firefox, Safari < 18.2) execute the callback * synchronously so the feature is a pure progressive enhancement. * * 4. **DOM swap strategy** (issue #220 rework): * - Prefer `