@postxl/ui-components 1.6.0 → 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";
@@ -11335,112 +11335,314 @@ ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
11335
11335
 
11336
11336
  //#endregion
11337
11337
  //#region src/tree-view/tree-view.tsx
11338
- 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 }) => {
11339
11373
  const isGroup = node.type === "group";
11340
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" });
11341
11438
  if (!isGroup) return /* @__PURE__ */ jsxs("div", {
11342
11439
  "data-test-id": `tree-node-${node.id}`,
11343
- 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 }),
11344
- style: { marginLeft: `${level * 24}px` },
11345
- 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,
11346
11452
  children: [
11347
11453
  node.icon,
11348
11454
  /* @__PURE__ */ jsx("span", {
11349
11455
  className: "text-sm select-none truncate",
11350
11456
  children: node.name
11351
11457
  }),
11352
- node.trailing
11458
+ node.trailing,
11459
+ !node.locked && (onNodeDelete || onNodeDrop) && /* @__PURE__ */ jsx(NodeActions, {
11460
+ node,
11461
+ onNodeDelete,
11462
+ onNodeDrop
11463
+ })
11353
11464
  ]
11354
11465
  });
11355
11466
  return /* @__PURE__ */ jsxs(AccordionItem, {
11356
11467
  value: node.id,
11357
- className: "border-0",
11468
+ className: "border-0 overflow-y-visible overflow-x-clip",
11358
11469
  "data-test-id": `tree-group-${node.id}`,
11359
- children: [/* @__PURE__ */ jsxs(AccordionTrigger, {
11360
- style: { "--margin-left": `calc(${level * 24 + 16}px - 16px)` },
11361
- 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 }),
11362
- children: [
11363
- /* @__PURE__ */ jsx(PlusIcon, {
11364
- "data-test-id": `tree-expand-${node.id}`,
11365
- className: "size-4 shrink-0 hidden [[data-state=closed]>&]:block hover:border rounded",
11366
- onClick: () => {
11367
- onToggleGroup?.({
11368
- isExpanded: true,
11369
- node
11370
- });
11371
- }
11372
- }),
11373
- /* @__PURE__ */ jsx(MinusIcon, {
11374
- "data-test-id": `tree-collapse-${node.id}`,
11375
- className: "size-4 shrink-0 hidden [[data-state=open]>&]:block hover:border rounded",
11376
- onClick: () => {
11377
- onToggleGroup?.({
11378
- isExpanded: false,
11379
- node
11380
- });
11381
- }
11382
- }),
11383
- /* @__PURE__ */ jsxs("div", {
11384
- "data-test-id": `tree-group-label-${node.id}`,
11385
- className: "flex w-[calc(100%-16px)] gap-2",
11386
- onClick: (e) => {
11387
- e.stopPropagation();
11388
- onGroupSelect?.(node);
11389
- },
11390
- children: [
11391
- node.icon,
11392
- /* @__PURE__ */ jsx("span", {
11393
- className: "text-sm select-none truncate text-left",
11394
- children: node.name
11395
- }),
11396
- node.trailing
11397
- ]
11398
- })
11399
- ]
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
+ })]
11400
11524
  }), hasChildren && /* @__PURE__ */ jsxs(AccordionContent, {
11401
- className: "pb-0 pt-0 relative",
11525
+ className: "pb-0 pt-0 relative overflow-y-visible overflow-x-clip",
11402
11526
  children: [/* @__PURE__ */ jsx("div", {
11403
11527
  style: { "--left-offset": `calc(${level * 24 + 32}px - 16px)` },
11404
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"
11405
11529
  }), /* @__PURE__ */ jsx(Accordion, {
11406
11530
  type: "multiple",
11407
11531
  value: defaultExpandedIds,
11408
- children: node.children?.map((child, index) => /* @__PURE__ */ jsx(TreeBranch, {
11532
+ children: node.children?.map((child) => /* @__PURE__ */ jsx(TreeBranch, {
11409
11533
  node: child,
11410
11534
  level: level + 1,
11411
11535
  onNodeSelect,
11412
11536
  onGroupSelect,
11413
11537
  onToggleGroup,
11414
- isLast: index === node.children.length - 1,
11415
- parentIsLast: [...parentIsLast, isLast],
11538
+ onNodeDelete,
11539
+ onNodeDrop,
11416
11540
  defaultExpandedIds,
11417
- selectedId
11541
+ selectedId,
11542
+ rootData
11418
11543
  }, child.id))
11419
11544
  })]
11420
11545
  })]
11421
11546
  });
11422
11547
  };
11423
- const TreeView = React$1.forwardRef(({ data, onNodeSelect, onGroupSelect, onToggleGroup, className, defaultExpandedIds = [], selectedId }, ref) => {
11424
- return /* @__PURE__ */ jsx("div", {
11425
- ref,
11426
- className: cn("w-full select-none", className),
11427
- children: /* @__PURE__ */ jsx(Accordion, {
11428
- type: "multiple",
11429
- value: defaultExpandedIds,
11430
- children: data.map((node, index) => /* @__PURE__ */ jsx(TreeBranch, {
11431
- node,
11432
- level: 0,
11433
- onNodeSelect,
11434
- onGroupSelect,
11435
- onToggleGroup,
11436
- isLast: index === data.length - 1,
11437
- parentIsLast: [],
11438
- defaultExpandedIds,
11439
- selectedId
11440
- }, 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
+ })
11441
11578
  })
11442
11579
  });
11443
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
+ };
11444
11646
  TreeView.displayName = "TreeView";
11445
11647
 
11446
11648
  //#endregion