@vctqs1/nav-progress-bar-react 0.0.2-4
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/README.md +153 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +112 -0
- package/dist/lib/nav-progress-bar-react.d.ts +11 -0
- package/dist/lib/nav-progress-bar-react.d.ts.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @vctqs1/nav-progress-bar-react
|
|
2
|
+
|
|
3
|
+
React wrapper for [`@vctqs1/nav-progress-bar`](https://www.npmjs.com/package/@vctqs1/nav-progress-bar) — a zero-dependency, CSP-safe top-of-page progress bar built as a native Web Component.
|
|
4
|
+
|
|
5
|
+
This package provides a thin React component that renders the `<vctqs1-nav-progress-bar>` custom element with proper TypeScript JSX types and SSR support via Declarative Shadow DOM.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @vctqs1/nav-progress-bar @vctqs1/nav-progress-bar-react
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @vctqs1/nav-progress-bar @vctqs1/nav-progress-bar-react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Peer Dependencies
|
|
16
|
+
|
|
17
|
+
| Package | Version |
|
|
18
|
+
|---------|---------|
|
|
19
|
+
| `react` | `>=18.0.0` |
|
|
20
|
+
| `@vctqs1/nav-progress-bar` | `>=1.0.0` |
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Next.js App Router (recommended setup)
|
|
25
|
+
|
|
26
|
+
**1. Add to root layout** (`app/layout.tsx`):
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import NavProgressBar from '@vctqs1/nav-progress-bar-react';
|
|
30
|
+
|
|
31
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
32
|
+
return (
|
|
33
|
+
<html>
|
|
34
|
+
<body>
|
|
35
|
+
<NavProgressBar />
|
|
36
|
+
{children}
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Create `instrumentation-client.ts`** at the project root:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { registerNavProgressBar, getNavProgressBar } from '@vctqs1/nav-progress-bar-react';
|
|
47
|
+
|
|
48
|
+
registerNavProgressBar();
|
|
49
|
+
|
|
50
|
+
export function onRouterTransitionStart(
|
|
51
|
+
url: string,
|
|
52
|
+
navigationType: 'push' | 'replace' | 'traverse',
|
|
53
|
+
) {
|
|
54
|
+
getNavProgressBar()?.start();
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**3. Enable instrumentation** in `next.config.ts`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const nextConfig = {
|
|
62
|
+
experimental: {
|
|
63
|
+
instrumentationHook: true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default nextConfig;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That's it. The bar starts on every route departure and finishes automatically when the new page commits.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Custom color
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<NavProgressBar primary="#ff6600" />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
<NavProgressBar primary="--your-brand-color" />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### React SPA (Vite, CRA, etc.)
|
|
87
|
+
|
|
88
|
+
For non-SSR React apps, register and place the component anywhere above your routes:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// main.tsx
|
|
92
|
+
import { registerNavProgressBar } from '@vctqs1/nav-progress-bar-react';
|
|
93
|
+
registerNavProgressBar();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// App.tsx
|
|
98
|
+
import NavProgressBar from '@vctqs1/nav-progress-bar-react';
|
|
99
|
+
|
|
100
|
+
export default function App() {
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<NavProgressBar />
|
|
104
|
+
<RouterProvider router={router} />
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
In a plain SPA the bar auto-starts and auto-finishes via the browser Navigation API — no extra wiring needed.
|
|
111
|
+
|
|
112
|
+
## Props
|
|
113
|
+
|
|
114
|
+
| Prop | Type | Default | Description |
|
|
115
|
+
|------|------|---------|-------------|
|
|
116
|
+
| `primary` | `string` | `#006bde` | Bar color — accepts any CSS color or a `--css-variable` name |
|
|
117
|
+
|
|
118
|
+
## How SSR Works
|
|
119
|
+
|
|
120
|
+
The component renders a `<vctqs1-nav-progress-bar>` custom element server-side. It uses `<template shadowrootmode="open">` (Declarative Shadow DOM) to pre-render the bar div so the element has visual structure before JavaScript runs — preventing a flash of invisible bar during hydration.
|
|
121
|
+
|
|
122
|
+
When the custom element upgrades in the browser it detects the existing shadow root:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const shadow = this.shadowRoot ?? this.attachShadow({ mode: 'open' });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
And skips creating a duplicate bar div if one already exists:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
if (!shadow.querySelector('.bar')) {
|
|
132
|
+
const bar = document.createElement('div');
|
|
133
|
+
bar.className = 'bar';
|
|
134
|
+
shadow.append(bar);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Why a Separate Package?
|
|
139
|
+
|
|
140
|
+
The core `@vctqs1/nav-progress-bar` package has zero dependencies and works in any environment. This wrapper exists solely to:
|
|
141
|
+
|
|
142
|
+
1. Provide correct TypeScript JSX types for `<vctqs1-nav-progress-bar>` so React doesn't complain about an unknown element
|
|
143
|
+
2. Ship a typed React component with a clean import
|
|
144
|
+
3. Handle the Declarative Shadow DOM template for SSR without pulling React into the core package
|
|
145
|
+
|
|
146
|
+
## Related
|
|
147
|
+
|
|
148
|
+
- [`@vctqs1/nav-progress-bar`](https://www.npmjs.com/package/@vctqs1/nav-progress-bar) — core Web Component, framework-agnostic
|
|
149
|
+
- [Next.js issue #43548](https://github.com/vercel/next.js/issues/43548) — the problem this solves
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0BAA0B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import "react/jsx-runtime";
|
|
2
|
+
//#region ../nav-progress-bar/dist/index.js
|
|
3
|
+
function e() {
|
|
4
|
+
let e = globalThis.navigation;
|
|
5
|
+
if (!(!e || typeof e != "object") && !(!("addEventListener" in e) || !("removeEventListener" in e))) return e;
|
|
6
|
+
}
|
|
7
|
+
var t = class extends HTMLElement {
|
|
8
|
+
_layoutSheet = void 0;
|
|
9
|
+
_sheet = void 0;
|
|
10
|
+
_progressSheet = void 0;
|
|
11
|
+
_autoTimer = void 0;
|
|
12
|
+
_startTime = void 0;
|
|
13
|
+
_options;
|
|
14
|
+
static get observedAttributes() {
|
|
15
|
+
return ["primary"];
|
|
16
|
+
}
|
|
17
|
+
constructor(e = {}) {
|
|
18
|
+
super(), this._options = e;
|
|
19
|
+
}
|
|
20
|
+
_onNavigateSuccess = () => this.finish();
|
|
21
|
+
_onNavigate = () => this.start();
|
|
22
|
+
connectedCallback() {
|
|
23
|
+
this._render();
|
|
24
|
+
let t = e();
|
|
25
|
+
t && (t.addEventListener("navigate", this._onNavigate), t.addEventListener("navigatesuccess", this._onNavigateSuccess));
|
|
26
|
+
}
|
|
27
|
+
disconnectedCallback() {
|
|
28
|
+
let t = e();
|
|
29
|
+
t && (t.removeEventListener("navigate", this._onNavigate), t.removeEventListener("navigatesuccess", this._onNavigateSuccess)), this._clearTimer();
|
|
30
|
+
}
|
|
31
|
+
attributeChangedCallback() {
|
|
32
|
+
this._sheet && this._applyColorSheet();
|
|
33
|
+
}
|
|
34
|
+
_resolveColor(e, t, n) {
|
|
35
|
+
let r = e ?? t;
|
|
36
|
+
return r ? r.startsWith("--") ? `var(${r}, ${n})` : r : n;
|
|
37
|
+
}
|
|
38
|
+
_buildColorSheet() {
|
|
39
|
+
let e = this._resolveColor(this.getAttribute("primary"), this._options.primary, "#006bde"), t = new CSSStyleSheet();
|
|
40
|
+
return t.insertRule(`
|
|
41
|
+
.bar {
|
|
42
|
+
height: 3px;
|
|
43
|
+
background: ${e};
|
|
44
|
+
opacity: 0;
|
|
45
|
+
transition: width 0.3s ease, opacity 0.4s ease;
|
|
46
|
+
box-shadow: 0 0 8px color-mix(in srgb, ${e} 53%, transparent);
|
|
47
|
+
}
|
|
48
|
+
`), t.insertRule(".bar.active { opacity: 1; }"), t.insertRule("\n .bar.complete {\n opacity: 0;\n transition: width 0.2s ease, opacity 0.5s ease 0.2s;\n }\n "), t;
|
|
49
|
+
}
|
|
50
|
+
_applyColorSheet() {
|
|
51
|
+
!this.shadowRoot || !this._layoutSheet || !this._progressSheet || (this._sheet = this._buildColorSheet(), this.shadowRoot.adoptedStyleSheets = [
|
|
52
|
+
this._layoutSheet,
|
|
53
|
+
this._sheet,
|
|
54
|
+
this._progressSheet
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
_render() {
|
|
58
|
+
let e = this.shadowRoot ?? this.attachShadow({ mode: "open" }), t = new CSSStyleSheet();
|
|
59
|
+
t.insertRule("\n :host {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n z-index: 9999;\n pointer-events: none;\n }\n "), this._layoutSheet = t;
|
|
60
|
+
let n = new CSSStyleSheet();
|
|
61
|
+
if (n.insertRule(".bar { width: 0%; }"), this._progressSheet = n, this._sheet = this._buildColorSheet(), e.adoptedStyleSheets = [
|
|
62
|
+
this._layoutSheet,
|
|
63
|
+
this._sheet,
|
|
64
|
+
this._progressSheet
|
|
65
|
+
], !e.querySelector(".bar")) {
|
|
66
|
+
let t = document.createElement("div");
|
|
67
|
+
t.className = "bar", e.append(t);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
_setWidth(e) {
|
|
71
|
+
this._progressSheet && this._progressSheet.replaceSync(`.bar { width: ${e}%; }`);
|
|
72
|
+
}
|
|
73
|
+
start() {
|
|
74
|
+
this._clearTimer(), this._setWidth(0), this._applyActive(!0);
|
|
75
|
+
let e = 0;
|
|
76
|
+
return this._autoTimer = setInterval(() => {
|
|
77
|
+
e += (85 - e) * .08, this._setWidth(Math.min(e, 85));
|
|
78
|
+
}, 200), this;
|
|
79
|
+
}
|
|
80
|
+
finish() {
|
|
81
|
+
this._clearTimer();
|
|
82
|
+
let e = this.shadowRoot?.querySelector(".bar");
|
|
83
|
+
return e ? (e.classList.remove("active"), e.classList.add("complete"), this._setWidth(100), setTimeout(() => {
|
|
84
|
+
e.classList.remove("complete"), this._setWidth(0), this._applyActive(!1);
|
|
85
|
+
}, 700), this) : this;
|
|
86
|
+
}
|
|
87
|
+
_applyActive(e) {
|
|
88
|
+
let t = this.shadowRoot?.querySelector(".bar");
|
|
89
|
+
if (t && (t.classList.toggle("active", e), e && this._startTime === void 0 && (this._startTime = performance.now()), !e && this._startTime !== void 0)) {
|
|
90
|
+
let e = performance.now() - this._startTime;
|
|
91
|
+
console.log(`[top-loading-bar] navigation took ${e.toFixed(0)}ms`), this._startTime = void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
_clearTimer() {
|
|
95
|
+
this._autoTimer !== void 0 && (clearInterval(this._autoTimer), this._autoTimer = void 0);
|
|
96
|
+
}
|
|
97
|
+
}, n = "vctqs1-nav-progress-bar";
|
|
98
|
+
function r(r) {
|
|
99
|
+
if (!(typeof customElements > "u") && e() && !customElements.get("vctqs1-nav-progress-bar")) {
|
|
100
|
+
class e extends t {
|
|
101
|
+
constructor() {
|
|
102
|
+
super(r);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
customElements.define(n, e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function i() {
|
|
109
|
+
return document.querySelector(n);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { t as NavProgressBar, n as TAG_NAME, i as getNavProgressBar, r as registerNavProgressBar };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NavProgressBarOptions } from '@vctqs1/nav-progress-bar';
|
|
2
|
+
declare module "react" {
|
|
3
|
+
namespace JSX {
|
|
4
|
+
interface IntrinsicElements {
|
|
5
|
+
"vctqs1-nav-progress-bar": import("react").HTMLAttributes<HTMLElement> & NavProgressBarOptions;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
declare function NavProgressBar({ primary }: NavProgressBarOptions): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export default NavProgressBar;
|
|
11
|
+
//# sourceMappingURL=nav-progress-bar-react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav-progress-bar-react.d.ts","sourceRoot":"","sources":["../../src/lib/nav-progress-bar-react.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAErE,OAAO,QAAQ,OAAO,CAAC;IACrB,UAAU,GAAG,CAAC;QACZ,UAAU,iBAAiB;YACzB,yBAAyB,EAAE,OAAO,OAAO,EAAE,cAAc,CAAC,WAAW,CAAC,GAAG,qBAAqB,CAAC;SAChG;KACF;CACF;AAED,iBAAS,cAAc,CAAC,EACtB,OAAmB,EACpB,EAAE,qBAAqB,2CAEvB;AAED,eAAe,cAAc,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vctqs1/nav-progress-bar-react",
|
|
3
|
+
"version": "0.0.2-4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!**/*.tsbuildinfo"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"progress-bar",
|
|
24
|
+
"navigation",
|
|
25
|
+
"react",
|
|
26
|
+
"web-component",
|
|
27
|
+
"custom-element",
|
|
28
|
+
"nextjs",
|
|
29
|
+
"app-router",
|
|
30
|
+
"loading",
|
|
31
|
+
"csp",
|
|
32
|
+
"shadow-dom"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@vctqs1/nav-progress-bar": "0.0.2-4"
|
|
39
|
+
},
|
|
40
|
+
"nx": {
|
|
41
|
+
"name": "@vctqs1/nav-progress-bar-react",
|
|
42
|
+
"projectType": "library"
|
|
43
|
+
}
|
|
44
|
+
}
|