playbook_ui 15.5.0 → 15.6.0.pre.alpha.PLAY2686contactkittextonly13049

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +96 -6
  3. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.html.erb +1 -1
  4. data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
  5. data/app/pb_kits/playbook/pb_background/background.test.js +5 -1
  6. data/app/pb_kits/playbook/pb_background/docs/_background_light.html.erb +1 -1
  7. data/app/pb_kits/playbook/pb_background/docs/_background_light.jsx +0 -1
  8. data/app/pb_kits/playbook/pb_background/docs/_background_light.md +1 -0
  9. data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
  10. data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
  11. data/app/pb_kits/playbook/pb_card/docs/_card_highlight.md +1 -1
  12. data/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +2 -2
  13. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleIcon.tsx +10 -8
  14. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_icons.jsx +0 -1
  15. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_state.jsx +0 -3
  16. data/app/pb_kits/playbook/pb_contact/_contact.tsx +51 -24
  17. data/app/pb_kits/playbook/pb_contact/contact.html.erb +53 -19
  18. data/app/pb_kits/playbook/pb_contact/contact.rb +11 -1
  19. data/app/pb_kits/playbook/pb_contact/contact.test.js +76 -0
  20. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.html.erb +33 -0
  21. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.jsx +46 -0
  22. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_rails.md +2 -0
  23. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_react.md +2 -0
  24. data/app/pb_kits/playbook/pb_contact/docs/example.yml +2 -0
  25. data/app/pb_kits/playbook/pb_contact/docs/index.js +1 -0
  26. data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +24 -0
  27. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +181 -3
  28. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
  29. data/app/pb_kits/playbook/pb_distribution_bar/docs/_distribution_bar_custom_colors.md +1 -1
  30. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +458 -7
  31. data/app/pb_kits/playbook/pb_draggable/context/types.ts +8 -3
  32. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.jsx +180 -0
  33. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.md +22 -0
  34. data/app/pb_kits/playbook/pb_draggable/docs/example.yml +3 -2
  35. data/app/pb_kits/playbook/pb_draggable/docs/index.js +2 -1
  36. data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +77 -1
  37. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +4 -4
  38. data/app/pb_kits/playbook/pb_filter/Filter/FilterBackground.tsx +3 -3
  39. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
  40. data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
  41. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
  42. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
  43. data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
  44. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
  45. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
  46. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
  47. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
  48. data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
  49. data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
  50. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
  51. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
  52. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
  53. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
  54. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  55. data/app/pb_kits/playbook/pb_select/_select.tsx +8 -3
  56. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  57. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +16 -0
  58. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +30 -0
  59. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +1 -0
  60. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  61. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  62. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  63. data/app/pb_kits/playbook/pb_select/select.rb +3 -1
  64. data/app/pb_kits/playbook/pb_select/select.test.js +23 -0
  65. data/app/pb_kits/playbook/pb_table/_table.tsx +187 -33
  66. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.jsx +134 -0
  67. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.md +34 -0
  68. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.html.erb +101 -0
  69. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +33 -0
  70. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.jsx +180 -0
  71. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.md +3 -0
  72. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.html.erb +122 -0
  73. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.md +3 -0
  74. data/app/pb_kits/playbook/pb_table/docs/example.yml +4 -0
  75. data/app/pb_kits/playbook/pb_table/docs/index.js +2 -0
  76. data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +49 -0
  77. data/app/pb_kits/playbook/pb_table/table.html.erb +68 -12
  78. data/app/pb_kits/playbook/pb_table/table.rb +22 -3
  79. data/app/pb_kits/playbook/pb_table/table.test.js +143 -0
  80. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  81. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  82. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -0
  83. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.html.erb +60 -0
  84. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.jsx +118 -0
  85. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.md +1 -0
  86. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_date.md +1 -1
  87. data/app/pb_kits/playbook/pb_timeline/docs/example.yml +2 -0
  88. data/app/pb_kits/playbook/pb_timeline/docs/index.js +1 -0
  89. data/app/pb_kits/playbook/pb_timeline/item.html.erb +1 -1
  90. data/app/pb_kits/playbook/pb_timeline/item.rb +2 -0
  91. data/app/pb_kits/playbook/pb_timeline/label.html.erb +2 -1
  92. data/app/pb_kits/playbook/pb_timeline/label.rb +2 -0
  93. data/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx +3 -0
  94. data/app/pb_kits/playbook/pb_timeline/timeline.test.js +51 -0
  95. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
  96. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
  97. data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
  98. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
  99. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
  100. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
  101. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
  102. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
  103. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  104. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
  105. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
  106. data/app/pb_kits/playbook/tokens/_colors.scss +2 -1
  107. data/app/pb_kits/playbook/utilities/deprecated.ts +73 -0
  108. data/dist/chunks/_typeahead-CHwm9MTE.js +6 -0
  109. data/dist/chunks/lib-Cugvy62C.js +29 -0
  110. data/dist/chunks/vendor.js +3 -3
  111. data/dist/playbook-rails-react-bindings.js +1 -1
  112. data/dist/playbook-rails.js +1 -1
  113. data/dist/playbook.css +1 -1
  114. data/lib/playbook/forms/builder/collection_select_field.rb +9 -1
  115. data/lib/playbook/forms/builder/select_field.rb +9 -1
  116. data/lib/playbook/forms/builder/time_zone_select_field.rb +9 -1
  117. data/lib/playbook/pb_kit_helper.rb +35 -0
  118. data/lib/playbook/version.rb +2 -2
  119. metadata +31 -5
  120. data/app/pb_kits/playbook/pb_bar_graph/BarGraphStyles.scss +0 -58
  121. data/dist/chunks/_typeahead-Bx4QsIEU.js +0 -6
  122. data/dist/chunks/lib-Dk4GKPut.js +0 -29
