@everystate/router 1.0.0 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ajdin Imsirovic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @everystate/router
2
2
 
3
- **SPA router for EveryState: routing is just state**
3
+ **SPA router for EveryState: routing is just state.**
4
4
 
5
- Treat routing as state. Routes, params, and navigation history live in your EveryState store.
5
+ Treat routing as state. Routes, params, query strings, and navigation history live in your EveryState store at `ui.route.*`.
6
6
 
7
7
  ## Installation
8
8
 
@@ -13,37 +13,93 @@ npm install @everystate/router @everystate/core
13
13
  ## Quick Start
14
14
 
15
15
  ```js
16
- import { createEventState } from '@everystate/core';
16
+ import { createEveryState } from '@everystate/core';
17
17
  import { createRouter } from '@everystate/router';
18
18
 
19
- const store = createEventState({});
19
+ const store = createEveryState({});
20
20
 
21
- const router = createRouter(store, {
22
- routes: {
23
- '/': 'home',
24
- '/about': 'about',
25
- '/user/:id': 'user'
26
- }
27
- });
21
+ const router = createRouter({
22
+ store,
23
+ routes: [
24
+ { path: '/', view: 'home', component: HomeView },
25
+ { path: '/about', view: 'about', component: AboutView },
26
+ { path: '/users/:id', view: 'user', component: UserView },
27
+ ],
28
+ fallback: { view: '404', component: NotFoundView },
29
+ }).start();
28
30
 
29
31
  // Subscribe to route changes
30
- store.subscribe('router.current', (route) => {
31
- console.log('Route changed:', route);
32
+ store.subscribe('ui.route.view', (view) => {
33
+ console.log('View changed:', view);
34
+ });
35
+
36
+ // Navigate programmatically
37
+ router.navigate('/users/123');
38
+
39
+ // Or navigate from anywhere via the store (no router import needed)
40
+ store.set('ui.route.go', '/about');
41
+ store.set('ui.route.go', { path: '/users/1', search: '?tab=posts' });
42
+ store.set('ui.route.go', { query: { tab: 'posts' } }); // patch query only
43
+
44
+ // Access route state
45
+ store.get('ui.route.view'); // 'user'
46
+ store.get('ui.route.path'); // '/users/123'
47
+ store.get('ui.route.params'); // { id: '123' }
48
+ store.get('ui.route.query'); // { tab: 'posts' }
49
+ ```
50
+
51
+ ## Route Config
52
+
53
+ ```js
54
+ createRouter({
55
+ store, // EveryState store instance
56
+ routes: [{ path, view, component }], // route definitions
57
+ fallback: { view, component }, // 404 fallback (optional)
58
+ rootSelector: '[data-route-root]', // mount point (default)
59
+ linkSelector: 'a[data-link]', // intercepted links (default)
60
+ navSelector: 'nav a[data-link]', // auto .active class (default)
61
+ debug: false, // console.debug logging
32
62
  });
63
+ ```
64
+
65
+ ## Component Boot Protocol
33
66
 
34
- // Navigate
35
- router.navigate('/user/123');
67
+ Each route's `component` must expose a `boot()` function:
36
68
 
37
- // Access params
38
- const userId = store.get('router.params.id'); // '123'
69
+ ```js
70
+ export const HomeView = {
71
+ async boot({ store, el, signal, params }) {
72
+ el.innerHTML = '<h1>Home</h1>';
73
+ // Return an unboot function for cleanup
74
+ return () => { el.innerHTML = ''; };
75
+ }
76
+ };
39
77
  ```
40
78
 
41
79
  ## Features
42
80
 
