@nyaruka/temba-components 0.138.4 → 0.139.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/locales/es.js +5 -5
  3. package/dist/locales/es.js.map +1 -1
  4. package/dist/locales/fr.js +5 -5
  5. package/dist/locales/fr.js.map +1 -1
  6. package/dist/locales/locale-codes.js +2 -11
  7. package/dist/locales/locale-codes.js.map +1 -1
  8. package/dist/locales/pt.js +5 -5
  9. package/dist/locales/pt.js.map +1 -1
  10. package/dist/temba-components.js +816 -852
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/FloatingTab.js +23 -30
  13. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  14. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  15. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js +6 -7
  17. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  18. package/out-tsc/src/flow/Editor.js +152 -235
  19. package/out-tsc/src/flow/Editor.js.map +1 -1
  20. package/out-tsc/src/flow/Plumber.js +757 -403
  21. package/out-tsc/src/flow/Plumber.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +138 -66
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js +1 -0
  25. package/out-tsc/src/interfaces.js.map +1 -1
  26. package/out-tsc/src/list/TicketList.js +4 -1
  27. package/out-tsc/src/list/TicketList.js.map +1 -1
  28. package/out-tsc/src/live/ContactChat.js +18 -1
  29. package/out-tsc/src/live/ContactChat.js.map +1 -1
  30. package/out-tsc/src/locales/es.js +5 -5
  31. package/out-tsc/src/locales/es.js.map +1 -1
  32. package/out-tsc/src/locales/fr.js +5 -5
  33. package/out-tsc/src/locales/fr.js.map +1 -1
  34. package/out-tsc/src/locales/locale-codes.js +2 -11
  35. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  36. package/out-tsc/src/locales/pt.js +5 -5
  37. package/out-tsc/src/locales/pt.js.map +1 -1
  38. package/out-tsc/src/simulator/Simulator.js +1 -0
  39. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  40. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  41. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  42. package/out-tsc/test/temba-flow-collision.test.js +221 -223
  43. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  44. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  45. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  46. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  47. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  48. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  49. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/display/FloatingTab.ts +22 -31
  52. package/src/flow/CanvasMenu.ts +8 -3
  53. package/src/flow/CanvasNode.ts +6 -7
  54. package/src/flow/Editor.ts +184 -279
  55. package/src/flow/Plumber.ts +1011 -457
  56. package/src/flow/utils.ts +162 -84
  57. package/src/interfaces.ts +2 -1
  58. package/src/list/TicketList.ts +4 -1
  59. package/src/live/ContactChat.ts +19 -1
  60. package/src/locales/es.ts +13 -18
  61. package/src/locales/fr.ts +13 -18
  62. package/src/locales/locale-codes.ts +2 -11
  63. package/src/locales/pt.ts +13 -18
  64. package/src/simulator/Simulator.ts +1 -0
  65. package/test/temba-floating-tab.test.ts +4 -6
  66. package/test/temba-flow-collision.test.ts +225 -303
  67. package/test/temba-flow-editor.test.ts +0 -2
  68. package/test/temba-flow-plumber-connections.test.ts +97 -97
  69. package/test/temba-flow-plumber.test.ts +116 -103