@@ -1,4 +1,4 @@
1
- import React, { createContext, useReducer, useContext, useEffect, useMemo } from "react";
1
+ import React, { createContext, useReducer, useContext, useEffect, useMemo, useRef } from "react";
2
2
  import { InitialStateType, ActionType, DraggableProviderType } from "./types";
3
3
 
4
4
  const initialState: InitialStateType = {
@@ -39,6 +39,88 @@ const reducer = (state: InitialStateType, action: ActionType) => {
39
39
 
40
40
  return { ...state, items: newItems };
41
41
  }
42
+
43
+ // Used only when enableCrossContainerPreview is true
44
+ case "REORDER_ITEMS_CROSS_CONTAINER": {
45
+ const { dragId, targetId, newContainer } = action.payload;
46
+ const newItems = [...state.items];
47
+ const draggedItem = newItems.find((item) => item && item.id === dragId);
48
+
49
+ if (!draggedItem) return state;
50
+
51
+ const draggedIndex = newItems.indexOf(draggedItem);
52
+ const targetIndex = newItems.findIndex(
53
+ (item) => item && item.id === targetId
54
+ );
55
+
56
+ if (draggedIndex === -1 || targetIndex === -1) return state;
57
+
58
+ const updatedItem = { ...draggedItem, container: newContainer };
59
+ newItems.splice(draggedIndex, 1);
60
+ newItems.splice(targetIndex, 0, updatedItem);
61
+
62
+ return { ...state, items: newItems };
63
+ }
64
+
65
+ // Used only when enableCrossContainerPreview is true
66
+ case "MOVE_TO_CONTAINER_END": {
67
+ const { dragId, newContainer } = action.payload;
68
+ const newItems = [...state.items];
69
+ const draggedItem = newItems.find((item) => item && item.id === dragId);
70
+
71
+ if (!draggedItem) return state;
72
+
73
+ const draggedIndex = newItems.indexOf(draggedItem);
74
+ if (draggedIndex === -1) return state;
75
+
76
+ const updatedItem = { ...draggedItem, container: newContainer };
77
+
78
+ // Remove from current position
79
+ newItems.splice(draggedIndex, 1);
80
+
81
+ // Insert at end of target container
82
+ const lastIndexInContainer = newItems
83
+ .map((item) => item && item.container)
84
+ .lastIndexOf(newContainer);
85
+
86
+ if (lastIndexInContainer === -1) {
87
+ newItems.push(updatedItem);
88
+ } else {
89
+ newItems.splice(lastIndexInContainer + 1, 0, updatedItem);
90
+ }
91
+
92
+ return { ...state, items: newItems };
93
+ }
94
+
95
+ // Reset item back to its original container and position (e.g., when drag ends without valid drop)
96
+ case "RESET_DRAG_CONTAINER": {
97
+ const { itemId, originalContainer, originalIndex } = action.payload;
98
+ const newItems = [...state.items];
99
+ const draggedItem = newItems.find(item => item && item.id === itemId);
100
+
101
+ if (!draggedItem) return state;
102
+
103
+ const currentIndex = newItems.indexOf(draggedItem);
104
+
105
+ // Remove from current position
106
+ newItems.splice(currentIndex, 1);
107
+
108
+ // Restore container property and insert at original index
109
+ const restoredItem = { ...draggedItem, container: originalContainer };
110
+
111
+ // Insert at original index, or at end if index is invalid
112
+ if (originalIndex !== undefined && originalIndex >= 0) {
113
+ newItems.splice(originalIndex, 0, restoredItem);
114
+ } else {
115
+ newItems.push(restoredItem);
116
+ }
117
+
118
+ return {
119
+ ...state,
120
+ items: newItems
121
+ };
122
+ }
123
+
42
124
  default:
43
125
  return state;
44
126
  }
