@classic-homes/theme-react 0.1.50 → 0.1.51

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.mjs CHANGED
@@ -375,6 +375,895 @@ var PageHeader = React10.forwardRef(
375
375
  )
376
376
  );
377
377
  PageHeader.displayName = "PageHeader";
378
+
379
+ // src/components/DataPanel.tsx
380
+ import * as React11 from "react";
381
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
382
+ import {
383
+ DEFAULT_PANEL_STATE,
384
+ DEFAULT_CONSTRAINTS,
385
+ loadPanelState,
386
+ savePanelState,
387
+ getPointerPosition,
388
+ calculateDragPosition,
389
+ calculateResize,
390
+ getResizeCursor,
391
+ getPinnedResizeHandle,
392
+ constrainSize,
393
+ constrainPosition,
394
+ constrainPinnedWidth,
395
+ constrainPinnedHeight,
396
+ detectEdgeSnap,
397
+ getPinnedPositionStyles,
398
+ getDetachedPositionStyles,
399
+ getInitialDetachedPosition,
400
+ isHorizontalEdge,
401
+ calculateDetachDistance,
402
+ calculatePullOffset,
403
+ calculateDetachedPositionFromPinned,
404
+ getPinnedPullTransform
405
+ } from "@classic-homes/data-panel-core";
406
+ import { Fragment, jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
407
+ function useDataPanel(options = {}) {
408
+ const {
409
+ persistKey,
410
+ initialMode = DEFAULT_PANEL_STATE.mode,
411
+ initialEdge = DEFAULT_PANEL_STATE.edge,
412
+ initialExpanded = DEFAULT_PANEL_STATE.isExpanded,
413
+ constraints = {}
414
+ } = options;
415
+ const resolvedConstraints = { ...DEFAULT_CONSTRAINTS, ...constraints };
416
+ const [persistedState] = React11.useState(() => {
417
+ if (persistKey && typeof window !== "undefined") {
418
+ return loadPanelState(persistKey);
419
+ }
420
+ return null;
421
+ });
422
+ const [mode, setMode] = React11.useState(persistedState?.mode ?? initialMode);
423
+ const [edge, setEdge] = React11.useState(persistedState?.edge ?? initialEdge);
424
+ const [isExpanded, setIsExpanded] = React11.useState(
425
+ persistedState?.isExpanded ?? initialExpanded
426
+ );
427
+ const [detachedPosition, setDetachedPosition] = React11.useState(
428
+ persistedState?.detachedPosition ?? DEFAULT_PANEL_STATE.detachedPosition
429
+ );
430
+ const [detachedSize, setDetachedSize] = React11.useState(
431
+ persistedState?.detachedSize ?? DEFAULT_PANEL_STATE.detachedSize
432
+ );
433
+ const [pinnedSize, setPinnedSize] = React11.useState(
434
+ persistedState?.pinnedSize ?? DEFAULT_PANEL_STATE.pinnedSize
435
+ );
436
+ const saveTimeoutRef = React11.useRef(null);
437
+ const debouncedSave = React11.useCallback(() => {
438
+ if (!persistKey) return;
439
+ if (saveTimeoutRef.current) {
440
+ clearTimeout(saveTimeoutRef.current);
441
+ }
442
+ saveTimeoutRef.current = setTimeout(() => {
443
+ savePanelState(persistKey, {
444
+ mode,
445
+ variant: "full",
446
+ edge,
447
+ isExpanded,
448
+ detachedPosition,
449
+ detachedSize,
450
+ pinnedSize,
451
+ cardSnapIndex: 0
452
+ });
453
+ }, 300);
454
+ }, [persistKey, mode, edge, isExpanded, detachedPosition, detachedSize, pinnedSize]);
455
+ const handleSetMode = React11.useCallback(
456
+ (newMode) => {
457
+ setMode(newMode);
458
+ if (newMode === "detached" && typeof window !== "undefined" && detachedPosition.x === DEFAULT_PANEL_STATE.detachedPosition.x && detachedPosition.y === DEFAULT_PANEL_STATE.detachedPosition.y) {
459
+ setDetachedPosition(
460
+ getInitialDetachedPosition(detachedSize, window.innerWidth, window.innerHeight)
461
+ );
462
+ }
463
+ },
464
+ [detachedPosition, detachedSize]
465
+ );
466
+ const handleSetDetachedPosition = React11.useCallback(
467
+ (position) => {
468
+ if (typeof window === "undefined") {
469
+ setDetachedPosition(position);
470
+ return;
471
+ }
472
+ setDetachedPosition(
473
+ constrainPosition(position, detachedSize, window.innerWidth, window.innerHeight)
474
+ );
475
+ },
476
+ [detachedSize]
477
+ );
478
+ const handleSetDetachedSize = React11.useCallback(
479
+ (size) => {
480
+ setDetachedSize(constrainSize(size, resolvedConstraints));
481
+ },
482
+ [resolvedConstraints]
483
+ );
484
+ const handleSetPinnedSize = React11.useCallback(
485
+ (size) => {
486
+ if (typeof window === "undefined") {
487
+ setPinnedSize(size);
488
+ return;
489
+ }
490
+ if (isHorizontalEdge(edge)) {
491
+ setPinnedSize(constrainPinnedWidth(size, resolvedConstraints, window.innerWidth));
492
+ } else {
493
+ setPinnedSize(constrainPinnedHeight(size, resolvedConstraints, window.innerHeight));
494
+ }
495
+ },
496
+ [edge, resolvedConstraints]
497
+ );
498
+ React11.useEffect(() => {
499
+ debouncedSave();
500
+ }, [debouncedSave]);
501
+ return {
502
+ mode,
503
+ setMode: handleSetMode,
504
+ edge,
505
+ setEdge,
506
+ isExpanded,
507
+ setIsExpanded,
508
+ detachedPosition,
509
+ setDetachedPosition: handleSetDetachedPosition,
510
+ detachedSize,
511
+ setDetachedSize: handleSetDetachedSize,
512
+ pinnedSize,
513
+ setPinnedSize: handleSetPinnedSize
514
+ };
515
+ }
516
+ var ChevronUpIcon = () => /* @__PURE__ */ jsx11(
517
+ "svg",
518
+ {
519
+ xmlns: "http://www.w3.org/2000/svg",
520
+ width: "16",
521
+ height: "16",
522
+ viewBox: "0 0 24 24",
523
+ fill: "none",
524
+ stroke: "currentColor",
525
+ strokeWidth: "2",
526
+ strokeLinecap: "round",
527
+ strokeLinejoin: "round",
528
+ children: /* @__PURE__ */ jsx11("path", { d: "m18 15-6-6-6 6" })
529
+ }
530
+ );
531
+ var MoreVerticalIcon = () => /* @__PURE__ */ jsxs3(
532
+ "svg",
533
+ {
534
+ xmlns: "http://www.w3.org/2000/svg",
535
+ width: "16",
536
+ height: "16",
537
+ viewBox: "0 0 24 24",
538
+ fill: "none",
539
+ stroke: "currentColor",
540
+ strokeWidth: "2",
541
+ strokeLinecap: "round",
542
+ strokeLinejoin: "round",
543
+ children: [
544
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "1" }),
545
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "5", r: "1" }),
546
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "19", r: "1" })
547
+ ]
548
+ }
549
+ );
550
+ var CloseIcon = () => /* @__PURE__ */ jsxs3(
551
+ "svg",
552
+ {
553
+ xmlns: "http://www.w3.org/2000/svg",
554
+ width: "16",
555
+ height: "16",
556
+ viewBox: "0 0 24 24",
557
+ fill: "none",
558
+ stroke: "currentColor",
559
+ strokeWidth: "2",
560
+ strokeLinecap: "round",
561
+ strokeLinejoin: "round",
562
+ children: [
563
+ /* @__PURE__ */ jsx11("path", { d: "M18 6 6 18" }),
564
+ /* @__PURE__ */ jsx11("path", { d: "m6 6 12 12" })
565
+ ]
566
+ }
567
+ );
568
+ var edgeLabels = {
569
+ left: "Pin to left",
570
+ right: "Pin to right",
571
+ top: "Pin to top",
572
+ bottom: "Pin to bottom"
573
+ };
574
+ var DataPanelHeader = React11.forwardRef(
575
+ ({
576
+ title,
577
+ subtitle,
578
+ mode,
579
+ edge,
580
+ isExpanded,
581
+ onModeChange,
582
+ onEdgeChange,
583
+ onExpandedChange,
584
+ onClose,
585
+ disableClose = false,
586
+ disableModeSwitch = false,
587
+ headerContent,
588
+ headerActions,
589
+ className,
590
+ draggable = false
591
+ }, ref) => {
592
+ return /* @__PURE__ */ jsxs3(
593
+ "div",
594
+ {
595
+ ref,
596
+ className: cn(
597
+ "flex items-center justify-between gap-2 border-b border-border bg-muted/30 px-4 py-3",
598
+ draggable && "cursor-grab active:cursor-grabbing",
599
+ className
600
+ ),
601
+ "data-panel-header": true,
602
+ children: [
603
+ /* @__PURE__ */ jsx11("div", { className: "flex flex-col min-w-0 flex-1", children: headerContent ? headerContent : /* @__PURE__ */ jsxs3(Fragment, { children: [
604
+ title && /* @__PURE__ */ jsx11("h3", { className: "text-sm font-semibold leading-none truncate", children: title }),
605
+ subtitle && /* @__PURE__ */ jsx11("p", { className: "text-xs text-muted-foreground mt-1 truncate", children: subtitle })
606
+ ] }) }),
607
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1 shrink-0", children: [
608
+ headerActions,
609
+ /* @__PURE__ */ jsx11(
610
+ "button",
611
+ {
612
+ type: "button",
613
+ className: "flex h-8 w-8 items-center justify-center rounded-md bg-transparent text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
614
+ onClick: () => onExpandedChange?.(!isExpanded),
615
+ "aria-label": isExpanded ? "Collapse panel" : "Expand panel",
616
+ "aria-expanded": isExpanded,
617
+ children: /* @__PURE__ */ jsx11("span", { className: cn("transition-transform", !isExpanded && "rotate-180"), children: /* @__PURE__ */ jsx11(ChevronUpIcon, {}) })
618
+ }
619
+ ),
620
+ !disableModeSwitch && /* @__PURE__ */ jsxs3(DropdownMenuPrimitive.Root, { children: [
621
+ /* @__PURE__ */ jsx11(DropdownMenuPrimitive.Trigger, { asChild: true, children: /* @__PURE__ */ jsx11(
622
+ "button",
623
+ {
624
+ type: "button",
625
+ className: "flex h-8 w-8 items-center justify-center rounded-md bg-transparent text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
626
+ "aria-label": "Panel options",
627
+ children: /* @__PURE__ */ jsx11(MoreVerticalIcon, {})
628
+ }
629
+ ) }),
630
+ /* @__PURE__ */ jsx11(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsxs3(
631
+ DropdownMenuPrimitive.Content,
632
+ {
633
+ className: "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
634
+ sideOffset: 4,
635
+ align: "end",
636
+ children: [
637
+ /* @__PURE__ */ jsx11(DropdownMenuPrimitive.Label, { className: "px-2 py-1.5 text-sm font-semibold", children: "Panel Position" }),
638
+ ["left", "right", "top", "bottom"].map((e) => /* @__PURE__ */ jsxs3(
639
+ DropdownMenuPrimitive.Item,
640
+ {
641
+ className: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
642
+ onSelect: () => {
643
+ onModeChange?.("pinned");
644
+ onEdgeChange?.(e);
645
+ },
646
+ children: [
647
+ edgeLabels[e],
648
+ mode === "pinned" && edge === e && /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs", children: "\u2713" })
649
+ ]
650
+ },
651
+ e
652
+ )),
653
+ /* @__PURE__ */ jsx11(DropdownMenuPrimitive.Separator, { className: "-mx-1 my-1 h-px bg-muted" }),
654
+ /* @__PURE__ */ jsxs3(
655
+ DropdownMenuPrimitive.Item,
656
+ {
657
+ className: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground",
658
+ onSelect: () => onModeChange?.("detached"),
659
+ children: [
660
+ "Detach",
661
+ mode === "detached" && /* @__PURE__ */ jsx11("span", { className: "ml-auto text-xs", children: "\u2713" })
662
+ ]
663
+ }
664
+ )
665
+ ]
666
+ }
667
+ ) })
668
+ ] }),
669
+ !disableClose && /* @__PURE__ */ jsx11(
670
+ "button",
671
+ {
672
+ type: "button",
673
+ className: "flex h-8 w-8 items-center justify-center rounded-md bg-transparent text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
674
+ onClick: onClose,
675
+ "aria-label": "Close panel",
676
+ children: /* @__PURE__ */ jsx11(CloseIcon, {})
677
+ }
678
+ )
679
+ ] })
680
+ ]
681
+ }
682
+ );
683
+ }
684
+ );
685
+ DataPanelHeader.displayName = "DataPanelHeader";
686
+ var DataPanelContent = React11.forwardRef(
687
+ ({ children, className }, ref) => {
688
+ return /* @__PURE__ */ jsx11("div", { ref, className: cn("flex-1 overflow-auto p-4", className), children });
689
+ }
690
+ );
691
+ DataPanelContent.displayName = "DataPanelContent";
692
+ var DataPanelFooter = React11.forwardRef(
693
+ ({ actions = [], footerContent, footerActions, className }, ref) => {
694
+ if (!footerContent && actions.length === 0 && !footerActions) {
695
+ return null;
696
+ }
697
+ const mapVariant = (variant) => {
698
+ if (variant === "primary") return "default";
699
+ return variant ?? "default";
700
+ };
701
+ return /* @__PURE__ */ jsx11(
702
+ "div",
703
+ {
704
+ ref,
705
+ className: cn(
706
+ "flex items-center justify-end gap-2 border-t border-border bg-muted/30 px-4 py-3",
707
+ className
708
+ ),
709
+ children: footerContent ? footerContent : /* @__PURE__ */ jsxs3(Fragment, { children: [
710
+ footerActions,
711
+ actions.map((action, index) => /* @__PURE__ */ jsx11(
712
+ Button,
713
+ {
714
+ variant: mapVariant(action.variant),
715
+ size: "sm",
716
+ disabled: action.disabled,
717
+ onClick: action.onClick,
718
+ children: action.label
719
+ },
720
+ index
721
+ ))
722
+ ] })
723
+ }
724
+ );
725
+ }
726
+ );
727
+ DataPanelFooter.displayName = "DataPanelFooter";
728
+ var DataPanelTab = React11.forwardRef(
729
+ ({ title = "Panel", edge, onClick, className }, ref) => {
730
+ const positionClasses = {
731
+ left: "left-0 top-1/2 -translate-y-1/2 rounded-r-md border-l-0",
732
+ right: "right-0 top-1/2 -translate-y-1/2 rounded-l-md border-r-0",
733
+ top: "top-0 left-1/2 -translate-x-1/2 rounded-b-md border-t-0",
734
+ bottom: "bottom-0 left-1/2 -translate-x-1/2 rounded-t-md border-b-0"
735
+ }[edge];
736
+ const isVertical = edge === "left" || edge === "right";
737
+ const iconRotation = {
738
+ left: "rotate-90",
739
+ right: "-rotate-90",
740
+ top: "rotate-180",
741
+ bottom: ""
742
+ }[edge];
743
+ return /* @__PURE__ */ jsxs3(
744
+ "button",
745
+ {
746
+ ref,
747
+ type: "button",
748
+ className: cn(
749
+ "fixed z-40 flex items-center justify-center gap-2 border border-border bg-background px-3 py-2 text-sm font-medium shadow-md transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
750
+ positionClasses,
751
+ isVertical && "[writing-mode:vertical-rl]",
752
+ className
753
+ ),
754
+ onClick,
755
+ "aria-label": `Expand ${title} panel`,
756
+ children: [
757
+ /* @__PURE__ */ jsx11("span", { className: cn("shrink-0", iconRotation), children: /* @__PURE__ */ jsx11(ChevronUpIcon, {}) }),
758
+ /* @__PURE__ */ jsx11("span", { className: cn(isVertical && "rotate-180"), children: title })
759
+ ]
760
+ }
761
+ );
762
+ }
763
+ );
764
+ DataPanelTab.displayName = "DataPanelTab";
765
+ var DataPanel = React11.forwardRef(
766
+ ({
767
+ open: controlledOpen,
768
+ onOpenChange,
769
+ mode: controlledMode,
770
+ onModeChange,
771
+ edge: controlledEdge,
772
+ onEdgeChange,
773
+ expanded: controlledExpanded,
774
+ onExpandedChange,
775
+ title,
776
+ subtitle,
777
+ children,
778
+ actions = [],
779
+ constraints = {},
780
+ disableClose = false,
781
+ disableResize = false,
782
+ disableDrag = false,
783
+ disableModeSwitch = false,
784
+ persistKey,
785
+ snapThreshold = 20,
786
+ detachThreshold = 40,
787
+ headerContent,
788
+ headerActions,
789
+ footerContent,
790
+ footerActions,
791
+ className
792
+ }, ref) => {
793
+ const resolvedConstraints = { ...DEFAULT_CONSTRAINTS, ...constraints };
794
+ const [internalOpen, setInternalOpen] = React11.useState(true);
795
+ const [internalMode, setInternalMode] = React11.useState(DEFAULT_PANEL_STATE.mode);
796
+ const [internalEdge, setInternalEdge] = React11.useState(DEFAULT_PANEL_STATE.edge);
797
+ const [internalExpanded, setInternalExpanded] = React11.useState(
798
+ DEFAULT_PANEL_STATE.isExpanded
799
+ );
800
+ const [detachedPosition, setDetachedPosition] = React11.useState(
801
+ DEFAULT_PANEL_STATE.detachedPosition
802
+ );
803
+ const [detachedSize, setDetachedSize] = React11.useState(DEFAULT_PANEL_STATE.detachedSize);
804
+ const [pinnedSize, setPinnedSize] = React11.useState(DEFAULT_PANEL_STATE.pinnedSize);
805
+ const [isDragging, setIsDragging] = React11.useState(false);
806
+ const [isResizing, setIsResizing] = React11.useState(false);
807
+ const [snapPreview, setSnapPreview] = React11.useState(null);
808
+ const [isPinnedDragging, setIsPinnedDragging] = React11.useState(false);
809
+ const [pullOffset, setPullOffset] = React11.useState(0);
810
+ const dragStateRef = React11.useRef({
811
+ startPosition: { x: 0, y: 0 },
812
+ startPanelPosition: { x: 0, y: 0 }
813
+ });
814
+ const pinnedDragStateRef = React11.useRef({
815
+ startPosition: { x: 0, y: 0 },
816
+ hasDetached: false
817
+ });
818
+ const resizeStateRef = React11.useRef({
819
+ handle: null,
820
+ startPosition: { x: 0, y: 0 },
821
+ startSize: { width: 0, height: 0 },
822
+ startPanelPosition: { x: 0, y: 0 }
823
+ });
824
+ const open = controlledOpen ?? internalOpen;
825
+ const mode = controlledMode ?? internalMode;
826
+ const edge = controlledEdge ?? internalEdge;
827
+ const isExpanded = controlledExpanded ?? internalExpanded;
828
+ const saveTimeoutRef = React11.useRef(null);
829
+ const debouncedSave = React11.useCallback(() => {
830
+ if (!persistKey) return;
831
+ if (saveTimeoutRef.current) {
832
+ clearTimeout(saveTimeoutRef.current);
833
+ }
834
+ saveTimeoutRef.current = setTimeout(() => {
835
+ savePanelState(persistKey, {
836
+ mode: internalMode,
837
+ variant: "full",
838
+ edge: internalEdge,
839
+ isExpanded: internalExpanded,
840
+ detachedPosition,
841
+ detachedSize,
842
+ pinnedSize,
843
+ cardSnapIndex: 0
844
+ });
845
+ }, 300);
846
+ }, [
847
+ persistKey,
848
+ internalMode,
849
+ internalEdge,
850
+ internalExpanded,
851
+ detachedPosition,
852
+ detachedSize,
853
+ pinnedSize
854
+ ]);
855
+ React11.useEffect(() => {
856
+ if (!persistKey) return;
857
+ const persisted = loadPanelState(persistKey);
858
+ if (persisted) {
859
+ if (controlledMode === void 0 && persisted.mode) setInternalMode(persisted.mode);
860
+ if (controlledEdge === void 0 && persisted.edge) setInternalEdge(persisted.edge);
861
+ if (controlledExpanded === void 0 && persisted.isExpanded !== void 0)
862
+ setInternalExpanded(persisted.isExpanded);
863
+ if (persisted.detachedPosition) setDetachedPosition(persisted.detachedPosition);
864
+ if (persisted.detachedSize) setDetachedSize(persisted.detachedSize);
865
+ if (persisted.pinnedSize) setPinnedSize(persisted.pinnedSize);
866
+ }
867
+ }, [persistKey, controlledMode, controlledEdge, controlledExpanded]);
868
+ React11.useEffect(() => {
869
+ if (mode === "detached" && detachedPosition.x === DEFAULT_PANEL_STATE.detachedPosition.x && detachedPosition.y === DEFAULT_PANEL_STATE.detachedPosition.y) {
870
+ setDetachedPosition(
871
+ getInitialDetachedPosition(detachedSize, window.innerWidth, window.innerHeight)
872
+ );
873
+ }
874
+ }, [mode, detachedPosition, detachedSize]);
875
+ const handleOpenChange = (newOpen) => {
876
+ if (controlledOpen === void 0) {
877
+ setInternalOpen(newOpen);
878
+ }
879
+ onOpenChange?.(newOpen);
880
+ };
881
+ const handleModeChange = (newMode) => {
882
+ if (controlledMode === void 0) {
883
+ setInternalMode(newMode);
884
+ }
885
+ onModeChange?.(newMode);
886
+ debouncedSave();
887
+ };
888
+ const handleEdgeChange = (newEdge) => {
889
+ if (controlledEdge === void 0) {
890
+ setInternalEdge(newEdge);
891
+ }
892
+ onEdgeChange?.(newEdge);
893
+ debouncedSave();
894
+ };
895
+ const handleExpandedChange = (newExpanded) => {
896
+ if (controlledExpanded === void 0) {
897
+ setInternalExpanded(newExpanded);
898
+ }
899
+ onExpandedChange?.(newExpanded);
900
+ debouncedSave();
901
+ };
902
+ const handleDragStart = (e) => {
903
+ if (disableDrag) return;
904
+ const target = e.target;
905
+ if (!target.closest("[data-panel-header]")) return;
906
+ const pos = getPointerPosition(e.nativeEvent);
907
+ if (mode === "detached") {
908
+ setIsDragging(true);
909
+ dragStateRef.current = {
910
+ startPosition: pos,
911
+ startPanelPosition: { ...detachedPosition }
912
+ };
913
+ document.body.style.cursor = "grabbing";
914
+ document.body.style.userSelect = "none";
915
+ } else if (mode === "pinned" && detachThreshold > 0) {
916
+ setIsPinnedDragging(true);
917
+ pinnedDragStateRef.current = {
918
+ startPosition: pos,
919
+ hasDetached: false
920
+ };
921
+ setPullOffset(0);
922
+ document.body.style.cursor = "grab";
923
+ document.body.style.userSelect = "none";
924
+ }
925
+ };
926
+ const handleDragMove = React11.useCallback(
927
+ (e) => {
928
+ if (!isDragging) return;
929
+ const currentPos = getPointerPosition(e);
930
+ const newPosition = calculateDragPosition(
931
+ {
932
+ isDragging: true,
933
+ startPosition: dragStateRef.current.startPosition,
934
+ startPanelPosition: dragStateRef.current.startPanelPosition,
935
+ currentPosition: currentPos
936
+ },
937
+ currentPos
938
+ );
939
+ const snap = detectEdgeSnap(
940
+ newPosition,
941
+ detachedSize,
942
+ window.innerWidth,
943
+ window.innerHeight,
944
+ snapThreshold
945
+ );
946
+ setSnapPreview(snap.shouldSnap ? snap.edge : null);
947
+ setDetachedPosition(
948
+ constrainPosition(newPosition, detachedSize, window.innerWidth, window.innerHeight)
949
+ );
950
+ },
951
+ [isDragging, detachedSize, snapThreshold]
952
+ );
953
+ const handleDragEnd = React11.useCallback(() => {
954
+ if (!isDragging) return;
955
+ setIsDragging(false);
956
+ document.body.style.cursor = "";
957
+ document.body.style.userSelect = "";
958
+ if (snapPreview) {
959
+ handleModeChange("pinned");
960
+ handleEdgeChange(snapPreview);
961
+ setSnapPreview(null);
962
+ } else {
963
+ debouncedSave();
964
+ }
965
+ }, [isDragging, snapPreview, handleModeChange, handleEdgeChange, debouncedSave]);
966
+ const handlePinnedDragMove = React11.useCallback(
967
+ (e) => {
968
+ if (!isPinnedDragging) return;
969
+ const currentPos = getPointerPosition(e);
970
+ const distance = calculateDetachDistance(
971
+ pinnedDragStateRef.current.startPosition,
972
+ currentPos,
973
+ edge
974
+ );
975
+ if (distance >= detachThreshold && !pinnedDragStateRef.current.hasDetached) {
976
+ pinnedDragStateRef.current.hasDetached = true;
977
+ setIsPinnedDragging(false);
978
+ setPullOffset(0);
979
+ const newPosition = calculateDetachedPositionFromPinned(
980
+ currentPos,
981
+ pinnedSize,
982
+ detachedSize,
983
+ edge,
984
+ window.innerWidth,
985
+ window.innerHeight
986
+ );
987
+ setDetachedPosition(newPosition);
988
+ handleModeChange("detached");
989
+ setIsDragging(true);
990
+ dragStateRef.current = {
991
+ startPosition: currentPos,
992
+ startPanelPosition: newPosition
993
+ };
994
+ document.body.style.cursor = "grabbing";
995
+ } else if (!pinnedDragStateRef.current.hasDetached) {
996
+ const pull = calculatePullOffset(distance, detachThreshold);
997
+ setPullOffset(pull);
998
+ }
999
+ },
1000
+ [isPinnedDragging, edge, detachThreshold, pinnedSize, detachedSize, handleModeChange]
1001
+ );
1002
+ const handlePinnedDragEnd = React11.useCallback(() => {
1003
+ if (!isPinnedDragging) return;
1004
+ setIsPinnedDragging(false);
1005
+ setPullOffset(0);
1006
+ document.body.style.cursor = "";
1007
+ document.body.style.userSelect = "";
1008
+ }, [isPinnedDragging]);
1009
+ const handleResizeStart = (e, handle) => {
1010
+ if (disableResize) return;
1011
+ e.preventDefault();
1012
+ e.stopPropagation();
1013
+ setIsResizing(true);
1014
+ const pos = getPointerPosition(e.nativeEvent);
1015
+ resizeStateRef.current = {
1016
+ handle,
1017
+ startPosition: pos,
1018
+ startSize: mode === "detached" ? { ...detachedSize } : { width: pinnedSize, height: pinnedSize },
1019
+ startPanelPosition: { ...detachedPosition }
1020
+ };
1021
+ document.body.style.cursor = getResizeCursor(handle);
1022
+ document.body.style.userSelect = "none";
1023
+ };
1024
+ const handleResizeMove = React11.useCallback(
1025
+ (e) => {
1026
+ if (!isResizing || !resizeStateRef.current.handle) return;
1027
+ const currentPos = getPointerPosition(e);
1028
+ const { handle, startPosition, startSize, startPanelPosition } = resizeStateRef.current;
1029
+ if (mode === "detached") {
1030
+ const result = calculateResize(
1031
+ {
1032
+ isResizing: true,
1033
+ handle,
1034
+ startPosition,
1035
+ startSize,
1036
+ startPanelPosition
1037
+ },
1038
+ currentPos
1039
+ );
1040
+ const constrainedSize = constrainSize(result.size, resolvedConstraints);
1041
+ const constrainedPosition = constrainPosition(
1042
+ result.position,
1043
+ constrainedSize,
1044
+ window.innerWidth,
1045
+ window.innerHeight
1046
+ );
1047
+ setDetachedSize(constrainedSize);
1048
+ setDetachedPosition(constrainedPosition);
1049
+ } else {
1050
+ const delta = isHorizontalEdge(edge) ? currentPos.x - startPosition.x : currentPos.y - startPosition.y;
1051
+ const direction = edge === "left" || edge === "top" ? 1 : -1;
1052
+ const newSize = startSize.width + delta * direction;
1053
+ if (isHorizontalEdge(edge)) {
1054
+ setPinnedSize(constrainPinnedWidth(newSize, resolvedConstraints, window.innerWidth));
1055
+ } else {
1056
+ setPinnedSize(constrainPinnedHeight(newSize, resolvedConstraints, window.innerHeight));
1057
+ }
1058
+ }
1059
+ },
1060
+ [isResizing, mode, edge, resolvedConstraints]
1061
+ );
1062
+ const handleResizeEnd = React11.useCallback(() => {
1063
+ if (!isResizing) return;
1064
+ setIsResizing(false);
1065
+ resizeStateRef.current.handle = null;
1066
+ document.body.style.cursor = "";
1067
+ document.body.style.userSelect = "";
1068
+ debouncedSave();
1069
+ }, [isResizing, debouncedSave]);
1070
+ React11.useEffect(() => {
1071
+ if (isDragging) {
1072
+ window.addEventListener("pointermove", handleDragMove);
1073
+ window.addEventListener("pointerup", handleDragEnd);
1074
+ return () => {
1075
+ window.removeEventListener("pointermove", handleDragMove);
1076
+ window.removeEventListener("pointerup", handleDragEnd);
1077
+ };
1078
+ }
1079
+ }, [isDragging, handleDragMove, handleDragEnd]);
1080
+ React11.useEffect(() => {
1081
+ if (isPinnedDragging) {
1082
+ window.addEventListener("pointermove", handlePinnedDragMove);
1083
+ window.addEventListener("pointerup", handlePinnedDragEnd);
1084
+ return () => {
1085
+ window.removeEventListener("pointermove", handlePinnedDragMove);
1086
+ window.removeEventListener("pointerup", handlePinnedDragEnd);
1087
+ };
1088
+ }
1089
+ }, [isPinnedDragging, handlePinnedDragMove, handlePinnedDragEnd]);
1090
+ React11.useEffect(() => {
1091
+ if (isResizing) {
1092
+ window.addEventListener("pointermove", handleResizeMove);
1093
+ window.addEventListener("pointerup", handleResizeEnd);
1094
+ return () => {
1095
+ window.removeEventListener("pointermove", handleResizeMove);
1096
+ window.removeEventListener("pointerup", handleResizeEnd);
1097
+ };
1098
+ }
1099
+ }, [isResizing, handleResizeMove, handleResizeEnd]);
1100
+ const panelStyles = React11.useMemo(() => {
1101
+ if (mode === "pinned") {
1102
+ const baseStyles = getPinnedPositionStyles(edge, pinnedSize);
1103
+ if (pullOffset > 0) {
1104
+ const transform = getPinnedPullTransform(pullOffset, edge);
1105
+ return { ...baseStyles, transform };
1106
+ }
1107
+ return baseStyles;
1108
+ }
1109
+ return getDetachedPositionStyles(detachedPosition, detachedSize);
1110
+ }, [mode, edge, pinnedSize, detachedPosition, detachedSize, pullOffset]);
1111
+ const pinnedClasses = {
1112
+ left: "left-0 top-0 bottom-0 border-r",
1113
+ right: "right-0 top-0 bottom-0 border-l",
1114
+ top: "top-0 left-0 right-0 border-b",
1115
+ bottom: "bottom-0 left-0 right-0 border-t"
1116
+ }[edge];
1117
+ const animationClass = mode === "pinned" ? {
1118
+ left: "animate-slide-right",
1119
+ right: "animate-slide-left",
1120
+ top: "animate-slide-down",
1121
+ bottom: "animate-slide-up"
1122
+ }[edge] : "animate-scale-in";
1123
+ if (!open) return null;
1124
+ if (!isExpanded && mode === "pinned") {
1125
+ return /* @__PURE__ */ jsx11(DataPanelTab, { title, edge, onClick: () => handleExpandedChange(true) });
1126
+ }
1127
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
1128
+ snapPreview && /* @__PURE__ */ jsx11(
1129
+ "div",
1130
+ {
1131
+ className: cn(
1132
+ "fixed z-40 bg-primary/20 border-2 border-primary border-dashed transition-all",
1133
+ snapPreview === "left" && "left-0 top-0 bottom-0 w-80",
1134
+ snapPreview === "right" && "right-0 top-0 bottom-0 w-80",
1135
+ snapPreview === "top" && "top-0 left-0 right-0 h-64",
1136
+ snapPreview === "bottom" && "bottom-0 left-0 right-0 h-64"
1137
+ )
1138
+ }
1139
+ ),
1140
+ /* @__PURE__ */ jsxs3(
1141
+ "div",
1142
+ {
1143
+ ref,
1144
+ className: cn(
1145
+ "fixed z-50 flex flex-col bg-background border border-border shadow-lg overflow-hidden",
1146
+ mode === "pinned" && pinnedClasses,
1147
+ mode === "detached" && "rounded-lg",
1148
+ isPinnedDragging && pullOffset > 0 && "shadow-2xl",
1149
+ animationClass,
1150
+ className
1151
+ ),
1152
+ style: panelStyles,
1153
+ role: mode === "detached" ? "dialog" : void 0,
1154
+ "aria-modal": mode === "detached" ? "true" : void 0,
1155
+ "aria-labelledby": title ? "data-panel-title" : void 0,
1156
+ onPointerDown: handleDragStart,
1157
+ children: [
1158
+ /* @__PURE__ */ jsx11(
1159
+ DataPanelHeader,
1160
+ {
1161
+ title,
1162
+ subtitle,
1163
+ mode,
1164
+ edge,
1165
+ isExpanded,
1166
+ onModeChange: handleModeChange,
1167
+ onEdgeChange: handleEdgeChange,
1168
+ onExpandedChange: handleExpandedChange,
1169
+ onClose: () => handleOpenChange(false),
1170
+ disableClose,
1171
+ disableModeSwitch,
1172
+ headerContent,
1173
+ headerActions,
1174
+ draggable: !disableDrag && (mode === "detached" || mode === "pinned" && detachThreshold > 0)
1175
+ }
1176
+ ),
1177
+ /* @__PURE__ */ jsx11(DataPanelContent, { children }),
1178
+ /* @__PURE__ */ jsx11(
1179
+ DataPanelFooter,
1180
+ {
1181
+ actions,
1182
+ footerContent,
1183
+ footerActions
1184
+ }
1185
+ ),
1186
+ !disableResize && /* @__PURE__ */ jsx11(Fragment, { children: mode === "detached" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1187
+ /* @__PURE__ */ jsx11(
1188
+ "div",
1189
+ {
1190
+ className: "resize-handle resize-handle--horizontal resize-handle--top",
1191
+ onPointerDown: (e) => handleResizeStart(e, "top")
1192
+ }
1193
+ ),
1194
+ /* @__PURE__ */ jsx11(
1195
+ "div",
1196
+ {
1197
+ className: "resize-handle resize-handle--horizontal resize-handle--bottom",
1198
+ onPointerDown: (e) => handleResizeStart(e, "bottom")
1199
+ }
1200
+ ),
1201
+ /* @__PURE__ */ jsx11(
1202
+ "div",
1203
+ {
1204
+ className: "resize-handle resize-handle--vertical resize-handle--left",
1205
+ onPointerDown: (e) => handleResizeStart(e, "left")
1206
+ }
1207
+ ),
1208
+ /* @__PURE__ */ jsx11(
1209
+ "div",
1210
+ {
1211
+ className: "resize-handle resize-handle--vertical resize-handle--right",
1212
+ onPointerDown: (e) => handleResizeStart(e, "right")
1213
+ }
1214
+ ),
1215
+ /* @__PURE__ */ jsx11(
1216
+ "div",
1217
+ {
1218
+ className: "resize-handle resize-handle--corner resize-handle--top-left",
1219
+ style: { "--handle-radius": "6px" },
1220
+ onPointerDown: (e) => handleResizeStart(e, "top-left")
1221
+ }
1222
+ ),
1223
+ /* @__PURE__ */ jsx11(
1224
+ "div",
1225
+ {
1226
+ className: "resize-handle resize-handle--corner resize-handle--top-right",
1227
+ style: { "--handle-radius": "6px" },
1228
+ onPointerDown: (e) => handleResizeStart(e, "top-right")
1229
+ }
1230
+ ),
1231
+ /* @__PURE__ */ jsx11(
1232
+ "div",
1233
+ {
1234
+ className: "resize-handle resize-handle--corner resize-handle--bottom-left",
1235
+ style: { "--handle-radius": "6px" },
1236
+ onPointerDown: (e) => handleResizeStart(e, "bottom-left")
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsx11(
1240
+ "div",
1241
+ {
1242
+ className: "resize-handle resize-handle--corner resize-handle--bottom-right",
1243
+ style: { "--handle-radius": "6px" },
1244
+ onPointerDown: (e) => handleResizeStart(e, "bottom-right")
1245
+ }
1246
+ )
1247
+ ] }) : /* @__PURE__ */ jsx11(
1248
+ "div",
1249
+ {
1250
+ className: cn(
1251
+ "resize-handle",
1252
+ edge === "left" && "resize-handle--vertical resize-handle--right",
1253
+ edge === "right" && "resize-handle--vertical resize-handle--left",
1254
+ edge === "top" && "resize-handle--horizontal resize-handle--bottom",
1255
+ edge === "bottom" && "resize-handle--horizontal resize-handle--top"
1256
+ ),
1257
+ onPointerDown: (e) => handleResizeStart(e, getPinnedResizeHandle(edge))
1258
+ }
1259
+ ) })
1260
+ ]
1261
+ }
1262
+ )
1263
+ ] });
1264
+ }
1265
+ );
1266
+ DataPanel.displayName = "DataPanel";
378
1267
  export {
379
1268
  Avatar,
380
1269
  AvatarFallback,
@@ -387,6 +1276,11 @@ export {
387
1276
  CardFooter,
388
1277
  CardHeader,
389
1278
  CardTitle,
1279
+ DataPanel,
1280
+ DataPanelContent,
1281
+ DataPanelFooter,
1282
+ DataPanelHeader,
1283
+ DataPanelTab,
390
1284
  Dialog,
391
1285
  DialogClose,
392
1286
  DialogContent,
@@ -408,5 +1302,6 @@ export {
408
1302
  pageHeaderActionsVariants,
409
1303
  pageHeaderContainerVariants,
410
1304
  pageHeaderSubtitleVariants,
411
- pageHeaderTitleVariants
1305
+ pageHeaderTitleVariants,
1306
+ useDataPanel
412
1307
  };