@lotics/ui 2.6.1 → 3.0.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/package.json +1 -15
- package/src/react_native.d.ts +2 -2
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Platform, View } from "react-native";
|
|
3
|
-
import { KanbanRenderCardProps, KanbanRenderInsertCardButtonProps } from "./types";
|
|
4
|
-
import { CardPlaceholder } from "./placeholders";
|
|
5
|
-
import { DRAG_THRESHOLD, TOUCH_DRAG_DELAY, TOUCH_TOLERANCE } from "./constants";
|
|
6
|
-
import { InsertCardZone } from "./insert_card_zone";
|
|
7
|
-
|
|
8
|
-
interface KanbanCardProps<T> {
|
|
9
|
-
id: string;
|
|
10
|
-
columnKey: string;
|
|
11
|
-
index: number;
|
|
12
|
-
item: T;
|
|
13
|
-
renderCard: (props: KanbanRenderCardProps<T>) => React.ReactNode;
|
|
14
|
-
isDragging: boolean;
|
|
15
|
-
isDropTarget: boolean;
|
|
16
|
-
/** True when any card is being dragged (disables insert zones) */
|
|
17
|
-
isDragInProgress: boolean;
|
|
18
|
-
/** When false, this card does not initiate a drag (press still works) */
|
|
19
|
-
cardDraggable: boolean;
|
|
20
|
-
onCardPress?: (cardId: string, item: T, columnKey: string) => void;
|
|
21
|
-
onAddCardAtIndex?: (columnKey: string, index: number) => void;
|
|
22
|
-
renderInsertCardButton?: (props: KanbanRenderInsertCardButtonProps) => React.ReactNode;
|
|
23
|
-
startCardDrag: (
|
|
24
|
-
cardId: string,
|
|
25
|
-
columnKey: string,
|
|
26
|
-
index: number,
|
|
27
|
-
startX: number,
|
|
28
|
-
startY: number,
|
|
29
|
-
rect: { x: number; y: number; width: number; height: number },
|
|
30
|
-
) => void;
|
|
31
|
-
/** Height of this card (used when this card is dragging and also drop target) */
|
|
32
|
-
placeholderHeight: number;
|
|
33
|
-
/** Height of the card being dragged (used for drop target placeholder) */
|
|
34
|
-
draggedCardHeight: number | null;
|
|
35
|
-
/** Gap between items for consistent spacing */
|
|
36
|
-
itemGap: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type PointerType = "mouse" | "touch" | "pen";
|
|
40
|
-
|
|
41
|
-
function KanbanCardInner<T>({
|
|
42
|
-
id,
|
|
43
|
-
columnKey,
|
|
44
|
-
index,
|
|
45
|
-
item,
|
|
46
|
-
renderCard,
|
|
47
|
-
isDragging,
|
|
48
|
-
isDropTarget,
|
|
49
|
-
isDragInProgress,
|
|
50
|
-
cardDraggable,
|
|
51
|
-
onCardPress,
|
|
52
|
-
onAddCardAtIndex,
|
|
53
|
-
renderInsertCardButton,
|
|
54
|
-
startCardDrag,
|
|
55
|
-
placeholderHeight,
|
|
56
|
-
draggedCardHeight,
|
|
57
|
-
itemGap,
|
|
58
|
-
}: KanbanCardProps<T>) {
|
|
59
|
-
const containerRef = useRef<View>(null);
|
|
60
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
61
|
-
const [isPressing, setIsPressing] = useState(false);
|
|
62
|
-
// Visual feedback when touch delay is active (preparing to drag)
|
|
63
|
-
const [isDragPending, setIsDragPending] = useState(false);
|
|
64
|
-
|
|
65
|
-
const pressStartRef = useRef<{ x: number; y: number } | null>(null);
|
|
66
|
-
const hasDraggedRef = useRef(false);
|
|
67
|
-
const wasCancelledRef = useRef(false);
|
|
68
|
-
const elementRectRef = useRef<{
|
|
69
|
-
x: number;
|
|
70
|
-
y: number;
|
|
71
|
-
width: number;
|
|
72
|
-
height: number;
|
|
73
|
-
} | null>(null);
|
|
74
|
-
const pointerTypeRef = useRef<PointerType>("mouse");
|
|
75
|
-
const delayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
76
|
-
// True when delay has elapsed and drag can start (always true for mouse)
|
|
77
|
-
const canDragRef = useRef(false);
|
|
78
|
-
|
|
79
|
-
// Clear hover when this card starts dragging or any drag is in progress
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (isDragging || isDragInProgress) setIsHovered(false);
|
|
82
|
-
}, [isDragging, isDragInProgress]);
|
|
83
|
-
|
|
84
|
-
// Reset internal state when drag ends (drag is handled at board level)
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (!isDragging && hasDraggedRef.current) {
|
|
87
|
-
// Drag just ended - reset all internal state
|
|
88
|
-
hasDraggedRef.current = false;
|
|
89
|
-
wasCancelledRef.current = false;
|
|
90
|
-
canDragRef.current = false;
|
|
91
|
-
setIsPressing(false);
|
|
92
|
-
setIsDragPending(false);
|
|
93
|
-
pressStartRef.current = null;
|
|
94
|
-
elementRectRef.current = null;
|
|
95
|
-
if (delayTimerRef.current) {
|
|
96
|
-
clearTimeout(delayTimerRef.current);
|
|
97
|
-
delayTimerRef.current = null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}, [isDragging]);
|
|
101
|
-
|
|
102
|
-
// Cleanup delay timer on unmount
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
return () => {
|
|
105
|
-
if (delayTimerRef.current) {
|
|
106
|
-
clearTimeout(delayTimerRef.current);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}, []);
|
|
110
|
-
|
|
111
|
-
// Handle pointer down - attached via useEffect on web
|
|
112
|
-
const handlePointerDown = useCallback(
|
|
113
|
-
(e: PointerEvent) => {
|
|
114
|
-
// Determine pointer type
|
|
115
|
-
const pointerType = e.pointerType as PointerType;
|
|
116
|
-
pointerTypeRef.current = pointerType;
|
|
117
|
-
|
|
118
|
-
pressStartRef.current = { x: e.clientX, y: e.clientY };
|
|
119
|
-
hasDraggedRef.current = false;
|
|
120
|
-
wasCancelledRef.current = false;
|
|
121
|
-
|
|
122
|
-
// For touch/pen: require a delay before drag can start
|
|
123
|
-
// For mouse: drag can start immediately
|
|
124
|
-
const isTouch = pointerType === "touch" || pointerType === "pen";
|
|
125
|
-
|
|
126
|
-
if (!cardDraggable) {
|
|
127
|
-
// Card cannot be dragged — only a press is detected. Never enable drag.
|
|
128
|
-
canDragRef.current = false;
|
|
129
|
-
} else if (isTouch) {
|
|
130
|
-
canDragRef.current = false;
|
|
131
|
-
setIsDragPending(true);
|
|
132
|
-
|
|
133
|
-
delayTimerRef.current = setTimeout(() => {
|
|
134
|
-
canDragRef.current = true;
|
|
135
|
-
delayTimerRef.current = null;
|
|
136
|
-
}, TOUCH_DRAG_DELAY);
|
|
137
|
-
} else {
|
|
138
|
-
canDragRef.current = true;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Measure element position
|
|
142
|
-
containerRef.current?.measureInWindow((x, y, width, height) => {
|
|
143
|
-
elementRectRef.current = { x, y, width, height };
|
|
144
|
-
setIsPressing(true);
|
|
145
|
-
});
|
|
146
|
-
},
|
|
147
|
-
[cardDraggable],
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Attach pointer down listener to DOM element on web
|
|
151
|
-
// Must depend on isDragging because when isDragging=true, the element returns null
|
|
152
|
-
// and is unmounted. When isDragging becomes false, a NEW element is created
|
|
153
|
-
// and we need to re-attach the listener.
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (Platform.OS !== "web") return;
|
|
156
|
-
if (isDragging) return; // Element doesn't exist when dragging
|
|
157
|
-
|
|
158
|
-
// Get the DOM node from the React Native View ref
|
|
159
|
-
const node = containerRef.current as unknown as HTMLElement | null;
|
|
160
|
-
if (!node) return;
|
|
161
|
-
|
|
162
|
-
node.addEventListener("pointerdown", handlePointerDown);
|
|
163
|
-
|
|
164
|
-
return () => {
|
|
165
|
-
node.removeEventListener("pointerdown", handlePointerDown);
|
|
166
|
-
};
|
|
167
|
-
}, [handlePointerDown, isDragging]);
|
|
168
|
-
|
|
169
|
-
useEffect(() => {
|
|
170
|
-
if (!isPressing) return;
|
|
171
|
-
|
|
172
|
-
const handlePointerMove = (e: PointerEvent) => {
|
|
173
|
-
if (!pressStartRef.current || hasDraggedRef.current || wasCancelledRef.current) return;
|
|
174
|
-
|
|
175
|
-
const dx = Math.abs(e.clientX - pressStartRef.current.x);
|
|
176
|
-
const dy = Math.abs(e.clientY - pressStartRef.current.y);
|
|
177
|
-
const distance = Math.max(dx, dy);
|
|
178
|
-
|
|
179
|
-
// If we're still in the delay period (touch only)
|
|
180
|
-
if (!canDragRef.current) {
|
|
181
|
-
// Too much movement during delay - cancel drag possibility
|
|
182
|
-
if (distance > TOUCH_TOLERANCE) {
|
|
183
|
-
wasCancelledRef.current = true;
|
|
184
|
-
setIsDragPending(false);
|
|
185
|
-
if (delayTimerRef.current) {
|
|
186
|
-
clearTimeout(delayTimerRef.current);
|
|
187
|
-
delayTimerRef.current = null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Past delay (or mouse) - check drag threshold
|
|
194
|
-
if (distance > DRAG_THRESHOLD) {
|
|
195
|
-
hasDraggedRef.current = true;
|
|
196
|
-
setIsDragPending(false);
|
|
197
|
-
if (elementRectRef.current) {
|
|
198
|
-
startCardDrag(id, columnKey, index, e.clientX, e.clientY, elementRectRef.current);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const handlePointerUp = () => {
|
|
204
|
-
// Cleanup delay timer
|
|
205
|
-
if (delayTimerRef.current) {
|
|
206
|
-
clearTimeout(delayTimerRef.current);
|
|
207
|
-
delayTimerRef.current = null;
|
|
208
|
-
}
|
|
209
|
-
setIsDragPending(false);
|
|
210
|
-
|
|
211
|
-
// Trigger click if no drag occurred and wasn't cancelled
|
|
212
|
-
if (!hasDraggedRef.current && !wasCancelledRef.current && onCardPress) {
|
|
213
|
-
onCardPress(id, item, columnKey);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
setIsPressing(false);
|
|
217
|
-
pressStartRef.current = null;
|
|
218
|
-
elementRectRef.current = null;
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const handlePointerCancel = () => {
|
|
222
|
-
if (delayTimerRef.current) {
|
|
223
|
-
clearTimeout(delayTimerRef.current);
|
|
224
|
-
delayTimerRef.current = null;
|
|
225
|
-
}
|
|
226
|
-
setIsDragPending(false);
|
|
227
|
-
setIsPressing(false);
|
|
228
|
-
pressStartRef.current = null;
|
|
229
|
-
elementRectRef.current = null;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// Use pointer events for unified mouse/touch handling
|
|
233
|
-
window.addEventListener("pointermove", handlePointerMove);
|
|
234
|
-
window.addEventListener("pointerup", handlePointerUp);
|
|
235
|
-
window.addEventListener("pointercancel", handlePointerCancel);
|
|
236
|
-
|
|
237
|
-
return () => {
|
|
238
|
-
window.removeEventListener("pointermove", handlePointerMove);
|
|
239
|
-
window.removeEventListener("pointerup", handlePointerUp);
|
|
240
|
-
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
241
|
-
};
|
|
242
|
-
}, [isPressing, id, columnKey, index, item, startCardDrag, onCardPress]);
|
|
243
|
-
|
|
244
|
-
const handleHoverIn = useCallback(() => {
|
|
245
|
-
// Don't show hover when any drag is in progress
|
|
246
|
-
if (!isDragInProgress) {
|
|
247
|
-
setIsHovered(true);
|
|
248
|
-
}
|
|
249
|
-
}, [isDragInProgress]);
|
|
250
|
-
|
|
251
|
-
const handleHoverOut = useCallback(() => {
|
|
252
|
-
setIsHovered(false);
|
|
253
|
-
}, []);
|
|
254
|
-
|
|
255
|
-
// Stable style for gap between placeholder and card - MUST be before early returns
|
|
256
|
-
const gapStyle = useMemo(() => ({ height: itemGap }), [itemGap]);
|
|
257
|
-
|
|
258
|
-
const cardContent = renderCard({
|
|
259
|
-
item,
|
|
260
|
-
columnKey,
|
|
261
|
-
id,
|
|
262
|
-
hovered: isHovered,
|
|
263
|
-
isDragging,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
if (isDragging) {
|
|
267
|
-
// This card is being dragged - show placeholder at its own position if it's also drop target
|
|
268
|
-
if (isDropTarget) {
|
|
269
|
-
return (
|
|
270
|
-
<View style={{ overflow: "visible" }}>
|
|
271
|
-
{/* Maintain gap above placeholder when insert zones are enabled */}
|
|
272
|
-
{onAddCardAtIndex && <View style={gapStyle} />}
|
|
273
|
-
<CardPlaceholder height={placeholderHeight} />
|
|
274
|
-
</View>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Placeholder should match the dragged card's height, not this card's height
|
|
281
|
-
const dropPlaceholderHeight = draggedCardHeight ?? placeholderHeight;
|
|
282
|
-
|
|
283
|
-
// Visual feedback style when drag is pending (touch hold)
|
|
284
|
-
const pendingStyle = isDragPending
|
|
285
|
-
? { opacity: 0.8, transform: [{ scale: 1.02 }] as const }
|
|
286
|
-
: undefined;
|
|
287
|
-
|
|
288
|
-
return (
|
|
289
|
-
<View style={{ overflow: "visible" }}>
|
|
290
|
-
{/* Insert zone above this card - allows inserting at this card's position */}
|
|
291
|
-
{onAddCardAtIndex && (
|
|
292
|
-
<View style={{ height: itemGap, overflow: "visible", zIndex: 1 }}>
|
|
293
|
-
<InsertCardZone
|
|
294
|
-
height={itemGap}
|
|
295
|
-
columnKey={columnKey}
|
|
296
|
-
index={index}
|
|
297
|
-
onAddCardAtIndex={onAddCardAtIndex}
|
|
298
|
-
renderInsertCardButton={renderInsertCardButton}
|
|
299
|
-
isDragInProgress={isDragInProgress}
|
|
300
|
-
/>
|
|
301
|
-
</View>
|
|
302
|
-
)}
|
|
303
|
-
{isDropTarget && (
|
|
304
|
-
<>
|
|
305
|
-
<CardPlaceholder height={dropPlaceholderHeight} />
|
|
306
|
-
<View style={gapStyle} />
|
|
307
|
-
</>
|
|
308
|
-
)}
|
|
309
|
-
<View
|
|
310
|
-
ref={containerRef}
|
|
311
|
-
style={pendingStyle}
|
|
312
|
-
onPointerEnter={handleHoverIn}
|
|
313
|
-
onPointerLeave={handleHoverOut}
|
|
314
|
-
>
|
|
315
|
-
{cardContent}
|
|
316
|
-
</View>
|
|
317
|
-
</View>
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export const KanbanCard = memo(KanbanCardInner) as typeof KanbanCardInner;
|