@@ -61,9 +143,34 @@ export const DraggableProvider = ({
61
143
  onDrop,
62
144
  onDragOver,
63
145
  dropZone = { type: 'ghost', color: 'neutral', direction: 'vertical' },
64
- providerId = 'default', // fallback provided for backward compatibility, so this does not become a required prop
146
+ providerId = 'default', // fallback provided for backward compatibility
147
+ // Opt-in flag for cross-container preview
148
+ enableCrossContainerPreview = false,
65
149
  }: DraggableProviderType) => {
66
150
  const [state, dispatch] = useReducer(reducer, initialState);
151
+
152
+ // Track drag state for global listener
153
+ const dragStateRef = useRef<{
154
+ isDragging: boolean;
155
+ draggedItemId: string;
156
+ originalContainer: string;
157
+ originalIndex: number;
158
+ currentContainer: string;
159
+ dropOccurred: boolean;
160
+ }>({
161
+ isDragging: false,
162
+ draggedItemId: '',
163
+ originalContainer: '',
164
+ originalIndex: -1,
165
+ currentContainer: '',
166
+ dropOccurred: false,
167
+ });
168
+
169
+ // Track current state for use in gated event listeners (avoid stale closures)
170
+ const stateRef = useRef(state);
171
+ useEffect(() => {
172
+ stateRef.current = state;
173
+ }, [state]);
67
174
 
68
175
  // Parse dropZone prop - handle both string format (backward compatibility) and object format
69
176
  let dropZoneType = 'ghost';
@@ -93,7 +200,209 @@ export const DraggableProvider = ({
93
200
  onReorder(state.items);
94
201
  }, [state.items]);
95
202
 
203
+ // Monitor for failed drops by detecting mouse/pointer release during drag (this is needed for cross container preview)
204
+ useEffect(() => {
205
+ if (!enableCrossContainerPreview) return;
206
+
207
+ // Allow drops anywhere on the document by preventing default dragover
208
+ const handleGlobalDragOver = (e: DragEvent) => {
209
+ if (dragStateRef.current.isDragging) {
210
+ e.preventDefault();
211
+ }
212
+ };
213
+
214
+ // Handle drops anywhere on the document (including non-container areas)
215
+ const handleGlobalDrop = (e: DragEvent) => {
216
+ if (!dragStateRef.current.isDragging) return;
217
+
218
+ // If a container already handled the drop, don't process again
219
+ if (dragStateRef.current.dropOccurred) return;
220
+
221
+ e.preventDefault();
222
+
223
+ // If we reach here, it means the drop was NOT on a valid container
224
+ // (otherwise the container's handleDrop would have set dropOccurred = true)
225
+ // So we should ALWAYS reset to original container for invalid drops
226
+ const originalContainer = dragStateRef.current.originalContainer;
227
+
228
+ dispatch({
229
+ type: 'RESET_DRAG_CONTAINER',
230
+ payload: {
231
+ itemId: dragStateRef.current.draggedItemId,
232
+ originalContainer: originalContainer,
233
+ originalIndex: dragStateRef.current.originalIndex,
234
+ },
235
+ });
236
+
237
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
238
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
239
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
240
+
241
+ // Clear drag state
242
+ dragStateRef.current = {
243
+ isDragging: false,
244
+ draggedItemId: '',
245
+ originalContainer: '',
246
+ originalIndex: -1,
247
+ currentContainer: '',
248
+ dropOccurred: false,
249
+ };
250
+ };
251
+
252
+ const handleGlobalMouseUp = () => {
253
+ // If we're dragging and mouse is released, wait a bit to see if drop occurs
254
+ if (dragStateRef.current.isDragging) {
255
+ setTimeout(() => {
256
+ const currentContainer = dragStateRef.current.currentContainer;
257
+
258
+ // If drop still hasn't occurred, check if item is in a different container
259
+ if (dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
260
+ // If item is in a different container than original, treat it as a successful drop
261
+ if (currentContainer && currentContainer !== dragStateRef.current.originalContainer) {
262
+ // Trigger onDrop callback with the current container
263
+ if (onDrop) {
264
+ const draggedItem = stateRef.current.items.find(item => item && item.id === dragStateRef.current.draggedItemId);
265
+ const updatedItem = draggedItem ? { ...draggedItem, container: currentContainer } : null;
266
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === currentContainer);
267
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === dragStateRef.current.draggedItemId);
268
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
269
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
270
+
271
+ onDrop(
272
+ dragStateRef.current.draggedItemId,
273
+ currentContainer,
274
+ dragStateRef.current.originalContainer,
275
+ updatedItem,
276
+ itemAbove,
277
+ itemBelow
278
+ );
279
+ }
280
+ } else {
281
+ dispatch({
282
+ type: 'RESET_DRAG_CONTAINER',
283
+ payload: {
284
+ itemId: dragStateRef.current.draggedItemId,
285
+ originalContainer: dragStateRef.current.originalContainer,
286
+ originalIndex: dragStateRef.current.originalIndex,
287
+ },
288
+ });
289
+ }
290
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
291
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
292
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
293
+
294
+ // Clear drag state
295
+ dragStateRef.current = {
296
+ isDragging: false,
297
+ draggedItemId: '',
298
+ originalContainer: '',
299
+ originalIndex: -1,
300
+ currentContainer: '',
301
+ dropOccurred: false,
302
+ };
303
+ }
304
+ }, 50); // Small delay to let drop event fire if it's going to
305
+ }
306
+ };
307
+
308
+ // Detect when drag leaves document boundaries
309
+ const handleDragLeave = (e: DragEvent) => {
310
+ // Check if we're leaving the document (relatedTarget will be null)
311
+ if (!e.relatedTarget && dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
312
+ // Drag left the document: reset to original container immediately
313
+ dispatch({
314
+ type: 'RESET_DRAG_CONTAINER',
315
+ payload: {
316
+ itemId: dragStateRef.current.draggedItemId,
317
+ originalContainer: dragStateRef.current.originalContainer,
318
+ originalIndex: dragStateRef.current.originalIndex,
319
+ },
320
+ });
321
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
322
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
323
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
324
+
325
+ // Clear drag state
326
+ dragStateRef.current = {
327
+ isDragging: false,
328
+ draggedItemId: '',
329
+ originalContainer: '',
330
+ originalIndex: -1,
331
+ currentContainer: '',
332
+ dropOccurred: false,
333
+ };
334
+ }
335
+ };
336
+
337
+ document.addEventListener('dragover', handleGlobalDragOver);
338
+ document.addEventListener('drop', handleGlobalDrop);
339
+ document.addEventListener('dragleave', handleDragLeave);
340
+ document.addEventListener('mouseup', handleGlobalMouseUp);
341
+ document.addEventListener('pointerup', handleGlobalMouseUp);
342
+
343
+ return () => {
344
+ document.removeEventListener('dragover', handleGlobalDragOver);
345
+ document.removeEventListener('drop', handleGlobalDrop);
346
+ document.removeEventListener('dragleave', handleDragLeave);
347
+ document.removeEventListener('mouseup', handleGlobalMouseUp);
348
+ document.removeEventListener('pointerup', handleGlobalMouseUp);
349
+ };
350
+ }, [enableCrossContainerPreview]);
351
+
352
+ // Detect when dragging stops (isDragging goes from truthy to empty)
353
+ const prevIsDraggingRef = useRef(state.isDragging);
354
+
355
+ useEffect(() => {
356
+ if (!enableCrossContainerPreview) return;
357
+
358
+ const wasDragging = prevIsDraggingRef.current;
359
+ const isNowDragging = state.isDragging;
360
+
361
+ // Drag just ended (was dragging, now not)
362
+ if (wasDragging && !isNowDragging) {
363
+
364
+ // If drop didn't occur, reset to original container
365
+ if (!dragStateRef.current.dropOccurred && dragStateRef.current.draggedItemId) {
366
+ dispatch({
367
+ type: 'RESET_DRAG_CONTAINER',
368
+ payload: {
369
+ itemId: dragStateRef.current.draggedItemId,
370
+ originalContainer: dragStateRef.current.originalContainer,
371
+ originalIndex: dragStateRef.current.originalIndex,
372
+ },
373
+ });
374
+ }
375
+
376
+ // Clear drag state
377
+ dragStateRef.current = {
378
+ isDragging: false,
379
+ draggedItemId: '',
380
+ originalContainer: '',
381
+ originalIndex: -1,
382
+ currentContainer: '',
383
+ dropOccurred: false,
384
+ };
385
+ }
386
+
387
+ prevIsDraggingRef.current = isNowDragging;
388
+ }, [state.isDragging, enableCrossContainerPreview]);
389
+
96
390
  const handleDragStart = (id: string, container: string) => {
391
+ // Track drag in ref for global listener
392
+ if (enableCrossContainerPreview) {
393
+ // Find the original index of the item
394
+ const originalIndex = state.items.findIndex(item => item && item.id === id);
395
+
396
+ dragStateRef.current = {
397
+ isDragging: true,
398
+ draggedItemId: id,
399
+ originalContainer: container,
400
+ originalIndex: originalIndex,
401
+ currentContainer: container,
402
+ dropOccurred: false,
403
+ };
404
+ }
405
+
97
406
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: id, initialGroup: container, originId: providerId } });
98
407
  dispatch({ type: 'SET_IS_DRAGGING', payload: id });
