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