@real-router/svelte 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,8 @@ All composables must be called during component initialization (not inside `$eff
69
69
  | `useRouteNode(name)` | `{ navigator, route: { current }, previousRoute: { current } }` | `.current` when node activates/deactivates |
70
70
  | `useRouteUtils()` | `RouteUtils` | Never |
71
71
  | `useRouterTransition()` | `{ current: RouterTransitionSnapshot }` | `.current` on transition start/end |
72
+ | `useRouteExit(handler, options?)` | `void` — wraps `subscribeLeave` with abort + same-route guards | Never (handler captured at init) |
73
+ | `useRouteEnter(handler, options?)` | `void` — fires once on nav-driven mount via `$effect` + `transition.from` | Never (handler captured at init) |
72
74
 
73
75
  ```svelte
74
76
  <!-- useRouteNode — updates only when "users.*" changes -->
@@ -111,6 +113,46 @@ All composables must be called during component initialization (not inside `$eff
111
113
  {/if}
112
114
  ```
113
115
 
116
+ ```svelte
117
+ <!-- useRouteExit — exit animations, draft autosave, AbortSignal-aware cleanup -->
118
+ <script lang="ts">
119
+ import { useRouteExit } from "@real-router/svelte";
120
+
121
+ let el: HTMLDivElement;
122
+
123
+ useRouteExit(async ({ signal }) => {
124
+ if (!el) return;
125
+ el.classList.add("fade-out");
126
+ const cleanup = () => el.classList.remove("fade-out");
127
+ signal.addEventListener("abort", cleanup, { once: true });
128
+ el.getBoundingClientRect(); // style flush
129
+ await Promise.allSettled(el.getAnimations().map((a) => a.finished));
130
+ cleanup();
131
+ });
132
+ </script>
133
+
134
+ <div bind:this={el}>...</div>
135
+ ```
136
+
137
+ ```svelte
138
+ <!-- useRouteEnter — page-enter analytics, focus management, entry animations -->
139
+ <script lang="ts">
140
+ import { useRouteEnter } from "@real-router/svelte";
141
+
142
+ useRouteEnter(({ route, previousRoute }) => {
143
+ analytics.track("page_enter", {
144
+ route: route.name,
145
+ from: previousRoute.name,
146
+ });
147
+ });
148
+ </script>
149
+ ```
150
+
151
+ > **Svelte handler-reactivity:** composables run once at init, so `handler` is
152
+ > captured at hook-call time. To vary behavior over time, read
153
+ > `$state` / `$derived` **inside** the handler body. See [CLAUDE.md](./CLAUDE.md)
154
+ > → "useRouteExit / useRouteEnter Handler Is Captured At Init".
155
+
114
156
  ## Components
115
157
 
116
158
  ### `<Link>`
@@ -375,12 +417,24 @@ Opt-in preservation of scroll position across navigations:
375
417
 
376
418
  Restores scroll on back/forward, scrolls to top (or `#hash`) on push. Three modes: `"restore"` (default), `"top"`, `"manual"`. Custom containers via `scrollContainer: () => HTMLElement | null`. Lifecycle tied to the provider — created on mount, destroyed on unmount. See [Scroll Restoration guide](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for details.
377
419
 
420
+ ## View Transitions
421
+
422
+ Opt-in animated route transitions via the browser's [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API):
423
+
424
+ ```svelte
425
+ <RouterProvider {router} viewTransitions>
426
+ <!-- Your app -->
427
+ </RouterProvider>
428
+ ```
429
+
430
+ Reactive via `$effect` — toggling the prop creates/destroys the utility. No-op on unsupported browsers (Firefox as of 2026-04, SSR). Customization is pure CSS via `::view-transition-*` pseudo-elements and `view-transition-name` for hero morphs. See [View Transitions guide](https://github.com/greydragon888/real-router/wiki/View-Transitions) for patterns.
431
+
378
432
  ## Documentation
379
433
 
380
434
  Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
381
435
 
382
- - [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration)
383
- - [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition)
436
+ - [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) · [View Transitions](https://github.com/greydragon888/real-router/wiki/View-Transitions)
437
+ - [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition) · [useRouteExit](https://github.com/greydragon888/real-router/wiki/useRouteExit) · [useRouteEnter](https://github.com/greydragon888/real-router/wiki/useRouteEnter)
384
438
 
385
439
  ## Examples
386
440
 
@@ -4,6 +4,7 @@
4
4
  import {
5
5
  createRouteAnnouncer,
6
6
  createScrollRestoration,
7
+ createViewTransitions,
7
8
  } from "./dom-utils";
8
9
  import { setContext, untrack } from "svelte";
9
10
 
@@ -20,11 +21,13 @@
20
21
  children,
21
22
  announceNavigation,
22
23
  scrollRestoration,
24
+ viewTransitions,
23
25
  }: {
24
26
  router: Router;
25
27
  children: Snippet;
26
28
  announceNavigation?: boolean;
27
29
  scrollRestoration?: ScrollRestorationOptions;
30
+ viewTransitions?: boolean;
28
31
  } = $props();
29
32
 
30
33
  $effect(() => {
@@ -54,6 +57,12 @@
54
57
  return () => sr.destroy();
55
58
  });
56
59
 
60
+ $effect(() => {
61
+ if (!viewTransitions) return;
62
+ const vt = createViewTransitions(router);
63
+ return () => vt.destroy();
64
+ });
65
+
57
66
  const navigator = getNavigator(router);
58
67
  const source = createRouteSource(router);
59
68
  const reactive = createReactiveSource(source);
@@ -6,6 +6,7 @@ type $$ComponentProps = {
6
6
  children: Snippet;
7
7
  announceNavigation?: boolean;
8
8
  scrollRestoration?: ScrollRestorationOptions;
9
+ viewTransitions?: boolean;
9
10
  };
10
11
  declare const RouterProvider: import("svelte").Component<$$ComponentProps, {}, "">;
11
12
  type RouterProvider = ReturnType<typeof RouterProvider>;
@@ -1,3 +1,7 @@
1
1
  import type { RouteContext } from "../types";
2
- import type { Params } from "@real-router/core";
3
- export declare const useRoute: <P extends Params = Params>() => RouteContext<P>;
2
+ import type { Params, State } from "@real-router/core";
3
+ export declare const useRoute: <P extends Params = Params>() => Omit<RouteContext<P>, "route"> & {
4
+ route: {
5
+ readonly current: State<P>;
6
+ };
7
+ };
@@ -1,2 +1,8 @@
1
1
  import { ROUTE_KEY, getContextOrThrow } from "../context";
2
- export const useRoute = () => getContextOrThrow(ROUTE_KEY, "useRoute");
2
+ export const useRoute = () => {
3
+ const ctx = getContextOrThrow(ROUTE_KEY, "useRoute");
4
+ if (!ctx.route.current) {
5
+ throw new Error("useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?");
6
+ }
7
+ return ctx;
8
+ };
@@ -0,0 +1,68 @@
1
+ import type { State } from "@real-router/core";
2
+ export interface RouteEnterContext {
3
+ /** The route that was just activated. */
4
+ route: State;
5
+ /** The route that was active immediately before this navigation. */
6
+ previousRoute: State;
7
+ }
8
+ export type RouteEnterHandler = (context: RouteEnterContext) => void;
9
+ export interface UseRouteEnterOptions {
10
+ /**
11
+ * Skip the handler when `route.name === previousRoute.name`
12
+ * (sort/filter/query-only navigations on the same route). Default:
13
+ * `true`. Symmetric with `useRouteExit`'s same-name option.
14
+ */
15
+ skipSameRoute?: boolean;
16
+ }
17
+ /**
18
+ * Fire `handler` once when the component mounts as a result of a
19
+ * navigation. Mirror of `useRouteExit` for the entry side.
20
+ *
21
+ * What this composable covers that an ad-hoc `$effect` + `useRoute()`
22
+ * doesn't:
23
+ *
24
+ * - **Skip-initial**: handler is skipped when there is no
25
+ * `route.transition.from` (i.e. first-load mount). Most consumers
26
+ * want to fire side effects only on real navigations, not on
27
+ * hydration.
28
+ * - **Same-route skip** (default): handler is skipped when
29
+ * `route.transition.from === route.name`. Sort/filter/query-only
30
+ * navigations re-run the effect (because the `route` reference
31
+ * changes), but they are not "entries" in the animation / analytics
32
+ * sense. Opt out with `skipSameRoute: false`.
33
+ * - **Mount-time `route` / `previousRoute` snapshot**: handler receives
34
+ * the values that were live at the moment of effect activation.
35
+ *
36
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
37
+ * component init; `handler` is captured in closure at the call site. To
38
+ * vary behavior over time, read `$state` / `$derived` values inside the
39
+ * handler body.
40
+ *
41
+ * @example Direction-aware entry animation
42
+ * ```svelte
43
+ * <script lang="ts">
44
+ * import { useRouteEnter } from "@real-router/svelte";
45
+ * let el: HTMLDivElement;
46
+ *
47
+ * useRouteEnter(({ route }) => {
48
+ * const direction = route.context.browser?.direction;
49
+ * el?.classList.add(
50
+ * direction === "back" ? "slide-from-left" : "slide-from-right",
51
+ * );
52
+ * });
53
+ * </script>
54
+ * ```
55
+ *
56
+ * @example Analytics page-enter event (skip-initial built-in)
57
+ * ```svelte
58
+ * <script lang="ts">
59
+ * useRouteEnter(({ route, previousRoute }) => {
60
+ * analytics.track("page_enter", {
61
+ * route: route.name,
62
+ * from: previousRoute.name,
63
+ * });
64
+ * });
65
+ * </script>
66
+ * ```
67
+ */
68
+ export declare function useRouteEnter(handler: RouteEnterHandler, options?: UseRouteEnterOptions): void;
@@ -0,0 +1,93 @@
1
+ import { useRoute } from "./useRoute.svelte";
2
+ /**
3
+ * Fire `handler` once when the component mounts as a result of a
4
+ * navigation. Mirror of `useRouteExit` for the entry side.
5
+ *
6
+ * What this composable covers that an ad-hoc `$effect` + `useRoute()`
7
+ * doesn't:
8
+ *
9
+ * - **Skip-initial**: handler is skipped when there is no
10
+ * `route.transition.from` (i.e. first-load mount). Most consumers
11
+ * want to fire side effects only on real navigations, not on
12
+ * hydration.
13
+ * - **Same-route skip** (default): handler is skipped when
14
+ * `route.transition.from === route.name`. Sort/filter/query-only
15
+ * navigations re-run the effect (because the `route` reference
16
+ * changes), but they are not "entries" in the animation / analytics
17
+ * sense. Opt out with `skipSameRoute: false`.
18
+ * - **Mount-time `route` / `previousRoute` snapshot**: handler receives
19
+ * the values that were live at the moment of effect activation.
20
+ *
21
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
22
+ * component init; `handler` is captured in closure at the call site. To
23
+ * vary behavior over time, read `$state` / `$derived` values inside the
24
+ * handler body.
25
+ *
26
+ * @example Direction-aware entry animation
27
+ * ```svelte
28
+ * <script lang="ts">
29
+ * import { useRouteEnter } from "@real-router/svelte";
30
+ * let el: HTMLDivElement;
31
+ *
32
+ * useRouteEnter(({ route }) => {
33
+ * const direction = route.context.browser?.direction;
34
+ * el?.classList.add(
35
+ * direction === "back" ? "slide-from-left" : "slide-from-right",
36
+ * );
37
+ * });
38
+ * </script>
39
+ * ```
40
+ *
41
+ * @example Analytics page-enter event (skip-initial built-in)
42
+ * ```svelte
43
+ * <script lang="ts">
44
+ * useRouteEnter(({ route, previousRoute }) => {
45
+ * analytics.track("page_enter", {
46
+ * route: route.name,
47
+ * from: previousRoute.name,
48
+ * });
49
+ * });
50
+ * </script>
51
+ * ```
52
+ */
53
+ export function useRouteEnter(handler, options) {
54
+ const { route, previousRoute } = useRoute();
55
+ const skipSameRoute = options?.skipSameRoute ?? true;
56
+ let lastHandledRoute = null;
57
+ $effect(() => {
58
+ const currentRoute = route.current;
59
+ const prev = previousRoute.current;
60
+ // Early-exit guards, top-down:
61
+ //
62
+ // - **Defensive**: `route.current` may be undefined during SSR or
63
+ // pre-start hydration. Not testable from vitest, v8-ignored.
64
+ // - **Skip-initial**: `state.transition.from` is undefined only
65
+ // for the very first state committed by `router.start()`.
66
+ // - **Skip-same-route**: query-only navigations have
67
+ // `transition.from === route.name`. Opt-out via
68
+ // `skipSameRoute: false`.
69
+ // - **Defensive dedupe + missing `previousRoute`**: same `route`
70
+ // ref between `$effect` re-runs is unexpected (createSubscriber
71
+ // only fires on real reference changes); `!prev` is unreachable
72
+ // once `transition.from` is set (core populates them together).
73
+ // Both kept for parity with React; v8-ignored.
74
+ /* v8 ignore start */
75
+ if (!currentRoute) {
76
+ return;
77
+ }
78
+ /* v8 ignore stop */
79
+ if (!currentRoute.transition.from) {
80
+ return;
81
+ }
82
+ if (skipSameRoute && currentRoute.transition.from === currentRoute.name) {
83
+ return;
84
+ }
85
+ /* v8 ignore start */
86
+ if (lastHandledRoute === currentRoute || !prev) {
87
+ return;
88
+ }
89
+ /* v8 ignore stop */
90
+ lastHandledRoute = currentRoute;
91
+ handler({ route: currentRoute, previousRoute: prev });
92
+ });
93
+ }
@@ -0,0 +1,83 @@
1
+ import type { State } from "@real-router/core";
2
+ export interface RouteExitContext {
3
+ /** The route being left. */
4
+ route: State;
5
+ /** The route being navigated to. */
6
+ nextRoute: State;
7
+ /**
8
+ * AbortSignal that fires when this navigation is superseded by a later
9
+ * one (rapid clicks). Already filtered: when the handler runs,
10
+ * `signal.aborted` is guaranteed to be `false`. Use
11
+ * `signal.addEventListener("abort", cleanup, { once: true })` for
12
+ * cleanup that must run on cancellation.
13
+ */
14
+ signal: AbortSignal;
15
+ }
16
+ export interface UseRouteExitOptions {
17
+ /**
18
+ * Skip the handler when `route.name === nextRoute.name`
19
+ * (sort/filter/query-only navigations on the same route). Default:
20
+ * `true`.
21
+ */
22
+ skipSameRoute?: boolean;
23
+ }
24
+ export type RouteExitHandler = (context: RouteExitContext) => void | Promise<void>;
25
+ /**
26
+ * Subscribe to the router's leave-window with the universal guards baked
27
+ * in. Wraps `router.subscribeLeave` so consumers don't repeat the same
28
+ * boilerplate every time:
29
+ *
30
+ * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`
31
+ * when the handler would run (rapid navigation superseded a slower
32
+ * one), the handler is skipped entirely.
33
+ * - **Same-route skip**: by default, `route.name === nextRoute.name`
34
+ * short-circuits the handler — query-only navigations skip the work.
35
+ * Opt out with `skipSameRoute: false`.
36
+ *
37
+ * Cleanup is bound to the component via `onDestroy`. Must be called
38
+ * during component initialization (synchronous in `<script>`).
39
+ *
40
+ * If the handler returns a Promise, the router blocks on it. If the
41
+ * Promise resolves, navigation proceeds. If it rejects, the router emits
42
+ * `TRANSITION_CANCELLED`.
43
+ *
44
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
45
+ * component init; `handler` is captured in closure at the call site. To
46
+ * vary behavior over time, read `$state` / `$derived` values inside the
47
+ * handler body — do not rely on swapping the handler reference.
48
+ *
49
+ * @example Animation
50
+ * ```svelte
51
+ * <script lang="ts">
52
+ * import { useRouteExit } from "@real-router/svelte";
53
+ * let el: HTMLDivElement;
54
+ *
55
+ * useRouteExit(async ({ signal }) => {
56
+ * if (!el) return;
57
+ * el.classList.add("fade-out");
58
+ * const cleanup = () => el.classList.remove("fade-out");
59
+ * signal.addEventListener("abort", cleanup, { once: true });
60
+ * try {
61
+ * el.getBoundingClientRect();
62
+ * await Promise.allSettled(el.getAnimations().map((a) => a.finished));
63
+ * } finally {
64
+ * cleanup();
65
+ * }
66
+ * });
67
+ * </script>
68
+ *
69
+ * <div bind:this={el}>...</div>
70
+ * ```
71
+ *
72
+ * @example Reading rich transition metadata via `nextRoute.transition`
73
+ * ```svelte
74
+ * <script lang="ts">
75
+ * useRouteExit(({ nextRoute }) => {
76
+ * if (nextRoute.transition.segments.deactivated.includes("products")) {
77
+ * productCache.clear();
78
+ * }
79
+ * });
80
+ * </script>
81
+ * ```
82
+ */
83
+ export declare function useRouteExit(handler: RouteExitHandler, options?: UseRouteExitOptions): void;
@@ -0,0 +1,74 @@
1
+ import { onDestroy } from "svelte";
2
+ import { useRouter } from "./useRouter.svelte";
3
+ /**
4
+ * Subscribe to the router's leave-window with the universal guards baked
5
+ * in. Wraps `router.subscribeLeave` so consumers don't repeat the same
6
+ * boilerplate every time:
7
+ *
8
+ * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`
9
+ * when the handler would run (rapid navigation superseded a slower
10
+ * one), the handler is skipped entirely.
11
+ * - **Same-route skip**: by default, `route.name === nextRoute.name`
12
+ * short-circuits the handler — query-only navigations skip the work.
13
+ * Opt out with `skipSameRoute: false`.
14
+ *
15
+ * Cleanup is bound to the component via `onDestroy`. Must be called
16
+ * during component initialization (synchronous in `<script>`).
17
+ *
18
+ * If the handler returns a Promise, the router blocks on it. If the
19
+ * Promise resolves, navigation proceeds. If it rejects, the router emits
20
+ * `TRANSITION_CANCELLED`.
21
+ *
22
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
23
+ * component init; `handler` is captured in closure at the call site. To
24
+ * vary behavior over time, read `$state` / `$derived` values inside the
25
+ * handler body — do not rely on swapping the handler reference.
26
+ *
27
+ * @example Animation
28
+ * ```svelte
29
+ * <script lang="ts">
30
+ * import { useRouteExit } from "@real-router/svelte";
31
+ * let el: HTMLDivElement;
32
+ *
33
+ * useRouteExit(async ({ signal }) => {
34
+ * if (!el) return;
35
+ * el.classList.add("fade-out");
36
+ * const cleanup = () => el.classList.remove("fade-out");
37
+ * signal.addEventListener("abort", cleanup, { once: true });
38
+ * try {
39
+ * el.getBoundingClientRect();
40
+ * await Promise.allSettled(el.getAnimations().map((a) => a.finished));
41
+ * } finally {
42
+ * cleanup();
43
+ * }
44
+ * });
45
+ * </script>
46
+ *
47
+ * <div bind:this={el}>...</div>
48
+ * ```
49
+ *
50
+ * @example Reading rich transition metadata via `nextRoute.transition`
51
+ * ```svelte
52
+ * <script lang="ts">
53
+ * useRouteExit(({ nextRoute }) => {
54
+ * if (nextRoute.transition.segments.deactivated.includes("products")) {
55
+ * productCache.clear();
56
+ * }
57
+ * });
58
+ * </script>
59
+ * ```
60
+ */
61
+ export function useRouteExit(handler, options) {
62
+ const router = useRouter();
63
+ const skipSameRoute = options?.skipSameRoute ?? true;
64
+ const off = router.subscribeLeave(({ route, nextRoute, signal }) => {
65
+ if (skipSameRoute && route.name === nextRoute.name) {
66
+ return;
67
+ }
68
+ if (signal.aborted) {
69
+ return;
70
+ }
71
+ return handler({ route, nextRoute, signal });
72
+ });
73
+ onDestroy(off);
74
+ }
@@ -0,0 +1,26 @@
1
+ import type { Router } from "@real-router/core";
2
+ export interface DirectionTracker {
3
+ destroy: () => void;
4
+ }
5
+ /**
6
+ * Track navigation direction (forward / back) and write it to
7
+ * `<html data-nav-direction>` on every leave. CSS / JS readers consume
8
+ * the attribute via `html[data-nav-direction="back"]` selectors or
9
+ * `document.documentElement.dataset.navDirection`.
10
+ *
11
+ * Mechanism-agnostic — works identically whether downstream UI uses CSS
12
+ * `@keyframes`, View Transitions pseudo-elements, or library state
13
+ * (motion's `motion.div initial={{ x: ... }}`).
14
+ *
15
+ * Implementation:
16
+ * - On install, set `data-nav-direction="forward"` baseline.
17
+ * - Attach a `popstate` listener that flips an internal flag to
18
+ * `true`. Browser back/forward navigation triggers popstate; user
19
+ * clicks on `<Link>` / programmatic `router.navigate(...)` do not.
20
+ * - On every `subscribeLeave`, write
21
+ * `popstateFlag ? "back" : "forward"` and reset the flag.
22
+ *
23
+ * Returns `{ destroy }` to clean up the listener and clear the dataset
24
+ * attribute.
25
+ */
26
+ export declare function createDirectionTracker(router: Router): DirectionTracker;
@@ -0,0 +1,57 @@
1
+ const NOOP_INSTANCE = Object.freeze({
2
+ destroy: () => {
3
+ /* no-op */
4
+ },
5
+ });
6
+ /**
7
+ * Track navigation direction (forward / back) and write it to
8
+ * `<html data-nav-direction>` on every leave. CSS / JS readers consume
9
+ * the attribute via `html[data-nav-direction="back"]` selectors or
10
+ * `document.documentElement.dataset.navDirection`.
11
+ *
12
+ * Mechanism-agnostic — works identically whether downstream UI uses CSS
13
+ * `@keyframes`, View Transitions pseudo-elements, or library state
14
+ * (motion's `motion.div initial={{ x: ... }}`).
15
+ *
16
+ * Implementation:
17
+ * - On install, set `data-nav-direction="forward"` baseline.
18
+ * - Attach a `popstate` listener that flips an internal flag to
19
+ * `true`. Browser back/forward navigation triggers popstate; user
20
+ * clicks on `<Link>` / programmatic `router.navigate(...)` do not.
21
+ * - On every `subscribeLeave`, write
22
+ * `popstateFlag ? "back" : "forward"` and reset the flag.
23
+ *
24
+ * Returns `{ destroy }` to clean up the listener and clear the dataset
25
+ * attribute.
26
+ */
27
+ export function createDirectionTracker(router) {
28
+ if (typeof document === "undefined") {
29
+ return NOOP_INSTANCE;
30
+ }
31
+ let popstateFlag = false;
32
+ document.documentElement.dataset.navDirection = "forward";
33
+ const onPopstate = () => {
34
+ popstateFlag = true;
35
+ };
36
+ // IMPORTANT — listener-ordering: `popstate` fires on `window`, which
37
+ // has no DOM descendants, so capture phase is moot. Listeners are
38
+ // dispatched in registration order. To beat the browser-plugin's own
39
+ // popstate handler, this tracker must be installed **before**
40
+ // `router.usePlugin(browserPluginFactory())` in user code. Otherwise
41
+ // the plugin's handler runs first and synchronously fires
42
+ // `subscribeLeave` while `popstateFlag` is still `false`.
43
+ globalThis.addEventListener("popstate", onPopstate);
44
+ const offLeave = router.subscribeLeave(() => {
45
+ document.documentElement.dataset.navDirection = popstateFlag
46
+ ? "back"
47
+ : "forward";
48
+ popstateFlag = false;
49
+ });
50
+ return {
51
+ destroy: () => {
52
+ offLeave();
53
+ globalThis.removeEventListener("popstate", onPopstate);
54
+ delete document.documentElement.dataset.navDirection;
55
+ },
56
+ };
57
+ }
@@ -1,5 +1,9 @@
1
+ export { createDirectionTracker } from "./direction-tracker.js";
1
2
  export { createRouteAnnouncer } from "./route-announcer.js";
2
3
  export { createScrollRestoration } from "./scroll-restore.js";
4
+ export { createViewTransitions } from "./view-transitions.js";
3
5
  export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
4
6
  export type { RouteAnnouncerOptions } from "./route-announcer.js";
5
7
  export type { ScrollRestorationOptions, ScrollRestorationMode, } from "./scroll-restore.js";
8
+ export type { DirectionTracker } from "./direction-tracker.js";
9
+ export type { ViewTransitions } from "./view-transitions.js";
@@ -1,3 +1,5 @@
1
+ export { createDirectionTracker } from "./direction-tracker.js";
1
2
  export { createRouteAnnouncer } from "./route-announcer.js";
2
3
  export { createScrollRestoration } from "./scroll-restore.js";
4
+ export { createViewTransitions } from "./view-transitions.js";
3
5
  export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
@@ -94,7 +94,7 @@ function resolveText(route, prefix, getCustomText, h1) {
94
94
  if (getCustomText) {
95
95
  return getCustomText(route);
96
96
  }
97
- const h1Text = h1?.textContent.trim() ?? "";
97
+ const h1Text = (h1?.textContent ?? "").trim();
98
98
  const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
99
99
  ? ""
100
100
  : route.name;
@@ -0,0 +1,5 @@
1
+ import type { Router } from "@real-router/core";
2
+ export interface ViewTransitions {
3
+ destroy: () => void;
4
+ }
5
+ export declare function createViewTransitions(router: Router): ViewTransitions;
@@ -0,0 +1,118 @@
1
+ const NOOP_INSTANCE = Object.freeze({
2
+ destroy: () => {
3
+ /* no-op */
4
+ },
5
+ });
6
+ export function createViewTransitions(router) {
7
+ if (typeof document === "undefined" ||
8
+ typeof document.startViewTransition !== "function") {
9
+ return NOOP_INSTANCE;
10
+ }
11
+ let closeVT = null;
12
+ let currentVT = null;
13
+ // Tracks whether TRANSITION_SUCCESS fired for the current leave. Used to
14
+ // distinguish "benign cleanup abort" (router's async path aborts its own
15
+ // controller in a finally block after successful navigation) from "real
16
+ // cancellation" (concurrent navigate, guard rejection, dispose).
17
+ let successFired = false;
18
+ const resolveAndClear = () => {
19
+ closeVT?.();
20
+ closeVT = null;
21
+ };
22
+ const offLeave = router.subscribeLeave(({ signal }) => {
23
+ // Reentrant abort: signal already aborted when we're called. Open no VT
24
+ // — router will fall through to TRANSITION_CANCELLED via isCurrentNav()
25
+ // after leave resolves. addEventListener("abort", ...) does not re-fire
26
+ // for past events, so skipping startViewTransition is the safe path.
27
+ if (signal.aborted) {
28
+ return;
29
+ }
30
+ successFired = false;
31
+ resolveAndClear();
32
+ // Return a Promise so the router awaits until the browser invokes
33
+ // updateCallback. This ensures old DOM snapshot is captured BEFORE the
34
+ // router commits the new state — giving correct exit→state→entry
35
+ // ordering (vs fire-and-forget, where URL changes before VT captures).
36
+ return new Promise((resolveLeave) => {
37
+ // Capture the resolver synchronously BEFORE startViewTransition() is
38
+ // called. The browser invokes updateCallback in a later task, but
39
+ // router.subscribe (TRANSITION_SUCCESS) can fire before that. If we
40
+ // captured `resolve` inside the callback, subscribe would see closeVT
41
+ // still null and skip resolving — the deferred would hang for 4s
42
+ // until the VT API aborts with TimeoutError.
43
+ const deferred = new Promise((resolve) => {
44
+ closeVT = resolve;
45
+ });
46
+ signal.addEventListener("abort", () => {
47
+ if (successFired) {
48
+ // Router's async path (#finishAsyncNavigation) aborts its own
49
+ // controller in a finally block AFTER completeTransition (and
50
+ // thus AFTER subscribe fired). This is cleanup, not
51
+ // cancellation — VT is progressing normally, do nothing.
52
+ return;
53
+ }
54
+ // Real cancellation (concurrent navigate, dispose). Resolve the
55
+ // deferred so updateCallback can complete, skip the VT so no
56
+ // stale animation leaks, and unblock the router if the abort
57
+ // fires before updateCallback was invoked.
58
+ resolveAndClear();
59
+ currentVT?.skipTransition?.();
60
+ resolveLeave();
61
+ }, { once: true });
62
+ try {
63
+ currentVT = document.startViewTransition(() => {
64
+ // Resolving here unblocks the router at the moment the browser
65
+ // enters updateCallback — by spec, old DOM snapshot is captured
66
+ // before this callback runs. Router now proceeds through
67
+ // activation guards and setState; the VT animation waits on
68
+ // `deferred`, which is resolved from router.subscribe after a
69
+ // task-queue tick (see NOTE on setTimeout below).
70
+ resolveLeave();
71
+ return deferred;
72
+ });
73
+ }
74
+ catch {
75
+ // Defensive: spec says startViewTransition doesn't throw under
76
+ // normal conditions, but Chromium has had edge cases (detached
77
+ // document, extension interference). Clean up and unblock router.
78
+ resolveAndClear();
79
+ resolveLeave();
80
+ }
81
+ });
82
+ });
83
+ const offSuccess = router.subscribe(() => {
84
+ const resolver = closeVT;
85
+ successFired = true;
86
+ closeVT = null;
87
+ if (resolver === null) {
88
+ currentVT = null;
89
+ }
90
+ else {
91
+ // CRITICAL: CANNOT use requestAnimationFrame here. When the router
92
+ // takes the async path (leave returned a Promise), subscribe fires
93
+ // AFTER the browser has already transitioned VT into the
94
+ // "update-callback-called" phase. In that phase Chromium sets
95
+ // rendering suppression to true, which ALSO blocks rAF callbacks.
96
+ // rAF would never fire → deferred never resolves → browser aborts
97
+ // vt.ready with TimeoutError after 4s (observed in Chromium).
98
+ //
99
+ // setTimeout runs on the task queue independent of the rendering
100
+ // pipeline, so it fires regardless of suppression. React's scheduler
101
+ // uses MessageChannel tasks, which are queued before our setTimeout,
102
+ // so the new DOM is committed by the time our callback runs.
103
+ setTimeout(() => {
104
+ resolver();
105
+ currentVT = null;
106
+ }, 0);
107
+ }
108
+ });
109
+ return {
110
+ destroy: () => {
111
+ offLeave();
112
+ offSuccess();
113
+ currentVT?.skipTransition?.();
114
+ currentVT = null;
115
+ resolveAndClear();
116
+ },
117
+ };
118
+ }
package/dist/index.d.ts CHANGED
@@ -9,10 +9,14 @@ export { useRouteUtils } from "./composables/useRouteUtils.svelte";
9
9
  export { useRoute } from "./composables/useRoute.svelte";
10
10
  export { useRouteNode } from "./composables/useRouteNode.svelte";
11
11
  export { useRouterTransition } from "./composables/useRouterTransition.svelte";
12
+ export { useRouteExit } from "./composables/useRouteExit.svelte";
13
+ export { useRouteEnter } from "./composables/useRouteEnter.svelte";
12
14
  export { createLinkAction } from "./actions/link.svelte";
13
15
  export type { LinkActionParams } from "./actions/link.svelte";
14
16
  export { default as RouterProvider } from "./RouterProvider.svelte";
15
17
  export { ROUTER_KEY, NAVIGATOR_KEY, ROUTE_KEY } from "./context";
16
18
  export type { LinkProps, RouteContext } from "./types";
19
+ export type { RouteExitContext, RouteExitHandler, UseRouteExitOptions, } from "./composables/useRouteExit.svelte";
20
+ export type { RouteEnterContext, RouteEnterHandler, UseRouteEnterOptions, } from "./composables/useRouteEnter.svelte";
17
21
  export type { Navigator } from "@real-router/core";
18
22
  export type { RouterTransitionSnapshot, RouterErrorSnapshot, } from "@real-router/sources";
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ export { useRouteUtils } from "./composables/useRouteUtils.svelte";
12
12
  export { useRoute } from "./composables/useRoute.svelte";
13
13
  export { useRouteNode } from "./composables/useRouteNode.svelte";
14
14
  export { useRouterTransition } from "./composables/useRouterTransition.svelte";
15
+ export { useRouteExit } from "./composables/useRouteExit.svelte";
16
+ export { useRouteEnter } from "./composables/useRouteEnter.svelte";
15
17
  // Actions
16
18
  export { createLinkAction } from "./actions/link.svelte";
17
19
  // Context
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/svelte",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Svelte 5 integration for Real-Router",
6
6
  "svelte": "./dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "license": "MIT",
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "@real-router/core": "^0.50.1",
47
+ "@real-router/core": "^0.50.2",
48
48
  "@real-router/route-utils": "^0.2.1",
49
49
  "@real-router/sources": "^0.7.2"
50
50
  },
@@ -54,11 +54,11 @@
54
54
  "@testing-library/jest-dom": "6.9.1",
55
55
  "@testing-library/svelte": "5.3.1",
56
56
  "@testing-library/user-event": "14.6.1",
57
- "eslint-plugin-svelte": "3.15.2",
57
+ "eslint-plugin-svelte": "3.17.1",
58
58
  "svelte": "5.54.0",
59
59
  "svelte-check": "4.4.5",
60
60
  "svelte-eslint-parser": "1.6.0",
61
- "@real-router/browser-plugin": "^0.15.1"
61
+ "@real-router/browser-plugin": "^0.16.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "svelte": ">=5.7.0"
@@ -4,6 +4,7 @@
4
4
  import {
5
5
  createRouteAnnouncer,
6
6
  createScrollRestoration,
7
+ createViewTransitions,
7
8
  } from "./dom-utils";
8
9
  import { setContext, untrack } from "svelte";
9
10
 
@@ -20,11 +21,13 @@
20
21
  children,
21
22
  announceNavigation,
22
23
  scrollRestoration,
24
+ viewTransitions,
23
25
  }: {
24
26
  router: Router;
25
27
  children: Snippet;
26
28
  announceNavigation?: boolean;
27
29
  scrollRestoration?: ScrollRestorationOptions;
30
+ viewTransitions?: boolean;
28
31
  } = $props();
29
32
 
30
33
  $effect(() => {
@@ -54,6 +57,12 @@
54
57
  return () => sr.destroy();
55
58
  });
56
59
 
60
+ $effect(() => {
61
+ if (!viewTransitions) return;
62
+ const vt = createViewTransitions(router);
63
+ return () => vt.destroy();
64
+ });
65
+
57
66
  const navigator = getNavigator(router);
58
67
  const source = createRouteSource(router);
59
68
  const reactive = createReactiveSource(source);
@@ -1,7 +1,21 @@
1
1
  import { ROUTE_KEY, getContextOrThrow } from "../context";
2
2
 
3
3
  import type { RouteContext } from "../types";
4
- import type { Params } from "@real-router/core";
4
+ import type { Params, State } from "@real-router/core";
5
5
 
6
- export const useRoute = <P extends Params = Params>(): RouteContext<P> =>
7
- getContextOrThrow<RouteContext>(ROUTE_KEY, "useRoute") as RouteContext<P>;
6
+ export const useRoute = <P extends Params = Params>(): Omit<
7
+ RouteContext<P>,
8
+ "route"
9
+ > & { route: { readonly current: State<P> } } => {
10
+ const ctx = getContextOrThrow<RouteContext>(ROUTE_KEY, "useRoute");
11
+
12
+ if (!ctx.route.current) {
13
+ throw new Error(
14
+ "useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
15
+ );
16
+ }
17
+
18
+ return ctx as Omit<RouteContext<P>, "route"> & {
19
+ route: { readonly current: State<P> };
20
+ };
21
+ };
@@ -0,0 +1,120 @@
1
+ import { useRoute } from "./useRoute.svelte";
2
+
3
+ import type { State } from "@real-router/core";
4
+
5
+ export interface RouteEnterContext {
6
+ /** The route that was just activated. */
7
+ route: State;
8
+ /** The route that was active immediately before this navigation. */
9
+ previousRoute: State;
10
+ }
11
+
12
+ export type RouteEnterHandler = (context: RouteEnterContext) => void;
13
+
14
+ export interface UseRouteEnterOptions {
15
+ /**
16
+ * Skip the handler when `route.name === previousRoute.name`
17
+ * (sort/filter/query-only navigations on the same route). Default:
18
+ * `true`. Symmetric with `useRouteExit`'s same-name option.
19
+ */
20
+ skipSameRoute?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Fire `handler` once when the component mounts as a result of a
25
+ * navigation. Mirror of `useRouteExit` for the entry side.
26
+ *
27
+ * What this composable covers that an ad-hoc `$effect` + `useRoute()`
28
+ * doesn't:
29
+ *
30
+ * - **Skip-initial**: handler is skipped when there is no
31
+ * `route.transition.from` (i.e. first-load mount). Most consumers
32
+ * want to fire side effects only on real navigations, not on
33
+ * hydration.
34
+ * - **Same-route skip** (default): handler is skipped when
35
+ * `route.transition.from === route.name`. Sort/filter/query-only
36
+ * navigations re-run the effect (because the `route` reference
37
+ * changes), but they are not "entries" in the animation / analytics
38
+ * sense. Opt out with `skipSameRoute: false`.
39
+ * - **Mount-time `route` / `previousRoute` snapshot**: handler receives
40
+ * the values that were live at the moment of effect activation.
41
+ *
42
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
43
+ * component init; `handler` is captured in closure at the call site. To
44
+ * vary behavior over time, read `$state` / `$derived` values inside the
45
+ * handler body.
46
+ *
47
+ * @example Direction-aware entry animation
48
+ * ```svelte
49
+ * <script lang="ts">
50
+ * import { useRouteEnter } from "@real-router/svelte";
51
+ * let el: HTMLDivElement;
52
+ *
53
+ * useRouteEnter(({ route }) => {
54
+ * const direction = route.context.browser?.direction;
55
+ * el?.classList.add(
56
+ * direction === "back" ? "slide-from-left" : "slide-from-right",
57
+ * );
58
+ * });
59
+ * </script>
60
+ * ```
61
+ *
62
+ * @example Analytics page-enter event (skip-initial built-in)
63
+ * ```svelte
64
+ * <script lang="ts">
65
+ * useRouteEnter(({ route, previousRoute }) => {
66
+ * analytics.track("page_enter", {
67
+ * route: route.name,
68
+ * from: previousRoute.name,
69
+ * });
70
+ * });
71
+ * </script>
72
+ * ```
73
+ */
74
+ export function useRouteEnter(
75
+ handler: RouteEnterHandler,
76
+ options?: UseRouteEnterOptions,
77
+ ): void {
78
+ const { route, previousRoute } = useRoute();
79
+ const skipSameRoute = options?.skipSameRoute ?? true;
80
+ let lastHandledRoute: State | null = null;
81
+
82
+ $effect(() => {
83
+ const currentRoute = route.current;
84
+ const prev = previousRoute.current;
85
+
86
+ // Early-exit guards, top-down:
87
+ //
88
+ // - **Defensive**: `route.current` may be undefined during SSR or
89
+ // pre-start hydration. Not testable from vitest, v8-ignored.
90
+ // - **Skip-initial**: `state.transition.from` is undefined only
91
+ // for the very first state committed by `router.start()`.
92
+ // - **Skip-same-route**: query-only navigations have
93
+ // `transition.from === route.name`. Opt-out via
94
+ // `skipSameRoute: false`.
95
+ // - **Defensive dedupe + missing `previousRoute`**: same `route`
96
+ // ref between `$effect` re-runs is unexpected (createSubscriber
97
+ // only fires on real reference changes); `!prev` is unreachable
98
+ // once `transition.from` is set (core populates them together).
99
+ // Both kept for parity with React; v8-ignored.
100
+ /* v8 ignore start */
101
+ if (!currentRoute) {
102
+ return;
103
+ }
104
+ /* v8 ignore stop */
105
+ if (!currentRoute.transition.from) {
106
+ return;
107
+ }
108
+ if (skipSameRoute && currentRoute.transition.from === currentRoute.name) {
109
+ return;
110
+ }
111
+ /* v8 ignore start */
112
+ if (lastHandledRoute === currentRoute || !prev) {
113
+ return;
114
+ }
115
+ /* v8 ignore stop */
116
+
117
+ lastHandledRoute = currentRoute;
118
+ handler({ route: currentRoute, previousRoute: prev });
119
+ });
120
+ }
@@ -0,0 +1,113 @@
1
+ import { onDestroy } from "svelte";
2
+
3
+ import { useRouter } from "./useRouter.svelte";
4
+
5
+ import type { State } from "@real-router/core";
6
+
7
+ export interface RouteExitContext {
8
+ /** The route being left. */
9
+ route: State;
10
+ /** The route being navigated to. */
11
+ nextRoute: State;
12
+ /**
13
+ * AbortSignal that fires when this navigation is superseded by a later
14
+ * one (rapid clicks). Already filtered: when the handler runs,
15
+ * `signal.aborted` is guaranteed to be `false`. Use
16
+ * `signal.addEventListener("abort", cleanup, { once: true })` for
17
+ * cleanup that must run on cancellation.
18
+ */
19
+ signal: AbortSignal;
20
+ }
21
+
22
+ export interface UseRouteExitOptions {
23
+ /**
24
+ * Skip the handler when `route.name === nextRoute.name`
25
+ * (sort/filter/query-only navigations on the same route). Default:
26
+ * `true`.
27
+ */
28
+ skipSameRoute?: boolean;
29
+ }
30
+
31
+ export type RouteExitHandler = (
32
+ context: RouteExitContext,
33
+ ) => void | Promise<void>;
34
+
35
+ /**
36
+ * Subscribe to the router's leave-window with the universal guards baked
37
+ * in. Wraps `router.subscribeLeave` so consumers don't repeat the same
38
+ * boilerplate every time:
39
+ *
40
+ * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`
41
+ * when the handler would run (rapid navigation superseded a slower
42
+ * one), the handler is skipped entirely.
43
+ * - **Same-route skip**: by default, `route.name === nextRoute.name`
44
+ * short-circuits the handler — query-only navigations skip the work.
45
+ * Opt out with `skipSameRoute: false`.
46
+ *
47
+ * Cleanup is bound to the component via `onDestroy`. Must be called
48
+ * during component initialization (synchronous in `<script>`).
49
+ *
50
+ * If the handler returns a Promise, the router blocks on it. If the
51
+ * Promise resolves, navigation proceeds. If it rejects, the router emits
52
+ * `TRANSITION_CANCELLED`.
53
+ *
54
+ * **Handler reactivity (Svelte):** Svelte composables run **once** at
55
+ * component init; `handler` is captured in closure at the call site. To
56
+ * vary behavior over time, read `$state` / `$derived` values inside the
57
+ * handler body — do not rely on swapping the handler reference.
58
+ *
59
+ * @example Animation
60
+ * ```svelte
61
+ * <script lang="ts">
62
+ * import { useRouteExit } from "@real-router/svelte";
63
+ * let el: HTMLDivElement;
64
+ *
65
+ * useRouteExit(async ({ signal }) => {
66
+ * if (!el) return;
67
+ * el.classList.add("fade-out");
68
+ * const cleanup = () => el.classList.remove("fade-out");
69
+ * signal.addEventListener("abort", cleanup, { once: true });
70
+ * try {
71
+ * el.getBoundingClientRect();
72
+ * await Promise.allSettled(el.getAnimations().map((a) => a.finished));
73
+ * } finally {
74
+ * cleanup();
75
+ * }
76
+ * });
77
+ * </script>
78
+ *
79
+ * <div bind:this={el}>...</div>
80
+ * ```
81
+ *
82
+ * @example Reading rich transition metadata via `nextRoute.transition`
83
+ * ```svelte
84
+ * <script lang="ts">
85
+ * useRouteExit(({ nextRoute }) => {
86
+ * if (nextRoute.transition.segments.deactivated.includes("products")) {
87
+ * productCache.clear();
88
+ * }
89
+ * });
90
+ * </script>
91
+ * ```
92
+ */
93
+ export function useRouteExit(
94
+ handler: RouteExitHandler,
95
+ options?: UseRouteExitOptions,
96
+ ): void {
97
+ const router = useRouter();
98
+ const skipSameRoute = options?.skipSameRoute ?? true;
99
+
100
+ const off = router.subscribeLeave(({ route, nextRoute, signal }) => {
101
+ if (skipSameRoute && route.name === nextRoute.name) {
102
+ return;
103
+ }
104
+
105
+ if (signal.aborted) {
106
+ return;
107
+ }
108
+
109
+ return handler({ route, nextRoute, signal });
110
+ });
111
+
112
+ onDestroy(off);
113
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,10 @@ export { useRouteNode } from "./composables/useRouteNode.svelte";
23
23
 
24
24
  export { useRouterTransition } from "./composables/useRouterTransition.svelte";
25
25
 
26
+ export { useRouteExit } from "./composables/useRouteExit.svelte";
27
+
28
+ export { useRouteEnter } from "./composables/useRouteEnter.svelte";
29
+
26
30
  // Actions
27
31
  export { createLinkAction } from "./actions/link.svelte";
28
32
 
@@ -36,6 +40,18 @@ export { ROUTER_KEY, NAVIGATOR_KEY, ROUTE_KEY } from "./context";
36
40
  // Types
37
41
  export type { LinkProps, RouteContext } from "./types";
38
42
 
43
+ export type {
44
+ RouteExitContext,
45
+ RouteExitHandler,
46
+ UseRouteExitOptions,
47
+ } from "./composables/useRouteExit.svelte";
48
+
49
+ export type {
50
+ RouteEnterContext,
51
+ RouteEnterHandler,
52
+ UseRouteEnterOptions,
53
+ } from "./composables/useRouteEnter.svelte";
54
+
39
55
  export type { Navigator } from "@real-router/core";
40
56
 
41
57
  export type {