99
408
  if (onDragStart) onDragStart(id, container);
@@ -103,17 +412,93 @@ export const DraggableProvider = ({
103
412
  if (state.dragData.originId !== providerId) return; // Ignore drag events from other providers
104
413
 
105
414
  if (state.dragData.id !== id) {
106
- dispatch({ type: 'REORDER_ITEMS', payload: { dragId: state.dragData.id, targetId: id } });
107
- dispatch({ type: 'SET_DRAG_DATA', payload: { id: state.dragData.id, initialGroup: container, originId: providerId } });
415
+ if (enableCrossContainerPreview) {
416
+ // Used only when enableCrossContainerPreview is true
417
+ const draggedItem = state.items.find(
418
+ (item) => item && item.id === state.dragData.id
419
+ );
420
+ const currentContainer =
421
+ draggedItem && draggedItem.container
422
+ ? draggedItem.container
423
+ : state.dragData.initialGroup;
424
+
425
+ const isCrossContainer =
426
+ currentContainer !== container &&
427
+ (currentContainer !== undefined || container !== undefined);
428
+
429
+ if (isCrossContainer) {
430
+ dispatch({
431
+ type: "REORDER_ITEMS_CROSS_CONTAINER",
432
+ payload: {
433
+ dragId: state.dragData.id,
434
+ targetId: id,
435
+ newContainer: container,
436
+ },
437
+ });
438
+ // Update current container in ref
439
+ if (enableCrossContainerPreview) {
440
+ dragStateRef.current.currentContainer = container;
441
+ }
442
+ } else {
443
+ // Same container: keep original behavior
444
+ dispatch({
445
+ type: "REORDER_ITEMS",
446
+ payload: { dragId: state.dragData.id, targetId: id },
447
+ });
448
+ }
449
+ } else {
450
+ // Original behavior (no preview across containers)
451
+ dispatch({type: "REORDER_ITEMS", payload: { dragId: state.dragData.id, targetId: id }});
452
+ }
453
+
454
+ // When enableCrossContainerPreview is true, preserve the original initialGroup
455
+ // Otherwise, update it to track the current container
456
+ const newInitialGroup = enableCrossContainerPreview ? state.dragData.initialGroup : container;
457
+ dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: newInitialGroup, originId: providerId}});
108
458
  }
