@postxl/ui-components 1.5.5 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar";
70
70
  import { DayPicker, getDefaultClassNames } from "react-day-picker";
71
71
  import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
72
72
  import useEmblaCarousel from "embla-carousel-react";
73
- import { BaselineIcon, BookmarkIcon, CalendarIcon as CalendarIcon$1, Check, CheckIcon as CheckIcon$1, CheckSquareIcon, ChevronDownIcon as ChevronDownIcon$1, ChevronRightIcon as ChevronRightIcon$1, ChevronUpIcon as ChevronUpIcon$1, CircleCheckIcon, CopyIcon, DownloadIcon, EraserIcon, EyeIcon, EyeOffIcon, FilterX, GlobeIcon, GripHorizontalIcon, HashIcon, ListChecksIcon, ListIcon, ListTreeIcon, MessageSquareIcon, MinusIcon, PanelLeftIcon, PanelRightIcon, PencilIcon, PinIcon, PinOffIcon, PlusIcon, SaveIcon, Settings2Icon, SquareIcon, TagIcon, TextInitialIcon, Trash2Icon, TrashIcon, XIcon } from "lucide-react";
73
+ import { BaselineIcon, BookmarkIcon, CalendarIcon as CalendarIcon$1, Check, CheckIcon as CheckIcon$1, CheckSquareIcon, ChevronDownIcon as ChevronDownIcon$1, ChevronRightIcon as ChevronRightIcon$1, ChevronUpIcon as ChevronUpIcon$1, CircleCheckIcon, CopyIcon, DownloadIcon, EraserIcon, EyeIcon, EyeOffIcon, FilterX, GlobeIcon, GripHorizontalIcon, GripVerticalIcon, HashIcon, ListChecksIcon, ListIcon, ListTreeIcon, MessageSquareIcon, MinusIcon, PanelLeftIcon, PanelRightIcon, PencilIcon, PinIcon, PinOffIcon, PlusIcon, SaveIcon, Settings2Icon, SquareIcon, TagIcon, TextInitialIcon, Trash2Icon, TrashIcon, XIcon } from "lucide-react";
74
74
  import * as CollapsePrimitive from "@radix-ui/react-collapsible";
75
75
  import { Command as Command$1 } from "cmdk";
76
76
  import * as DialogPrimitive from "@radix-ui/react-dialog";
