@opensite/ui 0.0.7 → 0.0.9

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.
Files changed (62) hide show
  1. package/README.md +46 -0
  2. package/dist/button-variants-CSFRwdBy.d.cts +14 -0
  3. package/dist/button-variants-CSFRwdBy.d.ts +14 -0
  4. package/dist/button.cjs +1 -1
  5. package/dist/button.cjs.map +1 -1
  6. package/dist/button.d.cts +2 -5
  7. package/dist/button.d.ts +2 -5
  8. package/dist/button.js +1 -1
  9. package/dist/button.js.map +1 -1
  10. package/dist/components.cjs +430 -26
  11. package/dist/components.cjs.map +1 -1
  12. package/dist/components.d.cts +5 -2
  13. package/dist/components.d.ts +5 -2
  14. package/dist/components.js +425 -23
  15. package/dist/components.js.map +1 -1
  16. package/dist/dynamic-icon.cjs +90 -19
  17. package/dist/dynamic-icon.cjs.map +1 -1
  18. package/dist/dynamic-icon.d.cts +12 -6
  19. package/dist/dynamic-icon.d.ts +12 -6
  20. package/dist/dynamic-icon.js +90 -19
  21. package/dist/dynamic-icon.js.map +1 -1
  22. package/dist/feature-showcase.cjs +123 -66
  23. package/dist/feature-showcase.cjs.map +1 -1
  24. package/dist/feature-showcase.js +119 -62
  25. package/dist/feature-showcase.js.map +1 -1
  26. package/dist/hooks.cjs +207 -0
  27. package/dist/hooks.cjs.map +1 -0
  28. package/dist/hooks.d.cts +2 -0
  29. package/dist/hooks.d.ts +2 -0
  30. package/dist/hooks.js +185 -0
  31. package/dist/hooks.js.map +1 -0
  32. package/dist/index.cjs +432 -26
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +6 -2
  35. package/dist/index.d.ts +6 -2
  36. package/dist/index.js +426 -23
  37. package/dist/index.js.map +1 -1
  38. package/dist/media-hover-ctas.cjs +75 -0
  39. package/dist/media-hover-ctas.cjs.map +1 -0
  40. package/dist/media-hover-ctas.d.cts +83 -0
  41. package/dist/media-hover-ctas.d.ts +83 -0
  42. package/dist/media-hover-ctas.js +73 -0
  43. package/dist/media-hover-ctas.js.map +1 -0
  44. package/dist/pressable.cjs +332 -0
  45. package/dist/pressable.cjs.map +1 -0
  46. package/dist/pressable.d.cts +132 -0
  47. package/dist/pressable.d.ts +132 -0
  48. package/dist/pressable.js +310 -0
  49. package/dist/pressable.js.map +1 -0
  50. package/dist/registry.cjs +240 -66
  51. package/dist/registry.cjs.map +1 -1
  52. package/dist/registry.js +237 -63
  53. package/dist/registry.js.map +1 -1
  54. package/dist/types.d.cts +58 -1
  55. package/dist/types.d.ts +58 -1
  56. package/dist/use-navigation.cjs +206 -0
  57. package/dist/use-navigation.cjs.map +1 -0
  58. package/dist/use-navigation.d.cts +49 -0
  59. package/dist/use-navigation.d.ts +49 -0
  60. package/dist/use-navigation.js +184 -0
  61. package/dist/use-navigation.js.map +1 -0
  62. package/package.json +22 -1
