@lotics/ui 2.6.1 → 3.1.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.
Files changed (46) hide show
  1. package/package.json +2 -15
  2. package/src/react_native.d.ts +2 -2
  3. package/src/segmented_control.tsx +201 -0
  4. package/src/cell_date.tsx +0 -30
  5. package/src/cell_date_format.test.ts +0 -32
  6. package/src/cell_date_format.ts +0 -73
  7. package/src/cell_number.test.ts +0 -42
  8. package/src/cell_number.tsx +0 -25
  9. package/src/cell_number_format.ts +0 -42
  10. package/src/cell_select.tsx +0 -68
  11. package/src/cell_text.tsx +0 -45
  12. package/src/grid/data_grid.tsx +0 -2003
  13. package/src/grid/data_grid_columns.test.ts +0 -72
  14. package/src/grid/data_grid_columns.ts +0 -30
  15. package/src/grid/data_grid_context.ts +0 -119
  16. package/src/grid/dispatch_safely.ts +0 -39
  17. package/src/grid/engine.module.css +0 -114
  18. package/src/grid/engine.tsx +0 -1042
  19. package/src/grid/helpers.ts +0 -205
  20. package/src/grid/layout.test.ts +0 -515
  21. package/src/grid/layout.ts +0 -425
  22. package/src/grid/recycling.test.ts +0 -236
  23. package/src/grid/recycling.ts +0 -172
  24. package/src/grid/row_cell.module.css +0 -105
  25. package/src/grid/row_cell.tsx +0 -313
  26. package/src/grid/search_highlight.ts +0 -71
  27. package/src/grid/select_cell.tsx +0 -58
  28. package/src/grid/select_group_summary_cell.tsx +0 -76
  29. package/src/grid/select_header_cell.tsx +0 -32
  30. package/src/grid/skeleton_row.module.css +0 -34
  31. package/src/grid/skeleton_row.tsx +0 -20
  32. package/src/grid/use_grid_groups.ts +0 -311
  33. package/src/grid/use_scroll_to_cell.ts +0 -135
  34. package/src/grid/use_virtual_grid.ts +0 -383
  35. package/src/grid/visibility.test.ts +0 -208
  36. package/src/grid/visibility.ts +0 -77
  37. package/src/kanban/constants.ts +0 -18
  38. package/src/kanban/default_renderers.tsx +0 -160
  39. package/src/kanban/drag_preview.tsx +0 -157
  40. package/src/kanban/index.ts +0 -13
  41. package/src/kanban/insert_card_zone.tsx +0 -135
  42. package/src/kanban/kanban_board.tsx +0 -635
  43. package/src/kanban/kanban_card.tsx +0 -321
  44. package/src/kanban/kanban_column.tsx +0 -499
  45. package/src/kanban/placeholders.tsx +0 -54
  46. package/src/kanban/types.ts +0 -116
