@nexushub/client 0.9.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1078
- package/dist/chunk-OP3LNZPC.cjs +116 -0
- package/dist/chunk-PB3YCBFH.js +116 -0
- package/dist/cli.cjs +259 -428
- package/dist/content/local-cache-server.cjs +36 -15
- package/dist/content/local-cache-server.d.cts +2 -5
- package/dist/content/local-cache-server.d.ts +2 -5
- package/dist/content/local-cache-server.js +36 -15
- package/dist/index.cjs +662 -341
- package/dist/index.d.cts +221 -32
- package/dist/index.d.ts +221 -32
- package/dist/index.js +652 -341
- package/dist/{local-cache-server-KUGMBMJP.cjs → local-cache-server-EKRKNL6N.cjs} +16 -8
- package/dist/{local-cache-server-RM7T456Z.js → local-cache-server-POO2S6OU.js} +15 -7
- package/dist/react.cjs +971 -374
- package/dist/react.d.cts +193 -76
- package/dist/react.d.ts +193 -76
- package/dist/react.js +1013 -416
- package/package.json +34 -28
- package/dist/chunk-3TDCUTZR.cjs +0 -53
- package/dist/chunk-VQ6IEN4I.js +0 -53
package/dist/react.js
CHANGED
|
@@ -1,16 +1,426 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SDK_VERSION,
|
|
3
|
+
buildQueryString,
|
|
2
4
|
getFullConfig,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
measurePerformance,
|
|
6
|
+
normalizeQuery,
|
|
7
|
+
validateConfig,
|
|
8
|
+
validateSlug
|
|
9
|
+
} from "./chunk-PB3YCBFH.js";
|
|
10
|
+
|
|
11
|
+
// src/components/NexusRenderer.tsx
|
|
12
|
+
import {
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useState
|
|
18
|
+
} from "react";
|
|
19
|
+
import { createPortal } from "react-dom";
|
|
20
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
21
|
+
function cn(...classes) {
|
|
22
|
+
return classes.filter(Boolean).join(" ");
|
|
23
|
+
}
|
|
24
|
+
var DEFAULT_CHART = {
|
|
25
|
+
chartType: "bar",
|
|
26
|
+
title: "Untitled Chart",
|
|
27
|
+
categories: ["Category A", "Category B", "Category C", "Category D"],
|
|
28
|
+
series: [
|
|
29
|
+
{
|
|
30
|
+
label: "Series 1",
|
|
31
|
+
color: "#6366f1",
|
|
32
|
+
data: [40, 65, 30, 80]
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
showLegend: true,
|
|
36
|
+
showGrid: true,
|
|
37
|
+
width: 560,
|
|
38
|
+
height: 320,
|
|
39
|
+
sourceNote: null
|
|
40
|
+
};
|
|
41
|
+
var FALLBACK_CHART_COLORS = [
|
|
42
|
+
"#6366f1",
|
|
43
|
+
"#f97316",
|
|
44
|
+
"#10b981",
|
|
45
|
+
"#ec4899",
|
|
46
|
+
"#0ea5e9",
|
|
47
|
+
"#eab308",
|
|
48
|
+
"#a855f7",
|
|
49
|
+
"#ef4444",
|
|
50
|
+
"#14b8a6",
|
|
51
|
+
"#84cc16"
|
|
52
|
+
];
|
|
53
|
+
function decodeChartSpec(raw) {
|
|
54
|
+
if (!raw) return null;
|
|
55
|
+
try {
|
|
56
|
+
const decoded = JSON.parse(
|
|
57
|
+
decodeURIComponent(atob(raw))
|
|
58
|
+
);
|
|
59
|
+
if (!decoded || typeof decoded !== "object") return null;
|
|
60
|
+
const categories = Array.isArray(decoded.categories) ? decoded.categories.map(String) : DEFAULT_CHART.categories;
|
|
61
|
+
const series = Array.isArray(decoded.series) ? decoded.series.filter(Boolean).map((s, index) => ({
|
|
62
|
+
label: typeof s.label === "string" ? s.label : `Series ${index + 1}`,
|
|
63
|
+
color: typeof s.color === "string" ? s.color : FALLBACK_CHART_COLORS[index % FALLBACK_CHART_COLORS.length],
|
|
64
|
+
data: Array.isArray(s.data) ? s.data.map((v) => {
|
|
65
|
+
const n = Number(v);
|
|
66
|
+
return Number.isFinite(n) ? n : 0;
|
|
67
|
+
}) : []
|
|
68
|
+
})) : DEFAULT_CHART.series;
|
|
69
|
+
return {
|
|
70
|
+
...DEFAULT_CHART,
|
|
71
|
+
...decoded,
|
|
72
|
+
categories,
|
|
73
|
+
series: series.length ? series : DEFAULT_CHART.series,
|
|
74
|
+
chartType: ["bar", "line", "pie", "donut", "area"].includes(decoded.chartType) ? decoded.chartType : DEFAULT_CHART.chartType,
|
|
75
|
+
title: typeof decoded.title === "string" ? decoded.title : DEFAULT_CHART.title,
|
|
76
|
+
showLegend: typeof decoded.showLegend === "boolean" ? decoded.showLegend : DEFAULT_CHART.showLegend,
|
|
77
|
+
showGrid: typeof decoded.showGrid === "boolean" ? decoded.showGrid : DEFAULT_CHART.showGrid,
|
|
78
|
+
width: typeof decoded.width === "number" && decoded.width > 0 ? decoded.width : DEFAULT_CHART.width,
|
|
79
|
+
height: typeof decoded.height === "number" && decoded.height > 0 ? decoded.height : DEFAULT_CHART.height,
|
|
80
|
+
sourceNote: typeof decoded.sourceNote === "string" ? decoded.sourceNote : null
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function recoverChartFromElement(element) {
|
|
87
|
+
const spec = decodeChartSpec(element.getAttribute("data-chart-spec"));
|
|
88
|
+
if (spec) return spec;
|
|
89
|
+
const title = element.getAttribute("data-chart-title") || DEFAULT_CHART.title;
|
|
90
|
+
const chartType = element.getAttribute("data-chart-type");
|
|
91
|
+
return {
|
|
92
|
+
...DEFAULT_CHART,
|
|
93
|
+
title,
|
|
94
|
+
chartType: ["bar", "line", "pie", "donut", "area"].includes(chartType) ? chartType : "bar"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function prepareDocumentHTML(html) {
|
|
98
|
+
if (typeof window === "undefined") return html;
|
|
99
|
+
const parser = new DOMParser();
|
|
100
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
101
|
+
doc.querySelectorAll("script, noscript").forEach((node) => node.remove());
|
|
102
|
+
doc.querySelectorAll("*").forEach((element) => {
|
|
103
|
+
[...element.attributes].forEach((attr) => {
|
|
104
|
+
if (attr.name.toLowerCase().startsWith("on")) {
|
|
105
|
+
element.removeAttribute(attr.name);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const href = element.getAttribute("href");
|
|
109
|
+
const src = element.getAttribute("src");
|
|
110
|
+
if (href && /^\s*javascript:/i.test(href)) element.removeAttribute("href");
|
|
111
|
+
if (src && /^\s*javascript:/i.test(src)) element.removeAttribute("src");
|
|
112
|
+
});
|
|
113
|
+
return doc.body.innerHTML;
|
|
114
|
+
}
|
|
115
|
+
function NativeNexusChart({ chart }) {
|
|
116
|
+
const [hoveredValue, setHoveredValue] = useState(null);
|
|
117
|
+
const categories = chart.categories;
|
|
118
|
+
const series = chart.series;
|
|
119
|
+
const allNumbers = series.flatMap((s) => s.data);
|
|
120
|
+
const maxVal = Math.max(...allNumbers, 10);
|
|
121
|
+
const minVal = Math.min(0, ...allNumbers);
|
|
122
|
+
const width = 560;
|
|
123
|
+
const height = Math.max(220, Math.min(chart.height || 300, 450));
|
|
124
|
+
const padLeft = 45;
|
|
125
|
+
const padRight = 20;
|
|
126
|
+
const padTop = 20;
|
|
127
|
+
const padBottom = 35;
|
|
128
|
+
const plotW = width - padLeft - padRight;
|
|
129
|
+
const plotH = height - padTop - padBottom;
|
|
130
|
+
const getY = (val) => {
|
|
131
|
+
const range = maxVal - minVal;
|
|
132
|
+
return padTop + plotH - (val - minVal) / range * plotH;
|
|
133
|
+
};
|
|
134
|
+
const getX = (catIndex) => {
|
|
135
|
+
return padLeft + catIndex / Math.max(categories.length - 1, 1) * plotW;
|
|
136
|
+
};
|
|
137
|
+
const renderGridAndAxes = () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
138
|
+
chart.showGrid && /* @__PURE__ */ jsx(Fragment, { children: [0, 0.25, 0.5, 0.75, 1].map((pct, i) => {
|
|
139
|
+
const y = padTop + plotH * (1 - pct);
|
|
140
|
+
const val = Math.round(minVal + (maxVal - minVal) * pct);
|
|
141
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
142
|
+
/* @__PURE__ */ jsx(
|
|
143
|
+
"line",
|
|
144
|
+
{
|
|
145
|
+
x1: padLeft,
|
|
146
|
+
y1: y,
|
|
147
|
+
x2: width - padRight,
|
|
148
|
+
y2: y,
|
|
149
|
+
stroke: "currentColor",
|
|
150
|
+
strokeOpacity: 0.08,
|
|
151
|
+
strokeDasharray: "3 3"
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
"text",
|
|
156
|
+
{
|
|
157
|
+
x: padLeft - 8,
|
|
158
|
+
y: y + 3,
|
|
159
|
+
textAnchor: "end",
|
|
160
|
+
fontSize: 10,
|
|
161
|
+
fill: "currentColor",
|
|
162
|
+
opacity: 0.5,
|
|
163
|
+
className: "font-mono",
|
|
164
|
+
children: val
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
] }, `grid-${i}`);
|
|
168
|
+
}) }),
|
|
169
|
+
categories.map((cat, i) => {
|
|
170
|
+
const x = categories.length === 1 ? padLeft + plotW / 2 : getX(i);
|
|
171
|
+
return /* @__PURE__ */ jsx(
|
|
172
|
+
"text",
|
|
173
|
+
{
|
|
174
|
+
x,
|
|
175
|
+
y: height - 10,
|
|
176
|
+
textAnchor: "middle",
|
|
177
|
+
fontSize: 11,
|
|
178
|
+
fill: "currentColor",
|
|
179
|
+
opacity: 0.6,
|
|
180
|
+
className: "font-mono",
|
|
181
|
+
children: cat
|
|
182
|
+
},
|
|
183
|
+
`cat-${i}`
|
|
184
|
+
);
|
|
185
|
+
})
|
|
186
|
+
] });
|
|
187
|
+
const renderBars = () => {
|
|
188
|
+
const groupW = plotW / categories.length;
|
|
189
|
+
const barWidth = Math.max(6, Math.min(28, groupW * 0.7 / series.length));
|
|
190
|
+
return categories.map((cat, catIdx) => {
|
|
191
|
+
const groupCenterX = padLeft + catIdx * groupW + groupW / 2;
|
|
192
|
+
const startX = groupCenterX - series.length * barWidth / 2;
|
|
193
|
+
return series.map((s, sIdx) => {
|
|
194
|
+
const val = s.data[catIdx] || 0;
|
|
195
|
+
const barH = (val - minVal) / (maxVal - minVal) * plotH;
|
|
196
|
+
const x = startX + sIdx * barWidth;
|
|
197
|
+
const y = padTop + plotH - barH;
|
|
198
|
+
return /* @__PURE__ */ jsx(
|
|
199
|
+
"rect",
|
|
200
|
+
{
|
|
201
|
+
x,
|
|
202
|
+
y,
|
|
203
|
+
width: barWidth - 2,
|
|
204
|
+
height: Math.max(barH, 2),
|
|
205
|
+
rx: 3,
|
|
206
|
+
fill: s.color,
|
|
207
|
+
opacity: 0.9,
|
|
208
|
+
className: "transition-all duration-300 hover:opacity-100 cursor-pointer",
|
|
209
|
+
onMouseEnter: () => setHoveredValue(`${cat} \u2022 ${s.label}: ${val.toLocaleString()}`),
|
|
210
|
+
onMouseLeave: () => setHoveredValue(null)
|
|
211
|
+
},
|
|
212
|
+
`bar-${catIdx}-${sIdx}`
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
const renderLinesOrArea = (isArea) => {
|
|
218
|
+
return series.map((s, sIdx) => {
|
|
219
|
+
const points = s.data.map((val, catIdx) => `${getX(catIdx)},${getY(val)}`).join(" ");
|
|
220
|
+
const areaPoints = `${getX(0)},${padTop + plotH} ${points} ${getX(s.data.length - 1)},${padTop + plotH}`;
|
|
221
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
222
|
+
isArea && /* @__PURE__ */ jsx("polygon", { points: areaPoints, fill: s.color, fillOpacity: 0.15 }),
|
|
223
|
+
/* @__PURE__ */ jsx(
|
|
224
|
+
"polyline",
|
|
225
|
+
{
|
|
226
|
+
points,
|
|
227
|
+
fill: "none",
|
|
228
|
+
stroke: s.color,
|
|
229
|
+
strokeWidth: 2.5,
|
|
230
|
+
strokeLinecap: "round",
|
|
231
|
+
strokeLinejoin: "round"
|
|
232
|
+
}
|
|
233
|
+
),
|
|
234
|
+
s.data.map((val, catIdx) => /* @__PURE__ */ jsx(
|
|
235
|
+
"circle",
|
|
236
|
+
{
|
|
237
|
+
cx: getX(catIdx),
|
|
238
|
+
cy: getY(val),
|
|
239
|
+
r: 4,
|
|
240
|
+
fill: s.color,
|
|
241
|
+
className: "cursor-pointer transition-transform hover:scale-150",
|
|
242
|
+
onMouseEnter: () => setHoveredValue(
|
|
243
|
+
`${categories[catIdx]} \u2022 ${s.label}: ${val.toLocaleString()}`
|
|
244
|
+
),
|
|
245
|
+
onMouseLeave: () => setHoveredValue(null)
|
|
246
|
+
},
|
|
247
|
+
`dot-${catIdx}`
|
|
248
|
+
))
|
|
249
|
+
] }, `series-${sIdx}`);
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
const renderPieOrDonut = () => {
|
|
253
|
+
const s = series[0] || { data: [] };
|
|
254
|
+
const total = s.data.reduce((a, b) => a + (Number(b) || 0), 0) || 1;
|
|
255
|
+
let accumulated = 0;
|
|
256
|
+
const cx = width / 2;
|
|
257
|
+
const cy = height / 2;
|
|
258
|
+
const r = Math.min(cx, cy) - 30;
|
|
259
|
+
const isDonut = chart.chartType === "donut";
|
|
260
|
+
return /* @__PURE__ */ jsxs("g", { transform: `translate(${cx}, ${cy})`, children: [
|
|
261
|
+
categories.map((cat, idx) => {
|
|
262
|
+
const val = s.data[idx] || 0;
|
|
263
|
+
const sliceAngle = val / total * 2 * Math.PI;
|
|
264
|
+
const startAngle = accumulated;
|
|
265
|
+
const endAngle = accumulated + sliceAngle;
|
|
266
|
+
accumulated += sliceAngle;
|
|
267
|
+
const x1 = Math.cos(startAngle) * r;
|
|
268
|
+
const y1 = Math.sin(startAngle) * r;
|
|
269
|
+
const x2 = Math.cos(endAngle) * r;
|
|
270
|
+
const y2 = Math.sin(endAngle) * r;
|
|
271
|
+
const largeArc = sliceAngle > Math.PI ? 1 : 0;
|
|
272
|
+
const color = FALLBACK_CHART_COLORS[idx % FALLBACK_CHART_COLORS.length];
|
|
273
|
+
return /* @__PURE__ */ jsx(
|
|
274
|
+
"path",
|
|
275
|
+
{
|
|
276
|
+
d: `M 0 0 L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`,
|
|
277
|
+
fill: color,
|
|
278
|
+
className: "cursor-pointer transition-transform duration-200 hover:scale-105",
|
|
279
|
+
onMouseEnter: () => setHoveredValue(
|
|
280
|
+
`${cat}: ${val.toLocaleString()} (${Math.round(val / total * 100)}%)`
|
|
281
|
+
),
|
|
282
|
+
onMouseLeave: () => setHoveredValue(null)
|
|
283
|
+
},
|
|
284
|
+
`pie-${idx}`
|
|
285
|
+
);
|
|
286
|
+
}),
|
|
287
|
+
isDonut && /* @__PURE__ */ jsx(
|
|
288
|
+
"circle",
|
|
289
|
+
{
|
|
290
|
+
cx: 0,
|
|
291
|
+
cy: 0,
|
|
292
|
+
r: r * 0.55,
|
|
293
|
+
className: "fill-white dark:fill-zinc-900"
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
] });
|
|
297
|
+
};
|
|
298
|
+
return /* @__PURE__ */ jsxs("figure", { className: "nexus-chart-rendered my-6 w-full rounded-2xl border border-zinc-200 bg-white/70 p-4 backdrop-blur-md dark:border-zinc-800 dark:bg-zinc-900/60", children: [
|
|
299
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
300
|
+
chart.title && /* @__PURE__ */ jsx("figcaption", { className: "text-sm font-semibold text-zinc-900 dark:text-zinc-100", children: chart.title }),
|
|
301
|
+
hoveredValue && /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-medium text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-950/50 px-2 py-0.5 rounded-md", children: hoveredValue })
|
|
302
|
+
] }),
|
|
303
|
+
/* @__PURE__ */ jsx("div", { className: "w-full overflow-hidden text-zinc-700 dark:text-zinc-300", children: /* @__PURE__ */ jsx(
|
|
304
|
+
"svg",
|
|
305
|
+
{
|
|
306
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
307
|
+
className: "w-full h-auto max-h-[360px] overflow-visible",
|
|
308
|
+
children: chart.chartType === "pie" || chart.chartType === "donut" ? renderPieOrDonut() : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
309
|
+
renderGridAndAxes(),
|
|
310
|
+
chart.chartType === "bar" && renderBars(),
|
|
311
|
+
chart.chartType === "line" && renderLinesOrArea(false),
|
|
312
|
+
chart.chartType === "area" && renderLinesOrArea(true)
|
|
313
|
+
] })
|
|
314
|
+
}
|
|
315
|
+
) }),
|
|
316
|
+
chart.showLegend && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-center gap-3 mt-3 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: (chart.chartType === "pie" || chart.chartType === "donut" ? categories : series).map((item, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
317
|
+
/* @__PURE__ */ jsx(
|
|
318
|
+
"span",
|
|
319
|
+
{
|
|
320
|
+
className: "h-2.5 w-2.5 rounded-full",
|
|
321
|
+
style: {
|
|
322
|
+
backgroundColor: item.color || FALLBACK_CHART_COLORS[idx % FALLBACK_CHART_COLORS.length]
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
/* @__PURE__ */ jsx("span", { children: item.label || item })
|
|
327
|
+
] }, `legend-${idx}`)) }),
|
|
328
|
+
chart.sourceNote && /* @__PURE__ */ jsx("figcaption", { className: "mt-2 text-center text-xs text-zinc-400", children: chart.sourceNote })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
function NexusRenderer({
|
|
332
|
+
content,
|
|
333
|
+
className,
|
|
334
|
+
onCommentClick,
|
|
335
|
+
onImageClick,
|
|
336
|
+
hydrateCharts = true,
|
|
337
|
+
enableImageInteraction = true,
|
|
338
|
+
documentClassName
|
|
339
|
+
}) {
|
|
340
|
+
const containerRef = useRef(null);
|
|
341
|
+
const mounted = typeof window !== "undefined";
|
|
342
|
+
const preparedHTML = useMemo(
|
|
343
|
+
() => prepareDocumentHTML(content || ""),
|
|
344
|
+
[content]
|
|
345
|
+
);
|
|
346
|
+
const isEmpty = !content || content.trim() === "" || content.trim() === "<p></p>" || content.trim() === "<p><br></p>";
|
|
347
|
+
const handleDocumentClick = useCallback(
|
|
348
|
+
(event) => {
|
|
349
|
+
const target = event.target;
|
|
350
|
+
if (!(target instanceof Element)) return;
|
|
351
|
+
const commentElement = target.closest("[data-comment-id]");
|
|
352
|
+
if (commentElement && onCommentClick) {
|
|
353
|
+
const commentId = commentElement.getAttribute("data-comment-id");
|
|
354
|
+
if (commentId) {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
onCommentClick(commentId);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (enableImageInteraction && onImageClick && target instanceof HTMLImageElement) {
|
|
361
|
+
const src = target.currentSrc || target.src;
|
|
362
|
+
if (src) {
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
onImageClick(src, target.alt || void 0);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
[enableImageInteraction, onCommentClick, onImageClick]
|
|
369
|
+
);
|
|
370
|
+
const [chartTargets, setChartTargets] = useState([]);
|
|
371
|
+
const updateChartTargets = useCallback(() => {
|
|
372
|
+
if (!hydrateCharts) {
|
|
373
|
+
setChartTargets([]);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const container = containerRef.current;
|
|
377
|
+
if (!container) return;
|
|
378
|
+
setChartTargets(
|
|
379
|
+
Array.from(
|
|
380
|
+
container.querySelectorAll("[data-nexus-chart]")
|
|
381
|
+
).map((element) => ({
|
|
382
|
+
element,
|
|
383
|
+
chart: recoverChartFromElement(element)
|
|
384
|
+
}))
|
|
385
|
+
);
|
|
386
|
+
}, [hydrateCharts]);
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
updateChartTargets();
|
|
389
|
+
}, [preparedHTML, updateChartTargets]);
|
|
390
|
+
if (isEmpty) {
|
|
391
|
+
return /* @__PURE__ */ jsx("p", { className: cn("text-sm italic text-zinc-500", className), children: "No descriptive data provisioned for this node." });
|
|
392
|
+
}
|
|
393
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
394
|
+
/* @__PURE__ */ jsx(
|
|
395
|
+
"article",
|
|
396
|
+
{
|
|
397
|
+
ref: containerRef,
|
|
398
|
+
onClick: handleDocumentClick,
|
|
399
|
+
className: cn(
|
|
400
|
+
"nexus-renderer max-w-none w-full min-w-0 text-[15px] leading-7 md:text-base text-zinc-700 dark:text-zinc-300",
|
|
401
|
+
documentClassName,
|
|
402
|
+
className
|
|
403
|
+
),
|
|
404
|
+
dangerouslySetInnerHTML: { __html: preparedHTML }
|
|
405
|
+
}
|
|
406
|
+
),
|
|
407
|
+
mounted && hydrateCharts && chartTargets.map(
|
|
408
|
+
({ element, chart }, index) => createPortal(
|
|
409
|
+
/* @__PURE__ */ jsx(NativeNexusChart, { chart }, `nexus-chart-${index}`),
|
|
410
|
+
element
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
] });
|
|
414
|
+
}
|
|
5
415
|
|
|
6
416
|
// src/components/NexusProvider.tsx
|
|
7
|
-
import
|
|
417
|
+
import React3, {
|
|
8
418
|
createContext as createContext2,
|
|
9
|
-
useEffect as
|
|
10
|
-
useRef,
|
|
11
|
-
useMemo,
|
|
12
|
-
useState as
|
|
13
|
-
useCallback as
|
|
419
|
+
useEffect as useEffect3,
|
|
420
|
+
useRef as useRef2,
|
|
421
|
+
useMemo as useMemo2,
|
|
422
|
+
useState as useState3,
|
|
423
|
+
useCallback as useCallback3,
|
|
14
424
|
Suspense
|
|
15
425
|
} from "react";
|
|
16
426
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
@@ -282,7 +692,7 @@ var LocalCacheProxy = class {
|
|
|
282
692
|
async getInstance() {
|
|
283
693
|
if (this.instance) return this.instance;
|
|
284
694
|
if (typeof window === "undefined") {
|
|
285
|
-
const mod = await import("./local-cache-server-
|
|
695
|
+
const mod = await import("./local-cache-server-POO2S6OU.js");
|
|
286
696
|
this.instance = new mod.LocalCache(this.cachePath);
|
|
287
697
|
} else {
|
|
288
698
|
const mod = await import("./local-cache-client-I6UYVJJV.js");
|
|
@@ -314,90 +724,69 @@ var LocalCacheProxy = class {
|
|
|
314
724
|
// src/content/strategies.ts
|
|
315
725
|
var RateLimiter = class {
|
|
316
726
|
constructor(config) {
|
|
317
|
-
this.requests = [];
|
|
318
727
|
this.config = config;
|
|
728
|
+
this.requests = [];
|
|
319
729
|
}
|
|
320
730
|
async checkLimit() {
|
|
321
731
|
while (true) {
|
|
322
732
|
const now = Date.now();
|
|
323
|
-
const
|
|
324
|
-
this.requests = this.requests.filter((
|
|
733
|
+
const cutoff = now - this.config.timeWindow;
|
|
734
|
+
this.requests = this.requests.filter((t) => t > cutoff);
|
|
325
735
|
if (this.requests.length < this.config.maxRequests) {
|
|
326
736
|
this.requests.push(now);
|
|
327
737
|
return;
|
|
328
738
|
}
|
|
329
|
-
|
|
330
|
-
const waitTime = oldestRequest + this.config.timeWindow - now;
|
|
331
|
-
if (waitTime > 0) {
|
|
332
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
333
|
-
} else {
|
|
334
|
-
}
|
|
739
|
+
await new Promise((r) => setTimeout(r, Math.max(1, this.requests[0] + this.config.timeWindow - now)));
|
|
335
740
|
}
|
|
336
741
|
}
|
|
337
742
|
getStats() {
|
|
338
743
|
const now = Date.now();
|
|
339
|
-
|
|
340
|
-
const currentRequests = this.requests.filter(
|
|
341
|
-
(time) => time > windowStart
|
|
342
|
-
).length;
|
|
343
|
-
return { currentRequests, limit: this.config.maxRequests };
|
|
744
|
+
return { currentRequests: this.requests.filter((t) => t > now - this.config.timeWindow).length, limit: this.config.maxRequests };
|
|
344
745
|
}
|
|
345
746
|
};
|
|
346
747
|
var ExponentialBackoff = class {
|
|
347
748
|
constructor(config) {
|
|
749
|
+
this.config = config;
|
|
348
750
|
this.config = { jitter: true, ...config };
|
|
349
751
|
}
|
|
350
752
|
async execute(fn, onRetry) {
|
|
351
|
-
let
|
|
352
|
-
for (let
|
|
753
|
+
let last;
|
|
754
|
+
for (let a = 0; a <= this.config.maxRetries; a++) {
|
|
353
755
|
try {
|
|
354
756
|
return await fn();
|
|
355
|
-
} catch (
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
757
|
+
} catch (e) {
|
|
758
|
+
last = e;
|
|
759
|
+
if (e?.code === "ABORTED" || e?.code === "VALIDATION_ERROR" || e?.status >= 400 && e?.status < 500 && e?.status !== 429) throw e;
|
|
760
|
+
if (a === this.config.maxRetries) break;
|
|
761
|
+
let delay = Math.min(this.config.maxDelay, this.config.baseDelay * 2 ** a);
|
|
762
|
+
if (this.config.jitter) delay *= 0.5 + Math.random();
|
|
763
|
+
onRetry?.(a + 1, delay, e);
|
|
764
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
364
765
|
}
|
|
365
766
|
}
|
|
366
|
-
throw
|
|
367
|
-
}
|
|
368
|
-
calculateDelay(attempt) {
|
|
369
|
-
let delay = this.config.baseDelay * Math.pow(2, attempt);
|
|
370
|
-
delay = Math.min(delay, this.config.maxDelay);
|
|
371
|
-
if (this.config.jitter) {
|
|
372
|
-
delay = delay * (0.5 + Math.random());
|
|
373
|
-
}
|
|
374
|
-
return delay;
|
|
375
|
-
}
|
|
376
|
-
isClientError(error) {
|
|
377
|
-
return error?.status >= 400 && error?.status < 500;
|
|
378
|
-
}
|
|
379
|
-
isRateLimitError(error) {
|
|
380
|
-
return error?.status === 429;
|
|
767
|
+
throw last;
|
|
381
768
|
}
|
|
382
769
|
};
|
|
770
|
+
var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
771
|
+
CircuitState2[CircuitState2["CLOSED"] = 0] = "CLOSED";
|
|
772
|
+
CircuitState2[CircuitState2["OPEN"] = 1] = "OPEN";
|
|
773
|
+
CircuitState2[CircuitState2["HALF_OPEN"] = 2] = "HALF_OPEN";
|
|
774
|
+
return CircuitState2;
|
|
775
|
+
})(CircuitState || {});
|
|
383
776
|
var CircuitBreaker = class {
|
|
384
|
-
constructor() {
|
|
777
|
+
constructor(threshold = 5, reset = 3e4) {
|
|
778
|
+
this.threshold = threshold;
|
|
779
|
+
this.reset = reset;
|
|
385
780
|
this.state = 0 /* CLOSED */;
|
|
386
781
|
this.failures = 0;
|
|
387
|
-
this.
|
|
388
|
-
this.failureThreshold = 5;
|
|
389
|
-
this.resetTimeout = 3e4;
|
|
782
|
+
this.lastFailure = 0;
|
|
390
783
|
}
|
|
391
|
-
// 30 seconds
|
|
392
784
|
isOpen() {
|
|
393
|
-
if (this.state === 1 /* OPEN */) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
785
|
+
if (this.state === 1 /* OPEN */ && Date.now() - this.lastFailure >= this.reset) {
|
|
786
|
+
this.state = 2 /* HALF_OPEN */;
|
|
787
|
+
return false;
|
|
399
788
|
}
|
|
400
|
-
return
|
|
789
|
+
return this.state === 1 /* OPEN */;
|
|
401
790
|
}
|
|
402
791
|
recordSuccess() {
|
|
403
792
|
this.failures = 0;
|
|
@@ -405,110 +794,33 @@ var CircuitBreaker = class {
|
|
|
405
794
|
}
|
|
406
795
|
recordFailure() {
|
|
407
796
|
this.failures++;
|
|
408
|
-
this.
|
|
409
|
-
if (this.failures >= this.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
797
|
+
this.lastFailure = Date.now();
|
|
798
|
+
if (this.failures >= this.threshold) this.state = 1 /* OPEN */;
|
|
799
|
+
}
|
|
800
|
+
getState() {
|
|
801
|
+
return CircuitState[this.state];
|
|
417
802
|
}
|
|
418
803
|
};
|
|
419
804
|
var RequestBatcher = class {
|
|
420
|
-
constructor(
|
|
421
|
-
this.
|
|
805
|
+
constructor(windowMs = 10, maxBatchSize = 50) {
|
|
806
|
+
this.windowMs = windowMs;
|
|
422
807
|
this.maxBatchSize = maxBatchSize;
|
|
423
|
-
this.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
} else if (!this.batchTimeout) {
|
|
432
|
-
this.batchTimeout = setTimeout(
|
|
433
|
-
() => this.processBatch(request),
|
|
434
|
-
this.batchWindow
|
|
435
|
-
);
|
|
436
|
-
}
|
|
808
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
809
|
+
}
|
|
810
|
+
schedule(key, request) {
|
|
811
|
+
const existing = this.pending.get(key);
|
|
812
|
+
if (existing) return existing;
|
|
813
|
+
const promise = new Promise((resolve, reject) => setTimeout(() => request().then(resolve, reject), this.windowMs));
|
|
814
|
+
this.pending.set(key, promise);
|
|
815
|
+
promise.finally(() => this.pending.delete(key)).catch(() => {
|
|
437
816
|
});
|
|
817
|
+
return promise;
|
|
438
818
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.processing = true;
|
|
442
|
-
if (this.batchTimeout) {
|
|
443
|
-
clearTimeout(this.batchTimeout);
|
|
444
|
-
this.batchTimeout = void 0;
|
|
445
|
-
}
|
|
446
|
-
const currentBatch = [...this.batch];
|
|
447
|
-
this.batch = [];
|
|
448
|
-
try {
|
|
449
|
-
const result = await request();
|
|
450
|
-
currentBatch.forEach((item) => item.resolve(result));
|
|
451
|
-
} catch (error) {
|
|
452
|
-
currentBatch.forEach((item) => item.reject(error));
|
|
453
|
-
} finally {
|
|
454
|
-
this.processing = false;
|
|
455
|
-
if (this.batch.length > 0) {
|
|
456
|
-
setTimeout(() => this.processBatch(request), 0);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
819
|
+
clear() {
|
|
820
|
+
this.pending.clear();
|
|
459
821
|
}
|
|
460
822
|
};
|
|
461
823
|
|
|
462
|
-
// src/content/utils.ts
|
|
463
|
-
function validateSlug(slug) {
|
|
464
|
-
if (!slug || typeof slug !== "string") {
|
|
465
|
-
throw new Error("Slug must be a non-empty string");
|
|
466
|
-
}
|
|
467
|
-
if (!/^[a-z0-9-_]+$/.test(slug)) {
|
|
468
|
-
throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
function normalizeQuery(query) {
|
|
472
|
-
const normalized = { ...query };
|
|
473
|
-
normalized.page = Math.max(1, normalized.page || 1);
|
|
474
|
-
normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
|
|
475
|
-
normalized.order = normalized.order || "desc";
|
|
476
|
-
if (normalized.page < 1) {
|
|
477
|
-
throw new Error("Page must be greater than 0");
|
|
478
|
-
}
|
|
479
|
-
if (normalized.limit < 1 || normalized.limit > 100) {
|
|
480
|
-
throw new Error("Limit must be between 1 and 100");
|
|
481
|
-
}
|
|
482
|
-
return normalized;
|
|
483
|
-
}
|
|
484
|
-
function buildQueryString(query) {
|
|
485
|
-
const params = new URLSearchParams();
|
|
486
|
-
if (query.page) params.append("page", query.page.toString());
|
|
487
|
-
if (query.limit) params.append("limit", query.limit.toString());
|
|
488
|
-
if (query.sort) params.append("sort", query.sort);
|
|
489
|
-
if (query.order) params.append("order", query.order);
|
|
490
|
-
if (query.search) params.append("search", query.search);
|
|
491
|
-
if (query.include?.length) {
|
|
492
|
-
params.append("include", query.include.join(","));
|
|
493
|
-
}
|
|
494
|
-
if (query.fields?.length) {
|
|
495
|
-
params.append("fields", query.fields.join(","));
|
|
496
|
-
}
|
|
497
|
-
if (query.filter) {
|
|
498
|
-
params.append("filter", JSON.stringify(query.filter));
|
|
499
|
-
}
|
|
500
|
-
return params.toString();
|
|
501
|
-
}
|
|
502
|
-
function measurePerformance(name, fn) {
|
|
503
|
-
const start = performance.now();
|
|
504
|
-
const result = fn();
|
|
505
|
-
const end = performance.now();
|
|
506
|
-
if (process.env.NODE_ENV === "development") {
|
|
507
|
-
console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
|
|
508
|
-
}
|
|
509
|
-
return { result, duration: end - start };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
824
|
// src/content/index.ts
|
|
513
825
|
var ContentEngine = class {
|
|
514
826
|
constructor(config) {
|
|
@@ -541,6 +853,12 @@ var ContentEngine = class {
|
|
|
541
853
|
window.addEventListener("beforeunload", this.cleanup.bind(this));
|
|
542
854
|
}
|
|
543
855
|
}
|
|
856
|
+
/** Update runtime configuration without exposing internal mutation. */
|
|
857
|
+
updateConfig(config) {
|
|
858
|
+
this.config = config;
|
|
859
|
+
this.defaultRevalidate = config.revalidateTime ?? false;
|
|
860
|
+
this.cacheStrategy = config.cacheStrategy || "memory";
|
|
861
|
+
}
|
|
544
862
|
/**
|
|
545
863
|
* Fetch a Single Page with full strategy pipeline
|
|
546
864
|
*/
|
|
@@ -699,7 +1017,7 @@ var ContentEngine = class {
|
|
|
699
1017
|
}
|
|
700
1018
|
}
|
|
701
1019
|
/**
|
|
702
|
-
* Fetch Global Settings
|
|
1020
|
+
* Fetch Global Settings
|
|
703
1021
|
*/
|
|
704
1022
|
async getGlobals(options = {}) {
|
|
705
1023
|
const {
|
|
@@ -751,6 +1069,9 @@ var ContentEngine = class {
|
|
|
751
1069
|
revalidate
|
|
752
1070
|
});
|
|
753
1071
|
if (!res.ok) {
|
|
1072
|
+
if (res.status === 408) {
|
|
1073
|
+
throw new Error("Request timeout");
|
|
1074
|
+
}
|
|
754
1075
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
755
1076
|
}
|
|
756
1077
|
const json = await res.json();
|
|
@@ -799,12 +1120,12 @@ var ContentEngine = class {
|
|
|
799
1120
|
method: "GET",
|
|
800
1121
|
headers: this.getHeaders(),
|
|
801
1122
|
tags: fetchTags,
|
|
802
|
-
// ?? not || — see the constructor comment on defaultRevalidate for
|
|
803
|
-
// why: an explicit `revalidate: 0` on this call must not be
|
|
804
|
-
// discarded in favor of the engine's default.
|
|
805
1123
|
revalidate: options.revalidate ?? this.defaultRevalidate
|
|
806
1124
|
});
|
|
807
1125
|
if (!res.ok) {
|
|
1126
|
+
if (res.status === 408) {
|
|
1127
|
+
throw new Error("Request timeout");
|
|
1128
|
+
}
|
|
808
1129
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
809
1130
|
}
|
|
810
1131
|
const json = await res.json();
|
|
@@ -836,12 +1157,15 @@ var ContentEngine = class {
|
|
|
836
1157
|
headers: this.getHeaders()
|
|
837
1158
|
});
|
|
838
1159
|
if (!res.ok) {
|
|
1160
|
+
if (res.status === 408) {
|
|
1161
|
+
throw new Error("Request timeout");
|
|
1162
|
+
}
|
|
839
1163
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
840
1164
|
}
|
|
841
1165
|
return await res.json();
|
|
842
1166
|
}
|
|
843
1167
|
/**
|
|
844
|
-
* Prefetch content
|
|
1168
|
+
* Prefetch content
|
|
845
1169
|
*/
|
|
846
1170
|
async prefetch(urls) {
|
|
847
1171
|
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
@@ -854,41 +1178,19 @@ var ContentEngine = class {
|
|
|
854
1178
|
}
|
|
855
1179
|
/**
|
|
856
1180
|
* Robust Real-time Subscriptions (SSE) with Auto-Reconnect
|
|
857
|
-
*
|
|
858
|
-
* ⚠️ SCOPE — READ BEFORE RELYING ON THIS FOR "instant" UPDATES:
|
|
859
|
-
* This connects from the BROWSER TAB it's called in, and on message it
|
|
860
|
-
* only clears THIS SDK INSTANCE's in-memory `memoryCache` (via
|
|
861
|
-
* invalidateCache below) in THAT browser tab's JS heap. In a typical
|
|
862
|
-
* Next.js deployment — and Cloudflare specifically, which is stateless
|
|
863
|
-
* per-request at the edge — that is a different process/isolate than the
|
|
864
|
-
* one that will render the NEXT server request for this content. So:
|
|
865
|
-
* - ✅ Useful for: a client component that reads from `nexus.content`
|
|
866
|
-
* directly in the browser and re-renders in place without a page
|
|
867
|
-
* navigation (e.g. a live-updating dashboard widget).
|
|
868
|
-
* - ❌ NOT sufficient for: "user A edits content in the CMS, user B's
|
|
869
|
-
* already-loaded page updates" via a server-rendered page. That
|
|
870
|
-
* requires the backend's /api/revalidate webhook (see
|
|
871
|
-
* content.service.ts `pingNextRevalidateWebhook`) to have actually
|
|
872
|
-
* cleared the *server's* Data Cache, so the NEXT navigation or
|
|
873
|
-
* server request picks up fresh data. This SSE channel does not
|
|
874
|
-
* replace that — it's a complementary, browser-local optimization.
|
|
875
|
-
* If your symptom was "stale content after editing," fix the webhook
|
|
876
|
-
* wiring first; treat this method as an enhancement layered on top.
|
|
877
1181
|
*/
|
|
878
1182
|
subscribeToUpdates(callback) {
|
|
879
1183
|
if (this.isServer || typeof EventSource === "undefined") {
|
|
880
1184
|
if (this.config.debug) {
|
|
881
1185
|
console.warn(
|
|
882
|
-
"[NexusHub] subscribeToUpdates() called in a non-browser environment
|
|
1186
|
+
"[NexusHub] subscribeToUpdates() called in a non-browser environment."
|
|
883
1187
|
);
|
|
884
1188
|
}
|
|
885
1189
|
return () => {
|
|
886
1190
|
};
|
|
887
1191
|
}
|
|
888
1192
|
if (!this.config.apiKey) {
|
|
889
|
-
console.warn(
|
|
890
|
-
"[NexusHub] subscribeToUpdates(): no apiKey configured, the SSE connection will likely be rejected by the backend. Set NEXT_PUBLIC_NEXUS_KEY."
|
|
891
|
-
);
|
|
1193
|
+
console.warn("[NexusHub] subscribeToUpdates(): no apiKey configured.");
|
|
892
1194
|
}
|
|
893
1195
|
let eventSource = null;
|
|
894
1196
|
let retryCount = 0;
|
|
@@ -1052,6 +1354,9 @@ var ContentEngine = class {
|
|
|
1052
1354
|
`Page '${slug}' not found. Check your Dashboard or Seed data.`
|
|
1053
1355
|
);
|
|
1054
1356
|
}
|
|
1357
|
+
if (res.status === 408) {
|
|
1358
|
+
throw new Error("Request timeout");
|
|
1359
|
+
}
|
|
1055
1360
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1056
1361
|
}
|
|
1057
1362
|
const json = await res.json();
|
|
@@ -1086,6 +1391,9 @@ var ContentEngine = class {
|
|
|
1086
1391
|
revalidate
|
|
1087
1392
|
});
|
|
1088
1393
|
if (!res.ok) {
|
|
1394
|
+
if (res.status === 408) {
|
|
1395
|
+
throw new Error("Request timeout");
|
|
1396
|
+
}
|
|
1089
1397
|
throw new Error(`API Error ${res.status}: ${res.statusText}`);
|
|
1090
1398
|
}
|
|
1091
1399
|
const json = await res.json();
|
|
@@ -1172,7 +1480,6 @@ var ContentEngine = class {
|
|
|
1172
1480
|
const response = await fetch(url, {
|
|
1173
1481
|
...fetchOptions,
|
|
1174
1482
|
...nextConfig,
|
|
1175
|
-
// Inject Next.js tags
|
|
1176
1483
|
signal: controller.signal
|
|
1177
1484
|
});
|
|
1178
1485
|
clearTimeout(id);
|
|
@@ -1185,7 +1492,8 @@ var ContentEngine = class {
|
|
|
1185
1492
|
getHeaders() {
|
|
1186
1493
|
const headers = {
|
|
1187
1494
|
"Content-Type": "application/json",
|
|
1188
|
-
"X-Nexus-Client":
|
|
1495
|
+
"X-Nexus-Client": `client-sdk/${this.config.sdkVersion ?? "1.1.0"}`,
|
|
1496
|
+
"X-Nexus-Request-ID": typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
1189
1497
|
"X-Nexus-Project": this.config.projectId
|
|
1190
1498
|
};
|
|
1191
1499
|
if (this.config.apiKey) {
|
|
@@ -1196,13 +1504,22 @@ var ContentEngine = class {
|
|
|
1196
1504
|
isCacheValid(metadata) {
|
|
1197
1505
|
return Date.now() < metadata.expiresAt;
|
|
1198
1506
|
}
|
|
1507
|
+
/**
|
|
1508
|
+
* 🛡️ FIX: Normalizes timeout errors (AbortError, 408, or Timeout messages)
|
|
1509
|
+
* into clean, standardized "Request timeout" errors.
|
|
1510
|
+
*/
|
|
1199
1511
|
normalizeError(error, context) {
|
|
1200
1512
|
if (error instanceof Error) {
|
|
1201
|
-
|
|
1513
|
+
const msg = error.message.toLowerCase();
|
|
1514
|
+
if (error.name === "AbortError" || error.message.includes("408") || msg.includes("timeout") || msg.includes("aborted")) {
|
|
1202
1515
|
return new Error(`${context}: Request timeout`);
|
|
1203
1516
|
}
|
|
1204
1517
|
return error;
|
|
1205
1518
|
}
|
|
1519
|
+
const str = String(error).toLowerCase();
|
|
1520
|
+
if (str.includes("408") || str.includes("timeout") || str.includes("aborted")) {
|
|
1521
|
+
return new Error(`${context}: Request timeout`);
|
|
1522
|
+
}
|
|
1206
1523
|
return new Error(`${context}: ${String(error)}`);
|
|
1207
1524
|
}
|
|
1208
1525
|
cancelRequests() {
|
|
@@ -1221,7 +1538,7 @@ var ContentEngine = class {
|
|
|
1221
1538
|
|
|
1222
1539
|
// src/analytics/fingerprint.ts
|
|
1223
1540
|
var cachedEntropy = null;
|
|
1224
|
-
var getDeviceEntropy = async () => {
|
|
1541
|
+
var getDeviceEntropy = async (allowFingerprinting = true) => {
|
|
1225
1542
|
if (cachedEntropy) return cachedEntropy;
|
|
1226
1543
|
if (typeof window === "undefined") return {};
|
|
1227
1544
|
const nav = window.navigator;
|
|
@@ -1236,7 +1553,7 @@ var getDeviceEntropy = async () => {
|
|
|
1236
1553
|
platform: nav.platform,
|
|
1237
1554
|
language: nav.language,
|
|
1238
1555
|
touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
|
|
1239
|
-
canvas_hash: await generateCanvasHash()
|
|
1556
|
+
canvas_hash: allowFingerprinting ? await generateCanvasHash() : void 0
|
|
1240
1557
|
};
|
|
1241
1558
|
return cachedEntropy;
|
|
1242
1559
|
};
|
|
@@ -1249,7 +1566,6 @@ var generateCanvasHash = async () => {
|
|
|
1249
1566
|
canvas.height = 50;
|
|
1250
1567
|
ctx.textBaseline = "top";
|
|
1251
1568
|
ctx.font = '16px "Arial"';
|
|
1252
|
-
ctx.textBaseline = "alphabetic";
|
|
1253
1569
|
ctx.fillStyle = "#f60";
|
|
1254
1570
|
ctx.fillRect(125, 1, 62, 20);
|
|
1255
1571
|
ctx.fillStyle = "#069";
|
|
@@ -1268,12 +1584,12 @@ var generateCanvasHash = async () => {
|
|
|
1268
1584
|
return "";
|
|
1269
1585
|
}
|
|
1270
1586
|
};
|
|
1271
|
-
var getVisitorId = async () => {
|
|
1587
|
+
var getVisitorId = async (allowFingerprinting = true) => {
|
|
1272
1588
|
if (typeof window === "undefined") return "server_visitor";
|
|
1273
1589
|
const STORAGE_KEY = "nexus_vid";
|
|
1274
1590
|
let vid = localStorage.getItem(STORAGE_KEY);
|
|
1275
1591
|
if (!vid) {
|
|
1276
|
-
const entropy = await getDeviceEntropy();
|
|
1592
|
+
const entropy = await getDeviceEntropy(allowFingerprinting);
|
|
1277
1593
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1278
1594
|
const timestamp = Date.now().toString(36);
|
|
1279
1595
|
const fingerprint = [
|
|
@@ -1281,7 +1597,7 @@ var getVisitorId = async () => {
|
|
|
1281
1597
|
entropy.hardware_concurrency,
|
|
1282
1598
|
entropy.timezone_offset,
|
|
1283
1599
|
entropy.platform,
|
|
1284
|
-
entropy.canvas_hash
|
|
1600
|
+
entropy.canvas_hash || "standard_entropy"
|
|
1285
1601
|
].join("|");
|
|
1286
1602
|
let hash = 0;
|
|
1287
1603
|
for (let i = 0; i < fingerprint.length; i++) {
|
|
@@ -1492,7 +1808,6 @@ var EventStorage = class {
|
|
|
1492
1808
|
var eventStorage = new EventStorage();
|
|
1493
1809
|
|
|
1494
1810
|
// src/analytics/tracker.ts
|
|
1495
|
-
var SDK_VERSION = "0.1.0";
|
|
1496
1811
|
var safeGetItem = (key) => {
|
|
1497
1812
|
try {
|
|
1498
1813
|
return localStorage.getItem(key);
|
|
@@ -1528,6 +1843,8 @@ var Tracker = class {
|
|
|
1528
1843
|
this.visitorId = "";
|
|
1529
1844
|
this.anonymousId = "";
|
|
1530
1845
|
this.isFlushing = false;
|
|
1846
|
+
this.flushPromise = null;
|
|
1847
|
+
this.batchSize = 25;
|
|
1531
1848
|
this.config = config;
|
|
1532
1849
|
this.endpoint = `${this.config.analyticsUrl}/api/collect`;
|
|
1533
1850
|
this.circuitBreaker = new CircuitBreaker();
|
|
@@ -1538,7 +1855,9 @@ var Tracker = class {
|
|
|
1538
1855
|
}
|
|
1539
1856
|
}
|
|
1540
1857
|
async initSession() {
|
|
1541
|
-
this.visitorId = await getVisitorId(
|
|
1858
|
+
this.visitorId = await getVisitorId(
|
|
1859
|
+
this.config.privacy?.fingerprinting ?? false
|
|
1860
|
+
);
|
|
1542
1861
|
let anonId = safeGetItem("nexus_anon_id");
|
|
1543
1862
|
if (!anonId) {
|
|
1544
1863
|
anonId = `anon_${generateUUID().replace(/-/g, "")}`;
|
|
@@ -1551,8 +1870,8 @@ var Tracker = class {
|
|
|
1551
1870
|
const SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
1552
1871
|
const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
|
|
1553
1872
|
if (isExpired) {
|
|
1554
|
-
const
|
|
1555
|
-
sid = `sess_${
|
|
1873
|
+
const uuid2 = generateUUID().replace(/-/g, "").substring(0, 16);
|
|
1874
|
+
sid = `sess_${uuid2}_${now}`;
|
|
1556
1875
|
safeSetItem("nexus_sid", sid);
|
|
1557
1876
|
this.sessionStart = now;
|
|
1558
1877
|
}
|
|
@@ -1562,7 +1881,9 @@ var Tracker = class {
|
|
|
1562
1881
|
async send(eventType, data = {}, eventName, ecommerce) {
|
|
1563
1882
|
if (typeof window === "undefined") return;
|
|
1564
1883
|
safeSetItem("nexus_last_active", Date.now().toString());
|
|
1565
|
-
const entropy = await getDeviceEntropy(
|
|
1884
|
+
const entropy = await getDeviceEntropy(
|
|
1885
|
+
this.config.privacy?.fingerprinting ?? false
|
|
1886
|
+
);
|
|
1566
1887
|
const perfMetrics = vitalsCollector.getMetricsSnapshot();
|
|
1567
1888
|
const utmParams = extractUtmParams(window.location.href);
|
|
1568
1889
|
const payload = {
|
|
@@ -1573,6 +1894,7 @@ var Tracker = class {
|
|
|
1573
1894
|
messageId: generateUUID(),
|
|
1574
1895
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1575
1896
|
version: SDK_VERSION,
|
|
1897
|
+
// Dynamically synced to SDK 1.1.0
|
|
1576
1898
|
url: window.location.href,
|
|
1577
1899
|
referrer: document.referrer,
|
|
1578
1900
|
userAgent: window.navigator.userAgent,
|
|
@@ -1590,8 +1912,8 @@ var Tracker = class {
|
|
|
1590
1912
|
hardwareConcurrency: entropy.hardware_concurrency,
|
|
1591
1913
|
deviceMemory: entropy.device_memory,
|
|
1592
1914
|
pixelRatio: entropy.pixel_ratio,
|
|
1593
|
-
|
|
1594
|
-
|
|
1915
|
+
platform: entropy.platform,
|
|
1916
|
+
...this.config.privacy?.fingerprinting ? { canvasFingerprint: entropy.canvas_hash } : {}
|
|
1595
1917
|
},
|
|
1596
1918
|
visitorIdLocal: this.visitorId,
|
|
1597
1919
|
utm: utmParams
|
|
@@ -1611,53 +1933,69 @@ var Tracker = class {
|
|
|
1611
1933
|
});
|
|
1612
1934
|
}
|
|
1613
1935
|
async flushQueue(useBeacon = false) {
|
|
1614
|
-
if (this.
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1936
|
+
if (this.flushPromise) return this.flushPromise;
|
|
1937
|
+
this.flushPromise = (async () => {
|
|
1938
|
+
if (this.circuitBreaker.isOpen()) return;
|
|
1939
|
+
try {
|
|
1940
|
+
const storedEvents = await eventStorage.peek(this.batchSize);
|
|
1941
|
+
if (!storedEvents.length) return;
|
|
1942
|
+
const payloads = storedEvents.map((e) => e.payload);
|
|
1943
|
+
const headers = {
|
|
1944
|
+
"Content-Type": "application/json",
|
|
1945
|
+
Authorization: this.config.apiKey ? `Bearer ${this.config.apiKey}` : "",
|
|
1946
|
+
"X-Nexus-Client": `analytics/${SDK_VERSION}`,
|
|
1947
|
+
"X-Nexus-Project": this.config.projectId
|
|
1948
|
+
};
|
|
1949
|
+
let response;
|
|
1950
|
+
try {
|
|
1951
|
+
response = await fetch(
|
|
1952
|
+
`${this.config.analyticsUrl}/api/collect/batch`,
|
|
1953
|
+
{
|
|
1954
|
+
method: "POST",
|
|
1955
|
+
headers,
|
|
1956
|
+
body: JSON.stringify({ events: payloads }),
|
|
1957
|
+
keepalive: useBeacon
|
|
1958
|
+
}
|
|
1959
|
+
);
|
|
1960
|
+
} catch {
|
|
1961
|
+
response = void 0;
|
|
1962
|
+
}
|
|
1963
|
+
if (!response || response.status === 404 || response.status === 405) {
|
|
1964
|
+
const results = await Promise.allSettled(
|
|
1965
|
+
payloads.map(
|
|
1966
|
+
(event) => fetch(this.endpoint, {
|
|
1967
|
+
method: "POST",
|
|
1968
|
+
headers,
|
|
1969
|
+
body: JSON.stringify(event),
|
|
1970
|
+
keepalive: useBeacon
|
|
1971
|
+
})
|
|
1972
|
+
)
|
|
1973
|
+
);
|
|
1974
|
+
const successIds = results.flatMap(
|
|
1975
|
+
(r, i) => r.status === "fulfilled" && r.value.ok ? [storedEvents[i].id] : []
|
|
1976
|
+
);
|
|
1977
|
+
if (successIds.length) await eventStorage.remove(successIds);
|
|
1978
|
+
if (successIds.length === storedEvents.length) {
|
|
1979
|
+
this.circuitBreaker.recordSuccess();
|
|
1980
|
+
} else {
|
|
1981
|
+
this.circuitBreaker.recordFailure();
|
|
1982
|
+
}
|
|
1983
|
+
} else if (response.ok) {
|
|
1984
|
+
await eventStorage.remove(storedEvents.map((e) => e.id));
|
|
1985
|
+
this.circuitBreaker.recordSuccess();
|
|
1641
1986
|
} else {
|
|
1642
|
-
|
|
1987
|
+
this.circuitBreaker.recordFailure();
|
|
1643
1988
|
}
|
|
1644
|
-
})
|
|
1645
|
-
if (successIds.length > 0) {
|
|
1646
|
-
await eventStorage.remove(successIds);
|
|
1647
|
-
this.circuitBreaker.recordSuccess();
|
|
1648
|
-
}
|
|
1649
|
-
if (failureCount > 0) {
|
|
1989
|
+
} catch (err) {
|
|
1650
1990
|
this.circuitBreaker.recordFailure();
|
|
1991
|
+
if (this.config.debug) {
|
|
1992
|
+
console.warn("[GN-Apex] Analytics flush deferred:", err);
|
|
1993
|
+
}
|
|
1994
|
+
} finally {
|
|
1995
|
+
this.flushPromise = null;
|
|
1651
1996
|
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
this.circuitBreaker.recordFailure();
|
|
1655
|
-
} finally {
|
|
1656
|
-
this.isFlushing = false;
|
|
1657
|
-
if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
|
|
1658
|
-
setTimeout(() => this.flushQueue(), 100);
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1997
|
+
})();
|
|
1998
|
+
return this.flushPromise;
|
|
1661
1999
|
}
|
|
1662
2000
|
getSession() {
|
|
1663
2001
|
return this.sessionId;
|
|
@@ -1936,70 +2274,35 @@ var AnalyticsEngine = class {
|
|
|
1936
2274
|
}
|
|
1937
2275
|
setupVideoTracking() {
|
|
1938
2276
|
if (typeof document === "undefined") return;
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
});
|
|
1951
|
-
video.addEventListener("
|
|
1952
|
-
this.tracker.send(
|
|
1953
|
-
"custom_event",
|
|
1954
|
-
{
|
|
1955
|
-
event_name: "video_pause",
|
|
1956
|
-
src,
|
|
1957
|
-
position_seconds: Math.round(video.currentTime)
|
|
1958
|
-
},
|
|
1959
|
-
"video_pause"
|
|
1960
|
-
);
|
|
1961
|
-
});
|
|
2277
|
+
const attach = (video) => {
|
|
2278
|
+
const v = video;
|
|
2279
|
+
if (v.__gnexusTracked) return;
|
|
2280
|
+
v.__gnexusTracked = true;
|
|
2281
|
+
v.__gnexusMilestones = /* @__PURE__ */ new Set();
|
|
2282
|
+
const src = () => video.currentSrc || video.src || "unknown";
|
|
2283
|
+
const emit = (name, data = {}) => this.tracker.send("media", { media_type: "video", event_name: name, src: src(), ...data }, name);
|
|
2284
|
+
video.addEventListener("loadedmetadata", () => emit("video_loaded", { duration_seconds: Number.isFinite(video.duration) ? Math.round(video.duration) : void 0 }));
|
|
2285
|
+
video.addEventListener("play", () => emit("video_play", { position_seconds: Math.round(video.currentTime) }));
|
|
2286
|
+
video.addEventListener("pause", () => emit("video_pause", { position_seconds: Math.round(video.currentTime) }));
|
|
2287
|
+
video.addEventListener("seeking", () => emit("video_seek", { position_seconds: Math.round(video.currentTime) }));
|
|
2288
|
+
video.addEventListener("ended", () => emit("video_complete", { duration_seconds: Math.round(video.duration) }));
|
|
2289
|
+
video.addEventListener("error", () => emit("video_error"));
|
|
1962
2290
|
video.addEventListener("timeupdate", () => {
|
|
1963
|
-
if (!video.duration || video.duration
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
{
|
|
1970
|
-
event_name: "video_50_percent",
|
|
1971
|
-
src
|
|
1972
|
-
},
|
|
1973
|
-
"video_50_percent"
|
|
1974
|
-
);
|
|
2291
|
+
if (!Number.isFinite(video.duration) || video.duration <= 0) return;
|
|
2292
|
+
for (const milestone of [25, 50, 75, 90, 100]) {
|
|
2293
|
+
if (video.currentTime / video.duration * 100 >= milestone && !v.__gnexusMilestones.has(milestone)) {
|
|
2294
|
+
v.__gnexusMilestones.add(milestone);
|
|
2295
|
+
emit(`video_${milestone}_percent`, { progress: milestone / 100 });
|
|
2296
|
+
}
|
|
1975
2297
|
}
|
|
1976
2298
|
});
|
|
1977
|
-
video.addEventListener("ended", () => {
|
|
1978
|
-
this.tracker.send(
|
|
1979
|
-
"custom_event",
|
|
1980
|
-
{
|
|
1981
|
-
event_name: "video_complete",
|
|
1982
|
-
src,
|
|
1983
|
-
duration_seconds: Math.round(video.duration)
|
|
1984
|
-
},
|
|
1985
|
-
"video_complete"
|
|
1986
|
-
);
|
|
1987
|
-
});
|
|
1988
2299
|
};
|
|
1989
|
-
document.querySelectorAll("video").forEach((v) =>
|
|
1990
|
-
const observer = new MutationObserver((
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
}
|
|
1996
|
-
if (node instanceof Element) {
|
|
1997
|
-
node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
|
|
1998
|
-
}
|
|
1999
|
-
});
|
|
2000
|
-
});
|
|
2001
|
-
});
|
|
2002
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
2300
|
+
document.querySelectorAll("video").forEach((v) => attach(v));
|
|
2301
|
+
const observer = new MutationObserver((ms) => ms.forEach((m) => m.addedNodes.forEach((n) => {
|
|
2302
|
+
if (n instanceof HTMLVideoElement) attach(n);
|
|
2303
|
+
if (n instanceof Element) n.querySelectorAll("video").forEach((v) => attach(v));
|
|
2304
|
+
})));
|
|
2305
|
+
if (document.body) observer.observe(document.body, { childList: true, subtree: true });
|
|
2003
2306
|
this.cleanupFns.push(() => observer.disconnect());
|
|
2004
2307
|
}
|
|
2005
2308
|
setupErrorTracking() {
|
|
@@ -2188,57 +2491,303 @@ function getSelector(el, depth = 0) {
|
|
|
2188
2491
|
return `${parent}${self}`;
|
|
2189
2492
|
}
|
|
2190
2493
|
|
|
2494
|
+
// src/errors.ts
|
|
2495
|
+
var NexusError = class extends Error {
|
|
2496
|
+
constructor(message, code = "UNKNOWN", status, requestId, details, retryable = false, cause) {
|
|
2497
|
+
super(message);
|
|
2498
|
+
this.code = code;
|
|
2499
|
+
this.status = status;
|
|
2500
|
+
this.requestId = requestId;
|
|
2501
|
+
this.details = details;
|
|
2502
|
+
this.retryable = retryable;
|
|
2503
|
+
this.cause = cause;
|
|
2504
|
+
this.name = "NexusError";
|
|
2505
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2509
|
+
// src/events.ts
|
|
2510
|
+
var NexusEventBus = class {
|
|
2511
|
+
constructor() {
|
|
2512
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
2513
|
+
}
|
|
2514
|
+
on(event, listener) {
|
|
2515
|
+
const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
2516
|
+
set.add(listener);
|
|
2517
|
+
this.listeners.set(event, set);
|
|
2518
|
+
return () => set.delete(listener);
|
|
2519
|
+
}
|
|
2520
|
+
emit(event, payload) {
|
|
2521
|
+
this.listeners.get(event)?.forEach((l) => {
|
|
2522
|
+
try {
|
|
2523
|
+
l(payload);
|
|
2524
|
+
} catch {
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
clear() {
|
|
2529
|
+
this.listeners.clear();
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// src/http.ts
|
|
2534
|
+
var uuid = () => typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `nx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
2535
|
+
var NexusHttpClient = class {
|
|
2536
|
+
constructor(config, events = new NexusEventBus()) {
|
|
2537
|
+
this.config = config;
|
|
2538
|
+
this.events = events;
|
|
2539
|
+
this.breaker = new CircuitBreaker();
|
|
2540
|
+
this.limiter = new RateLimiter({
|
|
2541
|
+
maxRequests: config.debug ? 200 : 100,
|
|
2542
|
+
timeWindow: 6e4
|
|
2543
|
+
});
|
|
2544
|
+
this.backoff = new ExponentialBackoff({
|
|
2545
|
+
maxRetries: config.retries ?? 3,
|
|
2546
|
+
baseDelay: 150,
|
|
2547
|
+
maxDelay: 8e3,
|
|
2548
|
+
jitter: true
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
updateConfig(config) {
|
|
2552
|
+
this.config = config;
|
|
2553
|
+
}
|
|
2554
|
+
async request(input, options = {}) {
|
|
2555
|
+
if (this.breaker.isOpen()) {
|
|
2556
|
+
throw new NexusError(
|
|
2557
|
+
"GN-Apex service circuit is open",
|
|
2558
|
+
"CIRCUIT_OPEN",
|
|
2559
|
+
void 0,
|
|
2560
|
+
void 0,
|
|
2561
|
+
void 0,
|
|
2562
|
+
true
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
const requestId = uuid();
|
|
2566
|
+
const url = input;
|
|
2567
|
+
const started = Date.now();
|
|
2568
|
+
const headers = new Headers(options.headers);
|
|
2569
|
+
headers.set("X-Nexus-Request-ID", requestId);
|
|
2570
|
+
headers.set(
|
|
2571
|
+
"X-Nexus-Client",
|
|
2572
|
+
`gnexus-sdk/${this.config.sdkVersion ?? "1.1.0"}`
|
|
2573
|
+
);
|
|
2574
|
+
headers.set("X-Nexus-Project", this.config.projectId);
|
|
2575
|
+
if (this.config.apiKey)
|
|
2576
|
+
headers.set("Authorization", `Bearer ${this.config.apiKey}`);
|
|
2577
|
+
this.events.emit("request:start", {
|
|
2578
|
+
requestId,
|
|
2579
|
+
url,
|
|
2580
|
+
method: options.method ?? "GET"
|
|
2581
|
+
});
|
|
2582
|
+
try {
|
|
2583
|
+
await this.limiter.checkLimit();
|
|
2584
|
+
const response = await this.backoff.execute(async () => {
|
|
2585
|
+
const controller = new AbortController();
|
|
2586
|
+
const timeout = options.timeout ?? this.config.timeout ?? 1e4;
|
|
2587
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
2588
|
+
try {
|
|
2589
|
+
return await fetch(url, {
|
|
2590
|
+
...options,
|
|
2591
|
+
headers,
|
|
2592
|
+
signal: options.signal ?? controller.signal
|
|
2593
|
+
});
|
|
2594
|
+
} catch (e) {
|
|
2595
|
+
if (e?.name === "AbortError") {
|
|
2596
|
+
throw new NexusError(
|
|
2597
|
+
`Request timed out after ${timeout}ms`,
|
|
2598
|
+
"TIMEOUT",
|
|
2599
|
+
void 0,
|
|
2600
|
+
requestId,
|
|
2601
|
+
void 0,
|
|
2602
|
+
true,
|
|
2603
|
+
e
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
throw new NexusError(
|
|
2607
|
+
"Network request failed",
|
|
2608
|
+
"NETWORK_ERROR",
|
|
2609
|
+
void 0,
|
|
2610
|
+
requestId,
|
|
2611
|
+
void 0,
|
|
2612
|
+
true,
|
|
2613
|
+
e
|
|
2614
|
+
);
|
|
2615
|
+
} finally {
|
|
2616
|
+
clearTimeout(timer);
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
if (!response.ok) {
|
|
2620
|
+
const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
2621
|
+
let details;
|
|
2622
|
+
try {
|
|
2623
|
+
details = await response.clone().json();
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
const code = response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR";
|
|
2627
|
+
const err = new NexusError(
|
|
2628
|
+
`API request failed (${response.status})`,
|
|
2629
|
+
code,
|
|
2630
|
+
response.status,
|
|
2631
|
+
response.headers.get("x-nexus-request-id") ?? requestId,
|
|
2632
|
+
details,
|
|
2633
|
+
retryable
|
|
2634
|
+
);
|
|
2635
|
+
throw err;
|
|
2636
|
+
}
|
|
2637
|
+
this.breaker.recordSuccess();
|
|
2638
|
+
this.events.emit("request:end", {
|
|
2639
|
+
requestId,
|
|
2640
|
+
url,
|
|
2641
|
+
status: response.status,
|
|
2642
|
+
duration: Date.now() - started
|
|
2643
|
+
});
|
|
2644
|
+
return response;
|
|
2645
|
+
} catch (e) {
|
|
2646
|
+
if (e instanceof NexusError) {
|
|
2647
|
+
if (e.retryable || e.status && (e.status >= 500 || e.status === 429)) {
|
|
2648
|
+
this.breaker.recordFailure();
|
|
2649
|
+
} else {
|
|
2650
|
+
this.breaker.recordSuccess();
|
|
2651
|
+
}
|
|
2652
|
+
} else {
|
|
2653
|
+
this.breaker.recordFailure();
|
|
2654
|
+
}
|
|
2655
|
+
this.events.emit("error", { error: e });
|
|
2656
|
+
throw e;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
getStats() {
|
|
2660
|
+
return {
|
|
2661
|
+
rateLimit: this.limiter.getStats(),
|
|
2662
|
+
circuit: this.breaker.getState()
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
|
|
2667
|
+
// src/flags.ts
|
|
2668
|
+
var FeatureFlags = class {
|
|
2669
|
+
constructor(initial) {
|
|
2670
|
+
this.values = {};
|
|
2671
|
+
this.values = { ...initial };
|
|
2672
|
+
}
|
|
2673
|
+
set(values) {
|
|
2674
|
+
this.values = { ...this.values, ...values };
|
|
2675
|
+
}
|
|
2676
|
+
isEnabled(key, fallback = false) {
|
|
2677
|
+
const v = this.values[key];
|
|
2678
|
+
return typeof v === "boolean" ? v : fallback;
|
|
2679
|
+
}
|
|
2680
|
+
get(key, fallback) {
|
|
2681
|
+
return this.values[key] ?? fallback;
|
|
2682
|
+
}
|
|
2683
|
+
all() {
|
|
2684
|
+
return { ...this.values };
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
var RemoteConfig = class {
|
|
2688
|
+
constructor() {
|
|
2689
|
+
this.values = {};
|
|
2690
|
+
}
|
|
2691
|
+
set(values) {
|
|
2692
|
+
this.values = { ...this.values, ...values };
|
|
2693
|
+
}
|
|
2694
|
+
get(key, fallback) {
|
|
2695
|
+
return this.values[key] ?? fallback;
|
|
2696
|
+
}
|
|
2697
|
+
all() {
|
|
2698
|
+
return { ...this.values };
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
|
|
2702
|
+
// src/diagnostics.ts
|
|
2703
|
+
var getDiagnostics = (version, environment) => ({ version, environment, online: typeof navigator === "undefined" ? true : navigator.onLine, userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent, memory: typeof performance !== "undefined" && "memory" in performance ? performance.memory?.usedJSHeapSize : void 0 });
|
|
2704
|
+
|
|
2191
2705
|
// src/client.ts
|
|
2192
|
-
var DEFAULT_REVALIDATE_SECONDS = false;
|
|
2193
2706
|
var NexusClient = class {
|
|
2194
|
-
constructor(config) {
|
|
2195
|
-
const
|
|
2707
|
+
constructor(config = {}) {
|
|
2708
|
+
const full = getFullConfig(config);
|
|
2196
2709
|
this.config = {
|
|
2197
|
-
debug:
|
|
2198
|
-
cacheStrategy:
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2710
|
+
debug: false,
|
|
2711
|
+
cacheStrategy: "memory",
|
|
2712
|
+
revalidateTime: false,
|
|
2713
|
+
timeout: 1e4,
|
|
2714
|
+
retries: 3,
|
|
2715
|
+
sdkVersion: SDK_VERSION,
|
|
2716
|
+
cacheInvalidation: "platform",
|
|
2717
|
+
environment: typeof process !== "undefined" && process.env?.NODE_ENV === "development" ? "development" : "production",
|
|
2718
|
+
autoTracking: true,
|
|
2719
|
+
publicKeyOnly: true,
|
|
2720
|
+
// 🚀 Default: fingerprinting: true (Active by default for high-precision analytics)
|
|
2721
|
+
privacy: { analytics: true, fingerprinting: true, redact: true },
|
|
2722
|
+
...full,
|
|
2723
|
+
...config
|
|
2209
2724
|
};
|
|
2210
2725
|
const errors = validateConfig(this.config);
|
|
2211
|
-
if (errors.length
|
|
2212
|
-
console.warn("
|
|
2213
|
-
if (!this.config.projectId) {
|
|
2214
|
-
console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
|
|
2215
|
-
}
|
|
2726
|
+
if (errors.length && this.config.debug) {
|
|
2727
|
+
console.warn("[GN-Apex] Configuration warnings:", errors);
|
|
2216
2728
|
}
|
|
2729
|
+
this.events = new NexusEventBus();
|
|
2730
|
+
this.http = new NexusHttpClient(this.config, this.events);
|
|
2731
|
+
this.flags = new FeatureFlags();
|
|
2732
|
+
this.remoteConfig = new RemoteConfig();
|
|
2217
2733
|
this.content = new ContentEngine(this.config);
|
|
2218
|
-
if (typeof window !== "undefined") {
|
|
2734
|
+
if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false) {
|
|
2219
2735
|
this.analytics = new AnalyticsEngine(this.config);
|
|
2220
2736
|
this.analytics.start();
|
|
2221
2737
|
}
|
|
2222
2738
|
}
|
|
2223
2739
|
/**
|
|
2224
|
-
* Helper alias for cleaner content fetching.
|
|
2740
|
+
* Helper alias for cleaner page content fetching.
|
|
2225
2741
|
*/
|
|
2226
2742
|
getPage(slug, options) {
|
|
2227
2743
|
return this.content.getPage(slug, options);
|
|
2228
2744
|
}
|
|
2229
2745
|
/**
|
|
2230
|
-
* Returns a readonly snapshot of the
|
|
2746
|
+
* Returns a readonly snapshot of the active configuration.
|
|
2231
2747
|
*/
|
|
2232
2748
|
getConfig() {
|
|
2233
|
-
return {
|
|
2749
|
+
return Object.freeze({
|
|
2750
|
+
...this.config,
|
|
2751
|
+
privacy: { ...this.config.privacy }
|
|
2752
|
+
});
|
|
2234
2753
|
}
|
|
2235
2754
|
/**
|
|
2236
|
-
* Updates
|
|
2237
|
-
* Replaces the previous pattern of `(nexus as any).config.projectId = x`
|
|
2238
|
-
* which bypassed TypeScript and mutated internal state unsafely.
|
|
2755
|
+
* Updates runtime configuration dynamically without breaking active listeners.
|
|
2239
2756
|
*/
|
|
2240
2757
|
updateConfig(updates) {
|
|
2241
|
-
this.config = {
|
|
2758
|
+
this.config = {
|
|
2759
|
+
...this.config,
|
|
2760
|
+
...updates,
|
|
2761
|
+
privacy: { ...this.config.privacy, ...updates.privacy }
|
|
2762
|
+
};
|
|
2763
|
+
this.http.updateConfig(this.config);
|
|
2764
|
+
this.content.updateConfig?.(this.config);
|
|
2765
|
+
if (typeof window !== "undefined" && this.config.autoTracking !== false && this.config.privacy?.analytics !== false && !this.analytics) {
|
|
2766
|
+
this.analytics = new AnalyticsEngine(this.config);
|
|
2767
|
+
this.analytics.start();
|
|
2768
|
+
}
|
|
2769
|
+
if (this.config.privacy?.analytics === false) {
|
|
2770
|
+
this.analytics?.stop();
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Returns deep system diagnostics including HTTP and cache performance.
|
|
2775
|
+
*/
|
|
2776
|
+
diagnostics() {
|
|
2777
|
+
return {
|
|
2778
|
+
...getDiagnostics(SDK_VERSION, this.config.environment ?? "production"),
|
|
2779
|
+
cache: this.content.getCacheStats(),
|
|
2780
|
+
http: this.http.getStats(),
|
|
2781
|
+
analyticsQueue: this.analytics ? void 0 : 0
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Gracefully terminates background tasks and cleans up listeners.
|
|
2786
|
+
*/
|
|
2787
|
+
destroy() {
|
|
2788
|
+
this.analytics?.stop(true);
|
|
2789
|
+
this.content.cleanup?.();
|
|
2790
|
+
this.events.clear();
|
|
2242
2791
|
}
|
|
2243
2792
|
};
|
|
2244
2793
|
var nexus = new NexusClient();
|
|
@@ -2247,21 +2796,21 @@ var nexus = new NexusClient();
|
|
|
2247
2796
|
import {
|
|
2248
2797
|
createContext,
|
|
2249
2798
|
useContext,
|
|
2250
|
-
useEffect,
|
|
2251
|
-
useState,
|
|
2252
|
-
useCallback
|
|
2799
|
+
useEffect as useEffect2,
|
|
2800
|
+
useState as useState2,
|
|
2801
|
+
useCallback as useCallback2
|
|
2253
2802
|
} from "react";
|
|
2254
|
-
import { jsx } from "react/jsx-runtime";
|
|
2803
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
2255
2804
|
var AuthContext = createContext(null);
|
|
2256
2805
|
var AuthProvider = ({
|
|
2257
2806
|
children,
|
|
2258
2807
|
config
|
|
2259
2808
|
}) => {
|
|
2260
|
-
const [user, setUser] =
|
|
2261
|
-
const [isLoading, setIsLoading] =
|
|
2262
|
-
const [error, setError] =
|
|
2809
|
+
const [user, setUser] = useState2(null);
|
|
2810
|
+
const [isLoading, setIsLoading] = useState2(true);
|
|
2811
|
+
const [error, setError] = useState2(null);
|
|
2263
2812
|
const AUTH_BASE = `${config.apiUrl}/auth/project/${config.projectId}`;
|
|
2264
|
-
const checkSession =
|
|
2813
|
+
const checkSession = useCallback2(async () => {
|
|
2265
2814
|
try {
|
|
2266
2815
|
const res = await fetch(`${AUTH_BASE}/me`, {
|
|
2267
2816
|
headers: getHeaders(config)
|
|
@@ -2286,7 +2835,7 @@ var AuthProvider = ({
|
|
|
2286
2835
|
setIsLoading(false);
|
|
2287
2836
|
}
|
|
2288
2837
|
}, [AUTH_BASE, config]);
|
|
2289
|
-
|
|
2838
|
+
useEffect2(() => {
|
|
2290
2839
|
checkSession();
|
|
2291
2840
|
}, [checkSession]);
|
|
2292
2841
|
const login = async (creds) => {
|
|
@@ -2386,7 +2935,7 @@ var AuthProvider = ({
|
|
|
2386
2935
|
});
|
|
2387
2936
|
if (!res.ok) throw await parseError(res);
|
|
2388
2937
|
};
|
|
2389
|
-
return /* @__PURE__ */
|
|
2938
|
+
return /* @__PURE__ */ jsx2(
|
|
2390
2939
|
AuthContext.Provider,
|
|
2391
2940
|
{
|
|
2392
2941
|
value: {
|
|
@@ -2522,27 +3071,27 @@ var NexusPushClient = class {
|
|
|
2522
3071
|
};
|
|
2523
3072
|
|
|
2524
3073
|
// src/components/NexusProvider.tsx
|
|
2525
|
-
import { jsx as
|
|
3074
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2526
3075
|
var NexusContext = createContext2(nexus);
|
|
2527
3076
|
var NexusLiveFeedContext = createContext2({
|
|
2528
3077
|
latestEvent: null,
|
|
2529
3078
|
isConnected: false
|
|
2530
3079
|
});
|
|
2531
|
-
var useNexusLiveFeed = () =>
|
|
3080
|
+
var useNexusLiveFeed = () => React3.useContext(NexusLiveFeedContext);
|
|
2532
3081
|
function NexusAnalyticsTracker({
|
|
2533
3082
|
disableAnalytics
|
|
2534
3083
|
}) {
|
|
2535
3084
|
const pathname = usePathname();
|
|
2536
3085
|
const searchParams = useSearchParams();
|
|
2537
|
-
const prevPathRef =
|
|
3086
|
+
const prevPathRef = useRef2("");
|
|
2538
3087
|
const searchParamsString = searchParams.toString();
|
|
2539
|
-
|
|
3088
|
+
useEffect3(() => {
|
|
2540
3089
|
if (nexus.analytics && !disableAnalytics) {
|
|
2541
3090
|
nexus.analytics.pageView(prevPathRef.current || document.referrer);
|
|
2542
3091
|
prevPathRef.current = pathname;
|
|
2543
3092
|
}
|
|
2544
3093
|
}, [pathname, searchParamsString, disableAnalytics]);
|
|
2545
|
-
return /* @__PURE__ */
|
|
3094
|
+
return /* @__PURE__ */ jsx3("div", { id: "__nexus_react_active", style: { display: "none" } });
|
|
2546
3095
|
}
|
|
2547
3096
|
var NexusProvider = ({
|
|
2548
3097
|
children,
|
|
@@ -2554,23 +3103,23 @@ var NexusProvider = ({
|
|
|
2554
3103
|
autoPromptPush = true
|
|
2555
3104
|
// 🚀 Default: true for zero-config automatic visitor subscription
|
|
2556
3105
|
}) => {
|
|
2557
|
-
const isInitialized =
|
|
2558
|
-
const socketRef =
|
|
2559
|
-
const [latestEvent, setLatestEvent] =
|
|
3106
|
+
const isInitialized = useRef2(false);
|
|
3107
|
+
const socketRef = useRef2(null);
|
|
3108
|
+
const [latestEvent, setLatestEvent] = useState3(
|
|
2560
3109
|
null
|
|
2561
3110
|
);
|
|
2562
|
-
const [isConnected, setIsConnected] =
|
|
2563
|
-
const liveFeedValue =
|
|
3111
|
+
const [isConnected, setIsConnected] = useState3(false);
|
|
3112
|
+
const liveFeedValue = useMemo2(
|
|
2564
3113
|
() => ({ latestEvent, isConnected }),
|
|
2565
3114
|
[latestEvent, isConnected]
|
|
2566
3115
|
);
|
|
2567
|
-
const config =
|
|
3116
|
+
const config = useMemo2(() => {
|
|
2568
3117
|
if (projectId && nexus.getConfig().projectId !== projectId) {
|
|
2569
3118
|
nexus.updateConfig({ projectId });
|
|
2570
3119
|
}
|
|
2571
3120
|
return nexus.getConfig();
|
|
2572
3121
|
}, [projectId]);
|
|
2573
|
-
|
|
3122
|
+
useEffect3(() => {
|
|
2574
3123
|
if (typeof window === "undefined" || !("serviceWorker" in navigator))
|
|
2575
3124
|
return;
|
|
2576
3125
|
navigator.serviceWorker.register("/sw.js").then(async (registration) => {
|
|
@@ -2593,7 +3142,7 @@ var NexusProvider = ({
|
|
|
2593
3142
|
console.warn("[NexusHub] Service Worker registration failed:", err);
|
|
2594
3143
|
});
|
|
2595
3144
|
}, [autoPromptPush]);
|
|
2596
|
-
|
|
3145
|
+
useEffect3(() => {
|
|
2597
3146
|
if (typeof window === "undefined" || disableAnalytics || !hasConsent)
|
|
2598
3147
|
return;
|
|
2599
3148
|
if (!isInitialized.current) {
|
|
@@ -2613,7 +3162,7 @@ var NexusProvider = ({
|
|
|
2613
3162
|
}
|
|
2614
3163
|
};
|
|
2615
3164
|
}, [disableAnalytics, hasConsent]);
|
|
2616
|
-
const connectLiveFeed =
|
|
3165
|
+
const connectLiveFeed = useCallback3(async () => {
|
|
2617
3166
|
if (typeof window === "undefined" || !enableLiveFeed) return;
|
|
2618
3167
|
try {
|
|
2619
3168
|
const { io } = await import("socket.io-client");
|
|
@@ -2643,7 +3192,7 @@ var NexusProvider = ({
|
|
|
2643
3192
|
console.error("[NexusHub] Live feed connection failed:", err);
|
|
2644
3193
|
}
|
|
2645
3194
|
}, [enableLiveFeed, onLiveEvent]);
|
|
2646
|
-
|
|
3195
|
+
useEffect3(() => {
|
|
2647
3196
|
if (enableLiveFeed) {
|
|
2648
3197
|
connectLiveFeed();
|
|
2649
3198
|
}
|
|
@@ -2655,13 +3204,13 @@ var NexusProvider = ({
|
|
|
2655
3204
|
}
|
|
2656
3205
|
};
|
|
2657
3206
|
}, [enableLiveFeed, connectLiveFeed]);
|
|
2658
|
-
return /* @__PURE__ */
|
|
2659
|
-
/* @__PURE__ */
|
|
3207
|
+
return /* @__PURE__ */ jsx3(NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ jsx3(NexusLiveFeedContext.Provider, { value: liveFeedValue, children: /* @__PURE__ */ jsxs2(AuthProvider, { config, children: [
|
|
3208
|
+
/* @__PURE__ */ jsx3(Suspense, { fallback: null, children: /* @__PURE__ */ jsx3(NexusAnalyticsTracker, { disableAnalytics }) }),
|
|
2660
3209
|
children
|
|
2661
3210
|
] }) }) });
|
|
2662
3211
|
};
|
|
2663
3212
|
var useNexus = () => {
|
|
2664
|
-
const context =
|
|
3213
|
+
const context = React3.useContext(NexusContext);
|
|
2665
3214
|
if (!context) {
|
|
2666
3215
|
throw new Error("useNexus must be used within a NexusProvider");
|
|
2667
3216
|
}
|
|
@@ -2696,26 +3245,56 @@ var useNexusAnalytics = () => {
|
|
|
2696
3245
|
};
|
|
2697
3246
|
|
|
2698
3247
|
// src/components/NexusRenders.tsx
|
|
2699
|
-
import { useState as
|
|
3248
|
+
import { useState as useState4 } from "react";
|
|
2700
3249
|
import * as LucideIcons from "lucide-react";
|
|
2701
|
-
import { Fragment, jsx as
|
|
2702
|
-
function
|
|
3250
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
3251
|
+
function sanitizeHtml(html) {
|
|
3252
|
+
if (!html) return "";
|
|
3253
|
+
if (typeof window === "undefined") {
|
|
3254
|
+
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, "").replace(/\bon\w+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, "").replace(/href\s*=\s*['"]\s*javascript:[^'"]*['"]/gi, 'href="#"').replace(/src\s*=\s*['"]\s*javascript:[^'"]*['"]/gi, 'src=""');
|
|
3255
|
+
}
|
|
3256
|
+
const parser = new DOMParser();
|
|
3257
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
3258
|
+
doc.querySelectorAll("script, noscript").forEach((n) => n.remove());
|
|
3259
|
+
doc.querySelectorAll("*").forEach((el) => {
|
|
3260
|
+
[...el.attributes].forEach((attr) => {
|
|
3261
|
+
if (attr.name.toLowerCase().startsWith("on")) {
|
|
3262
|
+
el.removeAttribute(attr.name);
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
const href = el.getAttribute("href");
|
|
3266
|
+
const src = el.getAttribute("src");
|
|
3267
|
+
if (href && /^\s*javascript:/i.test(href)) el.removeAttribute("href");
|
|
3268
|
+
if (src && /^\s*javascript:/i.test(src)) el.removeAttribute("src");
|
|
3269
|
+
});
|
|
3270
|
+
return doc.body.innerHTML;
|
|
3271
|
+
}
|
|
3272
|
+
function NexusRichText({
|
|
3273
|
+
value,
|
|
3274
|
+
className = "",
|
|
3275
|
+
hydrateCharts = true,
|
|
3276
|
+
onCommentClick,
|
|
3277
|
+
onImageClick
|
|
3278
|
+
}) {
|
|
2703
3279
|
if (!value) return null;
|
|
2704
|
-
return /* @__PURE__ */
|
|
2705
|
-
|
|
3280
|
+
return /* @__PURE__ */ jsx4(
|
|
3281
|
+
NexusRenderer,
|
|
2706
3282
|
{
|
|
2707
|
-
|
|
2708
|
-
|
|
3283
|
+
content: value,
|
|
3284
|
+
className,
|
|
3285
|
+
hydrateCharts,
|
|
3286
|
+
onCommentClick,
|
|
3287
|
+
onImageClick
|
|
2709
3288
|
}
|
|
2710
3289
|
);
|
|
2711
3290
|
}
|
|
2712
3291
|
function NexusLongText({ value, className = "" }) {
|
|
2713
3292
|
if (!value) return null;
|
|
2714
|
-
return /* @__PURE__ */
|
|
3293
|
+
return /* @__PURE__ */ jsx4(
|
|
2715
3294
|
"p",
|
|
2716
3295
|
{
|
|
2717
3296
|
className: `text-sm text-muted-foreground whitespace-pre-line leading-relaxed ${className}`,
|
|
2718
|
-
dangerouslySetInnerHTML: { __html: value }
|
|
3297
|
+
dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
|
|
2719
3298
|
}
|
|
2720
3299
|
);
|
|
2721
3300
|
}
|
|
@@ -2729,9 +3308,9 @@ function NexusIcon({
|
|
|
2729
3308
|
const IconComponent = LucideIcons[name];
|
|
2730
3309
|
if (!IconComponent) {
|
|
2731
3310
|
const Fallback = LucideIcons.HelpCircle;
|
|
2732
|
-
return /* @__PURE__ */
|
|
3311
|
+
return /* @__PURE__ */ jsx4(Fallback, { className, size, strokeWidth });
|
|
2733
3312
|
}
|
|
2734
|
-
return /* @__PURE__ */
|
|
3313
|
+
return /* @__PURE__ */ jsx4(
|
|
2735
3314
|
IconComponent,
|
|
2736
3315
|
{
|
|
2737
3316
|
className,
|
|
@@ -2752,11 +3331,13 @@ function NexusImage({
|
|
|
2752
3331
|
if (!imageUrl) return null;
|
|
2753
3332
|
return (
|
|
2754
3333
|
/* eslint-disable-next-line @next/next/no-img-element */
|
|
2755
|
-
/* @__PURE__ */
|
|
3334
|
+
/* @__PURE__ */ jsx4(
|
|
2756
3335
|
"img",
|
|
2757
3336
|
{
|
|
2758
3337
|
src: imageUrl,
|
|
2759
3338
|
alt: imageAlt,
|
|
3339
|
+
loading: "lazy",
|
|
3340
|
+
decoding: "async",
|
|
2760
3341
|
className: `max-w-full h-auto object-cover rounded-xl ${className}`,
|
|
2761
3342
|
...props
|
|
2762
3343
|
}
|
|
@@ -2769,15 +3350,15 @@ function NexusGallery({
|
|
|
2769
3350
|
imageClassName = ""
|
|
2770
3351
|
}) {
|
|
2771
3352
|
if (!value || value.length === 0) return null;
|
|
2772
|
-
return /* @__PURE__ */
|
|
3353
|
+
return /* @__PURE__ */ jsx4(
|
|
2773
3354
|
"div",
|
|
2774
3355
|
{
|
|
2775
3356
|
className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${className}`,
|
|
2776
|
-
children: value.map((img, idx) => /* @__PURE__ */
|
|
3357
|
+
children: value.map((img, idx) => /* @__PURE__ */ jsx4(
|
|
2777
3358
|
"div",
|
|
2778
3359
|
{
|
|
2779
3360
|
className: "overflow-hidden rounded-xl aspect-square bg-muted/20 border border-border",
|
|
2780
|
-
children: /* @__PURE__ */
|
|
3361
|
+
children: /* @__PURE__ */ jsx4(
|
|
2781
3362
|
NexusImage,
|
|
2782
3363
|
{
|
|
2783
3364
|
value: img,
|
|
@@ -2799,8 +3380,8 @@ function NexusVideo({
|
|
|
2799
3380
|
const isYouTube = value.includes("youtube.com") || value.includes("youtu.be");
|
|
2800
3381
|
const isVimeo = value.includes("vimeo.com");
|
|
2801
3382
|
if (isYouTube) {
|
|
2802
|
-
const videoId = value.includes("youtu.be") ? value.split("/").pop() : value.split("v=")[1]?.split("&")[0];
|
|
2803
|
-
return /* @__PURE__ */
|
|
3383
|
+
const videoId = value.includes("youtu.be") ? value.split("/").pop()?.split("?")[0] : value.split("v=")[1]?.split("&")[0];
|
|
3384
|
+
return /* @__PURE__ */ jsx4(
|
|
2804
3385
|
"iframe",
|
|
2805
3386
|
{
|
|
2806
3387
|
src: `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`,
|
|
@@ -2812,8 +3393,8 @@ function NexusVideo({
|
|
|
2812
3393
|
);
|
|
2813
3394
|
}
|
|
2814
3395
|
if (isVimeo) {
|
|
2815
|
-
const videoId = value.split("/").pop();
|
|
2816
|
-
return /* @__PURE__ */
|
|
3396
|
+
const videoId = value.split("/").pop()?.split("?")[0];
|
|
3397
|
+
return /* @__PURE__ */ jsx4(
|
|
2817
3398
|
"iframe",
|
|
2818
3399
|
{
|
|
2819
3400
|
src: `https://player.vimeo.com/video/${videoId}?autoplay=${autoplay ? 1 : 0}`,
|
|
@@ -2824,7 +3405,7 @@ function NexusVideo({
|
|
|
2824
3405
|
}
|
|
2825
3406
|
);
|
|
2826
3407
|
}
|
|
2827
|
-
return /* @__PURE__ */
|
|
3408
|
+
return /* @__PURE__ */ jsx4(
|
|
2828
3409
|
"video",
|
|
2829
3410
|
{
|
|
2830
3411
|
src: value,
|
|
@@ -2842,11 +3423,11 @@ function NexusMap({ value, className = "" }) {
|
|
|
2842
3423
|
value.address || `${value.lat},${value.lng}`
|
|
2843
3424
|
);
|
|
2844
3425
|
const embedUrl = `https://maps.google.com/maps?q=${query}&t=&z=13&ie=UTF8&iwloc=&output=embed`;
|
|
2845
|
-
return /* @__PURE__ */
|
|
3426
|
+
return /* @__PURE__ */ jsx4(
|
|
2846
3427
|
"div",
|
|
2847
3428
|
{
|
|
2848
3429
|
className: `overflow-hidden rounded-xl border border-border aspect-video w-full ${className}`,
|
|
2849
|
-
children: /* @__PURE__ */
|
|
3430
|
+
children: /* @__PURE__ */ jsx4(
|
|
2850
3431
|
"iframe",
|
|
2851
3432
|
{
|
|
2852
3433
|
title: "Embedded Map",
|
|
@@ -2867,15 +3448,15 @@ function NexusColor({
|
|
|
2867
3448
|
showHexLabel = true
|
|
2868
3449
|
}) {
|
|
2869
3450
|
if (!value) return null;
|
|
2870
|
-
return /* @__PURE__ */
|
|
2871
|
-
/* @__PURE__ */
|
|
3451
|
+
return /* @__PURE__ */ jsxs3("div", { className: `flex items-center gap-2.5 ${className}`, children: [
|
|
3452
|
+
/* @__PURE__ */ jsx4(
|
|
2872
3453
|
"div",
|
|
2873
3454
|
{
|
|
2874
3455
|
className: "h-6 w-6 rounded-full border border-border/80 shadow-xs shrink-0",
|
|
2875
3456
|
style: { backgroundColor: value }
|
|
2876
3457
|
}
|
|
2877
3458
|
),
|
|
2878
|
-
showHexLabel && /* @__PURE__ */
|
|
3459
|
+
showHexLabel && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs font-semibold text-foreground/80 uppercase", children: value })
|
|
2879
3460
|
] });
|
|
2880
3461
|
}
|
|
2881
3462
|
function NexusGradient({
|
|
@@ -2884,9 +3465,9 @@ function NexusGradient({
|
|
|
2884
3465
|
className = "",
|
|
2885
3466
|
asTextMask = false
|
|
2886
3467
|
}) {
|
|
2887
|
-
if (!value) return /* @__PURE__ */
|
|
3468
|
+
if (!value) return /* @__PURE__ */ jsx4(Fragment2, { children });
|
|
2888
3469
|
if (asTextMask) {
|
|
2889
|
-
return /* @__PURE__ */
|
|
3470
|
+
return /* @__PURE__ */ jsx4(
|
|
2890
3471
|
"span",
|
|
2891
3472
|
{
|
|
2892
3473
|
className: `bg-clip-text text-transparent font-bold ${className}`,
|
|
@@ -2895,7 +3476,7 @@ function NexusGradient({
|
|
|
2895
3476
|
}
|
|
2896
3477
|
);
|
|
2897
3478
|
}
|
|
2898
|
-
return /* @__PURE__ */
|
|
3479
|
+
return /* @__PURE__ */ jsx4(
|
|
2899
3480
|
"div",
|
|
2900
3481
|
{
|
|
2901
3482
|
className: `rounded-xl ${className}`,
|
|
@@ -2912,26 +3493,26 @@ function NexusAddress({ value, className = "" }) {
|
|
|
2912
3493
|
value.country
|
|
2913
3494
|
].filter(Boolean);
|
|
2914
3495
|
if (lines.length === 0) return null;
|
|
2915
|
-
return /* @__PURE__ */
|
|
3496
|
+
return /* @__PURE__ */ jsxs3(
|
|
2916
3497
|
"div",
|
|
2917
3498
|
{
|
|
2918
3499
|
className: `flex items-start gap-2.5 p-3 rounded-xl border border-border bg-card/50 ${className}`,
|
|
2919
3500
|
children: [
|
|
2920
|
-
/* @__PURE__ */
|
|
2921
|
-
/* @__PURE__ */
|
|
3501
|
+
/* @__PURE__ */ jsx4(LucideIcons.MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0 mt-0.5" }),
|
|
3502
|
+
/* @__PURE__ */ jsx4("div", { className: "text-xs text-foreground/80 leading-relaxed font-medium", children: lines.map((line, i) => /* @__PURE__ */ jsx4("div", { children: line }, i)) })
|
|
2922
3503
|
]
|
|
2923
3504
|
}
|
|
2924
3505
|
);
|
|
2925
3506
|
}
|
|
2926
3507
|
function NexusKeyValue({ value, className = "" }) {
|
|
2927
3508
|
if (!value || Object.keys(value).length === 0) return null;
|
|
2928
|
-
return /* @__PURE__ */
|
|
3509
|
+
return /* @__PURE__ */ jsx4(
|
|
2929
3510
|
"div",
|
|
2930
3511
|
{
|
|
2931
3512
|
className: `border border-border rounded-xl overflow-hidden bg-card/30 divide-y divide-border ${className}`,
|
|
2932
|
-
children: Object.entries(value).map(([k, v]) => /* @__PURE__ */
|
|
2933
|
-
/* @__PURE__ */
|
|
2934
|
-
/* @__PURE__ */
|
|
3513
|
+
children: Object.entries(value).map(([k, v]) => /* @__PURE__ */ jsxs3("div", { className: "flex text-xs px-4 py-3 gap-4", children: [
|
|
3514
|
+
/* @__PURE__ */ jsx4("div", { className: "w-1/3 font-bold text-muted-foreground select-none uppercase tracking-wider text-[10px]", children: k }),
|
|
3515
|
+
/* @__PURE__ */ jsx4("div", { className: "flex-1 font-medium text-foreground", children: v })
|
|
2935
3516
|
] }, k))
|
|
2936
3517
|
}
|
|
2937
3518
|
);
|
|
@@ -2942,7 +3523,7 @@ function NexusTags({
|
|
|
2942
3523
|
badgeClassName = ""
|
|
2943
3524
|
}) {
|
|
2944
3525
|
if (!value || value.length === 0) return null;
|
|
2945
|
-
return /* @__PURE__ */
|
|
3526
|
+
return /* @__PURE__ */ jsx4("div", { className: `flex flex-wrap gap-1.5 ${className}`, children: value.map((tag) => /* @__PURE__ */ jsx4(
|
|
2946
3527
|
"span",
|
|
2947
3528
|
{
|
|
2948
3529
|
className: `inline-flex items-center rounded-lg border border-border bg-muted/40 px-2.5 py-1 text-xs font-semibold text-foreground/80 ${badgeClassName}`,
|
|
@@ -2958,8 +3539,8 @@ function NexusProgress({
|
|
|
2958
3539
|
color = "bg-primary"
|
|
2959
3540
|
}) {
|
|
2960
3541
|
if (value === null) return null;
|
|
2961
|
-
const percent = Math.min(value / max * 100, 100);
|
|
2962
|
-
return /* @__PURE__ */
|
|
3542
|
+
const percent = Math.min(Math.max(value / max * 100, 0), 100);
|
|
3543
|
+
return /* @__PURE__ */ jsx4("div", { className: `w-full ${className}`, children: /* @__PURE__ */ jsx4("div", { className: "h-2 w-full rounded-full bg-muted overflow-hidden border border-border/30", children: /* @__PURE__ */ jsx4(
|
|
2963
3544
|
"div",
|
|
2964
3545
|
{
|
|
2965
3546
|
className: `h-full rounded-full transition-all duration-500 ${color}`,
|
|
@@ -2973,7 +3554,7 @@ function NexusBlendContainer({
|
|
|
2973
3554
|
className = ""
|
|
2974
3555
|
}) {
|
|
2975
3556
|
const blendStyle = mode ? { mixBlendMode: mode } : {};
|
|
2976
|
-
return /* @__PURE__ */
|
|
3557
|
+
return /* @__PURE__ */ jsx4("div", { className, style: blendStyle, children });
|
|
2977
3558
|
}
|
|
2978
3559
|
function NexusCode({
|
|
2979
3560
|
value,
|
|
@@ -2981,39 +3562,54 @@ function NexusCode({
|
|
|
2981
3562
|
className = "",
|
|
2982
3563
|
showLineNumbers = false
|
|
2983
3564
|
}) {
|
|
2984
|
-
const [copied, setCopied] =
|
|
3565
|
+
const [copied, setCopied] = useState4(false);
|
|
2985
3566
|
if (!value) return null;
|
|
2986
3567
|
const handleCopy = () => {
|
|
2987
|
-
navigator.clipboard.
|
|
3568
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
3569
|
+
navigator.clipboard.writeText(value);
|
|
3570
|
+
} else {
|
|
3571
|
+
const textArea = document.createElement("textarea");
|
|
3572
|
+
textArea.value = value;
|
|
3573
|
+
textArea.style.position = "fixed";
|
|
3574
|
+
textArea.style.left = "-999999px";
|
|
3575
|
+
document.body.appendChild(textArea);
|
|
3576
|
+
textArea.focus();
|
|
3577
|
+
textArea.select();
|
|
3578
|
+
try {
|
|
3579
|
+
document.execCommand("copy");
|
|
3580
|
+
} catch {
|
|
3581
|
+
}
|
|
3582
|
+
textArea.remove();
|
|
3583
|
+
}
|
|
2988
3584
|
setCopied(true);
|
|
2989
3585
|
setTimeout(() => setCopied(false), 2e3);
|
|
2990
3586
|
};
|
|
2991
3587
|
const lines = value.split("\n");
|
|
2992
|
-
return /* @__PURE__ */
|
|
3588
|
+
return /* @__PURE__ */ jsxs3(
|
|
2993
3589
|
"div",
|
|
2994
3590
|
{
|
|
2995
3591
|
className: `relative rounded-xl border border-border bg-[#090A0F] overflow-hidden ${className}`,
|
|
2996
3592
|
children: [
|
|
2997
|
-
/* @__PURE__ */
|
|
2998
|
-
/* @__PURE__ */
|
|
2999
|
-
/* @__PURE__ */
|
|
3593
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border bg-white/2 select-none", children: [
|
|
3594
|
+
/* @__PURE__ */ jsx4("span", { className: "text-[10px] font-black uppercase text-muted-foreground tracking-wider", children: language }),
|
|
3595
|
+
/* @__PURE__ */ jsx4(
|
|
3000
3596
|
"button",
|
|
3001
3597
|
{
|
|
3002
3598
|
onClick: handleCopy,
|
|
3003
3599
|
className: "flex items-center gap-1.5 text-[10px] font-bold text-muted-foreground hover:text-primary transition-colors cursor-pointer",
|
|
3004
|
-
children: copied ? /* @__PURE__ */
|
|
3005
|
-
/* @__PURE__ */
|
|
3006
|
-
/* @__PURE__ */
|
|
3007
|
-
] }) : /* @__PURE__ */
|
|
3008
|
-
/* @__PURE__ */
|
|
3009
|
-
/* @__PURE__ */
|
|
3600
|
+
children: copied ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
3601
|
+
/* @__PURE__ */ jsx4(LucideIcons.Check, { className: "h-3 w-3 text-emerald-400" }),
|
|
3602
|
+
/* @__PURE__ */ jsx4("span", { className: "text-emerald-400", children: "Copied!" })
|
|
3603
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
3604
|
+
/* @__PURE__ */ jsx4(LucideIcons.Copy, { className: "h-3 w-3" }),
|
|
3605
|
+
/* @__PURE__ */ jsx4("span", { children: "Copy Code" })
|
|
3010
3606
|
] })
|
|
3011
3607
|
}
|
|
3012
3608
|
)
|
|
3013
3609
|
] }),
|
|
3014
|
-
/* @__PURE__ */
|
|
3015
|
-
showLineNumbers && /* @__PURE__ */
|
|
3016
|
-
/* @__PURE__ */
|
|
3610
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex overflow-x-auto p-4 font-mono text-[11px] leading-relaxed text-emerald-400 select-text", children: [
|
|
3611
|
+
showLineNumbers && /* @__PURE__ */ jsx4("div", { className: "flex flex-col text-right text-gray-600 select-none pr-3.5 border-r border-border/30 mr-3.5", children: lines.map((_, i) => /* @__PURE__ */ jsx4("span", { children: i + 1 }, i)) }),
|
|
3612
|
+
/* @__PURE__ */ jsx4("pre", { className: "flex-1 whitespace-pre", children: value })
|
|
3017
3613
|
] })
|
|
3018
3614
|
]
|
|
3019
3615
|
}
|
|
@@ -3026,12 +3622,12 @@ function NexusBoolean({
|
|
|
3026
3622
|
falseLabel = "Inactive"
|
|
3027
3623
|
}) {
|
|
3028
3624
|
if (value === null) return null;
|
|
3029
|
-
return /* @__PURE__ */
|
|
3625
|
+
return /* @__PURE__ */ jsxs3(
|
|
3030
3626
|
"span",
|
|
3031
3627
|
{
|
|
3032
3628
|
className: `inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-black uppercase tracking-wider border shadow-sm ${value ? "border-emerald-500/20 bg-emerald-500/10 text-emerald-400" : "border-muted-foreground/20 bg-muted-foreground/10 text-muted-foreground"} ${className}`,
|
|
3033
3629
|
children: [
|
|
3034
|
-
/* @__PURE__ */
|
|
3630
|
+
/* @__PURE__ */ jsx4(
|
|
3035
3631
|
"span",
|
|
3036
3632
|
{
|
|
3037
3633
|
className: `h-1.5 w-1.5 rounded-full ${value ? "bg-emerald-500 animate-pulse" : "bg-muted-foreground"}`
|
|
@@ -3058,6 +3654,7 @@ export {
|
|
|
3058
3654
|
NexusMap,
|
|
3059
3655
|
NexusProgress,
|
|
3060
3656
|
NexusProvider,
|
|
3657
|
+
NexusRenderer,
|
|
3061
3658
|
NexusRichText,
|
|
3062
3659
|
NexusTags,
|
|
3063
3660
|
NexusVideo,
|