@ii_elif_ii/ui-node-tree 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.js ADDED
@@ -0,0 +1,843 @@
1
+ // src/components/node-tree.tsx
2
+ import * as React2 from "react";
3
+
4
+ // src/utils/cn.ts
5
+ import { clsx } from "clsx";
6
+ function cn(...inputs) {
7
+ return clsx(inputs);
8
+ }
9
+
10
+ // src/components/tree-connections.tsx
11
+ import { jsx } from "react/jsx-runtime";
12
+ function TreeConnections({
13
+ layoutState,
14
+ debug,
15
+ strokeColor,
16
+ strokeWidth,
17
+ opacity,
18
+ className
19
+ }) {
20
+ if (!layoutState.svgBounds) {
21
+ return null;
22
+ }
23
+ return /* @__PURE__ */ jsx(
24
+ "svg",
25
+ {
26
+ className: cn("unt-tree-connections", className),
27
+ width: layoutState.svgBounds.width,
28
+ height: layoutState.svgBounds.height,
29
+ viewBox: `0 0 ${layoutState.svgBounds.width} ${layoutState.svgBounds.height}`,
30
+ style: {
31
+ left: layoutState.svgBounds.offsetX,
32
+ top: layoutState.svgBounds.offsetY,
33
+ opacity
34
+ },
35
+ children: layoutState.segments.map((segment) => {
36
+ const debugPalette = [
37
+ "#22d3ee",
38
+ "#a855f7",
39
+ "#f59e0b",
40
+ "#10b981",
41
+ "#f97316",
42
+ "#38bdf8"
43
+ ];
44
+ const lineColor = debug ? debugPalette[segment.colorIndex % debugPalette.length] : strokeColor;
45
+ return /* @__PURE__ */ jsx(
46
+ "line",
47
+ {
48
+ x1: segment.x1 - layoutState.svgBounds.offsetX,
49
+ y1: segment.y1 - layoutState.svgBounds.offsetY,
50
+ x2: segment.x2 - layoutState.svgBounds.offsetX,
51
+ y2: segment.y2 - layoutState.svgBounds.offsetY,
52
+ className: "node-line",
53
+ style: {
54
+ strokeDasharray: segment.length,
55
+ strokeDashoffset: segment.length,
56
+ animationDelay: `${segment.delay}s`,
57
+ animationDuration: `${segment.duration}s`
58
+ },
59
+ stroke: lineColor,
60
+ strokeWidth,
61
+ strokeLinecap: "round"
62
+ },
63
+ `${segment.x1}-${segment.y1}-${segment.x2}-${segment.y2}-${segment.delay}`
64
+ );
65
+ })
66
+ }
67
+ );
68
+ }
69
+
70
+ // src/components/tree-renderer.tsx
71
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
72
+ function axisToFlexAlign(axis) {
73
+ if (axis === "start") {
74
+ return "flex-start";
75
+ }
76
+ if (axis === "end") {
77
+ return "flex-end";
78
+ }
79
+ return "center";
80
+ }
81
+ function axisToFlexJustify(axis) {
82
+ if (axis === "start") {
83
+ return "flex-start";
84
+ }
85
+ if (axis === "end") {
86
+ return "flex-end";
87
+ }
88
+ return "center";
89
+ }
90
+ function NodeFrame({ node, className, onRef, children, ...props }) {
91
+ return /* @__PURE__ */ jsx2(
92
+ "div",
93
+ {
94
+ ref: (element) => {
95
+ onRef(node.id, element);
96
+ },
97
+ className: cn("unt-tree-node-hit", className),
98
+ "data-nodeframe": true,
99
+ "data-viewport-no-pan": true,
100
+ ...props,
101
+ children
102
+ }
103
+ );
104
+ }
105
+ function renderTreeNode({
106
+ node,
107
+ index,
108
+ parentId,
109
+ depth,
110
+ path,
111
+ flowDown,
112
+ alignX,
113
+ alignY,
114
+ gap,
115
+ debug,
116
+ layoutState,
117
+ doneNodes,
118
+ registerNode,
119
+ nodeFrameClassName,
120
+ nodeFrameStyle
121
+ }) {
122
+ const stackUnder = !flowDown && node.children?.layout === "stack";
123
+ if (path.has(node.id)) {
124
+ return null;
125
+ }
126
+ path.add(node.id);
127
+ const childrenLayoutIsStack = node.children?.layout === "stack" || !flowDown;
128
+ const childCount = node.children?.nodes.length ?? 0;
129
+ const isLeaf = childCount === 0;
130
+ const pathIds = [...path];
131
+ const childrenContent = node.children?.nodes && node.children.nodes.length > 0 ? /* @__PURE__ */ jsx2(
132
+ "div",
133
+ {
134
+ className: "unt-tree-children",
135
+ style: {
136
+ display: "flex",
137
+ flexShrink: 0,
138
+ flexDirection: childrenLayoutIsStack ? "column" : "row",
139
+ alignItems: axisToFlexAlign(childrenLayoutIsStack ? alignX : alignY),
140
+ justifyContent: axisToFlexJustify(childrenLayoutIsStack ? alignY : alignX),
141
+ gap,
142
+ marginTop: flowDown || stackUnder ? gap : 0,
143
+ marginLeft: flowDown ? node.children?.layout === "stack" ? gap : 0 : stackUnder ? gap / 2 : gap
144
+ },
145
+ children: node.children.nodes.map(
146
+ (child, childIndex) => renderTreeNode({
147
+ node: child,
148
+ index: childIndex,
149
+ parentId: node.id,
150
+ depth: depth + 1,
151
+ path,
152
+ flowDown,
153
+ alignX,
154
+ alignY,
155
+ gap,
156
+ debug,
157
+ layoutState,
158
+ doneNodes,
159
+ registerNode,
160
+ nodeFrameClassName,
161
+ nodeFrameStyle
162
+ })
163
+ )
164
+ }
165
+ ) : null;
166
+ path.delete(node.id);
167
+ return /* @__PURE__ */ jsxs(
168
+ "div",
169
+ {
170
+ className: "unt-tree-node-wrap",
171
+ style: {
172
+ display: "flex",
173
+ position: "relative",
174
+ flexDirection: flowDown || stackUnder ? "column" : "row",
175
+ alignItems: axisToFlexAlign(flowDown ? alignX : alignY),
176
+ justifyContent: axisToFlexJustify(flowDown ? alignY : alignX)
177
+ },
178
+ children: [
179
+ /* @__PURE__ */ jsxs(
180
+ NodeFrame,
181
+ {
182
+ node,
183
+ className: cn("node-enter unt-tree-node-frame", nodeFrameClassName),
184
+ style: {
185
+ justifyContent: axisToFlexJustify(alignX),
186
+ animationDuration: `${layoutState.nodeAnimDuration}s`,
187
+ animationDelay: `${layoutState.nodeDelays.get(node.id) ?? depth * 0.08 + index * 0.04}s`,
188
+ ...nodeFrameStyle
189
+ },
190
+ onRef: registerNode,
191
+ children: [
192
+ debug ? /* @__PURE__ */ jsxs(
193
+ "div",
194
+ {
195
+ className: cn(
196
+ "unt-tree-debug-badge",
197
+ `unt-tree-debug-badge--${depth % 6}`
198
+ ),
199
+ children: [
200
+ /* @__PURE__ */ jsx2("div", { children: `DEPTH: ${depth}` }),
201
+ /* @__PURE__ */ jsx2("div", { children: `PARENT-ID: ${parentId ?? "root"}` }),
202
+ /* @__PURE__ */ jsx2("div", { children: `NODE-ID: ${node.id}` }),
203
+ /* @__PURE__ */ jsx2("div", { children: `C-LAYOUT: ${node.children?.layout ?? "N/A"}` })
204
+ ]
205
+ }
206
+ ) : null,
207
+ node.render({
208
+ node,
209
+ index,
210
+ depth,
211
+ parentId,
212
+ path: pathIds,
213
+ isLeaf,
214
+ childCount,
215
+ isNodeAnimationDone: doneNodes.has(node.id)
216
+ })
217
+ ]
218
+ }
219
+ ),
220
+ childrenContent
221
+ ]
222
+ },
223
+ `${node.id}-${index}`
224
+ );
225
+ }
226
+ function TreeRenderer({
227
+ nodeTree,
228
+ rootLayout,
229
+ flowDown,
230
+ alignX,
231
+ alignY,
232
+ gap,
233
+ debug,
234
+ layoutState,
235
+ doneNodes,
236
+ registerNode,
237
+ rendererClassName,
238
+ nodeFrameClassName,
239
+ nodeFrameStyle
240
+ }) {
241
+ const rootLayoutRow = rootLayout === "row";
242
+ return /* @__PURE__ */ jsx2(
243
+ "section",
244
+ {
245
+ className: cn("unt-tree-renderer", rendererClassName),
246
+ style: {
247
+ gap,
248
+ display: "flex",
249
+ width: "100%",
250
+ overflow: "visible",
251
+ position: "relative",
252
+ zIndex: 10,
253
+ flexDirection: rootLayoutRow ? "row" : "column",
254
+ alignItems: axisToFlexAlign(rootLayoutRow ? alignY : alignX),
255
+ justifyContent: axisToFlexJustify(rootLayoutRow ? alignX : alignY)
256
+ },
257
+ children: nodeTree.map(
258
+ (node, index) => renderTreeNode({
259
+ node,
260
+ index,
261
+ depth: 0,
262
+ path: /* @__PURE__ */ new Set(),
263
+ flowDown,
264
+ alignX,
265
+ alignY,
266
+ gap,
267
+ debug,
268
+ layoutState,
269
+ doneNodes,
270
+ registerNode,
271
+ nodeFrameClassName,
272
+ nodeFrameStyle
273
+ })
274
+ )
275
+ }
276
+ );
277
+ }
278
+
279
+ // src/hooks/use-node-tree-layout.ts
280
+ import * as React from "react";
281
+ var EMPTY_LAYOUT = {
282
+ segments: [],
283
+ nodeDelays: /* @__PURE__ */ new Map(),
284
+ nodeAnimDuration: 0.42,
285
+ animationTotal: 0,
286
+ svgBounds: null
287
+ };
288
+ function collectEdges(nodes) {
289
+ const edges = [];
290
+ const visiting = /* @__PURE__ */ new Set();
291
+ const visit = (node) => {
292
+ if (visiting.has(node.id)) {
293
+ return;
294
+ }
295
+ visiting.add(node.id);
296
+ if (node.children?.nodes && node.children.nodes.length > 0) {
297
+ node.children.nodes.forEach((child, index) => {
298
+ const key = `${node.id}=>${child.id}`;
299
+ edges.push({
300
+ key,
301
+ from: node.id,
302
+ to: child.id,
303
+ index,
304
+ count: node.children?.nodes.length ?? 1
305
+ });
306
+ visit(child);
307
+ });
308
+ }
309
+ visiting.delete(node.id);
310
+ };
311
+ nodes.forEach(visit);
312
+ return edges;
313
+ }
314
+ function collectDescendants(nodes) {
315
+ const map = /* @__PURE__ */ new Map();
316
+ const visiting = /* @__PURE__ */ new Set();
317
+ const visit = (node) => {
318
+ if (visiting.has(node.id)) {
319
+ return [];
320
+ }
321
+ visiting.add(node.id);
322
+ const descendants = [];
323
+ node.children?.nodes.forEach((child) => {
324
+ descendants.push(child.id);
325
+ descendants.push(...visit(child));
326
+ });
327
+ map.set(node.id, descendants);
328
+ visiting.delete(node.id);
329
+ return descendants;
330
+ };
331
+ nodes.forEach(visit);
332
+ return map;
333
+ }
334
+ function useNodeTreeLayout({
335
+ nodeTree,
336
+ direction,
337
+ gap,
338
+ padding,
339
+ animationSpeed,
340
+ debug,
341
+ containerRef,
342
+ nodeRefs
343
+ }) {
344
+ const [layoutState, setLayoutState] = React.useState(EMPTY_LAYOUT);
345
+ const [doneNodes, setDoneNodes] = React.useState(
346
+ () => /* @__PURE__ */ new Set()
347
+ );
348
+ const doneNodesRef = React.useRef(/* @__PURE__ */ new Set());
349
+ const edges = React.useMemo(() => collectEdges(nodeTree), [nodeTree]);
350
+ const descendantMap = React.useMemo(
351
+ () => collectDescendants(nodeTree),
352
+ [nodeTree]
353
+ );
354
+ const totalAnimationSec = Math.max(0.1, animationSpeed / 1e3);
355
+ const drawConnections = React.useCallback(() => {
356
+ const container = containerRef.current;
357
+ if (!container) {
358
+ return;
359
+ }
360
+ const flowDown = direction === "down";
361
+ const getRelativeRect = (element) => {
362
+ let left = 0;
363
+ let top = 0;
364
+ let current = element;
365
+ while (current && current !== container) {
366
+ left += current.offsetLeft;
367
+ top += current.offsetTop;
368
+ current = current.offsetParent;
369
+ }
370
+ return {
371
+ left,
372
+ top,
373
+ right: left + element.offsetWidth,
374
+ bottom: top + element.offsetHeight,
375
+ width: element.offsetWidth,
376
+ height: element.offsetHeight
377
+ };
378
+ };
379
+ const rectMap = /* @__PURE__ */ new Map();
380
+ nodeRefs.current.forEach((el, id) => {
381
+ rectMap.set(id, getRelativeRect(el));
382
+ });
383
+ const nextSegments = [];
384
+ const nextNodeDelays = /* @__PURE__ */ new Map();
385
+ const baseSecondsPerPixel = 1 / 900;
386
+ const baseNodeAnimDuration = 0.42;
387
+ const edgeColorIndex = debug ? /* @__PURE__ */ new Map() : null;
388
+ const edgeData = /* @__PURE__ */ new Map();
389
+ edges.forEach((edge) => {
390
+ const fromRect = rectMap.get(edge.from);
391
+ const toRect = rectMap.get(edge.to);
392
+ if (!fromRect || !toRect) {
393
+ return;
394
+ }
395
+ const fromX = flowDown ? fromRect.left + fromRect.width / 2 : fromRect.right;
396
+ const fromY = flowDown ? fromRect.bottom : fromRect.top + fromRect.height / 2;
397
+ const toX = flowDown ? toRect.left + toRect.width / 2 : toRect.left;
398
+ const toY = flowDown ? toRect.top : toRect.top + toRect.height / 2;
399
+ edgeData.set(edge.key, {
400
+ edge,
401
+ fromX,
402
+ fromY,
403
+ fromBottom: fromRect.bottom,
404
+ fromCenterX: fromRect.left + fromRect.width / 2,
405
+ toX,
406
+ toY,
407
+ toLeft: toRect.left,
408
+ toCenterY: toRect.top + toRect.height / 2
409
+ });
410
+ });
411
+ const pushSegment = (x1, y1, x2, y2, depth, delay, colorIndex, order) => {
412
+ const length = Math.hypot(x2 - x1, y2 - y1);
413
+ const duration = Math.max(0.05, length * baseSecondsPerPixel);
414
+ nextSegments.push({
415
+ x1,
416
+ y1,
417
+ x2,
418
+ y2,
419
+ length,
420
+ depth,
421
+ delay,
422
+ duration,
423
+ colorIndex,
424
+ order
425
+ });
426
+ return duration;
427
+ };
428
+ const visit = (node, depth, nodeDelay) => {
429
+ const existing = nextNodeDelays.get(node.id) ?? 0;
430
+ const resolvedDelay = Math.max(existing, nodeDelay);
431
+ nextNodeDelays.set(node.id, resolvedDelay);
432
+ const childEdges = node.children?.nodes.map((child) => edgeData.get(`${node.id}=>${child.id}`)).filter((edge) => Boolean(edge)) ?? [];
433
+ const stackLayout = node.children?.layout === "stack";
434
+ const descendantIds = flowDown && stackLayout ? descendantMap.get(node.id) ?? [] : [];
435
+ const descendantLefts = flowDown && stackLayout ? descendantIds.map((id) => rectMap.get(id)).filter((rect) => Boolean(rect)).map((rect) => rect.left) : [];
436
+ const descendantMinLeft = descendantLefts.length > 0 ? Math.min(...descendantLefts) : void 0;
437
+ const gutterX = flowDown && stackLayout ? (descendantMinLeft ?? (childEdges.length > 0 ? Math.min(...childEdges.map((edge) => edge.toLeft)) : 0)) - gap / 2 : 0;
438
+ const gutterXRight = !flowDown && stackLayout ? (childEdges.length > 0 ? Math.min(...childEdges.map((edge) => edge.toLeft)) : 0) - gap / 2 : 0;
439
+ const orderedChildren = node.children?.nodes.map((child) => {
440
+ const edge = edgeData.get(`${node.id}=>${child.id}`);
441
+ if (!edge) {
442
+ return null;
443
+ }
444
+ const dx = edge.toX - edge.fromX;
445
+ const dy = edge.toY - edge.fromY;
446
+ const length = Math.hypot(dx, dy);
447
+ return { child, edge, length, toX: edge.toX };
448
+ }).filter(
449
+ (entry) => Boolean(entry)
450
+ ) ?? [];
451
+ if (debug) {
452
+ orderedChildren.sort((a, b) => {
453
+ if (a.length !== b.length) {
454
+ return a.length - b.length;
455
+ }
456
+ return a.toX - b.toX;
457
+ });
458
+ }
459
+ orderedChildren.forEach((entry, index) => {
460
+ const { child, edge } = entry;
461
+ const edgeKey = edge.edge.key;
462
+ let colorIndex = 0;
463
+ if (edgeColorIndex) {
464
+ if (!edgeColorIndex.has(edgeKey)) {
465
+ edgeColorIndex.set(edgeKey, edgeColorIndex.size);
466
+ }
467
+ colorIndex = edgeColorIndex.get(edgeKey) ?? 0;
468
+ }
469
+ const order = orderedChildren.length - 1 - index;
470
+ const edgeDelay = resolvedDelay + baseNodeAnimDuration + index * 0.04;
471
+ let totalDuration = 0;
472
+ if (flowDown && stackLayout) {
473
+ const baseDrop = Math.max(12, gap / 2);
474
+ const targetY = edge.toCenterY;
475
+ const midY = edge.fromY + Math.min(baseDrop, (targetY - edge.fromY) * 0.6);
476
+ totalDuration += pushSegment(
477
+ edge.fromX,
478
+ edge.fromY,
479
+ edge.fromX,
480
+ midY,
481
+ depth,
482
+ edgeDelay,
483
+ colorIndex,
484
+ order
485
+ );
486
+ totalDuration += pushSegment(
487
+ edge.fromX,
488
+ midY,
489
+ gutterX,
490
+ midY,
491
+ depth,
492
+ edgeDelay + totalDuration,
493
+ colorIndex,
494
+ order
495
+ );
496
+ totalDuration += pushSegment(
497
+ gutterX,
498
+ midY,
499
+ gutterX,
500
+ targetY,
501
+ depth,
502
+ edgeDelay + totalDuration,
503
+ colorIndex,
504
+ order
505
+ );
506
+ totalDuration += pushSegment(
507
+ gutterX,
508
+ targetY,
509
+ edge.toLeft,
510
+ targetY,
511
+ depth,
512
+ edgeDelay + totalDuration,
513
+ colorIndex,
514
+ order
515
+ );
516
+ } else if (!flowDown && stackLayout) {
517
+ const targetY = edge.toCenterY;
518
+ const baseDrop = Math.max(12, gap / 2);
519
+ const midY = edge.fromBottom + Math.min(baseDrop, (targetY - edge.fromBottom) * 0.6);
520
+ totalDuration += pushSegment(
521
+ edge.fromCenterX,
522
+ edge.fromBottom,
523
+ edge.fromCenterX,
524
+ midY,
525
+ depth,
526
+ edgeDelay,
527
+ colorIndex,
528
+ order
529
+ );
530
+ totalDuration += pushSegment(
531
+ edge.fromCenterX,
532
+ midY,
533
+ gutterXRight,
534
+ midY,
535
+ depth,
536
+ edgeDelay + totalDuration,
537
+ colorIndex,
538
+ order
539
+ );
540
+ totalDuration += pushSegment(
541
+ gutterXRight,
542
+ midY,
543
+ gutterXRight,
544
+ targetY,
545
+ depth,
546
+ edgeDelay + totalDuration,
547
+ colorIndex,
548
+ order
549
+ );
550
+ totalDuration += pushSegment(
551
+ gutterXRight,
552
+ targetY,
553
+ edge.toLeft,
554
+ targetY,
555
+ depth,
556
+ edgeDelay + totalDuration,
557
+ colorIndex,
558
+ order
559
+ );
560
+ } else if (flowDown) {
561
+ const midY = edge.fromY + (edge.toY - edge.fromY) * 0.5;
562
+ totalDuration += pushSegment(
563
+ edge.fromX,
564
+ edge.fromY,
565
+ edge.fromX,
566
+ midY,
567
+ depth,
568
+ edgeDelay,
569
+ colorIndex,
570
+ order
571
+ );
572
+ totalDuration += pushSegment(
573
+ edge.fromX,
574
+ midY,
575
+ edge.toX,
576
+ midY,
577
+ depth,
578
+ edgeDelay + totalDuration,
579
+ colorIndex,
580
+ order
581
+ );
582
+ totalDuration += pushSegment(
583
+ edge.toX,
584
+ midY,
585
+ edge.toX,
586
+ edge.toY,
587
+ depth,
588
+ edgeDelay + totalDuration,
589
+ colorIndex,
590
+ order
591
+ );
592
+ } else {
593
+ const midX = edge.fromX + (edge.toX - edge.fromX) * 0.5;
594
+ totalDuration += pushSegment(
595
+ edge.fromX,
596
+ edge.fromY,
597
+ midX,
598
+ edge.fromY,
599
+ depth,
600
+ edgeDelay,
601
+ colorIndex,
602
+ order
603
+ );
604
+ totalDuration += pushSegment(
605
+ midX,
606
+ edge.fromY,
607
+ midX,
608
+ edge.toY,
609
+ depth,
610
+ edgeDelay + totalDuration,
611
+ colorIndex,
612
+ order
613
+ );
614
+ totalDuration += pushSegment(
615
+ midX,
616
+ edge.toY,
617
+ edge.toX,
618
+ edge.toY,
619
+ depth,
620
+ edgeDelay + totalDuration,
621
+ colorIndex,
622
+ order
623
+ );
624
+ }
625
+ visit(child, depth + 1, edgeDelay + totalDuration);
626
+ });
627
+ };
628
+ nodeTree.forEach((node) => visit(node, 0, 0));
629
+ if (nextSegments.length === 0) {
630
+ setLayoutState((prev) => ({
631
+ ...prev,
632
+ segments: [],
633
+ svgBounds: null,
634
+ animationTotal: 0
635
+ }));
636
+ setDoneNodes(/* @__PURE__ */ new Set());
637
+ doneNodesRef.current = /* @__PURE__ */ new Set();
638
+ return;
639
+ }
640
+ let minX = Infinity;
641
+ let minY = Infinity;
642
+ let maxX = -Infinity;
643
+ let maxY = -Infinity;
644
+ nextSegments.forEach((segment) => {
645
+ minX = Math.min(minX, segment.x1, segment.x2);
646
+ minY = Math.min(minY, segment.y1, segment.y2);
647
+ maxX = Math.max(maxX, segment.x1, segment.x2);
648
+ maxY = Math.max(maxY, segment.y1, segment.y2);
649
+ });
650
+ if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
651
+ return;
652
+ }
653
+ const width = Math.max(1, maxX - minX + padding * 2);
654
+ const height = Math.max(1, maxY - minY + padding * 2);
655
+ const offsetX = minX - padding;
656
+ const offsetY = minY - padding;
657
+ const maxLineEnd = Math.max(
658
+ 0,
659
+ ...nextSegments.map((segment) => segment.delay + segment.duration)
660
+ );
661
+ const maxNodeEnd = Math.max(0, ...Array.from(nextNodeDelays.values())) + baseNodeAnimDuration;
662
+ const animationMax = Math.max(maxLineEnd, maxNodeEnd);
663
+ const scale = animationMax > 0 ? totalAnimationSec / animationMax : 1;
664
+ const scaledSegments = nextSegments.map((segment) => ({
665
+ ...segment,
666
+ delay: segment.delay * scale,
667
+ duration: segment.duration * scale
668
+ })).sort((a, b) => a.order - b.order);
669
+ const scaledNodeDelays = /* @__PURE__ */ new Map();
670
+ nextNodeDelays.forEach((value, key) => {
671
+ scaledNodeDelays.set(key, value * scale);
672
+ });
673
+ setLayoutState({
674
+ segments: scaledSegments,
675
+ nodeDelays: scaledNodeDelays,
676
+ nodeAnimDuration: baseNodeAnimDuration * scale,
677
+ animationTotal: animationMax * scale,
678
+ svgBounds: { width, height, offsetX, offsetY }
679
+ });
680
+ setDoneNodes(/* @__PURE__ */ new Set());
681
+ doneNodesRef.current = /* @__PURE__ */ new Set();
682
+ }, [
683
+ animationSpeed,
684
+ containerRef,
685
+ debug,
686
+ descendantMap,
687
+ direction,
688
+ edges,
689
+ gap,
690
+ nodeRefs,
691
+ nodeTree,
692
+ padding,
693
+ totalAnimationSec
694
+ ]);
695
+ React.useLayoutEffect(() => {
696
+ const rafId = requestAnimationFrame(drawConnections);
697
+ return () => cancelAnimationFrame(rafId);
698
+ }, [drawConnections, nodeTree]);
699
+ React.useEffect(() => {
700
+ if (layoutState.animationTotal <= 0 || layoutState.nodeDelays.size === 0) {
701
+ return;
702
+ }
703
+ const entries = Array.from(layoutState.nodeDelays.entries()).map(([id, delay]) => ({
704
+ id,
705
+ end: delay + layoutState.nodeAnimDuration
706
+ })).sort((a, b) => a.end - b.end);
707
+ doneNodesRef.current = /* @__PURE__ */ new Set();
708
+ setDoneNodes(/* @__PURE__ */ new Set());
709
+ const start = performance.now();
710
+ let rafId = 0;
711
+ let index = 0;
712
+ const tick = () => {
713
+ const elapsed = (performance.now() - start) / 1e3;
714
+ let updated = false;
715
+ while (index < entries.length && elapsed >= entries[index].end) {
716
+ doneNodesRef.current.add(entries[index].id);
717
+ index += 1;
718
+ updated = true;
719
+ }
720
+ if (updated) {
721
+ setDoneNodes(new Set(doneNodesRef.current));
722
+ }
723
+ if (index < entries.length) {
724
+ rafId = requestAnimationFrame(tick);
725
+ }
726
+ };
727
+ rafId = requestAnimationFrame(tick);
728
+ return () => cancelAnimationFrame(rafId);
729
+ }, [
730
+ layoutState.animationTotal,
731
+ layoutState.nodeAnimDuration,
732
+ layoutState.nodeDelays
733
+ ]);
734
+ return { doneNodes, layoutState };
735
+ }
736
+
737
+ // src/components/node-tree.tsx
738
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
739
+ var NodeTree = React2.forwardRef(
740
+ ({
741
+ className,
742
+ nodeTree,
743
+ layout,
744
+ connection,
745
+ animation,
746
+ nodeFrame,
747
+ debug = false,
748
+ style,
749
+ ...props
750
+ }, ref) => {
751
+ const containerRef = React2.useRef(null);
752
+ const nodeRefs = React2.useRef(/* @__PURE__ */ new Map());
753
+ const registerNode = React2.useCallback(
754
+ (id, element) => {
755
+ const registry = nodeRefs.current;
756
+ if (element) {
757
+ registry.set(id, element);
758
+ } else {
759
+ registry.delete(id);
760
+ }
761
+ },
762
+ []
763
+ );
764
+ const resolvedAlign = layout?.align ?? "center";
765
+ const resolvedDirection = layout?.direction ?? "down";
766
+ const resolvedRootLayout = layout?.root ?? "stack";
767
+ const resolvedPaddingContainer = layout?.containerPadding ?? 128;
768
+ const resolvedPadding = layout?.padding ?? 64;
769
+ const resolvedGap = layout?.gap ?? 64;
770
+ const resolvedStrokeColor = connection?.color ?? "rgba(255,255,255)";
771
+ const resolvedStrokeWidth = connection?.width ?? 1;
772
+ const resolvedAnimationDurationMs = animation?.durationMs ?? 2e3;
773
+ const resolvedNodeFrameStyle = nodeFrame?.style;
774
+ const { doneNodes, layoutState } = useNodeTreeLayout({
775
+ nodeTree,
776
+ direction: resolvedDirection,
777
+ gap: resolvedGap,
778
+ padding: resolvedPadding,
779
+ animationSpeed: resolvedAnimationDurationMs,
780
+ debug,
781
+ containerRef,
782
+ nodeRefs
783
+ });
784
+ const flowDown = resolvedDirection === "down";
785
+ const alignValue = resolvedAlign;
786
+ const alignX = typeof alignValue === "string" ? alignValue : alignValue.x;
787
+ const alignY = typeof alignValue === "string" ? "start" : alignValue.y;
788
+ const resolvedConnectionOpacity = connection?.opacity ?? (debug ? 1 : 0.1);
789
+ return /* @__PURE__ */ jsx3(
790
+ "div",
791
+ {
792
+ ref,
793
+ className: cn("unt-tree-root-container", className?.root),
794
+ style,
795
+ ...props,
796
+ children: /* @__PURE__ */ jsxs2(
797
+ "div",
798
+ {
799
+ ref: containerRef,
800
+ className: cn("unt-tree-canvas", className?.canvas),
801
+ style: { padding: resolvedPaddingContainer },
802
+ children: [
803
+ /* @__PURE__ */ jsx3(
804
+ TreeConnections,
805
+ {
806
+ layoutState,
807
+ debug,
808
+ strokeColor: resolvedStrokeColor,
809
+ strokeWidth: resolvedStrokeWidth,
810
+ opacity: resolvedConnectionOpacity,
811
+ className: className?.connections
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsx3(
815
+ TreeRenderer,
816
+ {
817
+ nodeTree,
818
+ rootLayout: resolvedRootLayout,
819
+ flowDown,
820
+ alignX,
821
+ alignY,
822
+ gap: resolvedGap,
823
+ debug,
824
+ layoutState,
825
+ doneNodes,
826
+ registerNode,
827
+ rendererClassName: className?.renderer,
828
+ nodeFrameClassName: className?.frame,
829
+ nodeFrameStyle: resolvedNodeFrameStyle
830
+ }
831
+ )
832
+ ]
833
+ }
834
+ )
835
+ }
836
+ );
837
+ }
838
+ );
839
+ NodeTree.displayName = "NodeTree";
840
+ export {
841
+ NodeTree
842
+ };
843
+ //# sourceMappingURL=index.js.map