@nuraloom/navlink 1.0.1
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 +137 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +191 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
- package/src/NavLink.tsx +179 -0
- package/src/NavigationProgressClient.tsx +49 -0
- package/src/index.ts +7 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# 🧭 NavLink — Smart Active Link Component for Next.js (App Router)
|
|
2
|
+
|
|
3
|
+
A **production-ready `NavLink` component** for Next.js App Router.
|
|
4
|
+
It simplifies building navigation with **active link highlighting**, **SSR-safe active detection**, and **external link handling** — all fully typed with **TypeScript**.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
✅ Seamless integration with Next.js App Router (`usePathname`)
|
|
11
|
+
✅ Automatic **active link detection** (exact or partial match)
|
|
12
|
+
✅ Safe **external link handling** with proper security defaults
|
|
13
|
+
✅ Fully **typed** with `NavLinkProps`
|
|
14
|
+
✅ Built-in **accessibility** (`aria-current="page"`)
|
|
15
|
+
✅ **SSR-safe** — avoids hydration mismatches
|
|
16
|
+
✅ Minimal and **performance-optimized** (uses `useMemo` for all derived states)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @your-org/navlink
|
|
24
|
+
# or
|
|
25
|
+
yarn add @your-org/navlink
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
"use client";
|
|
34
|
+
|
|
35
|
+
import NavLink from "@your-org/navlink";
|
|
36
|
+
|
|
37
|
+
export default function Navbar() {
|
|
38
|
+
return (
|
|
39
|
+
<nav className="flex gap-4">
|
|
40
|
+
<NavLink href="/" exact>
|
|
41
|
+
Home
|
|
42
|
+
</NavLink>
|
|
43
|
+
<NavLink href="/about">About</NavLink>
|
|
44
|
+
<NavLink href="https://example.com" target="_blank">
|
|
45
|
+
External Site
|
|
46
|
+
</NavLink>
|
|
47
|
+
</nav>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Active Link Example
|
|
53
|
+
|
|
54
|
+
- When on `/about`, the “About” link automatically gets `activeClassName`.
|
|
55
|
+
- For `/about/team`, it also highlights `/about` unless you set `exact`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ⚙️ Props
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
| ----------------- | ----------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
63
|
+
| `href` | `string` | **required** | Destination URL (internal or external). |
|
|
64
|
+
| `exact` | `boolean` | `false` | Require exact match to mark as active. |
|
|
65
|
+
| `activeClassName` | `string` | `"text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20"` | Classes applied when active. |
|
|
66
|
+
| `className` | `string` | `"inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none"` | Base classes for the link. |
|
|
67
|
+
| `children` | `React.ReactNode` | — | Link label or content. |
|
|
68
|
+
| `[...rest]` | HTML anchor props | — | All other anchor attributes (`target`, `rel`, etc.). |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🧠 Active State Logic
|
|
73
|
+
|
|
74
|
+
The component:
|
|
75
|
+
|
|
76
|
+
- Uses `usePathname()` from `next/navigation`.
|
|
77
|
+
- Normalizes paths (`/about/` → `/about`).
|
|
78
|
+
- Supports **exact** or **startsWith** match.
|
|
79
|
+
- Avoids hydration mismatches during SSR.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🌐 External Link Handling
|
|
84
|
+
|
|
85
|
+
Automatically detects external URLs (`https://`, `http://`) and applies correct handling.
|
|
86
|
+
You can still override `target="_blank"` or `rel="noopener noreferrer"` manually if needed.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🧩 Type Exports
|
|
91
|
+
|
|
92
|
+
If you need strong typing in your project:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import type { NavLinkProps } from "@your-org/navlink";
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 🔍 Example Styling
|
|
101
|
+
|
|
102
|
+
You can customize styles easily with Tailwind or CSS:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<NavLink
|
|
106
|
+
href="/dashboard"
|
|
107
|
+
className="px-4 py-2 text-gray-700 hover:text-sky-600"
|
|
108
|
+
activeClassName="font-bold text-sky-600 border-b-2 border-sky-600"
|
|
109
|
+
>
|
|
110
|
+
Dashboard
|
|
111
|
+
</NavLink>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🧱 Folder Structure
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
src/
|
|
120
|
+
├── NavLink.tsx
|
|
121
|
+
└── index.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `index.ts`
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
export { default } from "./NavLink";
|
|
128
|
+
export type { NavLinkProps } from "./NavLink";
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 📜 License
|
|
134
|
+
|
|
135
|
+
MIT © 2025 NuraLoom
|
|
136
|
+
|
|
137
|
+
---
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface NavLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
5
|
+
href: string;
|
|
6
|
+
exact?: boolean;
|
|
7
|
+
activeClassName?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
declare function NavLink({ href, exact, activeClassName, className, children, ...rest }: NavLinkProps): react_jsx_runtime.JSX.Element;
|
|
12
|
+
declare namespace NavLink {
|
|
13
|
+
var displayName: string;
|
|
14
|
+
}
|
|
15
|
+
declare const useNavigate: () => (href: string, options?: {
|
|
16
|
+
replace?: boolean;
|
|
17
|
+
scroll?: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
|
|
20
|
+
declare const NavigationProgressClient: React.FC<{
|
|
21
|
+
color?: string;
|
|
22
|
+
height?: string;
|
|
23
|
+
duration?: number;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export { NavLink, type NavLinkProps, NavigationProgressClient as NavigationProgress, useNavigate };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface NavLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
5
|
+
href: string;
|
|
6
|
+
exact?: boolean;
|
|
7
|
+
activeClassName?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
declare function NavLink({ href, exact, activeClassName, className, children, ...rest }: NavLinkProps): react_jsx_runtime.JSX.Element;
|
|
12
|
+
declare namespace NavLink {
|
|
13
|
+
var displayName: string;
|
|
14
|
+
}
|
|
15
|
+
declare const useNavigate: () => (href: string, options?: {
|
|
16
|
+
replace?: boolean;
|
|
17
|
+
scroll?: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
|
|
20
|
+
declare const NavigationProgressClient: React.FC<{
|
|
21
|
+
color?: string;
|
|
22
|
+
height?: string;
|
|
23
|
+
duration?: number;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export { NavLink, type NavLinkProps, NavigationProgressClient as NavigationProgress, useNavigate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
12
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
13
|
+
var __spreadValues = (a, b) => {
|
|
14
|
+
for (var prop in b || (b = {}))
|
|
15
|
+
if (__hasOwnProp.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
if (__getOwnPropSymbols)
|
|
18
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
19
|
+
if (__propIsEnum.call(b, prop))
|
|
20
|
+
__defNormalProp(a, prop, b[prop]);
|
|
21
|
+
}
|
|
22
|
+
return a;
|
|
23
|
+
};
|
|
24
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
25
|
+
var __objRest = (source, exclude) => {
|
|
26
|
+
var target = {};
|
|
27
|
+
for (var prop in source)
|
|
28
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
29
|
+
target[prop] = source[prop];
|
|
30
|
+
if (source != null && __getOwnPropSymbols)
|
|
31
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
32
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
33
|
+
target[prop] = source[prop];
|
|
34
|
+
}
|
|
35
|
+
return target;
|
|
36
|
+
};
|
|
37
|
+
var __export = (target, all) => {
|
|
38
|
+
for (var name in all)
|
|
39
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
40
|
+
};
|
|
41
|
+
var __copyProps = (to, from, except, desc) => {
|
|
42
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
43
|
+
for (let key of __getOwnPropNames(from))
|
|
44
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
45
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
46
|
+
}
|
|
47
|
+
return to;
|
|
48
|
+
};
|
|
49
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
50
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
51
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
52
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
53
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
54
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
55
|
+
mod
|
|
56
|
+
));
|
|
57
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
58
|
+
|
|
59
|
+
// src/index.ts
|
|
60
|
+
var index_exports = {};
|
|
61
|
+
__export(index_exports, {
|
|
62
|
+
NavLink: () => NavLink,
|
|
63
|
+
NavigationProgress: () => NavigationProgressClient,
|
|
64
|
+
useNavigate: () => useNavigate
|
|
65
|
+
});
|
|
66
|
+
module.exports = __toCommonJS(index_exports);
|
|
67
|
+
|
|
68
|
+
// src/NavLink.tsx
|
|
69
|
+
var import_link = __toESM(require("next/link"));
|
|
70
|
+
var import_navigation = require("next/navigation");
|
|
71
|
+
var import_react = __toESM(require("react"));
|
|
72
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
73
|
+
var NavProgressManager = class {
|
|
74
|
+
constructor() {
|
|
75
|
+
this.startListeners = [];
|
|
76
|
+
this.finishListeners = [];
|
|
77
|
+
}
|
|
78
|
+
start() {
|
|
79
|
+
this.startListeners.forEach((cb) => cb());
|
|
80
|
+
}
|
|
81
|
+
finish() {
|
|
82
|
+
this.finishListeners.forEach((cb) => cb());
|
|
83
|
+
}
|
|
84
|
+
onStart(cb) {
|
|
85
|
+
this.startListeners.push(cb);
|
|
86
|
+
}
|
|
87
|
+
onFinish(cb) {
|
|
88
|
+
this.finishListeners.push(cb);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var progressManager = new NavProgressManager();
|
|
92
|
+
var EXTERNAL_LINK_REGEX = /^(https?:)?\/\//;
|
|
93
|
+
var DEFAULT_CLASSES = "inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none";
|
|
94
|
+
var DEFAULT_ACTIVE_CLASSES = "text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20";
|
|
95
|
+
var normalizePath = (path) => {
|
|
96
|
+
if (!path || path === "/") return "/";
|
|
97
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
98
|
+
};
|
|
99
|
+
var isExternalLink = (href) => EXTERNAL_LINK_REGEX.test(href);
|
|
100
|
+
var getPathnameFromExternalUrl = (url) => {
|
|
101
|
+
try {
|
|
102
|
+
return new URL(url).pathname;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function NavLink(_a) {
|
|
108
|
+
var _b = _a, {
|
|
109
|
+
href,
|
|
110
|
+
exact = false,
|
|
111
|
+
activeClassName = DEFAULT_ACTIVE_CLASSES,
|
|
112
|
+
className = DEFAULT_CLASSES,
|
|
113
|
+
children
|
|
114
|
+
} = _b, rest = __objRest(_b, [
|
|
115
|
+
"href",
|
|
116
|
+
"exact",
|
|
117
|
+
"activeClassName",
|
|
118
|
+
"className",
|
|
119
|
+
"children"
|
|
120
|
+
]);
|
|
121
|
+
const pathname = (0, import_navigation.usePathname)();
|
|
122
|
+
const router = (0, import_navigation.useRouter)();
|
|
123
|
+
const isExternal = import_react.default.useMemo(() => isExternalLink(href), [href]);
|
|
124
|
+
const isActive = import_react.default.useMemo(() => {
|
|
125
|
+
if (!pathname || isExternal) return false;
|
|
126
|
+
const currentPath = normalizePath(pathname);
|
|
127
|
+
const targetPath = href.startsWith("/") ? normalizePath(href) : normalizePath(getPathnameFromExternalUrl(href));
|
|
128
|
+
if (exact) return currentPath === targetPath;
|
|
129
|
+
if (targetPath === "/") return currentPath === "/";
|
|
130
|
+
return currentPath === targetPath || currentPath.startsWith(targetPath + "/");
|
|
131
|
+
}, [pathname, href, exact, isExternal]);
|
|
132
|
+
const mergedClassName = import_react.default.useMemo(() => {
|
|
133
|
+
const baseClasses = [className];
|
|
134
|
+
if (isActive && pathname !== null) baseClasses.push(activeClassName);
|
|
135
|
+
return baseClasses.filter(Boolean).join(" ").trim();
|
|
136
|
+
}, [className, isActive, activeClassName, pathname]);
|
|
137
|
+
const handleClick = (e) => {
|
|
138
|
+
if (isExternal) return;
|
|
139
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1)
|
|
140
|
+
return;
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
if (pathname === href) return;
|
|
143
|
+
progressManager.start();
|
|
144
|
+
router.push(href);
|
|
145
|
+
};
|
|
146
|
+
const ariaAttributes = import_react.default.useMemo(() => {
|
|
147
|
+
const attributes = {
|
|
148
|
+
"aria-current": isActive ? "page" : void 0
|
|
149
|
+
};
|
|
150
|
+
if (rest["aria-label"]) {
|
|
151
|
+
attributes["aria-label"] = rest["aria-label"];
|
|
152
|
+
}
|
|
153
|
+
return attributes;
|
|
154
|
+
}, [isActive, rest]);
|
|
155
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
156
|
+
import_link.default,
|
|
157
|
+
__spreadProps(__spreadValues(__spreadValues({
|
|
158
|
+
href,
|
|
159
|
+
className: mergedClassName
|
|
160
|
+
}, ariaAttributes), rest), {
|
|
161
|
+
onClick: handleClick,
|
|
162
|
+
prefetch: true,
|
|
163
|
+
children
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
NavLink.displayName = "NavLink";
|
|
168
|
+
var useNavigate = () => {
|
|
169
|
+
const router = (0, import_navigation.useRouter)();
|
|
170
|
+
const pathname = (0, import_navigation.usePathname)();
|
|
171
|
+
const navigate = (href, options) => {
|
|
172
|
+
if (pathname === href) return;
|
|
173
|
+
progressManager.start();
|
|
174
|
+
const { replace = false, scroll = true } = options || {};
|
|
175
|
+
if (replace) router.replace(href, { scroll });
|
|
176
|
+
else router.push(href, { scroll });
|
|
177
|
+
};
|
|
178
|
+
return navigate;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/NavigationProgressClient.tsx
|
|
182
|
+
var import_react2 = require("react");
|
|
183
|
+
var import_navigation2 = require("next/navigation");
|
|
184
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
185
|
+
var NavigationProgressClient = ({ color = "#2563EB", height = "3px", duration = 200 }) => {
|
|
186
|
+
const [width, setWidth] = (0, import_react2.useState)("0%");
|
|
187
|
+
const [visible, setVisible] = (0, import_react2.useState)(false);
|
|
188
|
+
const pathname = (0, import_navigation2.usePathname)();
|
|
189
|
+
(0, import_react2.useEffect)(() => {
|
|
190
|
+
progressManager.onStart(() => {
|
|
191
|
+
setVisible(true);
|
|
192
|
+
setWidth("0%");
|
|
193
|
+
setTimeout(() => setWidth("40%"), 10);
|
|
194
|
+
});
|
|
195
|
+
progressManager.onFinish(() => {
|
|
196
|
+
setWidth("100%");
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
setVisible(false);
|
|
199
|
+
setWidth("0%");
|
|
200
|
+
}, duration + 50);
|
|
201
|
+
});
|
|
202
|
+
}, [duration]);
|
|
203
|
+
(0, import_react2.useEffect)(() => {
|
|
204
|
+
progressManager.finish();
|
|
205
|
+
}, [pathname]);
|
|
206
|
+
if (!visible) return null;
|
|
207
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
208
|
+
"div",
|
|
209
|
+
{
|
|
210
|
+
style: {
|
|
211
|
+
width,
|
|
212
|
+
height,
|
|
213
|
+
backgroundColor: color,
|
|
214
|
+
transition: `width ${duration}ms ease`
|
|
215
|
+
},
|
|
216
|
+
className: "fixed top-0 left-0 z-[99999]"
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
221
|
+
0 && (module.exports = {
|
|
222
|
+
NavLink,
|
|
223
|
+
NavigationProgress,
|
|
224
|
+
useNavigate
|
|
225
|
+
});
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/NavLink.tsx","../src/NavigationProgressClient.tsx"],"sourcesContent":["\"use client\";\nexport { NavLink } from \"./NavLink\";\nexport type { NavLinkProps } from \"./NavLink\";\n\n// index.ts\nexport { NavigationProgressClient as NavigationProgress } from \"./NavigationProgressClient\";\nexport { useNavigate } from \"./NavLink\";\n","\"use client\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport React, { useEffect, useState } from \"react\";\n\n// --------------------\n// Types\n// --------------------\n\nexport interface NavLinkProps\n extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n href: string;\n exact?: boolean;\n activeClassName?: string;\n className?: string;\n children: React.ReactNode;\n}\n\n// --------------------\n// NavProgressManager\n// --------------------\n\ntype ProgressCallback = () => void;\n\nclass NavProgressManager {\n private startListeners: ProgressCallback[] = [];\n private finishListeners: ProgressCallback[] = [];\n\n start() {\n this.startListeners.forEach((cb) => cb());\n }\n\n finish() {\n this.finishListeners.forEach((cb) => cb());\n }\n\n onStart(cb: ProgressCallback) {\n this.startListeners.push(cb);\n }\n\n onFinish(cb: ProgressCallback) {\n this.finishListeners.push(cb);\n }\n}\n\nexport const progressManager = new NavProgressManager();\n\n// --------------------\n// Helper constants/functions for NavLink\n// --------------------\n\nconst EXTERNAL_LINK_REGEX = /^(https?:)?\\/\\//;\nconst DEFAULT_CLASSES =\n \"inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none\";\nconst DEFAULT_ACTIVE_CLASSES =\n \"text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20\";\n\nconst normalizePath = (path: string): string => {\n if (!path || path === \"/\") return \"/\";\n return path.endsWith(\"/\") ? path.slice(0, -1) : path;\n};\n\nconst isExternalLink = (href: string): boolean =>\n EXTERNAL_LINK_REGEX.test(href);\n\nconst getPathnameFromExternalUrl = (url: string): string => {\n try {\n return new URL(url).pathname;\n } catch {\n return url;\n }\n};\n\n// --------------------\n// NavLink Component\n// --------------------\n\nexport function NavLink({\n href,\n exact = false,\n activeClassName = DEFAULT_ACTIVE_CLASSES,\n className = DEFAULT_CLASSES,\n children,\n ...rest\n}: NavLinkProps) {\n const pathname = usePathname();\n const router = useRouter();\n\n const isExternal = React.useMemo(() => isExternalLink(href), [href]);\n\n const isActive = React.useMemo(() => {\n if (!pathname || isExternal) return false;\n\n const currentPath = normalizePath(pathname);\n const targetPath = href.startsWith(\"/\")\n ? normalizePath(href)\n : normalizePath(getPathnameFromExternalUrl(href));\n\n if (exact) return currentPath === targetPath;\n\n if (targetPath === \"/\") return currentPath === \"/\";\n\n return (\n currentPath === targetPath || currentPath.startsWith(targetPath + \"/\")\n );\n }, [pathname, href, exact, isExternal]);\n\n const mergedClassName = React.useMemo(() => {\n const baseClasses = [className];\n if (isActive && pathname !== null) baseClasses.push(activeClassName);\n return baseClasses.filter(Boolean).join(\" \").trim();\n }, [className, isActive, activeClassName, pathname]);\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n if (isExternal) return; // external links behave normally\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1)\n return;\n e.preventDefault();\n if (pathname === href) return;\n progressManager.start();\n router.push(href);\n };\n\n const ariaAttributes = React.useMemo(() => {\n const attributes: {\n \"aria-current\"?: \"page\" | undefined;\n \"aria-label\"?: string;\n } = {\n \"aria-current\": isActive ? \"page\" : undefined,\n };\n\n if (rest[\"aria-label\"]) {\n attributes[\"aria-label\"] = rest[\"aria-label\"];\n }\n\n return attributes;\n }, [isActive, rest]);\n\n return (\n <Link\n href={href}\n className={mergedClassName}\n {...ariaAttributes}\n {...rest}\n onClick={handleClick}\n prefetch={true}\n >\n {children}\n </Link>\n );\n}\n\nNavLink.displayName = \"NavLink\";\n\n// --------------------\n// useNavigate Hook\n// --------------------\n\nexport const useNavigate = () => {\n const router = useRouter();\n const pathname = usePathname();\n\n const navigate = (\n href: string,\n options?: { replace?: boolean; scroll?: boolean }\n ) => {\n if (pathname === href) return;\n progressManager.start();\n const { replace = false, scroll = true } = options || {};\n if (replace) router.replace(href, { scroll });\n else router.push(href, { scroll });\n };\n\n return navigate;\n};\n\n// --------------------\n// NavigationProgress Component\n// --------------------\n","\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { progressManager } from \"./NavLink\"; // export your singleton from your package\n\nexport const NavigationProgressClient: React.FC<{\n color?: string;\n height?: string;\n duration?: number;\n}> = ({ color = \"#2563EB\", height = \"3px\", duration = 200 }) => {\n const [width, setWidth] = useState(\"0%\");\n const [visible, setVisible] = useState(false);\n const pathname = usePathname();\n\n useEffect(() => {\n progressManager.onStart(() => {\n setVisible(true);\n setWidth(\"0%\");\n setTimeout(() => setWidth(\"40%\"), 10);\n });\n\n progressManager.onFinish(() => {\n setWidth(\"100%\");\n setTimeout(() => {\n setVisible(false);\n setWidth(\"0%\");\n }, duration + 50);\n });\n }, [duration]);\n\n useEffect(() => {\n progressManager.finish();\n }, [pathname]);\n\n if (!visible) return null;\n\n return (\n <div\n style={{\n width,\n height,\n backgroundColor: color,\n transition: `width ${duration}ms ease`,\n }}\n className=\"fixed top-0 left-0 z-[99999]\"\n />\n );\n};\n"],"mappings":"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","names":["React","Link","import_react","import_navigation","import_jsx_runtime"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
5
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __spreadValues = (a, b) => {
|
|
10
|
+
for (var prop in b || (b = {}))
|
|
11
|
+
if (__hasOwnProp.call(b, prop))
|
|
12
|
+
__defNormalProp(a, prop, b[prop]);
|
|
13
|
+
if (__getOwnPropSymbols)
|
|
14
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
+
if (__propIsEnum.call(b, prop))
|
|
16
|
+
__defNormalProp(a, prop, b[prop]);
|
|
17
|
+
}
|
|
18
|
+
return a;
|
|
19
|
+
};
|
|
20
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
+
var __objRest = (source, exclude) => {
|
|
22
|
+
var target = {};
|
|
23
|
+
for (var prop in source)
|
|
24
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
25
|
+
target[prop] = source[prop];
|
|
26
|
+
if (source != null && __getOwnPropSymbols)
|
|
27
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
28
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
29
|
+
target[prop] = source[prop];
|
|
30
|
+
}
|
|
31
|
+
return target;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/NavLink.tsx
|
|
35
|
+
import Link from "next/link";
|
|
36
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
37
|
+
import React from "react";
|
|
38
|
+
import { jsx } from "react/jsx-runtime";
|
|
39
|
+
var NavProgressManager = class {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.startListeners = [];
|
|
42
|
+
this.finishListeners = [];
|
|
43
|
+
}
|
|
44
|
+
start() {
|
|
45
|
+
this.startListeners.forEach((cb) => cb());
|
|
46
|
+
}
|
|
47
|
+
finish() {
|
|
48
|
+
this.finishListeners.forEach((cb) => cb());
|
|
49
|
+
}
|
|
50
|
+
onStart(cb) {
|
|
51
|
+
this.startListeners.push(cb);
|
|
52
|
+
}
|
|
53
|
+
onFinish(cb) {
|
|
54
|
+
this.finishListeners.push(cb);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var progressManager = new NavProgressManager();
|
|
58
|
+
var EXTERNAL_LINK_REGEX = /^(https?:)?\/\//;
|
|
59
|
+
var DEFAULT_CLASSES = "inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none";
|
|
60
|
+
var DEFAULT_ACTIVE_CLASSES = "text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20";
|
|
61
|
+
var normalizePath = (path) => {
|
|
62
|
+
if (!path || path === "/") return "/";
|
|
63
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
64
|
+
};
|
|
65
|
+
var isExternalLink = (href) => EXTERNAL_LINK_REGEX.test(href);
|
|
66
|
+
var getPathnameFromExternalUrl = (url) => {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url).pathname;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function NavLink(_a) {
|
|
74
|
+
var _b = _a, {
|
|
75
|
+
href,
|
|
76
|
+
exact = false,
|
|
77
|
+
activeClassName = DEFAULT_ACTIVE_CLASSES,
|
|
78
|
+
className = DEFAULT_CLASSES,
|
|
79
|
+
children
|
|
80
|
+
} = _b, rest = __objRest(_b, [
|
|
81
|
+
"href",
|
|
82
|
+
"exact",
|
|
83
|
+
"activeClassName",
|
|
84
|
+
"className",
|
|
85
|
+
"children"
|
|
86
|
+
]);
|
|
87
|
+
const pathname = usePathname();
|
|
88
|
+
const router = useRouter();
|
|
89
|
+
const isExternal = React.useMemo(() => isExternalLink(href), [href]);
|
|
90
|
+
const isActive = React.useMemo(() => {
|
|
91
|
+
if (!pathname || isExternal) return false;
|
|
92
|
+
const currentPath = normalizePath(pathname);
|
|
93
|
+
const targetPath = href.startsWith("/") ? normalizePath(href) : normalizePath(getPathnameFromExternalUrl(href));
|
|
94
|
+
if (exact) return currentPath === targetPath;
|
|
95
|
+
if (targetPath === "/") return currentPath === "/";
|
|
96
|
+
return currentPath === targetPath || currentPath.startsWith(targetPath + "/");
|
|
97
|
+
}, [pathname, href, exact, isExternal]);
|
|
98
|
+
const mergedClassName = React.useMemo(() => {
|
|
99
|
+
const baseClasses = [className];
|
|
100
|
+
if (isActive && pathname !== null) baseClasses.push(activeClassName);
|
|
101
|
+
return baseClasses.filter(Boolean).join(" ").trim();
|
|
102
|
+
}, [className, isActive, activeClassName, pathname]);
|
|
103
|
+
const handleClick = (e) => {
|
|
104
|
+
if (isExternal) return;
|
|
105
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1)
|
|
106
|
+
return;
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
if (pathname === href) return;
|
|
109
|
+
progressManager.start();
|
|
110
|
+
router.push(href);
|
|
111
|
+
};
|
|
112
|
+
const ariaAttributes = React.useMemo(() => {
|
|
113
|
+
const attributes = {
|
|
114
|
+
"aria-current": isActive ? "page" : void 0
|
|
115
|
+
};
|
|
116
|
+
if (rest["aria-label"]) {
|
|
117
|
+
attributes["aria-label"] = rest["aria-label"];
|
|
118
|
+
}
|
|
119
|
+
return attributes;
|
|
120
|
+
}, [isActive, rest]);
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
Link,
|
|
123
|
+
__spreadProps(__spreadValues(__spreadValues({
|
|
124
|
+
href,
|
|
125
|
+
className: mergedClassName
|
|
126
|
+
}, ariaAttributes), rest), {
|
|
127
|
+
onClick: handleClick,
|
|
128
|
+
prefetch: true,
|
|
129
|
+
children
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
NavLink.displayName = "NavLink";
|
|
134
|
+
var useNavigate = () => {
|
|
135
|
+
const router = useRouter();
|
|
136
|
+
const pathname = usePathname();
|
|
137
|
+
const navigate = (href, options) => {
|
|
138
|
+
if (pathname === href) return;
|
|
139
|
+
progressManager.start();
|
|
140
|
+
const { replace = false, scroll = true } = options || {};
|
|
141
|
+
if (replace) router.replace(href, { scroll });
|
|
142
|
+
else router.push(href, { scroll });
|
|
143
|
+
};
|
|
144
|
+
return navigate;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/NavigationProgressClient.tsx
|
|
148
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
149
|
+
import { usePathname as usePathname2 } from "next/navigation";
|
|
150
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
151
|
+
var NavigationProgressClient = ({ color = "#2563EB", height = "3px", duration = 200 }) => {
|
|
152
|
+
const [width, setWidth] = useState2("0%");
|
|
153
|
+
const [visible, setVisible] = useState2(false);
|
|
154
|
+
const pathname = usePathname2();
|
|
155
|
+
useEffect2(() => {
|
|
156
|
+
progressManager.onStart(() => {
|
|
157
|
+
setVisible(true);
|
|
158
|
+
setWidth("0%");
|
|
159
|
+
setTimeout(() => setWidth("40%"), 10);
|
|
160
|
+
});
|
|
161
|
+
progressManager.onFinish(() => {
|
|
162
|
+
setWidth("100%");
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
setVisible(false);
|
|
165
|
+
setWidth("0%");
|
|
166
|
+
}, duration + 50);
|
|
167
|
+
});
|
|
168
|
+
}, [duration]);
|
|
169
|
+
useEffect2(() => {
|
|
170
|
+
progressManager.finish();
|
|
171
|
+
}, [pathname]);
|
|
172
|
+
if (!visible) return null;
|
|
173
|
+
return /* @__PURE__ */ jsx2(
|
|
174
|
+
"div",
|
|
175
|
+
{
|
|
176
|
+
style: {
|
|
177
|
+
width,
|
|
178
|
+
height,
|
|
179
|
+
backgroundColor: color,
|
|
180
|
+
transition: `width ${duration}ms ease`
|
|
181
|
+
},
|
|
182
|
+
className: "fixed top-0 left-0 z-[99999]"
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
export {
|
|
187
|
+
NavLink,
|
|
188
|
+
NavigationProgressClient as NavigationProgress,
|
|
189
|
+
useNavigate
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NavLink.tsx","../src/NavigationProgressClient.tsx"],"sourcesContent":["\"use client\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport React, { useEffect, useState } from \"react\";\n\n// --------------------\n// Types\n// --------------------\n\nexport interface NavLinkProps\n extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"href\"> {\n href: string;\n exact?: boolean;\n activeClassName?: string;\n className?: string;\n children: React.ReactNode;\n}\n\n// --------------------\n// NavProgressManager\n// --------------------\n\ntype ProgressCallback = () => void;\n\nclass NavProgressManager {\n private startListeners: ProgressCallback[] = [];\n private finishListeners: ProgressCallback[] = [];\n\n start() {\n this.startListeners.forEach((cb) => cb());\n }\n\n finish() {\n this.finishListeners.forEach((cb) => cb());\n }\n\n onStart(cb: ProgressCallback) {\n this.startListeners.push(cb);\n }\n\n onFinish(cb: ProgressCallback) {\n this.finishListeners.push(cb);\n }\n}\n\nexport const progressManager = new NavProgressManager();\n\n// --------------------\n// Helper constants/functions for NavLink\n// --------------------\n\nconst EXTERNAL_LINK_REGEX = /^(https?:)?\\/\\//;\nconst DEFAULT_CLASSES =\n \"inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none\";\nconst DEFAULT_ACTIVE_CLASSES =\n \"text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20\";\n\nconst normalizePath = (path: string): string => {\n if (!path || path === \"/\") return \"/\";\n return path.endsWith(\"/\") ? path.slice(0, -1) : path;\n};\n\nconst isExternalLink = (href: string): boolean =>\n EXTERNAL_LINK_REGEX.test(href);\n\nconst getPathnameFromExternalUrl = (url: string): string => {\n try {\n return new URL(url).pathname;\n } catch {\n return url;\n }\n};\n\n// --------------------\n// NavLink Component\n// --------------------\n\nexport function NavLink({\n href,\n exact = false,\n activeClassName = DEFAULT_ACTIVE_CLASSES,\n className = DEFAULT_CLASSES,\n children,\n ...rest\n}: NavLinkProps) {\n const pathname = usePathname();\n const router = useRouter();\n\n const isExternal = React.useMemo(() => isExternalLink(href), [href]);\n\n const isActive = React.useMemo(() => {\n if (!pathname || isExternal) return false;\n\n const currentPath = normalizePath(pathname);\n const targetPath = href.startsWith(\"/\")\n ? normalizePath(href)\n : normalizePath(getPathnameFromExternalUrl(href));\n\n if (exact) return currentPath === targetPath;\n\n if (targetPath === \"/\") return currentPath === \"/\";\n\n return (\n currentPath === targetPath || currentPath.startsWith(targetPath + \"/\")\n );\n }, [pathname, href, exact, isExternal]);\n\n const mergedClassName = React.useMemo(() => {\n const baseClasses = [className];\n if (isActive && pathname !== null) baseClasses.push(activeClassName);\n return baseClasses.filter(Boolean).join(\" \").trim();\n }, [className, isActive, activeClassName, pathname]);\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n if (isExternal) return; // external links behave normally\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1)\n return;\n e.preventDefault();\n if (pathname === href) return;\n progressManager.start();\n router.push(href);\n };\n\n const ariaAttributes = React.useMemo(() => {\n const attributes: {\n \"aria-current\"?: \"page\" | undefined;\n \"aria-label\"?: string;\n } = {\n \"aria-current\": isActive ? \"page\" : undefined,\n };\n\n if (rest[\"aria-label\"]) {\n attributes[\"aria-label\"] = rest[\"aria-label\"];\n }\n\n return attributes;\n }, [isActive, rest]);\n\n return (\n <Link\n href={href}\n className={mergedClassName}\n {...ariaAttributes}\n {...rest}\n onClick={handleClick}\n prefetch={true}\n >\n {children}\n </Link>\n );\n}\n\nNavLink.displayName = \"NavLink\";\n\n// --------------------\n// useNavigate Hook\n// --------------------\n\nexport const useNavigate = () => {\n const router = useRouter();\n const pathname = usePathname();\n\n const navigate = (\n href: string,\n options?: { replace?: boolean; scroll?: boolean }\n ) => {\n if (pathname === href) return;\n progressManager.start();\n const { replace = false, scroll = true } = options || {};\n if (replace) router.replace(href, { scroll });\n else router.push(href, { scroll });\n };\n\n return navigate;\n};\n\n// --------------------\n// NavigationProgress Component\n// --------------------\n","\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { progressManager } from \"./NavLink\"; // export your singleton from your package\n\nexport const NavigationProgressClient: React.FC<{\n color?: string;\n height?: string;\n duration?: number;\n}> = ({ color = \"#2563EB\", height = \"3px\", duration = 200 }) => {\n const [width, setWidth] = useState(\"0%\");\n const [visible, setVisible] = useState(false);\n const pathname = usePathname();\n\n useEffect(() => {\n progressManager.onStart(() => {\n setVisible(true);\n setWidth(\"0%\");\n setTimeout(() => setWidth(\"40%\"), 10);\n });\n\n progressManager.onFinish(() => {\n setWidth(\"100%\");\n setTimeout(() => {\n setVisible(false);\n setWidth(\"0%\");\n }, duration + 50);\n });\n }, [duration]);\n\n useEffect(() => {\n progressManager.finish();\n }, [pathname]);\n\n if (!visible) return null;\n\n return (\n <div\n style={{\n width,\n height,\n backgroundColor: color,\n transition: `width ${duration}ms ease`,\n }}\n className=\"fixed top-0 left-0 z-[99999]\"\n />\n );\n};\n"],"mappings":"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","names":["useEffect","useState","usePathname","jsx","useState","usePathname","useEffect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nuraloom/navlink",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A production-ready NavLink component for Next.js (App Router)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"dct",
|
|
20
|
+
"delta",
|
|
21
|
+
"cube",
|
|
22
|
+
"delta cube",
|
|
23
|
+
"technology",
|
|
24
|
+
"delta cube technology",
|
|
25
|
+
"nextjs",
|
|
26
|
+
"react",
|
|
27
|
+
"navlink",
|
|
28
|
+
"navigation",
|
|
29
|
+
"component",
|
|
30
|
+
"typescript"
|
|
31
|
+
],
|
|
32
|
+
"author": "Your Name",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"next": ">=13",
|
|
36
|
+
"react": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^18.3.26",
|
|
40
|
+
"next": "^16.0.1",
|
|
41
|
+
"tsup": "^8.0.0",
|
|
42
|
+
"typescript": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/smattechnology/navlink.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/smattechnology/navlink/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/smattechnology/navlink#readme",
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/NavLink.tsx
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
// --------------------
|
|
7
|
+
// Types
|
|
8
|
+
// --------------------
|
|
9
|
+
|
|
10
|
+
export interface NavLinkProps
|
|
11
|
+
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
12
|
+
href: string;
|
|
13
|
+
exact?: boolean;
|
|
14
|
+
activeClassName?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --------------------
|
|
20
|
+
// NavProgressManager
|
|
21
|
+
// --------------------
|
|
22
|
+
|
|
23
|
+
type ProgressCallback = () => void;
|
|
24
|
+
|
|
25
|
+
class NavProgressManager {
|
|
26
|
+
private startListeners: ProgressCallback[] = [];
|
|
27
|
+
private finishListeners: ProgressCallback[] = [];
|
|
28
|
+
|
|
29
|
+
start() {
|
|
30
|
+
this.startListeners.forEach((cb) => cb());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
finish() {
|
|
34
|
+
this.finishListeners.forEach((cb) => cb());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onStart(cb: ProgressCallback) {
|
|
38
|
+
this.startListeners.push(cb);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onFinish(cb: ProgressCallback) {
|
|
42
|
+
this.finishListeners.push(cb);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const progressManager = new NavProgressManager();
|
|
47
|
+
|
|
48
|
+
// --------------------
|
|
49
|
+
// Helper constants/functions for NavLink
|
|
50
|
+
// --------------------
|
|
51
|
+
|
|
52
|
+
const EXTERNAL_LINK_REGEX = /^(https?:)?\/\//;
|
|
53
|
+
const DEFAULT_CLASSES =
|
|
54
|
+
"inline-flex items-center gap-2 px-3 py-2 rounded-md transition-colors duration-200 focus:outline-none";
|
|
55
|
+
const DEFAULT_ACTIVE_CLASSES =
|
|
56
|
+
"text-sky-600 font-semibold bg-sky-50 dark:bg-sky-950/20";
|
|
57
|
+
|
|
58
|
+
const normalizePath = (path: string): string => {
|
|
59
|
+
if (!path || path === "/") return "/";
|
|
60
|
+
return path.endsWith("/") ? path.slice(0, -1) : path;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isExternalLink = (href: string): boolean =>
|
|
64
|
+
EXTERNAL_LINK_REGEX.test(href);
|
|
65
|
+
|
|
66
|
+
const getPathnameFromExternalUrl = (url: string): string => {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url).pathname;
|
|
69
|
+
} catch {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// --------------------
|
|
75
|
+
// NavLink Component
|
|
76
|
+
// --------------------
|
|
77
|
+
|
|
78
|
+
export function NavLink({
|
|
79
|
+
href,
|
|
80
|
+
exact = false,
|
|
81
|
+
activeClassName = DEFAULT_ACTIVE_CLASSES,
|
|
82
|
+
className = DEFAULT_CLASSES,
|
|
83
|
+
children,
|
|
84
|
+
...rest
|
|
85
|
+
}: NavLinkProps) {
|
|
86
|
+
const pathname = usePathname();
|
|
87
|
+
const router = useRouter();
|
|
88
|
+
|
|
89
|
+
const isExternal = React.useMemo(() => isExternalLink(href), [href]);
|
|
90
|
+
|
|
91
|
+
const isActive = React.useMemo(() => {
|
|
92
|
+
if (!pathname || isExternal) return false;
|
|
93
|
+
|
|
94
|
+
const currentPath = normalizePath(pathname);
|
|
95
|
+
const targetPath = href.startsWith("/")
|
|
96
|
+
? normalizePath(href)
|
|
97
|
+
: normalizePath(getPathnameFromExternalUrl(href));
|
|
98
|
+
|
|
99
|
+
if (exact) return currentPath === targetPath;
|
|
100
|
+
|
|
101
|
+
if (targetPath === "/") return currentPath === "/";
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
currentPath === targetPath || currentPath.startsWith(targetPath + "/")
|
|
105
|
+
);
|
|
106
|
+
}, [pathname, href, exact, isExternal]);
|
|
107
|
+
|
|
108
|
+
const mergedClassName = React.useMemo(() => {
|
|
109
|
+
const baseClasses = [className];
|
|
110
|
+
if (isActive && pathname !== null) baseClasses.push(activeClassName);
|
|
111
|
+
return baseClasses.filter(Boolean).join(" ").trim();
|
|
112
|
+
}, [className, isActive, activeClassName, pathname]);
|
|
113
|
+
|
|
114
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
115
|
+
if (isExternal) return; // external links behave normally
|
|
116
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1)
|
|
117
|
+
return;
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
if (pathname === href) return;
|
|
120
|
+
progressManager.start();
|
|
121
|
+
router.push(href);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const ariaAttributes = React.useMemo(() => {
|
|
125
|
+
const attributes: {
|
|
126
|
+
"aria-current"?: "page" | undefined;
|
|
127
|
+
"aria-label"?: string;
|
|
128
|
+
} = {
|
|
129
|
+
"aria-current": isActive ? "page" : undefined,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (rest["aria-label"]) {
|
|
133
|
+
attributes["aria-label"] = rest["aria-label"];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return attributes;
|
|
137
|
+
}, [isActive, rest]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Link
|
|
141
|
+
href={href}
|
|
142
|
+
className={mergedClassName}
|
|
143
|
+
{...ariaAttributes}
|
|
144
|
+
{...rest}
|
|
145
|
+
onClick={handleClick}
|
|
146
|
+
prefetch={true}
|
|
147
|
+
>
|
|
148
|
+
{children}
|
|
149
|
+
</Link>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
NavLink.displayName = "NavLink";
|
|
154
|
+
|
|
155
|
+
// --------------------
|
|
156
|
+
// useNavigate Hook
|
|
157
|
+
// --------------------
|
|
158
|
+
|
|
159
|
+
export const useNavigate = () => {
|
|
160
|
+
const router = useRouter();
|
|
161
|
+
const pathname = usePathname();
|
|
162
|
+
|
|
163
|
+
const navigate = (
|
|
164
|
+
href: string,
|
|
165
|
+
options?: { replace?: boolean; scroll?: boolean }
|
|
166
|
+
) => {
|
|
167
|
+
if (pathname === href) return;
|
|
168
|
+
progressManager.start();
|
|
169
|
+
const { replace = false, scroll = true } = options || {};
|
|
170
|
+
if (replace) router.replace(href, { scroll });
|
|
171
|
+
else router.push(href, { scroll });
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return navigate;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// --------------------
|
|
178
|
+
// NavigationProgress Component
|
|
179
|
+
// --------------------
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { progressManager } from "./NavLink"; // export your singleton from your package
|
|
6
|
+
|
|
7
|
+
export const NavigationProgressClient: React.FC<{
|
|
8
|
+
color?: string;
|
|
9
|
+
height?: string;
|
|
10
|
+
duration?: number;
|
|
11
|
+
}> = ({ color = "#2563EB", height = "3px", duration = 200 }) => {
|
|
12
|
+
const [width, setWidth] = useState("0%");
|
|
13
|
+
const [visible, setVisible] = useState(false);
|
|
14
|
+
const pathname = usePathname();
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
progressManager.onStart(() => {
|
|
18
|
+
setVisible(true);
|
|
19
|
+
setWidth("0%");
|
|
20
|
+
setTimeout(() => setWidth("40%"), 10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
progressManager.onFinish(() => {
|
|
24
|
+
setWidth("100%");
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
setVisible(false);
|
|
27
|
+
setWidth("0%");
|
|
28
|
+
}, duration + 50);
|
|
29
|
+
});
|
|
30
|
+
}, [duration]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
progressManager.finish();
|
|
34
|
+
}, [pathname]);
|
|
35
|
+
|
|
36
|
+
if (!visible) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
backgroundColor: color,
|
|
44
|
+
transition: `width ${duration}ms ease`,
|
|
45
|
+
}}
|
|
46
|
+
className="fixed top-0 left-0 z-[99999]"
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
package/src/index.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"jsx": "react-jsx",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationDir": "dist",
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"types": ["react", "next"],
|
|
14
|
+
"alwaysStrict": false
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"], // build from index.ts
|
|
5
|
+
format: ["esm", "cjs"],
|
|
6
|
+
dts: true,
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
clean: true,
|
|
9
|
+
external: ["react", "next", "next/link", "next/navigation"],
|
|
10
|
+
});
|