@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 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
+ ---
@@ -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 };
@@ -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
+ }
@@ -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
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ export { NavLink } from "./NavLink";
3
+ export type { NavLinkProps } from "./NavLink";
4
+
5
+ // index.ts
6
+ export { NavigationProgressClient as NavigationProgress } from "./NavigationProgressClient";
7
+ export { useNavigate } from "./NavLink";
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
+ });