@particle-academy/react-fancy 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -11423,35 +11423,151 @@ var KanbanColumnContext = react.createContext("");
11423
11423
  function useKanbanColumn() {
11424
11424
  return react.useContext(KanbanColumnContext);
11425
11425
  }
11426
+ var DEFAULT_CARD_CLASSES = "rounded-lg border border-zinc-200 bg-white p-3 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-700 dark:bg-zinc-900";
11427
+ function KanbanCard({ children, id, className, unstyled }) {
11428
+ const { setDraggedCard, setDragSource } = useKanban();
11429
+ const columnId = useKanbanColumn();
11430
+ const handleDragStart = react.useCallback(() => {
11431
+ setDraggedCard(id);
11432
+ setDragSource(columnId);
11433
+ }, [id, columnId, setDraggedCard, setDragSource]);
11434
+ const handleDragEnd = react.useCallback(() => {
11435
+ setDraggedCard(null);
11436
+ setDragSource(null);
11437
+ }, [setDraggedCard, setDragSource]);
11438
+ return /* @__PURE__ */ jsxRuntime.jsx(
11439
+ "div",
11440
+ {
11441
+ "data-react-fancy-kanban-card": "",
11442
+ draggable: true,
11443
+ onDragStart: handleDragStart,
11444
+ onDragEnd: handleDragEnd,
11445
+ className: cn(
11446
+ // Drag affordance — kept even when unstyled so users still see grab cursors.
11447
+ "cursor-grab active:cursor-grabbing",
11448
+ !unstyled && DEFAULT_CARD_CLASSES,
11449
+ className
11450
+ ),
11451
+ children
11452
+ }
11453
+ );
11454
+ }
11455
+ KanbanCard.displayName = "KanbanCard";
11426
11456
  var DEFAULT_COLUMN_CLASSES = "min-h-[200px] w-72 rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800/50";