@@ -91,6 +91,7 @@ import { DndContext, DragOverlay, KeyboardCode, KeyboardSensor, MeasuringStrateg
91
91
  import { SortableContext, arrayMove, defaultAnimateLayoutChanges, horizontalListSortingStrategy, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
92
92
  import { CSS } from "@dnd-kit/utilities";
93
93
  import * as ReactDOM from "react-dom";
94
+ import { createPortal } from "react-dom";
94
95
  import * as MenubarPrimitive from "@radix-ui/react-menubar";
95
96
  import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
96
97
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
@@ -9756,12 +9757,24 @@ function SidebarMenuSubButton({ asChild = false, size = "md", isActive = false,
9756
9757
  const SidebarTabsContext = React$11.createContext(null);
9757
9758
  function SidebarTabsProvider({ children, storageKey }) {
9758
9759
  const [tabsMap, setTabsMap] = React$11.useState({});
9760
+ const [portalTargets, setPortalTargets] = React$11.useState({});
9759
9761
  const fallbackKey = React$11.useId();
9760
9762
  const [storedActiveTabs, setStoredActiveTabs] = useLocalStorageState(storageKey ?? fallbackKey, {
9761
9763
  defaultValue: {},
9762
9764
  storageSync: !!storageKey
9763
9765
  });
9764
9766
  const [activeTab, setActiveTabState] = React$11.useState(storedActiveTabs);
9767
+ const setPortalTarget = React$11.useCallback((side, tabId, element) => {
9768
+ setPortalTargets((prev) => {
9769
+ const next = new Map(prev[side]);
9770
+ if (element) next.set(tabId, element);
9771
+ else next.delete(tabId);
9772
+ return {
9773
+ ...prev,
9774
+ [side]: next
9775
+ };
9776
+ });
9777
+ }, []);
9765
9778
  const register = React$11.useCallback((side, tab) => {
9766
9779
  setTabsMap((prev) => {
9767
9780
  const next = new Map(prev[side]);
@@ -9815,13 +9828,17 @@ function SidebarTabsProvider({ children, storageKey }) {
9815
9828
  activeTab: resolvedActiveTab,
9816
9829
  register,
9817
9830
  unregister,
9818
- setActiveTab
9831
+ setActiveTab,
9832
+ portalTargets,
9833
+ setPortalTarget
9819
9834
  }), [
9820
9835
  sortedTabs,
9821
9836
  resolvedActiveTab,
9822
9837
  register,
9823
9838
  unregister,
9824
- setActiveTab
9839
+ setActiveTab,
9840
+ portalTargets,
9841
+ setPortalTarget
9825
9842
  ]);
9826
9843
  return /* @__PURE__ */ jsx(SidebarTabsContext.Provider, {
9827
9844
  value,
@@ -9850,21 +9867,17 @@ function useSidebarTabs(side) {
9850
9867
 
9851
9868
  //#endregion
9852
9869
  //#region src/sidebar/sidebar-tab.tsx
9853
- function SidebarTab({ side, id, icon, label, render, order, badge }) {
9870
+ function SidebarTab({ side, id, icon, label, order, badge, children }) {
9854
9871
  const ctx = React$10.useContext(SidebarTabsContext);
9855
9872
  if (!ctx) throw new Error("SidebarTab must be used within a SidebarTabsProvider.");
9856
9873
  const { register, unregister } = ctx;
9857
- const renderRef = React$10.useRef(render);
9858
- renderRef.current = render;
9859
9874
  const iconRef = React$10.useRef(icon);
9860
9875
  iconRef.current = icon;
9861
- const stableRender = React$10.useCallback(() => renderRef.current(), []);
9862
9876
  React$10.useEffect(() => {
9863
9877
  register(side, {
9864
9878
  id,
9865
9879
  icon: iconRef.current,
9866
9880
  label,
9867
- render: stableRender,
9868
9881
  order,
9869
9882
  badge
9870
9883
  });
@@ -9876,17 +9889,17 @@ function SidebarTab({ side, id, icon, label, render, order, badge }) {
9876
9889
  order,
9877
9890
  badge,
9878
9891
  register,
9879
- unregister,
9880
- stableRender
9892
+ unregister
9881
9893
  ]);
9882
- return null;
9894
+ const target = ctx.portalTargets[side]?.get(id);
9895
+ if (!target) return null;
9896
+ return createPortal(children, target);
9883
9897
  }
9884
9898
  function DynamicTabbedSidebar({ side, orientation = "horizontal", collapsible = "offcanvas", className,...sidebarProps }) {
9885
9899
  const ctx = React$10.useContext(SidebarTabsContext);
9886
9900
  if (!ctx) throw new Error("DynamicTabbedSidebar must be used within a SidebarTabsProvider.");
9887
9901
  const tabs = ctx.tabs[side] ?? [];
9888
9902
  const activeTabId = ctx.activeTab[side] ?? null;
9889
- const activeTab = tabs.find((t) => t.id === activeTabId);
9890
9903
  const isVertical = orientation === "vertical";
9891
9904
  const effectiveCollapsible = isVertical && collapsible === "offcanvas" ? "icon" : collapsible;
9892
9905
  if (tabs.length === 0) return null;
@@ -9899,23 +9912,49 @@ function DynamicTabbedSidebar({ side, orientation = "horizontal", collapsible =
9899
9912
  side,
9900
9913
  tabs,
9901
9914
  activeTabId,
9902
- activeTab,
9903
9915
  isVertical,
9904
9916
  collapsible: effectiveCollapsible
9905
9917
  }), /* @__PURE__ */ jsx(SidebarRail, {})] })
9906
9918
  });
9907
9919
  }
9908
- function TabbedSidebarContent({ side, tabs, activeTabId, activeTab, isVertical, collapsible }) {
9920
+ function TabPortalTargets({ side, tabs, activeTabId, hidden }) {
9921
+ const ctx = React$10.useContext(SidebarTabsContext);
9922
+ const callbackRefs = React$10.useRef(new Map());
9923
+ const getRef = (tabId) => {
9924
+ let ref = callbackRefs.current.get(tabId);
9925
+ if (!ref) {
9926
+ ref = (el) => {
9927
+ ctx.setPortalTarget(side, tabId, el);
9928
+ };
9929
+ callbackRefs.current.set(tabId, ref);
9930
+ }
9931
+ return ref;
9932
+ };
9933
+ return /* @__PURE__ */ jsx(Fragment, { children: tabs.map((tab) => {
9934
+ const isVisible = !hidden && tab.id === activeTabId;
9935
+ return /* @__PURE__ */ jsx("div", {
9936
+ ref: getRef(tab.id),
9937
+ hidden: !isVisible || void 0,
9938
+ className: isVisible ? "flex flex-col flex-1 min-h-0" : void 0
9939
+ }, tab.id);
9940
+ }) });
9941
+ }
9942
+ function TabbedSidebarContent({ side, tabs, activeTabId, isVertical, collapsible }) {
9909
9943
  const { state } = useSidebar(side);
9910
9944
  const isCollapsed = state === "collapsed";
9911
9945
  const isIconCollapsible = collapsible === "icon";
9912
- if (isCollapsed && isIconCollapsible) return /* @__PURE__ */ jsx("div", {
9946
+ if (isCollapsed && isIconCollapsible) return /* @__PURE__ */ jsxs("div", {
9913
9947
  className: "flex h-full flex-col",
9914
- children: /* @__PURE__ */ jsx(VerticalTabBar, {
9948
+ children: [/* @__PURE__ */ jsx(VerticalTabBar, {
9915
9949
  side,
9916
9950
  tabs,
9917
9951
  activeTabId
9918
- })
9952
+ }), /* @__PURE__ */ jsx(TabPortalTargets, {
9953
+ side,
9954
+ tabs,
9955
+ activeTabId,
9956
+ hidden: true
9957
+ })]
9919
9958
  });
9920
9959
  if (isVertical) return /* @__PURE__ */ jsxs("div", {
9921
9960
  className: "flex h-full flex-row",
@@ -9923,7 +9962,11 @@ function TabbedSidebarContent({ side, tabs, activeTabId, activeTab, isVertical,
9923
9962
  side,
9924
9963
  tabs,
9925
9964
  activeTabId
9926
- }), /* @__PURE__ */ jsx(SidebarContent, { children: activeTab?.render() })]
9965
+ }), /* @__PURE__ */ jsx(SidebarContent, { children: /* @__PURE__ */ jsx(TabPortalTargets, {
9966
+ side,
9967
+ tabs,
9968
+ activeTabId
9969
+ }) })]
9927
9970
  });
9928
9971
  return /* @__PURE__ */ jsxs("div", {
9929
9972
  className: "flex h-full flex-col",
@@ -9934,7 +9977,11 @@ function TabbedSidebarContent({ side, tabs, activeTabId, activeTab, isVertical,
9934
9977
  activeTabId
9935
9978
  }),
9936
9979
  tabs.length > 0 && /* @__PURE__ */ jsx(SidebarSeparator, {}),
9937
- /* @__PURE__ */ jsx(SidebarContent, { children: activeTab?.render() })
9980
+ /* @__PURE__ */ jsx(SidebarContent, { children: /* @__PURE__ */ jsx(TabPortalTargets, {
9981
+ side,
9982
+ tabs,
9983
+ activeTabId
9984
+ }) })
9938
9985
  ]
9939
9986
  });
9940
9987
  }
@@ -11288,112 +11335,314 @@ ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
11288
11335
 
11289
11336
  //#endregion
11290
11337
  //#region src/tree-view/tree-view.tsx
11291
- const TreeBranch = ({ node, level, onNodeSelect, onGroupSelect, onToggleGroup, isLast = false, parentIsLast = [], defaultExpandedIds = [], selectedId }) => {
11338
+ const DragContext = React$1.createContext({
11339
+ dragState: {
11340
+ draggedNodeId: null,
11341
+ dropTarget: null
11342
+ },
11343
+ setDragState: () => {}
11344
+ });
11345
+ /** Checks if `targetId` is a descendant of `ancestorId` in the tree. Used to prevent dropping a node into itself. */
11346
+ function isDescendant(nodes, ancestorId, targetId) {
11347
+ for (const node of nodes) {
11348
+ if (node.id === ancestorId && node.children) return containsNode(node.children, targetId);
11349
+ if (node.children && isDescendant(node.children, ancestorId, targetId)) return true;
11350
+ }
11351
+ return false;
11352
+ }
11353
+ /** Recursively checks if any node in the subtree has the given id. */
11354
+ function containsNode(nodes, targetId) {
11355
+ for (const node of nodes) {
11356
+ if (node.id === targetId) return true;
11357
+ if (node.children && containsNode(node.children, targetId)) return true;
11358
+ }
11359
+ return false;
11360
+ }
11361
+ /** Finds and returns a node by id from anywhere in the tree. */
11362
+ function findNode(nodes, id) {
11363
+ for (const node of nodes) {
11364
+ if (node.id === id) return node;
11365
+ if (node.children) {
11366
+ const found = findNode(node.children, id);
11367
+ if (found) return found;
11368
+ }
11369
+ }
11370
+ return void 0;
11371
+ }
11372
+ const TreeBranch = ({ node, level, onNodeSelect, onGroupSelect, onToggleGroup, onNodeDelete, onNodeDrop, defaultExpandedIds = [], selectedId, rootData }) => {
11292
11373
  const isGroup = node.type === "group";
11293
11374
  const hasChildren = node.children && node.children.length > 0;
11375
+ const { dragState, setDragState } = React$1.useContext(DragContext);
11376
+ /** Determines drop position (before/after/inside) based on cursor proximity to element edges. */
11377
+ const handleDragOver = (e) => {
11378
+ if (!dragState.draggedNodeId || dragState.draggedNodeId === node.id) return;
11379
+ if (isDescendant(rootData, dragState.draggedNodeId, node.id)) return;
11380
+ e.preventDefault();
11381
+ e.stopPropagation();
11382
+ const rect = e.currentTarget.getBoundingClientRect();
11383
+ const distFromTop = e.clientY - rect.top;
11384
+ const distFromBottom = rect.bottom - e.clientY;
11385
+ const isExpanded = isGroup && hasChildren && defaultExpandedIds.includes(node.id);
11386
+ let position;
11387
+ if (distFromTop < 10) position = "before";
11388
+ else if (!isExpanded && distFromBottom < 10) position = "after";
11389
+ else position = "inside";
11390
+ if (node.locked && position !== "inside") return;
11391
+ setDragState((prev) => {
11392
+ if (prev.dropTarget?.nodeId === node.id && prev.dropTarget.position === position) return prev;
11393
+ return {
11394
+ ...prev,
11395
+ dropTarget: {
11396
+ nodeId: node.id,
11397
+ position
11398
+ }
11399
+ };
11400
+ });
11401
+ };
11402
+ /** Fires onNodeDrop with the source node and target info, then resets drag state. */
11403
+ const handleDrop = (e) => {
11404
+ e.preventDefault();
11405
+ e.stopPropagation();
11406
+ if (!dragState.draggedNodeId || !dragState.dropTarget || !onNodeDrop) return;
11407
+ const sourceNode = findNode(rootData, dragState.draggedNodeId);
11408
+ if (!sourceNode) return;
11409
+ onNodeDrop(sourceNode, {
11410
+ node,
11411
+ position: dragState.dropTarget.position
11412
+ });
11413
+ setDragState({
11414
+ draggedNodeId: null,
11415
+ dropTarget: null
11416
+ });
11417
+ };
11418
+ /** Clears drop target when the cursor leaves this node (but not when moving to a child element). */
11419
+ const handleDragLeave = (e) => {
11420
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragState((prev) => prev.dropTarget?.nodeId === node.id ? {
11421
+ ...prev,
11422
+ dropTarget: null
11423
+ } : prev);
11424
+ };
11425
+ const isDraggedOver = dragState.dropTarget?.nodeId === node.id;
11426
+ const dropPosition = dragState.dropTarget?.position;
11427
+ const isDragging = dragState.draggedNodeId === node.id;
11428
+ const indent = level * 24;
11429
+ const dropLineBase = "after:absolute after:right-0 after:h-[2.5px] after:bg-primary after:left-(--drop-left) after:z-10";
11430
+ const showLine = isDraggedOver && dropPosition;
11431
+ const dropLineClass = cn("relative", {
11432
+ [`after:top-0 ${dropLineBase}`]: showLine === "before",
11433
+ [`after:-bottom-px ${dropLineBase}`]: showLine === "after" || showLine === "inside",
11434
+ "opacity-50": isDragging
11435
+ });
11436
+ const dropLineStyle = (offset) => isDraggedOver ? { "--drop-left": `${offset + (dropPosition === "inside" ? 24 : 0)}px` } : {};
11437
+ const dropHighlightClass = cn({ "bg-accent text-accent-foreground": isDraggedOver && dropPosition === "inside" });
11294
11438
  if (!isGroup) return /* @__PURE__ */ jsxs("div", {
11295
11439
  "data-test-id": `tree-node-${node.id}`,
11296
- className: cn("relative flex items-center gap-2 py-1.5 px-2 rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors border border-transparent hover:border hover:border-(--discreet-border)", node.className, { "bg-primary text-primary-foreground hover:border hover:border-(--discreet-border)": selectedId === node.id }),
11297
- style: { marginLeft: `${level * 24}px` },
11298
- onClick: () => onNodeSelect?.(node),
11440
+ className: cn("relative group/node flex items-center gap-2 py-1.5 px-2 rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground transition-colors border border-transparent hover:border hover:border-(--discreet-border)", node.className, { "bg-primary text-primary-foreground hover:border hover:border-(--discreet-border)": selectedId === node.id }, dropLineClass, dropHighlightClass),
11441
+ style: {
11442
+ marginLeft: `${indent}px`,
11443
+ ...dropLineStyle(0)
11444
+ },
11445
+ onClick: (e) => {
11446
+ if (e.target.closest("[data-slot=\"drag-handle\"]")) return;
11447
+ onNodeSelect?.(node);
11448
+ },
11449
+ onDragOver: onNodeDrop ? handleDragOver : void 0,
11450
+ onDrop: onNodeDrop ? handleDrop : void 0,
11451
+ onDragLeave: onNodeDrop ? handleDragLeave : void 0,
11299
11452
  children: [
11300
11453
  node.icon,
11301
11454
  /* @__PURE__ */ jsx("span", {
11302
11455
  className: "text-sm select-none truncate",
11303
11456
  children: node.name
11304
11457
  }),
11305
- node.trailing
11458
+ node.trailing,
11459
+ !node.locked && (onNodeDelete || onNodeDrop) && /* @__PURE__ */ jsx(NodeActions, {
11460
+ node,
11461
+ onNodeDelete,
11462
+ onNodeDrop
11463
+ })
11306
11464
  ]
11307
11465
  });
11308
11466
  return /* @__PURE__ */ jsxs(AccordionItem, {
11309
11467
  value: node.id,
11310
- className: "border-0",
11468
+ className: "border-0 overflow-y-visible overflow-x-clip",
11311
11469
  "data-test-id": `tree-group-${node.id}`,
11312
- children: [/* @__PURE__ */ jsxs(AccordionTrigger, {
11313
- style: { "--margin-left": `calc(${level * 24 + 16}px - 16px)` },
11314
- className: cn("flex flex-1 overflow-hidden items-center gap-2 py-1.5 px-2 rounded-md ml-(--margin-left) hover:bg-accent hover:text-accent-foreground hover:no-underline transition-colors border border-transparent hover:border hover:border-(--discreet-border)", "[&>svg:last-child]:hidden", node.className, { "bg-primary text-primary-foreground": selectedId === node.id }),
11315
- children: [
11316
- /* @__PURE__ */ jsx(PlusIcon, {
11317
- "data-test-id": `tree-expand-${node.id}`,
11318
- className: "size-4 shrink-0 hidden [[data-state=closed]>&]:block hover:border rounded",
11319
- onClick: () => {
11320
- onToggleGroup?.({
11321
- isExpanded: true,
11322
- node
11323
- });
11324
- }
11325
- }),
11326
- /* @__PURE__ */ jsx(MinusIcon, {
11327
- "data-test-id": `tree-collapse-${node.id}`,
11328
- className: "size-4 shrink-0 hidden [[data-state=open]>&]:block hover:border rounded",
11329
- onClick: () => {
11330
- onToggleGroup?.({
11331
- isExpanded: false,
11332
- node
11333
- });
11334
- }
11335
- }),
11336
- /* @__PURE__ */ jsxs("div", {
11337
- "data-test-id": `tree-group-label-${node.id}`,
11338
- className: "flex w-[calc(100%-16px)] gap-2",
11339
- onClick: (e) => {
11340
- e.stopPropagation();
11341
- onGroupSelect?.(node);
11342
- },
11343
- children: [
11344
- node.icon,
11345
- /* @__PURE__ */ jsx("span", {
11346
- className: "text-sm select-none truncate text-left",
11347
- children: node.name
11348
- }),
11349
- node.trailing
11350
- ]
11351
- })
11352
- ]
11470
+ children: [/* @__PURE__ */ jsxs("div", {
11471
+ className: cn("relative group/node overflow-y-visible overflow-x-clip", dropLineClass),
11472
+ style: dropLineStyle(indent),
11473
+ onDragOver: onNodeDrop ? handleDragOver : void 0,
11474
+ onDrop: onNodeDrop ? handleDrop : void 0,
11475
+ onDragLeave: onNodeDrop ? handleDragLeave : void 0,
11476
+ children: [/* @__PURE__ */ jsxs(AccordionTrigger, {
11477
+ style: { "--margin-left": `calc(${level * 24 + 16}px - 16px)` },
11478
+ className: cn("relative flex flex-1 overflow-hidden items-center gap-2 py-1.5 px-2 rounded-md ml-(--margin-left) group-hover/node:bg-accent group-hover/node:text-accent-foreground hover:no-underline transition-colors border border-transparent group-hover/node:border group-hover/node:border-(--discreet-border)", "[&>svg:last-child]:hidden", node.className, { "bg-primary text-primary-foreground": selectedId === node.id }, dropHighlightClass),
11479
+ children: [
11480
+ /* @__PURE__ */ jsx(PlusIcon, {
11481
+ "data-test-id": `tree-expand-${node.id}`,
11482
+ className: "size-4 shrink-0 hidden [[data-state=closed]>&]:block hover:border rounded",
11483
+ onClick: () => {
11484
+ onToggleGroup?.({
11485
+ isExpanded: true,
11486
+ node
11487
+ });
11488
+ }
11489
+ }),
11490
+ /* @__PURE__ */ jsx(MinusIcon, {
11491
+ "data-test-id": `tree-collapse-${node.id}`,
11492
+ className: "size-4 shrink-0 hidden [[data-state=open]>&]:block hover:border rounded",
11493
+ onClick: () => {
11494
+ onToggleGroup?.({
11495
+ isExpanded: false,
11496
+ node
11497
+ });
11498
+ }
11499
+ }),
11500
+ /* @__PURE__ */ jsxs("div", {
11501
+ "data-test-id": `tree-group-label-${node.id}`,
11502
+ className: "flex w-[calc(100%-16px)] gap-2",
11503
+ onClick: (e) => {
11504
+ e.stopPropagation();
11505
+ if (e.target.closest("[data-slot=\"dropdown-menu-trigger\"]")) return;
11506
+ if (e.target.closest("[data-slot=\"drag-handle\"]")) return;
11507
+ onGroupSelect?.(node);
11508
+ },
11509
+ children: [
11510
+ node.icon,
11511
+ /* @__PURE__ */ jsx("span", {
11512
+ className: "text-sm select-none truncate text-left",
11513
+ children: node.name
11514
+ }),
11515
+ node.trailing
11516
+ ]
11517
+ })
11518
+ ]
11519
+ }), !node.locked && (onNodeDelete || onNodeDrop) && /* @__PURE__ */ jsx(NodeActions, {
11520
+ node,
11521
+ onNodeDelete,
11522
+ onNodeDrop
11523
+ })]
11353
11524
  }), hasChildren && /* @__PURE__ */ jsxs(AccordionContent, {
11354
- className: "pb-0 pt-0 relative",
11525
+ className: "pb-0 pt-0 relative overflow-y-visible overflow-x-clip",
11355
11526
  children: [/* @__PURE__ */ jsx("div", {
11356
11527
  style: { "--left-offset": `calc(${level * 24 + 32}px - 16px)` },
11357
11528
  className: "before:absolute before:top-0 before:start-(--left-offset) before:w-0.5 before:-ms-px before:h-full before:bg-sidebar-ring dark:before:bg-sidebar-ring"
11358
11529
  }), /* @__PURE__ */ jsx(Accordion, {
11359
11530
  type: "multiple",
11360
11531
  value: defaultExpandedIds,
11361
- children: node.children?.map((child, index) => /* @__PURE__ */ jsx(TreeBranch, {
11532
+ children: node.children?.map((child) => /* @__PURE__ */ jsx(TreeBranch, {
11362
11533
  node: child,
11363
11534
  level: level + 1,
11364
11535
  onNodeSelect,
11365
11536
  onGroupSelect,
11366
11537
  onToggleGroup,
11367
- isLast: index === node.children.length - 1,
11368
- parentIsLast: [...parentIsLast, isLast],
11538
+ onNodeDelete,
11539
+ onNodeDrop,
11369
11540
  defaultExpandedIds,
11370
- selectedId
11541
+ selectedId,
11542
+ rootData
11371
11543
  }, child.id))
11372
11544
  })]
11373
11545
  })]
11374
11546
  });
11375
11547
  };
11376
- const TreeView = React$1.forwardRef(({ data, onNodeSelect, onGroupSelect, onToggleGroup, className, defaultExpandedIds = [], selectedId }, ref) => {
11377
- return /* @__PURE__ */ jsx("div", {
11378
- ref,
11379
- className: cn("w-full select-none", className),
11380
- children: /* @__PURE__ */ jsx(Accordion, {
11381
- type: "multiple",
11382
- value: defaultExpandedIds,
11383
- children: data.map((node, index) => /* @__PURE__ */ jsx(TreeBranch, {
11384
- node,
11385
- level: 0,
11386
- onNodeSelect,
11387
- onGroupSelect,
11388
- onToggleGroup,
11389
- isLast: index === data.length - 1,
11390
- parentIsLast: [],
11391
- defaultExpandedIds,
11392
- selectedId
11393
- }, node.id))
11548
+ const TreeView = React$1.forwardRef(({ data, onNodeSelect, onGroupSelect, onToggleGroup, onNodeDelete, onNodeDrop, className, defaultExpandedIds = [], selectedId }, ref) => {
11549
+ const [dragState, setDragState] = React$1.useState({
11550
+ draggedNodeId: null,
11551
+ dropTarget: null
11552
+ });
11553
+ return /* @__PURE__ */ jsx(DragContext.Provider, {
11554
+ value: {
11555
+ dragState,
11556
+ setDragState
11557
+ },
11558
+ children: /* @__PURE__ */ jsx("div", {
11559
+ ref,
11560
+ className: cn("w-full select-none", className),
11561
+ onDragOver: onNodeDrop && dragState.draggedNodeId ? (e) => e.preventDefault() : void 0,
11562
+ children: /* @__PURE__ */ jsx(Accordion, {
11563
+ type: "multiple",
11564
+ value: defaultExpandedIds,
11565
+ children: data.map((node) => /* @__PURE__ */ jsx(TreeBranch, {
11566
+ node,
11567
+ level: 0,
11568
+ onNodeSelect,
11569
+ onGroupSelect,
11570
+ onToggleGroup,
11571
+ onNodeDelete,
11572
+ onNodeDrop,
11573
+ defaultExpandedIds,
11574
+ selectedId,
11575
+ rootData: data
11576
+ }, node.id))
11577
+ })
11394
11578
  })
11395
11579
  });
11396
11580
  });
11581
+ const NodeActions = ({ node, onNodeDelete, onNodeDrop }) => {
11582
+ const [showDeleteConfirmation, setShowDeleteConfirmation] = React$1.useState(false);
11583
+ const containerRef = React$1.useRef(null);
11584
+ const { setDragState } = React$1.useContext(DragContext);
11585
+ React$1.useEffect(() => {
11586
+ if (!showDeleteConfirmation) return;
11587
+ const handleClickOutside = (e) => {
11588
+ if (containerRef.current && !containerRef.current.contains(e.target)) setShowDeleteConfirmation(false);
11589
+ };
11590
+ document.addEventListener("mousedown", handleClickOutside);
11591
+ return () => document.removeEventListener("mousedown", handleClickOutside);
11592
+ }, [showDeleteConfirmation]);
11593
+ return /* @__PURE__ */ jsx("div", {
11594
+ ref: containerRef,
11595
+ className: "absolute top-1/2 -translate-y-1/2 end-1 flex items-center gap-0.5 rounded transition-none group-hover/node:bg-accent",
11596
+ children: showDeleteConfirmation && onNodeDelete ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
11597
+ variant: "ghost",
11598
+ size: "iconXs",
11599
+ onClick: (e) => {
11600
+ e.stopPropagation();
11601
+ setShowDeleteConfirmation(false);
11602
+ },
11603
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-4" })
11604
+ }), /* @__PURE__ */ jsx(Button, {
11605
+ variant: "destructive",
11606
+ size: "iconXs",
11607
+ onClick: (e) => {
11608
+ e.stopPropagation();
11609
+ onNodeDelete(node);
11610
+ setShowDeleteConfirmation(false);
11611
+ },
11612
+ children: /* @__PURE__ */ jsx(CheckIcon$1, { className: "size-4" })
11613
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [onNodeDelete && /* @__PURE__ */ jsx(Button, {
11614
+ variant: "ghost",
11615
+ size: "iconXs",
11616
+ className: "opacity-0 group-hover/node:opacity-100",
11617
+ onClick: (e) => {
11618
+ e.stopPropagation();
11619
+ setShowDeleteConfirmation(true);
11620
+ },
11621
+ children: /* @__PURE__ */ jsx(Trash2Icon, { className: "size-4" })
11622
+ }), onNodeDrop && /* @__PURE__ */ jsx(Button, {
11623
+ variant: "ghost",
11624
+ size: "iconXs",
11625
+ "data-slot": "drag-handle",
11626
+ draggable: true,
11627
+ className: "opacity-0 group-hover/node:opacity-100 cursor-grab active:cursor-grabbing bg-transparent",
11628
+ onDragStart: (e) => {
11629
+ e.dataTransfer.effectAllowed = "move";
11630
+ e.dataTransfer.setData("text/plain", node.id);
11631
+ setDragState({
11632
+ draggedNodeId: node.id,
11633
+ dropTarget: null
11634
+ });
11635
+ },
11636
+ onDragEnd: () => {
11637
+ setDragState({
11638
+ draggedNodeId: null,
11639
+ dropTarget: null
11640
+ });
11641
+ },
11642
+ children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-4" })
11643
+ })] })
11644
+ });
11645
+ };
11397
11646
  TreeView.displayName = "TreeView";
11398
11647
 
11399
11648
  //#endregion