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