bosia 0.8.5 → 0.8.6
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/package.json
CHANGED
|
@@ -63,6 +63,26 @@
|
|
|
63
63
|
let firstNav = true;
|
|
64
64
|
let navDoneTimer: ReturnType<typeof setTimeout> | null = null;
|
|
65
65
|
|
|
66
|
+
// Scroll after a nav settles: forward navs go to top (or the URL hash);
|
|
67
|
+
// back/forward navs restore the position saved for that history entry.
|
|
68
|
+
// Runs after tick() so the destination page's real height is in the DOM.
|
|
69
|
+
// goto({ noScroll: true }) flips `router.suppressScroll` for one nav.
|
|
70
|
+
function settleScroll() {
|
|
71
|
+
if (router.isPush && !router.suppressScroll) {
|
|
72
|
+
const hash = window.location.hash;
|
|
73
|
+
tick().then(() => {
|
|
74
|
+
if (!scrollToHash(hash)) window.scrollTo(0, 0);
|
|
75
|
+
});
|
|
76
|
+
} else if (!router.isPush) {
|
|
77
|
+
const pos = router.pendingScroll;
|
|
78
|
+
// ponytail: single post-tick restore; content that loads later (images
|
|
79
|
+
// without dimensions) can still shift it. Revisit only if reported.
|
|
80
|
+
if (pos) tick().then(() => window.scrollTo(pos.x, pos.y));
|
|
81
|
+
}
|
|
82
|
+
router.pendingScroll = null;
|
|
83
|
+
router.suppressScroll = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
$effect(() => {
|
|
67
87
|
if (ssrMode) return;
|
|
68
88
|
|
|
@@ -247,13 +267,7 @@
|
|
|
247
267
|
appState.errorComponent = errMod.default;
|
|
248
268
|
appState.errorProps = { error: { status: errStatus, message: errMessage } };
|
|
249
269
|
appState.errorDepth = K;
|
|
250
|
-
|
|
251
|
-
const hash = window.location.hash;
|
|
252
|
-
tick().then(() => {
|
|
253
|
-
if (!scrollToHash(hash)) window.scrollTo(0, 0);
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
router.suppressScroll = false;
|
|
270
|
+
settleScroll();
|
|
257
271
|
settle({ url, params: match.params });
|
|
258
272
|
} catch {
|
|
259
273
|
window.location.href = path;
|
|
@@ -342,16 +356,7 @@
|
|
|
342
356
|
appState.errorProps = null;
|
|
343
357
|
appState.errorDepth = null;
|
|
344
358
|
|
|
345
|
-
|
|
346
|
-
// goto({ noScroll: true }) flips `router.suppressScroll` for one nav.
|
|
347
|
-
// If the destination URL has a hash, scroll to that element instead.
|
|
348
|
-
if (router.isPush && !router.suppressScroll) {
|
|
349
|
-
const hash = window.location.hash;
|
|
350
|
-
tick().then(() => {
|
|
351
|
-
if (!scrollToHash(hash)) window.scrollTo(0, 0);
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
router.suppressScroll = false;
|
|
359
|
+
settleScroll();
|
|
355
360
|
|
|
356
361
|
// Update document title and meta description from server metadata
|
|
357
362
|
if (result?.metadata) {
|
|
@@ -16,6 +16,20 @@ function buildTarget(path: string): { url: URL; params: Record<string, string> }
|
|
|
16
16
|
return { url, params: match?.params ?? {} };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// ─── Scroll restoration ───
|
|
20
|
+
// Positions keyed by a per-entry index stamped into `history.state.bosiaIndex`.
|
|
21
|
+
// `scrollRestoration = "manual"` because the browser restores at popstate time,
|
|
22
|
+
// before the SPA has rendered the destination page — it clamps against the
|
|
23
|
+
// wrong document height. We restore ourselves after the nav settles (App.svelte).
|
|
24
|
+
// Persisted to sessionStorage on unload so reload / back-into-app still restores.
|
|
25
|
+
const SCROLL_KEY = "bosia:scroll";
|
|
26
|
+
let scrollPositions: Record<number, { x: number; y: number }> = {};
|
|
27
|
+
let historyIndex = 0;
|
|
28
|
+
|
|
29
|
+
function saveScroll() {
|
|
30
|
+
scrollPositions[historyIndex] = { x: window.scrollX, y: window.scrollY };
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export function scrollToHash(hash: string): boolean {
|
|
20
34
|
if (typeof document === "undefined" || !hash) return false;
|
|
21
35
|
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
@@ -45,6 +59,8 @@ export const router = new (class Router {
|
|
|
45
59
|
lastNavType: NavType = "enter";
|
|
46
60
|
/** Set by `goto({ noScroll: true })`; consumed once by App.svelte after the next nav settles. */
|
|
47
61
|
suppressScroll = false;
|
|
62
|
+
/** Saved scroll position for the entry a popstate landed on; consumed once by App.svelte after the nav settles. */
|
|
63
|
+
pendingScroll: { x: number; y: number } | null = null;
|
|
48
64
|
|
|
49
65
|
navigate(path: string, opts: { replace?: boolean; source?: NavType } = {}) {
|
|
50
66
|
if (this.currentRoute === path) return;
|
|
@@ -74,12 +90,15 @@ export const router = new (class Router {
|
|
|
74
90
|
|
|
75
91
|
this.lastNavType = navType;
|
|
76
92
|
this.isPush = true;
|
|
93
|
+
this.pendingScroll = null;
|
|
77
94
|
this.currentRoute = finalPath;
|
|
78
95
|
if (typeof history !== "undefined") {
|
|
79
96
|
if (opts.replace) {
|
|
80
|
-
history.replaceState({}, "", finalPath);
|
|
97
|
+
history.replaceState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
81
98
|
} else {
|
|
82
|
-
|
|
99
|
+
saveScroll();
|
|
100
|
+
historyIndex++;
|
|
101
|
+
history.pushState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
83
102
|
}
|
|
84
103
|
}
|
|
85
104
|
}
|
|
@@ -87,6 +106,22 @@ export const router = new (class Router {
|
|
|
87
106
|
init() {
|
|
88
107
|
if (typeof window === "undefined") return;
|
|
89
108
|
|
|
109
|
+
history.scrollRestoration = "manual";
|
|
110
|
+
try {
|
|
111
|
+
scrollPositions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) ?? "{}");
|
|
112
|
+
} catch {
|
|
113
|
+
scrollPositions = {};
|
|
114
|
+
}
|
|
115
|
+
const stamped = history.state?.bosiaIndex != null;
|
|
116
|
+
historyIndex = stamped ? history.state.bosiaIndex : 0;
|
|
117
|
+
history.replaceState({ ...history.state, bosiaIndex: historyIndex }, "");
|
|
118
|
+
// Restore after a reload — manual mode means the browser won't. Only for
|
|
119
|
+
// entries we stamped before (a fresh visit has no bosiaIndex in state).
|
|
120
|
+
const initial = scrollPositions[historyIndex];
|
|
121
|
+
if (stamped && initial) {
|
|
122
|
+
requestAnimationFrame(() => window.scrollTo(initial.x, initial.y));
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
// Intercept <a> clicks for client-side navigation
|
|
91
126
|
window.addEventListener("click", (e) => {
|
|
92
127
|
// Let browser handle non-primary buttons, modifier-clicks, already-handled events
|
|
@@ -110,7 +145,9 @@ export const router = new (class Router {
|
|
|
110
145
|
e.preventDefault();
|
|
111
146
|
const finalPath = anchor.pathname + anchor.search + anchor.hash;
|
|
112
147
|
if (this.currentRoute !== finalPath) {
|
|
113
|
-
|
|
148
|
+
saveScroll();
|
|
149
|
+
historyIndex++;
|
|
150
|
+
history.pushState({ bosiaIndex: historyIndex }, "", finalPath);
|
|
114
151
|
this.currentRoute = finalPath;
|
|
115
152
|
}
|
|
116
153
|
scrollToHash(anchor.hash);
|
|
@@ -122,8 +159,13 @@ export const router = new (class Router {
|
|
|
122
159
|
});
|
|
123
160
|
|
|
124
161
|
// Browser back/forward
|
|
125
|
-
window.addEventListener("popstate", () => {
|
|
162
|
+
window.addEventListener("popstate", (e) => {
|
|
126
163
|
const finalPath = window.location.pathname + window.location.search + window.location.hash;
|
|
164
|
+
// Save the position of the entry we're leaving (historyIndex is still
|
|
165
|
+
// the old entry's), then look up where the landed-on entry was.
|
|
166
|
+
saveScroll();
|
|
167
|
+
historyIndex = e.state?.bosiaIndex ?? 0;
|
|
168
|
+
this.pendingScroll = scrollPositions[historyIndex] ?? null;
|
|
127
169
|
// Fire beforeNavigate listeners; popstate can't be reliably cancelled
|
|
128
170
|
// (browser history already advanced), so we surface the event for
|
|
129
171
|
// observation only — `cancel()` is a no-op for this source.
|
|
@@ -147,6 +189,14 @@ export const router = new (class Router {
|
|
|
147
189
|
// listeners can warn-on-leave (return value ignored; cancellation here
|
|
148
190
|
// requires `beforeunload`, not in scope).
|
|
149
191
|
window.addEventListener("beforeunload", () => {
|
|
192
|
+
// ponytail: beforeunload can be skipped on mobile page-discard; add a
|
|
193
|
+
// visibilitychange persist if restore-after-reload proves flaky there.
|
|
194
|
+
saveScroll();
|
|
195
|
+
try {
|
|
196
|
+
sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scrollPositions));
|
|
197
|
+
} catch {
|
|
198
|
+
// sessionStorage unavailable (private mode / quota) — skip persistence.
|
|
199
|
+
}
|
|
150
200
|
const fromTarget = buildTarget(this.currentRoute);
|
|
151
201
|
const nav: Navigation = {
|
|
152
202
|
from: fromTarget,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { parse, compile } from "svelte/compiler";
|
|
2
2
|
import MagicString from "magic-string";
|
|
3
|
-
import {
|
|
3
|
+
import { relative } from "node:path";
|
|
4
4
|
import type { BunPlugin } from "bun";
|
|
5
5
|
import { svelteMapCache } from "../../svelteCompiler.ts";
|
|
6
6
|
import { lineColFromOffset } from "../../sourceLoc.ts";
|
|
7
7
|
|
|
8
|
-
const VIRTUAL_NS = "bosia-inspector-css";
|
|
9
|
-
|
|
10
8
|
type AnyNode = {
|
|
11
9
|
type?: string;
|
|
12
10
|
name?: string;
|
|
@@ -124,7 +122,6 @@ const fnv = (s: string): string => {
|
|
|
124
122
|
export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPlugin {
|
|
125
123
|
const { cwd, target, dev } = opts;
|
|
126
124
|
const generate: "client" | "server" = target === "browser" ? "client" : "server";
|
|
127
|
-
const virtualCss = new Map<string, string>();
|
|
128
125
|
|
|
129
126
|
return {
|
|
130
127
|
name: "bosia-inspector",
|
|
@@ -139,7 +136,11 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
139
136
|
generate,
|
|
140
137
|
dev,
|
|
141
138
|
hmr: dev,
|
|
142
|
-
|
|
139
|
+
// Mirror the prod compiler (svelteCompiler.ts): client injects scoped
|
|
140
|
+
// CSS into the JS via `append_styles`, server discards it. No CSS
|
|
141
|
+
// chunks means Bun's `splitting:true` output-path collisions can't
|
|
142
|
+
// arise, so no runtime-injection workaround is needed.
|
|
143
|
+
css: generate === "client" ? "injected" : "external",
|
|
143
144
|
preserveWhitespace: dev,
|
|
144
145
|
preserveComments: dev,
|
|
145
146
|
cssHash: ({ css }) => `svelte-${fnv(css)}`,
|
|
@@ -164,39 +165,9 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
164
165
|
svelteMapCache.set(args.path, m);
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
// Inject component <style> blocks via runtime JS, not CSS chunks.
|
|
169
|
-
// Bun's `splitting: true` names CSS chunks after the importing JS
|
|
170
|
-
// chunk's `[name]`, not the virtual module's uid — so when several
|
|
171
|
-
// routes (e.g. multiple `+page.svelte`) transitively import the same
|
|
172
|
-
// styled component, each route emits its own CSS sidecar named
|
|
173
|
-
// `+page-<contentHash>.css`. Identical content → identical hash →
|
|
174
|
-
// "Multiple files share the same output path". Runtime injection
|
|
175
|
-
// avoids CSS chunking entirely.
|
|
176
|
-
if (result.css?.code?.trim() && generate !== "server") {
|
|
177
|
-
const safeBase = basename(args.path).replace(/\./g, "_");
|
|
178
|
-
const uid = `${safeBase}-${fnv(args.path)}-style.css`;
|
|
179
|
-
const virtualName = `${VIRTUAL_NS}:${uid}`;
|
|
180
|
-
virtualCss.set(virtualName, result.css.code);
|
|
181
|
-
js += `\nimport ${JSON.stringify(virtualName)};`;
|
|
182
|
-
}
|
|
183
|
-
|
|
168
|
+
const js = dev ? fixBindShadow(result.js.code) : result.js.code;
|
|
184
169
|
return { contents: js, loader: "ts" };
|
|
185
170
|
});
|
|
186
|
-
|
|
187
|
-
build.onResolve({ filter: new RegExp(`^${VIRTUAL_NS}:`) }, (args) => ({
|
|
188
|
-
path: args.path,
|
|
189
|
-
namespace: VIRTUAL_NS,
|
|
190
|
-
}));
|
|
191
|
-
|
|
192
|
-
build.onLoad({ filter: /.*/, namespace: VIRTUAL_NS }, (args) => {
|
|
193
|
-
const css = virtualCss.get(args.path) ?? "";
|
|
194
|
-
virtualCss.delete(args.path);
|
|
195
|
-
const contents = css
|
|
196
|
-
? `(()=>{const s=document.createElement('style');s.textContent=${JSON.stringify(css)};document.head.appendChild(s);})();`
|
|
197
|
-
: "";
|
|
198
|
-
return { contents, loader: "js" };
|
|
199
|
-
});
|
|
200
171
|
},
|
|
201
172
|
};
|
|
202
173
|
}
|