@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.
Files changed (45) hide show
  1. package/package.json +1 -15
  2. package/src/react_native.d.ts +2 -2
  3. package/src/cell_date.tsx +0 -30
  4. package/src/cell_date_format.test.ts +0 -32
  5. package/src/cell_date_format.ts +0 -73
  6. package/src/cell_number.test.ts +0 -42
  7. package/src/cell_number.tsx +0 -25
  8. package/src/cell_number_format.ts +0 -42
  9. package/src/cell_select.tsx +0 -68
  10. package/src/cell_text.tsx +0 -45
  11. package/src/grid/data_grid.tsx +0 -2003
  12. package/src/grid/data_grid_columns.test.ts +0 -72
  13. package/src/grid/data_grid_columns.ts +0 -30
  14. package/src/grid/data_grid_context.ts +0 -119
  15. package/src/grid/dispatch_safely.ts +0 -39
  16. package/src/grid/engine.module.css +0 -114
  17. package/src/grid/engine.tsx +0 -1042
  18. package/src/grid/helpers.ts +0 -205
  19. package/src/grid/layout.test.ts +0 -515
  20. package/src/grid/layout.ts +0 -425
  21. package/src/grid/recycling.test.ts +0 -236
  22. package/src/grid/recycling.ts +0 -172
  23. package/src/grid/row_cell.module.css +0 -105
  24. package/src/grid/row_cell.tsx +0 -313
  25. package/src/grid/search_highlight.ts +0 -71
  26. package/src/grid/select_cell.tsx +0 -58
  27. package/src/grid/select_group_summary_cell.tsx +0 -76
  28. package/src/grid/select_header_cell.tsx +0 -32
  29. package/src/grid/skeleton_row.module.css +0 -34
  30. package/src/grid/skeleton_row.tsx +0 -20
  31. package/src/grid/use_grid_groups.ts +0 -311
  32. package/src/grid/use_scroll_to_cell.ts +0 -135
  33. package/src/grid/use_virtual_grid.ts +0 -383
  34. package/src/grid/visibility.test.ts +0 -208
  35. package/src/grid/visibility.ts +0 -77
  36. package/src/kanban/constants.ts +0 -18
  37. package/src/kanban/default_renderers.tsx +0 -160
  38. package/src/kanban/drag_preview.tsx +0 -157
  39. package/src/kanban/index.ts +0 -13
  40. package/src/kanban/insert_card_zone.tsx +0 -135
  41. package/src/kanban/kanban_board.tsx +0 -635
  42. package/src/kanban/kanban_card.tsx +0 -321
  43. package/src/kanban/kanban_column.tsx +0 -499
  44. package/src/kanban/placeholders.tsx +0 -54
  45. 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;