@emblemvault/identity-mesh 1.0.0

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/dist/index.mjs ADDED
@@ -0,0 +1,632 @@
1
+ // src/IdentityMesh.tsx
2
+ import { useState as useState2, useEffect as useEffect2, useMemo } from "react";
3
+
4
+ // src/hooks/useMeshData.ts
5
+ import { useState, useEffect, useCallback } from "react";
6
+ function useMeshData({
7
+ apiUrl,
8
+ vaultId,
9
+ visitorId,
10
+ onError
11
+ }) {
12
+ const [links, setLinks] = useState([]);
13
+ const [visitorDetails, setVisitorDetails] = useState({});
14
+ const [loading, setLoading] = useState(true);
15
+ const [error, setError] = useState(null);
16
+ const fetchData = useCallback(async () => {
17
+ if (!apiUrl || !vaultId || !visitorId) {
18
+ setLoading(false);
19
+ return;
20
+ }
21
+ setLoading(true);
22
+ setError(null);
23
+ try {
24
+ const response = await fetch(apiUrl, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/json"
28
+ },
29
+ body: JSON.stringify({ vaultId, visitorId })
30
+ });
31
+ const data = await response.json();
32
+ if (!response.ok || !data.success) {
33
+ const err = new Error(data.error || `HTTP ${response.status}`);
34
+ setError(err);
35
+ onError == null ? void 0 : onError(err);
36
+ setLinks([]);
37
+ setVisitorDetails({});
38
+ return;
39
+ }
40
+ if (data.data) {
41
+ setLinks(data.data.links);
42
+ setVisitorDetails(data.data.visitorDetails);
43
+ }
44
+ } catch (err) {
45
+ const error2 = err instanceof Error ? err : new Error("Unknown error");
46
+ setError(error2);
47
+ onError == null ? void 0 : onError(error2);
48
+ setLinks([]);
49
+ setVisitorDetails({});
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ }, [apiUrl, vaultId, visitorId, onError]);
54
+ useEffect(() => {
55
+ fetchData();
56
+ }, [fetchData]);
57
+ return {
58
+ links,
59
+ visitorDetails,
60
+ loading,
61
+ error,
62
+ refetch: fetchData
63
+ };
64
+ }
65
+
66
+ // src/utils.tsx
67
+ import { jsx } from "react/jsx-runtime";
68
+ var defaultNodeTypes = {
69
+ visitor: { color: "#00f0ff", glow: "rgba(0, 240, 255, 0.5)", label: "Visitor", shape: "circle" },
70
+ device: { color: "#8b5cf6", glow: "rgba(139, 92, 246, 0.5)", label: "Device", shape: "circle" },
71
+ vault_account: { color: "#00f0ff", glow: "rgba(0, 240, 255, 0.6)", label: "Vault", shape: "hexagon" },
72
+ wallet: { color: "#a855f7", glow: "rgba(168, 85, 247, 0.6)", label: "Wallet", shape: "diamond" },
73
+ user_account: { color: "#00ff88", glow: "rgba(0, 255, 136, 0.6)", label: "User", shape: "circle" },
74
+ session: { color: "#ffb800", glow: "rgba(255, 184, 0, 0.5)", label: "Session", shape: "triangle" },
75
+ identity_link: { color: "#ff6b6b", glow: "rgba(255, 107, 107, 0.6)", label: "Same Identity", shape: "circle" }
76
+ };
77
+ function getCountryFlag(code) {
78
+ if (!code) return "";
79
+ const codePoints = code.toUpperCase().split("").map((char) => 127397 + char.charCodeAt(0));
80
+ return String.fromCodePoint(...codePoints);
81
+ }
82
+ function getDeviceIcon(platform, touch) {
83
+ const p = (platform == null ? void 0 : platform.toLowerCase()) || "";
84
+ if (p.includes("iphone") || p.includes("android")) return "\u{1F4F1}";
85
+ if (p.includes("ipad")) return "\u{1F4F1}";
86
+ if (touch) return "\u{1F4F1}";
87
+ if (p.includes("mac") || p.includes("win") || p.includes("linux")) return "\u{1F4BB}";
88
+ return "\u{1F5A5}\uFE0F";
89
+ }
90
+ function getBrowserIcon(browser) {
91
+ const b = (browser == null ? void 0 : browser.toLowerCase()) || "";
92
+ if (b.includes("chrome")) return "\u{1F310}";
93
+ if (b.includes("firefox")) return "\u{1F98A}";
94
+ if (b.includes("safari")) return "\u{1F9ED}";
95
+ if (b.includes("edge")) return "\u{1F4D0}";
96
+ return "\u{1F310}";
97
+ }
98
+ function renderNodeShape(nodeTypes, type, x, y, size, isHovered, isSelected) {
99
+ const config = nodeTypes[type] || nodeTypes.session || defaultNodeTypes.session;
100
+ const scale = isHovered ? 1.2 : isSelected ? 1.1 : 1;
101
+ const actualSize = size * scale;
102
+ const glowIntensity = isHovered ? 20 : isSelected ? 15 : 8;
103
+ const filter = `drop-shadow(0 0 ${glowIntensity}px ${config.glow})`;
104
+ const style = { filter, transition: "all 0.3s ease" };
105
+ switch (config.shape) {
106
+ case "hexagon": {
107
+ const hexPoints = Array.from({ length: 6 }, (_, i) => {
108
+ const angle = i / 6 * Math.PI * 2 - Math.PI / 2;
109
+ return `${x + Math.cos(angle) * actualSize},${y + Math.sin(angle) * actualSize}`;
110
+ }).join(" ");
111
+ return /* @__PURE__ */ jsx(
112
+ "polygon",
113
+ {
114
+ points: hexPoints,
115
+ fill: `${config.color}22`,
116
+ stroke: config.color,
117
+ strokeWidth: 2,
118
+ style
119
+ }
120
+ );
121
+ }
122
+ case "diamond": {
123
+ const diamondPoints = [
124
+ `${x},${y - actualSize}`,
125
+ `${x + actualSize * 0.7},${y}`,
126
+ `${x},${y + actualSize}`,
127
+ `${x - actualSize * 0.7},${y}`
128
+ ].join(" ");
129
+ return /* @__PURE__ */ jsx(
130
+ "polygon",
131
+ {
132
+ points: diamondPoints,
133
+ fill: `${config.color}22`,
134
+ stroke: config.color,
135
+ strokeWidth: 2,
136
+ style
137
+ }
138
+ );
139
+ }
140
+ case "triangle": {
141
+ const triPoints = [
142
+ `${x},${y - actualSize}`,
143
+ `${x + actualSize * 0.866},${y + actualSize * 0.5}`,
144
+ `${x - actualSize * 0.866},${y + actualSize * 0.5}`
145
+ ].join(" ");
146
+ return /* @__PURE__ */ jsx(
147
+ "polygon",
148
+ {
149
+ points: triPoints,
150
+ fill: `${config.color}22`,
151
+ stroke: config.color,
152
+ strokeWidth: 2,
153
+ style
154
+ }
155
+ );
156
+ }
157
+ default:
158
+ return /* @__PURE__ */ jsx(
159
+ "circle",
160
+ {
161
+ cx: x,
162
+ cy: y,
163
+ r: actualSize,
164
+ fill: `${config.color}22`,
165
+ stroke: config.color,
166
+ strokeWidth: 2,
167
+ style
168
+ }
169
+ );
170
+ }
171
+ }
172
+
173
+ // src/IdentityMesh.tsx
174
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
175
+ function MeshRenderer({
176
+ links,
177
+ visitorDetails,
178
+ selectedVisitor,
179
+ onSelectVisitor,
180
+ nodeTypes,
181
+ height,
182
+ showLegend,
183
+ showIdentityCount
184
+ }) {
185
+ const [hoveredNode, setHoveredNode] = useState2(null);
186
+ const [animationPhase, setAnimationPhase] = useState2(0);
187
+ useEffect2(() => {
188
+ const interval = setInterval(() => {
189
+ setAnimationPhase((p) => (p + 1) % 360);
190
+ }, 50);
191
+ return () => clearInterval(interval);
192
+ }, []);
193
+ const visitorGroups = useMemo(() => {
194
+ const groups = {};
195
+ links.forEach((link) => {
196
+ if (!groups[link.visitorId]) groups[link.visitorId] = [];
197
+ groups[link.visitorId].push(link);
198
+ });
199
+ return groups;
200
+ }, [links]);
201
+ const identityLinks = useMemo(() => {
202
+ const externalIdToVisitors = {};
203
+ links.forEach((link) => {
204
+ if (!externalIdToVisitors[link.externalId]) {
205
+ externalIdToVisitors[link.externalId] = /* @__PURE__ */ new Set();
206
+ }
207
+ externalIdToVisitors[link.externalId].add(link.visitorId);
208
+ });
209
+ const pairs = [];
210
+ const seenPairs = /* @__PURE__ */ new Set();
211
+ Object.entries(externalIdToVisitors).forEach(([externalId, visitors2]) => {
212
+ var _a;
213
+ if (visitors2.size > 1) {
214
+ const visitorList = Array.from(visitors2);
215
+ for (let i = 0; i < visitorList.length; i++) {
216
+ for (let j = i + 1; j < visitorList.length; j++) {
217
+ const pairKey = [visitorList[i], visitorList[j]].sort().join("-");
218
+ if (!seenPairs.has(pairKey)) {
219
+ seenPairs.add(pairKey);
220
+ const linkType = ((_a = links.find((l) => l.externalId === externalId)) == null ? void 0 : _a.externalType) || "unknown";
221
+ pairs.push({ v1: visitorList[i], v2: visitorList[j], sharedId: externalId, type: linkType });
222
+ }
223
+ }
224
+ }
225
+ }
226
+ });
227
+ return pairs;
228
+ }, [links]);
229
+ const visitors = Object.keys(visitorGroups);
230
+ const visitorPositions = useMemo(() => {
231
+ const centerX = 400;
232
+ const centerY = 300;
233
+ const radius = Math.min(200, 80 + visitors.length * 25);
234
+ return visitors.reduce((acc, visitor, i) => {
235
+ const angle = i / visitors.length * Math.PI * 2 - Math.PI / 2;
236
+ acc[visitor] = {
237
+ x: centerX + Math.cos(angle) * radius,
238
+ y: centerY + Math.sin(angle) * radius
239
+ };
240
+ return acc;
241
+ }, {});
242
+ }, [visitors]);
243
+ const linkPositions = useMemo(() => {
244
+ const positions = {};
245
+ Object.entries(visitorGroups).forEach(([visitorId, visitorLinks]) => {
246
+ const visitorPos = visitorPositions[visitorId];
247
+ if (!visitorPos) return;
248
+ const linkRadius = 60;
249
+ visitorLinks.forEach((link, i) => {
250
+ const angle = i / visitorLinks.length * Math.PI * 2 - Math.PI / 2;
251
+ positions[link.id] = {
252
+ x: visitorPos.x + Math.cos(angle) * linkRadius,
253
+ y: visitorPos.y + Math.sin(angle) * linkRadius,
254
+ type: link.externalType
255
+ };
256
+ });
257
+ });
258
+ return positions;
259
+ }, [visitorGroups, visitorPositions]);
260
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: "100%", height, overflow: "hidden" }, children: [
261
+ /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", style: { position: "absolute", top: 0, left: 0, opacity: 0.1 }, children: [
262
+ /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("pattern", { id: "mesh-grid", width: "40", height: "40", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx2("path", { d: "M 40 0 L 0 0 0 40", fill: "none", stroke: "currentColor", strokeWidth: "0.5" }) }) }),
263
+ /* @__PURE__ */ jsx2("rect", { width: "100%", height: "100%", fill: "url(#mesh-grid)" })
264
+ ] }),
265
+ /* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 800 600", style: { position: "relative" }, children: [
266
+ /* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsxs("radialGradient", { id: "mesh-centerGlow", children: [
267
+ /* @__PURE__ */ jsx2("stop", { offset: "0%", stopColor: "rgba(0, 240, 255, 0.2)" }),
268
+ /* @__PURE__ */ jsx2("stop", { offset: "100%", stopColor: "rgba(0, 240, 255, 0)" })
269
+ ] }) }),
270
+ /* @__PURE__ */ jsx2("circle", { cx: "400", cy: "300", r: "280", fill: "url(#mesh-centerGlow)" }),
271
+ identityLinks.map((link, idx) => {
272
+ const p1 = visitorPositions[link.v1];
273
+ const p2 = visitorPositions[link.v2];
274
+ if (!p1 || !p2) return null;
275
+ const midX = (p1.x + p2.x) / 2;
276
+ const midY = (p1.y + p2.y) / 2;
277
+ const dx = p2.x - p1.x;
278
+ const dy = p2.y - p1.y;
279
+ const dist = Math.sqrt(dx * dx + dy * dy);
280
+ const curvature = Math.min(50, dist * 0.2);
281
+ const perpX = -dy / dist * curvature;
282
+ const perpY = dx / dist * curvature;
283
+ const ctrlX = midX + perpX;
284
+ const ctrlY = midY + perpY;
285
+ return /* @__PURE__ */ jsxs("g", { children: [
286
+ /* @__PURE__ */ jsx2(
287
+ "path",
288
+ {
289
+ d: `M ${p1.x} ${p1.y} Q ${ctrlX} ${ctrlY} ${p2.x} ${p2.y}`,
290
+ fill: "none",
291
+ stroke: "#ff6b6b",
292
+ strokeWidth: 8,
293
+ strokeOpacity: 0.2,
294
+ style: { filter: "blur(4px)" }
295
+ }
296
+ ),
297
+ /* @__PURE__ */ jsx2(
298
+ "path",
299
+ {
300
+ d: `M ${p1.x} ${p1.y} Q ${ctrlX} ${ctrlY} ${p2.x} ${p2.y}`,
301
+ fill: "none",
302
+ stroke: "#ff6b6b",
303
+ strokeWidth: 3,
304
+ strokeOpacity: 0.8,
305
+ strokeDasharray: "8 4",
306
+ style: { strokeDashoffset: -animationPhase * 0.5 }
307
+ }
308
+ ),
309
+ /* @__PURE__ */ jsxs("g", { transform: `translate(${ctrlX}, ${ctrlY})`, children: [
310
+ /* @__PURE__ */ jsx2("rect", { x: -45, y: -12, width: 90, height: 24, rx: 12, fill: "rgba(255, 107, 107, 0.9)" }),
311
+ /* @__PURE__ */ jsxs("text", { x: 0, y: 4, textAnchor: "middle", fill: "white", fontSize: 9, fontWeight: "bold", children: [
312
+ "SAME ",
313
+ link.type === "wallet" ? "WALLET" : "VAULT"
314
+ ] })
315
+ ] }),
316
+ /* @__PURE__ */ jsx2("circle", { r: "4", fill: "#ff6b6b", children: /* @__PURE__ */ jsx2("animateMotion", { dur: "3s", repeatCount: "indefinite", path: `M ${p1.x} ${p1.y} Q ${ctrlX} ${ctrlY} ${p2.x} ${p2.y}` }) }),
317
+ /* @__PURE__ */ jsx2("circle", { r: "4", fill: "#ffd93d", children: /* @__PURE__ */ jsx2("animateMotion", { dur: "3s", repeatCount: "indefinite", begin: "1.5s", path: `M ${p1.x} ${p1.y} Q ${ctrlX} ${ctrlY} ${p2.x} ${p2.y}` }) })
318
+ ] }, `identity-${idx}`);
319
+ }),
320
+ visitors.map(
321
+ (v1, i) => visitors.slice(i + 1).map((v2) => {
322
+ const p1 = visitorPositions[v1];
323
+ const p2 = visitorPositions[v2];
324
+ if (!p1 || !p2) return null;
325
+ const distance = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
326
+ const opacity = Math.max(0, 0.08 - distance / 3e3);
327
+ if (identityLinks.some((l) => l.v1 === v1 && l.v2 === v2 || l.v1 === v2 && l.v2 === v1)) return null;
328
+ return /* @__PURE__ */ jsx2(
329
+ "line",
330
+ {
331
+ x1: p1.x,
332
+ y1: p1.y,
333
+ x2: p2.x,
334
+ y2: p2.y,
335
+ stroke: "rgba(0, 240, 255, 0.15)",
336
+ strokeWidth: 1,
337
+ strokeDasharray: "2 4",
338
+ style: { opacity, strokeDashoffset: animationPhase % 6 }
339
+ },
340
+ `mesh-${v1}-${v2}`
341
+ );
342
+ })
343
+ ),
344
+ links.map((link) => {
345
+ const visitorPos = visitorPositions[link.visitorId];
346
+ const linkPos = linkPositions[link.id];
347
+ if (!visitorPos || !linkPos) return null;
348
+ const config = nodeTypes[link.externalType] || nodeTypes.session || defaultNodeTypes.session;
349
+ const isVisitorSelected = selectedVisitor === link.visitorId;
350
+ const isLinkHovered = hoveredNode === link.id;
351
+ return /* @__PURE__ */ jsxs("g", { children: [
352
+ /* @__PURE__ */ jsx2(
353
+ "line",
354
+ {
355
+ x1: visitorPos.x,
356
+ y1: visitorPos.y,
357
+ x2: linkPos.x,
358
+ y2: linkPos.y,
359
+ stroke: config.color,
360
+ strokeWidth: isVisitorSelected || isLinkHovered ? 2 : 1,
361
+ strokeOpacity: isVisitorSelected ? 0.8 : 0.3,
362
+ strokeDasharray: link.confidence && link.confidence < 0.9 ? "4 2" : "none"
363
+ }
364
+ ),
365
+ isVisitorSelected && /* @__PURE__ */ jsx2("circle", { r: "3", fill: config.color, children: /* @__PURE__ */ jsx2("animateMotion", { dur: "2s", repeatCount: "indefinite", path: `M${visitorPos.x},${visitorPos.y} L${linkPos.x},${linkPos.y}` }) })
366
+ ] }, `connection-${link.id}`);
367
+ }),
368
+ visitors.map((visitorId) => {
369
+ var _a, _b, _c, _d;
370
+ const pos = visitorPositions[visitorId];
371
+ if (!pos) return null;
372
+ const isSelected = selectedVisitor === visitorId;
373
+ const isHovered = hoveredNode === `visitor-${visitorId}`;
374
+ const linkCount = ((_a = visitorGroups[visitorId]) == null ? void 0 : _a.length) || 0;
375
+ const details = visitorDetails[visitorId];
376
+ const hasIdentityLink = identityLinks.some((l) => l.v1 === visitorId || l.v2 === visitorId);
377
+ return /* @__PURE__ */ jsxs(
378
+ "g",
379
+ {
380
+ style: { cursor: "pointer" },
381
+ onMouseEnter: () => setHoveredNode(`visitor-${visitorId}`),
382
+ onMouseLeave: () => setHoveredNode(null),
383
+ onClick: () => onSelectVisitor(isSelected ? null : visitorId),
384
+ children: [
385
+ hasIdentityLink && /* @__PURE__ */ jsx2(
386
+ "circle",
387
+ {
388
+ cx: pos.x,
389
+ cy: pos.y,
390
+ r: isSelected ? 42 : isHovered ? 40 : 36,
391
+ fill: "none",
392
+ stroke: "#ff6b6b",
393
+ strokeWidth: 2,
394
+ strokeDasharray: "6 3",
395
+ style: {
396
+ transform: `rotate(${-animationPhase}deg)`,
397
+ transformOrigin: `${pos.x}px ${pos.y}px`,
398
+ filter: "drop-shadow(0 0 8px rgba(255, 107, 107, 0.6))"
399
+ }
400
+ }
401
+ ),
402
+ /* @__PURE__ */ jsx2(
403
+ "circle",
404
+ {
405
+ cx: pos.x,
406
+ cy: pos.y,
407
+ r: isSelected ? 35 : isHovered ? 32 : 28,
408
+ fill: "none",
409
+ stroke: "rgba(0, 240, 255, 0.3)",
410
+ strokeWidth: 2,
411
+ strokeDasharray: "4 2",
412
+ style: {
413
+ transform: `rotate(${animationPhase}deg)`,
414
+ transformOrigin: `${pos.x}px ${pos.y}px`
415
+ }
416
+ }
417
+ ),
418
+ /* @__PURE__ */ jsx2(
419
+ "circle",
420
+ {
421
+ cx: pos.x,
422
+ cy: pos.y,
423
+ r: isSelected ? 24 : isHovered ? 22 : 18,
424
+ fill: "rgba(0, 20, 30, 0.95)",
425
+ stroke: "#00f0ff",
426
+ strokeWidth: isSelected ? 3 : 2,
427
+ style: {
428
+ filter: `drop-shadow(0 0 ${isSelected ? 20 : isHovered ? 15 : 10}px rgba(0, 240, 255, 0.6))`,
429
+ transition: "all 0.3s ease"
430
+ }
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsx2("text", { x: pos.x, y: pos.y + 5, textAnchor: "middle", fill: "#00f0ff", fontSize: isSelected ? 16 : 14, fontWeight: "bold", children: details ? getDeviceIcon((_b = details.fingerprint) == null ? void 0 : _b.platform, (_c = details.fingerprint) == null ? void 0 : _c.touchSupport) : linkCount }),
434
+ /* @__PURE__ */ jsx2("text", { x: pos.x, y: pos.y + 48, textAnchor: "middle", fill: "rgba(255, 255, 255, 0.8)", fontSize: 10, fontFamily: "monospace", children: visitorId.length > 12 ? visitorId.slice(0, 12) + "..." : visitorId }),
435
+ ((_d = details == null ? void 0 : details.location) == null ? void 0 : _d.country) && /* @__PURE__ */ jsxs("text", { x: pos.x, y: pos.y + 60, textAnchor: "middle", fill: "rgba(255, 255, 255, 0.5)", fontSize: 9, children: [
436
+ getCountryFlag(details.location.country),
437
+ " ",
438
+ details.location.city || details.location.region
439
+ ] })
440
+ ]
441
+ },
442
+ `visitor-${visitorId}`
443
+ );
444
+ }),
445
+ links.map((link) => {
446
+ const pos = linkPositions[link.id];
447
+ if (!pos) return null;
448
+ const isHovered = hoveredNode === link.id;
449
+ const isVisitorSelected = selectedVisitor === link.visitorId;
450
+ const config = nodeTypes[link.externalType] || nodeTypes.session || defaultNodeTypes.session;
451
+ const isSharedId = identityLinks.some((l) => l.sharedId === link.externalId);
452
+ return /* @__PURE__ */ jsxs(
453
+ "g",
454
+ {
455
+ style: { cursor: "pointer", opacity: selectedVisitor && !isVisitorSelected ? 0.3 : 1 },
456
+ onMouseEnter: () => setHoveredNode(link.id),
457
+ onMouseLeave: () => setHoveredNode(null),
458
+ children: [
459
+ isSharedId && /* @__PURE__ */ jsx2(
460
+ "circle",
461
+ {
462
+ cx: pos.x,
463
+ cy: pos.y,
464
+ r: 22,
465
+ fill: "none",
466
+ stroke: "#ff6b6b",
467
+ strokeWidth: 2,
468
+ strokeOpacity: 0.6,
469
+ strokeDasharray: "3 2"
470
+ }
471
+ ),
472
+ renderNodeShape(nodeTypes, link.externalType, pos.x, pos.y, 14, isHovered, isVisitorSelected),
473
+ link.confidence && /* @__PURE__ */ jsx2(
474
+ "circle",
475
+ {
476
+ cx: pos.x,
477
+ cy: pos.y,
478
+ r: 18,
479
+ fill: "none",
480
+ stroke: config.color,
481
+ strokeWidth: 2,
482
+ strokeOpacity: 0.3,
483
+ strokeDasharray: `${link.confidence * 113} 113`,
484
+ strokeLinecap: "round",
485
+ transform: `rotate(-90, ${pos.x}, ${pos.y})`
486
+ }
487
+ ),
488
+ isHovered && /* @__PURE__ */ jsxs("g", { children: [
489
+ /* @__PURE__ */ jsx2("rect", { x: pos.x - 70, y: pos.y - 60, width: 140, height: 48, rx: 6, fill: "rgba(0, 10, 20, 0.95)", stroke: config.color, strokeWidth: 1 }),
490
+ /* @__PURE__ */ jsxs("text", { x: pos.x, y: pos.y - 42, textAnchor: "middle", fill: config.color, fontSize: 10, fontWeight: "bold", children: [
491
+ isSharedId ? "\u{1F517} " : "",
492
+ config.label
493
+ ] }),
494
+ /* @__PURE__ */ jsx2("text", { x: pos.x, y: pos.y - 28, textAnchor: "middle", fill: "rgba(255, 255, 255, 0.7)", fontSize: 9, fontFamily: "monospace", children: link.externalId.length > 18 ? link.externalId.slice(0, 18) + "..." : link.externalId })
495
+ ] })
496
+ ]
497
+ },
498
+ `node-${link.id}`
499
+ );
500
+ })
501
+ ] }),
502
+ showLegend && /* @__PURE__ */ jsxs("div", { style: {
503
+ position: "absolute",
504
+ bottom: 20,
505
+ left: 20,
506
+ display: "flex",
507
+ gap: 16,
508
+ flexWrap: "wrap",
509
+ padding: "12px 16px",
510
+ background: "rgba(0, 10, 20, 0.9)",
511
+ borderRadius: 8,
512
+ border: "1px solid rgba(0, 240, 255, 0.2)"
513
+ }, children: [
514
+ Object.entries(nodeTypes).filter(([key]) => !["visitor", "device", "identity_link"].includes(key)).map(([key, config]) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
515
+ /* @__PURE__ */ jsxs("svg", { width: 16, height: 16, viewBox: "0 0 24 24", children: [
516
+ config.shape === "hexagon" && /* @__PURE__ */ jsx2("polygon", { points: "12,2 22,7 22,17 12,22 2,17 2,7", fill: config.color, fillOpacity: 0.3, stroke: config.color, strokeWidth: 2 }),
517
+ config.shape === "diamond" && /* @__PURE__ */ jsx2("polygon", { points: "12,2 22,12 12,22 2,12", fill: config.color, fillOpacity: 0.3, stroke: config.color, strokeWidth: 2 }),
518
+ config.shape === "circle" && /* @__PURE__ */ jsx2("circle", { cx: 12, cy: 12, r: 9, fill: config.color, fillOpacity: 0.3, stroke: config.color, strokeWidth: 2 }),
519
+ config.shape === "triangle" && /* @__PURE__ */ jsx2("polygon", { points: "12,2 22,20 2,20", fill: config.color, fillOpacity: 0.3, stroke: config.color, strokeWidth: 2 })
520
+ ] }),
521
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 10, color: "rgba(255, 255, 255, 0.7)" }, children: config.label })
522
+ ] }, key)),
523
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
524
+ /* @__PURE__ */ jsx2("div", { style: { width: 20, height: 3, background: "#ff6b6b", borderRadius: 2 } }),
525
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 10, color: "rgba(255, 255, 255, 0.7)" }, children: "Same Identity" })
526
+ ] })
527
+ ] }),
528
+ showIdentityCount && identityLinks.length > 0 && /* @__PURE__ */ jsxs("div", { style: {
529
+ position: "absolute",
530
+ top: 20,
531
+ left: 20,
532
+ padding: "10px 14px",
533
+ background: "rgba(255, 107, 107, 0.15)",
534
+ borderRadius: 8,
535
+ border: "1px solid rgba(255, 107, 107, 0.4)"
536
+ }, children: [
537
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 10, color: "rgba(255, 255, 255, 0.6)", marginBottom: 4 }, children: "Cross-Device Identities" }),
538
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 20, fontWeight: "bold", color: "#ff6b6b" }, children: identityLinks.length })
539
+ ] })
540
+ ] });
541
+ }
542
+ function IdentityMesh({
543
+ apiUrl,
544
+ vaultId,
545
+ visitorId,
546
+ onError,
547
+ onNodeSelect,
548
+ theme,
549
+ className,
550
+ style,
551
+ height = 600,
552
+ showLegend = true,
553
+ showIdentityCount = true
554
+ }) {
555
+ const [selectedVisitor, setSelectedVisitor] = useState2(null);
556
+ const { links, visitorDetails, loading, error } = useMeshData({
557
+ apiUrl,
558
+ vaultId,
559
+ visitorId,
560
+ onError
561
+ });
562
+ const nodeTypes = useMemo(() => {
563
+ const merged = { ...defaultNodeTypes };
564
+ if (theme == null ? void 0 : theme.nodeColors) {
565
+ Object.entries(theme.nodeColors).forEach(([key, value]) => {
566
+ if (value) merged[key] = value;
567
+ });
568
+ }
569
+ return merged;
570
+ }, [theme == null ? void 0 : theme.nodeColors]);
571
+ const handleSelectVisitor = (id) => {
572
+ setSelectedVisitor(id);
573
+ onNodeSelect == null ? void 0 : onNodeSelect(id, id ? "visitor" : null);
574
+ };
575
+ const containerStyle = {
576
+ background: (theme == null ? void 0 : theme.background) || "#0a0a0f",
577
+ color: (theme == null ? void 0 : theme.gridColor) || "#ffffff",
578
+ borderRadius: 8,
579
+ overflow: "hidden",
580
+ ...style
581
+ };
582
+ if (loading) {
583
+ return /* @__PURE__ */ jsx2("div", { className, style: { ...containerStyle, height, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
584
+ /* @__PURE__ */ jsx2("div", { style: {
585
+ width: 40,
586
+ height: 40,
587
+ border: "3px solid rgba(0, 240, 255, 0.3)",
588
+ borderTopColor: "#00f0ff",
589
+ borderRadius: "50%",
590
+ animation: "spin 1s linear infinite",
591
+ margin: "0 auto 12px"
592
+ } }),
593
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` }),
594
+ /* @__PURE__ */ jsx2("div", { style: { color: "rgba(255, 255, 255, 0.6)", fontSize: 14 }, children: "Loading mesh data..." })
595
+ ] }) });
596
+ }
597
+ if (error) {
598
+ return /* @__PURE__ */ jsx2("div", { className, style: { ...containerStyle, height, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: 24 }, children: [
599
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 32, marginBottom: 12 }, children: "\u26A0\uFE0F" }),
600
+ /* @__PURE__ */ jsx2("div", { style: { color: "#ff6b6b", fontSize: 14, marginBottom: 8 }, children: "Failed to load mesh" }),
601
+ /* @__PURE__ */ jsx2("div", { style: { color: "rgba(255, 255, 255, 0.5)", fontSize: 12 }, children: error.message })
602
+ ] }) });
603
+ }
604
+ if (links.length === 0) {
605
+ return /* @__PURE__ */ jsx2("div", { className, style: { ...containerStyle, height, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: 24 }, children: [
606
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 32, marginBottom: 12 }, children: "\u{1F517}" }),
607
+ /* @__PURE__ */ jsx2("div", { style: { color: "rgba(255, 255, 255, 0.6)", fontSize: 14 }, children: "No connections found" })
608
+ ] }) });
609
+ }
610
+ return /* @__PURE__ */ jsx2("div", { className, style: containerStyle, children: /* @__PURE__ */ jsx2(
611
+ MeshRenderer,
612
+ {
613
+ links,
614
+ visitorDetails,
615
+ selectedVisitor,
616
+ onSelectVisitor: handleSelectVisitor,
617
+ nodeTypes,
618
+ height,
619
+ showLegend,
620
+ showIdentityCount
621
+ }
622
+ ) });
623
+ }
624
+ export {
625
+ IdentityMesh,
626
+ defaultNodeTypes,
627
+ getBrowserIcon,
628
+ getCountryFlag,
629
+ getDeviceIcon,
630
+ renderNodeShape,
631
+ useMeshData
632
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@emblemvault/identity-mesh",
3
+ "version": "1.0.0",
4
+ "description": "Embeddable Identity Mesh visualization component for displaying cross-device identity connections",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
+ "lint": "eslint src/",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "peerDependencies": {
25
+ "react": ">=17.0.0",
26
+ "react-dom": ">=17.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.0.0",
30
+ "@types/react-dom": "^18.0.0",
31
+ "react": "^18.0.0",
32
+ "react-dom": "^18.0.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "keywords": [
37
+ "identity",
38
+ "mesh",
39
+ "fingerprint",
40
+ "visualization",
41
+ "react",
42
+ "emblemvault"
43
+ ],
44
+ "author": "EmblemVault",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/emblemvault/identity-mesh"
49
+ }
50
+ }