43
- - **Routes as state** Current route stored at `router.current`
44
- - **Params as state** URL params at `router.params.*`
45
- - **History integration** Browser back/forward support
46
- - **Framework-agnostic** Works with any view layer
81
+ - **Routes as state** - current route at `ui.route.view`, `ui.route.path`
82
+ - **Params as state** - URL params at `ui.route.params.*`
83
+ - **Query as state** - query string at `ui.route.query.*`
84
+ - **Store-driven navigation** - `store.set('ui.route.go', '/path')` from anywhere
85
+ - **Transition state** - `ui.route.transitioning` flag for loading indicators
86
+ - **History integration** - browser back/forward with scroll position restore
87
+ - **Base path support** - auto-detects `<base href>` for subdirectory deploys
88
+ - **Nav active state** - auto-toggles `.active` class on `nav a[data-link]` elements
89
+ - **Abort controller** - cancels in-flight boots when navigation is superseded
90
+ - **Focus management** - moves focus to route root for accessibility
91
+ - **Framework-agnostic** - works with any view layer or vanilla DOM
92
+
93
+ ## API
94
+
95
+ | Method | Description |
96
+ |---|---|
97
+ | `router.start()` | Begin listening for clicks and popstate events |
98
+ | `router.stop()` | Remove all listeners and clean up |
99
+ | `router.navigate(path, opts?)` | Navigate to a path (`{ replace, search, restoreScroll }`) |
100
+ | `router.navigateQuery(patch, opts?)` | Patch query params without changing path |
101
+ | `router.navigatePath(path, opts?)` | Navigate keeping current search string |
102
+ | `router.getCurrent()` | Returns `{ view, path, search }` |
47
103
 
48
104
  ## License
49
105
 
package/index.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @everystate/router
3
+ *
4
+ * SPA router for EveryState stores. Routing is just state.
5
+ *
6
+ * Copyright (c) 2026 Ajdin Imsirovic. MIT License.
7
+ */
8
+
9
+ export interface RouteDefinition {
10
+ /** URL pattern, e.g. '/users/:id' */
11
+ path: string;
12
+ /** View key written to ui.route.view */
13
+ view: string;
14
+ /** Component with a boot({ store, el, signal, params }) method */
15
+ component?: { boot: (ctx: BootContext) => Promise<(() => void) | void> | (() => void) | void };
16
+ [key: string]: any;
17
+ }
18
+
19
+ export interface BootContext {
20
+ store: any;
21
+ el: HTMLElement;
22
+ signal: AbortSignal;
23
+ params: Record<string, string>;
24
+ }
25
+
26
+ export interface NavigateOptions {
27
+ replace?: boolean;
28
+ search?: string;
29
+ restoreScroll?: boolean;
30
+ }
31
+
32
+ export interface RouterConfig {
33
+ /** Route definitions */
34
+ routes?: RouteDefinition[];
35
+ /** EveryState store instance */
36
+ store?: any;
37
+ /** CSS selector for the route mount point (default: '[data-route-root]') */
38
+ rootSelector?: string;
39
+ /** Fallback route when nothing matches */
40
+ fallback?: Partial<RouteDefinition> | null;
41
+ /** Enable debug logging (default: false) */
42
+ debug?: boolean;
43
+ /** CSS selector for intercepted links (default: 'a[data-link]') */
44
+ linkSelector?: string;
45
+ /** CSS selector for nav links that get .active class (default: 'nav a[data-link]') */
46
+ navSelector?: string;
47
+ }
48
+
49
+ export interface Router {
50
+ /** Navigate to a pathname */
51
+ navigate(pathname: string, opts?: NavigateOptions): Promise<void>;
52
+ /** Patch query parameters without changing path */
53
+ navigateQuery(patch?: Record<string, string | null | undefined>, opts?: { replace?: boolean }): Promise<void>;
54
+ /** Navigate to a new path, keeping the current search string */
55
+ navigatePath(path: string, opts?: { replace?: boolean }): Promise<void>;
56
+ /** Start listening for click and popstate events, navigate to current URL */
57
+ start(): Router;
58
+ /** Remove all listeners and clean up */
59
+ stop(): Router;
60
+ /** Get the current route state */
61
+ getCurrent(): { view: string | null; path: string | null; search: string };
62
+ }
63
+
64
+ /**
65
+ * Create a SPA router bound to an EveryState store.
66
+ *
67
+ * Store paths written on navigation:
68
+ * ui.route.view - current view key
69
+ * ui.route.path - current pathname
70
+ * ui.route.params - extracted URL params
71
+ * ui.route.query - parsed query string
72
+ * ui.route.transitioning - true during view transitions
73
+ *
74
+ * Store-driven navigation (no router import needed):
75
+ * store.set('ui.route.go', '/about')
76
+ * store.set('ui.route.go', { path: '/users/1', search: '?tab=posts' })
77
+ * store.set('ui.route.go', { query: { tab: 'posts' } })
78
+ */
79
+ export function createRouter(config: RouterConfig): Router;
package/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * @everystate/router
3
3
  *
