@real-router/svelte 0.2.13 → 0.3.1

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.
Files changed (46) hide show
  1. package/README.md +12 -3
  2. package/dist/RouterProvider.svelte +3 -17
  3. package/dist/actions/link.svelte.d.ts +3 -1
  4. package/dist/actions/link.svelte.js +14 -16
  5. package/dist/components/Lazy.svelte +23 -15
  6. package/dist/components/Link.svelte +4 -3
  7. package/dist/components/RouteView.svelte +24 -19
  8. package/dist/components/RouteView.svelte.d.ts +1 -0
  9. package/dist/components/RouterErrorBoundary.svelte +20 -20
  10. package/dist/composables/useIsActiveRoute.svelte.d.ts +1 -1
  11. package/dist/composables/useIsActiveRoute.svelte.js +1 -1
  12. package/dist/composables/useNavigator.svelte.js +2 -9
  13. package/dist/composables/useRoute.svelte.js +2 -9
  14. package/dist/composables/useRouteNode.svelte.js +2 -17
  15. package/dist/composables/useRouter.svelte.js +2 -9
  16. package/dist/composables/useRouterTransition.svelte.js +2 -2
  17. package/dist/constants.d.ts +4 -0
  18. package/dist/constants.js +3 -0
  19. package/dist/context.d.ts +1 -0
  20. package/dist/context.js +13 -0
  21. package/dist/createRouteContext.svelte.d.ts +9 -0
  22. package/dist/createRouteContext.svelte.js +13 -0
  23. package/dist/dom-utils/index.d.ts +1 -1
  24. package/dist/dom-utils/index.js +1 -1
  25. package/dist/dom-utils/link-utils.d.ts +2 -1
  26. package/dist/dom-utils/link-utils.js +48 -6
  27. package/dist/dom-utils/route-announcer.js +37 -14
  28. package/package.json +5 -5
  29. package/src/RouterProvider.svelte +3 -17
  30. package/src/actions/link.svelte.ts +27 -29
  31. package/src/components/Lazy.svelte +23 -15
  32. package/src/components/Link.svelte +4 -3
  33. package/src/components/RouteView.svelte +24 -19
  34. package/src/components/RouterErrorBoundary.svelte +20 -20
  35. package/src/composables/useIsActiveRoute.svelte.ts +3 -3
  36. package/src/composables/useNavigator.svelte.ts +3 -12
  37. package/src/composables/useRoute.svelte.ts +3 -12
  38. package/src/composables/useRouteNode.svelte.ts +2 -17
  39. package/src/composables/useRouter.svelte.ts +3 -12
  40. package/src/composables/useRouterTransition.svelte.ts +2 -3
  41. package/src/constants.ts +9 -0
  42. package/src/context.ts +17 -0
  43. package/src/createRouteContext.svelte.ts +27 -0
  44. package/dist/composables/useRouterError.svelte.d.ts +0 -4
  45. package/dist/composables/useRouterError.svelte.js +0 -13
  46. package/src/composables/useRouterError.svelte.ts +0 -21
package/README.md CHANGED
@@ -142,6 +142,7 @@ Navigation link with automatic active state detection. Uses `$derived` for href
142
142
  | `activeStrict` | `boolean` | `false` | Exact match only (no ancestor matching) |