11457
+ function countCardChildren(children) {
11458
+ let n = 0;
11459
+ react.Children.forEach(children, (child) => {
11460
+ if (!react.isValidElement(child)) return;
11461
+ if (child.type === KanbanCard) {
11462
+ n += 1;
11463
+ return;
11464
+ }
11465
+ if (child.type === react.Fragment) {
11466
+ n += countCardChildren(child.props.children);
11467
+ }
11468
+ });
11469
+ return n;
11470
+ }
11427
11471
  function KanbanColumn({
11428
11472
  children,
11429
11473
  id,
11430
11474
  title,
11431
11475
  className,
11432
- unstyled
11476
+ unstyled,
11477
+ wipLimit,
11478
+ hideWhenEmpty
11433
11479
  }) {
11434
- const { onCardMove, draggedCard, dragSource } = useKanban();
11480
+ const { onCardMove, draggedCard, dragSource, registerColumn } = useKanban();
11435
11481
  const [dragOver, setDragOver] = react.useState(false);
11482
+ const [dropIndex, setDropIndex] = react.useState(null);
11483
+ const cardsRef = react.useRef(null);
11484
+ react.useEffect(() => registerColumn(id), [id, registerColumn]);
11485
+ const updateDropIndex = react.useCallback((clientY) => {
11486
+ const container = cardsRef.current;
11487
+ if (!container) {
11488
+ setDropIndex(null);
11489
+ return;
11490
+ }
11491
+ const cards = container.querySelectorAll(
11492
+ ":scope > [data-react-fancy-kanban-card]"
11493
+ );
11494
+ let idx = cards.length;
11495
+ for (let i = 0; i < cards.length; i++) {
11496
+ const rect = cards[i].getBoundingClientRect();
11497
+ if (clientY < rect.top + rect.height / 2) {
11498
+ idx = i;
11499
+ break;
11500
+ }
11501
+ }
11502
+ setDropIndex(idx);
11503
+ }, []);
11504
+ const handleDragOver = react.useCallback(
11505
+ (e) => {
11506
+ if (!draggedCard) return;
11507
+ e.preventDefault();
11508
+ e.stopPropagation();
11509
+ setDragOver(true);
11510
+ updateDropIndex(e.clientY);
11511
+ },
11512
+ [draggedCard, updateDropIndex]
11513
+ );
11514
+ const handleDragLeave = react.useCallback((e) => {
11515
+ if (e.currentTarget.contains(e.relatedTarget)) return;
11516
+ setDragOver(false);
11517
+ setDropIndex(null);
11518
+ }, []);
11436
11519
  const handleDrop = react.useCallback(
11437
11520
  (e) => {
11521
+ if (!draggedCard) return;
11438
11522
  e.preventDefault();
11439
- setDragOver(false);
11440
- if (draggedCard && dragSource && dragSource !== id) {
11441
- onCardMove?.(draggedCard, dragSource, id);
11523
+ e.stopPropagation();
11524
+ const target = dropIndex ?? 0;
11525
+ if (dragSource && draggedCard) {
11526
+ let finalIdx = target;
11527
+ if (dragSource === id) {
11528
+ const srcIdx = findCardIndex(children, draggedCard);
11529
+ if (srcIdx !== -1 && target > srcIdx) {
11530
+ finalIdx = target - 1;
11531
+ }
11532
+ if (srcIdx === finalIdx) {
11533
+ setDragOver(false);
11534
+ setDropIndex(null);
11535
+ return;
11536
+ }
11537
+ }
11538
+ onCardMove?.(draggedCard, dragSource, id, finalIdx);
11442
11539
  }
11540
+ setDragOver(false);
11541
+ setDropIndex(null);
11443
11542
  },
11444
- [draggedCard, dragSource, id, onCardMove]
11543
+ [draggedCard, dragSource, dropIndex, id, onCardMove, children]
11445
11544
  );
11446
- const handleDragOver = react.useCallback((e) => {
11447
- e.preventDefault();
11448
- setDragOver(true);
11449
- }, []);
11450
- const handleDragLeave = react.useCallback(() => setDragOver(false), []);
11545
+ const cardCount = countCardChildren(children);
11546
+ if (hideWhenEmpty && cardCount === 0 && !draggedCard) {
11547
+ return null;
11548
+ }
11549
+ let cardSeen = 0;
11550
+ const showIndicator = draggedCard !== null && dropIndex !== null && dragOver;
11551
+ const renderedChildren = react.Children.toArray(children).map((child, i) => {
11552
+ const isCard = react.isValidElement(child) && child.type === KanbanCard;
11553
+ const indicator = showIndicator && isCard && cardSeen === dropIndex ? /* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}, `drop-${i}`) : null;
11554
+ if (isCard) cardSeen += 1;
11555
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
11556
+ indicator,
11557
+ child
11558
+ ] }, i);
11559
+ });
11560
+ if (showIndicator && dropIndex === cardCount) {
11561
+ renderedChildren.push(/* @__PURE__ */ jsxRuntime.jsx(DropIndicator, {}, "drop-end"));
11562
+ }
11563
+ const overWip = wipLimit !== void 0 && cardCount > wipLimit;
11451
11564
  return /* @__PURE__ */ jsxRuntime.jsx(KanbanColumnContext.Provider, { value: id, children: /* @__PURE__ */ jsxRuntime.jsxs(
11452
11565
  "div",
11453
11566
  {
11454
11567
  "data-react-fancy-kanban-column": "",
11568
+ "data-column-id": id,
11569
+ role: "group",
11570
+ "aria-label": title,
11455
11571
  onDrop: handleDrop,
11456
11572
  onDragOver: handleDragOver,
11457
11573
  onDragLeave: handleDragLeave,
@@ -11462,55 +11578,181 @@ function KanbanColumn({
11462
11578
  className
11463
11579
  ),
11464
11580
  children: [
11465
- title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "mb-3 text-sm font-semibold text-zinc-600 dark:text-zinc-400", children: title }),
11466
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 flex-col gap-2", children })
11581
+ title && /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "mb-3 flex items-center gap-2 text-sm font-semibold text-zinc-600 dark:text-zinc-400", children: [
11582
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: title }),
11583
+ /* @__PURE__ */ jsxRuntime.jsxs(
11584
+ "span",
11585
+ {
11586
+ className: cn(
11587
+ "rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
11588
+ overWip ? "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300" : "bg-zinc-200 text-zinc-700 dark:bg-zinc-700 dark:text-zinc-300"
11589
+ ),
11590
+ children: [
11591
+ cardCount,
11592
+ wipLimit !== void 0 ? `/${wipLimit}` : ""
11593
+ ]
11594
+ }
11595
+ )
11596
+ ] }),
11597
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: cardsRef, className: "flex flex-1 flex-col gap-2", children: renderedChildren })
11467
11598
  ]