109
459
  if (onDragEnter) onDragEnter(id, container);
110
460
  };
111
461
 
112
462
  const handleDragEnd = () => {
463
+ const draggedItemId = state.dragData.id;
464
+ const originalContainer = state.dragData.initialGroup;
465
+
466
+ // If enableCrossContainerPreview is true and no drop occurred, reset item to original container
467
+ if (enableCrossContainerPreview && !dragStateRef.current.dropOccurred && draggedItemId && originalContainer) {
468
+ dispatch({ type: 'RESET_DRAG_CONTAINER', payload: { itemId: draggedItemId, originalContainer, originalIndex: dragStateRef.current.originalIndex } });
469
+ }
470
+
113
471
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
114
472
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
115
473
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
116
- if (onDragEnd) onDragEnd();
474
+
475
+ // Only call onDragEnd if drop didn't already occur (for enableCrossContainerPreview)
476
+ // If drop occurred, handleDrop or global drop handler already called onDragEnd
477
+ if (enableCrossContainerPreview && dragStateRef.current.dropOccurred) {
478
+ return;
479
+ }
480
+
481
+ if (onDragEnd) {
482
+ if (!enableCrossContainerPreview) {
483
+ onDragEnd();
484
+ } else {
485
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
486
+ const finalContainer = draggedItem ? draggedItem.container : originalContainer;
487
+
488
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === finalContainer);
489
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
490
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
491
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
492
+
493
+ onDragEnd(
494
+ draggedItemId,
495
+ finalContainer,
496
+ originalContainer,
497
+ itemAbove,
498
+ itemBelow
499
+ );
500
+ }
501
+ }
117
502
  };