143
143
  | `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
144
144
  | `target` | `string` | `undefined` | Link target (`_blank`, etc.) |
145
+ | `onclick` | `(evt: MouseEvent) => void` | `undefined` | Custom click handler. Runs **before** the navigation logic — call `evt.preventDefault()` to suppress navigation. |
145
146
 
146
147
  All other props are spread onto the `<a>` element.
147
148
 
@@ -206,7 +207,13 @@ Declarative error handling for navigation errors. Shows a fallback **alongside**
206
207
  </script>
207
208
 
208
209
  <RouterErrorBoundary
209
- onError={(error) => analytics.track("nav_error", { code: error.code })}
210
+ onError={(error, toRoute, fromRoute) =>
211
+ analytics.track("nav_error", {
212
+ code: error.code,
213
+ to: toRoute?.name,
214
+ from: fromRoute?.name,
215
+ })
216
+ }
210
217
  >
211
218
  {#snippet fallback(error, resetError)}
212
219
  <div class="toast">
@@ -220,6 +227,8 @@ Declarative error handling for navigation errors. Shows a fallback **alongside**
220
227
 
221
228
  Auto-resets on next successful navigation. Works with both `<Link>` and imperative `router.navigate()`.
222
229
 
230
+ **`onError` signature:** `(error, toRoute, fromRoute) => void`. Receives the `RouterError`, the attempted destination (`State | null`), and the previously active route (`State | null`). A throwing `onError` is caught by the boundary, logged via `console.error`, and never breaks reactivity.
231
+
223
232
  ## Actions
224
233
 
225
234
  ### `createLinkAction`
@@ -363,9 +372,9 @@ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
363
372
 
364
373
  ## Examples
365
374
 
366
- 15 runnable examples — each is a standalone Vite app. Run: `cd examples/svelte/basic && pnpm dev`
375
+ 16 runnable examples — each is a standalone Vite app. Run: `cd examples/svelte/basic && pnpm dev`
367
376
 
368
- [basic](../../examples/svelte/basic) · [nested-routes](../../examples/svelte/nested-routes) · [auth-guards](../../examples/svelte/auth-guards) · [data-loading](../../examples/svelte/data-loading) · [lazy-loading](../../examples/svelte/lazy-loading) · [async-guards](../../examples/svelte/async-guards) · [hash-routing](../../examples/svelte/hash-routing) · [persistent-params](../../examples/svelte/persistent-params) · [error-handling](../../examples/svelte/error-handling) · [dynamic-routes](../../examples/svelte/dynamic-routes) · [link-action](../../examples/svelte/link-action) · [lazy-loading-svelte](../../examples/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/svelte/snippets-routing) · [reactive-source](../../examples/svelte/reactive-source) · [combined](../../examples/svelte/combined)
377
+ [basic](../../examples/svelte/basic) · [nested-routes](../../examples/svelte/nested-routes) · [auth-guards](../../examples/svelte/auth-guards) · [data-loading](../../examples/svelte/data-loading) · [lazy-loading](../../examples/svelte/lazy-loading) · [async-guards](../../examples/svelte/async-guards) · [hash-routing](../../examples/svelte/hash-routing) · [persistent-params](../../examples/svelte/persistent-params) · [error-handling](../../examples/svelte/error-handling) · [dynamic-routes](../../examples/svelte/dynamic-routes) · [link-action](../../examples/svelte/link-action) · [lazy-loading-svelte](../../examples/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/svelte/snippets-routing) · [reactive-source](../../examples/svelte/reactive-source) · [search-schema](../../examples/svelte/search-schema) · [combined](../../examples/svelte/combined)
369
378
 
370
379
  ## Related Packages
371
380
 
@@ -5,6 +5,7 @@
5
5
  import { setContext } from "svelte";
6
6
 
7
7
  import { createReactiveSource } from "./createReactiveSource.svelte";
8
+ import { createRouteContext } from "./createRouteContext.svelte";
8
9
  import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
9
10
 
10
11
  import type { Router } from "@real-router/core";
@@ -26,26 +27,11 @@
26
27
  const navigator = getNavigator(router);
27
28
  const source = createRouteSource(router);
28
29
  const reactive = createReactiveSource(source);
30
+ const routeContext = createRouteContext(navigator, reactive);
29
31
 
30
32
  setContext(ROUTER_KEY, router);
31
33
  setContext(NAVIGATOR_KEY, navigator);
32
- setContext(ROUTE_KEY, {
33
- navigator,
34
- get route() {
35
- return {
36
- get current() {
37
- return reactive.current.route;
38
- },
39
- };
40
- },
41
- get previousRoute() {
42
- return {
43
- get current() {
44
- return reactive.current.previousRoute;
45
- },
46
- };
47
- },
48
- });
34
+ setContext(ROUTE_KEY, routeContext);
49
35
  </script>
50
36
 
51
37
  {@render children()}
@@ -5,6 +5,7 @@ export interface LinkActionParams {
5
5
  params?: Params;
6
6
  options?: NavigationOptions;
7
7
  }
8
+ type LinkAction = (node: HTMLElement, params: LinkActionParams) => ActionReturn<LinkActionParams>;
8
9
  /**
9
10
  * Factory function that captures router context during component initialization.
10
11
  * Must be called during component init (not inside event handlers or effects).
@@ -23,4 +24,5 @@ export interface LinkActionParams {
23
24
  * <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
24
25
  * ```
