@keenmate/svelte-spa-router 5.1.1 → 5.2.0-rc02

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/CHANGELOG.md CHANGED
@@ -5,6 +5,192 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.2.0-rc02] - 2026-05-01
9
+
10
+ ### Fixed
11
+ - **`ReferenceError: __PACKAGE_NAME__ is not defined` masking real route errors** — The global API setup in `src/lib/index.js` referenced bundler-injected placeholders (`__PACKAGE_NAME__`, `__VERSION__`, `__AUTHOR__`, `__LICENSE__`, `__REPOSITORY__`, `__HOMEPAGE__`) with `typeof X !== 'undefined'` fallbacks. Consumer Vite optimizers (esbuild prebundle) were not preserving the guard, leaving bare references that threw `ReferenceError` at module-init. The error surfaced asynchronously during route activation and masked real errors thrown by route components.
12
+ - Replaced the placeholder pattern with a direct `import pkg from '../../package.json'` — works universally because npm always ships `package.json` and all modern bundlers handle JSON imports natively.
13
+ - No consumer-side bundler config required.
14
+
15
+ - **Svelte 5 `state_referenced_locally` warnings on every consumer dev build** — `Router.svelte` parsed the `routes` prop into `routesList` at script top level, which captures only the initial value. Svelte 5 emitted three `state_referenced_locally` warnings (lines 172, 173, 177) in any consumer's dev console, and silently ignored prop swaps if a consumer ever replaced `routes`.
16
+ - Wrapped the parse logic in `$derived.by(() => { ... })` so the closure captures `routes` reactively. Warnings are gone and the routes prop is now properly reactive — replacing it rebuilds `routesList` and takes effect on the next navigation.
17
+ - `findParentRoute()` and `findMatchingRoute()` already read `routesList` per-call, so no other changes were needed.
18
+
19
+ - **`navigationContext()` was never `null` after any `push()` call, breaking the "no context" check in consumer code** — `push()` and `replace()` internally inject `_routeName` (for referrer tracking) and optionally `__scrollBehavior` (for scroll control) into the stored navigation context. Previously these internal keys leaked through the public `navigationContext()` accessor, so `navigationContext()` returned `{ _routeName: '/path' }` (truthy) instead of `null` even when the consumer hadn't passed any context. Code patterns like `{#if !navigationContext()}` or `if (ctx === null)` silently broke. The `NavigationContextDemo` example was a victim: clicking "Back to List" called `push('/navigation-context-demo')`, expecting `ctx` to become null and the list view to re-render — but `ctx` was `{ _routeName: ... }`, so none of the `{#if}` branches matched and the middle area went blank.
20
+ - `navigationContext()` now filters out the router's internal keys (`_routeName`, `__scrollBehavior`) and returns `null` if nothing user-visible remains.
21
+ - Added an internal `getRawNavigationContext()` accessor that returns the full state including internal keys. `Router.svelte` uses this for its own internal reads (referrer-route lookup, scroll-behavior override). Not exposed in the public type declarations.
22
+ - **Migration:** if you were depending on seeing `_routeName` or `__scrollBehavior` in `navigationContext()` output (you almost certainly weren't), use `getRawNavigationContext()` from utils — but these are internal flags and the contract is that they may change without notice.
23
+ - vitest coverage in `navigation.test.js`: two new cases assert `navigationContext()` returns `null` after a no-context `push()`, and that user keys pass through while `_routeName` is filtered.
24
+
25
+ - **`push('/path', {}, queryObject)` and `replace(...)` silently dropped the querystring** — The multi-parameter signature `push(route, params, query, context)` only serialized the `query` object when the first argument was a *named route*. When the first argument was a path string (starting with `/`), the path branch stored `query` on `opts` but the downstream code only read `opts.href`, so the query object was never applied to the URL. The named-route branch worked because it routes through `buildUrl()`, which does the serialization. Existing tests only covered the named-route case and the path case with empty `{}`, so the regression was invisible.
26
+ - Extracted `serializeQuery(query)` into `helpers/url-helpers.svelte.js` and reuse it in both `buildUrl()` (named branch, behavior unchanged) and `push()` / `replace()` (path branch).
27
+ - When the path already contains a `?`, the serialized query is appended with `&`; otherwise with `?`.
28
+ - Added 6 vitest cases covering: path + query object, URL encoding of keys/values, skipping `null` / `undefined`, merging with a path-embedded query, empty-query no-op, and the equivalent `replace()` case.
29
+
30
+ - **`onNotFound` was never emitted when a `'*'` catch-all route was configured** — `runRoutingPipeline()` only dispatched `notFound` on the true no-match path (`!ctx.match`). With a `'*'` route present, `regexparam` turned it into a wildcard that matched every unmatched URL, so `ctx.match` was always truthy and the event was suppressed. Apps that configured a `'*': NotFound` route to render a 404 page lost the ability to *observe* 404s programmatically (for logging, analytics, error tracking).
31
+ - In `src/lib/Router.svelte`, after the no-match early-exit, dispatch `notFound` whenever `ctx.match.routeItem.path === '*'`. The catch-all component still renders — the event is purely additive.
32
+ - The check uses the same exact-`'*'` test as the existing `isCatchAll` flag at line 727, so subtree wildcards like `'/admin/*'` (whose `routeItem.path` is `/admin/*`, not `*`) and the synthetic unauthorized-route match (whose `routeItem.path` is the unauthorized route) are unaffected.
33
+ - Added an e2e test in `e2e/router-events.spec.ts` asserting that both the catch-all component renders *and* `onNotFound` fires with the correct `{ location, querystring }` payload. The pre-existing nested-router-without-catch-all test still covers the true no-match path.
34
+
35
+ ### Added
36
+ - **`revalidateCurrentRoute()` + `onRevalidationFailure` — re-check the active route on user state changes** — `hasPermission()` reactivity (above) covers UI element visibility (menus, buttons), but it doesn't cover the case where the user is *sitting on a protected page* when their permissions are revoked. The router checks route conditions only during navigation, so a user already on `/admin` who loses admin permission would stay on `/admin` until they navigated away. This is exactly the websocket-permission-update scenario most real apps need to handle.
37
+ - New `revalidateCurrentRoute()` export from the main module. Re-runs guards and conditions against the currently mounted route without re-mounting the component. On success, nothing visible happens — the component keeps its state (no flicker, no scroll reset, no in-flight form data lost). On failure, the same unauthorized handling that runs for fresh navigation fires here too.
38
+ - New `configurePermissions({ onRevalidationFailure })` callback. When configured, fires *instead of* the standard unauthorized handling for revalidation failures — letting apps show a confirmation dialog, soft-warn the user, or log an audit trail before deciding what to do. If the callback doesn't navigate, the user stays on the current page. The `onConditionsFailed` Router event still fires for consistency with navigation behavior. Pass `null` to clear and fall back to standard handling.
39
+ - Calls to `revalidateCurrentRoute()` within a ~50ms window are coalesced into a single re-validation pass — safe to call on every websocket message.
40
+ - Multiple Router instances (nested routers, zones) each register independently and re-validate their own routes.
41
+ - Implementation: `runRoutingPipeline` accepts a `revalidationOnly` option that (a) skips the `beforeLeave` guards (user isn't navigating away), (b) skips the `routeLoading` event dispatch (nothing's loading), (c) on success returns early before the load/commit phases (preserves the mounted component), (d) on failure routes through the new callback when configured. The existing `loadingId` race-condition mechanism handles overlapping navigation/revalidation cleanly.
42
+ - vitest coverage in `navigation.test.js` (5 new cases: debounce, coalescing, multi-listener, unregister, throwing-listener tolerance) and `permissions.test.js` (4 new cases for the handler getter/setter contract).
43
+ - e2e: new `RevalidateTest` + `RevalidateProtected` fixtures and `e2e/revalidate.spec.ts` (3 tests: callback fires on protected-route downgrade, no callback when still authorized, no-op on unprotected routes). App.svelte wires up an `onRevalidationFailure` recorder for the spec to assert on.
44
+ - **Bug fix found while wiring this up:** the pipeline's internal `isPermissionFailure` value is the permissions object (truthy) rather than the literal `true`. Coerced to a boolean before exposing it in the callback's `detail`.
45
+
46
+ - **`setCurrentUser()` / `getCurrentUser()` — reactivity-by-default for `hasPermission()`** — Previously, `hasPermission()` only re-evaluated in `{#if}` blocks if the consumer's configured `getCurrentUser` happened to read reactive state. The README's canonical example used `getCurrentUser: () => get(currentUser)` (Svelte 4 store, non-reactive read), so following the docs literally meant `{#if hasPermission(...)}` only updated on navigation. A websocket pushing a permission change wouldn't update the UI until the user clicked a link.
47
+ - Added module-level `$state` (`currentUserState`) inside `permissions.svelte.js`. The default `currentUserGetter` now reads from this rune, so any `hasPermission()` call inside a reactive context (`{#if}`, `$derived`, `$effect`) automatically tracks user changes.
48
+ - New `setCurrentUser(user)` export — consumers call this on login/logout/websocket update; every reactive `hasPermission()` call site re-evaluates immediately. No subscription wiring needed on the consumer side.
49
+ - New `getCurrentUser()` export — symmetric reader for cases like `setCurrentUser({ ...getCurrentUser(), permissions: newPerms })`.
50
+ - `configurePermissions({ getCurrentUser })` still works for consumers who already maintain their own reactive user store (and remains the right choice when they do). Pass `getCurrentUser: null` to explicitly reset back to the default state-backed getter.
51
+ - **Migration:** existing apps keep working unchanged. New apps can skip `getCurrentUser` and use `setCurrentUser` instead — typically less code and guaranteed-reactive without thinking about it. The README, `permissions.d.ts`, and `ai/permissions.txt` now recommend this path and call out the non-reactive footgun explicitly.
52
+ - vitest coverage (`src/tests/permissions.test.js`): 5 new cases — `setCurrentUser`/`getCurrentUser` round-trip, `hasPermission` reflecting writes, explicit `getCurrentUser` overriding the default, `getCurrentUser: null` resetting back, and logged-out (`null`) handling.
53
+
54
+ - **Diagnostic warning when `shouldDisplayLoadingOnRouteLoad` routes never call `hideLoading()`** — Routes configured with `wrap({ shouldDisplayLoadingOnRouteLoad: true, loadingComponent: ... })` mount the real component immediately but keep it hidden under the loading component, waiting for the component itself to call `hideLoading()` from `@keenmate/svelte-spa-router/helpers/route-metadata` once data is ready. Previously, if the consumer forgot to call `hideLoading()` (or threw before reaching it, or hit a code path that didn't call it), the loading screen stayed up forever — no console message, no timeout, no recovery short of navigating away. The author hit this themselves while writing the e2e fixture and spent ten minutes debugging a "broken" wrap option.
55
+ - `startRouteLoading()` in `route-metadata.svelte.js` now schedules a `setTimeout(console.warn, 10_000)` that fires only if `isRouteLoading` is still true at the threshold. Bypasses the configurable logger (matches the project convention that misconfiguration warnings always print). The message names the option, names the required call, and links to the README.
56
+ - `hideLoading()` and a subsequent `startRouteLoading()` both clear the timer so the warning never fires when things are working normally.
57
+ - vitest coverage in `route-metadata.test.js`: three new cases (warning fires after threshold, warning doesn't fire when `hideLoading()` is called in time, warning timer resets on subsequent `startRouteLoading()`).
58
+
59
+ - **Pending `waitForRouteReady()` promises are resolved when a new navigation starts** — Previously, if a user navigated away from a `shouldDisplayLoadingOnRouteLoad` route before its `hideLoading()` fired, the next `startRouteLoading()` clobbered `routeReadyResolvers = []` without resolving the pending entry. The old pipeline's `await waitForRouteReady()` was orphaned and leaked one unresolvable promise per abandoned navigation. After resolving, it would also have raced with the new pipeline's state writes if not for the new race check.
60
+ - `startRouteLoading()` now resolves any pending resolvers before clearing the array.
61
+ - `Router.svelte` adds a `loadingId !== loadingId` race-condition check immediately after `await waitForRouteReady()`, matching the pattern already used after `pipelineLoadComponent` and `pipelineLoadZoneComponents`. Stale pipeline runs bail cleanly instead of racing.
62
+ - vitest coverage: new case asserting that a second `startRouteLoading()` resolves the first navigation's pending waiter.
63
+
64
+ - **Documented the `shouldDisplayLoadingOnRouteLoad` contract loudly** — `wrap.d.ts`, `wrap.js`, `routes.d.ts`, `helpers/hierarchy.d.ts`, and the README's Pattern 1 section now spell out the "you MUST call `hideLoading()`" requirement and the blank-page failure mode. Old docstring was 14 words; new copy names the failure mode and the dev-mode safety net.
65
+
66
+ - **`relativeLocation` field on every Router event payload** — Nested Routers configured with `prefix="/foo/bar"` define their routes in prefix-relative terms (e.g. `'/known'`, not `'/foo/bar/known'`) and match against the prefix-stripped path internally. But every event payload (`onRouteLoading`, `onRouteLoaded`, `onConditionsFailed`, `onNotFound`) reported the **full app URL** in `location` — so the router used two different notions of "location" depending on whether you looked at route definitions or event payloads. Consumers had to know the difference and strip the prefix themselves to reason about what the nested Router actually saw.
67
+ - Added a new `relativeLocation` field to every dispatched event payload that already carried `location`. For a root Router (no `prefix`), `relativeLocation === location`. For a nested Router with `prefix`, it's the path with the prefix stripped (`/` if stripping leaves it empty). For paths outside the prefix (the rare case where the parent app navigates to something the nested Router doesn't own), `relativeLocation` equals `location`.
68
+ - `location` is preserved unchanged — useful for logging, analytics, and re-navigating with `push()` (which always takes app-wide paths). `relativeLocation` is useful for reasoning about *this* Router's routing decisions.
69
+ - All 7 `dispatchNextTick` sites in `Router.svelte` updated. The field is computed once per pipeline run in `createPipelineContext` and threaded through `ctx`.
70
+ - README event-payload table updated to list the field and explain when it differs from `location`.
71
+ - e2e coverage: extended `e2e/router-events.spec.ts` to assert (1) `relativeLocation === '/missing'` for a nested Router with `prefix="/test/embed"` navigating to `/test/embed/missing`, and (2) `relativeLocation === location` for the root Router with no prefix.
72
+
73
+ ### Changed (docs)
74
+ - **Documented `conditions` failure behavior and the `conditions` vs `permissions` distinction** — Previously the README showed how to *write* a `wrap({ conditions: [...] })` guard and listed `onConditionsFailed` as a Router event, but never explained what the user actually sees when a condition returns `false`: the route component is unmounted, the slot becomes empty, and no built-in fallback UI is rendered. This surprised even the maintainers when writing the e2e suite — assertions assumed conditions failures would mount the same Unauthorized component that the permission system mounts.
75
+ - `README.md`: added a "What happens when a condition returns `false`" callout to the *Route guards (pre-conditions)* section; expanded the *Event handling* example with a payload-shape table (`onRouteLoading` / `onRouteLoaded` / `onConditionsFailed` / `onNotFound`); added a new *Conditions vs Permissions* subsection with a side-by-side comparison covering setup, where the logic lives, what happens on failure (UI + event), and when to choose each.
76
+ - `ai/guards-conditions.txt`: added a `WHAT HAPPENS ON FAILURE` section mirroring the README so the AI-facing summary captures the same fact.
77
+ - **No code change.** The conditions/permissions split is the intended design — conditions are the low-level primitive, permissions are the opinionated wrapper that builds on top. Per the project's "library shouldn't paint default UI" principle (see the toast removal in this same release), conditions stay unopinionated. The behavior just needed to be documented.
78
+
79
+ ### Removed (breaking — rc02 is unreleased)
80
+ - **Built-in error toast removed from `GlobalErrorHandler`** — The library used to render its own `<div class="error-toast">` on caught errors, gated by the `showToast` config flag (`true` by default). The render condition was `toastVisible && errorState.currentError && !config.showErrorComponent`, which made it dead code under the default `navigateSafe` strategy: that strategy calls `clearError()` synchronously after `push()` in the same handler tick, so by the time Svelte's reactive system flushed the `toastVisible = true` update, `errorState.currentError` was already `null` and the toast never rendered. `restart` with `autoRestart: false` and `showError` set `config.showErrorComponent = true`, which the same guard hides — so the toast was effectively only observable for `restart` with `autoRestart: true` during the delay window, and for `custom` strategies that left state untouched.
81
+ - Rather than patch the broken interaction, the toast is removed entirely. Notification UI is the consumer's responsibility — every app already has a preferred toast/snackbar library, and the router's job is to surface the event, not paint pixels.
82
+ - `onError(error, errorInfo, context)` is the supported integration point and was already wired up. Consumers can call their own toast library, Sentry, LogRocket, analytics, etc. from inside that callback.
83
+ - Removed: `showToast` config field (was: `boolean`, default `true`), `toastVisible` / `toastTimeoutId` state, `showToast()` / `dismissToast()` helpers, the toast `{#if}` block in the template, and all `.error-toast` / `.toast-*` CSS in `GlobalErrorHandler.svelte`.
84
+ - Updated: the `GlobalErrorHandlerConfig` TypeScript declaration (`error-handler.d.ts`), the example app (`main.js`, `ErrorHandlingDemo.svelte`, `test/ErrorTest.svelte`), and documentation (`README.md`, `ai/error-handling.txt`, `CLAUDE.md`, `e2e/README.md`, `e2e/error-handling.spec.ts`) to drop `showToast` and point at `onError` as the toast hook.
85
+ - **Migration:** if you were passing `showToast: true`, remove it (TypeScript will flag it). To preserve toast behavior, call your toast library inside `onError` — e.g. `onError: (error) => toast.error(error.message)`.
86
+
87
+ ### Added
88
+ - **Playwright e2e test suite** — Browser-level tests for the router, run against the example app in history mode on port 5050. Dedicated, minimal fixture pages live under `example/src/routes/test/` and are kept separate from the demo routes (which are user-oriented and evolve over time) so assertions stay stable.
89
+ - **16 specs / 98 tests** covering all 20 feature areas from `ai/INDEX.txt`: basic setup & events, navigation (push/replace/pop/goBack + array/object signatures), named routes, route params (required / optional / wildcard), permissions (RBAC + authorization), guards & conditions, hierarchical inheritance, tree structure (`createHierarchy`), link actions (`use:link`, `use:active`), error handling (GlobalErrorHandler), referrer tracking, breadcrumbs / route metadata, debug logging & `window.components` API, querystring helpers, filters, multi-zone, loading states, 404 / NotFound, `wrap()`, and `routeContext`.
90
+ - Playwright auto-starts the example dev server and reuses an already-running one (so `make dev` in another terminal is fine).
91
+ - Each fixture surfaces router state via `data-testid` nodes; action buttons use `data-testid="btn-<action>"`. Reload always resets fixture state.
92
+ - New npm scripts: `test:e2e`, `test:e2e:install`, `test:e2e:ui`, `test:e2e:headed`. New Makefile targets: `test-e2e`, `test-e2e-install`, `test-e2e-ui`.
93
+ - Documentation: `e2e/README.md` covers run instructions, the fixture/spec convention, a coverage matrix, and a 6-step recipe for adding new fixtures. A discoverable in-browser index of all fixtures lives at `/test`.
94
+
95
+ ## [5.2.0-rc01] - 2026-02-18
96
+
97
+ ### Fixed
98
+ - **`routeContext()` function missing / mangled name** (Issue #3) — The exported function was named `routerouteContext()` instead of `routeContext()` due to a find-replace accident during the `userData` → `routeContext` rename. The README also referenced the old name `routeUserData()`.
99
+ - Renamed `routerouteContext()` → `routeContext()` in `route-metadata.svelte.js` (function + all internal variable references)
100
+ - Updated `route-metadata.d.ts` type declaration to match
101
+ - Fixed README.md: `routeUserData` → `routeContext` in all import examples and API reference
102
+
103
+ - **`wrap()` not merging title/breadcrumbs into routeContext** (Issue #3) — `routeTitle()` and `routeBreadcrumbs()` returned empty values for routes defined with `wrap({ title, breadcrumbs })` because `wrap()` never merged these into `routeContext`. The Router's `pipelineComputeMetadata()` only reads from `routeItem.routeContext`, so title and breadcrumbs were silently lost.
104
+ - Fixed in both single-component and zones code paths in `wrap.js`
105
+ - `createRouteDefinition()` already had the merge logic — only `wrap()` was missing it
106
+
107
+ - **Logger TypeScript errors** — Added `.d.ts` type declarations for vendored loglevel libraries
108
+ - Created `src/lib/vendor/loglevel/index.d.ts` and `prefix.d.ts`
109
+ - Removed `@ts-ignore` comments from `logger.ts`
110
+ - `svelte-check` now passes with 0 errors and 0 warnings
111
+
112
+ - **Missing TypeScript declarations for GlobalErrorHandler and ErrorDisplay** (Issue #4) — `Cannot find module '@keenmate/svelte-spa-router/helpers/GlobalErrorHandler' or its corresponding type declarations`
113
+ - Created `GlobalErrorHandler.svelte.d.ts` with `GlobalErrorHandlerProps` and `ErrorComponentProps` interfaces
114
+ - Created `ErrorDisplay.svelte.d.ts` with `ErrorDisplayProps` interface
115
+ - Added `types` field to `./helpers/GlobalErrorHandler` export in `package.json`
116
+ - Added new `./helpers/ErrorDisplay` export to `package.json` (was not exported at all)
117
+
118
+ - **Missing TypeScript declarations for `setHierarchicalRoutesEnabled` and `setIncludeReferrer`** (Issue #5) — `Module has no exported member 'setHierarchicalRoutesEnabled'`
119
+ - Added `setHierarchicalRoutesEnabled()`, `getHierarchicalRoutesEnabled()`, `setIncludeReferrer()`, and `getIncludeReferrer()` declarations to `utils.d.ts`
120
+ - Removed phantom `active.svelte.js` re-export from `index.d.ts` (type declared an export that didn't exist at runtime)
121
+
122
+ ### Added
123
+ - **`defineRoutes()` — Type-safe route definitions** (Issue #2) - Single source of truth for routes, navigation, and URL building
124
+ - Returns `routes` (for `<Router>`), `nav` (navigation helpers), and `paths` (URL builders)
125
+ - Full TypeScript support with IDE autocomplete on route names and parameters
126
+ - Extracts `:param` names from path patterns at the type level — catches typos at compile time
127
+ - `nav.X.push(params)` / `nav.X.replace(params)` — programmatic navigation with autocomplete
128
+ - `nav.X.link(params)` — returns object for `use:link` action
129
+ - `paths.X(params)` — builds URL string for `href` attributes
130
+ - Smart optimization: sync components without options skip `wrap()` overhead
131
+ - Async components and routes with options automatically use `createRoute()`
132
+ - Automatically calls `registerRoutes()` — no separate registration step needed
133
+ - Supports all existing route options: `conditions`, `breadcrumbs`, `permissions`, `loadingComponent`, `props`, `title`, etc.
134
+ - Example:
135
+ ```javascript
136
+ import { defineRoutes } from '@keenmate/svelte-spa-router/routes'
137
+
138
+ const { routes, nav, paths } = defineRoutes({
139
+ home: { path: '/', component: Home },
140
+ user: { path: '/user/:id', component: () => import('./User.svelte') }
141
+ })
142
+
143
+ // <Router {routes} />
144
+ // nav.user.push({ id: 123 }) — autocomplete on 'id'
145
+ // <a href={paths.user({ id: 123 })} use:link>
146
+ ```
147
+
148
+ - **Route Context Demo pages** — Example pages demonstrating `routeContext()`, `routeTitle()`, and `routeBreadcrumbs()` with live output
149
+ - `example/src/routes/RouteContextDemo.svelte` — explains routeContext, shows live values, code examples
150
+ - `example/src/routes/RouteContextTarget.svelte` — target page reached via button, displays its own routeContext
151
+ - Both wired into App.svelte with nav link in Routing dropdown
152
+
153
+ - **Comprehensive test suite** — Expanded from ~156 to 347 passing tests across 16 test files (0 skipped)
154
+ - **New test files:**
155
+ - `route-metadata.test.js` (26 tests) — `updateRouteMetadata`, `routeContext()`, `routeTitle()`, `routeBreadcrumbs()`, `updateBreadcrumb()`, `updateTitle()`, `clearBreadcrumbCache()`, loading state functions
156
+ - `navigation-guard.test.js` (22 tests) — `NavigationCancelledError`, `registerBeforeLeave()`, `runBeforeLeaveGuards()`, `createDirtyCheckGuard()`
157
+ - `error-handler.test.js` (23 tests) — `configureGlobalErrorHandler()`, error state, `shouldIgnoreError()`, restart loop prevention, `createErrorInfo()`, `createRecoveryHelpers()`
158
+ - `filters.test.js` (18 tests) — `configureFilters()`, `filters()` flat/structured modes, `updateFilters()`, custom parse/stringify round-trip
159
+ - `querystring-shared.test.js` (7 tests) — `configureQuerystring()`, `query()` with arrayFormat/arrays config
160
+ - `zones-and-scroll.test.js` (12 tests) — `getZoneComponent()`, `setZoneComponents()`, `restoreScroll()`
161
+ - `logger.test.js` (13 tests) — `enableLogging()`, `disableLogging()`, `setLogLevel()`, `setCategoryLevel()` for all 12 categories, `logStructured()`
162
+ - **Extended test files:**
163
+ - `wrap.test.js` (9 → 42 tests) — zones mode, inheritance flags, `createRouteDefinition()`, `createRoute()`, sync component wrapping, condition normalization, validation errors
164
+ - `navigation.test.js` (10 → 27 tests) — `goBack()`, `loc()`, `routeParams()`/`setParams()`, `navigationContext()`/`setNavigationContext()`, array/object/multi-param push formats, `setIncludeReferrer()`, `setParamReplacementPlaceholder()`
165
+ - `permissions.test.js` (13 → 33 tests) — `createProtectedRouteDefinition()`, `authorizationCallback` execution order/fail-fast, `getUnauthorizedBehavior/Route/Component/Handler()`, `hasExplicitHandler()`, `all:` permission requirement
166
+ - **Removed 34 `it.skip` stubs** that required Svelte component rendering or real browser DOM (deleted 3 empty test files: Router.test.js, hierarchical-routes.test.js, link-action.test.js; trimmed active-action.test.js and querystring-helpers.test.js)
167
+
168
+ ### Documentation
169
+ - **defineRoutes() example page** — Added interactive demo page to example app (`example/src/routes/DefineRoutesDemo.svelte`)
170
+ - Covers basic usage, navigation helpers, path builders, and supported route options
171
+ - Includes interactive playground with real-time output
172
+ - Shows before/after comparison with manual route definitions
173
+ - **Example app navbar rework** — Replaced flat navigation with grouped dropdown menus
174
+ - 5 dropdown groups: Navigation, URL & Data, Routing, Errors, Security
175
+ - CSS hover-based dropdowns (no JavaScript state management)
176
+ - **AI Assistant Documentation** - Added 15 concise text files in `./ai` folder optimized for AI assistants
177
+ - Plain text format (no markdown) with bullet-style structure for efficient AI parsing
178
+ - Files organized by feature: basic-setup, navigation, named-routes, route-params, permissions, guards-conditions, hierarchical-routes, tree-structure, link-actions, error-handling, referrer-tracking, debug-logging, import-patterns, utilities, breadcrumbs
179
+ - Includes correct/incorrect usage patterns (✅/❌) for common mistakes
180
+ - Code examples designed for copy-paste usage
181
+ - Complements CLAUDE.md by providing quick-reference documentation
182
+ - Aimed at helping AI coding assistants (like Claude, Cursor, Copilot) quickly understand router functionality
183
+ - **Breadcrumbs Documentation** - Added comprehensive `ai/breadcrumbs.txt` covering breadcrumb navigation system
184
+ - Basic breadcrumb definition and structure
185
+ - Accessing breadcrumbs in components via `routeBreadcrumbs()` helper
186
+ - Breadcrumb component examples with navigation and styling
187
+ - Dynamic breadcrumb updates using `updateBreadcrumb(id, updates)` after data loads
188
+ - Integration with route parameters for dynamic segments
189
+ - Hierarchical breadcrumb inheritance with automatic concatenation
190
+ - Tree structure support with `createHierarchy()`
191
+ - Best practices and common patterns
192
+ - Debugging with ROUTER:METADATA logging category
193
+
8
194
  ## [5.1.1] - 2025-11-30
9
195
 
10
196
  ### Fixed
@@ -35,27 +221,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
35
221
  - Shows common pattern with page definitions array
36
222
  - Explains the "Route X not found in registry" error and how to fix it
37
223
 
38
- ## [Unreleased]
39
-
40
- ### Documentation
41
- - **AI Assistant Documentation** - Added 15 concise text files in `./ai` folder optimized for AI assistants
42
- - Plain text format (no markdown) with bullet-style structure for efficient AI parsing
43
- - Files organized by feature: basic-setup, navigation, named-routes, route-params, permissions, guards-conditions, hierarchical-routes, tree-structure, link-actions, error-handling, referrer-tracking, debug-logging, import-patterns, utilities, breadcrumbs
44
- - Includes correct/incorrect usage patterns (✅/❌) for common mistakes
45
- - Code examples designed for copy-paste usage
46
- - Complements CLAUDE.md by providing quick-reference documentation
47
- - Aimed at helping AI coding assistants (like Claude, Cursor, Copilot) quickly understand router functionality
48
- - **Breadcrumbs Documentation** - Added comprehensive `ai/breadcrumbs.txt` covering breadcrumb navigation system
49
- - Basic breadcrumb definition and structure
50
- - Accessing breadcrumbs in components via `routeBreadcrumbs()` helper
51
- - Breadcrumb component examples with navigation and styling
52
- - Dynamic breadcrumb updates using `updateBreadcrumb(id, updates)` after data loads
53
- - Integration with route parameters for dynamic segments
54
- - Hierarchical breadcrumb inheritance with automatic concatenation
55
- - Tree structure support with `createHierarchy()`
56
- - Best practices and common patterns
57
- - Debugging with ROUTER:METADATA logging category
58
-
59
224
  ## [5.1.0] - 2025-11-20 ✅ Published
60
225
 
61
226
  ### Added