118
503
 
119
504
  const changeCategory = (itemId: string, container: string) => {
@@ -123,10 +508,60 @@ export const DraggableProvider = ({
123
508
  const handleDrop = (container: string) => {
124
509
  if (state.dragData.originId !== providerId) return; // Ignore drop events from other providers
125
510
 
511
+ const draggedItemId = state.dragData.id;
512
+ const originalContainer = state.dragData.initialGroup;
513
+
514
+ // Mark drop as successful in ref for global listener
515
+ if (enableCrossContainerPreview) {
516
+ dragStateRef.current.dropOccurred = true;
517
+ }
518
+
519
+ // Gather data for callbacks BEFORE clearing state
520
+ const isCrossContainer = container !== originalContainer;
521
+ let callbackData = null;
522
+
523
+ if (enableCrossContainerPreview) {
524
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
525
+ const updatedItem = draggedItem ? { ...draggedItem, container } : null;
526
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === container);
527
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
528
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
529
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
530
+
531
+ callbackData = { updatedItem, itemAbove, itemBelow };
532
+ }
533
+
126
534
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
127
535
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
536
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
128
537
  changeCategory(state.dragData.id, container);
129
- if (onDrop) onDrop(container);
538
+
539
+ if (onDrop) {
540
+ if (!enableCrossContainerPreview) {
541
+ onDrop(container);
542
+ } else {
543
+ onDrop(
544
+ draggedItemId,
545
+ container,
546
+ originalContainer,
547
+ callbackData.updatedItem,
548
+ callbackData.itemAbove,
549
+ callbackData.itemBelow
550
+ );
551
+ }
552
+ }
553
+
554
+ // Trigger onDragEnd ONLY for cross-container drops (dragend doesn't fire reliably in that case)
555
+ // For same-container drops, handleDragEnd will be called normally
556
+ if (enableCrossContainerPreview && isCrossContainer && onDragEnd && callbackData) {
557
+ onDragEnd(
558
+ draggedItemId,
559
+ container,
560
+ originalContainer,
561
+ callbackData.itemAbove,
562
+ callbackData.itemBelow
563
+ );
564
+ }
130
565
  };
131
566
 
132
567
  const handleDragOver = (e: Event, container: string) => {
@@ -134,6 +569,22 @@ export const DraggableProvider = ({
134
569
 
135
570
  e.preventDefault();
136
571
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: container });
572
+
573
+ if (enableCrossContainerPreview && state.dragData.id) {
574
+ // Only when enableCrossContainerPreview is true: when hovering over a different container, move item to end
575
+ const draggedItem = state.items.find(
576
+ (item) => item && item.id === state.dragData.id
577
+ );
578
+ if (draggedItem && draggedItem.container !== container) {
579
+ dispatch({
580
+ type: "MOVE_TO_CONTAINER_END",
581
+ payload: { dragId: state.dragData.id, newContainer: container },
582
+ });
583
+ // Update current container in ref
584
+ dragStateRef.current.currentContainer = container;
585
+ }
586
+ }
587
+
137
588
  if (onDragOver) onDragOver(e, container);
138
589
  };
139
590
 
@@ -157,4 +608,4 @@ export const DraggableProvider = ({
157
608
  return (
158
609
  <DragContext.Provider value={contextValue}>{children}</DragContext.Provider>
159
610
  );
160
- };
611
+ };
@@ -18,8 +18,12 @@ export type ActionType =
18
18
  } }
