@particle-academy/react-fancy 2.7.1 → 2.8.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.cjs CHANGED
@@ -11423,35 +11423,175 @@ 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
+ const inner = child.props.children;
11466
+ if (inner !== void 0) n += countCardChildren(inner);
11467
+ });
11468
+ return n;
11469
+ }
11470
+ function findCardIndex(children, cardId) {
11471
+ let idx = -1;
11472
+ let i = 0;
11473
+ function walk2(nodes) {
11474
+ react.Children.forEach(nodes, (child) => {
11475
+ if (idx !== -1) return;
11476
+ if (!react.isValidElement(child)) return;
11477
+ if (child.type === KanbanCard) {
11478
+ if (child.props.id === cardId) idx = i;
11479
+ i += 1;
11480
+ return;
11481
+ }
11482
+ const inner = child.props.children;
11483
+ if (inner !== void 0) walk2(inner);
11484
+ });
11485
+ }
11486
+ walk2(children);
11487
+ return idx;
11488
+ }
11427
11489
  function KanbanColumn({
11428
11490
  children,
11429
11491
  id,
11430
11492
  title,
11431
11493
  className,
11432
- unstyled
11494
+ unstyled,
11495
+ wipLimit,
11496
+ hideWhenEmpty
11433
11497
  }) {
11434
- const { onCardMove, draggedCard, dragSource } = useKanban();
11498
+ const { onCardMove, draggedCard, dragSource, registerColumn } = useKanban();
11435
11499
  const [dragOver, setDragOver] = react.useState(false);
11500
+ const [dropIndex, setDropIndex] = react.useState(null);
11501
+ const [dropY, setDropY] = react.useState(null);
11502
+ const cardsRef = react.useRef(null);
11503
+ react.useEffect(() => registerColumn(id), [id, registerColumn]);
11504
+ const updateDrop = react.useCallback((clientY) => {
11505
+ const container = cardsRef.current;
11506
+ if (!container) {
11507
+ setDropIndex(null);
11508
+ setDropY(null);
11509
+ return;
11510
+ }
11511
+ const all = Array.from(
11512
+ container.querySelectorAll(
11513
+ "[data-react-fancy-kanban-card]"
11514
+ )
11515
+ );
11516
+ const cards = all.filter(
11517
+ (el) => el.closest("[data-react-fancy-kanban-column]") === cardsRef.current?.closest("[data-react-fancy-kanban-column]")
11518
+ );
11519
+ const containerRect = container.getBoundingClientRect();
11520
+ if (cards.length === 0) {
11521
+ setDropIndex(0);
11522
+ setDropY(0);
11523
+ return;
11524
+ }
11525
+ let idx = cards.length;
11526
+ let yRel = 0;
11527
+ for (let i = 0; i < cards.length; i++) {
11528
+ const rect = cards[i].getBoundingClientRect();
11529
+ if (clientY < rect.top + rect.height / 2) {
11530
+ idx = i;
11531
+ yRel = rect.top - containerRect.top;
11532
+ break;
11533
+ }
11534
+ }
11535
+ if (idx === cards.length) {
11536
+ const last = cards[cards.length - 1].getBoundingClientRect();
11537
+ yRel = last.bottom - containerRect.top;
11538
+ }
11539
+ setDropIndex(idx);
11540
+ setDropY(yRel);
11541
+ }, []);
11542
+ const handleDragOver = react.useCallback(
11543
+ (e) => {
11544
+ if (!draggedCard) return;
11545
+ e.preventDefault();
11546
+ e.stopPropagation();
11547
+ setDragOver(true);
11548
+ updateDrop(e.clientY);
11549
+ },
11550
+ [draggedCard, updateDrop]
11551
+ );
11552
+ const handleDragLeave = react.useCallback((e) => {
11553
+ if (e.currentTarget.contains(e.relatedTarget)) return;
11554
+ setDragOver(false);
11555
+ setDropIndex(null);
11556
+ setDropY(null);
11557
+ }, []);
11436
11558
  const handleDrop = react.useCallback(
11437
11559
  (e) => {
11560
+ if (!draggedCard) return;
11438
11561
  e.preventDefault();
11439
- setDragOver(false);
11440
- if (draggedCard && dragSource && dragSource !== id) {
11441
- onCardMove?.(draggedCard, dragSource, id);
11562
+ e.stopPropagation();
11563
+ const target = dropIndex ?? 0;
11564
+ if (dragSource) {
11565
+ let finalIdx = target;
11566
+ if (dragSource === id) {
11567
+ const srcIdx = findCardIndex(children, draggedCard);
11568
+ if (srcIdx !== -1 && target > srcIdx) finalIdx = target - 1;
11569
+ if (srcIdx === finalIdx) {
11570
+ setDragOver(false);
11571
+ setDropIndex(null);
11572
+ setDropY(null);
11573
+ return;
11574
+ }
11575
+ }
11576
+ onCardMove?.(draggedCard, dragSource, id, finalIdx);
11442
11577
  }
11578
+ setDragOver(false);
11579
+ setDropIndex(null);
11580
+ setDropY(null);
11443
11581
  },
11444
- [draggedCard, dragSource, id, onCardMove]
11582
+ [draggedCard, dragSource, dropIndex, id, onCardMove, children]
11445
11583
  );
11446
- const handleDragOver = react.useCallback((e) => {
11447
- e.preventDefault();
11448
- setDragOver(true);
11449
- }, []);
11450
- const handleDragLeave = react.useCallback(() => setDragOver(false), []);
11584
+ const cardCount = countCardChildren(children);
11585
+ if (hideWhenEmpty && cardCount === 0 && !draggedCard) return null;
11586
+ const showIndicator = draggedCard !== null && dropIndex !== null && dropY !== null && dragOver;
11587
+ const overWip = wipLimit !== void 0 && cardCount > wipLimit;
11451
11588
  return /* @__PURE__ */ jsxRuntime.jsx(KanbanColumnContext.Provider, { value: id, children: /* @__PURE__ */ jsxRuntime.jsxs(
11452
11589
  "div",
11453
11590
  {
11454
11591
  "data-react-fancy-kanban-column": "",
11592
+ "data-column-id": id,
11593
+ role: "group",
11594
+ "aria-label": title,
11455
11595
  onDrop: handleDrop,
11456
11596
  onDragOver: handleDragOver,
11457
11597
  onDragLeave: handleDragLeave,
@@ -11462,55 +11602,167 @@ function KanbanColumn({
11462
11602
  className
11463
11603
  ),
11464
11604
  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 })
11605
+ 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: [
11606
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: title }),
11607
+ /* @__PURE__ */ jsxRuntime.jsxs(
11608
+ "span",
11609
+ {
11610
+ className: cn(
11611
+ "rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
11612
+ 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"
11613
+ ),
11614
+ children: [
11615
+ cardCount,
11616
+ wipLimit !== void 0 ? `/${wipLimit}` : ""
11617
+ ]
11618
+ }
11619
+ )
11620
+ ] }),
11621
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: cardsRef, className: "relative flex flex-1 flex-col gap-2", children: [
11622
+ children,
11623
+ showIndicator && /* @__PURE__ */ jsxRuntime.jsx(
11624
+ "div",
11625
+ {
11626
+ "data-react-fancy-kanban-drop-indicator": "",
11627
+ style: { top: dropY },
11628
+ className: "pointer-events-none absolute left-0 right-0 h-0.5 -translate-y-1/2 rounded-full bg-blue-500/80 shadow-[0_0_0_3px_rgba(59,130,246,0.15)]"
11629
+ }
11630
+ )
11631
+ ] })
11467
11632
  ]
