@pyreon/router 0.11.5 → 0.11.7

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/src/router.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { createContext, onUnmount, useContext } from "@pyreon/core"
2
- import { computed, signal } from "@pyreon/reactivity"
3
- import { buildNameIndex, buildPath, resolveRoute, stringifyQuery } from "./match"
4
- import { ScrollManager } from "./scroll"
1
+ import { createContext, onUnmount, useContext } from '@pyreon/core'
2
+ import { computed, signal } from '@pyreon/reactivity'
3
+ import { buildNameIndex, buildPath, resolveRoute, stringifyQuery } from './match'
4
+ import { ScrollManager } from './scroll'
5
5
  import {
6
6
  type AfterEachHook,
7
7
  type Blocker,
@@ -16,13 +16,13 @@ import {
16
16
  type Router,
17
17
  type RouterInstance,
18
18
  type RouterOptions,
19
- } from "./types"
19
+ } from './types'
20
20
 
21
21
  // Evaluated once at module load — collapses to `true` in browser / happy-dom,
22
22
  // `false` on the server. Using a constant avoids per-call `typeof` branches
23
23
  // that are uncoverable in test environments.
24
- const _isBrowser = typeof window !== "undefined"
25
- const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
24
+ const _isBrowser = typeof window !== 'undefined'
25
+ const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
26
26
 
27
27
  // ─── Router context ───────────────────────────────────────────────────────────
28
28
  // Context-based access: isolated per request in SSR (ALS-backed via
@@ -51,19 +51,19 @@ export function useRouter(): Router {
51
51
  const router = useContext(RouterContext) ?? _activeRouter
52
52
  if (!router)
53
53
  throw new Error(
54
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
54
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
55
55
  )
56
56
  return router
57
57
  }
58
58
 
59
59
  export function useRoute<TPath extends string = string>(): () => ResolvedRoute<
60
- import("./types").ExtractParams<TPath> & Record<string, string>,
60
+ import('./types').ExtractParams<TPath> & Record<string, string>,
61
61
  Record<string, string>
62
62
  > {
63
63
  const router = useContext(RouterContext) ?? _activeRouter
64
64
  if (!router)
65
65
  throw new Error(
66
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
66
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
67
67
  )
68
68
  return router.currentRoute as never
69
69
  }
