@salesmind-ai/design-system 0.3.0 → 0.3.2
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 +32 -2
- package/dist/admin/index.cjs +68 -2928
- package/dist/admin/index.cjs.map +1 -1
- package/dist/admin/index.js +5 -2915
- package/dist/admin/index.js.map +1 -1
- package/dist/blog/index.cjs +53 -1064
- package/dist/blog/index.cjs.map +1 -1
- package/dist/blog/index.js +8 -1054
- package/dist/blog/index.js.map +1 -1
- package/dist/charts/index.cjs +46 -2694
- package/dist/charts/index.cjs.map +1 -1
- package/dist/charts/index.js +3 -2680
- package/dist/charts/index.js.map +1 -1
- package/dist/chunk-2GARWEJK.js +17 -0
- package/dist/chunk-2GARWEJK.js.map +1 -0
- package/dist/chunk-3NKRFUAR.js +37 -0
- package/dist/chunk-3NKRFUAR.js.map +1 -0
- package/dist/chunk-3TGSIILM.cjs +201 -0
- package/dist/chunk-3TGSIILM.cjs.map +1 -0
- package/dist/chunk-4GM5BGBN.cjs +801 -0
- package/dist/chunk-4GM5BGBN.cjs.map +1 -0
- package/dist/chunk-5LGDEZWY.cjs +2434 -0
- package/dist/chunk-5LGDEZWY.cjs.map +1 -0
- package/dist/chunk-6H4DSTXR.js +786 -0
- package/dist/chunk-6H4DSTXR.js.map +1 -0
- package/dist/chunk-6UNG76Y2.js +153 -0
- package/dist/chunk-6UNG76Y2.js.map +1 -0
- package/dist/chunk-7PX2AZ6Y.js +39 -0
- package/dist/chunk-7PX2AZ6Y.js.map +1 -0
- package/dist/chunk-B6AVAX4F.js +1415 -0
- package/dist/chunk-B6AVAX4F.js.map +1 -0
- package/dist/chunk-BILT5KD3.js +264 -0
- package/dist/chunk-BILT5KD3.js.map +1 -0
- package/dist/chunk-C2BCDNAV.js +24 -0
- package/dist/chunk-C2BCDNAV.js.map +1 -0
- package/dist/chunk-CH42VPWE.cjs +421 -0
- package/dist/chunk-CH42VPWE.cjs.map +1 -0
- package/dist/chunk-CJ2MKVAF.cjs +46 -0
- package/dist/chunk-CJ2MKVAF.cjs.map +1 -0
- package/dist/chunk-DP74LUXG.cjs +98 -0
- package/dist/chunk-DP74LUXG.cjs.map +1 -0
- package/dist/chunk-E7D6EKJ4.cjs +44 -0
- package/dist/chunk-E7D6EKJ4.cjs.map +1 -0
- package/dist/chunk-ECXBTUH6.cjs +584 -0
- package/dist/chunk-ECXBTUH6.cjs.map +1 -0
- package/dist/chunk-EFRAP5ES.js +157 -0
- package/dist/chunk-EFRAP5ES.js.map +1 -0
- package/dist/chunk-F6YYWMME.js +485 -0
- package/dist/chunk-F6YYWMME.js.map +1 -0
- package/dist/chunk-FAFAP4L5.js +183 -0
- package/dist/chunk-FAFAP4L5.js.map +1 -0
- package/dist/chunk-GUZIMHWS.js +1608 -0
- package/dist/chunk-GUZIMHWS.js.map +1 -0
- package/dist/chunk-H2Y6BSTL.cjs +69 -0
- package/dist/chunk-H2Y6BSTL.cjs.map +1 -0
- package/dist/chunk-HN4PHABT.js +126 -0
- package/dist/chunk-HN4PHABT.js.map +1 -0
- package/dist/chunk-HRENHNDJ.js +211 -0
- package/dist/chunk-HRENHNDJ.js.map +1 -0
- package/dist/chunk-I75BFEYT.cjs +2561 -0
- package/dist/chunk-I75BFEYT.cjs.map +1 -0
- package/dist/chunk-IFRATNLU.js +562 -0
- package/dist/chunk-IFRATNLU.js.map +1 -0
- package/dist/chunk-IYPXJ6YC.cjs +69 -0
- package/dist/chunk-IYPXJ6YC.cjs.map +1 -0
- package/dist/chunk-JPJN4YBC.js +409 -0
- package/dist/chunk-JPJN4YBC.js.map +1 -0
- package/dist/chunk-KBA2LFBG.js +62 -0
- package/dist/chunk-KBA2LFBG.js.map +1 -0
- package/dist/chunk-KCKUSU2M.cjs +166 -0
- package/dist/chunk-KCKUSU2M.cjs.map +1 -0
- package/dist/chunk-KJ2OXQF4.js +287 -0
- package/dist/chunk-KJ2OXQF4.js.map +1 -0
- package/dist/chunk-KNQEIU7O.cjs +1202 -0
- package/dist/chunk-KNQEIU7O.cjs.map +1 -0
- package/dist/chunk-KVGSVGRK.cjs +569 -0
- package/dist/chunk-KVGSVGRK.cjs.map +1 -0
- package/dist/chunk-L352JRV6.cjs +105 -0
- package/dist/chunk-L352JRV6.cjs.map +1 -0
- package/dist/chunk-LJADZITX.cjs +298 -0
- package/dist/chunk-LJADZITX.cjs.map +1 -0
- package/dist/chunk-LMJPWXTZ.cjs +194 -0
- package/dist/chunk-LMJPWXTZ.cjs.map +1 -0
- package/dist/chunk-LOWEAQST.js +701 -0
- package/dist/chunk-LOWEAQST.js.map +1 -0
- package/dist/chunk-MDB2WCRQ.cjs +137 -0
- package/dist/chunk-MDB2WCRQ.cjs.map +1 -0
- package/dist/chunk-MQDEE7HC.cjs +283 -0
- package/dist/chunk-MQDEE7HC.cjs.map +1 -0
- package/dist/chunk-MQRB634A.cjs +34 -0
- package/dist/chunk-MQRB634A.cjs.map +1 -0
- package/dist/chunk-MTI27RDV.js +185 -0
- package/dist/chunk-MTI27RDV.js.map +1 -0
- package/dist/chunk-MU6GW5ZV.js +2317 -0
- package/dist/chunk-MU6GW5ZV.js.map +1 -0
- package/dist/chunk-NN3TUHIH.js +28 -0
- package/dist/chunk-NN3TUHIH.js.map +1 -0
- package/dist/chunk-NT4LBP7D.cjs +111 -0
- package/dist/chunk-NT4LBP7D.cjs.map +1 -0
- package/dist/chunk-OLV7OD3X.cjs +502 -0
- package/dist/chunk-OLV7OD3X.cjs.map +1 -0
- package/dist/chunk-OXNXEQY7.js +2538 -0
- package/dist/chunk-OXNXEQY7.js.map +1 -0
- package/dist/chunk-P5BOFE5A.js +546 -0
- package/dist/chunk-P5BOFE5A.js.map +1 -0
- package/dist/chunk-Q2MFGYTE.cjs +1449 -0
- package/dist/chunk-Q2MFGYTE.cjs.map +1 -0
- package/dist/chunk-Q75DBVDY.cjs +68 -0
- package/dist/chunk-Q75DBVDY.cjs.map +1 -0
- package/dist/chunk-REQ5Q6ZI.js +1022 -0
- package/dist/chunk-REQ5Q6ZI.js.map +1 -0
- package/dist/chunk-SICKWUWB.js +62 -0
- package/dist/chunk-SICKWUWB.js.map +1 -0
- package/dist/chunk-T343CCH5.js +1190 -0
- package/dist/chunk-T343CCH5.js.map +1 -0
- package/dist/chunk-TEC62D4A.cjs +1624 -0
- package/dist/chunk-TEC62D4A.cjs.map +1 -0
- package/dist/chunk-TW5JB35D.js +2122 -0
- package/dist/chunk-TW5JB35D.js.map +1 -0
- package/dist/chunk-VC5LMUVQ.cjs +20 -0
- package/dist/chunk-VC5LMUVQ.cjs.map +1 -0
- package/dist/chunk-VM7WFMKI.cjs +76 -0
- package/dist/chunk-VM7WFMKI.cjs.map +1 -0
- package/dist/chunk-W2WTP6HS.cjs +233 -0
- package/dist/chunk-W2WTP6HS.cjs.map +1 -0
- package/dist/chunk-WH7PYHZY.cjs +35 -0
- package/dist/chunk-WH7PYHZY.cjs.map +1 -0
- package/dist/chunk-XQZVY7JJ.cjs +717 -0
- package/dist/chunk-XQZVY7JJ.cjs.map +1 -0
- package/dist/chunk-XU3OMQ7V.js +98 -0
- package/dist/chunk-XU3OMQ7V.js.map +1 -0
- package/dist/chunk-XWPDRMZG.js +62 -0
- package/dist/chunk-XWPDRMZG.js.map +1 -0
- package/dist/chunk-Y3CPKNB7.js +67 -0
- package/dist/chunk-Y3CPKNB7.js.map +1 -0
- package/dist/chunk-YNVRDD2P.js +98 -0
- package/dist/chunk-YNVRDD2P.js.map +1 -0
- package/dist/chunk-YSYR54XR.js +92 -0
- package/dist/chunk-YSYR54XR.js.map +1 -0
- package/dist/chunk-YTYDQBVY.cjs +162 -0
- package/dist/chunk-YTYDQBVY.cjs.map +1 -0
- package/dist/chunk-ZDLOA2UT.cjs +1042 -0
- package/dist/chunk-ZDLOA2UT.cjs.map +1 -0
- package/dist/chunk-ZWUKRCOJ.cjs +2162 -0
- package/dist/chunk-ZWUKRCOJ.cjs.map +1 -0
- package/dist/core/index.cjs +807 -4333
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +14 -4130
- package/dist/core/index.js.map +1 -1
- package/dist/i18n/index.cjs +86 -558
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.js +1 -544
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.cjs +1432 -17139
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +11 -7
- package/dist/index.css.map +1 -1
- package/dist/index.js +31 -16784
- package/dist/index.js.map +1 -1
- package/dist/marketing/index.cjs +142 -3072
- package/dist/marketing/index.cjs.map +1 -1
- package/dist/marketing/index.js +11 -3042
- package/dist/marketing/index.js.map +1 -1
- package/dist/motion/index.cjs +26 -1222
- package/dist/motion/index.cjs.map +1 -1
- package/dist/motion/index.js +2 -1215
- package/dist/motion/index.js.map +1 -1
- package/dist/nav/index.cjs +101 -1518
- package/dist/nav/index.cjs.map +1 -1
- package/dist/nav/index.js +4 -1498
- package/dist/nav/index.js.map +1 -1
- package/dist/report/index.cjs +171 -2403
- package/dist/report/index.cjs.map +1 -1
- package/dist/report/index.js +3 -2363
- package/dist/report/index.js.map +1 -1
- package/dist/sections/index.cjs +28 -378
- package/dist/sections/index.cjs.map +1 -1
- package/dist/sections/index.css +1 -4
- package/dist/sections/index.css.map +1 -1
- package/dist/sections/index.js +4 -372
- package/dist/sections/index.js.map +1 -1
- package/dist/social-proof/index.cjs +53 -1249
- package/dist/social-proof/index.cjs.map +1 -1
- package/dist/social-proof/index.css +10 -3
- package/dist/social-proof/index.css.map +1 -1
- package/dist/social-proof/index.js +6 -1234
- package/dist/social-proof/index.js.map +1 -1
- package/dist/theme/index.cjs +38 -565
- package/dist/theme/index.cjs.map +1 -1
- package/dist/theme/index.js +2 -555
- package/dist/theme/index.js.map +1 -1
- package/dist/web/client/index.cjs +48 -0
- package/dist/web/client/index.cjs.map +1 -0
- package/dist/web/client/index.css +456 -0
- package/dist/web/client/index.css.map +1 -0
- package/dist/web/client/index.d.cts +172 -0
- package/dist/web/client/index.d.ts +172 -0
- package/dist/web/client/index.js +7 -0
- package/dist/web/client/index.js.map +1 -0
- package/dist/web/index.cjs +158 -1346
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +5 -893
- package/dist/web/index.d.ts +5 -893
- package/dist/web/index.js +9 -1305
- package/dist/web/index.js.map +1 -1
- package/dist/web/server/index.cjs +32 -0
- package/dist/web/server/index.cjs.map +1 -0
- package/dist/web/server/index.d.cts +725 -0
- package/dist/web/server/index.d.ts +725 -0
- package/dist/web/server/index.js +3 -0
- package/dist/web/server/index.js.map +1 -0
- package/package.json +14 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkCH42VPWE_cjs = require('./chunk-CH42VPWE.cjs');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var clsx2 = require('clsx');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
var lucideReact = require('lucide-react');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
12
|
+
var clsx2__default = /*#__PURE__*/_interopDefault(clsx2);
|
|
13
|
+
|
|
14
|
+
var VoidBackground = React__default.default.forwardRef(
|
|
15
|
+
({ showGrid = true, showGrain = true, className, children }, ref) => {
|
|
16
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: clsx2__default.default("void", className), children: [
|
|
17
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__base", "aria-hidden": "true" }),
|
|
18
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "void__orbs", "aria-hidden": "true", children: [
|
|
19
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__orb void__orb--warm" }),
|
|
20
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__orb void__orb--purple" })
|
|
21
|
+
] }),
|
|
22
|
+
showGrid && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__grid", "aria-hidden": "true" }),
|
|
23
|
+
showGrain && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__grain", "aria-hidden": "true" }),
|
|
24
|
+
children && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "void__content", children })
|
|
25
|
+
] });
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
VoidBackground.displayName = "VoidBackground";
|
|
29
|
+
var ThemeSelector = ({ className, style }) => {
|
|
30
|
+
const { theme, setTheme } = chunkCH42VPWE_cjs.useAppearance();
|
|
31
|
+
const handleThemeChange = (newTheme) => {
|
|
32
|
+
setTheme(newTheme);
|
|
33
|
+
};
|
|
34
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx2__default.default("ds-theme-selector", className), style, role: "group", "aria-label": "Theme Selector", children: [
|
|
35
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
36
|
+
"button",
|
|
37
|
+
{
|
|
38
|
+
type: "button",
|
|
39
|
+
className: clsx2__default.default("ds-theme-selector__btn", { "ds-theme-selector__btn--active": theme === "light" }),
|
|
40
|
+
onClick: () => handleThemeChange("light"),
|
|
41
|
+
"aria-pressed": theme === "light",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sun, { className: "ds-theme-selector__icon" }),
|
|
44
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ds-theme-selector__label", children: "Light" })
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
),
|
|
48
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
49
|
+
"button",
|
|
50
|
+
{
|
|
51
|
+
type: "button",
|
|
52
|
+
className: clsx2__default.default("ds-theme-selector__btn", { "ds-theme-selector__btn--active": theme === "light-contrast" }),
|
|
53
|
+
onClick: () => handleThemeChange("light-contrast"),
|
|
54
|
+
"aria-pressed": theme === "light-contrast",
|
|
55
|
+
children: [
|
|
56
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sun, { className: "ds-theme-selector__icon" }),
|
|
57
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ds-theme-selector__label", children: "Light HC" })
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
type: "button",
|
|
65
|
+
className: clsx2__default.default("ds-theme-selector__btn", { "ds-theme-selector__btn--active": theme === "dark" }),
|
|
66
|
+
onClick: () => handleThemeChange("dark"),
|
|
67
|
+
"aria-pressed": theme === "dark",
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Moon, { className: "ds-theme-selector__icon" }),
|
|
70
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ds-theme-selector__label", children: "Dark" })
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
75
|
+
"button",
|
|
76
|
+
{
|
|
77
|
+
type: "button",
|
|
78
|
+
className: clsx2__default.default("ds-theme-selector__btn", { "ds-theme-selector__btn--active": theme === "dark-contrast" }),
|
|
79
|
+
onClick: () => handleThemeChange("dark-contrast"),
|
|
80
|
+
"aria-pressed": theme === "dark-contrast",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Moon, { className: "ds-theme-selector__icon" }),
|
|
83
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ds-theme-selector__label", children: "Dark HC" })
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
] });
|
|
88
|
+
};
|
|
89
|
+
ThemeSelector.displayName = "ThemeSelector";
|
|
90
|
+
var BRAND_PRESETS = [
|
|
91
|
+
{ value: "default", label: "Warm Intelligence", color: "#f97316" },
|
|
92
|
+
{ value: "salesmind", label: "SalesMind", color: "#ff005a" }
|
|
93
|
+
];
|
|
94
|
+
var ColorPicker = React__default.default.forwardRef(
|
|
95
|
+
({ className, style }, ref) => {
|
|
96
|
+
const { brand, setBrand, customColor, setCustomColor } = chunkCH42VPWE_cjs.useAppearance();
|
|
97
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
98
|
+
"div",
|
|
99
|
+
{
|
|
100
|
+
ref,
|
|
101
|
+
className: clsx2__default.default("ds-color-picker", className),
|
|
102
|
+
style,
|
|
103
|
+
role: "group",
|
|
104
|
+
"aria-label": "Brand Color",
|
|
105
|
+
children: [
|
|
106
|
+
BRAND_PRESETS.map(({ value, label, color }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
107
|
+
"button",
|
|
108
|
+
{
|
|
109
|
+
type: "button",
|
|
110
|
+
className: clsx2__default.default("ds-color-picker__swatch", {
|
|
111
|
+
"ds-color-picker__swatch--active": brand === value
|
|
112
|
+
}),
|
|
113
|
+
onClick: () => setBrand(value),
|
|
114
|
+
"aria-label": `Switch to ${label} color scheme`,
|
|
115
|
+
"aria-pressed": brand === value,
|
|
116
|
+
title: label,
|
|
117
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
118
|
+
"span",
|
|
119
|
+
{
|
|
120
|
+
className: "ds-color-picker__dot",
|
|
121
|
+
style: { "--swatch-color": color }
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
value
|
|
126
|
+
)),
|
|
127
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
128
|
+
"label",
|
|
129
|
+
{
|
|
130
|
+
className: clsx2__default.default("ds-color-picker__swatch", "ds-color-picker__swatch--custom", {
|
|
131
|
+
"ds-color-picker__swatch--active": brand === "custom"
|
|
132
|
+
}),
|
|
133
|
+
title: `Custom: ${customColor}`,
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
136
|
+
"span",
|
|
137
|
+
{
|
|
138
|
+
className: "ds-color-picker__dot",
|
|
139
|
+
style: { "--swatch-color": customColor }
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
"input",
|
|
144
|
+
{
|
|
145
|
+
type: "color",
|
|
146
|
+
value: customColor,
|
|
147
|
+
onChange: (e) => setCustomColor(e.target.value),
|
|
148
|
+
className: "ds-color-picker__native-input",
|
|
149
|
+
"aria-label": "Pick a custom accent color"
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
ColorPicker.displayName = "ColorPicker";
|
|
161
|
+
|
|
162
|
+
exports.ColorPicker = ColorPicker;
|
|
163
|
+
exports.ThemeSelector = ThemeSelector;
|
|
164
|
+
exports.VoidBackground = VoidBackground;
|
|
165
|
+
//# sourceMappingURL=out.js.map
|
|
166
|
+
//# sourceMappingURL=chunk-KCKUSU2M.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/VoidBackground/VoidBackground.tsx","../src/components/ThemeSelector/ThemeSelector.tsx","../src/components/ColorPicker/ColorPicker.tsx"],"names":["clsx","jsx","jsxs","React"],"mappings":"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","sourcesContent":["import React from 'react';\nimport clsx from 'clsx';\nimport './VoidBackground.css';\n\nexport interface VoidBackgroundProps {\n /** Show the engineered grid overlay */\n showGrid?: boolean;\n /** Show the film grain texture */\n showGrain?: boolean;\n /** Custom class name */\n className?: string;\n /** Child content rendered above the void */\n children?: React.ReactNode;\n}\n\n/**\n * VoidBackground — The Void Signature Design System Level 0 Environment\n *\n * A four-layer atmospheric background system:\n * 1. Base Color — Deep purple-black (#08040a)\n * 2. Light Orbs — Radial gradients with mix-blend-mode: screen\n * 3. Engineered Grid — 80×80px with radial fade mask\n * 4. Film Grain — Static SVG noise for texture\n *\n * The Void is not decoration — it is the lighting engine for the UI.\n */\nexport const VoidBackground = React.forwardRef<HTMLDivElement, VoidBackgroundProps>(\n ({ showGrid = true, showGrain = true, className, children }, ref) => {\n return (\n <div ref={ref} className={clsx('void', className)}>\n {/* Layer 1: Base Color */}\n <div className=\"void__base\" aria-hidden=\"true\" />\n\n {/* Layer 2: Light Orbs (Infusion Engine) */}\n <div className=\"void__orbs\" aria-hidden=\"true\">\n <div className=\"void__orb void__orb--warm\" />\n <div className=\"void__orb void__orb--purple\" />\n </div>\n\n {/* Layer 3: Engineered Grid */}\n {showGrid && <div className=\"void__grid\" aria-hidden=\"true\" />}\n\n {/* Layer 4: Film Grain */}\n {showGrain && <div className=\"void__grain\" aria-hidden=\"true\" />}\n\n {/* Content Layer */}\n {children && <div className=\"void__content\">{children}</div>}\n </div>\n );\n },\n);\n\nVoidBackground.displayName = 'VoidBackground';\n","import React from 'react';\nimport { Sun, Moon } from 'lucide-react';\nimport clsx from 'clsx';\nimport { useAppearance, Theme } from '../../theme/AppearanceProvider';\nimport './ThemeSelector.css';\n\nexport interface ThemeSelectorProps {\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport const ThemeSelector: React.FC<ThemeSelectorProps> = ({ className, style }) => {\n const { theme, setTheme } = useAppearance();\n\n const handleThemeChange = (newTheme: Theme) => {\n setTheme(newTheme);\n };\n\n return (\n <div className={clsx('ds-theme-selector', className)} style={style} role=\"group\" aria-label=\"Theme Selector\">\n <button\n type=\"button\"\n className={clsx('ds-theme-selector__btn', { 'ds-theme-selector__btn--active': theme === 'light' })}\n onClick={() => handleThemeChange('light')}\n aria-pressed={theme === 'light'}\n >\n <Sun className=\"ds-theme-selector__icon\" />\n <span className=\"ds-theme-selector__label\">Light</span>\n </button>\n\n <button\n type=\"button\"\n className={clsx('ds-theme-selector__btn', { 'ds-theme-selector__btn--active': theme === 'light-contrast' })}\n onClick={() => handleThemeChange('light-contrast')}\n aria-pressed={theme === 'light-contrast'}\n >\n <Sun className=\"ds-theme-selector__icon\" />\n <span className=\"ds-theme-selector__label\">Light HC</span>\n </button>\n\n <button\n type=\"button\"\n className={clsx('ds-theme-selector__btn', { 'ds-theme-selector__btn--active': theme === 'dark' })}\n onClick={() => handleThemeChange('dark')}\n aria-pressed={theme === 'dark'}\n >\n <Moon className=\"ds-theme-selector__icon\" />\n <span className=\"ds-theme-selector__label\">Dark</span>\n </button>\n\n <button\n type=\"button\"\n className={clsx('ds-theme-selector__btn', { 'ds-theme-selector__btn--active': theme === 'dark-contrast' })}\n onClick={() => handleThemeChange('dark-contrast')}\n aria-pressed={theme === 'dark-contrast'}\n >\n <Moon className=\"ds-theme-selector__icon\" />\n <span className=\"ds-theme-selector__label\">Dark HC</span>\n </button>\n </div>\n );\n};\n\nThemeSelector.displayName = 'ThemeSelector';\n","import React from 'react';\nimport clsx from 'clsx';\nimport { useAppearance, Brand } from '../../theme/AppearanceProvider';\nimport './ColorPicker.css';\n\n/** Pre-configured brand presets shown as swatches. */\nconst BRAND_PRESETS: { value: Brand; label: string; color: string }[] = [\n { value: 'default', label: 'Warm Intelligence', color: '#f97316' },\n { value: 'salesmind', label: 'SalesMind', color: '#ff005a' },\n];\n\nexport interface ColorPickerProps {\n /** Additional CSS class name. */\n className?: string;\n /** Inline styles. */\n style?: React.CSSProperties;\n}\n\n/**\n * Compact brand/accent color picker.\n * Renders preset brand swatches and an optional custom color input.\n * Consumes the `useAppearance()` context for state.\n */\nexport const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>(\n ({ className, style }, ref) => {\n const { brand, setBrand, customColor, setCustomColor } = useAppearance();\n\n return (\n <div\n ref={ref}\n className={clsx('ds-color-picker', className)}\n style={style}\n role=\"group\"\n aria-label=\"Brand Color\"\n >\n {BRAND_PRESETS.map(({ value, label, color }) => (\n <button\n key={value}\n type=\"button\"\n className={clsx('ds-color-picker__swatch', {\n 'ds-color-picker__swatch--active': brand === value,\n })}\n onClick={() => setBrand(value)}\n aria-label={`Switch to ${label} color scheme`}\n aria-pressed={brand === value}\n title={label}\n >\n <span\n className=\"ds-color-picker__dot\"\n style={{ '--swatch-color': color } as React.CSSProperties}\n />\n </button>\n ))}\n\n {/* Custom color — clicking the dot opens the native color picker */}\n <label\n className={clsx('ds-color-picker__swatch', 'ds-color-picker__swatch--custom', {\n 'ds-color-picker__swatch--active': brand === 'custom',\n })}\n title={`Custom: ${customColor}`}\n >\n <span\n className=\"ds-color-picker__dot\"\n style={{ '--swatch-color': customColor } as React.CSSProperties}\n />\n <input\n type=\"color\"\n value={customColor}\n onChange={(e) => setCustomColor(e.target.value)}\n className=\"ds-color-picker__native-input\"\n aria-label=\"Pick a custom accent color\"\n />\n </label>\n </div>\n );\n },\n);\n\nColorPicker.displayName = 'ColorPicker';\n"]}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { createContext, forwardRef, useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/web/utm/useUtmDefaults.ts
|
|
5
|
+
var UtmContext = createContext(null);
|
|
6
|
+
|
|
7
|
+
// src/web/utm/useUtmDefaults.ts
|
|
8
|
+
function useUtmDefaults() {
|
|
9
|
+
return useContext(UtmContext);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/web/utm/builders.ts
|
|
13
|
+
var PLACEHOLDER_ORIGIN = "https://__placeholder__.internal";
|
|
14
|
+
function buildUtmUrl(baseUrl, params) {
|
|
15
|
+
const isRelative = baseUrl.startsWith("/");
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = isRelative ? new URL(baseUrl, PLACEHOLDER_ORIGIN) : new URL(baseUrl);
|
|
19
|
+
} catch {
|
|
20
|
+
return baseUrl;
|
|
21
|
+
}
|
|
22
|
+
const existingParams = [];
|
|
23
|
+
url.searchParams.forEach((value, key) => {
|
|
24
|
+
existingParams.push([key, value]);
|
|
25
|
+
});
|
|
26
|
+
for (const [key] of existingParams) {
|
|
27
|
+
url.searchParams.delete(key);
|
|
28
|
+
}
|
|
29
|
+
for (const [key, value] of existingParams) {
|
|
30
|
+
if (!key.startsWith("utm_")) {
|
|
31
|
+
url.searchParams.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
url.searchParams.set("utm_source", params.source);
|
|
35
|
+
url.searchParams.set("utm_medium", params.medium);
|
|
36
|
+
url.searchParams.set("utm_campaign", params.campaign);
|
|
37
|
+
if (params.term !== void 0) {
|
|
38
|
+
url.searchParams.set("utm_term", params.term);
|
|
39
|
+
}
|
|
40
|
+
if (params.content !== void 0) {
|
|
41
|
+
url.searchParams.set("utm_content", params.content);
|
|
42
|
+
}
|
|
43
|
+
if (isRelative) {
|
|
44
|
+
return url.href.replace(PLACEHOLDER_ORIGIN, "");
|
|
45
|
+
}
|
|
46
|
+
return url.href;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/web/utm/classifiers.ts
|
|
50
|
+
var INTERNAL_PATTERNS = [
|
|
51
|
+
/^\/(?!\/)/,
|
|
52
|
+
// Relative paths
|
|
53
|
+
/^https?:\/\/(www\.)?sales-mind\.ai/i,
|
|
54
|
+
// Marketing site
|
|
55
|
+
/^https?:\/\/app\.sales-mind\.ai/i,
|
|
56
|
+
// Web app
|
|
57
|
+
/^https?:\/\/apps\.sales-mind\.ai/i,
|
|
58
|
+
// Web app (legacy)
|
|
59
|
+
/^https?:\/\/meet\.sales-mind\.ai/i,
|
|
60
|
+
// Booking
|
|
61
|
+
/^https?:\/\/docs\.sales-mind\.ai/i
|
|
62
|
+
// Docs
|
|
63
|
+
];
|
|
64
|
+
var SYSTEM_PATTERNS = [
|
|
65
|
+
/^https?:\/\/.*\/api\//i,
|
|
66
|
+
// API endpoints
|
|
67
|
+
/^https?:\/\/.*\/webhook/i,
|
|
68
|
+
// Webhooks
|
|
69
|
+
/^https?:\/\/.*\/oauth/i,
|
|
70
|
+
// OAuth callbacks
|
|
71
|
+
/^https?:\/\/.*\/callback/i,
|
|
72
|
+
// Callbacks
|
|
73
|
+
/^https?:\/\/.*\.supabase\.(co|com)/i,
|
|
74
|
+
// Supabase
|
|
75
|
+
/^https?:\/\/.*\.firebaseapp\.com/i,
|
|
76
|
+
// Firebase
|
|
77
|
+
/^https?:\/\/.*\.cloudfunctions\.net/i
|
|
78
|
+
// Cloud Functions
|
|
79
|
+
];
|
|
80
|
+
var ASSET_PATTERNS = [
|
|
81
|
+
/\.(css|js|mjs|map|woff2?|ttf|eot|svg|png|jpe?g|gif|webp|avif|ico|pdf)(\?.*)?$/i
|
|
82
|
+
];
|
|
83
|
+
var CONVERSION_PATTERNS = [
|
|
84
|
+
/^https?:\/\/(www\.)?calendly\.com/i,
|
|
85
|
+
/^https?:\/\/(checkout\.)?stripe\.com/i,
|
|
86
|
+
/^https?:\/\/buy\.stripe\.com/i,
|
|
87
|
+
/^https?:\/\/chromewebstore\.google\.com/i,
|
|
88
|
+
/^https?:\/\/meet\.sales-mind\.ai/i
|
|
89
|
+
];
|
|
90
|
+
var PROTOCOL_EXEMPT = [
|
|
91
|
+
/^mailto:/i,
|
|
92
|
+
/^tel:/i,
|
|
93
|
+
/^#/,
|
|
94
|
+
/^javascript:/i
|
|
95
|
+
];
|
|
96
|
+
function classifyUrl(url) {
|
|
97
|
+
if (PROTOCOL_EXEMPT.some((p) => p.test(url))) return "protocol";
|
|
98
|
+
if (ASSET_PATTERNS.some((p) => p.test(url))) return "asset";
|
|
99
|
+
if (SYSTEM_PATTERNS.some((p) => p.test(url))) return "system";
|
|
100
|
+
if (CONVERSION_PATTERNS.some((p) => p.test(url))) return "conversion";
|
|
101
|
+
if (INTERNAL_PATTERNS.some((p) => p.test(url))) return "internal";
|
|
102
|
+
return "external";
|
|
103
|
+
}
|
|
104
|
+
function requiresUtm(url) {
|
|
105
|
+
const classification = classifyUrl(url);
|
|
106
|
+
return classification === "external" || classification === "conversion";
|
|
107
|
+
}
|
|
108
|
+
function isUtmExempt(url) {
|
|
109
|
+
const classification = classifyUrl(url);
|
|
110
|
+
return classification === "protocol" || classification === "asset" || classification === "system" || classification === "internal";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/components/OutboundLink/outbound-link-utils.ts
|
|
114
|
+
var LEGACY_UTM_SOURCE = "salesmind";
|
|
115
|
+
var EXEMPT_PATTERNS = [
|
|
116
|
+
/^https?:\/\/(www\.)?(stripe\.com|checkout\.stripe\.com|paypal\.com)/i,
|
|
117
|
+
/^https?:\/\/(www\.)?github\.com\/login\/oauth/i
|
|
118
|
+
];
|
|
119
|
+
var isExemptUrl = (urlStr) => {
|
|
120
|
+
if (urlStr.startsWith("mailto:") || urlStr.startsWith("tel:")) return true;
|
|
121
|
+
const classification = classifyUrl(urlStr);
|
|
122
|
+
if (classification === "system" || classification === "protocol" || classification === "asset") {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return EXEMPT_PATTERNS.some((pattern) => pattern.test(urlStr));
|
|
126
|
+
};
|
|
127
|
+
var appendGovernedUTMs = (href, params, preserveExisting = true) => {
|
|
128
|
+
try {
|
|
129
|
+
const url = new URL(href);
|
|
130
|
+
if (preserveExisting) {
|
|
131
|
+
const hasAll = url.searchParams.has("utm_source") && url.searchParams.has("utm_medium") && url.searchParams.has("utm_campaign");
|
|
132
|
+
if (hasAll) return href;
|
|
133
|
+
}
|
|
134
|
+
return buildUtmUrl(href, params);
|
|
135
|
+
} catch {
|
|
136
|
+
return href;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var appendUTMs = (href, context, pageSlug, options) => {
|
|
140
|
+
try {
|
|
141
|
+
const url = new URL(href);
|
|
142
|
+
const { mediumOverride = "outbound_link", campaignOverride, preserveExisting = true } = options;
|
|
143
|
+
const utms = {
|
|
144
|
+
utm_source: LEGACY_UTM_SOURCE,
|
|
145
|
+
utm_medium: mediumOverride,
|
|
146
|
+
utm_campaign: campaignOverride || pageSlug,
|
|
147
|
+
utm_content: context
|
|
148
|
+
};
|
|
149
|
+
Object.entries(utms).forEach(([key, value]) => {
|
|
150
|
+
if (value) {
|
|
151
|
+
if (preserveExisting && url.searchParams.has(key)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
url.searchParams.set(key, value);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return url.toString();
|
|
158
|
+
} catch {
|
|
159
|
+
return href;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var OutboundLink = forwardRef(
|
|
163
|
+
({
|
|
164
|
+
href,
|
|
165
|
+
context,
|
|
166
|
+
campaignOverride,
|
|
167
|
+
mediumOverride = "outbound_link",
|
|
168
|
+
preserveExistingUTM = true,
|
|
169
|
+
openInNewTab = true,
|
|
170
|
+
disableTracking = false,
|
|
171
|
+
utmParams,
|
|
172
|
+
onClick,
|
|
173
|
+
children,
|
|
174
|
+
...props
|
|
175
|
+
}, ref) => {
|
|
176
|
+
const contextParams = useUtmDefaults();
|
|
177
|
+
const resolvedUtmParams = utmParams ?? contextParams;
|
|
178
|
+
let hostname = "";
|
|
179
|
+
try {
|
|
180
|
+
const url = new URL(href);
|
|
181
|
+
hostname = url.hostname;
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
const [finalHref, setFinalHref] = useState(href);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
let isExternal = false;
|
|
187
|
+
let currentMedium = mediumOverride;
|
|
188
|
+
try {
|
|
189
|
+
const url = new URL(href);
|
|
190
|
+
const currentHost = window.location.hostname;
|
|
191
|
+
isExternal = url.hostname !== currentHost;
|
|
192
|
+
if (isExternal && currentHost.includes("sales-mind.ai") && url.hostname.includes("sales-mind.ai")) {
|
|
193
|
+
if (currentMedium === "outbound_link") {
|
|
194
|
+
currentMedium = "cross_subdomain";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
isExternal = false;
|
|
199
|
+
}
|
|
200
|
+
const isExempt = isExemptUrl(href) || disableTracking;
|
|
201
|
+
if (isExternal && !isExempt) {
|
|
202
|
+
if (resolvedUtmParams) {
|
|
203
|
+
setFinalHref(appendGovernedUTMs(href, resolvedUtmParams, preserveExistingUTM));
|
|
204
|
+
} else {
|
|
205
|
+
const pageSlug = window.location.pathname.replace(/^\/|\/$/g, "") || "home";
|
|
206
|
+
setFinalHref(appendUTMs(href, context, pageSlug, {
|
|
207
|
+
mediumOverride: currentMedium,
|
|
208
|
+
campaignOverride,
|
|
209
|
+
preserveExisting: preserveExistingUTM
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
setFinalHref(href);
|
|
214
|
+
}
|
|
215
|
+
}, [href, context, mediumOverride, campaignOverride, preserveExistingUTM, disableTracking, resolvedUtmParams]);
|
|
216
|
+
const handleClick = (e) => {
|
|
217
|
+
if (typeof window === "undefined" || disableTracking) {
|
|
218
|
+
onClick?.(e);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
let clickExternal = false;
|
|
222
|
+
let clickCrossSubdomain = false;
|
|
223
|
+
let clickMedium = mediumOverride;
|
|
224
|
+
try {
|
|
225
|
+
const url = new URL(href);
|
|
226
|
+
const currentHost = window.location.hostname;
|
|
227
|
+
clickExternal = url.hostname !== currentHost;
|
|
228
|
+
if (clickExternal && currentHost.includes("sales-mind.ai") && url.hostname.includes("sales-mind.ai")) {
|
|
229
|
+
clickCrossSubdomain = true;
|
|
230
|
+
if (clickMedium === "outbound_link") {
|
|
231
|
+
clickMedium = "cross_subdomain";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
if (clickExternal) {
|
|
237
|
+
const detail = {
|
|
238
|
+
destination_domain: hostname,
|
|
239
|
+
destination_url: finalHref,
|
|
240
|
+
utm_medium_type: clickMedium,
|
|
241
|
+
page_slug: window.location.pathname,
|
|
242
|
+
component_location: context,
|
|
243
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
244
|
+
is_cross_subdomain: clickCrossSubdomain
|
|
245
|
+
};
|
|
246
|
+
const event = new CustomEvent("outbound_click", { detail });
|
|
247
|
+
window.dispatchEvent(event);
|
|
248
|
+
}
|
|
249
|
+
onClick?.(e);
|
|
250
|
+
};
|
|
251
|
+
const relParts = [];
|
|
252
|
+
let shouldOpenNewTab = openInNewTab;
|
|
253
|
+
try {
|
|
254
|
+
const url = new URL(href);
|
|
255
|
+
if (typeof window !== "undefined") {
|
|
256
|
+
const currentHost = window.location.hostname;
|
|
257
|
+
if (url.hostname !== currentHost && currentHost.includes("sales-mind.ai") && url.hostname.includes("sales-mind.ai")) {
|
|
258
|
+
shouldOpenNewTab = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
if (shouldOpenNewTab) relParts.push("noopener", "noreferrer");
|
|
264
|
+
if (mediumOverride === "citation") relParts.push("nofollow");
|
|
265
|
+
const rel = relParts.length > 0 ? relParts.join(" ") : void 0;
|
|
266
|
+
return (
|
|
267
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
268
|
+
/* @__PURE__ */ jsx(
|
|
269
|
+
"a",
|
|
270
|
+
{
|
|
271
|
+
ref,
|
|
272
|
+
href: finalHref,
|
|
273
|
+
target: shouldOpenNewTab ? "_blank" : void 0,
|
|
274
|
+
rel,
|
|
275
|
+
onClick: handleClick,
|
|
276
|
+
...props,
|
|
277
|
+
children
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
OutboundLink.displayName = "OutboundLink";
|
|
284
|
+
|
|
285
|
+
export { OutboundLink, UtmContext, appendGovernedUTMs, appendUTMs, buildUtmUrl, classifyUrl, isExemptUrl, isUtmExempt, requiresUtm, useUtmDefaults };
|
|
286
|
+
//# sourceMappingURL=out.js.map
|
|
287
|
+
//# sourceMappingURL=chunk-KJ2OXQF4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/web/utm/useUtmDefaults.ts","../src/web/utm/utm-context.ts","../src/web/utm/builders.ts","../src/web/utm/classifiers.ts","../src/components/OutboundLink/outbound-link-utils.ts","../src/components/OutboundLink/OutboundLink.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useContext } from 'react';\nimport { UtmContext } from './utm-context';\nimport type { UtmParams } from './types';\n\n/**\n * Read the nearest UtmProvider's default params.\n * Returns `null` when no provider is present (callers should fall back gracefully).\n */\nexport function useUtmDefaults(): UtmParams | null {\n return useContext(UtmContext);\n}\n","import { createContext } from 'react';\nimport type { UtmParams } from './types';\n\n/** Internal React context instance. Import via UtmProvider / useUtmDefaults — do not use directly. */\nexport const UtmContext = createContext<UtmParams | null>(null);\n","/* ============================================================================\n UTM URL Builders\n ============================================================================ */\n\nimport type { UtmParams } from './types';\n\n/** Placeholder origin used internally to parse relative URLs. Never appears in output. */\nconst PLACEHOLDER_ORIGIN = 'https://__placeholder__.internal';\n\n/**\n * Build a URL with UTM parameters appended.\n *\n * - Supports both absolute and relative URLs.\n * - Preserves existing query parameters and hash fragments.\n * - Always emits params in canonical order: source → medium → campaign → term → content.\n * - Omits optional params (term, content) when undefined.\n *\n * @example\n * buildUtmUrl('https://app.sales-mind.ai/register', { source: 'linkedin', medium: 'dm', campaign: 'discoveryCall' })\n * // → 'https://app.sales-mind.ai/register?utm_source=linkedin&utm_medium=dm&utm_campaign=discoveryCall'\n *\n * buildUtmUrl('/pricing', { source: 'website', medium: 'webHome', campaign: 'trial' })\n * // → '/pricing?utm_source=website&utm_medium=webHome&utm_campaign=trial'\n */\nexport function buildUtmUrl(baseUrl: string, params: UtmParams): string {\n const isRelative = baseUrl.startsWith('/');\n\n let url: URL;\n try {\n url = isRelative ? new URL(baseUrl, PLACEHOLDER_ORIGIN) : new URL(baseUrl);\n } catch {\n // If URL parsing fails entirely, return the base URL unchanged.\n return baseUrl;\n }\n\n // Preserve existing params by collecting them first, then clearing and re-adding in order.\n const existingParams: [string, string][] = [];\n url.searchParams.forEach((value, key) => {\n existingParams.push([key, value]);\n });\n\n // Clear all params to rebuild in canonical order.\n for (const [key] of existingParams) {\n url.searchParams.delete(key);\n }\n\n // Re-add existing non-UTM params first.\n for (const [key, value] of existingParams) {\n if (!key.startsWith('utm_')) {\n url.searchParams.set(key, value);\n }\n }\n\n // Append UTM params in canonical order.\n url.searchParams.set('utm_source', params.source);\n url.searchParams.set('utm_medium', params.medium);\n url.searchParams.set('utm_campaign', params.campaign);\n if (params.term !== undefined) {\n url.searchParams.set('utm_term', params.term);\n }\n if (params.content !== undefined) {\n url.searchParams.set('utm_content', params.content);\n }\n\n if (isRelative) {\n // Strip placeholder origin to return a relative path.\n return url.href.replace(PLACEHOLDER_ORIGIN, '');\n }\n\n return url.href;\n}\n","/* ============================================================================\n URL Classification & UTM Enforcement Logic\n ============================================================================\n Determines whether a URL requires UTM parameters based on its type.\n No imports from validators — avoids circular dependency.\n ============================================================================ */\n\nimport type { UrlClassification } from './types';\n\n// ── Patterns ─────────────────────────────────────────────────────────────────\n\n/** Internal domains — UTMs not required for same-origin navigation. */\nconst INTERNAL_PATTERNS = [\n /^\\/(?!\\/)/, // Relative paths\n /^https?:\\/\\/(www\\.)?sales-mind\\.ai/i, // Marketing site\n /^https?:\\/\\/app\\.sales-mind\\.ai/i, // Web app\n /^https?:\\/\\/apps\\.sales-mind\\.ai/i, // Web app (legacy)\n /^https?:\\/\\/meet\\.sales-mind\\.ai/i, // Booking\n /^https?:\\/\\/docs\\.sales-mind\\.ai/i, // Docs\n];\n\n/** System endpoints — never receive UTMs. */\nconst SYSTEM_PATTERNS = [\n /^https?:\\/\\/.*\\/api\\//i, // API endpoints\n /^https?:\\/\\/.*\\/webhook/i, // Webhooks\n /^https?:\\/\\/.*\\/oauth/i, // OAuth callbacks\n /^https?:\\/\\/.*\\/callback/i, // Callbacks\n /^https?:\\/\\/.*\\.supabase\\.(co|com)/i, // Supabase\n /^https?:\\/\\/.*\\.firebaseapp\\.com/i, // Firebase\n /^https?:\\/\\/.*\\.cloudfunctions\\.net/i, // Cloud Functions\n];\n\n/** Static assets — never receive UTMs. */\nconst ASSET_PATTERNS = [\n /\\.(css|js|mjs|map|woff2?|ttf|eot|svg|png|jpe?g|gif|webp|avif|ico|pdf)(\\?.*)?$/i,\n];\n\n/** Conversion destinations — always require UTMs. */\nconst CONVERSION_PATTERNS = [\n /^https?:\\/\\/(www\\.)?calendly\\.com/i,\n /^https?:\\/\\/(checkout\\.)?stripe\\.com/i,\n /^https?:\\/\\/buy\\.stripe\\.com/i,\n /^https?:\\/\\/chromewebstore\\.google\\.com/i,\n /^https?:\\/\\/meet\\.sales-mind\\.ai/i,\n];\n\n/** Protocol-level links — never receive UTMs. */\nconst PROTOCOL_EXEMPT = [\n /^mailto:/i,\n /^tel:/i,\n /^#/,\n /^javascript:/i,\n];\n\n// ── Classification ───────────────────────────────────────────────────────────\n\n/**\n * Classify a URL to determine its type.\n *\n * Classification order (first match wins):\n * 1. Protocol links (mailto, tel, anchor)\n * 2. Static assets\n * 3. System endpoints (API, webhooks, OAuth)\n * 4. Conversion destinations (Calendly, Stripe, Chrome Store)\n * 5. Internal (*.sales-mind.ai)\n * 6. External (everything else)\n */\nexport function classifyUrl(url: string): UrlClassification {\n // Protocol links\n if (PROTOCOL_EXEMPT.some(p => p.test(url))) return 'protocol';\n\n // Static assets\n if (ASSET_PATTERNS.some(p => p.test(url))) return 'asset';\n\n // System endpoints\n if (SYSTEM_PATTERNS.some(p => p.test(url))) return 'system';\n\n // Conversion destinations\n if (CONVERSION_PATTERNS.some(p => p.test(url))) return 'conversion';\n\n // Internal\n if (INTERNAL_PATTERNS.some(p => p.test(url))) return 'internal';\n\n // Everything else is external\n return 'external';\n}\n\n/**\n * Determine whether a URL requires UTM parameters.\n *\n * UTMs are mandatory for:\n * - External third-party destinations\n * - Conversion destinations (Calendly, Stripe, Chrome Web Store)\n *\n * UTMs are NOT required for:\n * - Internal navigation\n * - System endpoints\n * - Static assets\n * - Protocol links\n */\nexport function requiresUtm(url: string): boolean {\n const classification = classifyUrl(url);\n return classification === 'external' || classification === 'conversion';\n}\n\n/**\n * Check if a URL is explicitly exempt from UTM requirements.\n */\nexport function isUtmExempt(url: string): boolean {\n const classification = classifyUrl(url);\n return classification === 'protocol'\n || classification === 'asset'\n || classification === 'system'\n || classification === 'internal';\n}\n","import type { UtmParams } from '../../web/utm/types';\nimport { buildUtmUrl } from '../../web/utm/builders';\nimport { classifyUrl } from '../../web/utm/classifiers';\n\n/**\n * @deprecated Use governed `utmParams` prop on OutboundLink instead.\n * Legacy default source — superseded by governed UTM_SOURCES enum.\n */\nconst LEGACY_UTM_SOURCE = 'salesmind';\n\nconst EXEMPT_PATTERNS = [\n /^https?:\\/\\/(www\\.)?(stripe\\.com|checkout\\.stripe\\.com|paypal\\.com)/i,\n /^https?:\\/\\/(www\\.)?github\\.com\\/login\\/oauth/i,\n];\n\n/**\n * Check if a URL is exempt from UTM tagging.\n * Uses both legacy patterns and governed URL classification.\n */\nexport const isExemptUrl = (urlStr: string): boolean => {\n if (urlStr.startsWith('mailto:') || urlStr.startsWith('tel:')) return true;\n\n // Governed classification: system/protocol/asset URLs are exempt\n const classification = classifyUrl(urlStr);\n if (classification === 'system' || classification === 'protocol' || classification === 'asset') {\n return true;\n }\n\n return EXEMPT_PATTERNS.some(pattern => pattern.test(urlStr));\n};\n\n/**\n * Append governed UTM parameters to a URL using `buildUtmUrl`.\n * Preferred over the legacy `appendUTMs` function.\n */\nexport const appendGovernedUTMs = (\n href: string,\n params: UtmParams,\n preserveExisting = true,\n): string => {\n try {\n const url = new URL(href);\n\n // If preserveExisting and all UTM params are already set, return unchanged\n if (preserveExisting) {\n const hasAll = url.searchParams.has('utm_source')\n && url.searchParams.has('utm_medium')\n && url.searchParams.has('utm_campaign');\n if (hasAll) return href;\n }\n\n return buildUtmUrl(href, params);\n } catch {\n return href;\n }\n};\n\n/**\n * @deprecated Prefer `appendGovernedUTMs` with governed `UtmParams`.\n *\n * Legacy UTM append function. Uses non-governed defaults:\n * - utm_source = 'salesmind' (not in governed enum)\n * - utm_medium = 'outbound_link' (snake_case, not in governed enum)\n *\n * Retained for backward compatibility. Consumers should migrate to\n * the `utmParams` prop on OutboundLink.\n */\nexport const appendUTMs = (\n href: string,\n context: string,\n pageSlug: string,\n options: {\n mediumOverride?: string;\n campaignOverride?: string;\n preserveExisting?: boolean;\n }\n): string => {\n try {\n const url = new URL(href);\n const { mediumOverride = 'outbound_link', campaignOverride, preserveExisting = true } = options;\n\n const utms = {\n utm_source: LEGACY_UTM_SOURCE,\n utm_medium: mediumOverride,\n utm_campaign: campaignOverride || pageSlug,\n utm_content: context,\n };\n\n Object.entries(utms).forEach(([key, value]) => {\n if (value) {\n if (preserveExisting && url.searchParams.has(key)) {\n // Do not override if preserve is true\n return;\n }\n url.searchParams.set(key, value);\n }\n });\n\n return url.toString();\n } catch {\n return href; // Return original if not valid URL (e.g. relative)\n }\n};\n","import React, { AnchorHTMLAttributes, forwardRef, MouseEvent, useState, useEffect } from 'react';\nimport type { UtmParams } from '../../web/utm/types';\nimport { useUtmDefaults } from '../../web/utm/useUtmDefaults';\nimport { isExemptUrl, appendUTMs, appendGovernedUTMs } from './outbound-link-utils';\n\nexport interface OutboundLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n href: string;\n context: string;\n campaignOverride?: string;\n mediumOverride?: string;\n preserveExistingUTM?: boolean;\n openInNewTab?: boolean;\n disableTracking?: boolean;\n /**\n * Governed UTM parameters. When provided, the link uses the UTM governance\n * system (buildUtmUrl) instead of the legacy appendUTMs approach.\n *\n * Preferred for all new code. Existing usages without utmParams continue\n * to work via the legacy path.\n */\n utmParams?: UtmParams;\n children: React.ReactNode;\n}\n\nexport interface OutboundClickEventDetail {\n destination_domain: string;\n destination_url: string;\n utm_medium_type: string;\n page_slug: string;\n component_location: string;\n timestamp: string;\n is_cross_subdomain: boolean;\n}\n\nexport const OutboundLink = forwardRef<HTMLAnchorElement, OutboundLinkProps>(\n (\n {\n href,\n context,\n campaignOverride,\n mediumOverride = 'outbound_link',\n preserveExistingUTM = true,\n openInNewTab = true,\n disableTracking = false,\n utmParams,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n // ── Resolve UTM params: explicit prop > context > legacy fallback ──\n const contextParams = useUtmDefaults();\n const resolvedUtmParams = utmParams ?? contextParams;\n\n // Parse hostname for analytics events (safe on both server/client)\n let hostname = '';\n try {\n const url = new URL(href);\n hostname = url.hostname;\n } catch {\n // Relative URL or invalid — hostname stays empty\n }\n\n // SSR and initial client render use the plain href to avoid hydration\n // mismatches. UTM params are decorated after hydration via useEffect.\n const [finalHref, setFinalHref] = useState(href);\n\n useEffect(() => {\n let isExternal = false;\n let currentMedium = mediumOverride;\n\n try {\n const url = new URL(href);\n const currentHost = window.location.hostname;\n isExternal = url.hostname !== currentHost;\n\n if (isExternal && currentHost.includes('sales-mind.ai') && url.hostname.includes('sales-mind.ai')) {\n if (currentMedium === 'outbound_link') {\n currentMedium = 'cross_subdomain';\n }\n }\n } catch {\n isExternal = false;\n }\n\n const isExempt = isExemptUrl(href) || disableTracking;\n\n if (isExternal && !isExempt) {\n if (resolvedUtmParams) {\n // Governed path: use buildUtmUrl via appendGovernedUTMs\n setFinalHref(appendGovernedUTMs(href, resolvedUtmParams, preserveExistingUTM));\n } else {\n // Legacy path: use appendUTMs with non-governed defaults\n const pageSlug = window.location.pathname.replace(/^\\/|\\/$/g, '') || 'home';\n setFinalHref(appendUTMs(href, context, pageSlug, {\n mediumOverride: currentMedium,\n campaignOverride,\n preserveExisting: preserveExistingUTM,\n }));\n }\n } else {\n setFinalHref(href);\n }\n }, [href, context, mediumOverride, campaignOverride, preserveExistingUTM, disableTracking, resolvedUtmParams]);\n\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n if (typeof window === 'undefined' || disableTracking) {\n onClick?.(e);\n return;\n }\n\n let clickExternal = false;\n let clickCrossSubdomain = false;\n let clickMedium = mediumOverride;\n\n try {\n const url = new URL(href);\n const currentHost = window.location.hostname;\n clickExternal = url.hostname !== currentHost;\n\n if (clickExternal && currentHost.includes('sales-mind.ai') && url.hostname.includes('sales-mind.ai')) {\n clickCrossSubdomain = true;\n if (clickMedium === 'outbound_link') {\n clickMedium = 'cross_subdomain';\n }\n }\n } catch { /* relative URL */ }\n\n if (clickExternal) {\n const detail: OutboundClickEventDetail = {\n destination_domain: hostname,\n destination_url: finalHref,\n utm_medium_type: clickMedium,\n page_slug: window.location.pathname,\n component_location: context,\n timestamp: new Date().toISOString(),\n is_cross_subdomain: clickCrossSubdomain,\n };\n\n const event = new CustomEvent('outbound_click', { detail });\n window.dispatchEvent(event);\n }\n\n onClick?.(e);\n };\n\n // Determine SEO attributes\n const relParts = [];\n \n // Check if it's cross-subdomain for target logic (usually open internal links in same tab, external in new)\n let shouldOpenNewTab = openInNewTab;\n try {\n const url = new URL(href);\n if (typeof window !== 'undefined') {\n const currentHost = window.location.hostname;\n if (url.hostname !== currentHost && currentHost.includes('sales-mind.ai') && url.hostname.includes('sales-mind.ai')) {\n shouldOpenNewTab = false; // By default, prefer same tab for cross-subdomain ecosystem links\n }\n }\n } catch { /* empty */ }\n\n if (shouldOpenNewTab) relParts.push('noopener', 'noreferrer');\n if (mediumOverride === 'citation') relParts.push('nofollow');\n const rel = relParts.length > 0 ? relParts.join(' ') : undefined;\n\n return (\n // eslint-disable-next-line no-restricted-syntax\n <a\n ref={ref}\n href={finalHref}\n target={shouldOpenNewTab ? '_blank' : undefined}\n rel={rel}\n onClick={handleClick}\n {...props}\n >\n {children}\n </a>\n );\n }\n);\n\nOutboundLink.displayName = 'OutboundLink';\n"]}
|