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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kecan0406
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 ADDED
@@ -0,0 +1,376 @@
1
+ # solid-route-progress
2
+
3
+ [![CI](https://github.com/kecan0406/solid-route-progress/actions/workflows/ci.yml/badge.svg)](https://github.com/kecan0406/solid-route-progress/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Docs and a live demo: https://solid-route-progress.vercel.app
7
+
8
+ A web-native route progress bar for [SolidJS](https://solidjs.com) and [`@solidjs/router`](https://github.com/solidjs/solid-router), in the spirit of [NProgress](https://github.com/rstacruz/nprogress) and [BProgress](https://bprogress.vercel.app/):
9
+
10
+ - CSS does the animating: JavaScript writes the target value (`--sp-value`), the hop speed (`--sp-speed`), and one attribute (`data-state`). The loading trickle is a single long CSS transition, and no JS timer steps the bar forward. You can change its motion, color, and shape in CSS. `--sp-value` is registered with `@property`, so an element of your own can transition it too, such as a `conic-gradient()` ring.
11
+ - Quick loads never draw: A navigation shorter than `delay` (200 ms by default) shows nothing. Even with `delay: 0`, one that settles before the next frame is dropped.
12
+ - Tailwind CSS v4 ready: Styles ship in the `components.sprogress` sublayer, so utilities, your own `@layer components` rules, and unlayered CSS all win without `!important`. `data-state`, `data-error`, and `<html data-sp-busy>` work as Tailwind variants.
13
+ - Tiny: The core is about 2.1 kB min+gzip (the headless `createProgress` alone tree-shakes to about 0.8 kB), the router integration about 0.6 kB, the Navigation API one about 0.5 kB, and the CSS about 0.5 kB. It has no dependencies.
14
+ - Covers the whole navigation: It hooks `useIsRouting()`, so `<A>` clicks, `navigate()`, back/forward, action redirects, and every `<Suspense>` the new route waits on all show the bar. Navigations the page starts that leave the document (external links, plain form posts, `location.reload()`) show it too, through the Navigation API.
15
+ - SSR-safe and RTL-aware: It renders the idle shell on the server and exposes a labeled `role="progressbar"`. The bar is indeterminate while trickling, and once you `set()` a value it gets `aria-valuenow`, plus `aria-valuetext` from `getValueLabel`. It flips direction under `dir="rtl"` and paints with the system `Highlight` color under forced colors.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ pnpm add solid-route-progress
21
+ ```
22
+
23
+ Peer dependencies: `solid-js ^1.9` and, for the router integration, `@solidjs/router >= 1.0`.
24
+
25
+ Server rendering needs a bundler that resolves the `solid` export condition (`vite-plugin-solid`, SolidStart): the `default` export is compiled for the DOM.
26
+
27
+ The stylesheet uses modern CSS: `@layer`, `@property`, `linear()`, `oklch()` and `:dir()`. A browser without one of them may draw the bar wrongly or not at all.
28
+
29
+ ## Quick start (with `@solidjs/router`)
30
+
31
+ ```css
32
+ /* app.css */
33
+ @import 'tailwindcss'; /* optional */
34
+ @import 'solid-route-progress/style.css';
35
+ ```
36
+
37
+ ```tsx
38
+ import { Router, Route } from '@solidjs/router'
39
+ import { Suspense } from 'solid-js'
40
+ import { RouteProgress } from 'solid-route-progress/router'
41
+
42
+ const Layout = (props) => (
43
+ <>
44
+ <RouteProgress />
45
+ <Suspense>{props.children}</Suspense>
46
+ </>
47
+ )
48
+
49
+ <Router root={Layout}>
50
+ <Route path="/" component={Home} />
51
+ </Router>
52
+ ```
53
+
54
+ Render `<RouteProgress />` once anywhere under `<Router>`, and import the stylesheet once.
55
+
56
+ ### SolidStart
57
+
58
+ It takes the same two steps: import the stylesheet in `src/app.css`, and render the bar in the `<Router>` root of `src/app.tsx`.
59
+
60
+ ```tsx
61
+ // src/app.tsx
62
+ import { Router } from '@solidjs/router'
63
+ import { FileRoutes } from '@solidjs/start/router'
64
+ import { Suspense } from 'solid-js'
65
+ import { RouteProgress } from 'solid-route-progress/router'
66
+ import './app.css'
67
+
68
+ export default function App() {
69
+ return (
70
+ <Router
71
+ root={(props) => (
72
+ <>
73
+ <RouteProgress />
74
+ <Suspense>{props.children}</Suspense>
75
+ </>
76
+ )}
77
+ >
78
+ <FileRoutes />
79
+ </Router>
80
+ )
81
+ }
82
+ ```
83
+
84
+ ## Styling
85
+
86
+ Every tunable is a CSS custom property with a fallback, so you can set it on `:root`, in Tailwind's `@theme`, on a class, or inline:
87
+
88
+ | Property | Default | What it does |
89
+ | ----------------------- | ---------------------- | -------------------------------- |
90
+ | `--sp-color` | `oklch(0.65 0.14 241)` | bar color |
91
+ | `--sp-height` | `3px` | bar thickness |
92
+ | `--sp-z-index` | `9999` | |
93
+ | `--sp-start` | `0.08` | value the bar is revealed at |
94
+ | `--sp-trickle-duration` | `10s` | how long the loading drift takes |
95
+ | `--sp-trickle-easing` | `linear(…)` | shape of the drift |
96
+
97
+ `--sp-speed` (the `set()`/`done()` hop and fade duration) is written from the `speed` option so the CSS transitions and the JS timers share one clock: read it in your own rules, set it through `speed`.
98
+
99
+ ```css
100
+ :root {
101
+ /* any CSS colour, or a Tailwind v4 token: var(--color-indigo-500) */
102
+ --sp-color: oklch(0.62 0.19 264);
103
+ --sp-height: 2px;
104
+ }
105
+ ```
106
+
107
+ `oklch()` suits a bar well: keep the lightness and chroma and turn the hue, and the colors keep the same visual weight. Follow the color scheme with `light-dark()`:
108
+
109
+ ```css
110
+ :root {
111
+ color-scheme: light dark;
112
+ --sp-color: light-dark(oklch(0.55 0.2 264), oklch(0.75 0.12 264));
113
+ }
114
+ ```
115
+
116
+ Or use utilities, which beat the component layer without `!important`:
117
+
118
+ ```tsx
119
+ <RouteProgress class="h-1 data-[state=done]:opacity-50" />
120
+ ```
121
+
122
+ ### Inside a container
123
+
124
+ The bar is a normal component. Render it inside the container and make it `absolute` instead of `fixed`. The container needs `position: relative` and `overflow: hidden`.
125
+
126
+ ```tsx
127
+ <div class="relative overflow-hidden rounded-xl">
128
+ <Progress controller={panel} class="absolute" />…
129
+ </div>
130
+ ```
131
+
132
+ ### Hooks for the rest of the page
133
+
134
+ - `data-state="idle | trickle | active | done"` on the bar element, plus `data-error` during the `done` phase of a load that failed.
135
+ - `data-sp-busy` on `<html>` while a bar is visible. With several bars it stays until the last one goes idle; `busyAttribute={false}` opts a bar out. `ariaBusy` also sets `aria-busy="true"` there (off by default: how screen readers treat a busy root varies):
136
+
137
+ ```tsx
138
+ <main class="transition-opacity [[data-sp-busy]_&]:opacity-60">…</main>
139
+ ```
140
+
141
+ ### Your own template
142
+
143
+ The default template is a single `<Bar />`. Compose whatever you need. Children can read the controller with `useProgress()`:
144
+
145
+ ```tsx
146
+ import { Bar, useProgress } from 'solid-route-progress'
147
+
148
+ ;<RouteProgress>
149
+ <Bar class="rounded-r-full" />
150
+ <Percent />
151
+ </RouteProgress>
152
+
153
+ const Percent = () => <output>{Math.round(useProgress().value() * 100)}%</output>
154
+ ```
155
+
156
+ `--sp-value` is registered as a `<number>`, so an element of your own can transition it, e.g. a ring. Keep the `var()` fallbacks: without them the declarations are invalid unless you set those properties yourself.
157
+
158
+ ```css
159
+ .ring {
160
+ background: conic-gradient(
161
+ var(--sp-color, oklch(0.65 0.14 241)) calc(var(--sp-value) * 1turn),
162
+ transparent 0
163
+ );
164
+ transition: --sp-value var(--sp-trickle-duration, 10s) var(--sp-trickle-easing, ease-out);
165
+ }
166
+ ```
167
+
168
+ ### Recipes: NProgress' glow and spinner, a failed load
169
+
170
+ None of these ships in `style.css`; paste the one you want.
171
+
172
+ ```css
173
+ /* a failed load: `data-error` is set while the bar completes */
174
+ .sprogress[data-error] {
175
+ --sp-color: oklch(0.63 0.19 23);
176
+ }
177
+
178
+ /* the glow "peg" at the leading edge */
179
+ .sprogress-bar::after {
180
+ content: '';
181
+ position: absolute;
182
+ inset-inline-end: 0;
183
+ width: 100px;
184
+ height: 100%;
185
+ box-shadow:
186
+ 0 0 10px var(--sp-color, oklch(0.65 0.14 241)),
187
+ 0 0 5px var(--sp-color, oklch(0.65 0.14 241));
188
+ transform: rotate(3deg) translateY(-4px);
189
+ }
190
+ .sprogress:dir(rtl) .sprogress-bar::after {
191
+ transform: rotate(-3deg) translateY(-4px);
192
+ }
193
+
194
+ /* a spinner in the inline-end corner: <div class="spinner" aria-hidden="true" /> next to <Bar /> */
195
+ .spinner {
196
+ position: absolute;
197
+ top: 15px;
198
+ inset-inline-end: 15px;
199
+ box-sizing: border-box;
200
+ width: 18px;
201
+ height: 18px;
202
+ border: 2px solid transparent;
203
+ border-block-start-color: var(--sp-color, oklch(0.65 0.14 241));
204
+ border-inline-start-color: var(--sp-color, oklch(0.65 0.14 241));
205
+ border-radius: 50%;
206
+ animation: spin 400ms linear infinite;
207
+ }
208
+ .sprogress[data-state='idle'] .spinner {
209
+ animation: none;
210
+ }
211
+ @media (prefers-reduced-motion: reduce) {
212
+ .spinner {
213
+ display: none;
214
+ }
215
+ }
216
+ @keyframes spin {
217
+ to {
218
+ rotate: 1turn;
219
+ }
220
+ }
221
+ ```
222
+
223
+ ## Behavior options
224
+
225
+ All props of `<RouteProgress>` (and `<Progress>` / `createProgress()`):
226
+
227
+ | Prop | Default | Description |
228
+ | --------------- | --------- | ---------------------------------------------------------------------------------------- |
229
+ | `trickleTo` | `0.95` | value the bar drifts toward while loading |
230
+ | `delay` | `200` | ms before the bar shows; faster loads never show it |
231
+ | `stopDelay` | `0` | ms to wait before completing |
232
+ | `speed` | `200` | ms for hops/fades (written to `--sp-speed`) |
233
+ | `label` | `Loading` | accessible name |
234
+ | `getValueLabel` | none | `(percent) => string` for `aria-valuetext` when the value is known (not while trickling) |
235
+ | `busyAttribute` | `true` | toggle `data-sp-busy` on `<html>` |
236
+ | `ariaBusy` | `false` | also toggle `aria-busy="true"` on `<html>` |
237
+ | `controller` | none | drive the bar from a controller you own |
238
+
239
+ Options belong to whoever creates the controller. Inside `<ProgressProvider>` put them on the provider: a bar that picks up a provided controller (or a `controller` prop) has nothing to apply them to, and says so in development. Development builds (solid-js's `development` export condition) also warn when `style.css` is not loaded.
240
+
241
+ Router-specific:
242
+
243
+ | Prop | Default | Description |
244
+ | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `shallow` | `false` | skip navigations that only change the search string or hash |
246
+ | `filter` | none | `(to, from) => boolean`; return `false` to skip a navigation |
247
+ | `crossDocument` | `true` | also show the bar when the page leaves the document (external links, plain form posts, `location.reload()`). Needs the Navigation API; browser-UI navigations (reload button, address bar) never reach the page. Pass `{ timeout, filter }` to tune the 10 s safety net or skip navigations by event. |
248
+
249
+ A navigation that leaves the document but is cancelled (a stop, a newer navigation) or comes back from the back/forward cache fades the bar out instead of running it to 100 %. One a router intercepts completes on `navigatesuccess` / `navigateerror`; the timeout stops applying once it commits. `navigate` events another listener cancelled are skipped.
250
+
251
+ Mark any link (or a whole nav) with `data-sp-ignore` to keep the bar hidden for it. This works for `<A>`, plain anchors, and Navigation API navigations alike (plain anchors rely on `NavigateEvent.sourceElement`: Chrome 135, Firefox 147, Safari 26.2).
252
+
253
+ ## Manual control
254
+
255
+ Wrap the app in `<ProgressProvider>`; `<RouteProgress>` then drives the provider's controller and `useProgress()` reaches it from anywhere, which is handy for fetches, uploads, or anything else:
256
+
257
+ ```tsx
258
+ import { ProgressProvider, useProgress } from 'solid-route-progress'
259
+
260
+ const Layout = (props) => (
261
+ <ProgressProvider delay={300}>
262
+ <RouteProgress />
263
+ <Suspense>{props.children}</Suspense>
264
+ </ProgressProvider>
265
+ )
266
+
267
+ // anywhere below
268
+ const progress = useProgress()
269
+
270
+ const release = progress.start() // hold the bar open: show (after `delay`) + trickle
271
+ progress.set(0.6) // hop to 60 %, then keep trickling (ignored while `delay` is still pending)
272
+ release() // let go; the bar completes once every hold is released
273
+ // or: release('error') — completes with `data-error`; release('cancel') — fades out without reaching 100 %
274
+
275
+ progress.track(fetch('/api/items')) // hold until the promise settles; a rejection is an 'error'
276
+ progress.track(upload(file), { timeout: 30_000 }) // let go after 30 s even if it never settles
277
+ progress.done() // complete now, dropping every hold (also takes an outcome)
278
+
279
+ {
280
+ using hold = progress.start() // where `Symbol.dispose` exists, a release is also a disposable
281
+ await work()
282
+ } // released here, even on throw
283
+
284
+ progress.value() // Accessor<number> — target value
285
+ progress.state() // Accessor<'idle' | 'trickle' | 'active' | 'done'>
286
+ progress.active() // Accessor<boolean>
287
+ progress.error() // Accessor<boolean> — true during the done phase of a failed load
288
+ ```
289
+
290
+ Every source holds the bar separately: `<RouteProgress>`, cross-document navigations, and each `track()`. A route that finishes first therefore never cuts a tracked fetch short. `done()` overrides them all and ends every hold. An `'error'` from any hold wins when the last one lets go; `'cancel'` only fades the bar out when nothing failed. A load that settles before the bar shows (within `delay`, or before the first frame) draws nothing, failed or not.
291
+
292
+ Or create your own with `createProgress(options)` and render it with `<Progress controller={…} />`, which works without a router.
293
+
294
+ ## Without `@solidjs/router`: the Navigation API
295
+
296
+ `solid-route-progress/navigation` drives the bar from the browser's [Navigation API](https://developer.mozilla.org/docs/Web/API/Navigation_API) (Baseline since January 2026). It starts on `navigate` and completes once the navigation settles: on `navigatesuccess` or `navigateerror` for navigations a router intercepts, or right away for a plain `pushState` nobody intercepts (over before it paints, so nothing shows). An intercepted navigation starts as a `navigate` event whose `destination.sameDocument` is `false` (it only becomes same-document once intercepted), so it is held like a cross-document one, and the safety timeout stops applying once it commits. A `navigateerror` from an abort (a stop, a newer navigation) fades the bar out. Any other, such as a rejected intercept handler, completes it as an `'error'`. Cross-document navigations are covered exactly as with the router integration. Where the API is missing, nothing is tracked (development builds say so).
297
+
298
+ Routers that don't intercept through the Navigation API (including `@solidjs/router`) load their data outside of it, so their loads are invisible here. Use `solid-route-progress/router` for those.
299
+
300
+ ```tsx
301
+ import { NavigationProgress } from 'solid-route-progress/navigation'
302
+
303
+ ;<NavigationProgress filter={(e) => e.navigationType !== 'replace'} />
304
+ ```
305
+
306
+ ## Coming from NProgress / BProgress
307
+
308
+ | There | Here |
309
+ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
310
+ | `minimum` | `--sp-start` |
311
+ | `trickleSpeed`, `easing`, `speed` | `--sp-trickle-duration`, `--sp-trickle-easing`, `speed` / `--sp-speed` |
312
+ | `trickle: false` | `trickleTo` equal to `--sp-start`: the bar reveals and waits for `set()` / `done()` |
313
+ | `color`, `height`, `template` | `--sp-color`, `--sp-height`, children |
314
+ | `showSpinner`, `spinnerPosition` | the spinner recipe, placed with your own `top` / `inset-inline-end` |
315
+ | `parent` | render the bar inside the container, `class="absolute"` |
316
+ | `direction: 'rtl'` | automatic under `dir="rtl"` |
317
+ | `startPosition`, `delay`, `stopDelay` | `set(n)`, `delay` (default 200 ms), `stopDelay` |
318
+ | `shallowRouting`, `targetPreprocessor` | `shallow`, `filter(to, from)` |
319
+ | `disableSameURL` | built in: navigations the router drops never show a bar |
320
+ | `data-disable-progress`, `data-prevent-progress` | `data-sp-ignore` |
321
+ | `promise()` | `track(promise)`; `start()` returns its own release |
322
+ | `inc()`, `dec()`, `pause()`, `resume()` | none: the drift is a CSS transition, so JS holds no current position to step or freeze. Stepwise loads: `set(k / n)`. |
323
+ | `indeterminate` | none: trickling already reads as indeterminate to assistive tech; style `data-state="trickle"` as you like |
324
+ | `nonce`, `style`, `disableStyle`, `memo` | not needed: styles are a stylesheet you import, components are plain Solid |
325
+
326
+ ## API surface
327
+
328
+ ```ts
329
+ // solid-route-progress
330
+ createProgress(options?): ProgressController // { start(): Release, done(outcome?), set, track(promise, { timeout? }), value, state, active, error, options }
331
+ type Release = (outcome?: 'error' | 'cancel') => void // & Disposable where Symbol.dispose exists
332
+ // types: Release, Outcome, TrackOptions, DisposableLike, ProgressController, ProgressOptions, ProgressState
333
+ <Progress>, <ProgressProvider>, <Bar>, useProgress(), ProgressContext
334
+ createCrossDocumentProgress(controller, { timeout?, filter? })
335
+ IGNORE_ATTRIBUTE // 'data-sp-ignore'
336
+
337
+ // solid-route-progress/router
338
+ <RouteProgress>, createRouteProgress(controller, { shallow?, filter?, crossDocument? })
339
+
340
+ // solid-route-progress/navigation
341
+ <NavigationProgress>, createNavigationProgress(controller, { filter?, timeout? })
342
+ ```
343
+
344
+ ## How it works
345
+
346
+ 1. `start()` waits `delay` (200 ms), then flips `data-state` to `trickle` and sets `--sp-value` to `trickleTo` (0.95). The stylesheet's `trickle` rule has a 10 s transition on `transform` whose curve races out and then crawls. It is one transition, and no timer steps it.
347
+ 2. `set(n)` switches to the `active` rule (short `--sp-speed` transition) for the hop, then hands back to `trickle`. CSS transitions interrupt from the _current_ animated value, so there is nothing to sync.
348
+ 3. Once the last hold is released (or on `done()`), the bar moves to 100 % under the `done` rule (with `data-error` if a hold was released as an `'error'`; a `'cancel'` skips this step and fades straight out), then `idle` fades the whole bar out with `opacity` + a delayed `visibility: hidden`. The bar itself is parked back at `--sp-start` only after the fade has finished. Both steps take `speed`, which the bar also writes to `--sp-speed`, so the CSS and the timers never disagree.
349
+ 4. If the load ends while `delay` is still pending, or before the browser painted the bar (tracked with a single `requestAnimationFrame`), the bar is dropped silently.
350
+
351
+ ## Development
352
+
353
+ ```sh
354
+ pnpm install
355
+ pnpm dev # Vite + Tailwind v4 playground at http://localhost:5199 (and /navigation.html)
356
+ pnpm dev:www # landing page + docs (SolidStart, MDX) at http://localhost:5200
357
+ pnpm lint # oxlint, with eslint-plugin-solid loaded as a JS plugin
358
+ pnpm format:check
359
+ pnpm test # Vitest: jsdom, SSR, and real Chromium, Firefox and WebKit
360
+ # (once: pnpm exec playwright install chromium firefox webkit)
361
+ pnpm typecheck # the whole repo, plus the published entries under isolatedDeclarations
362
+ pnpm build # tsdown → dist/*.js (DOM), dist/*.jsx (`solid` condition), d.ts, style.css
363
+ pnpm size # minified gzip/brotli budget, incl. `createProgress` tree-shaken on its own
364
+ pnpm check # lint, typecheck, test, build, size
365
+ pnpm changeset # describe a change for the next release's notes
366
+ ```
367
+
368
+ CI runs `pnpm format:check`, `pnpm check` and the docs build on every push and pull request. Releases go through changesets with npm trusted publishing (provenance included).
369
+
370
+ The package ships JSX untouched under the `solid` export condition, so SolidStart / `vite-plugin-solid` compile it for DOM or SSR as appropriate, plus a DOM-compiled build for everyone else. That build does not render on the server, so SSR needs a bundler that resolves `solid`.
371
+
372
+ Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Report security issues as described in [SECURITY.md](SECURITY.md).
373
+
374
+ ## License
375
+
376
+ MIT
@@ -0,0 +1,2 @@
1
+ import { _ as Release, a as Bar, c as ProgressProps, d as useProgress, f as DisposableLike, g as ProgressState, h as ProgressOptions, i as NavigateEventLike, l as ProgressProvider, m as ProgressController, n as createCrossDocumentProgress, o as Progress, p as Outcome, r as IGNORE_ATTRIBUTE, s as ProgressContext, t as CrossDocumentOptions, u as ProgressProviderProps, v as TrackOptions, y as createProgress } from "./shared.js";
2
+ export { Bar, type CrossDocumentOptions, type DisposableLike, IGNORE_ATTRIBUTE, type NavigateEventLike, type Outcome, Progress, ProgressContext, type ProgressController, type ProgressOptions, type ProgressProps, ProgressProvider, type ProgressProviderProps, type ProgressState, type Release, type TrackOptions, createCrossDocumentProgress, createProgress, useProgress };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { c as Progress, f as useProgress, h as createProgress, l as ProgressContext, n as IGNORE_ATTRIBUTE, o as Bar, t as createCrossDocumentProgress, u as ProgressProvider } from "./shared.js";
2
+ export { Bar, IGNORE_ATTRIBUTE, Progress, ProgressContext, ProgressProvider, createCrossDocumentProgress, createProgress, useProgress };
package/dist/index.jsx ADDED
@@ -0,0 +1,2 @@
1
+ import { c as Progress, f as useProgress, h as createProgress, l as ProgressContext, n as IGNORE_ATTRIBUTE, o as Bar, t as createCrossDocumentProgress, u as ProgressProvider } from "./shared.jsx";
2
+ export { Bar, IGNORE_ATTRIBUTE, Progress, ProgressContext, ProgressProvider, createCrossDocumentProgress, createProgress, useProgress };
@@ -0,0 +1,17 @@
1
+ import { c as ProgressProps, m as ProgressController, t as CrossDocumentOptions } from "./shared.js";
2
+ import { JSX } from "solid-js";
3
+ //#region src/navigation.d.ts
4
+ type NavigationProgressOptions = CrossDocumentOptions;
5
+ /**
6
+ * Router-agnostic integration built on the browser Navigation API: the bar starts on
7
+ * `navigate` and completes once the navigation settles, so any same-document router that
8
+ * intercepts navigations — or none at all — is covered. Cross-document navigations show
9
+ * the bar until the page unloads. Hash changes, `download` links and `data-sp-ignore`
10
+ * links are skipped. Where the API is unavailable this does nothing.
11
+ */
12
+ declare function createNavigationProgress(controller: ProgressController, options?: NavigationProgressOptions): void;
13
+ interface NavigationProgressProps extends ProgressProps, NavigationProgressOptions {}
14
+ /** `<Progress>` driven by the Navigation API. No router required. Picks up a surrounding `<ProgressProvider>`. */
15
+ declare function NavigationProgress(props: NavigationProgressProps): JSX.Element;
16
+ //#endregion
17
+ export { NavigationProgress, NavigationProgressOptions, NavigationProgressProps, createNavigationProgress };
@@ -0,0 +1,50 @@
1
+ import { a as isIgnored, c as Progress, d as useController, i as getNavigation, m as warn, p as DEV, r as disposalSignal, s as OPTION_KEYS, t as createCrossDocumentProgress } from "./shared.js";
2
+ import { splitProps } from "solid-js";
3
+ import { createComponent, isServer, mergeProps } from "solid-js/web";
4
+ //#region src/navigation.tsx
5
+ /**
6
+ * Router-agnostic integration built on the browser Navigation API: the bar starts on
7
+ * `navigate` and completes once the navigation settles, so any same-document router that
8
+ * intercepts navigations — or none at all — is covered. Cross-document navigations show
9
+ * the bar until the page unloads. Hash changes, `download` links and `data-sp-ignore`
10
+ * links are skipped. Where the API is unavailable this does nothing.
11
+ */
12
+ function createNavigationProgress(controller, options = {}) {
13
+ if (isServer) return;
14
+ const navigation = getNavigation();
15
+ if (!navigation) {
16
+ if (DEV) warn("Navigation API unavailable: NavigationProgress shows nothing in this browser.");
17
+ return;
18
+ }
19
+ createCrossDocumentProgress(controller, options);
20
+ const signal = disposalSignal();
21
+ let release;
22
+ const done = (outcome) => {
23
+ release?.(outcome);
24
+ release = void 0;
25
+ };
26
+ navigation.addEventListener("navigate", (event) => {
27
+ if (event.defaultPrevented || !event.destination.sameDocument || event.hashChange || isIgnored(event.sourceElement) || options.filter?.(event) === false) return;
28
+ const previous = release;
29
+ release = controller.start();
30
+ previous?.();
31
+ }, { signal });
32
+ navigation.addEventListener("currententrychange", () => navigation.transition || done(), { signal });
33
+ navigation.addEventListener("navigatesuccess", () => done(), { signal });
34
+ navigation.addEventListener("navigateerror", (event) => done(event.error?.name === "AbortError" ? "cancel" : "error"), { signal });
35
+ signal.addEventListener("abort", () => done());
36
+ }
37
+ const LOCAL = [
38
+ "filter",
39
+ "timeout",
40
+ "controller"
41
+ ];
42
+ /** `<Progress>` driven by the Navigation API. No router required. Picks up a surrounding `<ProgressProvider>`. */
43
+ function NavigationProgress(props) {
44
+ const [local, options, rest] = splitProps(props, LOCAL, OPTION_KEYS);
45
+ const controller = useController(local.controller, options);
46
+ createNavigationProgress(controller, local);
47
+ return createComponent(Progress, mergeProps({ controller }, rest));
48
+ }
49
+ //#endregion
50
+ export { NavigationProgress, createNavigationProgress };
@@ -0,0 +1,50 @@
1
+ import { a as isIgnored, c as Progress, d as useController, i as getNavigation, m as warn, p as DEV, r as disposalSignal, s as OPTION_KEYS, t as createCrossDocumentProgress } from "./shared.jsx";
2
+ import { splitProps } from "solid-js";
3
+ import { isServer } from "solid-js/web";
4
+ //#region src/navigation.tsx
5
+ /**
6
+ * Router-agnostic integration built on the browser Navigation API: the bar starts on
7
+ * `navigate` and completes once the navigation settles, so any same-document router that
8
+ * intercepts navigations — or none at all — is covered. Cross-document navigations show
9
+ * the bar until the page unloads. Hash changes, `download` links and `data-sp-ignore`
10
+ * links are skipped. Where the API is unavailable this does nothing.
11
+ */
12
+ function createNavigationProgress(controller, options = {}) {
13
+ if (isServer) return;
14
+ const navigation = getNavigation();
15
+ if (!navigation) {
16
+ if (DEV) warn("Navigation API unavailable: NavigationProgress shows nothing in this browser.");
17
+ return;
18
+ }
19
+ createCrossDocumentProgress(controller, options);
20
+ const signal = disposalSignal();
21
+ let release;
22
+ const done = (outcome) => {
23
+ release?.(outcome);
24
+ release = void 0;
25
+ };
26
+ navigation.addEventListener("navigate", (event) => {
27
+ if (event.defaultPrevented || !event.destination.sameDocument || event.hashChange || isIgnored(event.sourceElement) || options.filter?.(event) === false) return;
28
+ const previous = release;
29
+ release = controller.start();
30
+ previous?.();
31
+ }, { signal });
32
+ navigation.addEventListener("currententrychange", () => navigation.transition || done(), { signal });
33
+ navigation.addEventListener("navigatesuccess", () => done(), { signal });
34
+ navigation.addEventListener("navigateerror", (event) => done(event.error?.name === "AbortError" ? "cancel" : "error"), { signal });
35
+ signal.addEventListener("abort", () => done());
36
+ }
37
+ const LOCAL = [
38
+ "filter",
39
+ "timeout",
40
+ "controller"
41
+ ];
42
+ /** `<Progress>` driven by the Navigation API. No router required. Picks up a surrounding `<ProgressProvider>`. */
43
+ function NavigationProgress(props) {
44
+ const [local, options, rest] = splitProps(props, LOCAL, OPTION_KEYS);
45
+ const controller = useController(local.controller, options);
46
+ createNavigationProgress(controller, local);
47
+ return <Progress controller={controller} {...rest} />;
48
+ }
49
+ //#endregion
50
+ export { NavigationProgress, createNavigationProgress };
@@ -0,0 +1,45 @@
1
+ import { c as ProgressProps, m as ProgressController, t as CrossDocumentOptions } from "./shared.js";
2
+ import { JSX } from "solid-js";
3
+ import { Location } from "@solidjs/router";
4
+ //#region src/router.d.ts
5
+ interface RouteProgressOptions {
6
+ /**
7
+ * Skip the bar when a navigation keeps the same `pathname` and only changes the search
8
+ * string or hash (BProgress' "shallow routing").
9
+ * @default false
10
+ */
11
+ shallow?: boolean;
12
+ /**
13
+ * Decide per navigation. Return `false` to keep the bar hidden. `to` is the resolved
14
+ * path (`pathname + search + hash`); `from` is the current location.
15
+ */
16
+ filter?: (to: string, from: Location) => boolean;
17
+ /**
18
+ * Also show the bar for navigations the page starts that leave the document: external
19
+ * links, plain form posts, `location.reload()`. Needs the Navigation API; browser-UI
20
+ * navigations (reload button, address bar) never reach the page. Pass an object to tune
21
+ * the safety timeout or filter by event.
22
+ * @default true
23
+ */
24
+ crossDocument?: boolean | CrossDocumentOptions;
25
+ }
26
+ /**
27
+ * Wire a controller to `@solidjs/router`: `useIsRouting()` covers `<A>` clicks,
28
+ * `navigate()`, back/forward and action redirects, including any `<Suspense>` the new
29
+ * route waits on. `useBeforeLeave()` supplies the target for `shallow` / `filter`.
30
+ */
31
+ declare function createRouteProgress(controller: ProgressController, options?: RouteProgressOptions): void;
32
+ interface RouteProgressProps extends ProgressProps, RouteProgressOptions {}
33
+ /**
34
+ * Drop-in route progress bar. Place it anywhere under `<Router>` — typically in the root
35
+ * layout — and import `style.css` once. Inside a `<ProgressProvider>` it drives that
36
+ * provider's controller, so `useProgress()` works anywhere in the app.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <Router root={(props) => <><RouteProgress /><Suspense>{props.children}</Suspense></>}>
41
+ * ```
42
+ */
43
+ declare function RouteProgress(props: RouteProgressProps): JSX.Element;
44
+ //#endregion
45
+ export { RouteProgress, RouteProgressOptions, RouteProgressProps, createRouteProgress };
package/dist/router.js ADDED
@@ -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.js";
2
+ import { createEffect, on, onCleanup, splitProps } from "solid-js";
3
+ import { createComponent, isServer, mergeProps } 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 createComponent(Progress, mergeProps({ controller }, rest));
65
+ }
66
+ //#endregion
67
+ export { RouteProgress, createRouteProgress };