@absolutejs/absolute 0.20.0-beta.169 → 0.20.0-beta.170

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 (238) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +232 -0
  3. package/changelog.json +228 -0
  4. package/dist/angular/browser.js +7 -8
  5. package/dist/angular/browser.js.map +6 -5
  6. package/dist/angular/components/core/streamingSlotRegistrar.js +6 -2
  7. package/dist/angular/components/core/streamingSlotRegistry.js +7 -3
  8. package/dist/angular/index.js +555 -156
  9. package/dist/angular/index.js.map +29 -23
  10. package/dist/angular/server.js +555 -156
  11. package/dist/angular/server.js.map +29 -23
  12. package/dist/build/buildWorker.js +14 -8
  13. package/dist/build.js +4949 -1941
  14. package/dist/build.js.map +72 -42
  15. package/dist/cli/{add-7azr0c83.js → add-6a7pjkn9.js} +11 -7
  16. package/dist/cli/{analyze-3cyek6v3.js → analyze-606fshwa.js} +1 -1
  17. package/dist/cli/{build-25rz8v25.js → build-g81xqbb9.js} +1 -1
  18. package/dist/cli/{compile-4jnfxts9.js → compile-9y1w9zpj.js} +3 -3
  19. package/dist/cli/config/server.js +10699 -1538
  20. package/dist/cli/{dev-9tk5hn2e.js → dev-hyeb89hp.js} +5 -1
  21. package/dist/cli/{doctor-4nv2c2j3.js → doctor-h55y9hj1.js} +1 -1
  22. package/dist/cli/{generate-v75p2fct.js → generate-20x1pkf7.js} +4 -4
  23. package/dist/cli/{htmx-427swtdz.js → htmx-6mz2h7k1.js} +2 -2
  24. package/dist/cli/{index-bhbs6d5z.js → index-32ge2w1f.js} +12 -4
  25. package/dist/cli/{index-a4ws2jwd.js → index-3wz3mktm.js} +1 -1
  26. package/dist/cli/{index-b7hjgknf.js → index-9sksd8c8.js} +13 -4
  27. package/dist/cli/{index-eyzwc7gz.js → index-qbrzvmh9.js} +4 -2
  28. package/dist/cli/index-qw98g1p6.js +269 -0
  29. package/dist/cli/{index-434b7j4a.js → index-vtp20gwf.js} +69 -1
  30. package/dist/cli/{index-h7qhz02e.js → index-wk0pyxrg.js} +3 -0
  31. package/dist/cli/{index-k3y8cb6c.js → index-z6f43b20.js} +1 -1
  32. package/dist/cli/index.js +16 -16
  33. package/dist/cli/{islands-8sgky9gh.js → islands-dc54e5y1.js} +1 -1
  34. package/dist/cli/{ls-4m4yqwww.js → ls-b6zz24nt.js} +1 -1
  35. package/dist/cli/{mobile-hcxjr2gf.js → mobile-ajnnvqtv.js} +3 -3
  36. package/dist/cli/prerender-fqpp0m1j.js +10 -0
  37. package/dist/cli/{remove-1ycjx85k.js → remove-t8x1mrxg.js} +4 -4
  38. package/dist/cli/{start-mbaejxhm.js → start-d6p831ny.js} +3 -3
  39. package/dist/cli/{typecheck-dr068pmh.js → typecheck-stbt0qt5.js} +6 -1
  40. package/dist/cli/{workspace-1sycp9g1.js → workspace-61hh6jq1.js} +1 -1
  41. package/dist/client/chunks/index-6b66nzyf.js +381 -0
  42. package/dist/client/chunks/index-6b66nzyf.js.map +11 -0
  43. package/dist/client/chunks/{islands-rc28k5sx.js → islands-s2ta5ajb.js} +5 -2
  44. package/dist/client/chunks/{islands-rc28k5sx.js.map → islands-s2ta5ajb.js.map} +7 -6
  45. package/dist/client/index.js +160 -3
  46. package/dist/client/index.js.map +6 -4
  47. package/dist/client/navigation.js +764 -0
  48. package/dist/client/navigation.js.map +10 -0
  49. package/dist/client/prefetch.js +33 -375
  50. package/dist/client/prefetch.js.map +3 -4
  51. package/dist/core/streamingSlotRegistrar.js +5 -2
  52. package/dist/core/streamingSlotRegistrar.js.map +5 -4
  53. package/dist/core/streamingSlotRegistry.js +5 -2
  54. package/dist/core/streamingSlotRegistry.js.map +5 -4
  55. package/dist/dev/client/cspNonce.ts +28 -0
  56. package/dist/dev/client/handlers/html.ts +4 -3
  57. package/dist/dev/client/handlers/htmx.ts +3 -2
  58. package/dist/dev/client/handlers/svelte.ts +15 -90
  59. package/dist/dev/client/syncDevtools.ts +2 -1
  60. package/dist/dev/serverBootstrap.js +11 -4
  61. package/dist/i18n/index.js +543 -0
  62. package/dist/i18n/index.js.map +15 -0
  63. package/dist/index.js +6339 -2233
  64. package/dist/index.js.map +89 -54
  65. package/dist/islands/index.js +282 -40
  66. package/dist/islands/index.js.map +14 -10
  67. package/dist/mobile/browser.js +7 -2
  68. package/dist/mobile/browser.js.map +5 -4
  69. package/dist/mobile/index.js +7 -2
  70. package/dist/mobile/index.js.map +5 -4
  71. package/dist/mobile/remoteMacAgentEntry.js +1 -1
  72. package/dist/react/components/browser/index.js +94 -4
  73. package/dist/react/components/index.js +150 -10
  74. package/dist/react/components/index.js.map +15 -10
  75. package/dist/react/hooks/index.js +479 -13
  76. package/dist/react/hooks/index.js.map +12 -4
  77. package/dist/react/index.js +769 -166
  78. package/dist/react/index.js.map +32 -21
  79. package/dist/react/router/browser.js +21 -4
  80. package/dist/react/router/browser.js.map +5 -4
  81. package/dist/react/router/index.js +21 -4
  82. package/dist/react/router/index.js.map +5 -4
  83. package/dist/react/server.js +597 -203
  84. package/dist/react/server.js.map +25 -15
  85. package/dist/src/angular/zoneRuntime.d.ts +5 -0
  86. package/dist/src/build/buildReactServerComponents.d.ts +22 -0
  87. package/dist/src/build/downlevelAsyncForZone.d.ts +1 -0
  88. package/dist/src/build/fileRoutes.d.ts +65 -0
  89. package/dist/src/build/fonts/buildFonts.d.ts +25 -0
  90. package/dist/src/build/fonts/fallbackMetrics.d.ts +13 -0
  91. package/dist/src/build/fonts/googleFonts.d.ts +18 -0
  92. package/dist/src/build/fonts/injectFontTags.d.ts +7 -0
  93. package/dist/src/build/fonts/parseFontMetrics.d.ts +4 -0
  94. package/dist/src/build/generateReactIndexes.d.ts +7 -1
  95. package/dist/src/build/htmlIncludes.d.ts +16 -0
  96. package/dist/src/build/inlineSourceMapComment.d.ts +1 -0
  97. package/dist/src/build/scanConventions.d.ts +3 -0
  98. package/dist/src/build/vitals.d.ts +21 -0
  99. package/dist/src/client/formActions.d.ts +29 -0
  100. package/dist/src/client/index.d.ts +2 -0
  101. package/dist/src/client/navigation.d.ts +16 -0
  102. package/dist/src/client/prefetch.d.ts +2 -30
  103. package/dist/src/core/build.d.ts +2 -2
  104. package/dist/src/core/cspNonce.d.ts +12 -0
  105. package/dist/src/core/fileRouteAdapter.d.ts +15 -0
  106. package/dist/src/core/fileRoutes.d.ts +5 -0
  107. package/dist/src/core/formActionFlash.d.ts +25 -0
  108. package/dist/src/core/formActions.d.ts +249 -0
  109. package/dist/src/core/index.d.ts +5 -0
  110. package/dist/src/core/partialPrerender.d.ts +28 -0
  111. package/dist/src/core/partialPrerenderServe.d.ts +24 -0
  112. package/dist/src/core/precompressed.d.ts +11 -1
  113. package/dist/src/core/prerenderNonce.d.ts +9 -0
  114. package/dist/src/core/productionRuntime.d.ts +4 -2
  115. package/dist/src/core/serverOnlyStaticFiles.d.ts +8 -0
  116. package/dist/src/core/staticHtmlNonce.d.ts +2 -0
  117. package/dist/src/core/vitals.d.ts +108 -0
  118. package/dist/src/dev/angular/fastHmrCompiler.d.ts +1 -1
  119. package/dist/src/dev/client/cspNonce.d.ts +2 -0
  120. package/dist/src/dev/fileRouteRefresher.d.ts +5 -0
  121. package/dist/src/dev/importCost/instrument.d.ts +1 -1
  122. package/dist/src/dev/reactServerComponentsRebuild.d.ts +3 -0
  123. package/dist/src/i18n/defineMessages.d.ts +5 -0
  124. package/dist/src/i18n/formatMessage.d.ts +26 -0
  125. package/dist/src/i18n/index.d.ts +6 -0
  126. package/dist/src/i18n/intlCache.d.ts +5 -0
  127. package/dist/src/i18n/routing.d.ts +28 -0
  128. package/dist/src/i18n/translator.d.ts +11 -0
  129. package/dist/src/mobile/androidRelease.d.ts +2 -2
  130. package/dist/src/mobile/iosRelease.d.ts +4 -4
  131. package/dist/src/mobile/materializedBundle.d.ts +1 -1
  132. package/dist/src/mobile/mobileInspect.d.ts +1 -1
  133. package/dist/src/mobile/releaseArtifact.d.ts +2 -2
  134. package/dist/src/mobile/remoteMacProtocol.d.ts +2 -2
  135. package/dist/src/plugins/i18n.d.ts +30 -0
  136. package/dist/src/plugins/index.d.ts +2 -1
  137. package/dist/src/plugins/secureHeaders.d.ts +48 -0
  138. package/dist/src/react/components/Head.d.ts +1 -1
  139. package/dist/src/react/components/StreamSlot.browser.d.ts +1 -0
  140. package/dist/src/react/components/StreamSlot.d.ts +5 -1
  141. package/dist/src/react/components/SuspenseSlot.browser.d.ts +2 -0
  142. package/dist/src/react/components/SuspenseSlot.d.ts +12 -0
  143. package/dist/src/react/fileRoute.d.ts +18 -0
  144. package/dist/src/react/hooks/index.d.ts +4 -0
  145. package/dist/src/react/hooks/useFormAction.d.ts +42 -0
  146. package/dist/src/react/hooks/useI18n.d.ts +15 -0
  147. package/dist/src/react/index.d.ts +2 -0
  148. package/dist/src/react/server.d.ts +2 -0
  149. package/dist/src/react/serverComponents/constants.d.ts +11 -0
  150. package/dist/src/react/serverComponents/flightPayload.d.ts +2 -0
  151. package/dist/src/react/serverComponents/pageHandler.d.ts +14 -0
  152. package/dist/src/svelte/browser.d.ts +1 -0
  153. package/dist/src/svelte/fileRoute.d.ts +15 -0
  154. package/dist/src/svelte/i18n.d.ts +8 -0
  155. package/dist/src/svelte/index.d.ts +2 -0
  156. package/dist/src/svelte/lowerAwaitSlotSyntax.d.ts +5 -1
  157. package/dist/src/svelte/renderToReadableStream.d.ts +10 -2
  158. package/dist/src/svelte/router/browser.d.ts +3 -1
  159. package/dist/src/svelte/router/goto.d.ts +13 -10
  160. package/dist/src/svelte/router/guards.d.ts +43 -0
  161. package/dist/src/svelte/router/index.d.ts +3 -1
  162. package/dist/src/svelte/router/lazy.d.ts +17 -0
  163. package/dist/src/svelte/router/lazyModule.d.ts +29 -0
  164. package/dist/src/svelte/router/navigation.d.ts +55 -0
  165. package/dist/src/svelte/router/pushState.d.ts +4 -2
  166. package/dist/src/svelte/router/scroll.d.ts +34 -0
  167. package/dist/src/svelte/routerSsr.d.ts +9 -0
  168. package/dist/src/svelte/server.d.ts +1 -0
  169. package/dist/src/utils/cspNonceRef.d.ts +8 -0
  170. package/dist/src/utils/fontAssets.d.ts +17 -0
  171. package/dist/src/utils/formActionProtocol.d.ts +10 -0
  172. package/dist/src/utils/generateHeadElement.d.ts +1 -1
  173. package/dist/src/utils/index.d.ts +1 -0
  174. package/dist/src/utils/inlinePageCss.d.ts +18 -0
  175. package/dist/src/utils/loadConfig.d.ts +5 -0
  176. package/dist/src/utils/localeRef.d.ts +10 -0
  177. package/dist/src/utils/resolveConvention.d.ts +6 -1
  178. package/dist/src/utils/streamingSlots.d.ts +5 -0
  179. package/dist/src/vue/browser.d.ts +2 -0
  180. package/dist/src/vue/components/Image.d.ts +2 -2
  181. package/dist/src/vue/index.d.ts +2 -0
  182. package/dist/src/vue/serverDataPayload.d.ts +32 -0
  183. package/dist/src/vue/serverDataState.d.ts +9 -0
  184. package/dist/src/vue/useServerData.browser.d.ts +6 -0
  185. package/dist/src/vue/useServerData.d.ts +24 -0
  186. package/dist/svelte/browser.js +330 -3
  187. package/dist/svelte/browser.js.map +8 -4
  188. package/dist/svelte/components/AwaitSlot.svelte +5 -0
  189. package/dist/svelte/components/Head.svelte +50 -0
  190. package/dist/svelte/components/JsonLd.svelte +9 -1
  191. package/dist/svelte/components/StreamSlot.svelte +5 -0
  192. package/dist/svelte/index.js +1017 -117
  193. package/dist/svelte/index.js.map +38 -24
  194. package/dist/svelte/router/Link.svelte +40 -8
  195. package/dist/svelte/router/Route.svelte +157 -12
  196. package/dist/svelte/router/Route.svelte.d.ts +39 -5
  197. package/dist/svelte/router/Router.svelte +140 -25
  198. package/dist/svelte/router/Router.svelte.d.ts +5 -1
  199. package/dist/svelte/router/browser.ts +10 -0
  200. package/dist/svelte/router/goto.ts +15 -86
  201. package/dist/svelte/router/guards.ts +112 -0
  202. package/dist/svelte/router/index.ts +10 -0
  203. package/dist/svelte/router/lazy.ts +82 -0
  204. package/dist/svelte/router/lazyModule.ts +74 -0
  205. package/dist/svelte/router/navigation.ts +412 -0
  206. package/dist/svelte/router/pushState.ts +7 -4
  207. package/dist/svelte/router/scroll.ts +286 -0
  208. package/dist/svelte/server.js +747 -252
  209. package/dist/svelte/server.js.map +30 -20
  210. package/dist/types/angular.d.ts +2 -0
  211. package/dist/types/build.d.ts +37 -0
  212. package/dist/types/conventions.d.ts +3 -0
  213. package/dist/types/fileRoutes.d.ts +72 -0
  214. package/dist/types/fonts.d.ts +86 -0
  215. package/dist/types/formAction.d.ts +54 -0
  216. package/dist/types/globals.d.ts +10 -0
  217. package/dist/types/i18n.d.ts +185 -0
  218. package/dist/types/index.d.ts +4 -0
  219. package/dist/types/metadata.d.ts +11 -0
  220. package/dist/types/navigation.d.ts +37 -0
  221. package/dist/types/prefetch.d.ts +10 -0
  222. package/dist/types/sitemap.d.ts +4 -0
  223. package/dist/types/svelteRouter.d.ts +89 -2
  224. package/dist/types/svelteRouter.ts +112 -2
  225. package/dist/types/vue.d.ts +46 -1
  226. package/dist/utils/iconVersion.ts +34 -0
  227. package/dist/vue/browser.js +107 -11
  228. package/dist/vue/browser.js.map +7 -4
  229. package/dist/vue/components/index.js +5 -2
  230. package/dist/vue/components/index.js.map +5 -4
  231. package/dist/vue/index.js +712 -158
  232. package/dist/vue/index.js.map +32 -23
  233. package/dist/vue/server.js +310 -79
  234. package/dist/vue/server.js.map +20 -15
  235. package/package.json +9 -1
  236. package/dist/cli/prerender-mfnwsvyt.js +0 -7
  237. package/dist/src/plugins/pageRouter.d.ts +0 -1
  238. /package/dist/cli/{index-j7m8pq48.js → index-rervww4p.js} +0 -0
