@ram_28/kf-ai-sdk 1.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/LICENSE +21 -0
- package/README.md +840 -0
- package/dist/api/client.d.ts +78 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/datetime.d.ts +21 -0
- package/dist/api/datetime.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/metadata.d.ts +75 -0
- package/dist/api/metadata.d.ts.map +1 -0
- package/dist/components/hooks/index.d.ts +8 -0
- package/dist/components/hooks/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/index.d.ts +5 -0
- package/dist/components/hooks/useFilter/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts +33 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/types.d.ts +137 -0
- package/dist/components/hooks/useFilter/types.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts +3 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts +38 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/apiClient.d.ts +71 -0
- package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/index.d.ts +6 -0
- package/dist/components/hooks/useForm/index.d.ts.map +1 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts +88 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts +29 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/types.d.ts +412 -0
- package/dist/components/hooks/useForm/types.d.ts.map +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts +3 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts +99 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/context.d.ts +4 -0
- package/dist/components/hooks/useKanban/context.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts +27 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/index.d.ts +6 -0
- package/dist/components/hooks/useKanban/index.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/types.d.ts +438 -0
- package/dist/components/hooks/useKanban/types.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts +3 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts +62 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts.map +1 -0
- package/dist/components/hooks/useTable/index.d.ts +3 -0
- package/dist/components/hooks/useTable/index.d.ts.map +1 -0
- package/dist/components/hooks/useTable/types.d.ts +107 -0
- package/dist/components/hooks/useTable/types.d.ts.map +1 -0
- package/dist/components/hooks/useTable/useTable.d.ts +8 -0
- package/dist/components/hooks/useTable/useTable.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/kanban/Kanban.d.ts +12 -0
- package/dist/components/ui/kanban/Kanban.d.ts.map +1 -0
- package/dist/components/ui/kanban/index.d.ts +2 -0
- package/dist/components/ui/kanban/index.d.ts.map +1 -0
- package/dist/index.cjs +45 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +6522 -0
- package/dist/types/base-fields.d.ts +182 -0
- package/dist/types/base-fields.d.ts.map +1 -0
- package/dist/types/common.d.ts +238 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/cn.d.ts +7 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/formatting.d.ts +52 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/sdk/api/client.ts +447 -0
- package/sdk/api/datetime.ts +33 -0
- package/sdk/api/index.ts +61 -0
- package/sdk/api/metadata.ts +148 -0
- package/sdk/components/hooks/index.ts +34 -0
- package/sdk/components/hooks/useFilter/index.ts +37 -0
- package/sdk/components/hooks/useFilter/payloadBuilder.utils.ts +298 -0
- package/sdk/components/hooks/useFilter/types.ts +158 -0
- package/sdk/components/hooks/useFilter/useFilter.llm.txt +497 -0
- package/sdk/components/hooks/useFilter/useFilter.ts +494 -0
- package/sdk/components/hooks/useFilter/validation.utils.ts +401 -0
- package/sdk/components/hooks/useForm/apiClient.ts +441 -0
- package/sdk/components/hooks/useForm/expressionValidator.utils.ts +444 -0
- package/sdk/components/hooks/useForm/index.ts +64 -0
- package/sdk/components/hooks/useForm/optimizedExpressionValidator.utils.ts +482 -0
- package/sdk/components/hooks/useForm/ruleClassifier.utils.ts +424 -0
- package/sdk/components/hooks/useForm/schemaParser.utils.ts +519 -0
- package/sdk/components/hooks/useForm/types.ts +630 -0
- package/sdk/components/hooks/useForm/useForm.llm.txt +340 -0
- package/sdk/components/hooks/useForm/useForm.ts +821 -0
- package/sdk/components/hooks/useKanban/apiClient.ts +494 -0
- package/sdk/components/hooks/useKanban/context.ts +14 -0
- package/sdk/components/hooks/useKanban/dragDropManager.ts +529 -0
- package/sdk/components/hooks/useKanban/index.ts +63 -0
- package/sdk/components/hooks/useKanban/types.ts +606 -0
- package/sdk/components/hooks/useKanban/useKanban.llm.txt +482 -0
- package/sdk/components/hooks/useKanban/useKanban.ts +725 -0
- package/sdk/components/hooks/useKanban/useKanbanSimple.ts +389 -0
- package/sdk/components/hooks/useTable/index.ts +5 -0
- package/sdk/components/hooks/useTable/types.ts +154 -0
- package/sdk/components/hooks/useTable/useTable.llm.txt +344 -0
- package/sdk/components/hooks/useTable/useTable.ts +413 -0
- package/sdk/components/index.ts +15 -0
- package/sdk/components/ui/index.ts +2 -0
- package/sdk/components/ui/kanban/Kanban.tsx +134 -0
- package/sdk/components/ui/kanban/index.ts +11 -0
- package/sdk/index.ts +13 -0
- package/sdk/types/base-fields.ts +221 -0
- package/sdk/types/common.ts +306 -0
- package/sdk/types/index.ts +5 -0
- package/sdk/utils/cn.ts +10 -0
- package/sdk/utils/formatting.ts +212 -0
- package/sdk/utils/index.ts +5 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// KANBAN DRAG & DROP MANAGER
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Complete drag & drop functionality with mouse, touch, and keyboard support
|
|
5
|
+
// Includes accessibility features and auto-scrolling
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
8
|
+
import type {
|
|
9
|
+
KanbanCard,
|
|
10
|
+
KanbanColumn,
|
|
11
|
+
DragDropState,
|
|
12
|
+
DragDropManager
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// CONFIGURATION CONSTANTS
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
const AUTO_SCROLL_THRESHOLD = 50;
|
|
20
|
+
const AUTO_SCROLL_SPEED = 5;
|
|
21
|
+
const TOUCH_MOVE_THRESHOLD = 5;
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// DRAG & DROP HOOK
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
export function useDragDropManager<T>({
|
|
28
|
+
onCardMove,
|
|
29
|
+
onError,
|
|
30
|
+
columns,
|
|
31
|
+
announceMove
|
|
32
|
+
}: {
|
|
33
|
+
onCardMove?: (card: KanbanCard<T>, fromColumnId: string, toColumnId: string) => void;
|
|
34
|
+
onError?: (error: Error) => void;
|
|
35
|
+
columns: KanbanColumn<T>[];
|
|
36
|
+
announceMove?: (card: KanbanCard<T>, fromColumn: string, toColumn: string) => void;
|
|
37
|
+
}): DragDropManager<T> {
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// STATE MANAGEMENT
|
|
41
|
+
// ============================================================
|
|
42
|
+
|
|
43
|
+
const [dragState, setDragState] = useState<DragDropState<T>>({
|
|
44
|
+
isDragging: false,
|
|
45
|
+
draggedCard: null,
|
|
46
|
+
dragOverColumn: null,
|
|
47
|
+
dragOverPosition: null,
|
|
48
|
+
dragSourceColumn: null
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Refs for managing drag operations
|
|
52
|
+
const dragImageRef = useRef<HTMLElement | null>(null);
|
|
53
|
+
const autoScrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
54
|
+
const touchStartRef = useRef<{ x: number; y: number; card: KanbanCard<T> } | null>(null);
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// UTILITY FUNCTIONS
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
const resetState = useCallback(() => {
|
|
61
|
+
setDragState({
|
|
62
|
+
isDragging: false,
|
|
63
|
+
draggedCard: null,
|
|
64
|
+
dragOverColumn: null,
|
|
65
|
+
dragOverPosition: null,
|
|
66
|
+
dragSourceColumn: null
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Clear any auto-scroll timeout
|
|
70
|
+
if (autoScrollTimeoutRef.current) {
|
|
71
|
+
clearTimeout(autoScrollTimeoutRef.current);
|
|
72
|
+
autoScrollTimeoutRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const findColumnById = useCallback((columnId: string): KanbanColumn<T> | null => {
|
|
77
|
+
return columns.find(col => col._id === columnId) || null;
|
|
78
|
+
}, [columns]);
|
|
79
|
+
|
|
80
|
+
const findCardById = useCallback((cardId: string): { card: KanbanCard<T>; column: KanbanColumn<T> } | null => {
|
|
81
|
+
for (const column of columns) {
|
|
82
|
+
const card = column.cards.find(c => c._id === cardId);
|
|
83
|
+
if (card) {
|
|
84
|
+
return { card, column };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}, [columns]);
|
|
89
|
+
|
|
90
|
+
// ============================================================
|
|
91
|
+
// MOUSE DRAG HANDLERS
|
|
92
|
+
// ============================================================
|
|
93
|
+
|
|
94
|
+
const handleDragStart = useCallback((event: DragEvent, card: KanbanCard<T>) => {
|
|
95
|
+
try {
|
|
96
|
+
const cardElement = event.target as HTMLElement;
|
|
97
|
+
const sourceColumn = findColumnById(card.columnId);
|
|
98
|
+
|
|
99
|
+
if (!sourceColumn) {
|
|
100
|
+
throw new Error("Source column not found");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set up drag data
|
|
104
|
+
event.dataTransfer?.setData("text/plain", card._id);
|
|
105
|
+
event.dataTransfer?.setData("application/json", JSON.stringify(card));
|
|
106
|
+
|
|
107
|
+
// Create drag image from the actual card element
|
|
108
|
+
const dragImage = cardElement.cloneNode(true) as HTMLElement;
|
|
109
|
+
dragImage.style.cssText = `
|
|
110
|
+
position: fixed;
|
|
111
|
+
top: -1000px;
|
|
112
|
+
left: -1000px;
|
|
113
|
+
opacity: 0.9;
|
|
114
|
+
transform: rotate(-2deg) scale(0.95);
|
|
115
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
z-index: 9999;
|
|
118
|
+
max-width: 300px;
|
|
119
|
+
background: white;
|
|
120
|
+
border-radius: 8px;
|
|
121
|
+
`;
|
|
122
|
+
document.body.appendChild(dragImage);
|
|
123
|
+
dragImageRef.current = dragImage;
|
|
124
|
+
|
|
125
|
+
if (event.dataTransfer) {
|
|
126
|
+
event.dataTransfer.setDragImage(dragImage, event.offsetX || 50, event.offsetY || 20);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Update state
|
|
130
|
+
setDragState({
|
|
131
|
+
isDragging: true,
|
|
132
|
+
draggedCard: card,
|
|
133
|
+
dragOverColumn: null,
|
|
134
|
+
dragOverPosition: null,
|
|
135
|
+
dragSourceColumn: sourceColumn._id
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Accessibility announcement
|
|
139
|
+
announceMove?.(card, sourceColumn.title, "being moved");
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
onError?.(error instanceof Error ? error : new Error("Drag start failed"));
|
|
143
|
+
resetState();
|
|
144
|
+
}
|
|
145
|
+
}, [findColumnById, onError, resetState, announceMove]);
|
|
146
|
+
|
|
147
|
+
const handleDragOver = useCallback((event: DragEvent, columnId?: string) => {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
|
|
150
|
+
if (!dragState.isDragging || !dragState.draggedCard) return;
|
|
151
|
+
|
|
152
|
+
const column = columnId ? findColumnById(columnId) : null;
|
|
153
|
+
|
|
154
|
+
setDragState(prev => ({
|
|
155
|
+
...prev,
|
|
156
|
+
dragOverColumn: column?._id || null,
|
|
157
|
+
dragOverPosition: null // Will be calculated based on mouse position
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
// Auto-scroll logic
|
|
161
|
+
handleAutoScroll(event);
|
|
162
|
+
|
|
163
|
+
}, [dragState.isDragging, dragState.draggedCard, findColumnById]);
|
|
164
|
+
|
|
165
|
+
const handleDrop = useCallback((event: DragEvent, columnId: string) => {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
|
|
168
|
+
if (!dragState.isDragging || !dragState.draggedCard) return;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const card = dragState.draggedCard;
|
|
172
|
+
const sourceColumnId = dragState.dragSourceColumn!;
|
|
173
|
+
const targetColumn = findColumnById(columnId);
|
|
174
|
+
const sourceColumn = findColumnById(sourceColumnId);
|
|
175
|
+
|
|
176
|
+
if (!targetColumn || !sourceColumn) {
|
|
177
|
+
throw new Error("Target or source column not found");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Don't do anything if dropping in the same position
|
|
181
|
+
if (sourceColumnId === columnId) {
|
|
182
|
+
resetState();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if column has a limit
|
|
187
|
+
if (targetColumn.limit && targetColumn.cards.length >= targetColumn.limit) {
|
|
188
|
+
throw new Error(`Column "${targetColumn.title}" has reached its limit of ${targetColumn.limit} cards`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Call the move handler
|
|
192
|
+
onCardMove?.(card, sourceColumnId, columnId);
|
|
193
|
+
|
|
194
|
+
// Accessibility announcement
|
|
195
|
+
announceMove?.(card, sourceColumn.title, targetColumn.title);
|
|
196
|
+
|
|
197
|
+
} catch (error) {
|
|
198
|
+
onError?.(error instanceof Error ? error : new Error("Drop failed"));
|
|
199
|
+
} finally {
|
|
200
|
+
resetState();
|
|
201
|
+
}
|
|
202
|
+
}, [dragState, findColumnById, onCardMove, onError, resetState, announceMove]);
|
|
203
|
+
|
|
204
|
+
const handleDragEnd = useCallback(() => {
|
|
205
|
+
// Clean up drag image
|
|
206
|
+
if (dragImageRef.current) {
|
|
207
|
+
document.body.removeChild(dragImageRef.current);
|
|
208
|
+
dragImageRef.current = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resetState();
|
|
212
|
+
}, [resetState]);
|
|
213
|
+
|
|
214
|
+
// ============================================================
|
|
215
|
+
// AUTO-SCROLL FUNCTIONALITY
|
|
216
|
+
// ============================================================
|
|
217
|
+
|
|
218
|
+
const handleAutoScroll = useCallback((event: DragEvent) => {
|
|
219
|
+
const container = (event.target as HTMLElement).closest('.kanban-container');
|
|
220
|
+
if (!container) return;
|
|
221
|
+
|
|
222
|
+
const rect = container.getBoundingClientRect();
|
|
223
|
+
const mouseX = event.clientX - rect.left;
|
|
224
|
+
const containerWidth = rect.width;
|
|
225
|
+
|
|
226
|
+
let scrollDirection = 0;
|
|
227
|
+
|
|
228
|
+
// Check if near left edge
|
|
229
|
+
if (mouseX < AUTO_SCROLL_THRESHOLD) {
|
|
230
|
+
scrollDirection = -AUTO_SCROLL_SPEED;
|
|
231
|
+
}
|
|
232
|
+
// Check if near right edge
|
|
233
|
+
else if (mouseX > containerWidth - AUTO_SCROLL_THRESHOLD) {
|
|
234
|
+
scrollDirection = AUTO_SCROLL_SPEED;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (scrollDirection !== 0) {
|
|
238
|
+
// Clear existing timeout
|
|
239
|
+
if (autoScrollTimeoutRef.current) {
|
|
240
|
+
clearTimeout(autoScrollTimeoutRef.current);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Start auto-scroll
|
|
244
|
+
const scroll = () => {
|
|
245
|
+
container.scrollLeft += scrollDirection;
|
|
246
|
+
autoScrollTimeoutRef.current = setTimeout(scroll, 16); // ~60fps
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
autoScrollTimeoutRef.current = setTimeout(scroll, 100);
|
|
250
|
+
} else {
|
|
251
|
+
// Stop auto-scroll
|
|
252
|
+
if (autoScrollTimeoutRef.current) {
|
|
253
|
+
clearTimeout(autoScrollTimeoutRef.current);
|
|
254
|
+
autoScrollTimeoutRef.current = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// KEYBOARD NAVIGATION
|
|
261
|
+
// ============================================================
|
|
262
|
+
|
|
263
|
+
const handleKeyDown = useCallback((event: KeyboardEvent, card: KanbanCard<T>) => {
|
|
264
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Space', 'Escape'].includes(event.key)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
|
|
270
|
+
const cardData = findCardById(card._id);
|
|
271
|
+
if (!cardData) return;
|
|
272
|
+
|
|
273
|
+
const { column: currentColumn } = cardData;
|
|
274
|
+
const currentColumnIndex = columns.findIndex(col => col._id === currentColumn._id);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
switch (event.key) {
|
|
278
|
+
case 'ArrowLeft':
|
|
279
|
+
// Move to previous column
|
|
280
|
+
if (currentColumnIndex > 0) {
|
|
281
|
+
const targetColumn = columns[currentColumnIndex - 1];
|
|
282
|
+
onCardMove?.(card, currentColumn._id, targetColumn._id);
|
|
283
|
+
announceMove?.(card, currentColumn.title, targetColumn.title);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case 'ArrowRight':
|
|
288
|
+
// Move to next column
|
|
289
|
+
if (currentColumnIndex < columns.length - 1) {
|
|
290
|
+
const targetColumn = columns[currentColumnIndex + 1];
|
|
291
|
+
onCardMove?.(card, currentColumn._id, targetColumn._id);
|
|
292
|
+
announceMove?.(card, currentColumn.title, targetColumn.title);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'ArrowUp':
|
|
297
|
+
// Move up within column (future enhancement)
|
|
298
|
+
announceMove?.(card, "current position", "position above");
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'ArrowDown':
|
|
302
|
+
// Move down within column (future enhancement)
|
|
303
|
+
announceMove?.(card, "current position", "position below");
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 'Enter':
|
|
307
|
+
case 'Space':
|
|
308
|
+
// Start drag mode (future enhancement for keyboard-only drag)
|
|
309
|
+
announceMove?.(card, currentColumn.title, "drag mode");
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'Escape':
|
|
313
|
+
// Cancel drag mode
|
|
314
|
+
announceMove?.(card, "drag mode", "cancelled");
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
onError?.(error instanceof Error ? error : new Error("Keyboard navigation failed"));
|
|
319
|
+
}
|
|
320
|
+
}, [findCardById, columns, onCardMove, onError, announceMove]);
|
|
321
|
+
|
|
322
|
+
// ============================================================
|
|
323
|
+
// TOUCH HANDLERS
|
|
324
|
+
// ============================================================
|
|
325
|
+
|
|
326
|
+
const handleTouchStart = useCallback((event: TouchEvent, card: KanbanCard<T>) => {
|
|
327
|
+
const touch = event.touches[0];
|
|
328
|
+
touchStartRef.current = {
|
|
329
|
+
x: touch.clientX,
|
|
330
|
+
y: touch.clientY,
|
|
331
|
+
card
|
|
332
|
+
};
|
|
333
|
+
}, []);
|
|
334
|
+
|
|
335
|
+
const handleTouchMove = useCallback((event: TouchEvent) => {
|
|
336
|
+
if (!touchStartRef.current) return;
|
|
337
|
+
|
|
338
|
+
const touch = event.touches[0];
|
|
339
|
+
const deltaX = Math.abs(touch.clientX - touchStartRef.current.x);
|
|
340
|
+
const deltaY = Math.abs(touch.clientY - touchStartRef.current.y);
|
|
341
|
+
|
|
342
|
+
// Check if moved enough to start drag
|
|
343
|
+
if (deltaX > TOUCH_MOVE_THRESHOLD || deltaY > TOUCH_MOVE_THRESHOLD) {
|
|
344
|
+
const card = touchStartRef.current.card;
|
|
345
|
+
const sourceColumn = findColumnById(card.columnId);
|
|
346
|
+
|
|
347
|
+
if (!sourceColumn) return;
|
|
348
|
+
|
|
349
|
+
setDragState({
|
|
350
|
+
isDragging: true,
|
|
351
|
+
draggedCard: card,
|
|
352
|
+
dragOverColumn: null,
|
|
353
|
+
dragOverPosition: null,
|
|
354
|
+
dragSourceColumn: sourceColumn._id
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
announceMove?.(card, sourceColumn.title, "being moved");
|
|
358
|
+
}
|
|
359
|
+
}, [findColumnById, announceMove]);
|
|
360
|
+
|
|
361
|
+
const handleTouchEnd = useCallback((event: TouchEvent) => {
|
|
362
|
+
if (!dragState.isDragging || !dragState.draggedCard || !touchStartRef.current) {
|
|
363
|
+
touchStartRef.current = null;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
// Find the element under the touch point
|
|
369
|
+
const touch = event.changedTouches[0];
|
|
370
|
+
const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
371
|
+
const columnElement = elementBelow?.closest('[data-column-id]') as HTMLElement;
|
|
372
|
+
|
|
373
|
+
if (columnElement) {
|
|
374
|
+
const columnId = columnElement.dataset.columnId!;
|
|
375
|
+
const card = dragState.draggedCard;
|
|
376
|
+
const sourceColumnId = dragState.dragSourceColumn!;
|
|
377
|
+
const targetColumn = findColumnById(columnId);
|
|
378
|
+
const sourceColumn = findColumnById(sourceColumnId);
|
|
379
|
+
|
|
380
|
+
if (targetColumn && sourceColumn && sourceColumnId !== columnId) {
|
|
381
|
+
// Check column limit
|
|
382
|
+
if (targetColumn.limit && targetColumn.cards.length >= targetColumn.limit) {
|
|
383
|
+
throw new Error(`Column "${targetColumn.title}" has reached its limit of ${targetColumn.limit} cards`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
onCardMove?.(card, sourceColumnId, columnId);
|
|
387
|
+
announceMove?.(card, sourceColumn.title, targetColumn.title);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
onError?.(error instanceof Error ? error : new Error("Touch drop failed"));
|
|
392
|
+
} finally {
|
|
393
|
+
touchStartRef.current = null;
|
|
394
|
+
resetState();
|
|
395
|
+
}
|
|
396
|
+
}, [dragState, findColumnById, onCardMove, onError, resetState, announceMove]);
|
|
397
|
+
|
|
398
|
+
// ============================================================
|
|
399
|
+
// ACCESSIBILITY ANNOUNCEMENTS
|
|
400
|
+
// ============================================================
|
|
401
|
+
|
|
402
|
+
const defaultAnnounceMove = useCallback((card: KanbanCard<T>, fromColumn: string, toColumn: string) => {
|
|
403
|
+
const message = `Moved "${card.title}" from ${fromColumn} to ${toColumn}`;
|
|
404
|
+
|
|
405
|
+
// Create live region announcement
|
|
406
|
+
const announcement = document.createElement('div');
|
|
407
|
+
announcement.setAttribute('aria-live', 'polite');
|
|
408
|
+
announcement.setAttribute('aria-atomic', 'true');
|
|
409
|
+
announcement.className = 'sr-only';
|
|
410
|
+
announcement.textContent = message;
|
|
411
|
+
|
|
412
|
+
document.body.appendChild(announcement);
|
|
413
|
+
|
|
414
|
+
// Remove after announcement
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
document.body.removeChild(announcement);
|
|
417
|
+
}, 1000);
|
|
418
|
+
|
|
419
|
+
// Also log for debugging
|
|
420
|
+
console.log(`Kanban: ${message}`);
|
|
421
|
+
}, []);
|
|
422
|
+
|
|
423
|
+
// ============================================================
|
|
424
|
+
// CLEANUP ON UNMOUNT
|
|
425
|
+
// ============================================================
|
|
426
|
+
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
return () => {
|
|
429
|
+
// Cleanup on unmount
|
|
430
|
+
if (autoScrollTimeoutRef.current) {
|
|
431
|
+
clearTimeout(autoScrollTimeoutRef.current);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (dragImageRef.current && document.body.contains(dragImageRef.current)) {
|
|
435
|
+
document.body.removeChild(dragImageRef.current);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}, []);
|
|
439
|
+
|
|
440
|
+
// ============================================================
|
|
441
|
+
// RETURN INTERFACE
|
|
442
|
+
// ============================================================
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
// State
|
|
446
|
+
isDragging: dragState.isDragging,
|
|
447
|
+
draggedCard: dragState.draggedCard,
|
|
448
|
+
dragOverColumn: dragState.dragOverColumn,
|
|
449
|
+
dragOverPosition: dragState.dragOverPosition,
|
|
450
|
+
dragSourceColumn: dragState.dragSourceColumn,
|
|
451
|
+
|
|
452
|
+
// Handlers
|
|
453
|
+
handleDragStart,
|
|
454
|
+
handleDragOver,
|
|
455
|
+
handleDrop,
|
|
456
|
+
handleDragEnd,
|
|
457
|
+
handleKeyDown,
|
|
458
|
+
handleTouchStart,
|
|
459
|
+
handleTouchMove,
|
|
460
|
+
handleTouchEnd,
|
|
461
|
+
|
|
462
|
+
// Utilities
|
|
463
|
+
announceMove: announceMove || defaultAnnounceMove,
|
|
464
|
+
reset: resetState
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================================
|
|
469
|
+
// UTILITY FUNCTIONS FOR DRAG & DROP
|
|
470
|
+
// ============================================================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Calculate drop position based on mouse coordinates
|
|
474
|
+
*/
|
|
475
|
+
export function calculateDropPosition<T>(
|
|
476
|
+
event: DragEvent,
|
|
477
|
+
column: KanbanColumn<T>,
|
|
478
|
+
cardHeight = 80
|
|
479
|
+
): number {
|
|
480
|
+
const columnElement = (event.target as HTMLElement).closest('.kanban-column');
|
|
481
|
+
if (!columnElement) return column.cards.length;
|
|
482
|
+
|
|
483
|
+
const columnRect = columnElement.getBoundingClientRect();
|
|
484
|
+
const relativeY = event.clientY - columnRect.top;
|
|
485
|
+
|
|
486
|
+
// Calculate which position in the column this corresponds to
|
|
487
|
+
const headerHeight = 60; // Approximate column header height
|
|
488
|
+
const cardY = relativeY - headerHeight;
|
|
489
|
+
|
|
490
|
+
if (cardY <= 0) return 0;
|
|
491
|
+
|
|
492
|
+
const position = Math.floor(cardY / cardHeight);
|
|
493
|
+
return Math.min(position, column.cards.length);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Add visual feedback classes to elements during drag
|
|
498
|
+
*/
|
|
499
|
+
export function addDragFeedback(element: HTMLElement, type: 'source' | 'target') {
|
|
500
|
+
element.classList.add(`kanban-drag-${type}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Remove visual feedback classes
|
|
505
|
+
*/
|
|
506
|
+
export function removeDragFeedback(element: HTMLElement) {
|
|
507
|
+
element.classList.remove('kanban-drag-source', 'kanban-drag-target');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Check if a column can accept a drop
|
|
512
|
+
*/
|
|
513
|
+
export function canAcceptDrop<T>(
|
|
514
|
+
column: KanbanColumn<T>,
|
|
515
|
+
draggedCard: KanbanCard<T>
|
|
516
|
+
): { canDrop: boolean; reason?: string } {
|
|
517
|
+
// Check if column has a limit
|
|
518
|
+
if (column.limit && column.cards.length >= column.limit && draggedCard.columnId !== column._id) {
|
|
519
|
+
return {
|
|
520
|
+
canDrop: false,
|
|
521
|
+
reason: `Column "${column.title}" has reached its limit of ${column.limit} cards`
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Add more validation rules here as needed
|
|
526
|
+
// e.g., check card type compatibility, user permissions, etc.
|
|
527
|
+
|
|
528
|
+
return { canDrop: true };
|
|
529
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// KANBAN SDK - Main Export
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export { useKanban } from './useKanban';
|
|
6
|
+
export { KanbanContext, useKanbanContext } from './context';
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
// Core interfaces
|
|
10
|
+
UseKanbanOptions,
|
|
11
|
+
UseKanbanReturn,
|
|
12
|
+
KanbanCard,
|
|
13
|
+
KanbanColumn,
|
|
14
|
+
ColumnConfig,
|
|
15
|
+
ColumnDefinition,
|
|
16
|
+
|
|
17
|
+
// Operations
|
|
18
|
+
CardOperations,
|
|
19
|
+
SearchOperations,
|
|
20
|
+
FilterOperations,
|
|
21
|
+
|
|
22
|
+
// Drag & Drop
|
|
23
|
+
DragDropState,
|
|
24
|
+
DragDropHandlers,
|
|
25
|
+
DragDropManager,
|
|
26
|
+
|
|
27
|
+
// API Types
|
|
28
|
+
CardApiResponse,
|
|
29
|
+
ColumnApiResponse,
|
|
30
|
+
MoveCardRequest,
|
|
31
|
+
ReorderRequest,
|
|
32
|
+
BulkCardUpdateRequest,
|
|
33
|
+
BulkColumnUpdateRequest,
|
|
34
|
+
|
|
35
|
+
// Validation
|
|
36
|
+
ValidationResult,
|
|
37
|
+
CardValidationContext,
|
|
38
|
+
ColumnValidationContext,
|
|
39
|
+
|
|
40
|
+
// Events
|
|
41
|
+
KanbanEventType,
|
|
42
|
+
KanbanEvent,
|
|
43
|
+
|
|
44
|
+
// Utility Types
|
|
45
|
+
ExtractCardType,
|
|
46
|
+
PartialBy,
|
|
47
|
+
CreateCardInput,
|
|
48
|
+
CreateColumnInput,
|
|
49
|
+
} from './types';
|
|
50
|
+
|
|
51
|
+
// Export utility functions for advanced use cases
|
|
52
|
+
export {
|
|
53
|
+
mergeCardsIntoColumns,
|
|
54
|
+
calculateCardPosition,
|
|
55
|
+
calculateColumnPosition,
|
|
56
|
+
normalizePositions,
|
|
57
|
+
handleKanbanApiError,
|
|
58
|
+
validateApiResponse
|
|
59
|
+
} from './apiClient';
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
useDragDropManager
|
|
63
|
+
} from './dragDropManager';
|