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