@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.js CHANGED
@@ -11113,35 +11113,151 @@ var KanbanColumnContext = createContext("");
11113
11113
  function useKanbanColumn() {
11114
11114
  return useContext(KanbanColumnContext);
11115
11115
  }
11116
+ 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";
11117
+ function KanbanCard({ children, id, className, unstyled }) {
11118
+ const { setDraggedCard, setDragSource } = useKanban();
11119
+ const columnId = useKanbanColumn();
11120
+ const handleDragStart = useCallback(() => {
11121
+ setDraggedCard(id);
11122
+ setDragSource(columnId);
11123
+ }, [id, columnId, setDraggedCard, setDragSource]);
11124
+ const handleDragEnd = useCallback(() => {
11125
+ setDraggedCard(null);
11126
+ setDragSource(null);
11127
+ }, [setDraggedCard, setDragSource]);
11128
+ return /* @__PURE__ */ jsx(
11129
+ "div",
11130
+ {
11131
+ "data-react-fancy-kanban-card": "",
11132
+ draggable: true,
11133
+ onDragStart: handleDragStart,
11134
+ onDragEnd: handleDragEnd,
11135
+ className: cn(
11136
+ // Drag affordance — kept even when unstyled so users still see grab cursors.
11137
+ "cursor-grab active:cursor-grabbing",
11138
+ !unstyled && DEFAULT_CARD_CLASSES,
11139
+ className
11140
+ ),
11141
+ children
11142
+ }
11143
+ );
11144
+ }
11145
+ KanbanCard.displayName = "KanbanCard";
11116
11146
  var DEFAULT_COLUMN_CLASSES = "min-h-[200px] w-72 rounded-xl bg-zinc-50 p-3 dark:bg-zinc-800/50";