11468
11633
  }
11469
11634
  ) });
11470
11635
  }
11471
11636
  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();
11637
+ function KanbanColumnHandle({
11638
+ children,
11639
+ className
11640
+ }) {
11641
+ const { setDraggedColumn } = useKanban();
11475
11642
  const columnId = useKanbanColumn();
11476
- const handleDragStart = react.useCallback(() => {
11477
- setDraggedCard(id);
11478
- setDragSource(columnId);
11479
- }, [id, columnId, setDraggedCard, setDragSource]);
11643
+ const handleDragStart = react.useCallback(
11644
+ (e) => {
11645
+ e.dataTransfer.effectAllowed = "move";
11646
+ e.dataTransfer.setData("text/plain", columnId);
11647
+ e.stopPropagation();
11648
+ setDraggedColumn(columnId);
11649
+ },
11650
+ [columnId, setDraggedColumn]
11651
+ );
11480
11652
  const handleDragEnd = react.useCallback(() => {
11481
- setDraggedCard(null);
11482
- setDragSource(null);
11483
- }, [setDraggedCard, setDragSource]);
11653
+ setDraggedColumn(null);
11654
+ }, [setDraggedColumn]);
11484
11655
  return /* @__PURE__ */ jsxRuntime.jsx(
11485
11656
  "div",
11486
11657
  {
11487
- "data-react-fancy-kanban-card": "",
11488
11658
  draggable: true,
11489
11659
  onDragStart: handleDragStart,
11490
11660
  onDragEnd: handleDragEnd,
11661
+ "data-react-fancy-kanban-column-handle": "",
11491
11662
  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,
11663
+ "cursor-grab active:cursor-grabbing select-none",
11495
11664
  className
11496
11665
  ),
11497
11666
  children
11498
11667
  }