25
26
  */
26
- export declare function createLinkAction(): (node: HTMLElement, params: LinkActionParams) => ActionReturn<LinkActionParams>;
27
+ export declare function createLinkAction(): LinkAction;
28
+ export {};
@@ -1,5 +1,5 @@
1
- import { getContext } from "svelte";
2
- import { ROUTER_KEY } from "../context";
1
+ import { ROUTER_KEY, getContextOrThrow } from "../context";
2
+ import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
3
3
  import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
4
4
  /**
5
5
  * Factory function that captures router context during component initialization.
@@ -20,30 +20,28 @@ import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
20
20
  * ```
21
21
  */
22
22
  export function createLinkAction() {
23
- const router = getContext(ROUTER_KEY);
24
- if (!router) {
25
- throw new Error("createLinkAction must be called inside a RouterProvider");
26
- }
23
+ const router = getContextOrThrow(ROUTER_KEY, "createLinkAction");
27
24
  return function link(node, params) {
28
25
  let currentParams = params;
29
26
  applyLinkA11y(node);
27
+ function navigate() {
28
+ router
29
+ .navigate(currentParams.name, currentParams.params ?? EMPTY_PARAMS, currentParams.options ?? EMPTY_OPTIONS)
30
+ .catch(NOOP);
31
+ }
30
32
  function handleClick(evt) {
31
33
  if (!shouldNavigate(evt))
32
34
  return;
35
+ if (node instanceof HTMLAnchorElement &&
36
+ node.getAttribute("target") === "_blank") {
37
+ return;
38
+ }
33
39
  evt.preventDefault();
34
- // router is guaranteed to exist due to check in factory
35
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
36
- router
37
- .navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
38
- .catch(() => { });
40
+ navigate();
39
41
  }
40
42
  function handleKeyDown(evt) {
41
43
  if (evt.key === "Enter" && !(node instanceof HTMLButtonElement)) {
42
- // router is guaranteed to exist due to check in factory
43
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
44
- router
45
- .navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
46
- .catch(() => { });
44
+ navigate();
47
45
  }
48
46
  }
49
47
  node.addEventListener("click", handleClick);
@@ -9,26 +9,33 @@
9
9
  fallback?: Component | undefined;
10
10
  } = $props();
11
11
 
12
- let LoadedComponent = $state<Component | null>(null);
13
- let error = $state<Error | null>(null);
14
- let loading = $state(true);
12
+ type LazyState =
13
+ | { status: "loading" }
14
+ | { status: "ready"; component: Component }
15
+ | { status: "error"; error: Error };
16
+
17
+ let state = $state<LazyState>({ status: "loading" });
15
18
 
16
19
  $effect(() => {
17
- loading = true;
18
- error = null;
19
- LoadedComponent = null;
20
+ state = { status: "loading" };
20
21
  let active = true;
21
22
 
22
23
  loader()
23
24
  .then((module) => {
24
25
  if (!active) return;
25
- LoadedComponent = module.default;
26
- loading = false;
26
+ if (!module || typeof module.default === "undefined") {
27
+ throw new Error(
28
+ "[real-router] Lazy loader resolved without a `default` export.",
29
+ );
30
+ }
31
+ state = { status: "ready", component: module.default };
27
32
  })
28
- .catch((err) => {
33
+ .catch((err: unknown) => {
29
34
  if (!active) return;
30
- error = err;
31
- loading = false;
35
+ state = {
36
+ status: "error",
37
+ error: err instanceof Error ? err : new Error(String(err)),
38
+ };
32
39
  });
33
40
 
34
41
  return () => {
@@ -37,11 +44,12 @@
37
44
  });
38
45
  </script>
39
46
 
40
- {#if loading && fallback}
47
+ {#if state.status === "loading" && fallback}
41
48
  {@const Fallback = fallback}
42
49
  <Fallback />
43
- {:else if error}
44
- <p>Error loading component: {error.message}</p>
45
- {:else if LoadedComponent}
50
+ {:else if state.status === "error"}
51
+ <p>Error loading component: {state.error.message}</p>
52
+ {:else if state.status === "ready"}
53
+ {@const LoadedComponent = state.component}
46
54
  <LoadedComponent />
47
55
  {/if}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { useIsActiveRoute } from "../composables/useIsActiveRoute.svelte";
3
3
  import { useRouter } from "../composables/useRouter.svelte";
4
+ import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
4
5
  import {
5
6
  shouldNavigate,
6
7
  buildHref,
@@ -12,8 +13,8 @@
12
13
 
13
14
  let {
14
15
  routeName,
15
- routeParams = {} as Params,
16
- routeOptions = {} as NavigationOptions,
16
+ routeParams = EMPTY_PARAMS,
17
+ routeOptions = EMPTY_OPTIONS,
17
18
  class: className = undefined,
18
19
  activeClassName = "active",
19
20
  activeStrict = false,
@@ -64,7 +65,7 @@
64
65
  }
65
66
 
66
67
  evt.preventDefault();
67
- router.navigate(routeName, routeParams, routeOptions).catch(() => {});
68
+ router.navigate(routeName, routeParams, routeOptions).catch(NOOP);
68
69
  }
69
70
  </script>
70
71
 
@@ -1,6 +1,26 @@
1
+ <script lang="ts" module>
2
+ import { startsWithSegment } from "@real-router/route-utils";
3
+
4
+ export function getActiveSegment(
5
+ routeName: string,
6
+ node: string,
7
+ snippets: Record<string, unknown>,
8
+ ): string {
9
+ const prefix = node ? `${node}.` : "";
10
+
11
+ for (const segment in snippets) {
12
+ if (segment === "notFound") continue;
13
+ if (startsWithSegment(routeName, prefix + segment)) {
14
+ return segment;
15
+ }
16
+ }
17
+
18
+ return "";
19
+ }
20
+ </script>
21
+
1
22
  <script lang="ts">
2
23
  import { UNKNOWN_ROUTE } from "@real-router/core";
3
- import { startsWithSegment } from "@real-router/route-utils";
4
24
 
5
25
  import { useRouteNode } from "../composables/useRouteNode.svelte";
6
26
 
@@ -17,29 +37,14 @@
17
37
  } = $props();
18
38
 
19
39
  const routeContext = useRouteNode(nodeName);
20
-
21
- function getActiveSegment(
22
- routeName: string,
23
- node: string,
24
- snippets: Record<string, unknown>,
25
- ): string {
26
- for (const segment of Object.keys(snippets)) {
27
- const fullSegmentName = node ? `${node}.${segment}` : segment;
28
-
29
- if (startsWithSegment(routeName, fullSegmentName)) {
30
- return segment;
31
- }
32
- }
33
-
34
- return "";
35
- }
36
40
  </script>
37
41
 
38
42
  {#if routeContext.route.current}
39
43
  {@const route = routeContext.route.current}
40
44
  {@const segment = getActiveSegment(route.name, nodeName, segmentSnippets)}
41
- {#if segment && segmentSnippets[segment]}
42
- {@render (segmentSnippets[segment] as Snippet)()}
45
+ {#if segment}
46
+ {@const snippet = segmentSnippets[segment] as Snippet}
47
+ {@render snippet()}
43
48
  {:else if route.name === UNKNOWN_ROUTE && notFound}
44
49
  {@render notFound()}
45
50
  {/if}
@@ -1,3 +1,4 @@
1
+ export declare function getActiveSegment(routeName: string, node: string, snippets: Record<string, unknown>): string;
1
2
  import type { Snippet } from "svelte";
2
3
  type $$ComponentProps = {
3
4
  nodeName: string;
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
+ import { createDismissableError } from "@real-router/sources";
2
3
  import { untrack } from "svelte";
3
4
 
4
- import { useRouterError } from "../composables/useRouterError.svelte";
5
+ import { useRouter } from "../composables/useRouter.svelte";
6
+ import { createReactiveSource } from "../createReactiveSource.svelte";
5
7
 
6
8
  import type { RouterError, State } from "@real-router/core";
7
9
  import type { Snippet } from "svelte";
@@ -18,30 +20,28 @@
18
20
 
19
21
  let { children, fallback, onError }: Props = $props();
20
22
 
21
- const snapshot = useRouterError();
22
- let dismissedVersion = $state(-1);
23
-
24
- const visibleError = $derived(
25
- snapshot.current.version > dismissedVersion
26
- ? snapshot.current.error
27
- : null,
28
- );
29
-
30
- function resetError(): void {
31
- dismissedVersion = snapshot.current.version;
32
- }
23
+ const router = useRouter();
24
+ const snapshot = createReactiveSource(createDismissableError(router));
33
25
 
34
26
  $effect(() => {
35
- if (snapshot.current.error) {
36
- const { error, toRoute, fromRoute } = snapshot.current;
37
- untrack(() => {
27
+ const snap = snapshot.current;
28
+ if (!snap.error) return;
29
+
30
+ const { error, toRoute, fromRoute } = snap;
31
+ untrack(() => {
32
+ try {
38
33
  onError?.(error, toRoute, fromRoute);
39
- });
40
- }
34
+ } catch (callbackError) {
35
+ console.error(
36
+ "[real-router] RouterErrorBoundary onError handler threw:",
37
+ callbackError,
38
+ );
39
+ }
40
+ });
41
41
  });
42
42
  </script>
43
43
 
44
44
  {@render children?.()}
45
- {#if visibleError}
46
- {@render fallback(visibleError, resetError)}
45
+ {#if snapshot.current.error}
46
+ {@render fallback(snapshot.current.error, snapshot.current.resetError)}
47
47
  {/if}
@@ -1,4 +1,4 @@
1
1
  import type { Params } from "@real-router/core";
2
- export declare function useIsActiveRoute(routeName: string, params?: Params, strict?: boolean, ignoreQueryParams?: boolean): {
2
+ export declare function useIsActiveRoute(routeName: string, params: Params | undefined, strict: boolean, ignoreQueryParams: boolean): {
3
3
  readonly current: boolean;
4
4
  };
@@ -1,7 +1,7 @@
1
1
  import { createActiveRouteSource } from "@real-router/sources";
2
2
  import { createReactiveSource } from "../createReactiveSource.svelte";
3
3
  import { useRouter } from "./useRouter.svelte";
4
- export function useIsActiveRoute(routeName, params, strict = false, ignoreQueryParams = true) {
4
+ export function useIsActiveRoute(routeName, params, strict, ignoreQueryParams) {
5
5
  const router = useRouter();
6
6
  const source = createActiveRouteSource(router, routeName, params, {
7
7
  strict,
@@ -1,9 +1,2 @@
1
- import { getContext } from "svelte";
2
- import { NAVIGATOR_KEY } from "../context";
3
- export const useNavigator = () => {
4
- const navigator = getContext(NAVIGATOR_KEY);
5
- if (!navigator) {
6
- throw new Error("useNavigator must be used within a RouterProvider");
7
- }
8
- return navigator;
9
- };
1
+ import { NAVIGATOR_KEY, getContextOrThrow } from "../context";
2
+ export const useNavigator = () => getContextOrThrow(NAVIGATOR_KEY, "useNavigator");
@@ -1,9 +1,2 @@
1
- import { getContext } from "svelte";
2
- import { ROUTE_KEY } from "../context";
3
- export const useRoute = () => {
4
- const routeContext = getContext(ROUTE_KEY);
5
- if (!routeContext) {
6
- throw new Error("useRoute must be used within a RouterProvider");
7
- }
8
- return routeContext;
9
- };
1
+ import { ROUTE_KEY, getContextOrThrow } from "../context";
2
+ export const useRoute = () => getContextOrThrow(ROUTE_KEY, "useRoute");
@@ -1,27 +1,12 @@
1
1
  import { getNavigator } from "@real-router/core";
2
2
  import { createRouteNodeSource } from "@real-router/sources";
3
3
  import { createReactiveSource } from "../createReactiveSource.svelte";
4
+ import { createRouteContext } from "../createRouteContext.svelte";
4
5
  import { useRouter } from "./useRouter.svelte";
5
6
  export function useRouteNode(nodeName) {
6
7
  const router = useRouter();
7
8
  const navigator = getNavigator(router);
8
9
  const source = createRouteNodeSource(router, nodeName);
9
10
  const reactive = createReactiveSource(source);
10
- return {
11
- navigator,
12
- get route() {
13
- return {
14
- get current() {
15
- return reactive.current.route;
16
- },
17
- };
18
- },
19
- get previousRoute() {
20
- return {
21
- get current() {
22
- return reactive.current.previousRoute;
23
- },
24
- };
25
- },
26
- };
11
+ return createRouteContext(navigator, reactive);
27
12
  }
@@ -1,9 +1,2 @@
1
- import { getContext } from "svelte";
2
- import { ROUTER_KEY } from "../context";
3
- export const useRouter = () => {
4
- const router = getContext(ROUTER_KEY);
5
- if (!router) {
6
- throw new Error("useRouter must be used within a RouterProvider");
7
- }
8
- return router;
9
- };
1
+ import { ROUTER_KEY, getContextOrThrow } from "../context";
2
+ export const useRouter = () => getContextOrThrow(ROUTER_KEY, "useRouter");
@@ -1,8 +1,8 @@
1
- import { createTransitionSource } from "@real-router/sources";
1
+ import { getTransitionSource } from "@real-router/sources";
2
2
  import { createReactiveSource } from "../createReactiveSource.svelte";
3
3
  import { useRouter } from "./useRouter.svelte";
4
4
  export function useRouterTransition() {
5
5
  const router = useRouter();
6
- const source = createTransitionSource(router);
6
+ const source = getTransitionSource(router);
7
7
  return createReactiveSource(source);
8
8
  }
@@ -0,0 +1,4 @@
1
+ import type { NavigationOptions, Params } from "@real-router/core";
2
+ export declare const EMPTY_PARAMS: Params;
3
+ export declare const EMPTY_OPTIONS: NavigationOptions;
4
+ export declare const NOOP: () => void;
@@ -0,0 +1,3 @@
1
+ export const EMPTY_PARAMS = Object.freeze({});
2
+ export const EMPTY_OPTIONS = Object.freeze({});
3
+ export const NOOP = () => { };
package/dist/context.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export declare const ROUTER_KEY = "real-router:router";
2
2
  export declare const NAVIGATOR_KEY = "real-router:navigator";
3
3
  export declare const ROUTE_KEY = "real-router:route";
4
+ export declare function getContextOrThrow<T>(key: string, consumerName: string): T;
package/dist/context.js CHANGED
@@ -1,3 +1,16 @@
1
+ import { getContext } from "svelte";
1
2
  export const ROUTER_KEY = "real-router:router";
2
3
  export const NAVIGATOR_KEY = "real-router:navigator";
3
4
  export const ROUTE_KEY = "real-router:route";
5
+ // The type parameter is used by the caller to narrow the return type.
6
+ // ESLint's no-unnecessary-type-parameters sees only a single textual use of T
7
+ // (the return type) — but each call site supplies a different T, so it is not
8
+ // unnecessary. Inline generic helpers are a standard pattern for typed context.
9
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
10
+ export function getContextOrThrow(key, consumerName) {
11
+ const value = getContext(key);
12
+ if (!value) {
13
+ throw new Error(`${consumerName} must be used within a RouterProvider`);
14
+ }
15
+ return value;
16
+ }
@@ -0,0 +1,9 @@
1
+ import type { Navigator, State } from "@real-router/core";
2
+ import type { RouteContext } from "./types";
3
+ export interface RouteSnapshot {
4
+ readonly route: State | undefined;
5
+ readonly previousRoute: State | undefined;
6
+ }
7
+ export declare function createRouteContext(navigator: Navigator, reactive: {
8
+ readonly current: RouteSnapshot;
9
+ }): RouteContext;
@@ -0,0 +1,13 @@
1
+ export function createRouteContext(navigator, reactive) {
2
+ const route = {
3
+ get current() {
4
+ return reactive.current.route;
5
+ },
6
+ };
7
+ const previousRoute = {
8
+ get current() {
9
+ return reactive.current.previousRoute;
10
+ },
11
+ };
12
+ return { navigator, route, previousRoute };
13
+ }
@@ -1,3 +1,3 @@
1
1
  export { createRouteAnnouncer } from "./route-announcer.js";
2
- export { shouldNavigate, buildHref, buildActiveClassName, applyLinkA11y, } from "./link-utils.js";
2
+ export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
3
3
  export type { RouteAnnouncerOptions } from "./route-announcer.js";
@@ -1,2 +1,2 @@
1
1
  export { createRouteAnnouncer } from "./route-announcer.js";
2
- export { shouldNavigate, buildHref, buildActiveClassName, applyLinkA11y, } from "./link-utils.js";
2
+ export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
@@ -2,4 +2,5 @@ import type { Router, Params } from "@real-router/core";
2
2
  export declare function shouldNavigate(evt: MouseEvent): boolean;
3
3
  export declare function buildHref(router: Router, routeName: string, routeParams: Params): string | undefined;
4
4
  export declare function buildActiveClassName(isActive: boolean, activeClassName: string | undefined, baseClassName: string | undefined): string | undefined;
5
- export declare function applyLinkA11y(element: HTMLElement): void;
5
+ export declare function shallowEqual(prev: object | undefined, next: object | undefined): boolean;
6
+ export declare function applyLinkA11y(element: HTMLElement | null | undefined): void;
@@ -9,7 +9,10 @@ export function buildHref(router, routeName, routeParams) {
9
9
  try {
10
10
  const buildUrl = router.buildUrl;
11
11
  if (buildUrl) {
12
- return buildUrl(routeName, routeParams);
12
+ const url = buildUrl(routeName, routeParams);
13
+ if (url !== undefined) {
14
+ return url;
15
+ }
13
16
  }
14
17
  return router.buildPath(routeName, routeParams);
15
18
  }
@@ -18,23 +21,62 @@ export function buildHref(router, routeName, routeParams) {
18
21
  return undefined;
19
22
  }
20
23
  }
24
+ function parseTokens(value) {
25
+ return value ? (value.match(/\S+/g) ?? []) : [];
26
+ }
21
27
  export function buildActiveClassName(isActive, activeClassName, baseClassName) {
22
28
  if (isActive && activeClassName) {
23
- return baseClassName
24
- ? `${baseClassName} ${activeClassName}`.trim()
25
- : activeClassName;
29
+ const activeTokens = parseTokens(activeClassName);
30
+ if (activeTokens.length === 0) {
31
+ return baseClassName ?? undefined;
32
+ }
33
+ if (!baseClassName) {
34
+ return activeTokens.join(" ");
35
+ }
36
+ const baseTokens = parseTokens(baseClassName);
37
+ const seen = new Set(baseTokens);
38
+ for (const token of activeTokens) {
39
+ if (!seen.has(token)) {
40
+ seen.add(token);
41
+ baseTokens.push(token);
42
+ }
43
+ }
44
+ return baseTokens.join(" ");
26
45
  }
27
46
  return baseClassName ?? undefined;
28
47
  }
48
+ export function shallowEqual(prev, next) {
49
+ if (Object.is(prev, next)) {
50
+ return true;
51
+ }
52
+ if (!prev || !next) {
53
+ return false;
54
+ }
55
+ const prevKeys = Object.keys(prev);
56
+ if (prevKeys.length !== Object.keys(next).length) {
57
+ return false;
58
+ }
59
+ const prevRecord = prev;
60
+ const nextRecord = next;
61
+ for (const key of prevKeys) {
62
+ if (!Object.is(prevRecord[key], nextRecord[key])) {
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
29
68
  export function applyLinkA11y(element) {
69
+ if (!element) {
70
+ return;
71
+ }
30
72
  if (element instanceof HTMLAnchorElement ||
31
73
  element instanceof HTMLButtonElement) {
32
74
  return;
33
75
  }
34
- if (!element.getAttribute("role")) {
76
+ if (!element.hasAttribute("role")) {
35
77
  element.setAttribute("role", "link");
36
78
  }
37
- if (!element.getAttribute("tabindex")) {
79
+ if (!element.hasAttribute("tabindex")) {
38
80
  element.setAttribute("tabindex", "0");
39
81
  }
40
82
  }