11147
+ function countCardChildren(children) {
11148
+ let n = 0;
11149
+ Children.forEach(children, (child) => {
11150
+ if (!isValidElement(child)) return;
11151
+ if (child.type === KanbanCard) {
11152
+ n += 1;
11153
+ return;
11154
+ }
11155
+ if (child.type === Fragment$1) {
11156
+ n += countCardChildren(child.props.children);
11157
+ }
11158
+ });
11159
+ return n;
11160
+ }
11117
11161
  function KanbanColumn({
11118
11162
  children,
11119
11163
  id,
11120
11164
  title,
11121
11165
  className,
11122
- unstyled
11166
+ unstyled,
11167
+ wipLimit,
11168
+ hideWhenEmpty
11123
11169
  }) {
11124
- const { onCardMove, draggedCard, dragSource } = useKanban();
11170
+ const { onCardMove, draggedCard, dragSource, registerColumn } = useKanban();
11125
11171
  const [dragOver, setDragOver] = useState(false);
11172
+ const [dropIndex, setDropIndex] = useState(null);
11173
+ const cardsRef = useRef(null);
11174
+ useEffect(() => registerColumn(id), [id, registerColumn]);
11175
+ const updateDropIndex = useCallback((clientY) => {
11176
+ const container = cardsRef.current;
11177
+ if (!container) {
11178
+ setDropIndex(null);
11179
+ return;
11180
+ }
11181
+ const cards = container.querySelectorAll(
11182
+ ":scope > [data-react-fancy-kanban-card]"
11183
+ );
11184
+ let idx = cards.length;
11185
+ for (let i = 0; i < cards.length; i++) {
11186
+ const rect = cards[i].getBoundingClientRect();
11187
+ if (clientY < rect.top + rect.height / 2) {
11188
+ idx = i;
11189
+ break;
11190
+ }
11191
+ }
11192
+ setDropIndex(idx);
11193
+ }, []);
11194
+ const handleDragOver = useCallback(
11195
+ (e) => {
11196
+ if (!draggedCard) return;
11197
+ e.preventDefault();
11198
+ e.stopPropagation();
11199
+ setDragOver(true);
11200
+ updateDropIndex(e.clientY);
11201
+ },
11202
+ [draggedCard, updateDropIndex]
11203
+ );
11204
+ const handleDragLeave = useCallback((e) => {
11205
+ if (e.currentTarget.contains(e.relatedTarget)) return;
11206
+ setDragOver(false);
11207
+ setDropIndex(null);
11208
+ }, []);
11126
11209
  const handleDrop = useCallback(
11127
11210
  (e) => {
11211
+ if (!draggedCard) return;
11128
11212
  e.preventDefault();
11129
- setDragOver(false);
11130
- if (draggedCard && dragSource && dragSource !== id) {
11131
- onCardMove?.(draggedCard, dragSource, id);
11213
+ e.stopPropagation();
11214
+ const target = dropIndex ?? 0;
11215
+ if (dragSource && draggedCard) {
11216
+ let finalIdx = target;
11217
+ if (dragSource === id) {
11218
+ const srcIdx = findCardIndex(children, draggedCard);
11219
+ if (srcIdx !== -1 && target > srcIdx) {
11220
+ finalIdx = target - 1;
11221
+ }
11222
+ if (srcIdx === finalIdx) {
11223
+ setDragOver(false);
11224
+ setDropIndex(null);
11225
+ return;
11226
+ }
11227
+ }
11228
+ onCardMove?.(draggedCard, dragSource, id, finalIdx);
11132
11229
  }
11230
+ setDragOver(false);
11231
+ setDropIndex(null);
11133
11232
  },
11134
- [draggedCard, dragSource, id, onCardMove]
11233
+ [draggedCard, dragSource, dropIndex, id, onCardMove, children]
11135
11234
  );
11136
- const handleDragOver = useCallback((e) => {
11137
- e.preventDefault();
11138
- setDragOver(true);
11139
- }, []);
11140
- const handleDragLeave = useCallback(() => setDragOver(false), []);
11235
+ const cardCount = countCardChildren(children);
11236
+ if (hideWhenEmpty && cardCount === 0 && !draggedCard) {
11237
+ return null;
11238
+ }
11239
+ let cardSeen = 0;
11240
+ const showIndicator = draggedCard !== null && dropIndex !== null && dragOver;
11241
+ const renderedChildren = Children.toArray(children).map((child, i) => {
11242
+ const isCard = isValidElement(child) && child.type === KanbanCard;
11243
+ const indicator = showIndicator && isCard && cardSeen === dropIndex ? /* @__PURE__ */ jsx(DropIndicator, {}, `drop-${i}`) : null;
11244
+ if (isCard) cardSeen += 1;
11245
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
11246
+ indicator,
11247
+ child
11248
+ ] }, i);
11249
+ });
11250
+ if (showIndicator && dropIndex === cardCount) {
11251
+ renderedChildren.push(/* @__PURE__ */ jsx(DropIndicator, {}, "drop-end"));
11252
+ }
11253
+ const overWip = wipLimit !== void 0 && cardCount > wipLimit;
11141
11254
  return /* @__PURE__ */ jsx(KanbanColumnContext.Provider, { value: id, children: /* @__PURE__ */ jsxs(
11142
11255
  "div",
11143
11256
  {
11144
11257
  "data-react-fancy-kanban-column": "",
11258
+ "data-column-id": id,
11259
+ role: "group",
11260
+ "aria-label": title,
11145
11261
  onDrop: handleDrop,
11146
11262
  onDragOver: handleDragOver,
11147
11263
  onDragLeave: handleDragLeave,
@@ -11152,55 +11268,181 @@ function KanbanColumn({
11152
11268
  className
11153
11269
  ),
11154
11270
  children: [
11155
- title && /* @__PURE__ */ jsx("h3", { className: "mb-3 text-sm font-semibold text-zinc-600 dark:text-zinc-400", children: title }),
11156
- /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-2", children })
11271
+ title && /* @__PURE__ */ jsxs("h3", { className: "mb-3 flex items-center gap-2 text-sm font-semibold text-zinc-600 dark:text-zinc-400", children: [
11272
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: title }),
11273
+ /* @__PURE__ */ jsxs(
11274
+ "span",
11275
+ {
11276
+ className: cn(
11277
+ "rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
11278
+ 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"
11279
+ ),
11280
+ children: [
11281
+ cardCount,
11282
+ wipLimit !== void 0 ? `/${wipLimit}` : ""
11283
+ ]
11284
+ }
11285
+ )
11286
+ ] }),
11287
+ /* @__PURE__ */ jsx("div", { ref: cardsRef, className: "flex flex-1 flex-col gap-2", children: renderedChildren })
11157
11288
  ]
11158
11289
  }
11159
11290
  ) });
