@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,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;