11468
11599
  }
11469
11600
  ) });
11470
11601
  }
11471
11602
  KanbanColumn.displayName = "KanbanColumn";
11472
- var DEFAULT_CARD_CLASSES = "rounded-lg border border-zinc-200 bg-white p-3 shadow-sm transition-shadow hover:shadow-md dark:border-zinc-700 dark:bg-zinc-900";
11473
- function KanbanCard({ children, id, className, unstyled }) {
11474
- const { setDraggedCard, setDragSource } = useKanban();
11603
+ function DropIndicator() {
11604
+ return /* @__PURE__ */ jsxRuntime.jsx(
11605
+ "div",
11606
+ {
11607
+ "data-react-fancy-kanban-drop-indicator": "",
11608
+ className: "h-0.5 -my-1 rounded-full bg-blue-500/80"
11609
+ }
11610
+ );
11611
+ }
11612
+ function findCardIndex(children, cardId) {
11613
+ let idx = -1;
11614
+ let i = 0;
11615
+ react.Children.forEach(children, (child) => {
11616
+ if (idx !== -1) return;
11617
+ if (!react.isValidElement(child)) return;
11618
+ if (child.type === KanbanCard) {
11619
+ if (child.props.id === cardId) {
11620
+ idx = i;
11621
+ }
11622
+ i += 1;
11623
+ }
11624
+ });
11625
+ return idx;
11626
+ }
11627
+ function KanbanColumnHandle({
11628
+ children,
11629
+ className
11630
+ }) {
11631
+ const { setDraggedColumn } = useKanban();
11475
11632
  const columnId = useKanbanColumn();
11476
- const handleDragStart = react.useCallback(() => {
11477
- setDraggedCard(id);
11478
- setDragSource(columnId);
11479
- }, [id, columnId, setDraggedCard, setDragSource]);
11633
+ const handleDragStart = react.useCallback(
11634
+ (e) => {
11635
+ e.dataTransfer.effectAllowed = "move";
11636
+ e.dataTransfer.setData("text/plain", columnId);
11637
+ e.stopPropagation();
11638
+ setDraggedColumn(columnId);
11639
+ },
11640
+ [columnId, setDraggedColumn]
11641
+ );
11480
11642
  const handleDragEnd = react.useCallback(() => {
11481
- setDraggedCard(null);
11482
- setDragSource(null);
11483
- }, [setDraggedCard, setDragSource]);
11643
+ setDraggedColumn(null);
11644
+ }, [setDraggedColumn]);
11484
11645
  return /* @__PURE__ */ jsxRuntime.jsx(
11485
11646
  "div",
11486
11647
  {
11487
- "data-react-fancy-kanban-card": "",
11488
11648
  draggable: true,
11489
11649
  onDragStart: handleDragStart,
11490
11650
  onDragEnd: handleDragEnd,
11651
+ "data-react-fancy-kanban-column-handle": "",
11491
11652
  className: cn(
11492
- // Drag affordance — kept even when unstyled so users still see grab cursors.
11493
- "cursor-grab active:cursor-grabbing",
11494
- !unstyled && DEFAULT_CARD_CLASSES,
11653
+ "cursor-grab active:cursor-grabbing select-none",
11495
11654
  className
11496
11655
  ),
11497
11656
  children
11498
11657
  }
11499
11658
  );
11500
11659
  }
