beads-map 0.2.4 → 0.3.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 +3 -3
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- 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/route.js +2 -2
- package/.next/server/app/api/beads/stream/route.js +3 -3
- package/.next/server/app/api/beads.body +1 -1
- package/.next/server/app/api/config/route.js +2 -2
- 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 +5 -5
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/page-68492e6aaf15a6dd.js +1 -0
- package/.next/static/css/c854bc2280bc4b27.css +3 -0
- package/README.md +12 -4
- package/app/api/config/route.ts +2 -0
- package/app/globals.css +12 -0
- package/app/page.tsx +111 -6
- package/components/BeadTooltip.tsx +222 -0
- package/components/BeadsGraph.tsx +264 -56
- package/components/ContextMenu.tsx +49 -3
- package/lib/parse-beads.ts +2 -0
- package/lib/types.ts +2 -0
- package/package.json +1 -1
- package/public/image.png +0 -0
- package/.next/server/app/api/config.body +0 -1
- package/.next/server/app/api/config.meta +0 -1
- package/.next/static/chunks/666-fb778298a77f3754.js +0 -1
- package/.next/static/chunks/app/page-13ee27a84e4a0c70.js +0 -1
- package/.next/static/css/dbf588b653aa4019.css +0 -3
- /package/.next/static/{5zW6ptqKxGc0tcnRau9j2 → ac0cLw5kGBDWoceTBnu21}/_buildManifest.js +0 -0
- /package/.next/static/{5zW6ptqKxGc0tcnRau9j2 → ac0cLw5kGBDWoceTBnu21}/_ssgManifest.js +0 -0
|
@@ -9,7 +9,7 @@ import React, {
|
|
|
9
9
|
useRef,
|
|
10
10
|
useState,
|
|
11
11
|
} from "react";
|
|
12
|
-
import { forceCollide } from "d3-force";
|
|
12
|
+
import { forceCollide, forceRadial, forceX, forceY } from "d3-force";
|
|
13
13
|
import type { GraphNode, GraphLink } from "@/lib/types";
|
|
14
14
|
import { STATUS_COLORS, STATUS_LABELS, PREFIX_COLORS } from "@/lib/types";
|
|
15
15
|
|
|
@@ -18,9 +18,7 @@ import { STATUS_COLORS, STATUS_LABELS, PREFIX_COLORS } from "@/lib/types";
|
|
|
18
18
|
// that does NOT forward refs, breaking graphRef.current.centerAt/zoom/etc.
|
|
19
19
|
let _ForceGraph2DModule: React.ComponentType<any> | null = null;
|
|
20
20
|
|
|
21
|
-
type LayoutMode = "force" | "dag";
|
|
22
|
-
type ViewMode = "full" | "epics";
|
|
23
|
-
|
|
21
|
+
type LayoutMode = "force" | "dag" | "radial" | "cluster" | "spread";
|
|
24
22
|
export interface BeadsGraphHandle {
|
|
25
23
|
focusNode: (node: GraphNode) => void;
|
|
26
24
|
}
|
|
@@ -31,7 +29,7 @@ interface BeadsGraphProps {
|
|
|
31
29
|
selectedNode: GraphNode | null;
|
|
32
30
|
hoveredNode: GraphNode | null;
|
|
33
31
|
onNodeClick: (node: GraphNode) => void;
|
|
34
|
-
onNodeHover: (node: GraphNode | null) => void;
|
|
32
|
+
onNodeHover: (node: GraphNode | null, x: number, y: number) => void;
|
|
35
33
|
onBackgroundClick: () => void;
|
|
36
34
|
onNodeRightClick?: (node: GraphNode, event: MouseEvent) => void;
|
|
37
35
|
commentedNodeIds?: Map<string, number>;
|
|
@@ -41,6 +39,12 @@ interface BeadsGraphProps {
|
|
|
41
39
|
stats?: { total: number; edges: number; prefixes: string[] };
|
|
42
40
|
/** When a right sidebar (NodeDetail, Comments, Activity) is open, shift bottom-right legend inward */
|
|
43
41
|
sidebarOpen?: boolean;
|
|
42
|
+
/** Set of epic IDs that are currently collapsed */
|
|
43
|
+
collapsedEpicIds?: Set<string>;
|
|
44
|
+
/** Collapse all epics at once */
|
|
45
|
+
onCollapseAll?: () => void;
|
|
46
|
+
/** Expand all epics at once */
|
|
47
|
+
onExpandAll?: () => void;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// Node size calculation
|
|
@@ -208,6 +212,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
208
212
|
timelineActive,
|
|
209
213
|
stats,
|
|
210
214
|
sidebarOpen,
|
|
215
|
+
collapsedEpicIds,
|
|
216
|
+
onCollapseAll,
|
|
217
|
+
onExpandAll,
|
|
211
218
|
}, ref) {
|
|
212
219
|
const graphRef = useRef<any>(null);
|
|
213
220
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -247,8 +254,6 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
247
254
|
// Layout mode: "force" (physics-based) or "dag" (topological top-down)
|
|
248
255
|
const [layoutMode, setLayoutMode] = useState<LayoutMode>("dag");
|
|
249
256
|
|
|
250
|
-
// View mode: "full" (all nodes) or "epics" (collapse children into parent epics)
|
|
251
|
-
const [viewMode, setViewMode] = useState<ViewMode>("full");
|
|
252
257
|
|
|
253
258
|
|
|
254
259
|
|
|
@@ -272,6 +277,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
272
277
|
// Track which avatar is currently hovered to avoid redundant callbacks
|
|
273
278
|
const hoveredAvatarNodeRef = useRef<string | null>(null);
|
|
274
279
|
|
|
280
|
+
// Track last mouse position for passing coordinates with onNodeHover
|
|
281
|
+
const lastMouseRef = useRef({ x: 0, y: 0 });
|
|
282
|
+
|
|
275
283
|
// Ref for current viewNodes (used by mousemove handler to respect epics view)
|
|
276
284
|
const viewNodesRef = useRef<GraphNode[]>(nodes);
|
|
277
285
|
|
|
@@ -286,14 +294,14 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
286
294
|
};
|
|
287
295
|
const clustersRef = useRef<ClusterInfo[]>([]);
|
|
288
296
|
|
|
289
|
-
// Compute collapsed view when
|
|
290
|
-
// Builds a child
|
|
297
|
+
// Compute collapsed view when any epics are collapsed via collapsedEpicIds.
|
|
298
|
+
// Builds a child->parent map from parent-child dependencies and hierarchical IDs,
|
|
291
299
|
// then removes child nodes and remaps their links to the parent epic.
|
|
292
300
|
// Must be declared BEFORE effects that reference viewNodes/viewLinks.
|
|
293
301
|
const { viewNodes, viewLinks } = useMemo(() => {
|
|
294
|
-
if (
|
|
302
|
+
if (!collapsedEpicIds || collapsedEpicIds.size === 0) return { viewNodes: nodes, viewLinks: links };
|
|
295
303
|
|
|
296
|
-
// Build child
|
|
304
|
+
// Build child->parent map from parent-child links
|
|
297
305
|
const childToParent = new Map<string, string>();
|
|
298
306
|
for (const link of links) {
|
|
299
307
|
const src = typeof link.source === "object" ? (link.source as any).id : link.source;
|
|
@@ -303,7 +311,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
303
311
|
childToParent.set(tgt, src);
|
|
304
312
|
}
|
|
305
313
|
}
|
|
306
|
-
// Fallback: infer from hierarchical IDs (e.g., "beads-map-3r3.1"
|
|
314
|
+
// Fallback: infer from hierarchical IDs (e.g., "beads-map-3r3.1" -> parent "beads-map-3r3")
|
|
307
315
|
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
308
316
|
for (const node of nodes) {
|
|
309
317
|
if (!childToParent.has(node.id) && node.id.includes(".")) {
|
|
@@ -314,13 +322,27 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
314
322
|
}
|
|
315
323
|
}
|
|
316
324
|
|
|
317
|
-
|
|
325
|
+
// Collapse children whose parent is in collapsedEpicIds
|
|
326
|
+
const childIds = new Set<string>();
|
|
327
|
+
for (const [childId, parentId] of childToParent) {
|
|
328
|
+
if (collapsedEpicIds.has(parentId)) {
|
|
329
|
+
childIds.add(childId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (childIds.size === 0) return { viewNodes: nodes, viewLinks: links };
|
|
334
|
+
|
|
335
|
+
// Also build a filtered childToParent for only the collapsed children (for link remapping)
|
|
336
|
+
const collapsedChildToParent = new Map<string, string>();
|
|
337
|
+
for (const childId of childIds) {
|
|
338
|
+
collapsedChildToParent.set(childId, childToParent.get(childId)!);
|
|
339
|
+
}
|
|
318
340
|
|
|
319
341
|
// Accumulate collapsed children count and extra connections onto parent nodes
|
|
320
342
|
const collapsedCounts = new Map<string, number>();
|
|
321
343
|
const extraBlockerCount = new Map<string, number>();
|
|
322
344
|
const extraDependentCount = new Map<string, number>();
|
|
323
|
-
for (const [childId, parentId] of
|
|
345
|
+
for (const [childId, parentId] of collapsedChildToParent) {
|
|
324
346
|
collapsedCounts.set(parentId, (collapsedCounts.get(parentId) || 0) + 1);
|
|
325
347
|
const child = nodes.find((n) => n.id === childId);
|
|
326
348
|
if (child) {
|
|
@@ -329,7 +351,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
|
|
332
|
-
// Filter nodes: remove children, augment parents
|
|
354
|
+
// Filter nodes: remove collapsed children, augment their parents
|
|
333
355
|
const filteredNodes: GraphNode[] = nodes
|
|
334
356
|
.filter((n) => !childIds.has(n.id))
|
|
335
357
|
.map((n) => ({
|
|
@@ -339,15 +361,17 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
339
361
|
collapsedCount: collapsedCounts.get(n.id) || 0,
|
|
340
362
|
}));
|
|
341
363
|
|
|
342
|
-
// Remap links: replace child IDs with parent IDs, drop parent-child
|
|
364
|
+
// Remap links: replace collapsed child IDs with parent IDs, drop internal parent-child links
|
|
343
365
|
const remappedLinks: GraphLink[] = [];
|
|
344
366
|
const linkSeen = new Set<string>();
|
|
345
367
|
for (const link of links) {
|
|
346
|
-
if (link.type === "parent-child") continue; // internal to collapsed epic
|
|
347
368
|
let src = typeof link.source === "object" ? (link.source as any).id : link.source;
|
|
348
369
|
let tgt = typeof link.target === "object" ? (link.target as any).id : link.target;
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
// Drop parent-child links where the child is collapsed
|
|
371
|
+
if (link.type === "parent-child" && childIds.has(tgt)) continue;
|
|
372
|
+
// Remap collapsed child endpoints to their parent
|
|
373
|
+
src = collapsedChildToParent.get(src) || src;
|
|
374
|
+
tgt = collapsedChildToParent.get(tgt) || tgt;
|
|
351
375
|
if (src === tgt) continue; // self-link after collapse
|
|
352
376
|
const key = `${src}->${tgt}:${link.type}`;
|
|
353
377
|
if (linkSeen.has(key)) continue;
|
|
@@ -356,7 +380,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
356
380
|
}
|
|
357
381
|
|
|
358
382
|
return { viewNodes: filteredNodes, viewLinks: remappedLinks };
|
|
359
|
-
}, [nodes, links,
|
|
383
|
+
}, [nodes, links, collapsedEpicIds]);
|
|
360
384
|
|
|
361
385
|
// Keep viewNodesRef in sync for mousemove avatar hit-testing
|
|
362
386
|
viewNodesRef.current = viewNodes;
|
|
@@ -463,6 +487,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
463
487
|
if (!container) return;
|
|
464
488
|
|
|
465
489
|
const handleMouseMove = (e: MouseEvent) => {
|
|
490
|
+
// Track last mouse position for onNodeHover coordinates
|
|
491
|
+
lastMouseRef.current = { x: e.clientX, y: e.clientY };
|
|
492
|
+
|
|
466
493
|
const fg = graphRef.current;
|
|
467
494
|
const cb = onAvatarHoverRef.current;
|
|
468
495
|
if (!fg || !cb) return;
|
|
@@ -575,16 +602,139 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
575
602
|
const fg = graphRef.current;
|
|
576
603
|
if (!fg || viewNodes.length === 0) return;
|
|
577
604
|
|
|
605
|
+
// Helper: clear custom forces that only specific layouts use.
|
|
606
|
+
// Must be called at the start of every branch to prevent stale forces.
|
|
607
|
+
const clearCustomForces = () => {
|
|
608
|
+
fg.d3Force("radial", null);
|
|
609
|
+
fg.d3Force("x", null);
|
|
610
|
+
fg.d3Force("y", null);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Helper: clear fixed positions left over from DAG mode.
|
|
614
|
+
const clearFixedPositions = () => {
|
|
615
|
+
viewNodes.forEach((node: any) => {
|
|
616
|
+
delete node.fx;
|
|
617
|
+
delete node.fy;
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
|
|
578
621
|
if (layoutMode === "dag") {
|
|
579
622
|
// DAG mode: weaker charge since vertical spacing is handled by layers.
|
|
623
|
+
clearCustomForces();
|
|
580
624
|
fg.d3Force("charge")?.strength(-80).distanceMax(300);
|
|
581
625
|
fg.d3Force("link")?.distance(50).strength(0.3);
|
|
582
626
|
fg.d3Force("center")?.strength(0.01);
|
|
583
|
-
|
|
584
627
|
// Remove collision in DAG mode (layers handle vertical separation)
|
|
585
628
|
fg.d3Force("collision", null);
|
|
629
|
+
|
|
630
|
+
} else if (layoutMode === "radial") {
|
|
631
|
+
// Radial layout: concentric rings by dependency depth.
|
|
632
|
+
// Compute BFS depth from root nodes (no incoming blocks edges).
|
|
633
|
+
clearCustomForces();
|
|
634
|
+
clearFixedPositions();
|
|
635
|
+
|
|
636
|
+
const incoming = new Map<string, string[]>();
|
|
637
|
+
for (const link of viewLinks) {
|
|
638
|
+
if (link.type === "parent-child") continue;
|
|
639
|
+
const tgt = typeof link.target === "object" ? (link.target as any).id : link.target;
|
|
640
|
+
const src = typeof link.source === "object" ? (link.source as any).id : link.source;
|
|
641
|
+
if (!incoming.has(tgt)) incoming.set(tgt, []);
|
|
642
|
+
incoming.get(tgt)!.push(src);
|
|
643
|
+
}
|
|
644
|
+
const depthMap = new Map<string, number>();
|
|
645
|
+
const queue: string[] = [];
|
|
646
|
+
viewNodes.forEach((n: any) => {
|
|
647
|
+
if (!incoming.has(n.id)) { depthMap.set(n.id, 0); queue.push(n.id); }
|
|
648
|
+
});
|
|
649
|
+
let qi = 0;
|
|
650
|
+
while (qi < queue.length) {
|
|
651
|
+
const id = queue[qi++];
|
|
652
|
+
const d = depthMap.get(id)!;
|
|
653
|
+
for (const link of viewLinks) {
|
|
654
|
+
if (link.type === "parent-child") continue;
|
|
655
|
+
const src = typeof link.source === "object" ? (link.source as any).id : link.source;
|
|
656
|
+
const tgt = typeof link.target === "object" ? (link.target as any).id : link.target;
|
|
657
|
+
if (src === id && !depthMap.has(tgt)) {
|
|
658
|
+
depthMap.set(tgt, d + 1);
|
|
659
|
+
queue.push(tgt);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Store depth transiently on each node for the radial force accessor
|
|
664
|
+
viewNodes.forEach((n: any) => { n._depth = depthMap.get(n.id) ?? 0; });
|
|
665
|
+
|
|
666
|
+
// Scale ring spacing by node count so rings don't overlap
|
|
667
|
+
const maxDepth = Math.max(1, ...Array.from(depthMap.values()));
|
|
668
|
+
const ringSpacing = Math.max(200, viewNodes.length * 4);
|
|
669
|
+
|
|
670
|
+
fg.d3Force("charge")?.strength(-300).distanceMax(800);
|
|
671
|
+
fg.d3Force("link")?.distance(150).strength(0.15);
|
|
672
|
+
fg.d3Force("center")?.strength(0); // no center pull — radial handles centering
|
|
673
|
+
fg.d3Force("radial",
|
|
674
|
+
forceRadial(
|
|
675
|
+
(node: any) => ((node as any)._depth || 0) * ringSpacing,
|
|
676
|
+
0, 0
|
|
677
|
+
).strength(0.8)
|
|
678
|
+
);
|
|
679
|
+
fg.d3Force("x", null); // let radial + charge handle positioning
|
|
680
|
+
fg.d3Force("y", null);
|
|
681
|
+
fg.d3Force("collision",
|
|
682
|
+
forceCollide()
|
|
683
|
+
.radius((node: any) => getNodeSize(node as GraphNode) + 10)
|
|
684
|
+
.strength(0.9)
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
} else if (layoutMode === "cluster") {
|
|
688
|
+
// Cluster layout: group nodes by project prefix.
|
|
689
|
+
clearCustomForces();
|
|
690
|
+
clearFixedPositions();
|
|
691
|
+
|
|
692
|
+
const prefixes = [...new Set(viewNodes.map((n: any) => (n as GraphNode).prefix))];
|
|
693
|
+
// Scale cluster separation by total node count — more nodes need more space
|
|
694
|
+
const radius = Math.max(400, viewNodes.length * 5, prefixes.length * 150);
|
|
695
|
+
const prefixCenters = new Map<string, { x: number; y: number }>();
|
|
696
|
+
prefixes.forEach((prefix, i) => {
|
|
697
|
+
const angle = (2 * Math.PI * i) / prefixes.length - Math.PI / 2;
|
|
698
|
+
prefixCenters.set(prefix, {
|
|
699
|
+
x: Math.cos(angle) * radius,
|
|
700
|
+
y: Math.sin(angle) * radius,
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
fg.d3Force("charge")?.strength(-200).distanceMax(600);
|
|
705
|
+
fg.d3Force("link")?.distance(100).strength(0.15);
|
|
706
|
+
fg.d3Force("center")?.strength(0); // no center pull — x/y handle positioning
|
|
707
|
+
fg.d3Force("x",
|
|
708
|
+
forceX((node: any) => prefixCenters.get((node as GraphNode).prefix)?.x || 0).strength(0.5)
|
|
709
|
+
);
|
|
710
|
+
fg.d3Force("y",
|
|
711
|
+
forceY((node: any) => prefixCenters.get((node as GraphNode).prefix)?.y || 0).strength(0.5)
|
|
712
|
+
);
|
|
713
|
+
fg.d3Force("collision",
|
|
714
|
+
forceCollide()
|
|
715
|
+
.radius((node: any) => getNodeSize(node as GraphNode) + 10)
|
|
716
|
+
.strength(0.9)
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
} else if (layoutMode === "spread") {
|
|
720
|
+
// Spread layout: like force but maximally spaced for readability.
|
|
721
|
+
clearCustomForces();
|
|
722
|
+
clearFixedPositions();
|
|
723
|
+
|
|
724
|
+
fg.d3Force("charge")?.strength(-300).distanceMax(500);
|
|
725
|
+
fg.d3Force("link")?.distance(180).strength(0.4);
|
|
726
|
+
fg.d3Force("center")?.strength(0.02);
|
|
727
|
+
fg.d3Force("collision",
|
|
728
|
+
forceCollide()
|
|
729
|
+
.radius((node: any) => getNodeSize(node as GraphNode) + 8)
|
|
730
|
+
.strength(0.8)
|
|
731
|
+
);
|
|
732
|
+
|
|
586
733
|
} else {
|
|
587
|
-
// Force mode: full physics
|
|
734
|
+
// Force mode: full physics (default)
|
|
735
|
+
clearCustomForces();
|
|
736
|
+
clearFixedPositions();
|
|
737
|
+
|
|
588
738
|
fg.d3Force("charge")?.strength(-180).distanceMax(400);
|
|
589
739
|
fg.d3Force("link")
|
|
590
740
|
?.distance((link: any) => {
|
|
@@ -599,23 +749,11 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
599
749
|
})
|
|
600
750
|
.strength(0.6);
|
|
601
751
|
fg.d3Force("center")?.strength(0.03);
|
|
602
|
-
|
|
603
|
-
// Add collision force (prevent node overlap).
|
|
604
|
-
// Using static import so collision is active from tick 1 (including warmup).
|
|
605
|
-
fg.d3Force(
|
|
606
|
-
"collision",
|
|
752
|
+
fg.d3Force("collision",
|
|
607
753
|
forceCollide()
|
|
608
754
|
.radius((node: any) => getNodeSize(node as GraphNode) + 6)
|
|
609
755
|
.strength(0.7)
|
|
610
756
|
);
|
|
611
|
-
|
|
612
|
-
// Clear any fixed positions left over from DAG mode.
|
|
613
|
-
// The force simulation mutates node objects in-place, so the nodes
|
|
614
|
-
// prop array already has fx/fy set by DAG mode — delete them directly.
|
|
615
|
-
viewNodes.forEach((node: any) => {
|
|
616
|
-
delete node.fx;
|
|
617
|
-
delete node.fy;
|
|
618
|
-
});
|
|
619
757
|
}
|
|
620
758
|
|
|
621
759
|
// Re-heat simulation so new forces take effect immediately
|
|
@@ -630,7 +768,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
630
768
|
initialLayoutApplied.current = true;
|
|
631
769
|
|
|
632
770
|
return () => clearTimeout(timer);
|
|
633
|
-
}, [layoutMode, viewNodes
|
|
771
|
+
}, [layoutMode, viewNodes, viewLinks]);
|
|
634
772
|
|
|
635
773
|
// Bootstrap trick: start in DAG to spread nodes into good positions,
|
|
636
774
|
// then auto-switch to Force mode. This replicates the exact code path
|
|
@@ -1643,14 +1781,11 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1643
1781
|
<span className="hidden sm:inline">DAG</span>
|
|
1644
1782
|
</span>
|
|
1645
1783
|
</button>
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
{/* View mode toggle: Full / Epics */}
|
|
1649
|
-
<div className="flex bg-white/90 backdrop-blur-sm rounded-lg border border-zinc-200 shadow-sm overflow-hidden">
|
|
1784
|
+
<div className="w-px bg-zinc-200" />
|
|
1650
1785
|
<button
|
|
1651
|
-
onClick={() =>
|
|
1786
|
+
onClick={() => setLayoutMode("radial")}
|
|
1652
1787
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
1653
|
-
|
|
1788
|
+
layoutMode === "radial"
|
|
1654
1789
|
? "bg-emerald-500 text-white"
|
|
1655
1790
|
: "text-zinc-500 hover:text-zinc-700 hover:bg-zinc-50"
|
|
1656
1791
|
}`}
|
|
@@ -1663,21 +1798,47 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1663
1798
|
stroke="currentColor"
|
|
1664
1799
|
strokeWidth="1.5"
|
|
1665
1800
|
>
|
|
1666
|
-
{/*
|
|
1667
|
-
<circle cx="
|
|
1668
|
-
<circle cx="
|
|
1669
|
-
<circle cx="
|
|
1670
|
-
|
|
1671
|
-
|
|
1801
|
+
{/* Radial icon: concentric rings with center dot */}
|
|
1802
|
+
<circle cx="8" cy="8" r="2" fill="currentColor" stroke="none" />
|
|
1803
|
+
<circle cx="8" cy="8" r="5" fill="none" strokeOpacity="0.5" />
|
|
1804
|
+
<circle cx="8" cy="8" r="7.5" fill="none" strokeOpacity="0.3" />
|
|
1805
|
+
</svg>
|
|
1806
|
+
<span className="hidden sm:inline">Radial</span>
|
|
1807
|
+
</span>
|
|
1808
|
+
</button>
|
|
1809
|
+
<div className="w-px bg-zinc-200" />
|
|
1810
|
+
<button
|
|
1811
|
+
onClick={() => setLayoutMode("cluster")}
|
|
1812
|
+
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
1813
|
+
layoutMode === "cluster"
|
|
1814
|
+
? "bg-emerald-500 text-white"
|
|
1815
|
+
: "text-zinc-500 hover:text-zinc-700 hover:bg-zinc-50"
|
|
1816
|
+
}`}
|
|
1817
|
+
>
|
|
1818
|
+
<span className="flex items-center gap-1.5">
|
|
1819
|
+
<svg
|
|
1820
|
+
className="w-3.5 h-3.5"
|
|
1821
|
+
viewBox="0 0 16 16"
|
|
1822
|
+
fill="none"
|
|
1823
|
+
stroke="currentColor"
|
|
1824
|
+
strokeWidth="1.5"
|
|
1825
|
+
>
|
|
1826
|
+
{/* Cluster icon: two groups of dots */}
|
|
1827
|
+
<circle cx="3.5" cy="4" r="1.5" fill="currentColor" stroke="none" />
|
|
1828
|
+
<circle cx="6" cy="6" r="1.5" fill="currentColor" stroke="none" />
|
|
1829
|
+
<circle cx="3" cy="7" r="1.5" fill="currentColor" stroke="none" />
|
|
1830
|
+
<circle cx="11" cy="10" r="1.5" fill="currentColor" stroke="none" />
|
|
1831
|
+
<circle cx="13.5" cy="11.5" r="1.5" fill="currentColor" stroke="none" />
|
|
1832
|
+
<circle cx="11" cy="13" r="1.5" fill="currentColor" stroke="none" />
|
|
1672
1833
|
</svg>
|
|
1673
|
-
<span className="hidden sm:inline">
|
|
1834
|
+
<span className="hidden sm:inline">Cluster</span>
|
|
1674
1835
|
</span>
|
|
1675
1836
|
</button>
|
|
1676
1837
|
<div className="w-px bg-zinc-200" />
|
|
1677
1838
|
<button
|
|
1678
|
-
onClick={() =>
|
|
1839
|
+
onClick={() => setLayoutMode("spread")}
|
|
1679
1840
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
1680
|
-
|
|
1841
|
+
layoutMode === "spread"
|
|
1681
1842
|
? "bg-emerald-500 text-white"
|
|
1682
1843
|
: "text-zinc-500 hover:text-zinc-700 hover:bg-zinc-50"
|
|
1683
1844
|
}`}
|
|
@@ -1690,14 +1851,61 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1690
1851
|
stroke="currentColor"
|
|
1691
1852
|
strokeWidth="1.5"
|
|
1692
1853
|
>
|
|
1693
|
-
{/*
|
|
1694
|
-
<circle cx="
|
|
1695
|
-
<circle cx="
|
|
1854
|
+
{/* Spread icon: dots spread far apart */}
|
|
1855
|
+
<circle cx="2" cy="2" r="1.5" fill="currentColor" stroke="none" />
|
|
1856
|
+
<circle cx="14" cy="3" r="1.5" fill="currentColor" stroke="none" />
|
|
1857
|
+
<circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none" />
|
|
1858
|
+
<circle cx="3" cy="14" r="1.5" fill="currentColor" stroke="none" />
|
|
1859
|
+
<circle cx="13" cy="13" r="1.5" fill="currentColor" stroke="none" />
|
|
1696
1860
|
</svg>
|
|
1697
|
-
<span className="hidden sm:inline">
|
|
1861
|
+
<span className="hidden sm:inline">Spread</span>
|
|
1698
1862
|
</span>
|
|
1699
1863
|
</button>
|
|
1700
1864
|
</div>
|
|
1865
|
+
|
|
1866
|
+
{/* Collapse / Expand all toggle */}
|
|
1867
|
+
{(onCollapseAll || onExpandAll) && (
|
|
1868
|
+
<button
|
|
1869
|
+
onClick={collapsedEpicIds && collapsedEpicIds.size > 0 ? onExpandAll : onCollapseAll}
|
|
1870
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-white/90 backdrop-blur-sm rounded-lg border border-zinc-200 shadow-sm text-zinc-500 hover:text-zinc-700 hover:bg-zinc-50 transition-colors"
|
|
1871
|
+
>
|
|
1872
|
+
{collapsedEpicIds && collapsedEpicIds.size > 0 ? (
|
|
1873
|
+
<>
|
|
1874
|
+
<svg
|
|
1875
|
+
className="w-3.5 h-3.5"
|
|
1876
|
+
viewBox="0 0 24 24"
|
|
1877
|
+
fill="none"
|
|
1878
|
+
strokeWidth={1.5}
|
|
1879
|
+
stroke="currentColor"
|
|
1880
|
+
>
|
|
1881
|
+
<path
|
|
1882
|
+
strokeLinecap="round"
|
|
1883
|
+
strokeLinejoin="round"
|
|
1884
|
+
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
|
1885
|
+
/>
|
|
1886
|
+
</svg>
|
|
1887
|
+
<span className="hidden sm:inline">Expand all</span>
|
|
1888
|
+
</>
|
|
1889
|
+
) : (
|
|
1890
|
+
<>
|
|
1891
|
+
<svg
|
|
1892
|
+
className="w-3.5 h-3.5"
|
|
1893
|
+
viewBox="0 0 24 24"
|
|
1894
|
+
fill="none"
|
|
1895
|
+
strokeWidth={1.5}
|
|
1896
|
+
stroke="currentColor"
|
|
1897
|
+
>
|
|
1898
|
+
<path
|
|
1899
|
+
strokeLinecap="round"
|
|
1900
|
+
strokeLinejoin="round"
|
|
1901
|
+
d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25"
|
|
1902
|
+
/>
|
|
1903
|
+
</svg>
|
|
1904
|
+
<span className="hidden sm:inline">Collapse all</span>
|
|
1905
|
+
</>
|
|
1906
|
+
)}
|
|
1907
|
+
</button>
|
|
1908
|
+
)}
|
|
1701
1909
|
</div>
|
|
1702
1910
|
|
|
1703
1911
|
{/* Bottom-right info panel: stats + status colors + legend (hidden when timeline active) */}
|
|
@@ -1867,7 +2075,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1867
2075
|
// Interactions
|
|
1868
2076
|
onNodeClick={(node: any) => onNodeClick(node as GraphNode)}
|
|
1869
2077
|
onNodeHover={(node: any) =>
|
|
1870
|
-
onNodeHover(node ? (node as GraphNode) : null)
|
|
2078
|
+
onNodeHover(node ? (node as GraphNode) : null, lastMouseRef.current.x, lastMouseRef.current.y)
|
|
1871
2079
|
}
|
|
1872
2080
|
onNodeRightClick={(node: any, event: MouseEvent) => {
|
|
1873
2081
|
event.preventDefault();
|
|
@@ -11,6 +11,8 @@ interface ContextMenuProps {
|
|
|
11
11
|
onAddComment: () => void;
|
|
12
12
|
onClaimTask?: () => void;
|
|
13
13
|
onUnclaimTask?: () => void;
|
|
14
|
+
onCollapseEpic?: () => void;
|
|
15
|
+
onUncollapseEpic?: () => void;
|
|
14
16
|
onClose: () => void;
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -22,6 +24,8 @@ export function ContextMenu({
|
|
|
22
24
|
onAddComment,
|
|
23
25
|
onClaimTask,
|
|
24
26
|
onUnclaimTask,
|
|
27
|
+
onCollapseEpic,
|
|
28
|
+
onUncollapseEpic,
|
|
25
29
|
onClose,
|
|
26
30
|
}: ContextMenuProps) {
|
|
27
31
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
@@ -115,7 +119,7 @@ export function ContextMenu({
|
|
|
115
119
|
)}
|
|
116
120
|
<button
|
|
117
121
|
onClick={onAddComment}
|
|
118
|
-
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onClaimTask || onUnclaimTask ? " border-b border-zinc-100" : ""}`}
|
|
122
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onClaimTask || onUnclaimTask || onCollapseEpic || onUncollapseEpic ? " border-b border-zinc-100" : ""}`}
|
|
119
123
|
>
|
|
120
124
|
<svg
|
|
121
125
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -135,7 +139,7 @@ export function ContextMenu({
|
|
|
135
139
|
{onClaimTask && (
|
|
136
140
|
<button
|
|
137
141
|
onClick={onClaimTask}
|
|
138
|
-
className=
|
|
142
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onCollapseEpic || onUncollapseEpic ? " border-b border-zinc-100" : ""}`}
|
|
139
143
|
>
|
|
140
144
|
<svg
|
|
141
145
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -156,7 +160,7 @@ export function ContextMenu({
|
|
|
156
160
|
{onUnclaimTask && (
|
|
157
161
|
<button
|
|
158
162
|
onClick={onUnclaimTask}
|
|
159
|
-
className=
|
|
163
|
+
className={`w-full px-3 py-2.5 text-xs text-red-500 hover:bg-red-50 flex items-center gap-2 transition-colors${onCollapseEpic || onUncollapseEpic ? " border-b border-zinc-100" : ""}`}
|
|
160
164
|
>
|
|
161
165
|
<svg
|
|
162
166
|
className="w-3.5 h-3.5 text-red-400"
|
|
@@ -174,6 +178,48 @@ export function ContextMenu({
|
|
|
174
178
|
Unclaim task
|
|
175
179
|
</button>
|
|
176
180
|
)}
|
|
181
|
+
{onCollapseEpic && (
|
|
182
|
+
<button
|
|
183
|
+
onClick={onCollapseEpic}
|
|
184
|
+
className="w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors"
|
|
185
|
+
>
|
|
186
|
+
<svg
|
|
187
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
188
|
+
fill="none"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
strokeWidth={1.5}
|
|
191
|
+
stroke="currentColor"
|
|
192
|
+
>
|
|
193
|
+
<path
|
|
194
|
+
strokeLinecap="round"
|
|
195
|
+
strokeLinejoin="round"
|
|
196
|
+
d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25"
|
|
197
|
+
/>
|
|
198
|
+
</svg>
|
|
199
|
+
Collapse epic
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
{onUncollapseEpic && (
|
|
203
|
+
<button
|
|
204
|
+
onClick={onUncollapseEpic}
|
|
205
|
+
className="w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors"
|
|
206
|
+
>
|
|
207
|
+
<svg
|
|
208
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
209
|
+
fill="none"
|
|
210
|
+
viewBox="0 0 24 24"
|
|
211
|
+
strokeWidth={1.5}
|
|
212
|
+
stroke="currentColor"
|
|
213
|
+
>
|
|
214
|
+
<path
|
|
215
|
+
strokeLinecap="round"
|
|
216
|
+
strokeLinejoin="round"
|
|
217
|
+
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
|
218
|
+
/>
|
|
219
|
+
</svg>
|
|
220
|
+
Uncollapse epic
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
177
223
|
</div>
|
|
178
224
|
</div>
|
|
179
225
|
);
|
package/lib/parse-beads.ts
CHANGED
|
@@ -145,6 +145,8 @@ export function buildGraphData(
|
|
|
145
145
|
priority: issue.priority,
|
|
146
146
|
issueType: issue.issue_type,
|
|
147
147
|
owner: issue.owner,
|
|
148
|
+
assignee: issue.assignee,
|
|
149
|
+
createdBy: issue.created_by,
|
|
148
150
|
createdAt: issue.created_at,
|
|
149
151
|
updatedAt: issue.updated_at,
|
|
150
152
|
closedAt: issue.closed_at,
|
package/lib/types.ts
CHANGED
package/package.json
CHANGED
package/public/image.png
CHANGED
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"name":"beads-map","prefix":null,"repoCount":1,"repos":["."],"repoUrls":{"beads-map":"https://github.com/GainForest/beads-map"}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"status":200,"headers":{"content-type":"application/json","x-next-cache-tags":"_N_T_/layout,_N_T_/api/layout,_N_T_/api/config/layout,_N_T_/api/config/route,_N_T_/api/config"}}
|