lilact 0.26.0 → 0.26.2

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.
@@ -420,7 +420,7 @@ export function DragHandle({
420
420
  * @param max - Maximum allowed position. Defaults to `0.9`.
421
421
  Can also be a function that will be called with (containerWidth|containerHeight,splitterSize) as arguments
422
422
  * @param splitterSize - Thickness of the draggable splitter in pixels. Defaults to `8`.
423
- * @param resizeMode - The behavior on container resize. Its value can be `proportional`, `fixFirst` or `fixSecond`.
423
+ * @param resizePolicy - The behavior on container resize. Its value can be `proportional`, `fixFirst` or `fixSecond`.
424
424
  * Default is `proportional`.
425
425
  * @param onSizeChange - Callback invoked when the position changes. Receives the new normalized position.
426
426
  * @param style - Optional root container styles.
@@ -449,267 +449,391 @@ export function DragHandle({
449
449
  */
450
450
 
451
451
  export const SplitPane = forwardRef(function SplitPane(
452
- {
453
- mode = "horizontal",
454
- position,
455
- defaultPosition = 0.5,
456
- min = 0.1,
457
- max = 0.9,
458
- splitterSize = 8,
459
- onSizeChange,
460
- resizeMode = "proportional",
461
- style,
462
- className,
463
- firstPaneStyle,
464
- secondPaneStyle,
465
- splitterStyle,
466
- splitterChild,
467
- children,
468
- },
469
- ref
452
+ {
453
+ mode = "horizontal",
454
+ position,
455
+ defaultPosition = 0.5,
456
+ min = 0.1,
457
+ max = 0.9,
458
+ splitterSize = 8,
459
+ onSizeChange,
460
+ resizePolicy = "proportional",
461
+ style,
462
+ className,
463
+ firstPaneStyle,
464
+ secondPaneStyle,
465
+ splitterStyle,
466
+ splitterChild,
467
+ children,
468
+ },
469
+ ref
470
470
  ) {
471
- const containerRef = useRef(null);
472
- const sizeRef = useRef({ w: 0, h: 0 });
473
-
474
- const initialMode = mode === "vertical" ? "vertical" : "horizontal";
475
- const [internalMode, setInternalMode] = useState(initialMode);
476
-
477
- const [internalPos, setInternalPos] = useState(clamp(defaultPosition, min, max,
478
- mode==='verical'?sizeRef.current.h:sizeRef.current.w, splitterSize));
479
-
480
-
481
- let posResolved = position == null ? internalPos : position;
482
- posResolved = clamp(posResolved, min, max,
483
- mode==='verical'?sizeRef.current.h:sizeRef.current.w, splitterSize);
484
-
485
- useEffect(() => {
486
- if (position == null) return;
487
- setInternalPos(clamp(position, min, max,
488
- mode==='verical'?sizeRef.current.h:sizeRef.current.w, splitterSize));
489
- }, [position, min, max]);
490
-
491
- useEffect(() => {
492
- setInternalMode(mode === "vertical" ? "vertical" : "horizontal");
493
- }, [mode]);
494
-
495
- const setPosition = (next) => {
496
- const clamped = clamp(next, min, max,
497
- mode==='verical'?sizeRef.current.h:sizeRef.current.w, splitterSize);
498
- if (position == null) setInternalPos(clamped);
499
- onSizeChange?.(clamped);
500
- };
501
-
502
- useImperativeHandle(ref, () => ({
503
- setPosition,
504
- setMode: (nextMode) => setInternalMode(nextMode === "vertical" ? "vertical" : "horizontal"),
505
- getPosition: () => posResolved,
506
- getMode: () => internalMode,
507
- }));
508
-
509
-
510
- useLayoutEffect(() => {
511
- const el = containerRef.current;
512
- if (!el) return;
513
-
514
- const ro = new ResizeObserver(() => {
515
- const rect = el.getBoundingClientRect();
516
- const o = sizeRef.current;
517
- sizeRef.current = { w: rect.width, h: rect.height };
518
-
519
-
520
- if(ref.current?.getPosition) {
521
- if( resizeMode==='fixFirst') {
522
- if(internalMode==='vertical' && o.h>0) {
523
- setPosition( ref.current.getPosition() * o.h/sizeRef.current.h );
524
- }
525
- else if(o.w>0) {
526
- setPosition( ref.current.getPosition() * o.w/sizeRef.current.w );
527
- }
528
- }
529
- else if( resizeMode==='fixSecond') {
530
- if(internalMode==='vertical' && o.h>0) {
531
- setPosition( 1 - ( (1-ref.current.getPosition()) * o.h/sizeRef.current.h ) );
532
- }
533
- else if(o.w>0) {
534
- setPosition( 1 - ( (1-ref.current.getPosition()) * o.w/sizeRef.current.w ) );
535
- }
536
- }
537
- }
538
- });
539
-
540
- ro.observe(el);
541
- const rect = el.getBoundingClientRect();
542
- sizeRef.current = { w: rect.width, h: rect.height };
543
-
544
- return () => ro.disconnect();
545
- }, [resizeMode, internalMode]);
546
-
547
- const startPosRef = useRef(posResolved);
548
- const [dragging, setDragging] = useState(false);
549
-
550
- const handleStart = () => {
551
- setDragging(true);
552
- startPosRef.current = posResolved;
553
- };
554
-
555
- const handleDelta = (x, y, _data) => {
556
- const { w, h } = sizeRef.current;
557
- if (internalMode === "horizontal") {
558
- setPosition(startPosRef.current + x / (w || 1));
559
- } else {
560
- setPosition(startPosRef.current + y / (h || 1));
561
- }
562
- };
563
-
564
- const handleEnd = () => setDragging(false);
565
-
566
- const arr = Children.toArray(children);
567
- const firstChild = arr[0];
568
- const secondChild = arr[1];
569
-
570
-
571
- const p = posResolved;
572
- const { w, h } = sizeRef.current;
573
-
574
- const computedPane1Style = {
575
- position: "absolute",
576
- top: 0,
577
- left: 0,
578
- overflow: "auto",
579
-
580
- ...(firstPaneStyle || {}),
581
- };
582
-
583
- const computedPane2Style = {
584
- position: "absolute",
585
- bottom: 0,
586
- right: 0,
587
- overflow: "auto",
588
-
589
- ...(secondPaneStyle || {}),
590
- };
591
-
592
-
593
- const computedSplitterStyle = {
594
- background: "rgba(0,0,0,0.08)",
595
- boxShadow: "inset 0 0 2px rgba(0,0,0,0.25)",
596
- zIndex: 10,
597
- position: "absolute",
598
- cursor: internalMode==='horizontal' ? "col-resize" : "row-resize",
599
- touchAction: "none",
600
- pointerEvents: "auto",
601
- ...(splitterStyle || {}),
602
- };
603
-
604
-
605
- if(internalMode==='horizontal') {
606
- computedPane1Style.bottom = 0;
607
- computedPane2Style.top = 0;
608
-
609
- if(resizeMode==='fixFirst') {
610
- computedPane1Style.width = p*(w-splitterSize);
611
- computedPane2Style.left = computedPane1Style.width+splitterSize;
612
-
613
- Object.assign( computedSplitterStyle, {
614
- top: 0,
615
- bottom: 0,
616
- left: computedPane1Style.width,
617
- width: splitterSize,
618
- });
619
- }
620
- else if(resizeMode==='fixSecond') {
621
- computedPane2Style.width = (1-p)*(w-splitterSize);
622
- computedPane1Style.right = computedPane2Style.width+splitterSize;
623
-
624
- Object.assign( computedSplitterStyle, {
625
- top: 0,
626
- bottom: 0,
627
- left: computedPane2Style.width,
628
- width: splitterSize,
629
- });
630
- }
631
- else {
632
- computedPane1Style.width = `calc(${p} * (100% - ${splitterSize}px))`;
633
- computedPane2Style.left = `calc(${p} * (100% - ${splitterSize}px) + ${splitterSize}px)`;
634
-
635
- Object.assign( computedSplitterStyle, {
636
- top: 0,
637
- bottom: 0,
638
- left: `calc(${p} * (100% - ${splitterSize}px))`,
639
- width: splitterSize,
640
- });
641
- }
642
- }
643
- else {
644
- computedPane1Style.right = 0;
645
- computedPane2Style.left = 0;
646
-
647
- if(resizeMode==='fixFirst') {
648
- computedPane1Style.height = p*(h-splitterSize);
649
- computedPane2Style.top = computedPane1Style.height+splitterSize;
650
-
651
- Object.assign( computedSplitterStyle, {
652
- left: 0,
653
- right: 0,
654
- top: computedPane1Style.height,
655
- height: splitterSize,
656
- });
657
- }
658
- else if(resizeMode==='fixSecond') {
659
- computedPane2Style.height = (1-p)*(h-splitterSize);
660
- computedPane1Style.bottom = computedPane2Style.height+splitterSize;
661
-
662
- Object.assign( computedSplitterStyle, {
663
- left: 0,
664
- right: 0,
665
- bottom: computedPane2Style.height,
666
- height: splitterSize,
667
- });
668
- }
669
- else {
670
- computedPane1Style.height = `calc(${p} * (100% - ${splitterSize}px))`;
671
- computedPane2Style.top = `calc(${p} * (100% - ${splitterSize}px) + ${splitterSize}px)`;
672
-
673
- Object.assign( computedSplitterStyle, {
674
- left: 0,
675
- right: 0,
676
- top: computedPane1Style.height,
677
- height: splitterSize,
678
- });
679
- }
680
- }
681
-
682
- return (
683
- <div
684
- ref={containerRef}
685
- className={className}
686
- style={{
687
- position: "relative",
688
- width: "100%",
689
- height: "100%",
690
- overflow: "hidden",
691
- ...(style || {}),
692
- }}
693
- >
694
- <div style={computedPane1Style}>{firstChild}</div>
695
- <div style={computedPane2Style}>{secondChild}</div>
696
-
697
- <DragHandle
698
- onStart={handleStart}
699
- onDelta={handleDelta}
700
- onEnd={handleEnd}
701
- style={computedSplitterStyle}
702
- className="splitter"
703
- >
704
- {splitterChild}
705
- </DragHandle>
706
- </div>
707
- );
471
+ const containerRef = useRef(null);
472
+
473
+ // Measured container size
474
+ const sizeRef = useRef({ w: 0, h: 0 });
475
+
476
+ // Internal mode supports imperative setMode
477
+ const [internalMode, setInternalMode] = useState(mode === "vertical" ? "vertical" : "horizontal");
478
+ const internalModeRef = useRef(internalMode);
479
+
480
+ // Internal position for uncontrolled usage
481
+ const [internalPos, setInternalPos] = useState(defaultPosition);
482
+ const internalPosRef = useRef(internalPos);
483
+
484
+ // Latest effective pos (controlled or uncontrolled)
485
+ const effectivePosRef = useRef(undefined);
486
+
487
+ const resizePolicyRef = useRef(resizePolicy);
488
+ useEffect(() => {
489
+ resizePolicyRef.current = resizePolicy;
490
+ }, [resizePolicy]);
491
+
492
+ // Keep refs synced
493
+ internalModeRef.current = internalMode;
494
+
495
+ useEffect(() => {
496
+ internalPosRef.current = internalPos;
497
+ }, [internalPos]);
498
+
499
+ // If mode prop changes, update internalMode synchronously
500
+ useLayoutEffect(() => {
501
+ setInternalMode(mode === "vertical" ? "vertical" : "horizontal");
502
+ }, [mode]);
503
+
504
+ const clampWithSize = (v, modeNow, w, h) => {
505
+ const size = modeNow === "vertical" ? h : w;
506
+ return clamp(v, min, max, size, splitterSize);
507
+ };
508
+
509
+ // Determine effective position (controlled vs uncontrolled) without clamping on first render
510
+ const posResolved = useMemo(() => {
511
+ const raw = position == null ? internalPos : position;
512
+
513
+ // If we don't know size yet, just keep raw (no size-based min/max function evaluation)
514
+ // to avoid “startup jump from wrong clamping”.
515
+ const { w, h } = sizeRef.current;
516
+ const hasSize = (mode === "vertical" ? h > 0 : w > 0);
517
+
518
+ if (!hasSize) return raw;
519
+
520
+ return clampWithSize(raw, internalModeRef.current, w, h);
521
+ }, [position, internalPos, min, max, splitterSize, mode]);
522
+
523
+ useEffect(() => {
524
+ effectivePosRef.current = posResolved;
525
+ }, [posResolved]);
526
+
527
+ // setPosition for imperative usage + drag + resizePolicy
528
+ const setPosition = (next) => {
529
+ const modeNow = internalModeRef.current;
530
+ const { w, h } = sizeRef.current;
531
+
532
+ // If size unknown, just update internalPos (we'll clamp on first measure)
533
+ if ((modeNow === "vertical" ? h <= 0 : w <= 0)) {
534
+ if (position == null) setInternalPos(next);
535
+ onSizeChange?.(next);
536
+ return;
537
+ }
538
+
539
+ const clamped = clampWithSize(next, modeNow, w, h);
540
+ if (position == null) setInternalPos(clamped);
541
+ effectivePosRef.current = clamped;
542
+ onSizeChange?.(clamped);
543
+ };
544
+
545
+ useImperativeHandle(ref, () => ({
546
+ setPosition,
547
+ setMode: (nextMode) => setInternalMode(nextMode === "vertical" ? "vertical" : "horizontal"),
548
+ getPosition: () => (effectivePosRef.current ?? posResolved),
549
+ getMode: () => internalModeRef.current,
550
+ }));
551
+
552
+ // One-time “first valid size” initialization:
553
+ // - update clamp
554
+ // - emit onSizeChange once so parent splitterPos becomes correct
555
+ const didInitRef = useRef(false);
556
+
557
+ useLayoutEffect(() => {
558
+ const el = containerRef.current;
559
+ if (!el) return;
560
+
561
+ const ro = new ResizeObserver((entries) => {
562
+ const rect = el.getBoundingClientRect();
563
+ const prev = sizeRef.current;
564
+ const nextSize = { w: rect.width, h: rect.height };
565
+ sizeRef.current = nextSize;
566
+
567
+ const modeNow = internalModeRef.current;
568
+
569
+ const hadSize = (modeNow === "vertical" ? prev.h > 0 : prev.w > 0);
570
+ const hasSizeNow = (modeNow === "vertical" ? nextSize.h > 0 : nextSize.w > 0);
571
+
572
+ // First time we get meaningful size -> clamp + notify exactly once
573
+ if (!didInitRef.current && hasSizeNow) {
574
+ didInitRef.current = true;
575
+
576
+ const raw = position == null ? internalPosRef.current : position;
577
+ const clamped = clampWithSize(raw, modeNow, nextSize.w, nextSize.h);
578
+
579
+ if (position == null) setInternalPos(clamped);
580
+ effectivePosRef.current = clamped;
581
+ onSizeChange?.(clamped);
582
+
583
+ return; // don't apply resizePolicy math on the same tick as init
584
+ }
585
+
586
+ // Apply resizePolicy on subsequent resizes (when we have both old & new size)
587
+ if (!hadSize || !hasSizeNow) return;
588
+
589
+ const p = effectivePosRef.current ?? posResolved;
590
+ const rp = resizePolicyRef.current;
591
+ const s = splitterSize;
592
+
593
+ if (rp === "fixFirst") {
594
+ if (modeNow === "vertical") {
595
+ const availOld = prev.h - s;
596
+ const availNew = nextSize.h - s;
597
+ if (availOld > 0 && availNew > 0) {
598
+ const firstPx = p * availOld;
599
+ const nextP = firstPx / availNew;
600
+ if (Math.abs(nextP - p) > EPS) setPosition(nextP);
601
+ }
602
+ }
603
+ else {
604
+ const availOld = prev.w - s;
605
+ const availNew = nextSize.w - s;
606
+ if (availOld > 0 && availNew > 0) {
607
+ const firstPx = p * availOld;
608
+ const nextP = firstPx / availNew;
609
+ if (Math.abs(nextP - p) > EPS) setPosition(nextP);
610
+ }
611
+ }
612
+ }
613
+ else if (rp === "fixSecond") {
614
+ if (modeNow === "vertical") {
615
+ const availOld = prev.h - s;
616
+ const availNew = nextSize.h - s;
617
+ if (availOld > 0 && availNew > 0) {
618
+ const secondPx = (1 - p) * availOld;
619
+ const nextP = 1 - secondPx / availNew;
620
+ if (Math.abs(nextP - p) > EPS) setPosition(nextP);
621
+ }
622
+ }
623
+ else {
624
+ const availOld = prev.w - s;
625
+ const availNew = nextSize.w - s;
626
+ if (availOld > 0 && availNew > 0) {
627
+ const secondPx = (1 - p) * availOld;
628
+ const nextP = 1 - secondPx / availNew;
629
+ if (Math.abs(nextP - p) > EPS) setPosition(nextP);
630
+ }
631
+ }
632
+ } // proportional => derived from p; no policy adjustment needed
633
+ });
634
+
635
+ ro.observe(el);
636
+ return () => ro.disconnect();
637
+ // We intentionally do NOT include posResolved/internalPos here to avoid re-attaching observer.
638
+ // ResizeObserver uses refs to get latest values.
639
+ }, [min, max, splitterSize, onSizeChange, position]);
640
+
641
+ // Drag support
642
+ const startPosRef = useRef(posResolved);
643
+ const [dragging, setDragging] = useState(false);
644
+
645
+ useEffect(() => {
646
+ if (!dragging) startPosRef.current = posResolved;
647
+ }, [posResolved, dragging]);
648
+
649
+ const handleStart = () => {
650
+ setDragging(true);
651
+ startPosRef.current = effectivePosRef.current ?? posResolved;
652
+ };
653
+
654
+ const handleDelta = (x, y) => {
655
+ const modeNow = internalModeRef.current;
656
+ const { w, h } = sizeRef.current;
657
+
658
+ if (modeNow === "horizontal") {
659
+ const denom = w > 0 ? w : 1;
660
+ setPosition(startPosRef.current + x / denom);
661
+ }
662
+ else {
663
+ const denom = h > 0 ? h : 1;
664
+ setPosition(startPosRef.current + y / denom);
665
+ }
666
+ };
667
+
668
+ const handleEnd = () => setDragging(false);
669
+
670
+ const arr = Children.toArray(children);
671
+ const firstChild = arr[0];
672
+ const secondChild = arr[1];
673
+
674
+ const p = posResolved;
675
+ const { w, h } = sizeRef.current;
676
+
677
+ const computedPane1Style = {
678
+ position: "absolute",
679
+ top: 0,
680
+ left: 0,
681
+ overflow: "auto",
682
+ ...(firstPaneStyle || {}),
683
+ };
684
+
685
+ const computedPane2Style = {
686
+ position: "absolute",
687
+ bottom: 0,
688
+ right: 0,
689
+ overflow: "auto",
690
+ ...(secondPaneStyle || {}),
691
+ };
692
+
693
+ const computedSplitterStyle = {
694
+ background: "rgba(0,0,0,0.08)",
695
+ boxShadow: "inset 0 0 2px rgba(0,0,0,0.25)",
696
+ zIndex: 10,
697
+ position: "absolute",
698
+ cursor: internalMode === "horizontal" ? "col-resize" : "row-resize",
699
+ touchAction: "none",
700
+ pointerEvents: "auto",
701
+ ...(splitterStyle || {}),
702
+ };
703
+
704
+ if (internalMode === "horizontal") {
705
+ computedPane1Style.bottom = 0;
706
+ computedPane2Style.top = 0;
707
+
708
+ if (resizePolicy === "fixFirst") {
709
+ const ww = w - splitterSize;
710
+ const pane1W = ww > 0 ? p * ww : 0;
711
+
712
+ computedPane1Style.width = pane1W;
713
+ computedPane2Style.left = pane1W + splitterSize;
714
+
715
+ Object.assign(computedSplitterStyle, {
716
+ top: 0,
717
+ bottom: 0,
718
+ left: pane1W,
719
+ width: splitterSize,
720
+ });
721
+ }
722
+ else if (resizePolicy === "fixSecond") {
723
+ const ww = w - splitterSize;
724
+ const pane2W = ww > 0 ? (1 - p) * ww : 0;
725
+
726
+ computedPane2Style.width = pane2W;
727
+ computedPane1Style.right = pane2W + splitterSize;
728
+
729
+ Object.assign(computedSplitterStyle, {
730
+ top: 0,
731
+ bottom: 0,
732
+ right: pane2W,
733
+ width: splitterSize,
734
+ });
735
+ } else {
736
+ computedPane1Style.width = `calc(${p} * (100% - ${splitterSize}px))`;
737
+ computedPane2Style.left = `calc(${p} * (100% - ${splitterSize}px) + ${splitterSize}px)`;
738
+
739
+ Object.assign(computedSplitterStyle, {
740
+ top: 0,
741
+ bottom: 0,
742
+ left: `calc(${p} * (100% - ${splitterSize}px))`,
743
+ width: splitterSize,
744
+ });
745
+ }
746
+ } else {
747
+ // vertical
748
+ computedPane1Style.right = 0;
749
+ computedPane2Style.left = 0;
750
+
751
+ if (resizePolicy === "fixFirst") {
752
+ const hh = h - splitterSize;
753
+ const pane1H = hh > 0 ? p * hh : 0;
754
+
755
+ computedPane1Style.height = pane1H;
756
+ computedPane2Style.top = pane1H + splitterSize;
757
+
758
+ Object.assign(computedSplitterStyle, {
759
+ left: 0,
760
+ right: 0,
761
+ top: pane1H,
762
+ height: splitterSize,
763
+ });
764
+ } else if (resizePolicy === "fixSecond") {
765
+ const hh = h - splitterSize;
766
+ const pane2H = hh > 0 ? (1 - p) * hh : 0;
767
+
768
+ computedPane2Style.height = pane2H;
769
+ computedPane1Style.bottom = pane2H + splitterSize;
770
+
771
+ Object.assign(computedSplitterStyle, {
772
+ left: 0,
773
+ right: 0,
774
+ bottom: pane2H,
775
+ height: splitterSize,
776
+ });
777
+ } else {
778
+ computedPane1Style.height = `calc(${p} * (100% - ${splitterSize}px))`;
779
+ computedPane2Style.top = `calc(${p} * (100% - ${splitterSize}px) + ${splitterSize}px)`;
780
+
781
+ Object.assign(computedSplitterStyle, {
782
+ left: 0,
783
+ right: 0,
784
+ top: computedPane1Style.height,
785
+ height: splitterSize,
786
+ });
787
+ }
788
+ }
789
+
790
+ return (
791
+ <div
792
+ ref={containerRef}
793
+ className={className}
794
+ style={{
795
+ position: "relative",
796
+ width: "100%",
797
+ height: "100%",
798
+ overflow: "hidden",
799
+ ...(style || {}),
800
+ }}
801
+ >
802
+ <div style={computedPane1Style}>{firstChild}</div>
803
+ <div style={computedPane2Style}>{secondChild}</div>
804
+
805
+ <DragHandle
806
+ key={internalMode}
807
+ onStart={handleStart}
808
+ onDelta={handleDelta}
809
+ onEnd={handleEnd}
810
+ style={computedSplitterStyle}
811
+ className="splitter"
812
+ >
813
+ {splitterChild}
814
+ </DragHandle>
815
+ </div>
816
+ );
708
817
  });
709
818
 
710
-
711
- const clamp = function(v, min, max, size, splitterSize) {
712
- if(typeof(min)==='function') { if(size>0) min = min(size, splitterSize); else min=0; }
713
- if(typeof(max)==='function') { if(size>0) max = max(size, splitterSize); else max=1; }
714
- return Math.max(min, Math.min(max, v));
715
- }
819
+ const EPS = 1e-6;
820
+
821
+ const clamp = function (v, min, max, size, splitterSize) {
822
+ let _min = min;
823
+ let _max = max;
824
+
825
+ // Only evaluate min/max functions when size is known (>0).
826
+ // When size is unknown, we don't want to clamp based on a fake size (prevents startup jumps).
827
+ if (typeof _min === "function") {
828
+ if (size > 0) _min = _min(size, splitterSize);
829
+ else _min = 0;
830
+ }
831
+ if (typeof _max === "function") {
832
+ if (size > 0) _max = _max(size, splitterSize);
833
+ else _max = 1;
834
+ }
835
+
836
+ _min = Math.max(0, Math.min(_min, 1));
837
+ _max = Math.max(0, Math.min(_max, 1));
838
+ return Math.max(_min, Math.min(_max, v));
839
+ };