solid-route-progress 0.1.0

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.
@@ -0,0 +1,67 @@
1
+ import { a as isIgnored, c as Progress, d as useController, r as disposalSignal, s as OPTION_KEYS, t as createCrossDocumentProgress } from "./shared.jsx";
2
+ import { createEffect, on, onCleanup, splitProps } from "solid-js";
3
+ import { isServer } from "solid-js/web";
4
+ import { useBeforeLeave, useIsRouting } from "@solidjs/router";
5
+ //#region src/router.tsx
6
+ /**
7
+ * Wire a controller to `@solidjs/router`: `useIsRouting()` covers `<A>` clicks,
8
+ * `navigate()`, back/forward and action redirects, including any `<Suspense>` the new
9
+ * route waits on. `useBeforeLeave()` supplies the target for `shallow` / `filter`.
10
+ */
11
+ function createRouteProgress(controller, options = {}) {
12
+ if (isServer) return;
13
+ const isRouting = useIsRouting();
14
+ let skip = false;
15
+ const skipNext = () => {
16
+ skip = true;
17
+ queueMicrotask(() => {
18
+ skip = false;
19
+ });
20
+ };
21
+ document.addEventListener("click", (event) => {
22
+ if (isIgnored(event.target)) skipNext();
23
+ }, {
24
+ capture: true,
25
+ signal: disposalSignal()
26
+ });
27
+ useBeforeLeave((event) => {
28
+ if (typeof event.to !== "string") return;
29
+ if (options.shallow === true && samePathname(event.to, event.from.pathname) || options.filter?.(event.to, event.from) === false) skipNext();
30
+ });
31
+ let release;
32
+ createEffect(on(isRouting, (routing) => {
33
+ release?.();
34
+ release = routing && !skip ? controller.start() : void 0;
35
+ if (routing) skip = false;
36
+ }));
37
+ onCleanup(() => release?.());
38
+ if (options.crossDocument !== false) createCrossDocumentProgress(controller, typeof options.crossDocument === "object" ? options.crossDocument : void 0);
39
+ }
40
+ const samePathname = (to, pathname) => {
41
+ const end = to.search(/[?#]/);
42
+ return (end === -1 ? to : to.slice(0, end)).replace(/\/+$/, "") === pathname.replace(/\/+$/, "");
43
+ };
44
+ const LOCAL = [
45
+ "shallow",
46
+ "filter",
47
+ "crossDocument",
48
+ "controller"
49
+ ];
50
+ /**
51
+ * Drop-in route progress bar. Place it anywhere under `<Router>` — typically in the root
52
+ * layout — and import `style.css` once. Inside a `<ProgressProvider>` it drives that
53
+ * provider's controller, so `useProgress()` works anywhere in the app.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * <Router root={(props) => <><RouteProgress /><Suspense>{props.children}</Suspense></>}>
58
+ * ```
59
+ */
60
+ function RouteProgress(props) {
61
+ const [local, options, rest] = splitProps(props, LOCAL, OPTION_KEYS);
62
+ const controller = useController(local.controller, options);
63
+ createRouteProgress(controller, local);
64
+ return <Progress controller={controller} {...rest} />;
65
+ }
66
+ //#endregion
67
+ export { RouteProgress, createRouteProgress };
@@ -0,0 +1,202 @@
1
+ import { Accessor, Context, JSX, ParentProps } from "solid-js";
2
+ //#region src/core.d.ts
3
+ /**
4
+ * Visual state of the bar. Mirrored to the root element as `data-state`.
5
+ *
6
+ * - `idle` hidden (faded out, `visibility: hidden`)
7
+ * - `trickle` visible, drifting toward `trickleTo` on a long CSS transition
8
+ * - `active` visible, moving to an explicit `set()` value on a short transition
9
+ * - `done` visible, moving to 100% before fading out
10
+ */
11
+ type ProgressState = "idle" | "trickle" | "active" | "done";
12
+ /**
13
+ * How a load ended, when it did not simply succeed: `'error'` completes the bar with
14
+ * `data-error` set, `'cancel'` fades it out without running to 100%.
15
+ */
16
+ type Outcome = "error" | "cancel";
17
+ /**
18
+ * `{ [Symbol.dispose](): void }` where the consumer's TypeScript `lib` knows `Symbol.dispose`,
19
+ * and no constraint otherwise, so the type never forces `esnext.disposable` on anyone.
20
+ */
21
+ type DisposableLike = SymbolConstructor extends {
22
+ readonly dispose: infer K extends symbol;
23
+ } ? { [P in K]: () => void; } : unknown;
24
+ /**
25
+ * Lets go of one hold on the bar. Calling it again is a no-op. Where `Symbol.dispose`
26
+ * exists it is also a disposable, so `using hold = progress.start()` releases on scope exit.
27
+ */
28
+ type Release = ((outcome?: Outcome) => void) & DisposableLike;
29
+ interface TrackOptions {
30
+ /** Release the hold after this many milliseconds even if the promise never settles. */
31
+ timeout?: number;
32
+ }
33
+ interface ProgressOptions {
34
+ /**
35
+ * Value (0–1) the bar drifts toward while loading. The drift itself is a single CSS
36
+ * transition (`--sp-trickle-duration` / `--sp-trickle-easing`); no JS timer steps it.
37
+ * @default 0.95
38
+ */
39
+ trickleTo?: number;
40
+ /**
41
+ * Milliseconds a load must last before the bar shows, so quick loads never draw one. A
42
+ * load that settles before the next frame is dropped unseen even with `0`.
43
+ * @default 200
44
+ */
45
+ delay?: number;
46
+ /**
47
+ * Milliseconds to wait before completing once the last hold is released. Useful when a
48
+ * route mounts and immediately kicks off another load you also want covered.
49
+ * @default 0
50
+ */
51
+ stopDelay?: number;
52
+ /**
53
+ * Milliseconds for the short transitions: `set()`, `done()` and the fade in/out. The bar
54
+ * writes it to `--sp-speed`, so JS timers and CSS transitions always agree.
55
+ * @default 200
56
+ */
57
+ speed?: number;
58
+ }
59
+ interface ProgressController {
60
+ /** Target value (0–1) the bar is transitioning toward. */
61
+ readonly value: Accessor<number>;
62
+ /** Current visual state. */
63
+ readonly state: Accessor<ProgressState>;
64
+ /** `true` while the bar is visible (any state other than `idle`). */
65
+ readonly active: Accessor<boolean>;
66
+ /** `true` during the `done` phase of a load that failed. Mirrored as `data-error`. */
67
+ readonly error: Accessor<boolean>;
68
+ /**
69
+ * Hold the bar open: shows it (after `delay`) and starts trickling. Returns a release
70
+ * function; the bar completes once every hold is released, so the router, `track()` and
71
+ * your own code never finish each other's loads.
72
+ */
73
+ start(): Release;
74
+ /**
75
+ * Complete the bar (after `stopDelay`), dropping every hold. No-op while hidden. An
76
+ * `outcome` marks the load as failed or cancelled, as with a release.
77
+ */
78
+ done(outcome?: Outcome): void;
79
+ /**
80
+ * Move the bar to an explicit value (0–1) on a short transition, then resume
81
+ * trickling. Values `>= 1` complete the bar. Shows the bar if it is hidden, except
82
+ * while a `delay` is still pending: a load that finishes early stays invisible.
83
+ */
84
+ set(value: number): void;
85
+ /**
86
+ * Hold the bar open until `promise` settles; a rejection ends it as an `'error'`. Returns
87
+ * `promise` itself, so it can wrap a call in place.
88
+ */
89
+ track<P extends PromiseLike<unknown>>(promise: P, options?: TrackOptions): P;
90
+ /** The options this controller was created with (read lazily, so reactive props work). */
91
+ readonly options: ProgressOptions;
92
+ }
93
+ /**
94
+ * Headless progress state machine. Rendering is left to CSS: the controller only
95
+ * exposes a target `value` and a `state`, which `<Progress>` mirrors to
96
+ * `--sp-value` and `data-state`.
97
+ */
98
+ declare function createProgress(options?: ProgressOptions): ProgressController;
99
+ //#endregion
100
+ //#region src/components.d.ts
101
+ /** Context carrying the active controller. Exposed for integrations; prefer `useProgress()`. */
102
+ declare const ProgressContext: Context<ProgressController | undefined>;
103
+ /**
104
+ * Read the nearest progress controller — from `<ProgressProvider>`, `<Progress>`,
105
+ * `<RouteProgress>` or `<NavigationProgress>`.
106
+ */
107
+ declare function useProgress(): ProgressController;
108
+ interface ProgressProviderProps extends ProgressOptions {
109
+ /** Share an existing controller instead of creating one. */
110
+ controller?: ProgressController;
111
+ children?: JSX.Element;
112
+ }
113
+ /** Provides a controller to descendants without rendering anything. */
114
+ declare function ProgressProvider(props: ProgressProviderProps): JSX.Element;
115
+ interface ProgressProps extends ProgressOptions, Omit<JSX.HTMLAttributes<HTMLDivElement>, "style" | "children" | "role"> {
116
+ /** Drive this bar from an existing controller (defaults to the nearest provided one, else a new one). */
117
+ controller?: ProgressController;
118
+ /** Inline styles; merged with the custom properties the component sets. */
119
+ style?: JSX.CSSProperties;
120
+ /** Accessible name announced for the progress bar. @default "Loading" */
121
+ label?: string;
122
+ /**
123
+ * Human-readable text for a known value, announced as `aria-valuetext` (as in Kobalte and
124
+ * Radix), e.g. `(percent) => percent + ' percent uploaded'`. Not used while trickling.
125
+ */
126
+ getValueLabel?: (percent: number) => string;
127
+ /**
128
+ * Set `data-sp-busy` on `<html>` while the bar is visible, for page-wide styling hooks
129
+ * (e.g. Tailwind `in-data-sp-busy:opacity-50`). With several bars it stays set until the
130
+ * last of them goes idle.
131
+ * @default true
132
+ */
133
+ busyAttribute?: boolean;
134
+ /**
135
+ * Also set `aria-busy="true"` on `<html>` while the bar is visible, as Turbo does. Off by
136
+ * default: how screen readers treat a busy root varies.
137
+ * @default false
138
+ */
139
+ ariaBusy?: boolean;
140
+ /** Custom template. Defaults to `<Bar />`. */
141
+ children?: JSX.Element;
142
+ }
143
+ /**
144
+ * The bar shell. Renders a fixed, full-width `role="progressbar"` element and mirrors the
145
+ * controller into `--sp-value` / `--sp-speed` / `data-state` / `data-error`; everything
146
+ * visual lives in `style.css`. Renders the idle shell on the server.
147
+ */
148
+ declare function Progress(props: ProgressProps): JSX.Element;
149
+ /** The sliding bar: a full-width strip slid in from the inline-start edge. */
150
+ declare function Bar(props: ParentProps<JSX.HTMLAttributes<HTMLDivElement>>): JSX.Element;
151
+ //#endregion
152
+ //#region src/navigation-api.d.ts
153
+ /**
154
+ * Minimal structural typings for the Navigation API (Baseline 2026-01), kept local so the
155
+ * library type-checks against any `lib.dom` version.
156
+ */
157
+ interface NavigateEventLike extends Event {
158
+ readonly navigationType: "push" | "replace" | "reload" | "traverse";
159
+ readonly destination: {
160
+ readonly url: string;
161
+ readonly sameDocument: boolean;
162
+ };
163
+ readonly canIntercept: boolean;
164
+ readonly userInitiated: boolean;
165
+ readonly hashChange: boolean;
166
+ readonly downloadRequest: string | null;
167
+ readonly formData: FormData | null;
168
+ /** The element that initiated the navigation, when known (Chrome 135, Firefox 147, Safari 26.2). */
169
+ readonly sourceElement?: Element | null;
170
+ }
171
+ /** Anchors (or their ancestors) carrying this attribute never trigger the bar. */
172
+ declare const IGNORE_ATTRIBUTE = "data-sp-ignore";
173
+ //#endregion
174
+ //#region src/cross-document.d.ts
175
+ interface CrossDocumentOptions {
176
+ /**
177
+ * Safety net: a cross-document navigation that never unloads the page and is never
178
+ * reported as cancelled (a `204` response, a server-sent download) completes the bar after
179
+ * this many milliseconds. `0` disables it.
180
+ * @default 10000
181
+ */
182
+ timeout?: number;
183
+ /** Decide per navigation. Return `false` to keep the bar hidden. */
184
+ filter?: (event: NavigateEventLike) => boolean;
185
+ }
186
+ /**
187
+ * Show the bar for navigations the page starts that leave the current document: external
188
+ * links, plain form posts, `location` assignments and reloads, back/forward to another
189
+ * document. Browser-UI navigations (the reload button, the address bar, bookmarks) never
190
+ * reach the page, so they show nothing.
191
+ *
192
+ * Uses the Navigation API `navigate` event, which fires before the request is made; where
193
+ * the API is missing this does nothing. Links marked `data-sp-ignore` are skipped. The bar
194
+ * fades out when the navigation is cancelled before the document unloads (`navigateerror`:
195
+ * a stop, a newer navigation) or the page is restored from the back/forward cache, and
196
+ * completes after `timeout`, the only end for a `204` or a server-sent download, which
197
+ * Chromium does not report back. A navigation a router intercepts instead completes on
198
+ * `navigatesuccess` / `navigateerror`.
199
+ */
200
+ declare function createCrossDocumentProgress(controller: ProgressController, options?: CrossDocumentOptions): void;
201
+ //#endregion
202
+ export { Release as _, Bar as a, ProgressProps as c, useProgress as d, DisposableLike as f, ProgressState as g, ProgressOptions as h, NavigateEventLike as i, ProgressProvider as l, ProgressController as m, createCrossDocumentProgress as n, Progress as o, Outcome as p, IGNORE_ATTRIBUTE as r, ProgressContext as s, CrossDocumentOptions as t, ProgressProviderProps as u, TrackOptions as v, createProgress as y };
package/dist/shared.js ADDED
@@ -0,0 +1,360 @@
1
+ import { batch, createContext, createEffect, createSignal, getOwner, onCleanup, onMount, splitProps, useContext } from "solid-js";
2
+ import { createComponent, getNextElement, insert, isDev as DEV, isServer, memo, mergeProps, runHydrationEvents, spread, template, use } from "solid-js/web";
3
+ //#region src/core.ts
4
+ /** `Symbol.dispose` where the runtime has it. */
5
+ const dispose = Symbol.dispose;
6
+ const DEFAULTS = {
7
+ trickleTo: .95,
8
+ delay: 200,
9
+ stopDelay: 0,
10
+ speed: 200
11
+ };
12
+ /**
13
+ * Headless progress state machine. Rendering is left to CSS: the controller only
14
+ * exposes a target `value` and a `state`, which `<Progress>` mirrors to
15
+ * `--sp-value` and `data-state`.
16
+ */
17
+ function createProgress(options = {}) {
18
+ const [value, setValue] = createSignal(0);
19
+ const [state, setState] = createSignal("idle");
20
+ const [error, setError] = createSignal(false);
21
+ const opt = (key) => options[key] ?? DEFAULTS[key];
22
+ const holds = /* @__PURE__ */ new Set();
23
+ /** The one scheduled step: reveal while `idle`, resume trickling while `active`, hide while `done`. */
24
+ let timer;
25
+ let stopTimer;
26
+ /** `true` once the browser has had a chance to paint the visible bar. */
27
+ let painted = false;
28
+ /** A hold was released as an `'error'` during the current load. */
29
+ let failed = false;
30
+ const schedule = (step, ms) => {
31
+ clearTimeout(timer);
32
+ timer = setTimeout(() => {
33
+ timer = void 0;
34
+ step();
35
+ }, ms);
36
+ };
37
+ const cancel = () => {
38
+ clearTimeout(timer);
39
+ timer = void 0;
40
+ };
41
+ /** A reveal is scheduled: the load has not lasted `delay` yet. */
42
+ const pending = () => state() === "idle" && timer !== void 0;
43
+ const move = (s, v) => batch(() => {
44
+ setState(s);
45
+ setValue(v);
46
+ });
47
+ const hide = () => {
48
+ failed = false;
49
+ batch(() => {
50
+ setError(false);
51
+ move("idle", 0);
52
+ });
53
+ };
54
+ const drop = () => {
55
+ cancel();
56
+ hide();
57
+ };
58
+ const reveal = (s, v) => {
59
+ painted = false;
60
+ requestAnimationFrame(() => painted = true);
61
+ move(s, v);
62
+ };
63
+ const show = () => reveal("trickle", opt("trickleTo"));
64
+ const finish = () => {
65
+ if (!painted) return drop();
66
+ batch(() => {
67
+ setError(failed);
68
+ move("done", 1);
69
+ });
70
+ schedule(hide, opt("speed"));
71
+ };
72
+ /** Every hold is gone: complete or drop the bar, or cancel a reveal that is not due yet. */
73
+ const settle = (cancelled) => {
74
+ const s = state();
75
+ if (s === "trickle" || s === "active") {
76
+ const end = cancelled && !failed ? drop : finish;
77
+ const stopDelay = opt("stopDelay");
78
+ clearTimeout(stopTimer);
79
+ if (stopDelay > 0) stopTimer = setTimeout(end, stopDelay);
80
+ else end();
81
+ } else if (s === "idle") {
82
+ cancel();
83
+ failed = false;
84
+ }
85
+ };
86
+ const release = (hold, outcome) => {
87
+ if (!holds.delete(hold)) return;
88
+ if (outcome === "error") failed = true;
89
+ if (!holds.size) settle(outcome === "cancel");
90
+ };
91
+ const start = () => {
92
+ const hold = {};
93
+ if (!isServer) {
94
+ holds.add(hold);
95
+ clearTimeout(stopTimer);
96
+ const s = state();
97
+ if (s === "done") {
98
+ hide();
99
+ schedule(show, opt("speed"));
100
+ } else if (s === "idle" && !pending()) {
101
+ const delay = opt("delay");
102
+ if (delay > 0) schedule(show, delay);
103
+ else show();
104
+ }
105
+ }
106
+ const releaseHold = (outcome) => release(hold, outcome);
107
+ if (dispose) releaseHold[dispose] = releaseHold;
108
+ return releaseHold;
109
+ };
110
+ const done = (outcome) => {
111
+ holds.clear();
112
+ if (outcome === "error") failed = true;
113
+ settle(outcome === "cancel");
114
+ };
115
+ const set = (n) => {
116
+ if (isServer) return;
117
+ n = Math.min(1, Math.max(0, n));
118
+ if (n === 1) return done();
119
+ const s = state();
120
+ if (s === "done" || pending()) return;
121
+ if (s === "idle") reveal("active", n);
122
+ else move("active", n);
123
+ const trickleTo = opt("trickleTo");
124
+ if (n < trickleTo) schedule(() => move("trickle", trickleTo), opt("speed"));
125
+ else cancel();
126
+ };
127
+ const track = (promise, options) => {
128
+ const releaseHold = start();
129
+ promise.then(() => releaseHold(), () => releaseHold("error"));
130
+ const timeout = options?.timeout;
131
+ if (timeout && !isServer) setTimeout(releaseHold, timeout);
132
+ return promise;
133
+ };
134
+ if (getOwner()) onCleanup(() => {
135
+ clearTimeout(timer);
136
+ clearTimeout(stopTimer);
137
+ });
138
+ return {
139
+ value,
140
+ state,
141
+ error,
142
+ active: () => state() !== "idle",
143
+ start,
144
+ done,
145
+ set,
146
+ track,
147
+ options
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region src/dev.ts
152
+ const warn = (message) => console.warn(`[sprogress] ${message}`);
153
+ //#endregion
154
+ //#region src/components.tsx
155
+ var _tmpl$ = /*#__PURE__*/ template(`<div>`);
156
+ /** Context carrying the active controller. Exposed for integrations; prefer `useProgress()`. */
157
+ const ProgressContext = createContext();
158
+ /**
159
+ * Read the nearest progress controller — from `<ProgressProvider>`, `<Progress>`,
160
+ * `<RouteProgress>` or `<NavigationProgress>`.
161
+ */
162
+ function useProgress() {
163
+ const controller = useContext(ProgressContext);
164
+ if (!controller) throw new Error("useProgress(): no progress controller in scope. Wrap the app in <ProgressProvider> (the route bar picks it up automatically) or call it inside <Progress>.");
165
+ return controller;
166
+ }
167
+ /** The props that configure a controller, as opposed to the bar element. */
168
+ const OPTION_KEYS = [
169
+ "trickleTo",
170
+ "delay",
171
+ "stopDelay",
172
+ "speed"
173
+ ];
174
+ /**
175
+ * The controller a bar drives: the `controller` prop, else the nearest `<ProgressProvider>`,
176
+ * else a new one built from `options`. Options only apply in the last case, so development
177
+ * builds say so when they would be dropped.
178
+ */
179
+ function useController(controller, options) {
180
+ const provided = controller ?? useContext(ProgressContext);
181
+ if (!provided) return createProgress(options);
182
+ if (DEV) {
183
+ const dropped = OPTION_KEYS.filter((key) => options[key] !== void 0);
184
+ if (dropped.length) warn(`${dropped.join(", ")} ignored: this bar drives an existing controller, so set options where it is created (createProgress() or <ProgressProvider>).`);
185
+ }
186
+ return provided;
187
+ }
188
+ /** Provides a controller to descendants without rendering anything. */
189
+ function ProgressProvider(props) {
190
+ const [local, options] = splitProps(props, ["controller", "children"]);
191
+ const controller = local.controller ?? createProgress(options);
192
+ return createComponent(ProgressContext.Provider, {
193
+ value: controller,
194
+ get children() {
195
+ return local.children;
196
+ }
197
+ });
198
+ }
199
+ const LOCAL = [
200
+ "controller",
201
+ "style",
202
+ "label",
203
+ "getValueLabel",
204
+ "busyAttribute",
205
+ "ariaBusy",
206
+ "children",
207
+ "class"
208
+ ];
209
+ /** Bars currently marking the page busy, per attribute: it stays until the last one goes idle. */
210
+ const busyBars = /* @__PURE__ */ new Map();
211
+ function createBusyAttribute(attribute, value, active) {
212
+ let bars = busyBars.get(attribute);
213
+ if (!bars) busyBars.set(attribute, bars = /* @__PURE__ */ new Set());
214
+ const bar = {};
215
+ const sync = (on) => {
216
+ if (on) bars.add(bar);
217
+ else bars.delete(bar);
218
+ if (bars.size) document.documentElement.setAttribute(attribute, value);
219
+ else document.documentElement.removeAttribute(attribute);
220
+ };
221
+ createEffect(() => sync(active()));
222
+ onCleanup(() => sync(false));
223
+ }
224
+ /**
225
+ * The bar shell. Renders a fixed, full-width `role="progressbar"` element and mirrors the
226
+ * controller into `--sp-value` / `--sp-speed` / `data-state` / `data-error`; everything
227
+ * visual lives in `style.css`. Renders the idle shell on the server.
228
+ */
229
+ function Progress(props) {
230
+ const [local, options, rest] = splitProps(props, LOCAL, OPTION_KEYS);
231
+ const controller = useController(local.controller, options);
232
+ let root;
233
+ const valueNow = () => controller.state() === "trickle" ? void 0 : Math.round(controller.value() * 100);
234
+ const valueText = () => {
235
+ const percent = valueNow();
236
+ return percent === void 0 ? void 0 : local.getValueLabel?.(percent);
237
+ };
238
+ if (!isServer) {
239
+ if (DEV) onMount(() => {
240
+ if (root.getClientRects().length && getComputedStyle(root).position === "static") warn("style.css is not loaded: import 'solid-route-progress/style.css' once.");
241
+ });
242
+ createBusyAttribute("data-sp-busy", "", () => local.busyAttribute !== false && controller.active());
243
+ createBusyAttribute("aria-busy", "true", () => local.ariaBusy === true && controller.active());
244
+ }
245
+ return createComponent(ProgressContext.Provider, {
246
+ value: controller,
247
+ get children() {
248
+ var _el$ = getNextElement(_tmpl$);
249
+ var _ref$ = root;
250
+ typeof _ref$ === "function" ? use(_ref$, _el$) : root = _el$;
251
+ spread(_el$, mergeProps(rest, {
252
+ "role": "progressbar",
253
+ get ["aria-label"]() {
254
+ return local.label ?? "Loading";
255
+ },
256
+ "aria-valuemin": 0,
257
+ "aria-valuemax": 100,
258
+ get ["aria-valuenow"]() {
259
+ return valueNow();
260
+ },
261
+ get ["aria-valuetext"]() {
262
+ return valueText();
263
+ },
264
+ get ["data-state"]() {
265
+ return controller.state();
266
+ },
267
+ get ["data-error"]() {
268
+ return controller.error() ? "" : void 0;
269
+ },
270
+ get ["class"]() {
271
+ return memo(() => !!local.class)() ? `sprogress ${local.class}` : "sprogress";
272
+ },
273
+ get style() {
274
+ return {
275
+ ...local.style,
276
+ "--sp-value": controller.value(),
277
+ "--sp-speed": `${controller.options.speed ?? DEFAULTS.speed}ms`
278
+ };
279
+ }
280
+ }), false, true);
281
+ insert(_el$, () => local.children ?? createComponent(Bar, {}));
282
+ runHydrationEvents();
283
+ return _el$;
284
+ }
285
+ });
286
+ }
287
+ /** The sliding bar: a full-width strip slid in from the inline-start edge. */
288
+ function Bar(props) {
289
+ const [local, rest] = splitProps(props, ["class"]);
290
+ return (() => {
291
+ var _el$2 = getNextElement(_tmpl$);
292
+ spread(_el$2, mergeProps({ get ["class"]() {
293
+ return memo(() => !!local.class)() ? `sprogress-bar ${local.class}` : "sprogress-bar";
294
+ } }, rest), false, false);
295
+ runHydrationEvents();
296
+ return _el$2;
297
+ })();
298
+ }
299
+ //#endregion
300
+ //#region src/navigation-api.ts
301
+ /** `window.navigation`, or `undefined` where the Navigation API is unavailable. */
302
+ const getNavigation = () => globalThis.navigation;
303
+ /** Anchors (or their ancestors) carrying this attribute never trigger the bar. */
304
+ const IGNORE_ATTRIBUTE = "data-sp-ignore";
305
+ /** Whether `target` is, or sits inside, an element marked `data-sp-ignore`. */
306
+ const isIgnored = (target) => target instanceof Element && target.closest(`[data-sp-ignore]`) !== null;
307
+ /**
308
+ * An `AbortSignal` that fires when the surrounding Solid owner is disposed. Pass it to
309
+ * `addEventListener` and the listener removes itself.
310
+ */
311
+ function disposalSignal() {
312
+ const controller = new AbortController();
313
+ if (getOwner()) onCleanup(() => controller.abort());
314
+ return controller.signal;
315
+ }
316
+ //#endregion
317
+ //#region src/cross-document.ts
318
+ /**
319
+ * Show the bar for navigations the page starts that leave the current document: external
320
+ * links, plain form posts, `location` assignments and reloads, back/forward to another
321
+ * document. Browser-UI navigations (the reload button, the address bar, bookmarks) never
322
+ * reach the page, so they show nothing.
323
+ *
324
+ * Uses the Navigation API `navigate` event, which fires before the request is made; where
325
+ * the API is missing this does nothing. Links marked `data-sp-ignore` are skipped. The bar
326
+ * fades out when the navigation is cancelled before the document unloads (`navigateerror`:
327
+ * a stop, a newer navigation) or the page is restored from the back/forward cache, and
328
+ * completes after `timeout`, the only end for a `204` or a server-sent download, which
329
+ * Chromium does not report back. A navigation a router intercepts instead completes on
330
+ * `navigatesuccess` / `navigateerror`.
331
+ */
332
+ function createCrossDocumentProgress(controller, options = {}) {
333
+ const navigation = getNavigation();
334
+ if (isServer || !navigation) return;
335
+ const signal = disposalSignal();
336
+ let timer;
337
+ /** Releases the hold of the navigation still in flight. */
338
+ let release;
339
+ const finish = (outcome) => {
340
+ clearTimeout(timer);
341
+ release?.(outcome);
342
+ release = void 0;
343
+ };
344
+ navigation.addEventListener("navigate", (event) => {
345
+ if (event.defaultPrevented || event.destination.sameDocument || event.downloadRequest !== null || !/^https?:/.test(event.destination.url) || isIgnored(event.sourceElement) || options.filter?.(event) === false) return;
346
+ const previous = release;
347
+ release = controller.start();
348
+ previous?.();
349
+ clearTimeout(timer);
350
+ const timeout = options.timeout ?? 1e4;
351
+ if (timeout > 0) timer = setTimeout(finish, timeout);
352
+ }, { signal });
353
+ navigation.addEventListener("currententrychange", () => navigation.transition && clearTimeout(timer), { signal });
354
+ navigation.addEventListener("navigatesuccess", () => finish(), { signal });
355
+ navigation.addEventListener("navigateerror", (event) => finish(event.error?.name === "AbortError" ? "cancel" : "error"), { signal });
356
+ window.addEventListener("pageshow", (event) => event.persisted && finish("cancel"), { signal });
357
+ signal.addEventListener("abort", () => finish());
358
+ }
359
+ //#endregion
360
+ export { isIgnored as a, Progress as c, useController as d, useProgress as f, createProgress as h, getNavigation as i, ProgressContext as l, warn as m, IGNORE_ATTRIBUTE as n, Bar as o, DEV as p, disposalSignal as r, OPTION_KEYS as s, createCrossDocumentProgress as t, ProgressProvider as u };