4
- * EveryState wrapper for @uistate/router
5
- * Re-exports all functionality from the underlying @uistate/router package
4
+ * SPA router for EveryState stores. Routing is just state.
6
5
  */
7
6
 
8
- export * from '@uistate/router';
7
+ export { createRouter } from './router.js';
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@everystate/router",
3
- "version": "1.0.0",
4
- "description": "EveryState Router: SPA router for EventState stores. Routing is just state",
3
+ "version": "1.0.2",
4
+ "description": "EveryState Router: SPA router for EveryState stores. Routing is just state",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "types": "index.d.ts",
7
8
  "keywords": [
8
9
  "everystate",
9
10
  "router",
@@ -23,7 +24,10 @@
23
24
  "type": "git",
24
25
  "url": "https://github.com/ImsirovicAjdin/everystate-router"
25
26
  },
26
- "dependencies": {
27
- "@uistate/router": "^1.0.1"
28
- }
27
+ "files": [
28
+ "index.js",
29
+ "router.js",
30
+ "index.d.ts",
31
+ "README.md"
32
+ ]
29
33
  }
package/router.js ADDED
@@ -0,0 +1,364 @@
1
+ // @everystate/router: SPA router factory for EveryState stores
2
+ // Routing is just state: navigate() writes to store paths, components subscribe.
3
+ //
4
+ // Copyright (c) 2026 Ajdin Imsirovic. MIT License.
5
+
6
+ /**
7
+ * Compile a route pattern like '/users/:id/posts/:postId' into a matcher.
8
+ * Returns { regex, paramNames } for extraction.
9
+ */
10
+ function compilePattern(pattern) {
11
+ const paramNames = [];
12
+ const parts = pattern.split(/:([a-zA-Z_][a-zA-Z0-9_]*)/);
13
+ const regexStr = parts
14
+ .map((part, i) => {
15
+ if (i % 2 === 1) {
16
+ paramNames.push(part);
17
+ return '([^/]+)';
18
+ }
19
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
20
+ })
21
+ .join('');
22
+ return { regex: new RegExp('^' + regexStr + '$'), paramNames };
23
+ }
24
+
25
+ /**
26
+ * Create a SPA router bound to an EveryState store.
27
+ *
28
+ * @param {Object} config
29
+ * @param {Array} config.routes - [{ path: '/users/:id', view: 'user', component: UserView }]
30
+ * @param {Object} [config.store] - EveryState store instance
31
+ * @param {string} [config.rootSelector='[data-route-root]'] - Root element for view mounting
32
+ * @param {Object} [config.fallback] - Fallback route when nothing matches
33
+ * @param {boolean} [config.debug=false]
34
+ * @param {string} [config.linkSelector='a[data-link]'] - Selector for intercepted links
35
+ * @param {string} [config.navSelector='nav a[data-link]'] - Selector for nav links to toggle .active class
36
+ *
37
+ * Store-driven navigation (requires store):
38
+ * Any code with store access can navigate without importing the router:
39
+ * - store.set('ui.route.go', '/about')
40
+ * - store.set('ui.route.go', { path: '/users/1', search: '?tab=posts' })
41
+ * - store.set('ui.route.go', { query: { tab: 'posts' } }) // patch query only
42
+ */
43
+ export function createRouter(config) {
44
+ const {
45
+ routes = [],
46
+ store,
47
+ rootSelector = '[data-route-root]',
48
+ fallback = null,
49
+ debug = false,
50
+ linkSelector = 'a[data-link]',
51
+ navSelector = 'nav a[data-link]',
52
+ } = config;
53
+
54
+ // Pre-compile route patterns
55
+ const compiled = routes.map(route => ({
56
+ ...route,
57
+ ...compilePattern(route.path),
58
+ }));
59
+
60
+ const compiledFallback = fallback
61
+ ? { ...fallback, ...compilePattern(fallback.path || '/*'), params: {} }
62
+ : null;
63
+
64
+ // Detect base path from <base href> if present
65
+ const BASE_PATH = (() => {
66
+ const b = document.querySelector('base[href]');
67
+ if (!b) return '';
68
+ try {
69
+ const u = new URL(b.getAttribute('href'), location.href);
70
+ let p = u.pathname;
71
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
72
+ return p;
73
+ } catch { return ''; }
74
+ })();
75
+
76
+ function stripBase(pathname) {
77
+ if (BASE_PATH && pathname.startsWith(BASE_PATH)) {
78
+ const rest = pathname.slice(BASE_PATH.length) || '/';
79
+ return rest.startsWith('/') ? rest : ('/' + rest);
80
+ }
81
+ return pathname;
82
+ }
83
+
84
+ function withBase(pathname) {
85
+ if (!BASE_PATH) return pathname;
86
+ if (pathname === '/') return BASE_PATH || '/';
87
+ return BASE_PATH + (pathname.startsWith('/') ? '' : '/') + pathname;
88
+ }
89
+
90
+ function normalizePath(p) {
91
+ if (!p) return '/';
92
+ if (p[0] !== '/') p = '/' + p;
93
+ if (p === '/index.html') return '/';
94
+ if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
95
+ return p;
96
+ }
97
+
98
+ function resolve(pathname) {
99
+ const p = normalizePath(pathname);
100
+ for (const route of compiled) {
101
+ const match = p.match(route.regex);
102
+ if (match) {
103
+ const params = {};
104
+ route.paramNames.forEach((name, i) => {
105
+ params[name] = decodeURIComponent(match[i + 1]);
106
+ });
107
+ return { path: route.path, view: route.view, component: route.component, params };
108
+ }
109
+ }
110
+ if (compiledFallback) return { ...compiledFallback, params: {} };
111
+ return null;
112
+ }
113
+
114
+ function getRoot() {
115
+ const el = document.querySelector(rootSelector);
116
+ if (!el) throw new Error('[router] Route root not found: ' + rootSelector);
117
+ return el;
118
+ }
119
+
120
+ function log(...args) {
121
+ if (debug) console.debug('[router]', ...args);
122
+ }
123
+
124
+ function setActiveNav(pathname) {
125
+ document.querySelectorAll(navSelector).forEach(a => {
126
+ const url = new URL(a.getAttribute('href'), location.href);
127
+ const linkPath = normalizePath(stripBase(url.pathname));
128
+ const here = normalizePath(pathname);
129
+ const isExact = linkPath === here;
130
+ const isParent = !isExact && linkPath !== '/' && here.startsWith(linkPath);
131
+ const active = isExact || isParent;
132
+ a.classList.toggle('active', active);
133
+ if (isExact) a.setAttribute('aria-current', 'page');
134
+ else a.removeAttribute('aria-current');
135
+ });
136
+ }
137
+
138
+ // Internal state
139
+ let current = { viewKey: null, unboot: null, path: null, search: '' };
140
+ let navController = null;
141
+ const scrollPositions = new Map();
142
+ history.scrollRestoration = 'manual';
143
+
144
+ /**
145
+ * Navigate to a pathname.
146
+ * @param {string} pathname
147
+ * @param {Object} [opts]
148
+ * @param {boolean} [opts.replace=false]
149
+ * @param {string} [opts.search='']
150
+ * @param {boolean} [opts.restoreScroll=false]
151
+ */
152
+ async function navigate(pathname, { replace = false, search = '', restoreScroll = false } = {}) {
153
+ const root = getRoot();
154
+ const appPath = normalizePath(stripBase(pathname));
155
+ const resolved = resolve(appPath);
156
+
157
+ if (!resolved) {
158
+ log('no route found for:', appPath);
159
+ return;
160
+ }
161
+
162
+ const viewKey = resolved.view;
163
+ const component = resolved.component;
164
+ const searchStr = search && search.startsWith('?') ? search : (search ? ('?' + search) : '');
165
+
166
+ log('navigate', { from: current.path, to: appPath, view: viewKey, params: resolved.params });
167
+
168
+ // Same-route no-op guard
169
+ if (current.path === appPath && current.search === searchStr) {
170
+ return;
171
+ }
172
+
173
+ // Abort in-flight boot
174
+ if (navController) navController.abort();
175
+ navController = new AbortController();
176
+ const { signal } = navController;
177
+
178
+ // Transition start
179
+ const html = document.documentElement;
180
+ html.setAttribute('data-transitioning', 'on');
181
+ if (store) {
182
+ try { store.set('ui.route.transitioning', true); } catch {}
183
+ }
184
+
185
+ // Save scroll position for current route
186
+ if (current.path) {
187
+ scrollPositions.set(current.path, { x: scrollX, y: scrollY });
188
+ if (scrollPositions.size > 50) scrollPositions.delete(scrollPositions.keys().next().value);
189
+ }
190
+
191
+ // Unboot previous view
192
+ if (typeof current.unboot === 'function') {
193
+ try { await current.unboot(); } catch {}
194
+ }
195
+
196
+ // Clear root
197
+ root.replaceChildren();
198
+
199
+ // Boot new view
200
+ let unboot = null;
201
+ if (component && typeof component.boot === 'function') {
202
+ unboot = await component.boot({ store, el: root, signal, params: resolved.params });
203
+ }
204
+
205
+ // Guard: if navigation was superseded during boot, bail out
206
+ if (signal.aborted) return;
207
+
208
+ const prevViewKey = current.viewKey;
209
+ current = { viewKey, unboot, path: appPath, search: searchStr };
210
+
211
+ // Parse query params
212
+ const fullUrl = new URL(location.origin + withBase(appPath) + searchStr);
213
+ const query = {};
214
+ fullUrl.searchParams.forEach((v, k) => { query[k] = v; });
215
+
216
+ // Update store with route state + end transition atomically
217
+ if (store) {
218
+ try {
219
+ store.setMany({
220
+ 'ui.route.view': viewKey,
221
+ 'ui.route.path': appPath,
222
+ 'ui.route.params': resolved.params || {},
223
+ 'ui.route.query': query,
224
+ 'ui.route.transitioning': false,
225
+ });
226
+ } catch {}
227
+ }
228
+
229
+ // Update browser history
230
+ const useReplace = replace;
231
+ if (useReplace) history.replaceState({}, '', withBase(appPath) + searchStr);
232
+ else history.pushState({}, '', withBase(appPath) + searchStr);
233
+
234
+ // Set view attribute on <html> for CSS hooks
235
+ html.setAttribute('data-view', viewKey);
236
+ html.setAttribute('data-transitioning', 'off');
237
+
238
+ // Update nav active state
239
+ setActiveNav(appPath);
240
+
241
+ // Focus management (accessibility)
242
+ if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');
243
+ try { root.focus({ preventScroll: true }); } catch {}
244
+
245
+ // Scroll
246
+ if (restoreScroll) {
247
+ const pos = scrollPositions.get(appPath);
248
+ if (pos) scrollTo(pos.x, pos.y);
249
+ } else {
250
+ scrollTo(0, 0);
251
+ }
252
+
253
+ log('routed', { view: viewKey, path: appPath, params: resolved.params, query });
254
+ }
255
+
256
+ /**
257
+ * Patch query parameters without changing the path.
258
+ * Pass null/undefined/'' as a value to remove a key.
259
+ */
260
+ function navigateQuery(patch = {}, { replace = true } = {}) {
261
+ const params = new URLSearchParams(current.search?.replace(/^\?/, '') || '');
262
+ for (const [k, v] of Object.entries(patch)) {
263
+ if (v === null || v === undefined || v === '') params.delete(k);
264
+ else params.set(k, String(v));
265
+ }
266
+ const searchStr = params.toString();
267
+ const prefixed = searchStr ? ('?' + searchStr) : '';
268
+ const path = current.path || normalizePath(stripBase(location.pathname));
269
+ return navigate(path, { search: prefixed, replace });
270
+ }
271
+
272
+ /**
273
+ * Navigate to a new path, keeping the current search string.
274
+ */
275
+ function navigatePath(path, { replace = true } = {}) {
276
+ const appPath = normalizePath(stripBase(path));
277
+ const searchStr = current.search || '';
278
+ return navigate(appPath, { search: searchStr, replace });
279
+ }
280
+
281
+ // Event handlers
282
+ function onClick(e) {
283
+ const a = e.target.closest(linkSelector);
284
+ if (!a) return;
285
+ if (e.defaultPrevented) return;
286
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
287
+ const href = a.getAttribute('href');
288
+ if (!href) return;
289
+ const url = new URL(href, location.href);
290
+ if (url.origin !== location.origin) return;
291
+ e.preventDefault();
292
+ log('click', { href, text: a.textContent.trim() });
293
+ navigate(url.pathname, { search: url.search }).catch(() => {});
294
+ }
295
+
296
+ function onPop() {
297
+ navigate(location.pathname, {
298
+ replace: true,
299
+ search: location.search,
300
+ restoreScroll: true,
301
+ }).catch(() => {});
302
+ }
303
+
304
+ // Store-driven navigation: write ui.route.go to navigate from anywhere
305
+ let unsubGo = null;
306
+ let processingGo = false;
307
+ if (store) {
308
+ unsubGo = store.subscribe('ui.route.go', (value) => {
309
+ if (processingGo || !value) return;
310
+ processingGo = true;
311
+ try { store.set('ui.route.go', null); } catch {}
312
+ processingGo = false;
313
+
314
+ if (typeof value === 'string') {
315
+ navigate(value).catch(() => {});
316
+ } else if (typeof value === 'object') {
317
+ if (!value.path && value.query) {
318
+ navigateQuery(value.query, { replace: value.replace ?? true }).catch(() => {});
319
+ } else {
320
+ navigate(value.path || '/', {
321
+ search: value.search || '',
322
+ replace: value.replace || false,
323
+ }).catch(() => {});
324
+ }
325
+ }
326
+ });
327
+ }
328
+
329
+ // Public API
330
+ return {
331
+ navigate,
332
+ navigateQuery,
333
+ navigatePath,
334
+
335
+ start() {
336
+ window.addEventListener('click', onClick);
337
+ window.addEventListener('popstate', onPop);
338
+ navigate(location.pathname, {
339
+ replace: true,
340
+ search: location.search,
341
+ restoreScroll: true,
342
+ });
343
+ return this;
344
+ },
345
+
346
+ stop() {
347
+ window.removeEventListener('click', onClick);
348
+ window.removeEventListener('popstate', onPop);
349
+ if (unsubGo) { unsubGo(); unsubGo = null; }
350
+ if (typeof current.unboot === 'function') {
351
+ try { Promise.resolve(current.unboot()).catch(() => {}); } catch {}
352
+ }
353
+ return this;
354
+ },
355
+
356
+ getCurrent() {
357
+ return {
358
+ view: current.viewKey,
359
+ path: current.path,
360
+ search: current.search,
361
+ };
362
+ },
363
+ };
364
+ }