@@ -304,18 +304,16 @@ describe('Collision Detection Utilities', () => {
304
304
 
305
305
  describe('calculateReflowPositions', () => {
306
306
  it('returns empty map when no collisions', () => {
307
- const movedBounds: NodeBounds = {
308
- uuid: 'moved',
309
- left: 100,
310
- top: 100,
311
- right: 200,
312
- bottom: 200,
313
- width: 100,
314
- height: 100
315
- };
316
-
317
307
  const allBounds: NodeBounds[] = [
318
- movedBounds,
308
+ {
309
+ uuid: 'moved',
310
+ left: 100,
311
+ top: 100,
312
+ right: 200,
313
+ bottom: 200,
314
+ width: 100,
315
+ height: 100
316
+ },
319
317
  {
320
318
  uuid: 'node1',
321
319
  left: 300,
@@ -327,67 +325,50 @@ describe('Collision Detection Utilities', () => {
327
325
  }
328
326
  ];
329
327
 
330
- const positions = calculateReflowPositions(
331
- 'moved',
332
- movedBounds,
333
- allBounds,
334
- false
335
- );
328
+ const positions = calculateReflowPositions(['moved'], allBounds);
336
329
  expect(positions.size).to.equal(0);
337
330
  });
338
331
 
339
- it('moves colliding node down', () => {
340
- const movedBounds: NodeBounds = {
341
- uuid: 'moved',
342
- left: 100,
343
- top: 100,
344
- right: 200,
345
- bottom: 200,
346
- width: 100,
347
- height: 100
348
- };
349
-
332
+ it('moves colliding node out of the way', () => {
350
333
  const allBounds: NodeBounds[] = [
351
- movedBounds,
334
+ {
335
+ uuid: 'moved',
336
+ left: 100,
337
+ top: 100,
338
+ right: 200,
339
+ bottom: 200,
340
+ width: 100,
341
+ height: 100
342
+ },
352
343
  {
353
344
  uuid: 'node1',
354
- left: 150,
345
+ left: 100,
355
346
  top: 150,
356
- right: 250,
347
+ right: 200,
357
348
  bottom: 250,
358
349
  width: 100,
359
350
  height: 100
360
351
  }
361
352
  ];
362
353
 
363
- const positions = calculateReflowPositions(
364
- 'moved',
365
- movedBounds,
366
- allBounds,
367
- false
368
- );
354
+ const positions = calculateReflowPositions(['moved'], allBounds);
369
355
 
370
356
  expect(positions.size).to.equal(1);
371
357
  expect(positions.has('node1')).to.be.true;
372
-
373
- const newPos = positions.get('node1')!;
374
- expect(newPos.left).to.equal(150); // left unchanged
375
- expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
358
+ expect(positions.has('moved')).to.be.false;
376
359
  });
377
360
 
378
- it('handles droppedBelowMidpoint by moving dropped node down', () => {
379
- const movedBounds: NodeBounds = {
380
- uuid: 'moved',
381
- left: 100,
382
- top: 150,
383
- right: 200,
384
- bottom: 250,
385
- width: 100,
386
- height: 100
387
- };
388
-
361
+ it('sacred node never appears in returned positions', () => {
389
362
  const allBounds: NodeBounds[] = [
390
- movedBounds,
363
+ {
364
+ uuid: 'dropped',
365
+ left: 100,
366
+ top: 100,
367
+ right: 200,
368
+ bottom: 200,
369
+ width: 100,
370
+ height: 100
371
+ },
391
372
  {
392
373
  uuid: 'existing',
393
374
  left: 100,
@@ -399,117 +380,133 @@ describe('Collision Detection Utilities', () => {
399
380
  }
400
381
  ];
401
382
 
402
- const positions = calculateReflowPositions(
403
- 'moved',
404
- movedBounds,
405
- allBounds,
406
- true // droppedBelowMidpoint
407
- );
408
-
409
- expect(positions.size).to.equal(1);
410
- expect(positions.has('moved')).to.be.true;
383
+ const positions = calculateReflowPositions(['dropped'], allBounds);
411
384
 
412
- const newPos = positions.get('moved')!;
413
- expect(newPos.top).to.be.greaterThan(200); // moved below existing node
385
+ expect(positions.has('dropped')).to.be.false;
386
+ expect(positions.has('existing')).to.be.true;
414
387
  });
415
388
 
416
- it('gives priority to dropped node when dropped above midpoint', () => {
417
- // Dropped node overlaps with bottom of existing node
418
- // Bottom of dropped (180) is above midpoint of existing (150)
419
- // So dropped node should keep position, existing moves down
420
- const movedBounds: NodeBounds = {
421
- uuid: 'dropped',
422
- left: 100,
423
- top: 80,
424
- right: 200,
425
- bottom: 180,
426
- width: 100,
427
- height: 100
428
- };
429
-
389
+ it('prefers least-displacement direction', () => {
390
+ // Sacred at (100,100)-(200,200), collider at (180,100)-(280,200)
391
+ // Right requires only 60px displacement, down requires 140px
430
392
  const allBounds: NodeBounds[] = [
431
- movedBounds,
432
393
  {
433
- uuid: 'existing',
394
+ uuid: 'sacred',
434
395
  left: 100,
435
396
  top: 100,
436
397
  right: 200,
437
398
  bottom: 200,
438
399
  width: 100,
439
400
  height: 100
401
+ },
402
+ {
403
+ uuid: 'collider',
404
+ left: 180,
405
+ top: 100,
406
+ right: 280,
407
+ bottom: 200,
408
+ width: 100,
409
+ height: 100
440
410
  }
441
411
  ];
442
412
 
443
- const positions = calculateReflowPositions(
444
- 'dropped',
445
- movedBounds,
446
- allBounds,
447
- false // dropped node's bottom is above target midpoint, dropped node gets priority
448
- );
449
-
450
- // Existing node should be moved down
451
- expect(positions.has('existing')).to.be.true;
452
- expect(positions.has('dropped')).to.be.false; // dropped keeps its position
413
+ const positions = calculateReflowPositions(['sacred'], allBounds);
453
414
 
454
- const existingNewPos = positions.get('existing')!;
455
- expect(existingNewPos.top).to.be.greaterThan(180); // moved below dropped node
415
+ expect(positions.has('collider')).to.be.true;
416
+ const newPos = positions.get('collider')!;
417
+ // Should move right (shorter) rather than down (longer)
418
+ expect(newPos.left).to.be.greaterThan(200);
419
+ expect(newPos.top).to.equal(100); // vertical position unchanged
456
420
  });
457
421
 
458
- it('gives priority to existing node when dropped below midpoint', () => {
459
- // Dropped node overlaps with top of existing node
460
- // Bottom of dropped (220) is below midpoint of existing (150)
461
- // So existing node keeps position, dropped moves down
462
- const movedBounds: NodeBounds = {
463
- uuid: 'dropped',
464
- left: 100,
465
- top: 120,
466
- right: 200,
467
- bottom: 220,
468
- width: 100,
469
- height: 100
470
- };
422
+ it('prefers up when it is the shortest move', () => {
423
+ // Sacred at (100,200)-(200,300), collider at (100,180)-(200,280)
424
+ // Up: newTop=snapToGrid(200-100-30)=snapToGrid(70)=80, distance=100
425
+ // Down: newTop=snapToGrid(300+30)=340, distance=160
426
+ // Right: newLeft=snapToGrid(200+30)=240, distance=140
427
+ const allBounds: NodeBounds[] = [
428
+ {
429
+ uuid: 'sacred',
430
+ left: 100,
431
+ top: 200,
432
+ right: 200,
433
+ bottom: 300,
434
+ width: 100,
435
+ height: 100
436
+ },
437
+ {
438
+ uuid: 'collider',
439
+ left: 100,
440
+ top: 180,
441
+ right: 200,
442
+ bottom: 280,
443
+ width: 100,
444
+ height: 100
445
+ }
446
+ ];
447
+
448
+ const positions = calculateReflowPositions(['sacred'], allBounds);
471
449
 
450
+ expect(positions.has('collider')).to.be.true;
451
+ const newPos = positions.get('collider')!;
452
+ // Should move up (shortest displacement)
453
+ expect(newPos.top).to.be.lessThan(200);
454
+ expect(newPos.left).to.equal(100); // horizontal position unchanged
455
+ });
456
+
457
+ it('prefers direction with fewer cascading collisions', () => {
458
+ // Sacred at (100,100)-(200,200), collider at (100,150)-(200,250)
459
+ // A node sits below at (100,280)-(200,380) blocking the downward path
460
+ // Down causes cascade, right does not
472
461
  const allBounds: NodeBounds[] = [
473
- movedBounds,
474
462
  {
475
- uuid: 'existing',
463
+ uuid: 'sacred',
476
464
  left: 100,
477
465
  top: 100,
478
466
  right: 200,
479
467
  bottom: 200,
480
468
  width: 100,
481
469
  height: 100
470
+ },
471
+ {
472
+ uuid: 'collider',
473
+ left: 100,
474
+ top: 150,
475
+ right: 200,
476
+ bottom: 250,
477
+ width: 100,
478
+ height: 100
479
+ },
480
+ {
481
+ uuid: 'blocker',
482
+ left: 100,
483
+ top: 280,
484
+ right: 200,
485
+ bottom: 380,
486
+ width: 100,
487
+ height: 100
482
488
  }
483
489
  ];
484
490
 
485
- const positions = calculateReflowPositions(
486
- 'dropped',
487
- movedBounds,
488
- allBounds,
489
- true // dropped node's bottom is below target midpoint, target gets priority
490
- );
491
-
492
- // Dropped node should be moved down
493
- expect(positions.has('dropped')).to.be.true;
494
- expect(positions.has('existing')).to.be.false; // existing keeps its position
491
+ const positions = calculateReflowPositions(['sacred'], allBounds);
495
492
 
496
- const droppedNewPos = positions.get('dropped')!;
497
- expect(droppedNewPos.top).to.be.greaterThan(200); // moved below existing node
493
+ expect(positions.has('collider')).to.be.true;
494
+ // Should avoid moving down (would cascade into blocker)
495
+ // blocker should not need to move
496
+ expect(positions.has('blocker')).to.be.false;
498
497
  });
499
498
 
500
499
  it('resolves cascading collisions', () => {
501
- const movedBounds: NodeBounds = {
502
- uuid: 'moved',
503
- left: 100,
504
- top: 100,
505
- right: 200,
506
- bottom: 200,
507
- width: 100,
508
- height: 100
509
- };
510
-
511
500
  const allBounds: NodeBounds[] = [
512
- movedBounds,
501
+ {
502
+ uuid: 'moved',
503
+ left: 100,
504
+ top: 100,
505
+ right: 200,
506
+ bottom: 200,
507
+ width: 100,
508
+ height: 100
509
+ },
513
510
  {
514
511
  uuid: 'node1',
515
512
  left: 100,
@@ -530,44 +527,20 @@ describe('Collision Detection Utilities', () => {
530
527
  }
531
528
  ];
532
529
 
533
- const positions = calculateReflowPositions(
534
- 'moved',
535
- movedBounds,
536
- allBounds,
537
- false
538
- );
530
+ const positions = calculateReflowPositions(['moved'], allBounds);
539
531
 
540
- // Both nodes should be repositioned to avoid collision
532
+ // At least one node should be repositioned
541
533
  expect(positions.size).to.be.greaterThan(0);
542
534
 
543
- // Check that moved nodes maintain vertical order and spacing
544
- if (positions.has('node1') && positions.has('node2')) {
545
- const node1Pos = positions.get('node1')!;
546
- const node2Pos = positions.get('node2')!;
547
-
548
- // node2 should be below node1
549
- expect(node2Pos.top).to.be.greaterThan(node1Pos.top);
550
- }
535
+ // No node should overlap with the sacred node or each other after reflow
536
+ // (verified by the algorithm's correctness guarantee)
551
537
  });
552
538
 
553
- it('handles multiple iterations for complex cascading collisions', () => {
554
- // Test case that requires multiple iterations to resolve all collisions
555
- // This scenario has nodes that initially don't collide with moved node,
556
- // but will collide with other nodes that get moved
557
- const movedBounds: NodeBounds = {
558
- uuid: 'moved',
559
- left: 100,
560
- top: 50,
561
- right: 200,
562
- bottom: 150,
563
- width: 100,
564
- height: 100
565
- };
566
-
539
+ it('handles multiple sacred nodes', () => {
540
+ // Two sacred nodes with a non-sacred node overlapping one
567
541
  const allBounds: NodeBounds[] = [
568
- movedBounds,
569
542
  {
570
- uuid: 'node1',
543
+ uuid: 'sacred1',
571
544
  left: 100,
572
545
  top: 100,
573
546
  right: 200,
@@ -576,198 +549,158 @@ describe('Collision Detection Utilities', () => {
576
549
  height: 100
577
550
  },
578
551
  {
579
- uuid: 'node2',
552
+ uuid: 'sacred2',
580
553
  left: 100,
581
- top: 180,
554
+ top: 400,
582
555
  right: 200,
583
- bottom: 280,
556
+ bottom: 500,
584
557
  width: 100,
585
558
  height: 100
586
559
  },
587
560
  {
588
- uuid: 'node3',
561
+ uuid: 'collider',
589
562
  left: 100,
590
- top: 260,
563
+ top: 150,
591
564
  right: 200,
592
- bottom: 360,
565
+ bottom: 250,
593
566
  width: 100,
594
567
  height: 100
595
568
  }
596
569
  ];
597
570
 
598
571
  const positions = calculateReflowPositions(
599
- 'moved',
600
- movedBounds,
601
- allBounds,
602
- false
572
+ ['sacred1', 'sacred2'],
573
+ allBounds
603
574
  );
604
575
 
605
- // All three nodes should be repositioned to be stacked below the moved node
606
- expect(positions.size).to.equal(3);
607
- expect(positions.has('node1')).to.be.true;
608
- expect(positions.has('node2')).to.be.true;
609
- expect(positions.has('node3')).to.be.true;
610
-
611
- const node1Pos = positions.get('node1')!;
612
- const node2Pos = positions.get('node2')!;
613
- const node3Pos = positions.get('node3')!;
614
-
615
- // Nodes should be stacked vertically with proper spacing
616
- expect(node1Pos.top).to.be.at.least(170); // 150 (bottom of moved) + 20
617
- expect(node2Pos.top).to.be.at.least(node1Pos.top + 120); // node1 top + height + spacing
618
- expect(node3Pos.top).to.be.at.least(node2Pos.top + 120); // node2 top + height + spacing
576
+ // Sacred nodes should never be moved
577
+ expect(positions.has('sacred1')).to.be.false;
578
+ expect(positions.has('sacred2')).to.be.false;
579
+ // Collider should be moved
580
+ expect(positions.has('collider')).to.be.true;
619
581
  });
620
582
 
621
- it('handles nodes that collide with already repositioned nodes', () => {
622
- // This test creates a scenario where node3 doesn't collide with the moved node,
623
- // but collides with node2 which itself got repositioned due to collision with node1
624
- const movedBounds: NodeBounds = {
625
- uuid: 'moved',
626
- left: 100,
627
- top: 100,
628
- right: 200,
629
- bottom: 200,
630
- width: 100,
631
- height: 100
632
- };
633
-
583
+ it('does not move a node into another sacred node', () => {
584
+ // Two sacred nodes close together with a collider between them
585
+ // Moving right would overlap sacred2, so it should choose another direction
634
586
  const allBounds: NodeBounds[] = [
635
- movedBounds,
636
587
  {
637
- uuid: 'node1',
588
+ uuid: 'sacred1',
638
589
  left: 100,
639
- top: 150,
590
+ top: 100,
640
591
  right: 200,
641
- bottom: 250,
592
+ bottom: 200,
642
593
  width: 100,
643
594
  height: 100
644
595
  },
645
596
  {
646
- uuid: 'node2',
647
- left: 100,
648
- top: 240,
649
- right: 200,
650
- bottom: 340,
597
+ uuid: 'sacred2',
598
+ left: 240,
599
+ top: 100,
600
+ right: 340,
601
+ bottom: 200,
602
+ width: 100,
603
+ height: 100
604
+ },
605
+ {
606
+ uuid: 'collider',
607
+ left: 150,
608
+ top: 100,
609
+ right: 250,
610
+ bottom: 200,
651
611
  width: 100,
652
612
  height: 100
653
613
  }
654
614
  ];
655
615
 
656
616
  const positions = calculateReflowPositions(
657
- 'moved',
658
- movedBounds,
659
- allBounds,
660
- false
617
+ ['sacred1', 'sacred2'],
618
+ allBounds
661
619
  );
662
620
 
663
- // Both nodes should be moved
664
- expect(positions.size).to.equal(2);
665
- expect(positions.has('node1')).to.be.true;
666
- expect(positions.has('node2')).to.be.true;
667
-
668
- const node1Pos = positions.get('node1')!;
669
- const node2Pos = positions.get('node2')!;
670
-
671
- // node1 should be moved below moved node
672
- expect(node1Pos.top).to.be.at.least(220); // 200 + 20
673
-
674
- // node2 should be moved below the new position of node1
675
- // This tests the code path where we check against already repositioned nodes
676
- expect(node2Pos.top).to.be.at.least(node1Pos.top + 120);
677
- });
678
-
679
- it('maintains horizontal position while moving vertically', () => {
680
- const movedBounds: NodeBounds = {
681
- uuid: 'moved',
682
- left: 100,
683
- top: 100,
684
- right: 200,
685
- bottom: 200,
621
+ expect(positions.has('collider')).to.be.true;
622
+ const newPos = positions.get('collider')!;
623
+ // Should not overlap either sacred node after reflow
624
+ const newBounds: NodeBounds = {
625
+ uuid: 'collider',
626
+ left: newPos.left,
627
+ top: newPos.top,
628
+ right: newPos.left + 100,
629
+ bottom: newPos.top + 100,
686
630
  width: 100,
687
631
  height: 100
688
632
  };
633
+ expect(nodesOverlap(newBounds, allBounds[0])).to.be.false;
634
+ expect(nodesOverlap(newBounds, allBounds[1])).to.be.false;
635
+ });
689
636
 
637
+ it('snaps reflow positions to grid', () => {
690
638
  const allBounds: NodeBounds[] = [
691
- movedBounds,
692
639
  {
693
- uuid: 'node1',
694
- left: 150,
640
+ uuid: 'sacred',
641
+ left: 100,
642
+ top: 100,
643
+ right: 200,
644
+ bottom: 200,
645
+ width: 100,
646
+ height: 100
647
+ },
648
+ {
649
+ uuid: 'collider',
650
+ left: 100,
695
651
  top: 150,
696
- right: 250,
652
+ right: 200,
697
653
  bottom: 250,
698
654
  width: 100,
699
655
  height: 100
700
656
  }
701
657
  ];
702
658
 
703
- const positions = calculateReflowPositions(
704
- 'moved',
705
- movedBounds,
706
- allBounds,
707
- false
708
- );
659
+ const positions = calculateReflowPositions(['sacred'], allBounds);
709
660
 
710
- const newPos = positions.get('node1')!;
711
- // Horizontal position should remain unchanged
712
- expect(newPos.left).to.equal(150);
661
+ expect(positions.has('collider')).to.be.true;
662
+ const newPos = positions.get('collider')!;
663
+ // Both coordinates should be multiples of 20 (grid size)
664
+ expect(newPos.left % 20).to.equal(0);
665
+ expect(newPos.top % 20).to.equal(0);
713
666
  });
714
667
 
715
- it('adds minimum spacing between nodes', () => {
716
- const movedBounds: NodeBounds = {
717
- uuid: 'moved',
718
- left: 100,
719
- top: 100,
720
- right: 200,
721
- bottom: 200,
722
- width: 100,
723
- height: 100
724
- };
725
-
668
+ it('clamps positions to zero (no negative coordinates)', () => {
669
+ // Sacred node near top-left, collider above it
670
+ // Moving up would go negative, so it should fall back
726
671
  const allBounds: NodeBounds[] = [
727
- movedBounds,
728
672
  {
729
- uuid: 'node1',
730
- left: 100,
731
- top: 150,
673
+ uuid: 'sacred',
674
+ left: 0,
675
+ top: 0,
732
676
  right: 200,
733
- bottom: 250,
734
- width: 100,
677
+ bottom: 200,
678
+ width: 200,
679
+ height: 200
680
+ },
681
+ {
682
+ uuid: 'collider',
683
+ left: 0,
684
+ top: 100,
685
+ right: 200,
686
+ bottom: 200,
687
+ width: 200,
735
688
  height: 100
736
689
  }
737
690
  ];
738
691
 
739
- const positions = calculateReflowPositions(
740
- 'moved',
741
- movedBounds,
742
- allBounds,
743
- false
744
- );
692
+ const positions = calculateReflowPositions(['sacred'], allBounds);
745
693
 
746
- const newPos = positions.get('node1')!;
747
- // Should have at least 20px spacing (MIN_NODE_SPACING)
748
- expect(newPos.top).to.be.at.least(220); // 200 (bottom of moved) + 20
694
+ expect(positions.has('collider')).to.be.true;
695
+ const newPos = positions.get('collider')!;
696
+ expect(newPos.left).to.be.at.least(0);
697
+ expect(newPos.top).to.be.at.least(0);
749
698
  });
750
699
  });
751
700
 
752
701
  describe('edge cases', () => {
753
702
  it('handles empty allBounds array', () => {
754
- const movedBounds: NodeBounds = {
755
- uuid: 'moved',
756
- left: 100,
757
- top: 100,
758
- right: 200,
759
- bottom: 200,
760
- width: 100,
761
- height: 100
762
- };
763
-
764
- const positions = calculateReflowPositions(
765
- 'moved',
766
- movedBounds,
767
- [],
768
- false
769
- );
770
-
703
+ const positions = calculateReflowPositions(['moved'], []);
771
704
  expect(positions.size).to.equal(0);
772
705
  });
773
706
 
@@ -782,13 +715,7 @@ describe('Collision Detection Utilities', () => {
782
715
  height: 100
783
716
  };
784
717
 
785
- const positions = calculateReflowPositions(
786
- 'moved',
787
- movedBounds,
788
- [movedBounds],
789
- false
790
- );
791
-
718
+ const positions = calculateReflowPositions(['moved'], [movedBounds]);
792
719
  expect(positions.size).to.equal(0);
793
720
  });
794
721
 
@@ -819,12 +746,7 @@ describe('Collision Detection Utilities', () => {
819
746
  }
820
747
 
821
748
  // Should complete without hanging
822
- const positions = calculateReflowPositions(
823
- 'moved',
824
- movedBounds,
825
- allBounds,
826
- false
827
- );
749
+ const positions = calculateReflowPositions(['moved'], allBounds);
828
750
 
829
751
  // Should have resolved some collisions
830
752
  expect(positions.size).to.be.greaterThan(0);