@@ -1,635 +0,0 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { FlatList, ScrollView, StyleSheet, View } from "react-native";
3
- import { KanbanColumn } from "./kanban_column";
4
- import { DragPreview } from "./drag_preview";
5
- import { ColumnPlaceholder } from "./placeholders";
6
- import {
7
- CardMoveResult,
8
- ColumnMoveResult,
9
- DragInfo,
10
- DragPreviewInfo,
11
- DropTarget,
12
- KanbanColumn as KanbanColumnType,
13
- KanbanRenderAddCardPlaceholderProps,
14
- KanbanRenderInsertCardButtonProps,
15
- KanbanRenderCardProps,
16
- KanbanRenderCollapsedColumnProps,
17
- KanbanRenderColumnHeaderProps,
18
- } from "./types";
19
- import {
20
- AUTO_SCROLL_SPEED,
21
- AUTO_SCROLL_THRESHOLD,
22
- COLUMN_CONTENT_PADDING,
23
- DEFAULT_COLUMN_GAP,
24
- DEFAULT_COLUMN_WIDTH,
25
- DEFAULT_ITEM_GAP,
26
- MINIMIZED_COLUMN_WIDTH,
27
- } from "./constants";
28
- import {
29
- defaultRenderAddCardPlaceholder,
30
- defaultRenderCollapsedColumn,
31
- defaultRenderColumnHeader,
32
- } from "./default_renderers";
33
-
34
- export interface KanbanBoardProps<T> {
35
- columns: KanbanColumnType<T>[];
36
- renderCard: (props: KanbanRenderCardProps<T>) => React.ReactNode;
37
- renderColumnHeader?: (props: KanbanRenderColumnHeaderProps) => React.ReactNode;
38
- renderAddCardPlaceholder?: (props: KanbanRenderAddCardPlaceholderProps) => React.ReactNode;
39
- /** Custom renderer for the insert card button shown between cards on hover */
40
- renderInsertCardButton?: (props: KanbanRenderInsertCardButtonProps) => React.ReactNode;
41
- renderCollapsedColumn?: (props: KanbanRenderCollapsedColumnProps) => React.ReactNode;
42
- onCardMove?: (result: CardMoveResult<T>) => void;
43
- onColumnMove?: (result: ColumnMoveResult) => void;
44
- onCardPress?: (cardId: string, item: T, columnKey: string) => void;
45
- onAddCard?: (columnKey: string) => void;
46
- /** Called when user clicks the insert button between cards */
47
- onAddCardAtIndex?: (columnKey: string, index: number) => void;
48
- columnWidth?: number;
49
- columnGap?: number;
50
- /** Height of each card. Number for fixed height, function for variable heights */
51
- itemHeight: number | ((item: T, index: number) => number);
52
- /** Gap between items in a column */
53
- itemGap?: number;
54
- /** When true, cards can only be dropped into columns (highlights entire column), not between specific cards */
55
- columnOnlyCardDrop?: boolean;
56
- /**
57
- * Whether cards can be dragged. Defaults to whether `onCardMove` is provided —
58
- * a card with no move handler does not initiate a drag (it can still be
59
- * pressed via `onCardPress`). Pass `false` for a read-only / action-driven
60
- * board.
61
- */
62
- cardDraggable?: boolean;
63
- /** Whether columns can be reordered. Defaults to whether `onColumnMove` is provided. */
64
- columnDraggable?: boolean;
65
- }
66
-
67
- /** Column registration data */
68
- export interface ColumnRegistration {
69
- element: HTMLElement;
70
- headerHeight: number;
71
- /** FlatList ref for scrolling - null when column is minimized */
72
- listRef: FlatList | null;
73
- }
74
-
75
- export function KanbanBoard<T>({
76
- columns,
77
- renderCard,
78
- renderColumnHeader = defaultRenderColumnHeader,
79
- renderAddCardPlaceholder = defaultRenderAddCardPlaceholder,
80
- renderInsertCardButton,
81
- renderCollapsedColumn = defaultRenderCollapsedColumn,
82
- onCardMove,
83
- onColumnMove,
84
- onCardPress,
85
- onAddCard,
86
- onAddCardAtIndex,
87
- columnWidth = DEFAULT_COLUMN_WIDTH,
88
- columnGap = DEFAULT_COLUMN_GAP,
89
- itemHeight,
90
- itemGap = DEFAULT_ITEM_GAP,
91
- columnOnlyCardDrop = false,
92
- cardDraggable,
93
- columnDraggable,
94
- }: KanbanBoardProps<T>) {
95
- // A card/column is draggable only when there is a handler to receive the
96
- // move — an explicit prop overrides. This makes a read-only or action-driven
97
- // board (no move handler) non-draggable without extra wiring.
98
- const cardsDraggable = cardDraggable ?? onCardMove != null;
99
- const columnsDraggable = columnDraggable ?? onColumnMove != null;
100
-
101
- // Helper to get height for an item
102
- const getHeight = useCallback(
103
- (item: T, index: number): number => {
104
- return typeof itemHeight === "function" ? itemHeight(item, index) : itemHeight;
105
- },
106
- [itemHeight],
107
- );
108
-
109
- // === State ===
110
- const [dragInfo, setDragInfo] = useState<DragInfo | null>(null);
111
- const [dropTarget, setDropTarget] = useState<DropTarget | null>(null);
112
- const [dragPreview, setDragPreview] = useState<DragPreviewInfo | null>(null);
113
- const [minimizedColumns, setMinimizedColumns] = useState<Set<string>>(new Set());
114
-
115
- const toggleMinimized = useCallback((columnKey: string) => {
116
- setMinimizedColumns((prev) => {
117
- const next = new Set(prev);
118
- if (next.has(columnKey)) {
119
- next.delete(columnKey);
120
- } else {
121
- next.add(columnKey);
122
- }
123
- return next;
124
- });
125
- }, []);
126
-
127
- // === Refs ===
128
- const dragPositionRef = useRef<{ x: number; y: number } | null>(null);
129
- const dragPreviewUpdateRef = useRef<((x: number, y: number) => void) | null>(null);
130
-
131
- // Column data - consolidated into single map
132
- const columnDataRef = useRef<Map<string, ColumnRegistration & { scrollY: number }>>(new Map());
133
-
134
- // Track which column mouse is currently over during drag
135
- const hoveredColumnRef = useRef<string | null>(null);
136
-
137
- // Refs to avoid stale closures
138
- const dragInfoRef = useRef<DragInfo | null>(null);
139
- const dropTargetRef = useRef<DropTarget | null>(null);
140
- const onCardMoveRef = useRef(onCardMove);
141
- const onColumnMoveRef = useRef(onColumnMove);
142
- const columnsRef = useRef(columns);
143
- const getHeightRef = useRef(getHeight);
144
- const itemGapRef = useRef(itemGap);
145
-
146
- // Keep refs in sync
147
- onCardMoveRef.current = onCardMove;
148
- onColumnMoveRef.current = onColumnMove;
149
- columnsRef.current = columns;
150
- getHeightRef.current = getHeight;
151
- itemGapRef.current = itemGap;
152
-
153
- const isDragging = dragInfo !== null;
154
-
155
- // === Drop Target Detection ===
156
- const findDropTarget = useCallback(
157
- (clientX: number, clientY: number, dragType: "card" | "column"): DropTarget | null => {
158
- if (dragType === "column") {
159
- const draggedColumnKey = dragInfoRef.current?.id;
160
- const sourceIndex = dragInfoRef.current?.sourceIndex ?? 0;
161
-
162
- // The dragged column is not rendered (hidden), so we work with visible columns only
163
- const visibleColumns = columnsRef.current.filter((c) => c.key !== draggedColumnKey);
164
-
165
- // Sort by visual position (left edge) to handle any layout differences
166
- const sortedColumns = [...visibleColumns].sort((a, b) => {
167
- const dataA = columnDataRef.current.get(a.key);
168
- const dataB = columnDataRef.current.get(b.key);
169
- if (!dataA || !dataB) return 0;
170
- return (
171
- dataA.element.getBoundingClientRect().left - dataB.element.getBoundingClientRect().left
172
- );
173
- });
174
-
175
- // Find drop position among visible columns using midpoint detection
176
- let visibleTargetIndex = 0;
177
- for (let i = 0; i < sortedColumns.length; i++) {
178
- const data = columnDataRef.current.get(sortedColumns[i].key);
179
- if (!data) continue;
180
- const rect = data.element.getBoundingClientRect();
181
- const midpoint = rect.left + rect.width / 2;
182
- if (clientX < midpoint) {
183
- visibleTargetIndex = i;
184
- break;
185
- }
186
- visibleTargetIndex = i + 1;
187
- }
188
-
189
- // Map visible index back to full array index
190
- // Example: columns [A, B, C, D], dragging B (sourceIndex=1)
191
- // Visible: [A, C, D] at indices [0, 1, 2]
192
- // If visibleTargetIndex=2 (between C and D), actual index should be 3
193
- // because B was at index 1, so indices after it shifted down by 1
194
- const targetIndex =
195
- visibleTargetIndex > sourceIndex ? visibleTargetIndex + 1 : visibleTargetIndex;
196
-
197
- return {
198
- type: "column",
199
- columnKey:
200
- columnsRef.current[Math.min(targetIndex, columnsRef.current.length - 1)]?.key ?? "",
201
- index: targetIndex,
202
- };
203
- }
204
-
205
- // For card drag, detect column from pointer position
206
- // (more reliable than relying on pointerenter events)
207
- let columnKey: string | null = null;
208
- let columnData:
209
- | (typeof columnDataRef.current extends Map<string, infer V> ? V : never)
210
- | null = null;
211
- let rect: DOMRect | null = null;
212
-
213
- // Find which column contains the pointer
214
- for (const [key, data] of columnDataRef.current) {
215
- const columnRect = data.element.getBoundingClientRect();
216
- if (
217
- clientX >= columnRect.left &&
218
- clientX <= columnRect.right &&
219
- clientY >= columnRect.top &&
220
- clientY <= columnRect.bottom
221
- ) {
222
- columnKey = key;
223
- columnData = data;
224
- rect = columnRect;
225
- break;
226
- }
227
- }
228
-
229
- if (!columnKey || !columnData || !rect) return null;
230
-
231
- // Update hoveredColumnRef for auto-scroll
232
- hoveredColumnRef.current = columnKey;
233
-
234
- // Column-only card drop mode: just return the column, no specific index
235
- if (columnOnlyCardDrop) {
236
- return { type: "card", columnKey, index: -1 };
237
- }
238
-
239
- // Y position relative to item layout (subtract content padding)
240
- const relativeY =
241
- clientY - rect.top - columnData.headerHeight + columnData.scrollY - COLUMN_CONTENT_PADDING;
242
-
243
- // Find card index based on cumulative heights
244
- const column = columnsRef.current.find((c) => c.key === columnKey);
245
- const items = column?.items ?? [];
246
-
247
- // Get dragging card info
248
- const draggedCardSourceColumn = dragInfoRef.current?.sourceColumnKey;
249
- const draggedCardSourceIndex = dragInfoRef.current?.sourceIndex ?? -1;
250
- const isInSourceColumn = draggedCardSourceColumn === columnKey;
251
-
252
- let offset = 0;
253
- for (let i = 0; i < items.length; i++) {
254
- const height = getHeightRef.current(items[i].data, i);
255
- const midpoint = offset + height / 2;
256
- if (relativeY < midpoint) {
257
- // When in source column, dropping at sourceIndex or sourceIndex+1
258
- // both mean "keep card in same position", so normalize to sourceIndex
259
- if (
260
- isInSourceColumn &&
261
- (i === draggedCardSourceIndex || i === draggedCardSourceIndex + 1)
262
- ) {
263
- return { type: "card", columnKey, index: draggedCardSourceIndex };
264
- }
265
- return { type: "card", columnKey, index: i };
266
- }
267
- offset += height + itemGapRef.current;
268
- }
269
-
270
- // Dropping at the end - same normalization for source column
271
- if (isInSourceColumn && items.length === draggedCardSourceIndex + 1) {
272
- return { type: "card", columnKey, index: draggedCardSourceIndex };
273
- }
274
- return { type: "card", columnKey, index: items.length };
275
- },
276
- [columnOnlyCardDrop],
277
- );
278
-
279
- // === Auto-scroll within columns ===
280
- const performAutoScroll = useCallback((clientX: number, clientY: number) => {
281
- const columnKey = hoveredColumnRef.current;
282
- if (!columnKey) return;
283
-
284
- const columnData = columnDataRef.current.get(columnKey);
285
- if (!columnData || !columnData.listRef) return; // Skip if minimized (no listRef)
286
-
287
- const rect = columnData.element.getBoundingClientRect();
288
- const relativeY = clientY - rect.top - columnData.headerHeight;
289
- const contentHeight = rect.height - columnData.headerHeight;
290
-
291
- if (relativeY < AUTO_SCROLL_THRESHOLD) {
292
- columnData.listRef.scrollToOffset({
293
- offset: Math.max(0, columnData.scrollY - AUTO_SCROLL_SPEED),
294
- animated: false,
295
- });
296
- } else if (relativeY > contentHeight - AUTO_SCROLL_THRESHOLD) {
297
- columnData.listRef.scrollToOffset({
298
- offset: columnData.scrollY + AUTO_SCROLL_SPEED,
299
- animated: false,
300
- });
301
- }
302
- }, []);
303
-
304
- // === Drag Position Updates ===
305
- const updateDragPosition = useCallback(
306
- (clientX: number, clientY: number) => {
307
- dragPositionRef.current = { x: clientX, y: clientY };
308
- dragPreviewUpdateRef.current?.(clientX, clientY);
309
-
310
- const info = dragInfoRef.current;
311
- if (!info) return;
312
-
313
- const target = findDropTarget(clientX, clientY, info.type);
314
-
315
- // Only update state if target changed
316
- const prev = dropTargetRef.current;
317
- if (
318
- target?.type !== prev?.type ||
319
- target?.columnKey !== prev?.columnKey ||
320
- target?.index !== prev?.index
321
- ) {
322
- dropTargetRef.current = target;
323
- setDropTarget(target);
324
- }
325
-
326
- if (info.type === "card") {
327
- performAutoScroll(clientX, clientY);
328
- }
329
- },
330
- [findDropTarget, performAutoScroll],
331
- );
332
-
333
- // === End Drag ===
334
- const endDrag = useCallback(() => {
335
- const info = dragInfoRef.current;
336
- const target = dropTargetRef.current;
337
-
338
- if (info && target) {
339
- if (info.type === "card" && onCardMoveRef.current) {
340
- const card = columnsRef.current
341
- .find((col) => col.key === info.sourceColumnKey)
342
- ?.items.find((item) => item.id === info.id);
343
-
344
- if (card) {
345
- // Adjust index when moving within same column and moving forward
346
- // Because removing the card shifts all subsequent indices down by 1
347
- let targetIndex = target.index;
348
- if (info.sourceColumnKey === target.columnKey && info.sourceIndex < target.index) {
349
- targetIndex--;
350
- }
351
-
352
- onCardMoveRef.current({
353
- cardId: info.id,
354
- cardData: card.data,
355
- sourceColumnKey: info.sourceColumnKey,
356
- sourceIndex: info.sourceIndex,
357
- targetColumnKey: target.columnKey,
358
- targetIndex,
359
- });
360
- }
361
- } else if (info.type === "column" && onColumnMoveRef.current) {
362
- // Adjust index when moving forward (same reason as cards)
363
- // findDropTarget returns "placeholder position", we need "final position after removal"
364
- let targetIndex = target.index;
365
- if (info.sourceIndex < target.index) {
366
- targetIndex--;
367
- }
368
-
369
- onColumnMoveRef.current({
370
- columnKey: info.id,
371
- sourceIndex: info.sourceIndex,
372
- targetIndex,
373
- });
374
- }
375
- }
376
-
377
- // Reset all state
378
- setDragInfo(null);
379
- setDropTarget(null);
380
- setDragPreview(null);
381
- dragInfoRef.current = null;
382
- dropTargetRef.current = null;
383
- dragPositionRef.current = null;
384
- hoveredColumnRef.current = null;
385
- }, []);
386
-
387
- // === Global Pointer Listeners ===
388
- useEffect(() => {
389
- if (!isDragging) return;
390
-
391
- const handlePointerMove = (e: PointerEvent) => {
392
- e.preventDefault();
393
- updateDragPosition(e.clientX, e.clientY);
394
- };
395
-
396
- const handlePointerUp = (e: PointerEvent) => {
397
- e.preventDefault();
398
- endDrag();
399
- };
400
-
401
- const preventDefault = (e: Event) => e.preventDefault();
402
-
403
- // Use pointer events only - they handle both mouse and touch
404
- window.addEventListener("pointermove", handlePointerMove);
405
- window.addEventListener("pointerup", handlePointerUp);
406
- window.addEventListener("selectstart", preventDefault);
407
-
408
- return () => {
409
- window.removeEventListener("pointermove", handlePointerMove);
410
- window.removeEventListener("pointerup", handlePointerUp);
411
- window.removeEventListener("selectstart", preventDefault);
412
- };
413
- }, [isDragging, updateDragPosition, endDrag]);
414
-
415
- // === Start Drag Callbacks ===
416
- const startCardDrag = useCallback(
417
- (
418
- cardId: string,
419
- columnKey: string,
420
- index: number,
421
- startX: number,
422
- startY: number,
423
- rect: { x: number; y: number; width: number; height: number },
424
- ) => {
425
- const info: DragInfo = {
426
- type: "card",
427
- id: cardId,
428
- sourceColumnKey: columnKey,
429
- sourceIndex: index,
430
- };
431
- setDragInfo(info);
432
- dragInfoRef.current = info;
433
- dragPositionRef.current = { x: startX, y: startY };
434
- hoveredColumnRef.current = columnKey;
435
- setDragPreview({
436
- width: rect.width,
437
- height: rect.height,
438
- offsetX: startX - rect.x,
439
- offsetY: startY - rect.y,
440
- });
441
- // In column-only mode, use index -1 to show column highlight instead of position indicator
442
- const initialIndex = columnOnlyCardDrop ? -1 : index;
443
- setDropTarget({ type: "card", columnKey, index: initialIndex });
444
- dropTargetRef.current = { type: "card", columnKey, index: initialIndex };
445
- },
446
- [columnOnlyCardDrop],
447
- );
448
-
449
- const startColumnDrag = useCallback(
450
- (
451
- columnKey: string,
452
- index: number,
453
- startX: number,
454
- startY: number,
455
- rect: { x: number; y: number; width: number; height: number },
456
- ) => {
457
- const info: DragInfo = {
458
- type: "column",
459
- id: columnKey,
460
- sourceColumnKey: columnKey,
461
- sourceIndex: index,
462
- };
463
- setDragInfo(info);
464
- dragInfoRef.current = info;
465
- dragPositionRef.current = { x: startX, y: startY };
466
- setDragPreview({
467
- width: rect.width,
468
- height: rect.height,
469
- offsetX: startX - rect.x,
470
- offsetY: startY - rect.y,
471
- });
472
- setDropTarget({ type: "column", columnKey, index });
473
- dropTargetRef.current = { type: "column", columnKey, index };
474
- },
475
- [],
476
- );
477
-
478
- // === Column Registration (consolidated) ===
479
- const registerColumn = useCallback((columnKey: string, data: ColumnRegistration | null) => {
480
- if (data) {
481
- const existing = columnDataRef.current.get(columnKey);
482
- columnDataRef.current.set(columnKey, {
483
- ...data,
484
- scrollY: existing?.scrollY ?? 0,
485
- });
486
- } else {
487
- columnDataRef.current.delete(columnKey);
488
- }
489
- }, []);
490
-
491
- const updateColumnScroll = useCallback((columnKey: string, scrollY: number) => {
492
- const data = columnDataRef.current.get(columnKey);
493
- if (data) {
494
- data.scrollY = scrollY;
495
- }
496
- }, []);
497
-
498
- // Kept for potential future use, but column detection now happens in findDropTarget
499
- const handleColumnPointerEnter = useCallback((columnKey: string) => {
500
- if (dragInfoRef.current?.type === "card") {
501
- hoveredColumnRef.current = columnKey;
502
- }
503
- }, []);
504
-
505
- // STABLE: Callback for DragPreview position updates
506
- const handlePositionUpdate = useCallback((cb: (x: number, y: number) => void) => {
507
- dragPreviewUpdateRef.current = cb;
508
- }, []);
509
-
510
- // === Derived State ===
511
- const dragType = dragInfo?.type ?? null;
512
- const dragId = dragInfo?.id ?? null;
513
- const sourceColumnKey = dragInfo?.sourceColumnKey ?? null;
514
- const dropTargetType = dropTarget?.type ?? null;
515
- const dropTargetColumnKey = dropTarget?.columnKey ?? null;
516
- const dropTargetIndex = dropTarget?.index ?? null;
517
-
518
- // Width of the column being dragged (for placeholder sizing)
519
- const draggedColumnWidth =
520
- dragType === "column" && dragId
521
- ? minimizedColumns.has(dragId)
522
- ? MINIMIZED_COLUMN_WIDTH
523
- : columnWidth
524
- : columnWidth;
525
-
526
- // Height of the column being dragged (for placeholder sizing)
527
- const draggedColumnHeight = dragType === "column" && dragPreview ? dragPreview.height : undefined;
528
-
529
- // Height of the card being dragged (for placeholder sizing)
530
- const draggedCardHeight = dragType === "card" && dragPreview ? dragPreview.height : null;
531
-
532
- // Stable scroll content style
533
- const scrollContentStyle = useMemo(() => [styles.scrollContent, { gap: columnGap }], [columnGap]);
534
-
535
- return (
536
- <View style={styles.container}>
537
- <ScrollView
538
- horizontal
539
- showsHorizontalScrollIndicator={false}
540
- style={styles.scrollView}
541
- contentContainerStyle={scrollContentStyle}
542
- scrollEventThrottle={16}
543
- >
544
- {columns.map((column, columnIndex) => {
545
- const isColumnDragging = dragType === "column" && dragId === column.key;
546
- const isColumnDropTarget =
547
- dragType === "column" && dropTargetType === "column" && dropTargetIndex === columnIndex;
548
-
549
- // Only source column needs cardDragId (to mark dragged card)
550
- const isSourceColumn = sourceColumnKey === column.key;
551
- // Only target column needs drop index (to show placeholder)
552
- const isTargetColumn =
553
- dragType === "card" && dropTargetType === "card" && dropTargetColumnKey === column.key;
554
-
555
- const isMinimized = minimizedColumns.has(column.key);
556
- const effectiveWidth = isMinimized ? MINIMIZED_COLUMN_WIDTH : columnWidth;
557
-
558
- // In column-only mode, show full column highlight instead of position indicators
559
- const showCardDropHighlight = columnOnlyCardDrop && isTargetColumn;
560
-
561
- return (
562
- <KanbanColumn
563
- key={column.key}
564
- columnKey={column.key}
565
- title={column.title}
566
- index={columnIndex}
567
- items={column.items}
568
- renderCard={renderCard}
569
- renderColumnHeader={renderColumnHeader}
570
- renderAddCardPlaceholder={renderAddCardPlaceholder}
571
- renderInsertCardButton={renderInsertCardButton}
572
- renderCollapsedColumn={renderCollapsedColumn}
573
- isDragging={isColumnDragging}
574
- isDropTarget={isColumnDropTarget}
575
- isDragInProgress={isDragging}
576
- // Only pass to source column - other columns don't need to re-render
577
- cardDragId={isSourceColumn ? dragId : null}
578
- // Only pass to target column - other columns don't need to re-render
579
- // Don't pass index in column-only mode (no position indicators)
580
- cardDropTargetIndex={isTargetColumn && !columnOnlyCardDrop ? dropTargetIndex : null}
581
- draggedCardHeight={isTargetColumn ? draggedCardHeight : null}
582
- showCardDropHighlight={showCardDropHighlight}
583
- columnWidth={effectiveWidth}
584
- placeholderWidth={isColumnDropTarget ? draggedColumnWidth : effectiveWidth}
585
- placeholderHeight={isColumnDropTarget ? draggedColumnHeight : undefined}
586
- itemHeight={itemHeight}
587
- itemGap={itemGap}
588
- onCardPress={onCardPress}
589
- onAddCard={onAddCard}
590
- onAddCardAtIndex={onAddCardAtIndex}
591
- cardDraggable={cardsDraggable}
592
- columnDraggable={columnsDraggable}
593
- startCardDrag={startCardDrag}
594
- startColumnDrag={startColumnDrag}
595
- registerColumn={registerColumn}
596
- updateColumnScroll={updateColumnScroll}
597
- onPointerEnter={handleColumnPointerEnter}
598
- isMinimized={isMinimized}
599
- onMinimizeToggle={toggleMinimized}
600
- />
601
- );
602
- })}
603
- {dragType === "column" &&
604
- dropTargetType === "column" &&
605
- dropTargetIndex === columns.length && (
606
- <ColumnPlaceholder width={draggedColumnWidth} height={draggedColumnHeight} />
607
- )}
608
- </ScrollView>
609
- <DragPreview
610
- columns={columns}
611
- dragInfo={dragInfo}
612
- dragPreview={dragPreview}
613
- renderCard={renderCard}
614
- renderColumnHeader={renderColumnHeader}
615
- onPositionUpdate={handlePositionUpdate}
616
- initialPosition={dragPositionRef.current}
617
- />
618
- </View>
619
- );
620
- }
621
-
622
- const styles = StyleSheet.create({
623
- container: {
624
- flex: 1,
625
- flexBasis: 0,
626
- },
627
- scrollView: {
628
- flex: 1,
629
- },
630
- scrollContent: {
631
- flexDirection: "row",
632
- alignItems: "flex-start",
633
- height: "100%",
634
- },
635
- });