11160
11291
  }
11161
11292
  KanbanColumn.displayName = "KanbanColumn";
11162
- 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";
11163
- function KanbanCard({ children, id, className, unstyled }) {
11164
- const { setDraggedCard, setDragSource } = useKanban();
11293
+ function DropIndicator() {
11294
+ return /* @__PURE__ */ jsx(
11295
+ "div",
11296
+ {
11297
+ "data-react-fancy-kanban-drop-indicator": "",
11298
+ className: "h-0.5 -my-1 rounded-full bg-blue-500/80"
11299
+ }
11300
+ );
11301
+ }
11302
+ function findCardIndex(children, cardId) {
11303
+ let idx = -1;
11304
+ let i = 0;
11305
+ Children.forEach(children, (child) => {
11306
+ if (idx !== -1) return;
11307
+ if (!isValidElement(child)) return;
11308
+ if (child.type === KanbanCard) {
11309
+ if (child.props.id === cardId) {
11310
+ idx = i;
11311
+ }
11312
+ i += 1;
11313
+ }
11314
+ });
11315
+ return idx;
11316
+ }
11317
+ function KanbanColumnHandle({
11318
+ children,
11319
+ className
11320
+ }) {
11321
+ const { setDraggedColumn } = useKanban();
11165
11322
  const columnId = useKanbanColumn();
11166
- const handleDragStart = useCallback(() => {
11167
- setDraggedCard(id);
11168
- setDragSource(columnId);
11169
- }, [id, columnId, setDraggedCard, setDragSource]);
11323
+ const handleDragStart = useCallback(
11324
+ (e) => {
11325
+ e.dataTransfer.effectAllowed = "move";
11326
+ e.dataTransfer.setData("text/plain", columnId);
11327
+ e.stopPropagation();
11328
+ setDraggedColumn(columnId);
11329
+ },
11330
+ [columnId, setDraggedColumn]
11331
+ );
11170
11332
  const handleDragEnd = useCallback(() => {
11171
- setDraggedCard(null);
11172
- setDragSource(null);
11173
- }, [setDraggedCard, setDragSource]);
11333
+ setDraggedColumn(null);
11334
+ }, [setDraggedColumn]);
11174
11335
  return /* @__PURE__ */ jsx(
11175
11336
  "div",
11176
11337
  {
11177
- "data-react-fancy-kanban-card": "",
11178
11338
  draggable: true,
11179
11339
  onDragStart: handleDragStart,
11180
11340
  onDragEnd: handleDragEnd,
11341
+ "data-react-fancy-kanban-column-handle": "",
11181
11342
  className: cn(
11182
- // Drag affordance — kept even when unstyled so users still see grab cursors.
11183
- "cursor-grab active:cursor-grabbing",
11184
- !unstyled && DEFAULT_CARD_CLASSES,
11343
+ "cursor-grab active:cursor-grabbing select-none",
11185
11344
  className
11186
11345
  ),
11187
11346
  children
11188
11347
  }
11189
11348
  );
11190
11349
  }
