@page-speed/pressable 0.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.
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+
5
+ function _interopNamespace(e) {
6
+ if (e && e.__esModule) return e;
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
24
+
25
+ // src/hooks/useNavigation.ts
26
+ function normalizePhoneNumber(input) {
27
+ const trimmed = input.trim();
28
+ if (trimmed.toLowerCase().startsWith("tel:")) {
29
+ return trimmed;
30
+ }
31
+ const extensionMatch = trimmed.match(/^(.+?)\s*(x|ext\.?|extension)\s*(\d+)$/i);
32
+ let phoneNumber = trimmed;
33
+ let extension = "";
34
+ if (extensionMatch) {
35
+ phoneNumber = extensionMatch[1];
36
+ extension = extensionMatch[3];
37
+ }
38
+ const hasPlus = phoneNumber.trim().startsWith("+");
39
+ const cleaned = phoneNumber.replace(/[^\d]/g, "");
40
+ let normalized = cleaned;
41
+ if (!hasPlus) {
42
+ if (cleaned.length === 10) {
43
+ normalized = `+1${cleaned}`;
44
+ } else if (cleaned.length >= 11) {
45
+ normalized = `+${cleaned}`;
46
+ }
47
+ }
48
+ const withExtension = extension ? `${normalized};ext=${extension}` : normalized;
49
+ return `tel:${withExtension}`;
50
+ }
51
+ function normalizeEmail(input) {
52
+ const trimmed = input.trim();
53
+ if (trimmed.toLowerCase().startsWith("mailto:")) {
54
+ return trimmed;
55
+ }
56
+ return `mailto:${trimmed}`;
57
+ }
58
+ function isEmail(input) {
59
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
60
+ return emailRegex.test(input.trim());
61
+ }
62
+ function isPhoneNumber(input) {
63
+ const trimmed = input.trim();
64
+ if (trimmed.toLowerCase().startsWith("tel:")) {
65
+ return true;
66
+ }
67
+ const phoneRegex = /^[\s\+\-\(\)]*\d[\d\s\-\(\)\.]*\d[\s\-]*(x|ext\.?|extension)?[\s\-]*\d*$/i;
68
+ return phoneRegex.test(trimmed);
69
+ }
70
+ function isInternalUrl(href) {
71
+ if (typeof window === "undefined") {
72
+ return href.startsWith("/") && !href.startsWith("//");
73
+ }
74
+ const trimmed = href.trim();
75
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
76
+ return true;
77
+ }
78
+ try {
79
+ const url = new URL(trimmed, window.location.href);
80
+ const currentOrigin = window.location.origin;
81
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
82
+ return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ function toRelativePath(href) {
88
+ if (typeof window === "undefined") {
89
+ return href;
90
+ }
91
+ const trimmed = href.trim();
92
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
93
+ return trimmed;
94
+ }
95
+ try {
96
+ const url = new URL(trimmed, window.location.href);
97
+ const currentOrigin = window.location.origin;
98
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
99
+ if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {
100
+ return url.pathname + url.search + url.hash;
101
+ }
102
+ } catch {
103
+ }
104
+ return trimmed;
105
+ }
106
+ function useNavigation({
107
+ href,
108
+ onClick
109
+ } = {}) {
110
+ const linkType = React__namespace.useMemo(() => {
111
+ if (!href || href.trim() === "") {
112
+ return onClick ? "none" : "none";
113
+ }
114
+ const trimmed = href.trim();
115
+ if (trimmed.toLowerCase().startsWith("mailto:") || isEmail(trimmed)) {
116
+ return "mailto";
117
+ }
118
+ if (trimmed.toLowerCase().startsWith("tel:") || isPhoneNumber(trimmed)) {
119
+ return "tel";
120
+ }
121
+ if (isInternalUrl(trimmed)) {
122
+ return "internal";
123
+ }
124
+ try {
125
+ new URL(
126
+ trimmed,
127
+ typeof window !== "undefined" ? window.location.href : "http://localhost"
128
+ );
129
+ return "external";
130
+ } catch {
131
+ return "internal";
132
+ }
133
+ }, [href, onClick]);
134
+ const normalizedHref = React__namespace.useMemo(() => {
135
+ if (!href || href.trim() === "") {
136
+ return void 0;
137
+ }
138
+ const trimmed = href.trim();
139
+ switch (linkType) {
140
+ case "tel":
141
+ return normalizePhoneNumber(trimmed);
142
+ case "mailto":
143
+ return normalizeEmail(trimmed);
144
+ case "internal":
145
+ return toRelativePath(trimmed);
146
+ case "external":
147
+ return trimmed;
148
+ default:
149
+ return trimmed;
150
+ }
151
+ }, [href, linkType]);
152
+ const target = React__namespace.useMemo(() => {
153
+ switch (linkType) {
154
+ case "external":
155
+ return "_blank";
156
+ case "internal":
157
+ return "_self";
158
+ case "mailto":
159
+ case "tel":
160
+ return void 0;
161
+ default:
162
+ return void 0;
163
+ }
164
+ }, [linkType]);
165
+ const rel = React__namespace.useMemo(() => {
166
+ if (linkType === "external") {
167
+ return "noopener noreferrer";
168
+ }
169
+ return void 0;
170
+ }, [linkType]);
171
+ const isExternal = linkType === "external";
172
+ const isInternal = linkType === "internal";
173
+ const shouldUseRouter = isInternal && typeof normalizedHref === "string" && normalizedHref.startsWith("/");
174
+ const handleClick = React__namespace.useCallback(
175
+ (event) => {
176
+ if (onClick) {
177
+ try {
178
+ onClick(event);
179
+ } catch (error) {
180
+ console.error("Error in user onClick handler:", error);
181
+ }
182
+ }
183
+ if (event.defaultPrevented) {
184
+ return;
185
+ }
186
+ if (shouldUseRouter && normalizedHref && event.button === 0 && // left-click only
187
+ !event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey) {
188
+ if (typeof window !== "undefined") {
189
+ const handler = window.__opensiteNavigationHandler;
190
+ if (typeof handler === "function") {
191
+ try {
192
+ const handled = handler(
193
+ normalizedHref,
194
+ event.nativeEvent || event
195
+ );
196
+ if (handled !== false) {
197
+ event.preventDefault();
198
+ }
199
+ } catch (error) {
200
+ console.error("Error in navigation handler:", error);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ },
206
+ [onClick, shouldUseRouter, normalizedHref]
207
+ );
208
+ return {
209
+ linkType,
210
+ normalizedHref,
211
+ target,
212
+ rel,
213
+ isExternal,
214
+ isInternal,
215
+ shouldUseRouter,
216
+ handleClick
217
+ };
218
+ }
219
+
220
+ exports.useNavigation = useNavigation;
221
+ //# sourceMappingURL=index.cjs.map
222
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useNavigation.ts"],"names":["React"],"mappings":"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","file":"index.cjs","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { UseNavigationArgs, UseNavigationReturn, LinkType } from \"../types\";\n\n/**\n * Normalizes phone numbers to tel: format\n * Handles formats like:\n * - \"+14322386131\"\n * - \"(432) 238-6131\"\n * - \"512-232-2212x123\"\n * - \"tel:+14322386131\"\n */\nfunction normalizePhoneNumber(input: string): string {\n const trimmed = input.trim();\n\n // Already has tel: prefix\n if (trimmed.toLowerCase().startsWith(\"tel:\")) {\n return trimmed;\n }\n\n // Check for extension markers (x, ext, extension)\n const extensionMatch = trimmed.match(/^(.+?)\\s*(x|ext\\.?|extension)\\s*(\\d+)$/i);\n let phoneNumber = trimmed;\n let extension = \"\";\n\n if (extensionMatch) {\n phoneNumber = extensionMatch[1];\n extension = extensionMatch[3];\n }\n\n // Clean the phone number (remove everything except digits and leading +)\n const hasPlus = phoneNumber.trim().startsWith(\"+\");\n const cleaned = phoneNumber.replace(/[^\\d]/g, \"\");\n\n // Add country code if needed:\n // - For exactly 10 digits (US/Canada format), prepend +1\n // - For 11+ digits without +, just add +\n let normalized = cleaned;\n if (!hasPlus) {\n if (cleaned.length === 10) {\n normalized = `+1${cleaned}`;\n } else if (cleaned.length >= 11) {\n normalized = `+${cleaned}`;\n }\n }\n\n // Add extension if present\n const withExtension = extension\n ? `${normalized};ext=${extension}`\n : normalized;\n\n return `tel:${withExtension}`;\n}\n\n/**\n * Normalizes email addresses to mailto: format\n */\nfunction normalizeEmail(input: string): string {\n const trimmed = input.trim();\n\n // Already has mailto: prefix\n if (trimmed.toLowerCase().startsWith(\"mailto:\")) {\n return trimmed;\n }\n\n return `mailto:${trimmed}`;\n}\n\n/**\n * Detects if a string is an email address\n */\nfunction isEmail(input: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(input.trim());\n}\n\n/**\n * Detects if a string is a phone number\n */\nfunction isPhoneNumber(input: string): boolean {\n const trimmed = input.trim();\n\n // Already has tel: prefix\n if (trimmed.toLowerCase().startsWith(\"tel:\")) {\n return true;\n }\n\n // Match various phone formats\n const phoneRegex =\n /^[\\s\\+\\-\\(\\)]*\\d[\\d\\s\\-\\(\\)\\.]*\\d[\\s\\-]*(x|ext\\.?|extension)?[\\s\\-]*\\d*$/i;\n return phoneRegex.test(trimmed);\n}\n\n/**\n * Detects if a URL is internal to the current site\n * Handles cases like:\n * - \"/blog-123\"\n * - \"https://jordansite.com/blog-123\"\n * - \"https://www.jordansite.com/blog-123\"\n */\nfunction isInternalUrl(href: string): boolean {\n if (typeof window === \"undefined\") {\n // SSR fallback: assume relative paths are internal\n return href.startsWith(\"/\") && !href.startsWith(\"//\");\n }\n\n const trimmed = href.trim();\n\n // Relative paths are internal\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return true;\n }\n\n // Check if full URL matches current origin\n try {\n const url = new URL(trimmed, window.location.href);\n const currentOrigin = window.location.origin;\n\n // Normalize both origins (remove www. for comparison)\n const normalizeOrigin = (origin: string) =>\n origin.replace(/^(https?:\\/\\/)(www\\.)?/, \"$1\");\n\n return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);\n } catch {\n return false;\n }\n}\n\n/**\n * Converts a full URL to a relative path if it's internal\n */\nfunction toRelativePath(href: string): string {\n if (typeof window === \"undefined\") {\n return href;\n }\n\n const trimmed = href.trim();\n\n // Already relative\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return trimmed;\n }\n\n try {\n const url = new URL(trimmed, window.location.href);\n const currentOrigin = window.location.origin;\n\n // Normalize both origins for comparison\n const normalizeOrigin = (origin: string) =>\n origin.replace(/^(https?:\\/\\/)(www\\.)?/, \"$1\");\n\n if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {\n // Return pathname + search + hash\n return url.pathname + url.search + url.hash;\n }\n } catch {\n // Invalid URL, return as-is\n }\n\n return trimmed;\n}\n\n/**\n * Hook for handling navigation with automatic link type detection,\n * URL normalization, and proper attributes for SEO and accessibility.\n *\n * Features:\n * - Detects link types: internal, external, mailto, tel\n * - Normalizes phone numbers (various formats to tel:)\n * - Normalizes email addresses to mailto:\n * - Converts full URLs matching current origin to relative paths\n * - Determines proper target and rel attributes\n * - Handles React Router-style internal navigation\n *\n * @example\n * ```tsx\n * const nav = useNavigation({ href: \"/about\" });\n * // nav.linkType === \"internal\"\n * // nav.normalizedHref === \"/about\"\n * // nav.target === \"_self\"\n *\n * const nav2 = useNavigation({ href: \"(432) 238-6131\" });\n * // nav2.linkType === \"tel\"\n * // nav2.normalizedHref === \"tel:+14322386131\"\n *\n * const nav3 = useNavigation({ href: \"https://google.com\" });\n * // nav3.linkType === \"external\"\n * // nav3.target === \"_blank\"\n * // nav3.rel === \"noopener noreferrer\"\n * ```\n */\nexport function useNavigation({\n href,\n onClick,\n}: UseNavigationArgs = {}): UseNavigationReturn {\n const linkType = React.useMemo((): LinkType => {\n if (!href || href.trim() === \"\") {\n return onClick ? \"none\" : \"none\";\n }\n\n const trimmed = href.trim();\n\n // Check for mailto\n if (trimmed.toLowerCase().startsWith(\"mailto:\") || isEmail(trimmed)) {\n return \"mailto\";\n }\n\n // Check for tel\n if (trimmed.toLowerCase().startsWith(\"tel:\") || isPhoneNumber(trimmed)) {\n return \"tel\";\n }\n\n // Check for internal vs external\n if (isInternalUrl(trimmed)) {\n return \"internal\";\n }\n\n // Check if it's a valid URL\n try {\n new URL(\n trimmed,\n typeof window !== \"undefined\" ? window.location.href : \"http://localhost\"\n );\n return \"external\";\n } catch {\n // Not a valid URL, treat as internal path\n return \"internal\";\n }\n }, [href, onClick]);\n\n const normalizedHref = React.useMemo((): string | undefined => {\n if (!href || href.trim() === \"\") {\n return undefined;\n }\n\n const trimmed = href.trim();\n\n switch (linkType) {\n case \"tel\":\n return normalizePhoneNumber(trimmed);\n case \"mailto\":\n return normalizeEmail(trimmed);\n case \"internal\":\n return toRelativePath(trimmed);\n case \"external\":\n return trimmed;\n default:\n return trimmed;\n }\n }, [href, linkType]);\n\n const target = React.useMemo((): \"_blank\" | \"_self\" | undefined => {\n switch (linkType) {\n case \"external\":\n return \"_blank\";\n case \"internal\":\n return \"_self\";\n case \"mailto\":\n case \"tel\":\n // Let browser handle default behavior\n return undefined;\n default:\n return undefined;\n }\n }, [linkType]);\n\n const rel = React.useMemo((): string | undefined => {\n if (linkType === \"external\") {\n return \"noopener noreferrer\";\n }\n return undefined;\n }, [linkType]);\n\n const isExternal = linkType === \"external\";\n const isInternal = linkType === \"internal\";\n const shouldUseRouter =\n isInternal &&\n typeof normalizedHref === \"string\" &&\n normalizedHref.startsWith(\"/\");\n\n const handleClick = React.useCallback<React.MouseEventHandler<HTMLElement>>(\n (event) => {\n // Call user's onClick first\n if (onClick) {\n try {\n onClick(event);\n } catch (error) {\n console.error(\"Error in user onClick handler:\", error);\n }\n }\n\n // If event was prevented, don't do anything else\n if (event.defaultPrevented) {\n return;\n }\n\n // Only handle internal navigation for left-clicks without modifiers\n if (\n shouldUseRouter &&\n normalizedHref &&\n event.button === 0 && // left-click only\n !event.metaKey &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.shiftKey\n ) {\n // Check if there's a navigation handler (from opensite-blocks or similar)\n if (typeof window !== \"undefined\") {\n const handler = (window as any).__opensiteNavigationHandler;\n if (typeof handler === \"function\") {\n try {\n const handled = handler(\n normalizedHref,\n event.nativeEvent || event\n );\n if (handled !== false) {\n event.preventDefault();\n }\n } catch (error) {\n console.error(\"Error in navigation handler:\", error);\n }\n }\n }\n }\n },\n [onClick, shouldUseRouter, normalizedHref]\n );\n\n return {\n linkType,\n normalizedHref,\n target,\n rel,\n isExternal,\n isInternal,\n shouldUseRouter,\n handleClick,\n };\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import { U as UseNavigationArgs, b as UseNavigationReturn } from '../index-2mStzo0F.cjs';
2
+ export { a as LinkType } from '../index-2mStzo0F.cjs';
3
+ import 'react';
4
+ import 'class-variance-authority';
5
+ import 'class-variance-authority/types';
6
+
7
+ /**
8
+ * Hook for handling navigation with automatic link type detection,
9
+ * URL normalization, and proper attributes for SEO and accessibility.
10
+ *
11
+ * Features:
12
+ * - Detects link types: internal, external, mailto, tel
13
+ * - Normalizes phone numbers (various formats to tel:)
14
+ * - Normalizes email addresses to mailto:
15
+ * - Converts full URLs matching current origin to relative paths
16
+ * - Determines proper target and rel attributes
17
+ * - Handles React Router-style internal navigation
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const nav = useNavigation({ href: "/about" });
22
+ * // nav.linkType === "internal"
23
+ * // nav.normalizedHref === "/about"
24
+ * // nav.target === "_self"
25
+ *
26
+ * const nav2 = useNavigation({ href: "(432) 238-6131" });
27
+ * // nav2.linkType === "tel"
28
+ * // nav2.normalizedHref === "tel:+14322386131"
29
+ *
30
+ * const nav3 = useNavigation({ href: "https://google.com" });
31
+ * // nav3.linkType === "external"
32
+ * // nav3.target === "_blank"
33
+ * // nav3.rel === "noopener noreferrer"
34
+ * ```
35
+ */
36
+ declare function useNavigation({ href, onClick, }?: UseNavigationArgs): UseNavigationReturn;
37
+
38
+ export { UseNavigationArgs, UseNavigationReturn, useNavigation };
@@ -0,0 +1,38 @@
1
+ import { U as UseNavigationArgs, b as UseNavigationReturn } from '../index-2mStzo0F.js';
2
+ export { a as LinkType } from '../index-2mStzo0F.js';
3
+ import 'react';
4
+ import 'class-variance-authority';
5
+ import 'class-variance-authority/types';
6
+
7
+ /**
8
+ * Hook for handling navigation with automatic link type detection,
9
+ * URL normalization, and proper attributes for SEO and accessibility.
10
+ *
11
+ * Features:
12
+ * - Detects link types: internal, external, mailto, tel
13
+ * - Normalizes phone numbers (various formats to tel:)
14
+ * - Normalizes email addresses to mailto:
15
+ * - Converts full URLs matching current origin to relative paths
16
+ * - Determines proper target and rel attributes
17
+ * - Handles React Router-style internal navigation
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const nav = useNavigation({ href: "/about" });
22
+ * // nav.linkType === "internal"
23
+ * // nav.normalizedHref === "/about"
24
+ * // nav.target === "_self"
25
+ *
26
+ * const nav2 = useNavigation({ href: "(432) 238-6131" });
27
+ * // nav2.linkType === "tel"
28
+ * // nav2.normalizedHref === "tel:+14322386131"
29
+ *
30
+ * const nav3 = useNavigation({ href: "https://google.com" });
31
+ * // nav3.linkType === "external"
32
+ * // nav3.target === "_blank"
33
+ * // nav3.rel === "noopener noreferrer"
34
+ * ```
35
+ */
36
+ declare function useNavigation({ href, onClick, }?: UseNavigationArgs): UseNavigationReturn;
37
+
38
+ export { UseNavigationArgs, UseNavigationReturn, useNavigation };
@@ -0,0 +1,200 @@
1
+ import * as React from 'react';
2
+
3
+ // src/hooks/useNavigation.ts
4
+ function normalizePhoneNumber(input) {
5
+ const trimmed = input.trim();
6
+ if (trimmed.toLowerCase().startsWith("tel:")) {
7
+ return trimmed;
8
+ }
9
+ const extensionMatch = trimmed.match(/^(.+?)\s*(x|ext\.?|extension)\s*(\d+)$/i);
10
+ let phoneNumber = trimmed;
11
+ let extension = "";
12
+ if (extensionMatch) {
13
+ phoneNumber = extensionMatch[1];
14
+ extension = extensionMatch[3];
15
+ }
16
+ const hasPlus = phoneNumber.trim().startsWith("+");
17
+ const cleaned = phoneNumber.replace(/[^\d]/g, "");
18
+ let normalized = cleaned;
19
+ if (!hasPlus) {
20
+ if (cleaned.length === 10) {
21
+ normalized = `+1${cleaned}`;
22
+ } else if (cleaned.length >= 11) {
23
+ normalized = `+${cleaned}`;
24
+ }
25
+ }
26
+ const withExtension = extension ? `${normalized};ext=${extension}` : normalized;
27
+ return `tel:${withExtension}`;
28
+ }
29
+ function normalizeEmail(input) {
30
+ const trimmed = input.trim();
31
+ if (trimmed.toLowerCase().startsWith("mailto:")) {
32
+ return trimmed;
33
+ }
34
+ return `mailto:${trimmed}`;
35
+ }
36
+ function isEmail(input) {
37
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
38
+ return emailRegex.test(input.trim());
39
+ }
40
+ function isPhoneNumber(input) {
41
+ const trimmed = input.trim();
42
+ if (trimmed.toLowerCase().startsWith("tel:")) {
43
+ return true;
44
+ }
45
+ const phoneRegex = /^[\s\+\-\(\)]*\d[\d\s\-\(\)\.]*\d[\s\-]*(x|ext\.?|extension)?[\s\-]*\d*$/i;
46
+ return phoneRegex.test(trimmed);
47
+ }
48
+ function isInternalUrl(href) {
49
+ if (typeof window === "undefined") {
50
+ return href.startsWith("/") && !href.startsWith("//");
51
+ }
52
+ const trimmed = href.trim();
53
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
54
+ return true;
55
+ }
56
+ try {
57
+ const url = new URL(trimmed, window.location.href);
58
+ const currentOrigin = window.location.origin;
59
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
60
+ return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ function toRelativePath(href) {
66
+ if (typeof window === "undefined") {
67
+ return href;
68
+ }
69
+ const trimmed = href.trim();
70
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
71
+ return trimmed;
72
+ }
73
+ try {
74
+ const url = new URL(trimmed, window.location.href);
75
+ const currentOrigin = window.location.origin;
76
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
77
+ if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {
78
+ return url.pathname + url.search + url.hash;
79
+ }
80
+ } catch {
81
+ }
82
+ return trimmed;
83
+ }
84
+ function useNavigation({
85
+ href,
86
+ onClick
87
+ } = {}) {
88
+ const linkType = React.useMemo(() => {
89
+ if (!href || href.trim() === "") {
90
+ return onClick ? "none" : "none";
91
+ }
92
+ const trimmed = href.trim();
93
+ if (trimmed.toLowerCase().startsWith("mailto:") || isEmail(trimmed)) {
94
+ return "mailto";
95
+ }
96
+ if (trimmed.toLowerCase().startsWith("tel:") || isPhoneNumber(trimmed)) {
97
+ return "tel";
98
+ }
99
+ if (isInternalUrl(trimmed)) {
100
+ return "internal";
101
+ }
102
+ try {
103
+ new URL(
104
+ trimmed,
105
+ typeof window !== "undefined" ? window.location.href : "http://localhost"
106
+ );
107
+ return "external";
108
+ } catch {
109
+ return "internal";
110
+ }
111
+ }, [href, onClick]);
112
+ const normalizedHref = React.useMemo(() => {
113
+ if (!href || href.trim() === "") {
114
+ return void 0;
115
+ }
116
+ const trimmed = href.trim();
117
+ switch (linkType) {
118
+ case "tel":
119
+ return normalizePhoneNumber(trimmed);
120
+ case "mailto":
121
+ return normalizeEmail(trimmed);
122
+ case "internal":
123
+ return toRelativePath(trimmed);
124
+ case "external":
125
+ return trimmed;
126
+ default:
127
+ return trimmed;
128
+ }
129
+ }, [href, linkType]);
130
+ const target = React.useMemo(() => {
131
+ switch (linkType) {
132
+ case "external":
133
+ return "_blank";
134
+ case "internal":
135
+ return "_self";
136
+ case "mailto":
137
+ case "tel":
138
+ return void 0;
139
+ default:
140
+ return void 0;
141
+ }
142
+ }, [linkType]);
143
+ const rel = React.useMemo(() => {
144
+ if (linkType === "external") {
145
+ return "noopener noreferrer";
146
+ }
147
+ return void 0;
148
+ }, [linkType]);
149
+ const isExternal = linkType === "external";
150
+ const isInternal = linkType === "internal";
151
+ const shouldUseRouter = isInternal && typeof normalizedHref === "string" && normalizedHref.startsWith("/");
152
+ const handleClick = React.useCallback(
153
+ (event) => {
154
+ if (onClick) {
155
+ try {
156
+ onClick(event);
157
+ } catch (error) {
158
+ console.error("Error in user onClick handler:", error);
159
+ }
160
+ }
161
+ if (event.defaultPrevented) {
162
+ return;
163
+ }
164
+ if (shouldUseRouter && normalizedHref && event.button === 0 && // left-click only
165
+ !event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey) {
166
+ if (typeof window !== "undefined") {
167
+ const handler = window.__opensiteNavigationHandler;
168
+ if (typeof handler === "function") {
169
+ try {
170
+ const handled = handler(
171
+ normalizedHref,
172
+ event.nativeEvent || event
173
+ );
174
+ if (handled !== false) {
175
+ event.preventDefault();
176
+ }
177
+ } catch (error) {
178
+ console.error("Error in navigation handler:", error);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ },
184
+ [onClick, shouldUseRouter, normalizedHref]
185
+ );
186
+ return {
187
+ linkType,
188
+ normalizedHref,
189
+ target,
190
+ rel,
191
+ isExternal,
192
+ isInternal,
193
+ shouldUseRouter,
194
+ handleClick
195
+ };
196
+ }
197
+
198
+ export { useNavigation };
199
+ //# sourceMappingURL=index.js.map
200
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/useNavigation.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport type { UseNavigationArgs, UseNavigationReturn, LinkType } from \"../types\";\n\n/**\n * Normalizes phone numbers to tel: format\n * Handles formats like:\n * - \"+14322386131\"\n * - \"(432) 238-6131\"\n * - \"512-232-2212x123\"\n * - \"tel:+14322386131\"\n */\nfunction normalizePhoneNumber(input: string): string {\n const trimmed = input.trim();\n\n // Already has tel: prefix\n if (trimmed.toLowerCase().startsWith(\"tel:\")) {\n return trimmed;\n }\n\n // Check for extension markers (x, ext, extension)\n const extensionMatch = trimmed.match(/^(.+?)\\s*(x|ext\\.?|extension)\\s*(\\d+)$/i);\n let phoneNumber = trimmed;\n let extension = \"\";\n\n if (extensionMatch) {\n phoneNumber = extensionMatch[1];\n extension = extensionMatch[3];\n }\n\n // Clean the phone number (remove everything except digits and leading +)\n const hasPlus = phoneNumber.trim().startsWith(\"+\");\n const cleaned = phoneNumber.replace(/[^\\d]/g, \"\");\n\n // Add country code if needed:\n // - For exactly 10 digits (US/Canada format), prepend +1\n // - For 11+ digits without +, just add +\n let normalized = cleaned;\n if (!hasPlus) {\n if (cleaned.length === 10) {\n normalized = `+1${cleaned}`;\n } else if (cleaned.length >= 11) {\n normalized = `+${cleaned}`;\n }\n }\n\n // Add extension if present\n const withExtension = extension\n ? `${normalized};ext=${extension}`\n : normalized;\n\n return `tel:${withExtension}`;\n}\n\n/**\n * Normalizes email addresses to mailto: format\n */\nfunction normalizeEmail(input: string): string {\n const trimmed = input.trim();\n\n // Already has mailto: prefix\n if (trimmed.toLowerCase().startsWith(\"mailto:\")) {\n return trimmed;\n }\n\n return `mailto:${trimmed}`;\n}\n\n/**\n * Detects if a string is an email address\n */\nfunction isEmail(input: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(input.trim());\n}\n\n/**\n * Detects if a string is a phone number\n */\nfunction isPhoneNumber(input: string): boolean {\n const trimmed = input.trim();\n\n // Already has tel: prefix\n if (trimmed.toLowerCase().startsWith(\"tel:\")) {\n return true;\n }\n\n // Match various phone formats\n const phoneRegex =\n /^[\\s\\+\\-\\(\\)]*\\d[\\d\\s\\-\\(\\)\\.]*\\d[\\s\\-]*(x|ext\\.?|extension)?[\\s\\-]*\\d*$/i;\n return phoneRegex.test(trimmed);\n}\n\n/**\n * Detects if a URL is internal to the current site\n * Handles cases like:\n * - \"/blog-123\"\n * - \"https://jordansite.com/blog-123\"\n * - \"https://www.jordansite.com/blog-123\"\n */\nfunction isInternalUrl(href: string): boolean {\n if (typeof window === \"undefined\") {\n // SSR fallback: assume relative paths are internal\n return href.startsWith(\"/\") && !href.startsWith(\"//\");\n }\n\n const trimmed = href.trim();\n\n // Relative paths are internal\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return true;\n }\n\n // Check if full URL matches current origin\n try {\n const url = new URL(trimmed, window.location.href);\n const currentOrigin = window.location.origin;\n\n // Normalize both origins (remove www. for comparison)\n const normalizeOrigin = (origin: string) =>\n origin.replace(/^(https?:\\/\\/)(www\\.)?/, \"$1\");\n\n return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);\n } catch {\n return false;\n }\n}\n\n/**\n * Converts a full URL to a relative path if it's internal\n */\nfunction toRelativePath(href: string): string {\n if (typeof window === \"undefined\") {\n return href;\n }\n\n const trimmed = href.trim();\n\n // Already relative\n if (trimmed.startsWith(\"/\") && !trimmed.startsWith(\"//\")) {\n return trimmed;\n }\n\n try {\n const url = new URL(trimmed, window.location.href);\n const currentOrigin = window.location.origin;\n\n // Normalize both origins for comparison\n const normalizeOrigin = (origin: string) =>\n origin.replace(/^(https?:\\/\\/)(www\\.)?/, \"$1\");\n\n if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {\n // Return pathname + search + hash\n return url.pathname + url.search + url.hash;\n }\n } catch {\n // Invalid URL, return as-is\n }\n\n return trimmed;\n}\n\n/**\n * Hook for handling navigation with automatic link type detection,\n * URL normalization, and proper attributes for SEO and accessibility.\n *\n * Features:\n * - Detects link types: internal, external, mailto, tel\n * - Normalizes phone numbers (various formats to tel:)\n * - Normalizes email addresses to mailto:\n * - Converts full URLs matching current origin to relative paths\n * - Determines proper target and rel attributes\n * - Handles React Router-style internal navigation\n *\n * @example\n * ```tsx\n * const nav = useNavigation({ href: \"/about\" });\n * // nav.linkType === \"internal\"\n * // nav.normalizedHref === \"/about\"\n * // nav.target === \"_self\"\n *\n * const nav2 = useNavigation({ href: \"(432) 238-6131\" });\n * // nav2.linkType === \"tel\"\n * // nav2.normalizedHref === \"tel:+14322386131\"\n *\n * const nav3 = useNavigation({ href: \"https://google.com\" });\n * // nav3.linkType === \"external\"\n * // nav3.target === \"_blank\"\n * // nav3.rel === \"noopener noreferrer\"\n * ```\n */\nexport function useNavigation({\n href,\n onClick,\n}: UseNavigationArgs = {}): UseNavigationReturn {\n const linkType = React.useMemo((): LinkType => {\n if (!href || href.trim() === \"\") {\n return onClick ? \"none\" : \"none\";\n }\n\n const trimmed = href.trim();\n\n // Check for mailto\n if (trimmed.toLowerCase().startsWith(\"mailto:\") || isEmail(trimmed)) {\n return \"mailto\";\n }\n\n // Check for tel\n if (trimmed.toLowerCase().startsWith(\"tel:\") || isPhoneNumber(trimmed)) {\n return \"tel\";\n }\n\n // Check for internal vs external\n if (isInternalUrl(trimmed)) {\n return \"internal\";\n }\n\n // Check if it's a valid URL\n try {\n new URL(\n trimmed,\n typeof window !== \"undefined\" ? window.location.href : \"http://localhost\"\n );\n return \"external\";\n } catch {\n // Not a valid URL, treat as internal path\n return \"internal\";\n }\n }, [href, onClick]);\n\n const normalizedHref = React.useMemo((): string | undefined => {\n if (!href || href.trim() === \"\") {\n return undefined;\n }\n\n const trimmed = href.trim();\n\n switch (linkType) {\n case \"tel\":\n return normalizePhoneNumber(trimmed);\n case \"mailto\":\n return normalizeEmail(trimmed);\n case \"internal\":\n return toRelativePath(trimmed);\n case \"external\":\n return trimmed;\n default:\n return trimmed;\n }\n }, [href, linkType]);\n\n const target = React.useMemo((): \"_blank\" | \"_self\" | undefined => {\n switch (linkType) {\n case \"external\":\n return \"_blank\";\n case \"internal\":\n return \"_self\";\n case \"mailto\":\n case \"tel\":\n // Let browser handle default behavior\n return undefined;\n default:\n return undefined;\n }\n }, [linkType]);\n\n const rel = React.useMemo((): string | undefined => {\n if (linkType === \"external\") {\n return \"noopener noreferrer\";\n }\n return undefined;\n }, [linkType]);\n\n const isExternal = linkType === \"external\";\n const isInternal = linkType === \"internal\";\n const shouldUseRouter =\n isInternal &&\n typeof normalizedHref === \"string\" &&\n normalizedHref.startsWith(\"/\");\n\n const handleClick = React.useCallback<React.MouseEventHandler<HTMLElement>>(\n (event) => {\n // Call user's onClick first\n if (onClick) {\n try {\n onClick(event);\n } catch (error) {\n console.error(\"Error in user onClick handler:\", error);\n }\n }\n\n // If event was prevented, don't do anything else\n if (event.defaultPrevented) {\n return;\n }\n\n // Only handle internal navigation for left-clicks without modifiers\n if (\n shouldUseRouter &&\n normalizedHref &&\n event.button === 0 && // left-click only\n !event.metaKey &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.shiftKey\n ) {\n // Check if there's a navigation handler (from opensite-blocks or similar)\n if (typeof window !== \"undefined\") {\n const handler = (window as any).__opensiteNavigationHandler;\n if (typeof handler === \"function\") {\n try {\n const handled = handler(\n normalizedHref,\n event.nativeEvent || event\n );\n if (handled !== false) {\n event.preventDefault();\n }\n } catch (error) {\n console.error(\"Error in navigation handler:\", error);\n }\n }\n }\n }\n },\n [onClick, shouldUseRouter, normalizedHref]\n );\n\n return {\n linkType,\n normalizedHref,\n target,\n rel,\n isExternal,\n isInternal,\n shouldUseRouter,\n handleClick,\n };\n}\n"]}
@@ -0,0 +1,89 @@
1
+ import * as React from 'react';
2
+ import { VariantProps } from 'class-variance-authority';
3
+ import * as class_variance_authority_types from 'class-variance-authority/types';
4
+
5
+ declare const buttonVariants: (props?: ({
6
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
7
+ size?: "default" | "sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg" | null | undefined;
8
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
9
+
10
+ type LinkType = "internal" | "external" | "mailto" | "tel" | "unknown" | "none";
11
+ interface UseNavigationArgs {
12
+ href?: string;
13
+ onClick?: React.MouseEventHandler<HTMLElement>;
14
+ }
15
+ interface UseNavigationReturn {
16
+ linkType: LinkType;
17
+ normalizedHref: string | undefined;
18
+ target: "_blank" | "_self" | undefined;
19
+ rel: string | undefined;
20
+ isExternal: boolean;
21
+ isInternal: boolean;
22
+ shouldUseRouter: boolean;
23
+ handleClick: React.MouseEventHandler<HTMLElement>;
24
+ }
25
+ type FallbackComponentType = "span" | "div" | "button";
26
+ interface PressableBaseProps {
27
+ /**
28
+ * Content inside the Pressable component
29
+ */
30
+ children: React.ReactNode;
31
+ /**
32
+ * Additional CSS classes
33
+ */
34
+ className?: string;
35
+ /**
36
+ * URL to navigate to (can be internal path, external URL, mailto:, tel:, or email/phone string)
37
+ * Examples:
38
+ * - "/about" - internal link
39
+ * - "https://google.com" - external link
40
+ * - "mailto:hello@example.com" or "hello@example.com" - email link
41
+ * - "tel:+14322386131" or "(432) 238-6131" - phone link
42
+ * - "https://mysite.com/blog" - will be converted to "/blog" if on mysite.com
43
+ */
44
+ href?: string;
45
+ /**
46
+ * Click handler
47
+ */
48
+ onClick?: React.MouseEventHandler<HTMLElement>;
49
+ /**
50
+ * The component type to render when there's no href or onClick
51
+ * @default "span"
52
+ */
53
+ fallbackComponentType?: FallbackComponentType;
54
+ /**
55
+ * Explicit component type to render (overrides automatic selection)
56
+ * Note: Internal links will ALWAYS render as <a> tags for SEO, even if componentType="button"
57
+ */
58
+ componentType?: "a" | "button" | FallbackComponentType;
59
+ /**
60
+ * Whether to render as a button styled link (uses ShadCN button styles)
61
+ * When true, will apply button variant classes even when rendering an <a> tag
62
+ * @default false
63
+ */
64
+ asButton?: boolean;
65
+ /**
66
+ * ARIA label for accessibility
67
+ */
68
+ "aria-label"?: string;
69
+ /**
70
+ * ARIA describedby for accessibility
71
+ */
72
+ "aria-describedby"?: string;
73
+ /**
74
+ * ID attribute
75
+ */
76
+ id?: string;
77
+ /**
78
+ * Data attributes
79
+ */
80
+ [key: `data-${string}`]: any;
81
+ }
82
+ interface PressableProps extends PressableBaseProps, VariantProps<typeof buttonVariants> {
83
+ }
84
+ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
85
+ }
86
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
87
+ }
88
+
89
+ export { type ButtonProps as B, type LinkProps as L, type PressableProps as P, type UseNavigationArgs as U, type LinkType as a, type UseNavigationReturn as b, buttonVariants as c };