11499
11668
  );
11500
11669
  }
11501
- KanbanCard.displayName = "KanbanCard";
11502
- function KanbanRoot({ children, onCardMove, className }) {
11670
+ KanbanColumnHandle.displayName = "KanbanColumnHandle";
11671
+ function KanbanRoot({
11672
+ children,
11673
+ onCardMove,
11674
+ onColumnMove,
11675
+ className
11676
+ }) {
11503
11677
  const [draggedCard, setDraggedCard] = react.useState(null);
11504
11678
  const [dragSource, setDragSource] = react.useState(null);
11679
+ const [draggedColumn, setDraggedColumn] = react.useState(null);
11680
+ const orderRef = react.useRef([]);
11681
+ const [columnIds, setColumnIds] = react.useState([]);
11682
+ const registerColumn = react.useCallback((id) => {
11683
+ if (!orderRef.current.includes(id)) {
11684
+ orderRef.current = [...orderRef.current, id];
11685
+ setColumnIds(orderRef.current);
11686
+ }
11687
+ return () => {
11688
+ orderRef.current = orderRef.current.filter((x) => x !== id);
11689
+ setColumnIds(orderRef.current);
11690
+ };
11691
+ }, []);
11505
11692
  const ctx = react.useMemo(
11506
- () => ({ onCardMove, draggedCard, setDraggedCard, dragSource, setDragSource }),
11507
- [onCardMove, draggedCard, dragSource]
11693
+ () => ({
11694
+ draggedCard,
11695
+ setDraggedCard,
11696
+ dragSource,
11697
+ setDragSource,
11698
+ draggedColumn,
11699
+ setDraggedColumn,
11700
+ onCardMove,
11701
+ onColumnMove,
11702
+ columnIds,
11703
+ registerColumn
11704
+ }),
11705
+ [
11706
+ draggedCard,
11707
+ dragSource,
11708
+ draggedColumn,
11709
+ onCardMove,
11710
+ onColumnMove,
11711
+ columnIds,
11712
+ registerColumn
11713
+ ]
11714
+ );
11715
+ const containerRef = react.useRef(null);
11716
+ const handleDragOver = react.useCallback(
11717
+ (e) => {
11718
+ if (!draggedColumn) return;
11719
+ e.preventDefault();
11720
+ },
11721
+ [draggedColumn]
11508
11722
  );
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 }) });
11723
+ const handleDrop = react.useCallback(
11724
+ (e) => {
11725
+ if (!draggedColumn || !containerRef.current) return;
11726
+ e.preventDefault();
11727
+ const cols = containerRef.current.querySelectorAll(
11728
+ "[data-react-fancy-kanban-column]"
11729
+ );
11730
+ const x = e.clientX;
11731
+ let dropIdx = cols.length;
11732
+ for (let i = 0; i < cols.length; i++) {
11733
+ const rect = cols[i].getBoundingClientRect();
11734
+ if (x < rect.left + rect.width / 2) {
11735
+ dropIdx = i;
11736
+ break;
11737
+ }
11738
+ }
11739
+ const sourceIdx = columnIds.indexOf(draggedColumn);
11740
+ const finalIdx = sourceIdx >= 0 && dropIdx > sourceIdx ? dropIdx - 1 : dropIdx;
11741
+ if (finalIdx !== sourceIdx) {
11742
+ onColumnMove?.(draggedColumn, finalIdx);
11743
+ }
11744
+ setDraggedColumn(null);
11745
+ },
11746
+ [draggedColumn, columnIds, onColumnMove]
11747
+ );
11748
+ return /* @__PURE__ */ jsxRuntime.jsx(KanbanContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
11749
+ "div",
11750
+ {
11751
+ ref: containerRef,
11752
+ "data-react-fancy-kanban": "",
11753
+ onDragOver: handleDragOver,
11754
+ onDrop: handleDrop,
11755
+ role: "application",
11756
+ "aria-roledescription": "kanban board",
11757
+ className: cn("flex gap-4 overflow-x-auto p-4", className),
11758
+ children
11759
+ }
11760
+ ) });
11510
11761
  }
11511
11762
  var Kanban = Object.assign(KanbanRoot, {
11512
11763
  Column: KanbanColumn,
11513
- Card: KanbanCard
11764
+ Card: KanbanCard,
11765
+ ColumnHandle: KanbanColumnHandle
11514
11766
  });
11515
11767
  var CanvasContext = react.createContext(null);
11516
11768
  function useCanvas() {