@@ -82,7 +82,7 @@ export function onBeforeRouteLeave(guard: NavigationGuard): () => void {
82
82
  const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
83
83
  if (!router)
84
84
  throw new Error(
85
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
85
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
86
86
  )
87
87
  // Register as a global guard that only fires when leaving the current route
88
88
  const currentMatched = router.currentRoute().matched
@@ -111,7 +111,7 @@ export function onBeforeRouteUpdate(guard: NavigationGuard): () => void {
111
111
  const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
112
112
  if (!router)
113
113
  throw new Error(
114
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
114
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
115
115
  )
116
116
  const currentMatched = router.currentRoute().matched
117
117
  const wrappedGuard: NavigationGuard = (to, from) => {
@@ -143,7 +143,7 @@ export function useBlocker(fn: BlockerFn): Blocker {
143
143
  const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
144
144
  if (!router)
145
145
  throw new Error(
146
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
146
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
147
147
  )
148
148
  router._blockers.add(fn)
149
149
 
@@ -154,13 +154,13 @@ export function useBlocker(fn: BlockerFn): Blocker {
154
154
  }
155
155
  : null
156
156
  if (beforeUnloadHandler) {
157
- window.addEventListener("beforeunload", beforeUnloadHandler)
157
+ window.addEventListener('beforeunload', beforeUnloadHandler)
158
158
  }
159
159
 
160
160
  const remove = () => {
161
161
  router._blockers.delete(fn)
162
162
  if (beforeUnloadHandler) {
163
- window.removeEventListener("beforeunload", beforeUnloadHandler)
163
+ window.removeEventListener('beforeunload', beforeUnloadHandler)
164
164
  }
165
165
  }
166
166
 
@@ -202,14 +202,14 @@ export function useIsActive(path: string, exact = false): () => boolean {
202
202
  const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
203
203
  if (!router)
204
204
  throw new Error(
205
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
205
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
206
206
  )
207
207
  return () => {
208
208
  const current = router.currentRoute().path
209
209
  if (exact) {
210
210
  return matchSegments(current, path, true)
211
211
  }
212
- if (path === "/") return current === "/"
212
+ if (path === '/') return current === '/'
213
213
  // Segment-aware prefix: /admin matches /admin/users but NOT /admin-panel
214
214
  return matchSegments(current, path, false)
215
215
  }
@@ -217,14 +217,14 @@ export function useIsActive(path: string, exact = false): () => boolean {
217
217
 
218
218
  /** Match current path segments against a pattern that may contain `:param` segments. */
219
219
  function matchSegments(current: string, pattern: string, exact: boolean): boolean {
220
- const cs = current.split("/").filter(Boolean)
221
- const ps = pattern.split("/").filter(Boolean)
220
+ const cs = current.split('/').filter(Boolean)
221
+ const ps = pattern.split('/').filter(Boolean)
222
222
  if (exact) {
223
223
  if (cs.length !== ps.length) return false
224
- return ps.every((seg, i) => seg.startsWith(":") || seg === cs[i])
224
+ return ps.every((seg, i) => seg.startsWith(':') || seg === cs[i])
225
225
  }
226
226
  if (ps.length > cs.length) return false
227
- return ps.every((seg, i) => seg.startsWith(":") || seg === cs[i])
227
+ return ps.every((seg, i) => seg.startsWith(':') || seg === cs[i])
228
228
  }
229
229
 
230
230
  export function useSearchParams<T extends Record<string, string>>(
@@ -233,7 +233,7 @@ export function useSearchParams<T extends Record<string, string>>(
233
233
  const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null
234
234
  if (!router)
235
235
  throw new Error(
236
- "[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.",
236
+ '[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.',
237
237
  )
238
238
  const get = (): T => {
239
239
  const query = router.currentRoute().query
@@ -254,15 +254,15 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
254
254
  const opts: RouterOptions = Array.isArray(options) ? { routes: options } : options
255
255
  const {
256
256
  routes,
257
- mode = "hash",
257
+ mode = 'hash',
258
258
  scrollBehavior,
259
259
  onError,
260
260
  maxCacheSize = 100,
261
- trailingSlash = "strip",
261
+ trailingSlash = 'strip',
262
262
  } = opts
263
263
 
264
264
  // Base path only applies to history mode — hash-based routing already namespaces via #
265
- const base = mode === "history" ? normalizeBase(opts.base ?? "") : ""
265
+ const base = mode === 'history' ? normalizeBase(opts.base ?? '') : ''
266
266
 
267
267
  // Pre-built O(1) name → record index. Computed once at startup.
268
268
  const nameIndex = buildNameIndex(routes)
@@ -280,21 +280,21 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
280
280
  const getInitialLocation = (): string => {
281
281
  // SSR: use explicitly provided url (strip base if present)
282
282
  if (opts.url) return stripBase(opts.url, base)
283
- if (!_isBrowser) return "/"
284
- if (mode === "history") {
283
+ if (!_isBrowser) return '/'
284
+ if (mode === 'history') {
285
285
  return stripBase(window.location.pathname, base) + window.location.search
286
286
  }
287
287
  const hash = window.location.hash
288
- return hash.startsWith("#") ? hash.slice(1) || "/" : "/"
288
+ return hash.startsWith('#') ? hash.slice(1) || '/' : '/'
289
289
  }
290
290
 
291
291
  const getCurrentLocation = (): string => {
292
292
  if (!_isBrowser) return currentPath()
293
- if (mode === "history") {
293
+ if (mode === 'history') {
294
294
  return stripBase(window.location.pathname, base) + window.location.search
295
295
  }
296
296
  const hash = window.location.hash
297
- return hash.startsWith("#") ? hash.slice(1) || "/" : "/"
297
+ return hash.startsWith('#') ? hash.slice(1) || '/' : '/'
298
298
  }
299
299
 
300
300
  // ── Signals ───────────────────────────────────────────────────────────────
@@ -307,12 +307,12 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
307
307
  let _hashchangeHandler: (() => void) | null = null
308
308
 
309
309
  if (_isBrowser) {
310
- if (mode === "history") {
310
+ if (mode === 'history') {
311
311
  _popstateHandler = () => currentPath.set(getCurrentLocation())
312
- window.addEventListener("popstate", _popstateHandler)
312
+ window.addEventListener('popstate', _popstateHandler)
313
313
  } else {
314
314
  _hashchangeHandler = () => currentPath.set(getCurrentLocation())
315
- window.addEventListener("hashchange", _hashchangeHandler)
315
+ window.addEventListener('hashchange', _hashchangeHandler)
316
316
  }
317
317
  }
318
318
 
@@ -322,9 +322,9 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
322
322
  // ── Navigation ────────────────────────────────────────────────────────────
323
323
 
324
324
  type GuardOutcome =
325
- | { action: "continue" }
326
- | { action: "cancel" }
327
- | { action: "redirect"; target: string }
325
+ | { action: 'continue' }
326
+ | { action: 'cancel' }
327
+ | { action: 'redirect'; target: string }
328
328
 
329
329
  async function evaluateGuard(
330
330
  guard: NavigationGuard,
@@ -333,15 +333,15 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
333
333
  gen: number,
334
334
  ): Promise<GuardOutcome> {
335
335
  const result = await runGuard(guard, to, from)
336
- if (gen !== _navGen) return { action: "cancel" }
337
- if (result === false) return { action: "cancel" }
338
- if (typeof result === "string") return { action: "redirect", target: result }
339
- return { action: "continue" }
336
+ if (gen !== _navGen) return { action: 'cancel' }
337
+ if (result === false) return { action: 'cancel' }
338
+ if (typeof result === 'string') return { action: 'redirect', target: result }
339
+ return { action: 'continue' }
340
340
  }
341
341
 
342
342
  async function runRouteGuards(
343
343
  records: RouteRecord[],
344
- guardKey: "beforeLeave" | "beforeEnter",
344
+ guardKey: 'beforeLeave' | 'beforeEnter',
345
345
  to: ResolvedRoute,
346
346
  from: ResolvedRoute,
347
347
  gen: number,
@@ -352,10 +352,10 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
352
352
  const routeGuards = Array.isArray(raw) ? raw : [raw]
353
353
  for (const guard of routeGuards) {
354
354
  const outcome = await evaluateGuard(guard, to, from, gen)
355
- if (outcome.action !== "continue") return outcome
355
+ if (outcome.action !== 'continue') return outcome
356
356
  }
357
357
  }
358
- return { action: "continue" }
358
+ return { action: 'continue' }
359
359
  }
360
360
 
361
361
  async function runGlobalGuards(
@@ -366,9 +366,9 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
366
366
  ): Promise<GuardOutcome> {
367
367
  for (const guard of globalGuards) {
368
368
  const outcome = await evaluateGuard(guard, to, from, gen)
369
- if (outcome.action !== "continue") return outcome
369
+ if (outcome.action !== 'continue') return outcome
370
370
  }
371
- return { action: "continue" }
371
+ return { action: 'continue' }
372
372
  }
373
373
 
374
374
  function processLoaderResult(
@@ -377,7 +377,7 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
377
377
  ac: AbortController,
378
378
  to: ResolvedRoute,
379
379
  ): boolean {
380
- if (result.status === "fulfilled") {
380
+ if (result.status === 'fulfilled') {
381
381
  router._loaderData.set(record, result.value)
382
382
  return true
383
383
  }
@@ -392,18 +392,18 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
392
392
 
393
393
  function syncBrowserUrl(path: string, replace: boolean): void {
394
394
  if (!_isBrowser) return
395
- const url = mode === "history" ? `${base}${path}` : `#${path}`
395
+ const url = mode === 'history' ? `${base}${path}` : `#${path}`
396
396
  if (replace) {
397
- window.history.replaceState(null, "", url)
397
+ window.history.replaceState(null, '', url)
398
398
  } else {
399
- window.history.pushState(null, "", url)
399
+ window.history.pushState(null, '', url)
400
400
  }
401
401
  }
402
402
 
403
403
  function resolveRedirect(to: ResolvedRoute): string | null {
404
404
  const leaf = to.matched[to.matched.length - 1]
405
405
  if (!leaf?.redirect) return null
406
- return sanitizePath(typeof leaf.redirect === "function" ? leaf.redirect(to) : leaf.redirect)
406
+ return sanitizePath(typeof leaf.redirect === 'function' ? leaf.redirect(to) : leaf.redirect)
407
407
  }
408
408
 
409
409
  async function runAllGuards(
@@ -411,11 +411,11 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
411
411
  from: ResolvedRoute,
412
412
  gen: number,
413
413
  ): Promise<GuardOutcome> {
414
- const leaveOutcome = await runRouteGuards(from.matched, "beforeLeave", to, from, gen)
415
- if (leaveOutcome.action !== "continue") return leaveOutcome
414
+ const leaveOutcome = await runRouteGuards(from.matched, 'beforeLeave', to, from, gen)
415
+ if (leaveOutcome.action !== 'continue') return leaveOutcome
416
416
 
417
- const enterOutcome = await runRouteGuards(to.matched, "beforeEnter", to, from, gen)
418
- if (enterOutcome.action !== "continue") return enterOutcome
417
+ const enterOutcome = await runRouteGuards(to.matched, 'beforeEnter', to, from, gen)
418
+ if (enterOutcome.action !== 'continue') return enterOutcome
419
419
 
420
420
  return runGlobalGuards(guards, to, from, gen)
421
421
  }
@@ -521,12 +521,12 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
521
521
  to: ResolvedRoute,
522
522
  from: ResolvedRoute,
523
523
  gen: number,
524
- ): Promise<"continue" | "cancel"> {
524
+ ): Promise<'continue' | 'cancel'> {
525
525
  for (const blocker of router._blockers) {
526
526
  const blocked = await blocker(to, from)
527
- if (gen !== _navGen || blocked) return "cancel"
527
+ if (gen !== _navGen || blocked) return 'cancel'
528
528
  }
529
- return "continue"
529
+ return 'continue'
530
530
  }
531
531
 
532
532
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: navigation is inherently multi-step
@@ -536,7 +536,7 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
536
536
  // biome-ignore lint/suspicious/noConsole: dev-only warning
537
537
  console.warn(
538
538
  `[Pyreon] Navigation to "${rawPath}" aborted: redirect depth exceeded 10 levels. ` +
539
- "This likely indicates a redirect loop in your route configuration.",
539
+ 'This likely indicates a redirect loop in your route configuration.',
540
540
  )
541
541
  }
542
542
  return
@@ -556,15 +556,15 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
556
556
  }
557
557
 
558
558
  const blockerResult = await checkBlockers(to, from, gen)
559
- if (blockerResult !== "continue") {
559
+ if (blockerResult !== 'continue') {
560
560
  loadingSignal.update((n) => n - 1)
561
561
  return
562
562
  }
563
563
 
564
564
  const guardOutcome = await runAllGuards(to, from, gen)
565
- if (guardOutcome.action !== "continue") {
565
+ if (guardOutcome.action !== 'continue') {
566
566
  loadingSignal.update((n) => n - 1)
567
- if (guardOutcome.action === "redirect") {
567
+ if (guardOutcome.action === 'redirect') {
568
568
  return navigate(sanitizePath(guardOutcome.target), replace, redirectDepth + 1)
569
569
  }
570
570
  return
@@ -620,7 +620,7 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
620
620
  | string
621
621
  | { name: string; params?: Record<string, string>; query?: Record<string, string> },
622
622
  ) {
623
- if (typeof location === "string") {
623
+ if (typeof location === 'string') {
624
624
  const resolved = resolveRelativePath(location, currentPath())
625
625
  return navigate(sanitizePath(resolved), false)
626
626
  }
@@ -638,7 +638,7 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
638
638
  | string
639
639
  | { name: string; params?: Record<string, string>; query?: Record<string, string> },
640
640
  ) {
641
- if (typeof location === "string") {
641
+ if (typeof location === 'string') {
642
642
  const resolved = resolveRelativePath(location, currentPath())
643
643
  return navigate(sanitizePath(resolved), true)
644
644
  }
@@ -687,11 +687,11 @@ export function createRouter(options: RouterOptions | RouteRecord[]): Router {
687
687
 
688
688
  destroy() {
689
689
  if (_popstateHandler) {
690
- window.removeEventListener("popstate", _popstateHandler)
690
+ window.removeEventListener('popstate', _popstateHandler)
691
691
  _popstateHandler = null
692
692
  }
693
693
  if (_hashchangeHandler) {
694
- window.removeEventListener("hashchange", _hashchangeHandler)
694
+ window.removeEventListener('hashchange', _hashchangeHandler)
695
695
  _hashchangeHandler = null
696
696
  }
697
697
  guards.length = 0
@@ -743,47 +743,47 @@ function resolveNamedPath(
743
743
  ): string {
744
744
  const record = index.get(name)
745
745
  if (!record) {
746
- return "/"
746
+ return '/'
747
747
  }
748
748
  let path = buildPath(record.path, params)
749
749
  const qs = Object.entries(query)
750
750
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
751
- .join("&")
751
+ .join('&')
752
752
  if (qs) path += `?${qs}`
753
753
  return path
754
754
  }
755
755
 
756
756
  /** Normalize a base path: ensure leading `/`, strip trailing `/`. */
757
757
  function normalizeBase(raw: string): string {
758
- if (!raw) return ""
758
+ if (!raw) return ''
759
759
  let b = raw
760
- if (!b.startsWith("/")) b = `/${b}`
761
- if (b.endsWith("/")) b = b.slice(0, -1)
760
+ if (!b.startsWith('/')) b = `/${b}`
761
+ if (b.endsWith('/')) b = b.slice(0, -1)
762
762
  return b
763
763
  }
764
764
 
765
765
  /** Strip the base prefix from a full URL path. Returns the app-relative path. */
766
766
  function stripBase(path: string, base: string): string {
767
767
  if (!base) return path
768
- if (path === base || path === `${base}/`) return "/"
768
+ if (path === base || path === `${base}/`) return '/'
769
769
  if (path.startsWith(`${base}/`)) return path.slice(base.length)
770
770
  return path
771
771
  }
772
772
 
773
773
  /** Normalize trailing slash on a path according to the configured strategy. */
774
- function normalizeTrailingSlash(path: string, strategy: "strip" | "add" | "ignore"): string {
775
- if (strategy === "ignore" || path === "/") return path
774
+ function normalizeTrailingSlash(path: string, strategy: 'strip' | 'add' | 'ignore'): string {
775
+ if (strategy === 'ignore' || path === '/') return path
776
776
  // Split off query string + hash so we only touch the path portion
777
- const qIdx = path.indexOf("?")
778
- const hIdx = path.indexOf("#")
777
+ const qIdx = path.indexOf('?')
778
+ const hIdx = path.indexOf('#')
779
779
  const endIdx = qIdx >= 0 ? qIdx : hIdx >= 0 ? hIdx : path.length
780
780
  const pathPart = path.slice(0, endIdx)
781
781
  const suffix = path.slice(endIdx)
782
- if (strategy === "strip") {
783
- return pathPart.length > 1 && pathPart.endsWith("/") ? pathPart.slice(0, -1) + suffix : path
782
+ if (strategy === 'strip') {
783
+ return pathPart.length > 1 && pathPart.endsWith('/') ? pathPart.slice(0, -1) + suffix : path
784
784
  }
785
785
  // strategy === "add"
786
- return !pathPart.endsWith("/") ? `${pathPart}/${suffix}` : path
786
+ return !pathPart.endsWith('/') ? `${pathPart}/${suffix}` : path
787
787
  }
788
788
 
789
789
  /**
@@ -791,32 +791,32 @@ function normalizeTrailingSlash(path: string, strategy: "strip" | "add" | "ignor
791
791
  * Non-relative paths are returned as-is.
792
792
  */
793
793
  function resolveRelativePath(to: string, from: string): string {
794
- if (!to.startsWith("./") && !to.startsWith("../") && to !== "." && to !== "..") return to
794
+ if (!to.startsWith('./') && !to.startsWith('../') && to !== '.' && to !== '..') return to
795
795
 
796
796
  // Split current path into segments, drop the last segment (file-like resolution)
797
- const fromSegments = from.split("/").filter(Boolean)
797
+ const fromSegments = from.split('/').filter(Boolean)
798
798
  fromSegments.pop()
799
799
 
800
- const toSegments = to.split("/").filter(Boolean)
800
+ const toSegments = to.split('/').filter(Boolean)
801
801
  for (const seg of toSegments) {
802
- if (seg === "..") {
802
+ if (seg === '..') {
803
803
  fromSegments.pop()
804
- } else if (seg !== ".") {
804
+ } else if (seg !== '.') {
805
805
  fromSegments.push(seg)
806
806
  }
807
807
  }
808
- return `/${fromSegments.join("/")}`
808
+ return `/${fromSegments.join('/')}`
809
809
  }
810
810
 
811
811
  /** Block unsafe navigation targets: javascript/data/vbscript URIs and absolute URLs. */
812
812
  function sanitizePath(path: string): string {
813
813
  const trimmed = path.trim()
814
814
  if (/^(?:javascript|data|vbscript):/i.test(trimmed)) {
815
- return "/"
815
+ return '/'
816
816
  }
817
817
  // Block absolute URLs and protocol-relative URLs — router only handles same-origin paths
818
818
  if (/^\/\/|^https?:/i.test(trimmed)) {
819
- return "/"
819
+ return '/'
820
820
  }
821
821
  return path
822
822
  }
package/src/scroll.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ResolvedRoute, RouterOptions } from "./types"
1
+ import type { ResolvedRoute, RouterOptions } from './types'
2
2
 
3
3
  /**
4
4
  * Scroll restoration manager.
@@ -8,9 +8,9 @@ import type { ResolvedRoute, RouterOptions } from "./types"
8
8
  */
9
9
  export class ScrollManager {
10
10
  private readonly _positions = new Map<string, number>()
11
- private readonly _behavior: RouterOptions["scrollBehavior"]
11
+ private readonly _behavior: RouterOptions['scrollBehavior']
12
12
 
13
- constructor(behavior: RouterOptions["scrollBehavior"] = "top") {
13
+ constructor(behavior: RouterOptions['scrollBehavior'] = 'top') {
14
14
  this._behavior = behavior
15
15
  }
16
16
 
@@ -22,9 +22,9 @@ export class ScrollManager {
22
22
 
23
23
  /** Call after navigation is committed — applies scroll behavior */
24
24
  restore(to: ResolvedRoute, from: ResolvedRoute): void {
25
- const behavior = (to.meta.scrollBehavior as typeof this._behavior) ?? this._behavior ?? "top"
25
+ const behavior = (to.meta.scrollBehavior as typeof this._behavior) ?? this._behavior ?? 'top'
26
26
 
27
- if (typeof behavior === "function") {
27
+ if (typeof behavior === 'function') {
28
28
  const saved = this._positions.get(to.path) ?? null
29
29
  const result = behavior(to, from, saved)
30
30
  this._applyResult(result, to.path)
@@ -34,19 +34,19 @@ export class ScrollManager {
34
34
  this._applyResult(behavior, to.path)
35
35
  }
36
36
 
37
- private _applyResult(result: "top" | "restore" | "none" | number, toPath: string): void {
38
- if (result === "none") return
39
- if (result === "top" || result === undefined) {
40
- window.scrollTo({ top: 0, behavior: "instant" as ScrollBehavior })
37
+ private _applyResult(result: 'top' | 'restore' | 'none' | number, toPath: string): void {
38
+ if (result === 'none') return
39
+ if (result === 'top' || result === undefined) {
40
+ window.scrollTo({ top: 0, behavior: 'instant' as ScrollBehavior })
41
41
  return
42
42
  }
43
- if (result === "restore") {
43
+ if (result === 'restore') {
44
44
  const saved = this._positions.get(toPath) ?? 0
45
- window.scrollTo({ top: saved, behavior: "instant" as ScrollBehavior })
45
+ window.scrollTo({ top: saved, behavior: 'instant' as ScrollBehavior })
46
46
  return
47
47
  }
48
48
  // At this point result must be a number (all string cases handled above)
49
- window.scrollTo({ top: result as number, behavior: "instant" as ScrollBehavior })
49
+ window.scrollTo({ top: result as number, behavior: 'instant' as ScrollBehavior })
50
50
  }
51
51
 
52
52
  getSavedPosition(path: string): number | null {