11191
- KanbanCard.displayName = "KanbanCard";
11192
- function KanbanRoot({ children, onCardMove, className }) {
11350
+ KanbanColumnHandle.displayName = "KanbanColumnHandle";
11351
+ function KanbanRoot({
11352
+ children,
11353
+ onCardMove,
11354
+ onColumnMove,
11355
+ className
11356
+ }) {
11193
11357
  const [draggedCard, setDraggedCard] = useState(null);
11194
11358
  const [dragSource, setDragSource] = useState(null);
11359
+ const [draggedColumn, setDraggedColumn] = useState(null);
11360
+ const orderRef = useRef([]);
11361
+ const [columnIds, setColumnIds] = useState([]);
11362
+ const registerColumn = useCallback((id) => {
11363
+ if (!orderRef.current.includes(id)) {
11364
+ orderRef.current = [...orderRef.current, id];
11365
+ setColumnIds(orderRef.current);
11366
+ }
11367
+ return () => {
11368
+ orderRef.current = orderRef.current.filter((x) => x !== id);
11369
+ setColumnIds(orderRef.current);
11370
+ };
11371
+ }, []);
11195
11372
  const ctx = useMemo(
11196
- () => ({ onCardMove, draggedCard, setDraggedCard, dragSource, setDragSource }),
11197
- [onCardMove, draggedCard, dragSource]
11373
+ () => ({
11374
+ draggedCard,
11375
+ setDraggedCard,
11376
+ dragSource,
11377
+ setDragSource,
11378
+ draggedColumn,
11379
+ setDraggedColumn,
11380
+ onCardMove,
11381
+ onColumnMove,
11382
+ columnIds,
11383
+ registerColumn
11384
+ }),
11385
+ [
11386
+ draggedCard,
11387
+ dragSource,
11388
+ draggedColumn,
11389
+ onCardMove,
11390
+ onColumnMove,
11391
+ columnIds,
11392
+ registerColumn
11393
+ ]
11394
+ );
11395
+ const containerRef = useRef(null);
11396
+ const handleDragOver = useCallback(
11397
+ (e) => {
11398
+ if (!draggedColumn) return;
11399
+ e.preventDefault();
11400
+ },
11401
+ [draggedColumn]
11198
11402
  );
11199
- return /* @__PURE__ */ jsx(KanbanContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx("div", { "data-react-fancy-kanban": "", className: cn("flex gap-4 overflow-x-auto p-4", className), children }) });
11403
+ const handleDrop = useCallback(
11404
+ (e) => {
11405
+ if (!draggedColumn || !containerRef.current) return;
11406
+ e.preventDefault();
11407
+ const cols = containerRef.current.querySelectorAll(
11408
+ "[data-react-fancy-kanban-column]"
11409
+ );
11410
+ const x = e.clientX;
11411
+ let dropIdx = cols.length;
11412
+ for (let i = 0; i < cols.length; i++) {
11413
+ const rect = cols[i].getBoundingClientRect();
11414
+ if (x < rect.left + rect.width / 2) {
11415
+ dropIdx = i;
11416
+ break;
11417
+ }
11418
+ }
11419
+ const sourceIdx = columnIds.indexOf(draggedColumn);
11420
+ const finalIdx = sourceIdx >= 0 && dropIdx > sourceIdx ? dropIdx - 1 : dropIdx;
11421
+ if (finalIdx !== sourceIdx) {
11422
+ onColumnMove?.(draggedColumn, finalIdx);
11423
+ }
11424
+ setDraggedColumn(null);
11425
+ },
11426
+ [draggedColumn, columnIds, onColumnMove]
11427
+ );
11428
+ return /* @__PURE__ */ jsx(KanbanContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
11429
+ "div",
11430
+ {
11431
+ ref: containerRef,
11432
+ "data-react-fancy-kanban": "",
11433
+ onDragOver: handleDragOver,
11434
+ onDrop: handleDrop,
11435
+ role: "application",
11436
+ "aria-roledescription": "kanban board",
11437
+ className: cn("flex gap-4 overflow-x-auto p-4", className),
11438
+ children
11439
+ }
11440
+ ) });
11200
11441
  }
11201
11442
  var Kanban = Object.assign(KanbanRoot, {
11202
11443
  Column: KanbanColumn,
11203
- Card: KanbanCard
11444
+ Card: KanbanCard,
11445
+ ColumnHandle: KanbanColumnHandle
11204
11446
  });
11205
11447
  var CanvasContext = createContext(null);
11206
11448
  function useCanvas() {