19
19
  | { type: 'SET_IS_DRAGGING'; payload: string }
20
20
  | { type: 'SET_ACTIVE_CONTAINER'; payload: string }
21
+ | { type: 'SET_CROSS_CONTAINER_PREVIEW'; payload: boolean }
21
22
  | { type: 'CHANGE_CATEGORY'; payload: { itemId: string; container: string } }
22
- | { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } };
23
+ | { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } }
24
+ | { type: 'REORDER_ITEMS_CROSS_CONTAINER'; payload: { dragId: string; targetId: string; newContainer: string } }
25
+ | { type: 'MOVE_TO_CONTAINER_END'; payload: { dragId: string; newContainer: string } }
26
+ | { type: 'RESET_DRAG_CONTAINER'; payload: { itemId: string; originalContainer: string, originalIndex: number } };
23
27
 
24
28
  export interface DropZoneConfig {
25
29
  type?: 'ghost' | 'outline' | 'shadow' | 'line';
@@ -33,9 +37,10 @@ export type ActionType =
33
37
  onReorder: (items: ItemType[]) => void;
34
38
  onDragStart?: (id: string, container: string) => void;
35
39
  onDragEnter?: (id: string, container: string) => void;
36
- onDragEnd?: () => void;
37
- onDrop?: (container: string) => void;
40
+ onDragEnd?: (...args: any[]) => void;
41
+ onDrop?: (...args: any[]) => void;
38
42
  onDragOver?: (e: Event, container: string) => void;
39
43
  dropZone?: DropZoneConfig | string; // Can accept string for backward compatibility
40
44
  providerId?: string;
45
+ enableCrossContainerPreview?: boolean;
41
46
  }