package/dist/hooks.js ADDED
@@ -0,0 +1,185 @@
1
+ import * as React from 'react';
2
+
3
+ // lib/useNavigation.ts
4
+ function normalizePhoneNumber(input) {
5
+ const trimmed = input.trim();
6
+ if (trimmed.toLowerCase().startsWith("tel:")) {
7
+ return trimmed;
8
+ }
9
+ const match = trimmed.match(/^[\s\+\-\(\)]*(\d[\d\s\-\(\)\.]*\d)[\s\-]*(x|ext\.?|extension)?[\s\-]*(\d+)?$/i);
10
+ if (match) {
11
+ const mainNumber = match[1].replace(/[\s\-\(\)\.]/g, "");
12
+ const extension = match[3];
13
+ const normalized = mainNumber.length >= 10 && !trimmed.startsWith("+") ? `+${mainNumber}` : mainNumber;
14
+ const withExtension = extension ? `${normalized};ext=${extension}` : normalized;
15
+ return `tel:${withExtension}`;
16
+ }
17
+ const cleaned = trimmed.replace(/[\s\-\(\)\.]/g, "");
18
+ return `tel:${cleaned}`;
19
+ }
20
+ function normalizeEmail(input) {
21
+ const trimmed = input.trim();
22
+ if (trimmed.toLowerCase().startsWith("mailto:")) {
23
+ return trimmed;
24
+ }
25
+ return `mailto:${trimmed}`;
26
+ }
27
+ function isEmail(input) {
28
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
29
+ return emailRegex.test(input.trim());
30
+ }
31
+ function isPhoneNumber(input) {
32
+ const trimmed = input.trim();
33
+ if (trimmed.toLowerCase().startsWith("tel:")) {
34
+ return true;
35
+ }
36
+ const phoneRegex = /^[\s\+\-\(\)]*\d[\d\s\-\(\)\.]*\d[\s\-]*(x|ext\.?|extension)?[\s\-]*\d*$/i;
37
+ return phoneRegex.test(trimmed);
38
+ }
39
+ function isInternalUrl(href) {
40
+ if (typeof window === "undefined") {
41
+ return href.startsWith("/") && !href.startsWith("//");
42
+ }
43
+ const trimmed = href.trim();
44
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
45
+ return true;
46
+ }
47
+ try {
48
+ const url = new URL(trimmed, window.location.href);
49
+ const currentOrigin = window.location.origin;
50
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
51
+ return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ function toRelativePath(href) {
57
+ if (typeof window === "undefined") {
58
+ return href;
59
+ }
60
+ const trimmed = href.trim();
61
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
62
+ return trimmed;
63
+ }
64
+ try {
65
+ const url = new URL(trimmed, window.location.href);
66
+ const currentOrigin = window.location.origin;
67
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
68
+ if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {
69
+ return url.pathname + url.search + url.hash;
70
+ }
71
+ } catch {
72
+ }
73
+ return trimmed;
74
+ }
75
+ function useNavigation({
76
+ href,
77
+ onClick
78
+ } = {}) {
79
+ const linkType = React.useMemo(() => {
80
+ if (!href || href.trim() === "") {
81
+ return onClick ? "none" : "none";
82
+ }
83
+ const trimmed = href.trim();
84
+ if (trimmed.toLowerCase().startsWith("mailto:") || isEmail(trimmed)) {
85
+ return "mailto";
86
+ }
87
+ if (trimmed.toLowerCase().startsWith("tel:") || isPhoneNumber(trimmed)) {
88
+ return "tel";
89
+ }
90
+ if (isInternalUrl(trimmed)) {
91
+ return "internal";
92
+ }
93
+ try {
94
+ new URL(trimmed, typeof window !== "undefined" ? window.location.href : "http://localhost");
95
+ return "external";
96
+ } catch {
97
+ return "internal";
98
+ }
99
+ }, [href, onClick]);
100
+ const normalizedHref = React.useMemo(() => {
101
+ if (!href || href.trim() === "") {
102
+ return void 0;
103
+ }
104
+ const trimmed = href.trim();
105
+ switch (linkType) {
106
+ case "tel":
107
+ return normalizePhoneNumber(trimmed);
108
+ case "mailto":
109
+ return normalizeEmail(trimmed);
110
+ case "internal":
111
+ return toRelativePath(trimmed);
112
+ case "external":
113
+ return trimmed;
114
+ default:
115
+ return trimmed;
116
+ }
117
+ }, [href, linkType]);
118
+ const target = React.useMemo(() => {
119
+ switch (linkType) {
120
+ case "external":
121
+ return "_blank";
122
+ case "internal":
123
+ return "_self";
124
+ case "mailto":
125
+ case "tel":
126
+ return void 0;
127
+ default:
128
+ return void 0;
129
+ }
130
+ }, [linkType]);
131
+ const rel = React.useMemo(() => {
132
+ if (linkType === "external") {
133
+ return "noopener noreferrer";
134
+ }
135
+ return void 0;
136
+ }, [linkType]);
137
+ const isExternal = linkType === "external";
138
+ const isInternal = linkType === "internal";
139
+ const shouldUseRouter = isInternal && typeof normalizedHref === "string" && normalizedHref.startsWith("/");
140
+ const handleClick = React.useCallback(
141
+ (event) => {
142
+ if (onClick) {
143
+ try {
144
+ onClick(event);
145
+ } catch (error) {
146
+ console.error("Error in user onClick handler:", error);
147
+ }
148
+ }
149
+ if (event.defaultPrevented) {
150
+ return;
151
+ }
152
+ if (shouldUseRouter && normalizedHref && event.button === 0 && // left-click only
153
+ !event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey) {
154
+ if (typeof window !== "undefined") {
155
+ const handler = window.__opensiteNavigationHandler;
156
+ if (typeof handler === "function") {
157
+ try {
158
+ const handled = handler(normalizedHref, event.nativeEvent || event);
159
+ if (handled !== false) {
160
+ event.preventDefault();
161
+ }
162
+ } catch (error) {
163
+ console.error("Error in navigation handler:", error);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ },
169
+ [onClick, shouldUseRouter, normalizedHref]
170
+ );
171
+ return {
172
+ linkType,
173
+ normalizedHref,
174
+ target,
175
+ rel,
176
+ isExternal,
177
+ isInternal,
178
+ shouldUseRouter,
179
+ handleClick
180
+ };
181
+ }
182
+
183
+ export { useNavigation };
184
+ //# sourceMappingURL=hooks.js.map
185
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../lib/useNavigation.ts"],"names":[],"mappings":";;;AA8BA,SAAS,qBAAqB,KAAA,EAAuB;AACnD,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAG3B,EAAA,IAAI,OAAA,CAAQ,WAAA,EAAY,CAAE,UAAA,CAAW,MAAM,CAAA,EAAG;AAC5C,IAAA,OAAO,OAAA;AAAA,EACT;AAGA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,gFAAgF,CAAA;AAE5G,EAAA,IAAI,KAAA,EAAO;AAET,IAAA,MAAM,aAAa,KAAA,CAAM,CAAC,CAAA,CAAE,OAAA,CAAQ,iBAAiB,EAAE,CAAA;AACvD,IAAA,MAAM,SAAA,GAAY,MAAM,CAAC,CAAA;AAGzB,IAAA,MAAM,UAAA,GAAa,UAAA,CAAW,MAAA,IAAU,EAAA,IAAM,CAAC,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,GACjE,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA,GACd,UAAA;AAGJ,IAAA,MAAM,gBAAgB,SAAA,GAAY,CAAA,EAAG,UAAU,CAAA,KAAA,EAAQ,SAAS,CAAA,CAAA,GAAK,UAAA;AAErE,IAAA,OAAO,OAAO,aAAa,CAAA,CAAA;AAAA,EAC7B;AAGA,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,eAAA,EAAiB,EAAE,CAAA;AACnD,EAAA,OAAO,OAAO,OAAO,CAAA,CAAA;AACvB;AAKA,SAAS,eAAe,KAAA,EAAuB;AAC7C,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAG3B,EAAA,IAAI,OAAA,CAAQ,WAAA,EAAY,CAAE,UAAA,CAAW,SAAS,CAAA,EAAG;AAC/C,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,OAAO,UAAU,OAAO,CAAA,CAAA;AAC1B;AAKA,SAAS,QAAQ,KAAA,EAAwB;AACvC,EAAA,MAAM,UAAA,GAAa,4BAAA;AACnB,EAAA,OAAO,UAAA,CAAW,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAA;AACrC;AAKA,SAAS,cAAc,KAAA,EAAwB;AAC7C,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAG3B,EAAA,IAAI,OAAA,CAAQ,WAAA,EAAY,CAAE,UAAA,CAAW,MAAM,CAAA,EAAG;AAC5C,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,MAAM,UAAA,GAAa,2EAAA;AACnB,EAAA,OAAO,UAAA,CAAW,KAAK,OAAO,CAAA;AAChC;AASA,SAAS,cAAc,IAAA,EAAuB;AAC5C,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEjC,IAAA,OAAO,KAAK,UAAA,CAAW,GAAG,KAAK,CAAC,IAAA,CAAK,WAAW,IAAI,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAG1B,EAAA,IAAI,OAAA,CAAQ,WAAW,GAAG,CAAA,IAAK,CAAC,OAAA,CAAQ,UAAA,CAAW,IAAI,CAAA,EAAG;AACxD,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,IAAI;AACF,IAAA,MAAM,MAAM,IAAI,GAAA,CAAI,OAAA,EAAS,MAAA,CAAO,SAAS,IAAI,CAAA;AACjD,IAAA,MAAM,aAAA,GAAgB,OAAO,QAAA,CAAS,MAAA;AAGtC,IAAA,MAAM,kBAAkB,CAAC,MAAA,KACvB,MAAA,CAAO,OAAA,CAAQ,0BAA0B,IAAI,CAAA;AAE/C,IAAA,OAAO,eAAA,CAAgB,GAAA,CAAI,MAAM,CAAA,KAAM,gBAAgB,aAAa,CAAA;AAAA,EACtE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAKA,SAAS,eAAe,IAAA,EAAsB;AAC5C,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAG1B,EAAA,IAAI,OAAA,CAAQ,WAAW,GAAG,CAAA,IAAK,CAAC,OAAA,CAAQ,UAAA,CAAW,IAAI,CAAA,EAAG;AACxD,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,MAAM,IAAI,GAAA,CAAI,OAAA,EAAS,MAAA,CAAO,SAAS,IAAI,CAAA;AACjD,IAAA,MAAM,aAAA,GAAgB,OAAO,QAAA,CAAS,MAAA;AAGtC,IAAA,MAAM,kBAAkB,CAAC,MAAA,KACvB,MAAA,CAAO,OAAA,CAAQ,0BAA0B,IAAI,CAAA;AAE/C,IAAA,IAAI,gBAAgB,GAAA,CAAI,MAAM,CAAA,KAAM,eAAA,CAAgB,aAAa,CAAA,EAAG;AAElE,MAAA,OAAO,GAAA,CAAI,QAAA,GAAW,GAAA,CAAI,MAAA,GAAS,GAAA,CAAI,IAAA;AAAA,IACzC;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAER;AAEA,EAAA,OAAO,OAAA;AACT;AA+BO,SAAS,aAAA,CAAc;AAAA,EAC5B,IAAA;AAAA,EACA;AACF,CAAA,GAAuB,EAAC,EAAwB;AAC9C,EAAA,MAAM,QAAA,GAAiB,cAAQ,MAAgB;AAC7C,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,IAAA,OAAW,EAAA,EAAI;AAC/B,MAAA,OAAO,UAAU,MAAA,GAAS,MAAA;AAAA,IAC5B;AAEA,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAG1B,IAAA,IAAI,OAAA,CAAQ,aAAY,CAAE,UAAA,CAAW,SAAS,CAAA,IAAK,OAAA,CAAQ,OAAO,CAAA,EAAG;AACnE,MAAA,OAAO,QAAA;AAAA,IACT;AAGA,IAAA,IAAI,OAAA,CAAQ,aAAY,CAAE,UAAA,CAAW,MAAM,CAAA,IAAK,aAAA,CAAc,OAAO,CAAA,EAAG;AACtE,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,aAAA,CAAc,OAAO,CAAA,EAAG;AAC1B,MAAA,OAAO,UAAA;AAAA,IACT;AAGA,IAAA,IAAI;AACF,MAAA,IAAI,GAAA,CAAI,SAAS,OAAO,MAAA,KAAW,cAAc,MAAA,CAAO,QAAA,CAAS,OAAO,kBAAkB,CAAA;AAC1F,MAAA,OAAO,UAAA;AAAA,IACT,CAAA,CAAA,MAAQ;AAEN,MAAA,OAAO,UAAA;AAAA,IACT;AAAA,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,OAAO,CAAC,CAAA;AAElB,EAAA,MAAM,cAAA,GAAuB,cAAQ,MAA0B;AAC7D,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,IAAA,OAAW,EAAA,EAAI;AAC/B,MAAA,OAAO,MAAA;AAAA,IACT;AAEA,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAE1B,IAAA,QAAQ,QAAA;AAAU,MAChB,KAAK,KAAA;AACH,QAAA,OAAO,qBAAqB,OAAO,CAAA;AAAA,MACrC,KAAK,QAAA;AACH,QAAA,OAAO,eAAe,OAAO,CAAA;AAAA,MAC/B,KAAK,UAAA;AACH,QAAA,OAAO,eAAe,OAAO,CAAA;AAAA,MAC/B,KAAK,UAAA;AACH,QAAA,OAAO,OAAA;AAAA,MACT;AACE,QAAA,OAAO,OAAA;AAAA;AACX,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,QAAQ,CAAC,CAAA;AAEnB,EAAA,MAAM,MAAA,GAAe,cAAQ,MAAsC;AACjE,IAAA,QAAQ,QAAA;AAAU,MAChB,KAAK,UAAA;AACH,QAAA,OAAO,QAAA;AAAA,MACT,KAAK,UAAA;AACH,QAAA,OAAO,OAAA;AAAA,MACT,KAAK,QAAA;AAAA,MACL,KAAK,KAAA;AAEH,QAAA,OAAO,MAAA;AAAA,MACT;AACE,QAAA,OAAO,MAAA;AAAA;AACX,EACF,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,MAAM,GAAA,GAAY,cAAQ,MAA0B;AAClD,IAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,MAAA,OAAO,qBAAA;AAAA,IACT;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,MAAM,aAAa,QAAA,KAAa,UAAA;AAChC,EAAA,MAAM,aAAa,QAAA,KAAa,UAAA;AAChC,EAAA,MAAM,kBAAkB,UAAA,IAAc,OAAO,mBAAmB,QAAA,IAAY,cAAA,CAAe,WAAW,GAAG,CAAA;AAEzG,EAAA,MAAM,WAAA,GAAoB,KAAA,CAAA,WAAA;AAAA,IACxB,CAAC,KAAA,KAAU;AAET,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,QACf,SAAS,KAAA,EAAO;AACd,UAAA,OAAA,CAAQ,KAAA,CAAM,kCAAkC,KAAK,CAAA;AAAA,QACvD;AAAA,MACF;AAGA,MAAA,IAAI,MAAM,gBAAA,EAAkB;AAC1B,QAAA;AAAA,MACF;AAGA,MAAA,IACE,eAAA,IACA,cAAA,IACA,KAAA,CAAM,MAAA,KAAW,CAAA;AAAA,MACjB,CAAC,KAAA,CAAM,OAAA,IACP,CAAC,KAAA,CAAM,MAAA,IACP,CAAC,KAAA,CAAM,OAAA,IACP,CAAC,KAAA,CAAM,QAAA,EACP;AAEA,QAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,UAAA,MAAM,UAAW,MAAA,CAAe,2BAAA;AAChC,UAAA,IAAI,OAAO,YAAY,UAAA,EAAY;AACjC,YAAA,IAAI;AACF,cAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,cAAA,EAAgB,KAAA,CAAM,eAAe,KAAK,CAAA;AAClE,cAAA,IAAI,YAAY,KAAA,EAAO;AACrB,gBAAA,KAAA,CAAM,cAAA,EAAe;AAAA,cACvB;AAAA,YACF,SAAS,KAAA,EAAO;AACd,cAAA,OAAA,CAAQ,KAAA,CAAM,gCAAgC,KAAK,CAAA;AAAA,YACrD;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,CAAC,OAAA,EAAS,eAAA,EAAiB,cAAc;AAAA,GAC3C;AAEA,EAAA,OAAO;AAAA,IACL,QAAA;AAAA,IACA,cAAA;AAAA,IACA,MAAA;AAAA,IACA,GAAA;AAAA,IACA,UAAA;AAAA,IACA,UAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,GACF;AACF","file":"hooks.js","sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport type LinkType = \"internal\" | \"external\" | \"mailto\" | \"tel\" | \"unknown\" | \"none\";\n\nexport interface UseNavigationArgs {\n href?: string;\n onClick?: React.MouseEventHandler<HTMLElement>;\n}\n\nexport interface UseNavigationReturn {\n linkType: LinkType;\n normalizedHref: string | undefined;\n target: \"_blank\" | \"_self\" | undefined;\n rel: string | undefined;\n isExternal: boolean;\n isInternal: boolean;\n shouldUseRouter: boolean;\n handleClick: React.MouseEventHandler<HTMLElement>;\n}\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 // Extract digits and extension\n const match = trimmed.match(/^[\\s\\+\\-\\(\\)]*(\\d[\\d\\s\\-\\(\\)\\.]*\\d)[\\s\\-]*(x|ext\\.?|extension)?[\\s\\-]*(\\d+)?$/i);\n\n if (match) {\n // Clean the main number (remove spaces, dashes, dots, parentheses)\n const mainNumber = match[1].replace(/[\\s\\-\\(\\)\\.]/g, \"\");\n const extension = match[3];\n\n // Add + prefix if not already there and number looks international (10+ digits)\n const normalized = mainNumber.length >= 10 && !trimmed.startsWith(\"+\")\n ? `+${mainNumber}`\n : mainNumber;\n\n // Add extension if present\n const withExtension = extension ? `${normalized};ext=${extension}` : normalized;\n\n return `tel:${withExtension}`;\n }\n\n // Fallback: just clean and prefix\n const cleaned = trimmed.replace(/[\\s\\-\\(\\)\\.]/g, \"\");\n return `tel:${cleaned}`;\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 = /^[\\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(trimmed, typeof window !== \"undefined\" ? window.location.href : \"http://localhost\");\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 = isInternal && typeof normalizedHref === \"string\" && 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(normalizedHref, event.nativeEvent || event);\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"]}