11501
- KanbanCard.displayName = "KanbanCard";
11502
- function KanbanRoot({ children, onCardMove, className }) {
11660
+ KanbanColumnHandle.displayName = "KanbanColumnHandle";
11661
+ function KanbanRoot({
11662
+ children,
11663
+ onCardMove,
11664
+ onColumnMove,
11665
+ className
11666
+ }) {
11503
11667
  const [draggedCard, setDraggedCard] = react.useState(null);
11504
11668
  const [dragSource, setDragSource] = react.useState(null);
11669
+ const [draggedColumn, setDraggedColumn] = react.useState(null);
11670
+ const orderRef = react.useRef([]);
11671
+ const [columnIds, setColumnIds] = react.useState([]);
11672
+ const registerColumn = react.useCallback((id) => {
11673
+ if (!orderRef.current.includes(id)) {
11674
+ orderRef.current = [...orderRef.current, id];
11675
+ setColumnIds(orderRef.current);
11676
+ }
11677
+ return () => {
11678
+ orderRef.current = orderRef.current.filter((x) => x !== id);
11679
+ setColumnIds(orderRef.current);
11680
+ };
11681
+ }, []);
11505
11682
  const ctx = react.useMemo(
11506
- () => ({ onCardMove, draggedCard, setDraggedCard, dragSource, setDragSource }),
11507
- [onCardMove, draggedCard, dragSource]
11683
+ () => ({
11684
+ draggedCard,
11685
+ setDraggedCard,
11686
+ dragSource,
11687
+ setDragSource,
11688
+ draggedColumn,
11689
+ setDraggedColumn,
11690
+ onCardMove,
11691
+ onColumnMove,
11692
+ columnIds,
11693
+ registerColumn
11694
+ }),
11695
+ [
11696
+ draggedCard,
11697
+ dragSource,
11698
+ draggedColumn,
11699
+ onCardMove,
11700
+ onColumnMove,
11701
+ columnIds,
11702
+ registerColumn
11703
+ ]
11704
+ );
11705
+ const containerRef = react.useRef(null);
11706
+ const handleDragOver = react.useCallback(
11707
+ (e) => {
11708
+ if (!draggedColumn) return;
11709
+ e.preventDefault();
11710
+ },
11711
+ [draggedColumn]
11508
11712
  );
11509
- return /* @__PURE__ */ jsxRuntime.jsx(KanbanContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-react-fancy-kanban": "", className: cn("flex gap-4 overflow-x-auto p-4", className), children }) });
11713
+ const handleDrop = react.useCallback(
11714
+ (e) => {
11715
+ if (!draggedColumn || !containerRef.current) return;
11716
+ e.preventDefault();
11717
+ const cols = containerRef.current.querySelectorAll(
11718
+ "[data-react-fancy-kanban-column]"
11719
+ );
11720
+ const x = e.clientX;
11721
+ let dropIdx = cols.length;
11722
+ for (let i = 0; i < cols.length; i++) {
11723
+ const rect = cols[i].getBoundingClientRect();
11724
+ if (x < rect.left + rect.width / 2) {
11725
+ dropIdx = i;
11726
+ break;
11727
+ }
11728
+ }
11729
+ const sourceIdx = columnIds.indexOf(draggedColumn);
11730
+ const finalIdx = sourceIdx >= 0 && dropIdx > sourceIdx ? dropIdx - 1 : dropIdx;
11731
+ if (finalIdx !== sourceIdx) {
11732
+ onColumnMove?.(draggedColumn, finalIdx);
11733
+ }
11734
+ setDraggedColumn(null);
11735
+ },
11736
+ [draggedColumn, columnIds, onColumnMove]
11737
+ );
11738
+ return /* @__PURE__ */ jsxRuntime.jsx(KanbanContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
11739
+ "div",
11740
+ {
11741
+ ref: containerRef,
11742
+ "data-react-fancy-kanban": "",
11743
+ onDragOver: handleDragOver,
11744
+ onDrop: handleDrop,
11745
+ role: "application",
11746
+ "aria-roledescription": "kanban board",
11747
+ className: cn("flex gap-4 overflow-x-auto p-4", className),
11748
+ children
11749
+ }
11750
+ ) });
11510
11751
  }
11511
11752
  var Kanban = Object.assign(KanbanRoot, {
11512
11753
  Column: KanbanColumn,
11513
- Card: KanbanCard
11754
+ Card: KanbanCard,
11755
+ ColumnHandle: KanbanColumnHandle
11514
11756
  });
11515
11757
  var CanvasContext = react.createContext(null);
11516
11758
  function useCanvas() {