beads-map 0.1.0 → 0.2.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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +5 -5
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/beads.body +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +4 -4
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/page-f36cdcae49f1d2af.js +1 -0
- package/.next/static/css/a4e34aaaa51183d9.css +3 -0
- package/README.md +44 -20
- package/app/page.tsx +313 -90
- package/bin/beads-map.mjs +9 -3
- package/components/AuthButton.tsx +7 -7
- package/components/BeadsGraph.tsx +372 -19
- package/components/BeadsLogo.tsx +111 -0
- package/components/ContextMenu.tsx +180 -0
- package/components/DescriptionModal.tsx +64 -0
- package/components/NodeDetail.tsx +18 -3
- package/package.json +8 -2
- package/.next/static/chunks/app/page-49d569c912d5af9d.js +0 -1
- package/.next/static/css/10ef08b24212fe36.css +0 -3
- /package/.next/static/{99eOjoTtoO32H-c1faxZ5 → YVdbDxCehgqcYmLncYRFB}/_buildManifest.js +0 -0
- /package/.next/static/{99eOjoTtoO32H-c1faxZ5 → YVdbDxCehgqcYmLncYRFB}/_ssgManifest.js +0 -0
- /package/.next/static/chunks/{945-bf736d0119e7437b.js → 945-3ff1d381a0af1ecd.js} +0 -0
|
@@ -35,6 +35,8 @@ interface BeadsGraphProps {
|
|
|
35
35
|
onBackgroundClick: () => void;
|
|
36
36
|
onNodeRightClick?: (node: GraphNode, event: MouseEvent) => void;
|
|
37
37
|
commentedNodeIds?: Map<string, number>;
|
|
38
|
+
claimedNodeAvatars?: Map<string, { avatar?: string; handle: string; claimedAt: string }>;
|
|
39
|
+
onAvatarHover?: (info: { handle: string; avatar?: string; claimedAt: string; x: number; y: number } | null) => void;
|
|
38
40
|
timelineActive?: boolean;
|
|
39
41
|
stats?: { total: number; edges: number; prefixes: string[] };
|
|
40
42
|
}
|
|
@@ -85,6 +87,55 @@ function easeOutQuad(t: number): number {
|
|
|
85
87
|
return 1 - (1 - t) * (1 - t);
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
// --- Module-level avatar image cache for canvas rendering ---
|
|
91
|
+
const avatarImageCache = new Map<
|
|
92
|
+
string,
|
|
93
|
+
HTMLImageElement | "loading" | "failed"
|
|
94
|
+
>();
|
|
95
|
+
|
|
96
|
+
function getAvatarImage(
|
|
97
|
+
url: string,
|
|
98
|
+
onLoad: () => void
|
|
99
|
+
): HTMLImageElement | null {
|
|
100
|
+
const cached = avatarImageCache.get(url);
|
|
101
|
+
if (cached === "loading" || cached === "failed") return null;
|
|
102
|
+
if (cached) return cached;
|
|
103
|
+
|
|
104
|
+
avatarImageCache.set(url, "loading");
|
|
105
|
+
const img = new Image();
|
|
106
|
+
img.onload = () => {
|
|
107
|
+
avatarImageCache.set(url, img);
|
|
108
|
+
onLoad();
|
|
109
|
+
};
|
|
110
|
+
img.onerror = () => {
|
|
111
|
+
avatarImageCache.set(url, "failed");
|
|
112
|
+
};
|
|
113
|
+
img.src = url;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function drawAvatarFallback(
|
|
118
|
+
ctx: CanvasRenderingContext2D,
|
|
119
|
+
x: number,
|
|
120
|
+
y: number,
|
|
121
|
+
radius: number,
|
|
122
|
+
handle: string,
|
|
123
|
+
globalScale: number
|
|
124
|
+
) {
|
|
125
|
+
ctx.beginPath();
|
|
126
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
127
|
+
ctx.fillStyle = "#e4e4e7"; // zinc-200
|
|
128
|
+
ctx.fill();
|
|
129
|
+
|
|
130
|
+
const letter = handle.replace("@", "").charAt(0).toUpperCase();
|
|
131
|
+
const fontSize = Math.min(7, Math.max(3, radius * 1.3));
|
|
132
|
+
ctx.font = `600 ${fontSize}px 'Inter', system-ui, sans-serif`;
|
|
133
|
+
ctx.textAlign = "center";
|
|
134
|
+
ctx.textBaseline = "middle";
|
|
135
|
+
ctx.fillStyle = "#71717a"; // zinc-500
|
|
136
|
+
ctx.fillText(letter, x, y + 0.3);
|
|
137
|
+
}
|
|
138
|
+
|
|
88
139
|
/**
|
|
89
140
|
* Compute connected subgraph via BFS (depth 2) - pure function, no React state
|
|
90
141
|
*/
|
|
@@ -150,6 +201,8 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
150
201
|
onBackgroundClick,
|
|
151
202
|
onNodeRightClick,
|
|
152
203
|
commentedNodeIds,
|
|
204
|
+
claimedNodeAvatars,
|
|
205
|
+
onAvatarHover,
|
|
153
206
|
timelineActive,
|
|
154
207
|
stats,
|
|
155
208
|
}, ref) {
|
|
@@ -161,11 +214,21 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
161
214
|
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
|
162
215
|
const initialLayoutApplied = useRef(false);
|
|
163
216
|
|
|
164
|
-
// Minimap
|
|
165
|
-
const
|
|
166
|
-
const
|
|
217
|
+
// Minimap dimensions (resizable via drag)
|
|
218
|
+
const [minimapSize, setMinimapSize] = useState({ w: 160, h: 120 });
|
|
219
|
+
const MINIMAP_W = minimapSize.w;
|
|
220
|
+
const MINIMAP_H = minimapSize.h;
|
|
167
221
|
const MINIMAP_PAD = 8; // internal padding so dots aren't clipped at edges
|
|
168
222
|
|
|
223
|
+
// Minimap resize drag state
|
|
224
|
+
const minimapDragRef = useRef<{
|
|
225
|
+
edge: "top" | "right" | "top-right";
|
|
226
|
+
startX: number;
|
|
227
|
+
startY: number;
|
|
228
|
+
startW: number;
|
|
229
|
+
startH: number;
|
|
230
|
+
} | null>(null);
|
|
231
|
+
|
|
169
232
|
// Lazy-load ForceGraph2D on the client (preserves ref forwarding)
|
|
170
233
|
const [ForceGraph2D, setForceGraph2D] =
|
|
171
234
|
useState<React.ComponentType<any> | null>(_ForceGraph2DModule);
|
|
@@ -192,6 +255,22 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
192
255
|
const hoveredNodeRef = useRef<GraphNode | null>(hoveredNode);
|
|
193
256
|
const connectedNodesRef = useRef<Set<string>>(new Set());
|
|
194
257
|
const commentedNodeIdsRef = useRef<Map<string, number>>(commentedNodeIds || new Map());
|
|
258
|
+
const claimedNodeAvatarsRef = useRef<Map<string, { avatar?: string; handle: string; claimedAt: string }>>(
|
|
259
|
+
claimedNodeAvatars || new Map()
|
|
260
|
+
);
|
|
261
|
+
// Callback ref for refreshing graph when avatar images finish loading
|
|
262
|
+
const avatarRefreshRef = useRef<() => void>(() => {});
|
|
263
|
+
avatarRefreshRef.current = () => refreshGraph(graphRef);
|
|
264
|
+
|
|
265
|
+
// Ref for avatar hover callback (avoids stale closures in mousemove handler)
|
|
266
|
+
const onAvatarHoverRef = useRef(onAvatarHover);
|
|
267
|
+
onAvatarHoverRef.current = onAvatarHover;
|
|
268
|
+
|
|
269
|
+
// Track which avatar is currently hovered to avoid redundant callbacks
|
|
270
|
+
const hoveredAvatarNodeRef = useRef<string | null>(null);
|
|
271
|
+
|
|
272
|
+
// Ref for current viewNodes (used by mousemove handler to respect epics view)
|
|
273
|
+
const viewNodesRef = useRef<GraphNode[]>(nodes);
|
|
195
274
|
|
|
196
275
|
// Cluster data ref for semantic zoom: maps epic (parent) IDs to their
|
|
197
276
|
// member node IDs so we can compute centroids and draw cluster labels
|
|
@@ -276,6 +355,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
276
355
|
return { viewNodes: filteredNodes, viewLinks: remappedLinks };
|
|
277
356
|
}, [nodes, links, viewMode]);
|
|
278
357
|
|
|
358
|
+
// Keep viewNodesRef in sync for mousemove avatar hit-testing
|
|
359
|
+
viewNodesRef.current = viewNodes;
|
|
360
|
+
|
|
279
361
|
// Build cluster info for semantic zoom: group nodes by parent epic.
|
|
280
362
|
// This is used by onRenderFramePost to draw cluster labels when zoomed out.
|
|
281
363
|
useEffect(() => {
|
|
@@ -367,6 +449,77 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
367
449
|
refreshGraph(graphRef);
|
|
368
450
|
}, [commentedNodeIds]);
|
|
369
451
|
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
claimedNodeAvatarsRef.current = claimedNodeAvatars || new Map();
|
|
454
|
+
refreshGraph(graphRef);
|
|
455
|
+
}, [claimedNodeAvatars]);
|
|
456
|
+
|
|
457
|
+
// Avatar hover detection: mousemove on container, hit-test against avatar positions
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
const container = containerRef.current;
|
|
460
|
+
if (!container) return;
|
|
461
|
+
|
|
462
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
463
|
+
const fg = graphRef.current;
|
|
464
|
+
const cb = onAvatarHoverRef.current;
|
|
465
|
+
if (!fg || !cb) return;
|
|
466
|
+
|
|
467
|
+
const claimedMap = claimedNodeAvatarsRef.current;
|
|
468
|
+
if (claimedMap.size === 0) {
|
|
469
|
+
if (hoveredAvatarNodeRef.current) {
|
|
470
|
+
hoveredAvatarNodeRef.current = null;
|
|
471
|
+
cb(null);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Convert screen coords to graph coords
|
|
477
|
+
const rect = container.getBoundingClientRect();
|
|
478
|
+
const screenX = e.clientX - rect.left;
|
|
479
|
+
const screenY = e.clientY - rect.top;
|
|
480
|
+
let graphCoords: { x: number; y: number };
|
|
481
|
+
try {
|
|
482
|
+
graphCoords = fg.screen2GraphCoords(screenX, screenY);
|
|
483
|
+
} catch {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Hit-test against each claimed node's avatar position
|
|
488
|
+
const globalScale = fg.zoom() || 1;
|
|
489
|
+
const avatarRadius = Math.max(4, 10 / globalScale);
|
|
490
|
+
|
|
491
|
+
for (const node of viewNodesRef.current) {
|
|
492
|
+
const n = node as any;
|
|
493
|
+
if (n.x == null || n.y == null) continue;
|
|
494
|
+
const claim = claimedMap.get(node.id);
|
|
495
|
+
if (!claim) continue;
|
|
496
|
+
|
|
497
|
+
const size = getNodeSize(node);
|
|
498
|
+
const avatarX = n.x + size * 0.7;
|
|
499
|
+
const avatarY = n.y + size * 0.7;
|
|
500
|
+
const dx = graphCoords.x - avatarX;
|
|
501
|
+
const dy = graphCoords.y - avatarY;
|
|
502
|
+
|
|
503
|
+
if (dx * dx + dy * dy <= avatarRadius * avatarRadius) {
|
|
504
|
+
if (hoveredAvatarNodeRef.current !== node.id) {
|
|
505
|
+
hoveredAvatarNodeRef.current = node.id;
|
|
506
|
+
cb({ handle: claim.handle, avatar: claim.avatar, claimedAt: claim.claimedAt, x: e.clientX, y: e.clientY });
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// No avatar hit
|
|
513
|
+
if (hoveredAvatarNodeRef.current) {
|
|
514
|
+
hoveredAvatarNodeRef.current = null;
|
|
515
|
+
cb(null);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
container.addEventListener("mousemove", handleMouseMove);
|
|
520
|
+
return () => container.removeEventListener("mousemove", handleMouseMove);
|
|
521
|
+
}, []);
|
|
522
|
+
|
|
370
523
|
// Track dimensions
|
|
371
524
|
useEffect(() => {
|
|
372
525
|
const updateDimensions = () => {
|
|
@@ -745,6 +898,67 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
745
898
|
ctx.restore();
|
|
746
899
|
}
|
|
747
900
|
|
|
901
|
+
// Claimant avatar — small circular profile picture at bottom-right
|
|
902
|
+
const claimInfo = claimedNodeAvatarsRef.current.get(graphNode.id);
|
|
903
|
+
if (claimInfo) {
|
|
904
|
+
// Constant screen-space size: divide by globalScale so avatar stays
|
|
905
|
+
// roughly the same pixel size regardless of zoom level
|
|
906
|
+
const avatarSize = Math.max(4, 10 / globalScale);
|
|
907
|
+
const avatarX = node.x + animatedSize * 0.7;
|
|
908
|
+
const avatarY = node.y + animatedSize * 0.7;
|
|
909
|
+
|
|
910
|
+
ctx.save();
|
|
911
|
+
ctx.globalAlpha = 1;
|
|
912
|
+
|
|
913
|
+
if (claimInfo.avatar) {
|
|
914
|
+
const img = getAvatarImage(claimInfo.avatar, () =>
|
|
915
|
+
avatarRefreshRef.current()
|
|
916
|
+
);
|
|
917
|
+
if (img) {
|
|
918
|
+
// Clip to circle and draw image
|
|
919
|
+
ctx.save();
|
|
920
|
+
ctx.beginPath();
|
|
921
|
+
ctx.arc(avatarX, avatarY, avatarSize, 0, Math.PI * 2);
|
|
922
|
+
ctx.clip();
|
|
923
|
+
ctx.drawImage(
|
|
924
|
+
img,
|
|
925
|
+
avatarX - avatarSize,
|
|
926
|
+
avatarY - avatarSize,
|
|
927
|
+
avatarSize * 2,
|
|
928
|
+
avatarSize * 2
|
|
929
|
+
);
|
|
930
|
+
ctx.restore();
|
|
931
|
+
} else {
|
|
932
|
+
drawAvatarFallback(
|
|
933
|
+
ctx,
|
|
934
|
+
avatarX,
|
|
935
|
+
avatarY,
|
|
936
|
+
avatarSize,
|
|
937
|
+
claimInfo.handle,
|
|
938
|
+
globalScale
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
drawAvatarFallback(
|
|
943
|
+
ctx,
|
|
944
|
+
avatarX,
|
|
945
|
+
avatarY,
|
|
946
|
+
avatarSize,
|
|
947
|
+
claimInfo.handle,
|
|
948
|
+
globalScale
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// White border ring for contrast
|
|
953
|
+
ctx.beginPath();
|
|
954
|
+
ctx.arc(avatarX, avatarY, avatarSize, 0, Math.PI * 2);
|
|
955
|
+
ctx.strokeStyle = "#ffffff";
|
|
956
|
+
ctx.lineWidth = Math.max(0.8, 1.2 / globalScale);
|
|
957
|
+
ctx.stroke();
|
|
958
|
+
|
|
959
|
+
ctx.restore();
|
|
960
|
+
}
|
|
961
|
+
|
|
748
962
|
ctx.restore();
|
|
749
963
|
},
|
|
750
964
|
[] // No dependencies - reads from refs
|
|
@@ -921,7 +1135,10 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
921
1135
|
const clusters = clustersRef.current;
|
|
922
1136
|
if (clusters.length === 0) return;
|
|
923
1137
|
|
|
924
|
-
// Build a fast lookup from node ID to current position
|
|
1138
|
+
// Build a fast lookup from node ID to current LIVE position.
|
|
1139
|
+
// Only use viewNodes (the nodes actually in the simulation) — in epics
|
|
1140
|
+
// view, child nodes are collapsed into parent epics and their positions
|
|
1141
|
+
// are stale/frozen. Using stale positions causes centroids to drift.
|
|
925
1142
|
const nodeMap = new Map<string, { x: number; y: number }>();
|
|
926
1143
|
for (const node of viewNodes) {
|
|
927
1144
|
const n = node as any;
|
|
@@ -929,13 +1146,6 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
929
1146
|
nodeMap.set(node.id, { x: n.x, y: n.y });
|
|
930
1147
|
}
|
|
931
1148
|
}
|
|
932
|
-
// Also check the full nodes array (viewNodes may be filtered in epics mode)
|
|
933
|
-
for (const node of nodes) {
|
|
934
|
-
const n = node as any;
|
|
935
|
-
if (!nodeMap.has(node.id) && n.x != null && n.y != null) {
|
|
936
|
-
nodeMap.set(node.id, { x: n.x, y: n.y });
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
1149
|
|
|
940
1150
|
ctx.save();
|
|
941
1151
|
|
|
@@ -1178,6 +1388,58 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1178
1388
|
}
|
|
1179
1389
|
ctx.globalAlpha = 1;
|
|
1180
1390
|
|
|
1391
|
+
// Draw claimed avatars on minimap
|
|
1392
|
+
const claimedMap = claimedNodeAvatarsRef.current;
|
|
1393
|
+
if (claimedMap.size > 0) {
|
|
1394
|
+
for (const node of viewNodes) {
|
|
1395
|
+
const n = node as any;
|
|
1396
|
+
if (n.x == null || n.y == null) continue;
|
|
1397
|
+
const claim = claimedMap.get(node.id);
|
|
1398
|
+
if (!claim) continue;
|
|
1399
|
+
|
|
1400
|
+
const mx = offsetX + (n.x - xMin) * scale;
|
|
1401
|
+
const my = offsetY + (n.y - yMin) * scale;
|
|
1402
|
+
const r = 5; // fixed pixel radius on minimap
|
|
1403
|
+
|
|
1404
|
+
ctx.save();
|
|
1405
|
+
ctx.globalAlpha = 1;
|
|
1406
|
+
|
|
1407
|
+
if (claim.avatar) {
|
|
1408
|
+
const img = getAvatarImage(claim.avatar, () =>
|
|
1409
|
+
avatarRefreshRef.current()
|
|
1410
|
+
);
|
|
1411
|
+
if (img) {
|
|
1412
|
+
ctx.save();
|
|
1413
|
+
ctx.beginPath();
|
|
1414
|
+
ctx.arc(mx, my, r, 0, Math.PI * 2);
|
|
1415
|
+
ctx.clip();
|
|
1416
|
+
ctx.drawImage(img, mx - r, my - r, r * 2, r * 2);
|
|
1417
|
+
ctx.restore();
|
|
1418
|
+
} else {
|
|
1419
|
+
// Fallback circle
|
|
1420
|
+
ctx.beginPath();
|
|
1421
|
+
ctx.arc(mx, my, r, 0, Math.PI * 2);
|
|
1422
|
+
ctx.fillStyle = "#d4d4d8";
|
|
1423
|
+
ctx.fill();
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
ctx.beginPath();
|
|
1427
|
+
ctx.arc(mx, my, r, 0, Math.PI * 2);
|
|
1428
|
+
ctx.fillStyle = "#d4d4d8";
|
|
1429
|
+
ctx.fill();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// White border
|
|
1433
|
+
ctx.beginPath();
|
|
1434
|
+
ctx.arc(mx, my, r, 0, Math.PI * 2);
|
|
1435
|
+
ctx.strokeStyle = "#ffffff";
|
|
1436
|
+
ctx.lineWidth = 1;
|
|
1437
|
+
ctx.stroke();
|
|
1438
|
+
|
|
1439
|
+
ctx.restore();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1181
1443
|
// Draw FOV rectangle
|
|
1182
1444
|
try {
|
|
1183
1445
|
const tl = fg.screen2GraphCoords(0, 0);
|
|
@@ -1466,15 +1728,106 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1466
1728
|
</div>
|
|
1467
1729
|
)}
|
|
1468
1730
|
|
|
1469
|
-
{/* Minimap — bottom-left, hidden on mobile */}
|
|
1470
|
-
<
|
|
1471
|
-
|
|
1472
|
-
width={MINIMAP_W}
|
|
1473
|
-
height={MINIMAP_H}
|
|
1474
|
-
onClick={handleMinimapClick}
|
|
1475
|
-
className="hidden sm:block absolute bottom-4 left-4 z-10 rounded-lg border border-zinc-200 shadow-sm cursor-crosshair"
|
|
1731
|
+
{/* Minimap — bottom-left, hidden on mobile, resizable */}
|
|
1732
|
+
<div
|
|
1733
|
+
className="hidden sm:block absolute bottom-4 left-4 z-10"
|
|
1476
1734
|
style={{ width: MINIMAP_W, height: MINIMAP_H }}
|
|
1477
|
-
|
|
1735
|
+
>
|
|
1736
|
+
<canvas
|
|
1737
|
+
ref={minimapCanvasRef}
|
|
1738
|
+
width={MINIMAP_W}
|
|
1739
|
+
height={MINIMAP_H}
|
|
1740
|
+
onClick={handleMinimapClick}
|
|
1741
|
+
className="rounded-lg border border-zinc-200 shadow-sm cursor-crosshair"
|
|
1742
|
+
style={{ width: MINIMAP_W, height: MINIMAP_H }}
|
|
1743
|
+
/>
|
|
1744
|
+
{/* Resize handle — top edge */}
|
|
1745
|
+
<div
|
|
1746
|
+
className="absolute top-0 left-2 right-2 h-1.5 cursor-n-resize hover:bg-zinc-300/40 rounded-t-lg transition-colors"
|
|
1747
|
+
onMouseDown={(e) => {
|
|
1748
|
+
e.preventDefault();
|
|
1749
|
+
e.stopPropagation();
|
|
1750
|
+
minimapDragRef.current = {
|
|
1751
|
+
edge: "top",
|
|
1752
|
+
startX: e.clientX,
|
|
1753
|
+
startY: e.clientY,
|
|
1754
|
+
startW: MINIMAP_W,
|
|
1755
|
+
startH: MINIMAP_H,
|
|
1756
|
+
};
|
|
1757
|
+
const onMove = (ev: MouseEvent) => {
|
|
1758
|
+
if (!minimapDragRef.current) return;
|
|
1759
|
+
const dy = minimapDragRef.current.startY - ev.clientY;
|
|
1760
|
+
const newH = Math.max(80, Math.min(400, minimapDragRef.current.startH + dy));
|
|
1761
|
+
setMinimapSize((prev) => ({ ...prev, h: newH }));
|
|
1762
|
+
};
|
|
1763
|
+
const onUp = () => {
|
|
1764
|
+
minimapDragRef.current = null;
|
|
1765
|
+
window.removeEventListener("mousemove", onMove);
|
|
1766
|
+
window.removeEventListener("mouseup", onUp);
|
|
1767
|
+
};
|
|
1768
|
+
window.addEventListener("mousemove", onMove);
|
|
1769
|
+
window.addEventListener("mouseup", onUp);
|
|
1770
|
+
}}
|
|
1771
|
+
/>
|
|
1772
|
+
{/* Resize handle — right edge */}
|
|
1773
|
+
<div
|
|
1774
|
+
className="absolute top-2 right-0 bottom-2 w-1.5 cursor-e-resize hover:bg-zinc-300/40 rounded-r-lg transition-colors"
|
|
1775
|
+
onMouseDown={(e) => {
|
|
1776
|
+
e.preventDefault();
|
|
1777
|
+
e.stopPropagation();
|
|
1778
|
+
minimapDragRef.current = {
|
|
1779
|
+
edge: "right",
|
|
1780
|
+
startX: e.clientX,
|
|
1781
|
+
startY: e.clientY,
|
|
1782
|
+
startW: MINIMAP_W,
|
|
1783
|
+
startH: MINIMAP_H,
|
|
1784
|
+
};
|
|
1785
|
+
const onMove = (ev: MouseEvent) => {
|
|
1786
|
+
if (!minimapDragRef.current) return;
|
|
1787
|
+
const dx = ev.clientX - minimapDragRef.current.startX;
|
|
1788
|
+
const newW = Math.max(100, Math.min(500, minimapDragRef.current.startW + dx));
|
|
1789
|
+
setMinimapSize((prev) => ({ ...prev, w: newW }));
|
|
1790
|
+
};
|
|
1791
|
+
const onUp = () => {
|
|
1792
|
+
minimapDragRef.current = null;
|
|
1793
|
+
window.removeEventListener("mousemove", onMove);
|
|
1794
|
+
window.removeEventListener("mouseup", onUp);
|
|
1795
|
+
};
|
|
1796
|
+
window.addEventListener("mousemove", onMove);
|
|
1797
|
+
window.addEventListener("mouseup", onUp);
|
|
1798
|
+
}}
|
|
1799
|
+
/>
|
|
1800
|
+
{/* Resize handle — top-right corner */}
|
|
1801
|
+
<div
|
|
1802
|
+
className="absolute top-0 right-0 w-3 h-3 cursor-ne-resize hover:bg-zinc-300/40 rounded-tr-lg transition-colors"
|
|
1803
|
+
onMouseDown={(e) => {
|
|
1804
|
+
e.preventDefault();
|
|
1805
|
+
e.stopPropagation();
|
|
1806
|
+
minimapDragRef.current = {
|
|
1807
|
+
edge: "top-right",
|
|
1808
|
+
startX: e.clientX,
|
|
1809
|
+
startY: e.clientY,
|
|
1810
|
+
startW: MINIMAP_W,
|
|
1811
|
+
startH: MINIMAP_H,
|
|
1812
|
+
};
|
|
1813
|
+
const onMove = (ev: MouseEvent) => {
|
|
1814
|
+
if (!minimapDragRef.current) return;
|
|
1815
|
+
const dx = ev.clientX - minimapDragRef.current.startX;
|
|
1816
|
+
const dy = minimapDragRef.current.startY - ev.clientY;
|
|
1817
|
+
const newW = Math.max(100, Math.min(500, minimapDragRef.current.startW + dx));
|
|
1818
|
+
const newH = Math.max(80, Math.min(400, minimapDragRef.current.startH + dy));
|
|
1819
|
+
setMinimapSize({ w: newW, h: newH });
|
|
1820
|
+
};
|
|
1821
|
+
const onUp = () => {
|
|
1822
|
+
minimapDragRef.current = null;
|
|
1823
|
+
window.removeEventListener("mousemove", onMove);
|
|
1824
|
+
window.removeEventListener("mouseup", onUp);
|
|
1825
|
+
};
|
|
1826
|
+
window.addEventListener("mousemove", onMove);
|
|
1827
|
+
window.addEventListener("mouseup", onUp);
|
|
1828
|
+
}}
|
|
1829
|
+
/>
|
|
1830
|
+
</div>
|
|
1478
1831
|
|
|
1479
1832
|
{ForceGraph2D ? (
|
|
1480
1833
|
<ForceGraph2D
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Animated geometric logo for beads-map.
|
|
5
|
+
* Inspired by plresearch.org's HexagonIcon — a rotating hexagonal network
|
|
6
|
+
* with pulsing nodes, representing the dependency graph.
|
|
7
|
+
*/
|
|
8
|
+
export function BeadsLogo({ className }: { className?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 32 32"
|
|
12
|
+
fill="none"
|
|
13
|
+
className={className}
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
>
|
|
16
|
+
{/* Outer hexagon — slow rotation */}
|
|
17
|
+
<polygon
|
|
18
|
+
points="16,3 27,9.5 27,22.5 16,29 5,22.5 5,9.5"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="1.2"
|
|
21
|
+
fill="none"
|
|
22
|
+
opacity="0.7"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
>
|
|
25
|
+
<animateTransform
|
|
26
|
+
attributeName="transform"
|
|
27
|
+
type="rotate"
|
|
28
|
+
values="0 16 16;360 16 16"
|
|
29
|
+
dur="40s"
|
|
30
|
+
repeatCount="indefinite"
|
|
31
|
+
/>
|
|
32
|
+
</polygon>
|
|
33
|
+
|
|
34
|
+
{/* Inner hexagon — counter-rotation, slightly smaller */}
|
|
35
|
+
<polygon
|
|
36
|
+
points="16,8 22.5,11.5 22.5,20.5 16,24 9.5,20.5 9.5,11.5"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
strokeWidth="0.8"
|
|
39
|
+
fill="none"
|
|
40
|
+
opacity="0.35"
|
|
41
|
+
strokeLinejoin="round"
|
|
42
|
+
>
|
|
43
|
+
<animateTransform
|
|
44
|
+
attributeName="transform"
|
|
45
|
+
type="rotate"
|
|
46
|
+
values="360 16 16;0 16 16"
|
|
47
|
+
dur="30s"
|
|
48
|
+
repeatCount="indefinite"
|
|
49
|
+
/>
|
|
50
|
+
</polygon>
|
|
51
|
+
|
|
52
|
+
{/* Connection lines from center to outer vertices */}
|
|
53
|
+
<line x1="16" y1="16" x2="16" y2="3" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
54
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" />
|
|
55
|
+
</line>
|
|
56
|
+
<line x1="16" y1="16" x2="27" y2="9.5" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
57
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" begin="0.5s" />
|
|
58
|
+
</line>
|
|
59
|
+
<line x1="16" y1="16" x2="27" y2="22.5" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
60
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" begin="1s" />
|
|
61
|
+
</line>
|
|
62
|
+
<line x1="16" y1="16" x2="16" y2="29" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
63
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" begin="1.5s" />
|
|
64
|
+
</line>
|
|
65
|
+
<line x1="16" y1="16" x2="5" y2="22.5" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
66
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" begin="2s" />
|
|
67
|
+
</line>
|
|
68
|
+
<line x1="16" y1="16" x2="5" y2="9.5" stroke="currentColor" strokeWidth="0.6" opacity="0.2">
|
|
69
|
+
<animate attributeName="opacity" values="0.15;0.35;0.15" dur="3s" repeatCount="indefinite" begin="2.5s" />
|
|
70
|
+
</line>
|
|
71
|
+
|
|
72
|
+
{/* Center node — breathing pulse */}
|
|
73
|
+
<circle cx="16" cy="16" r="2.5" fill="currentColor" opacity="0.8">
|
|
74
|
+
<animate attributeName="r" values="2;3;2" dur="3s" repeatCount="indefinite" />
|
|
75
|
+
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" />
|
|
76
|
+
</circle>
|
|
77
|
+
|
|
78
|
+
{/* Vertex nodes — staggered pulse */}
|
|
79
|
+
<circle cx="16" cy="3" r="1.5" fill="currentColor" opacity="0.5">
|
|
80
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" />
|
|
81
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" />
|
|
82
|
+
</circle>
|
|
83
|
+
<circle cx="27" cy="9.5" r="1.5" fill="currentColor" opacity="0.5">
|
|
84
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" begin="0.4s" />
|
|
85
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" begin="0.4s" />
|
|
86
|
+
</circle>
|
|
87
|
+
<circle cx="27" cy="22.5" r="1.5" fill="currentColor" opacity="0.5">
|
|
88
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" begin="0.8s" />
|
|
89
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" begin="0.8s" />
|
|
90
|
+
</circle>
|
|
91
|
+
<circle cx="16" cy="29" r="1.5" fill="currentColor" opacity="0.5">
|
|
92
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" begin="1.2s" />
|
|
93
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" begin="1.2s" />
|
|
94
|
+
</circle>
|
|
95
|
+
<circle cx="5" cy="22.5" r="1.5" fill="currentColor" opacity="0.5">
|
|
96
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" begin="1.6s" />
|
|
97
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" begin="1.6s" />
|
|
98
|
+
</circle>
|
|
99
|
+
<circle cx="5" cy="9.5" r="1.5" fill="currentColor" opacity="0.5">
|
|
100
|
+
<animate attributeName="r" values="1.2;2;1.2" dur="2.5s" repeatCount="indefinite" begin="2s" />
|
|
101
|
+
<animate attributeName="opacity" values="0.4;0.7;0.4" dur="2.5s" repeatCount="indefinite" begin="2s" />
|
|
102
|
+
</circle>
|
|
103
|
+
|
|
104
|
+
{/* Orbiting ring — subtle depth */}
|
|
105
|
+
<circle cx="16" cy="16" r="10" stroke="currentColor" strokeWidth="0.4" fill="none" opacity="0.15">
|
|
106
|
+
<animate attributeName="r" values="9;11;9" dur="4s" repeatCount="indefinite" />
|
|
107
|
+
<animate attributeName="opacity" values="0.1;0.25;0.1" dur="4s" repeatCount="indefinite" />
|
|
108
|
+
</circle>
|
|
109
|
+
</svg>
|
|
110
|
+
);
|
|
111
|
+
}
|