@@ -0,0 +1,286 @@
1
+ import type {
2
+ RouterHistoryMeta,
3
+ ScrollPosition
4
+ } from '../../types/svelteRouter';
5
+
6
+ /* History bookkeeping + scroll restoration for the Svelte router.
7
+ *
8
+ * Every history entry the router writes carries a small record next to the
9
+ * user's own state: `{ __absoluteRouter: { key, index, scroll? }, user }`.
10
+ * - `key` identifies the entry, so the scroll position saved when the
11
+ * user left it can be restored when they come back.
12
+ * - `index` is the entry's position in the session history, so a guard
13
+ * that cancels a back/forward navigation can undo it with `history.go`.
14
+ * - `scroll` is written into the entry itself when it is left (and on
15
+ * `pagehide`), so the position survives a reload or a cross-document
16
+ * round trip, not only in-memory navigations.
17
+ * `page.state` and `event.state` consumers see the user's state unchanged
18
+ * through `readUserState`. Browser-only: nothing here runs during SSR. */
19
+
20
+ const META_FIELD = '__absoluteRouter';
21
+ const KEY_RADIX = 36;
22
+ const KEY_RANDOM_START = 2;
23
+ const KEY_RANDOM_END = 10;
24
+ const SCROLL_SETTLE_MS = 150;
25
+ const SCROLL_STORAGE_PREFIX = 'absolute-router-scroll:';
26
+
27
+ type WrappedHistoryState = {
28
+ [META_FIELD]: RouterHistoryMeta;
29
+ user: unknown;
30
+ };
31
+
32
+ const positions = new Map<string, ScrollPosition>();
33
+ let current: RouterHistoryMeta | undefined;
34
+ let committedHref: string | undefined;
35
+
36
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
37
+ typeof value === 'object' && value !== null;
38
+
39
+ const isScrollPosition = (value: unknown): value is ScrollPosition =>
40
+ isRecord(value) &&
41
+ typeof value.x === 'number' &&
42
+ typeof value.y === 'number';
43
+
44
+ const createKey = () =>
45
+ `${Date.now().toString(KEY_RADIX)}${Math.random()
46
+ .toString(KEY_RADIX)
47
+ .slice(KEY_RANDOM_START, KEY_RANDOM_END)}`;
48
+
49
+ export const readHistoryMeta = (state: unknown) => {
50
+ if (!isRecord(state)) return undefined;
51
+ const meta = state[META_FIELD];
52
+ if (!isRecord(meta)) return undefined;
53
+ if (typeof meta.key !== 'string' || typeof meta.index !== 'number') {
54
+ return undefined;
55
+ }
56
+ const parsed: RouterHistoryMeta = { index: meta.index, key: meta.key };
57
+ if (isScrollPosition(meta.scroll)) parsed.scroll = meta.scroll;
58
+
59
+ return parsed;
60
+ };
61
+
62
+ /** The state the application stored (via `goto({ state })`, `pushState`,
63
+ * …), without the router's bookkeeping. */
64
+ export const readUserState = (state: unknown) => {
65
+ if (readHistoryMeta(state) === undefined || !isRecord(state)) return state;
66
+
67
+ return state.user;
68
+ };
69
+
70
+ const wrapState = (user: unknown, meta: RouterHistoryMeta) => {
71
+ const wrapped: WrappedHistoryState = { [META_FIELD]: meta, user };
72
+
73
+ return wrapped;
74
+ };
75
+
76
+ const readScroll = () => {
77
+ const position: ScrollPosition = {
78
+ x: window.scrollX,
79
+ y: window.scrollY
80
+ };
81
+
82
+ return position;
83
+ };
84
+
85
+ /** Adopt the entry a `popstate` landed on as the current one. Entries the
86
+ * router didn't write (a plain `<a href="#x">` click, say) get stamped. */
87
+ export const adoptHistoryEntry = (state: unknown) => {
88
+ const existing = readHistoryMeta(state);
89
+ committedHref = window.location.href;
90
+ if (existing) {
91
+ current = existing;
92
+
93
+ return existing;
94
+ }
95
+ const meta: RouterHistoryMeta = {
96
+ index: (current?.index ?? 0) + 1,
97
+ key: createKey()
98
+ };
99
+ window.history.replaceState(wrapState(state, meta), '');
100
+ current = meta;
101
+
102
+ return meta;
103
+ };
104
+
105
+ /** The full URL (hash included) of the entry the router currently shows. */
106
+ export const currentCommittedHref = () => committedHref;
107
+
108
+ /** The entry the router currently shows. */
109
+ export const currentHistoryEntry = () => current;
110
+
111
+ /** Record the live scroll position for the current entry, in memory and in
112
+ * the entry's `history.state`, so it survives a reload. */
113
+ export const persistScroll = () => {
114
+ if (!current || typeof window === 'undefined') return;
115
+ const scroll = readScroll();
116
+ positions.set(current.key, scroll);
117
+ if (readHistoryMeta(window.history.state)?.key !== current.key) return;
118
+ current = { ...current, scroll };
119
+ window.history.replaceState(
120
+ wrapState(readUserState(window.history.state), current),
121
+ ''
122
+ );
123
+ };
124
+
125
+ /** Record the live scroll position for the current entry (in memory). */
126
+ export const rememberScroll = () => {
127
+ if (!current || typeof window === 'undefined') return;
128
+ positions.set(current.key, readScroll());
129
+ };
130
+
131
+ /** Where the user was when they last left `meta`'s entry, if known. */
132
+ export const savedScrollFor = (meta: RouterHistoryMeta) =>
133
+ positions.get(meta.key) ?? meta.scroll;
134
+
135
+ const storageKey = (key: string) => `${SCROLL_STORAGE_PREFIX}${key}`;
136
+
137
+ /** The position `pagehide` stored for `key` in sessionStorage — the
138
+ * reliable record for a reload, since some engines drop a
139
+ * `history.replaceState` made while the page unloads. Read once. */
140
+ const readStoredScroll = (key: string) => {
141
+ try {
142
+ const raw = window.sessionStorage.getItem(storageKey(key));
143
+ if (raw === null) return undefined;
144
+ window.sessionStorage.removeItem(storageKey(key));
145
+ const parsed: unknown = JSON.parse(raw);
146
+
147
+ return isScrollPosition(parsed) ? parsed : undefined;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ };
152
+
153
+ const storeScroll = () => {
154
+ if (!current) return;
155
+ try {
156
+ window.sessionStorage.setItem(
157
+ storageKey(current.key),
158
+ JSON.stringify(readScroll())
159
+ );
160
+ } catch {
161
+ // Storage disabled or full: history.state is the only record.
162
+ }
163
+ };
164
+
165
+ /** Make sure the entry the page loaded on carries router bookkeeping, and
166
+ * adopt it as the current one. Returns its meta. */
167
+ export const stampInitialEntry = () => {
168
+ const existing = readHistoryMeta(window.history.state);
169
+ committedHref = window.location.href;
170
+ if (existing) {
171
+ const scroll = readStoredScroll(existing.key) ?? existing.scroll;
172
+ current = scroll ? { ...existing, scroll } : existing;
173
+ if (scroll) positions.set(existing.key, scroll);
174
+
175
+ return current;
176
+ }
177
+ const meta: RouterHistoryMeta = { index: 0, key: createKey() };
178
+ window.history.replaceState(wrapState(window.history.state, meta), '');
179
+ current = meta;
180
+
181
+ return meta;
182
+ };
183
+
184
+ /** Write a new entry (`push`) or replace the current one, saving the
185
+ * scroll position of the entry being left first. */
186
+ export const writeHistoryEntry = (
187
+ href: string,
188
+ user: unknown,
189
+ mode: 'push' | 'replace'
190
+ ) => {
191
+ if (mode === 'push') persistScroll();
192
+ const index = current?.index ?? 0;
193
+ const meta: RouterHistoryMeta = {
194
+ index: mode === 'push' ? index + 1 : index,
195
+ key: createKey()
196
+ };
197
+ if (mode === 'push') {
198
+ window.history.pushState(wrapState(user, meta), '', href);
199
+ } else {
200
+ window.history.replaceState(wrapState(user, meta), '', href);
201
+ }
202
+ current = meta;
203
+ committedHref = window.location.href;
204
+
205
+ return meta;
206
+ };
207
+
208
+ const decodeFragment = (raw: string) => {
209
+ try {
210
+ return decodeURIComponent(raw);
211
+ } catch {
212
+ // Malformed escape: look the raw fragment up instead.
213
+ return raw;
214
+ }
215
+ };
216
+
217
+ const findHashTarget = (hash: string) => {
218
+ const raw = hash.startsWith('#') ? hash.slice(1) : hash;
219
+ if (raw === '') return undefined;
220
+ const fragment = decodeFragment(raw);
221
+
222
+ return (
223
+ document.getElementById(fragment) ??
224
+ document.getElementsByName(fragment).item(0) ??
225
+ undefined
226
+ );
227
+ };
228
+
229
+ /** Take over scroll restoration from the browser while a router is
230
+ * mounted: `history.scrollRestoration = 'manual'`, track the scroll
231
+ * position of the current entry, and persist it on `pagehide`. Returns
232
+ * the cleanup that hands control back. */
233
+ export const activateScrollRestoration = () => {
234
+ const { history } = window;
235
+ const previousMode = history.scrollRestoration;
236
+ history.scrollRestoration = 'manual';
237
+
238
+ // In memory on every frame; into `history.state` once scrolling settles
239
+ // (a reload only sees what is in the entry, and `pagehide` is too late
240
+ // for some engines to accept a `replaceState`).
241
+ let frame = 0;
242
+ let settleTimer: ReturnType<typeof setTimeout> | undefined;
243
+ const onScroll = () => {
244
+ clearTimeout(settleTimer);
245
+ settleTimer = setTimeout(persistScroll, SCROLL_SETTLE_MS);
246
+ if (frame !== 0) return;
247
+ frame = window.requestAnimationFrame(() => {
248
+ frame = 0;
249
+ rememberScroll();
250
+ });
251
+ };
252
+ const onPageHide = () => {
253
+ persistScroll();
254
+ storeScroll();
255
+ };
256
+
257
+ window.addEventListener('scroll', onScroll, { passive: true });
258
+ window.addEventListener('pagehide', onPageHide);
259
+
260
+ return () => {
261
+ if (frame !== 0) window.cancelAnimationFrame(frame);
262
+ clearTimeout(settleTimer);
263
+ window.removeEventListener('scroll', onScroll);
264
+ window.removeEventListener('pagehide', onPageHide);
265
+ history.scrollRestoration = previousMode;
266
+ };
267
+ };
268
+
269
+ /** Scroll the `#hash` target into view. Returns whether one was found —
270
+ * `#` and `#top` scroll to the top, as browsers do. */
271
+ export const scrollToHash = (hash: string) => {
272
+ if (hash === '#' || hash.toLowerCase() === '#top') {
273
+ window.scrollTo({ left: 0, top: 0 });
274
+
275
+ return true;
276
+ }
277
+ const target = findHashTarget(hash);
278
+ if (!target) return false;
279
+ target.scrollIntoView();
280
+
281
+ return true;
282
+ };
283
+
284
+ export const scrollToPosition = (position: ScrollPosition) => {
285
+ window.scrollTo({ left: position.x, top: position.y });
286
+ };