@buoy-gg/core 1.7.8 → 2.1.2
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/lib/commonjs/Buoy.js +104 -0
- package/lib/commonjs/floatingMenu/AppHost.js +2 -2
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +18 -279
- package/lib/commonjs/floatingMenu/FloatingDevTools.js +8 -1
- package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +1 -2
- package/lib/commonjs/floatingMenu/FloatingMenu.js +4 -4
- package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +2 -2
- package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +15 -0
- package/lib/commonjs/floatingMenu/dial/DialDevTools.js +4 -4
- package/lib/commonjs/floatingMenu/floatingTools.js +268 -74
- package/lib/commonjs/index.js +24 -8
- package/lib/module/Buoy.js +100 -0
- package/lib/module/floatingMenu/AppHost.js +3 -3
- package/lib/module/floatingMenu/DevToolsSettingsModal.js +20 -281
- package/lib/module/floatingMenu/FloatingDevTools.js +8 -1
- package/lib/module/floatingMenu/FloatingDevTools.web.js +1 -2
- package/lib/module/floatingMenu/FloatingMenu.js +5 -5
- package/lib/module/floatingMenu/MinimizedToolsContext.js +3 -3
- package/lib/module/floatingMenu/autoDiscoverPresets.js +15 -0
- package/lib/module/floatingMenu/dial/DialDevTools.js +5 -5
- package/lib/module/floatingMenu/floatingTools.js +269 -75
- package/lib/module/index.js +3 -1
- package/lib/typescript/commonjs/Buoy.d.ts +79 -0
- package/lib/typescript/commonjs/Buoy.d.ts.map +1 -0
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +25 -5
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts +1 -1
- package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/module/Buoy.d.ts +79 -0
- package/lib/typescript/module/Buoy.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +25 -5
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/floatingTools.d.ts +1 -1
- package/lib/typescript/module/floatingMenu/floatingTools.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -85,7 +85,13 @@ const STORAGE_KEYS = {
|
|
|
85
85
|
BUBBLE_POSITION_Y: "@react_buoy_bubble_position_y"
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// Position constants
|
|
89
|
+
const VISIBLE_HANDLE_WIDTH = 32;
|
|
90
|
+
const EDGE_PADDING = 20;
|
|
91
|
+
const LAYOUT_SETTLE_DELAY_MS = 150;
|
|
92
|
+
|
|
93
|
+
/** Helper to get current value from Animated.Value without triggering updates */
|
|
94
|
+
const getAnimatedValue = value => value.__getValue();
|
|
89
95
|
|
|
90
96
|
// =============================
|
|
91
97
|
// Position persistence hook
|
|
@@ -104,8 +110,9 @@ const STORAGE_KEYS = {
|
|
|
104
110
|
* @param props.enabled - Whether position persistence is enabled
|
|
105
111
|
* @param props.visibleHandleWidth - Width of visible handle when bubble is hidden
|
|
106
112
|
* @param props.listenersSuspended - Pause automatic listeners without disabling manual saves
|
|
113
|
+
* @param props.onPositionLoaded - Callback fired once position is loaded and set
|
|
107
114
|
*
|
|
108
|
-
* @returns Object containing position management functions
|
|
115
|
+
* @returns Object containing position management functions and loading state
|
|
109
116
|
*
|
|
110
117
|
* @performance Uses debounced saving to avoid excessive storage operations
|
|
111
118
|
* @performance Validates positions against screen boundaries and safe areas
|
|
@@ -116,33 +123,66 @@ function useFloatingToolsPosition({
|
|
|
116
123
|
bubbleHeight = 32,
|
|
117
124
|
enabled = true,
|
|
118
125
|
visibleHandleWidth = 32,
|
|
119
|
-
listenersSuspended = false
|
|
126
|
+
listenersSuspended = false,
|
|
127
|
+
onPositionLoaded
|
|
120
128
|
}) {
|
|
121
|
-
|
|
129
|
+
// Use state instead of ref so we can react to changes and prevent races
|
|
130
|
+
const [isInitialized, setIsInitialized] = (0, _react.useState)(false);
|
|
122
131
|
const saveTimeoutRef = (0, _react.useRef)(undefined);
|
|
132
|
+
// Track if component is mounted to prevent state updates after unmount
|
|
133
|
+
const isMountedRef = (0, _react.useRef)(true);
|
|
134
|
+
|
|
135
|
+
// Cleanup on unmount - cancel any pending saves
|
|
136
|
+
(0, _react.useEffect)(() => {
|
|
137
|
+
isMountedRef.current = true;
|
|
138
|
+
return () => {
|
|
139
|
+
isMountedRef.current = false;
|
|
140
|
+
// Clear any pending debounced saves on unmount to prevent saving stale/invalid positions
|
|
141
|
+
if (saveTimeoutRef.current) {
|
|
142
|
+
clearTimeout(saveTimeoutRef.current);
|
|
143
|
+
saveTimeoutRef.current = undefined;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
123
147
|
const savePosition = (0, _react.useCallback)(async (x, y) => {
|
|
124
148
|
if (!enabled) return;
|
|
149
|
+
// Don't save if component unmounted or position is clearly invalid
|
|
150
|
+
if (!isMountedRef.current) return;
|
|
151
|
+
// Guard against saving invalid positions (e.g., during HMR with stale dimensions)
|
|
152
|
+
const {
|
|
153
|
+
width: screenWidth
|
|
154
|
+
} = _reactNative.Dimensions.get("window");
|
|
155
|
+
if (screenWidth <= 0 || x < 0 || y < 0) return;
|
|
125
156
|
try {
|
|
126
|
-
await Promise.all([
|
|
157
|
+
await Promise.all([_sharedUi.persistentStorage.setItem(STORAGE_KEYS.BUBBLE_POSITION_X, x.toString()), _sharedUi.persistentStorage.setItem(STORAGE_KEYS.BUBBLE_POSITION_Y, y.toString())]);
|
|
127
158
|
} catch (error) {
|
|
128
159
|
// Failed to save position - continue without persistence
|
|
129
160
|
}
|
|
130
161
|
}, [enabled]);
|
|
131
162
|
const debouncedSavePosition = (0, _react.useCallback)((x, y) => {
|
|
163
|
+
// Don't schedule saves if unmounted
|
|
164
|
+
if (!isMountedRef.current) return;
|
|
132
165
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
|
133
|
-
saveTimeoutRef.current = setTimeout(() =>
|
|
166
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
167
|
+
// Double-check mount status before saving
|
|
168
|
+
if (isMountedRef.current) {
|
|
169
|
+
savePosition(x, y);
|
|
170
|
+
}
|
|
171
|
+
}, 500);
|
|
134
172
|
}, [savePosition]);
|
|
135
173
|
const loadPosition = (0, _react.useCallback)(async () => {
|
|
136
174
|
if (!enabled) return null;
|
|
137
175
|
try {
|
|
138
|
-
const [xStr, yStr] = await Promise.all([
|
|
176
|
+
const [xStr, yStr] = await Promise.all([_sharedUi.persistentStorage.getItem(STORAGE_KEYS.BUBBLE_POSITION_X), _sharedUi.persistentStorage.getItem(STORAGE_KEYS.BUBBLE_POSITION_Y)]);
|
|
139
177
|
if (xStr !== null && yStr !== null) {
|
|
140
178
|
const x = parseFloat(xStr);
|
|
141
179
|
const y = parseFloat(yStr);
|
|
142
|
-
if (!Number.isNaN(x) && !Number.isNaN(y))
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
if (!Number.isNaN(x) && !Number.isNaN(y)) {
|
|
181
|
+
return {
|
|
182
|
+
x,
|
|
183
|
+
y
|
|
184
|
+
};
|
|
185
|
+
}
|
|
146
186
|
}
|
|
147
187
|
} catch (error) {
|
|
148
188
|
// Failed to load position - use default
|
|
@@ -155,62 +195,121 @@ function useFloatingToolsPosition({
|
|
|
155
195
|
height: screenHeight
|
|
156
196
|
} = _reactNative.Dimensions.get("window");
|
|
157
197
|
const safeArea = (0, _sharedUi.getSafeAreaInsets)();
|
|
198
|
+
|
|
199
|
+
// Guard against invalid screen dimensions (can happen during HMR)
|
|
200
|
+
// Return the position as-is but mark as invalid so caller knows not to trust it
|
|
201
|
+
if (screenWidth <= 0 || screenHeight <= 0) {
|
|
202
|
+
return {
|
|
203
|
+
...position,
|
|
204
|
+
isValid: false
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
158
208
|
// Prevent going off left, top, and bottom edges with safe area
|
|
159
209
|
// Allow pushing off-screen to the right so only the grab handle remains visible
|
|
160
|
-
const minX = safeArea.left;
|
|
161
|
-
const maxX = screenWidth - visibleHandleWidth;
|
|
162
|
-
// Add
|
|
163
|
-
const minY = safeArea.top +
|
|
210
|
+
const minX = safeArea.left;
|
|
211
|
+
const maxX = screenWidth - visibleHandleWidth;
|
|
212
|
+
// Add padding below the safe area top to ensure bubble doesn't go behind notch
|
|
213
|
+
const minY = safeArea.top + EDGE_PADDING;
|
|
164
214
|
const maxY = screenHeight - bubbleHeight - safeArea.bottom; // Respect safe area bottom
|
|
165
215
|
const clamped = {
|
|
166
216
|
x: Math.max(minX, Math.min(position.x, maxX)),
|
|
167
|
-
y: Math.max(minY, Math.min(position.y, maxY))
|
|
217
|
+
y: Math.max(minY, Math.min(position.y, maxY)),
|
|
218
|
+
isValid: true
|
|
168
219
|
};
|
|
169
220
|
return clamped;
|
|
170
221
|
}, [visibleHandleWidth, bubbleHeight]);
|
|
171
222
|
(0, _react.useEffect)(() => {
|
|
172
|
-
if (!enabled || isInitialized
|
|
223
|
+
if (!enabled || isInitialized) return;
|
|
224
|
+
let cancelled = false;
|
|
173
225
|
const restore = async () => {
|
|
174
226
|
const saved = await loadPosition();
|
|
227
|
+
|
|
228
|
+
// Check if cancelled or unmounted before updating state
|
|
229
|
+
if (cancelled || !isMountedRef.current) return;
|
|
230
|
+
const {
|
|
231
|
+
width: screenWidth,
|
|
232
|
+
height: screenHeight
|
|
233
|
+
} = _reactNative.Dimensions.get("window");
|
|
234
|
+
const safeArea = (0, _sharedUi.getSafeAreaInsets)();
|
|
235
|
+
|
|
236
|
+
// If dimensions are invalid (HMR scenario), wait and retry
|
|
237
|
+
if (screenWidth <= 0 || screenHeight <= 0) {
|
|
238
|
+
// Schedule a retry after a short delay
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
if (isMountedRef.current && !cancelled) {
|
|
241
|
+
restore();
|
|
242
|
+
}
|
|
243
|
+
}, 100);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let finalPosition;
|
|
247
|
+
let wasHidden = false;
|
|
175
248
|
if (saved) {
|
|
176
249
|
const validated = validatePosition(saved);
|
|
250
|
+
|
|
251
|
+
// If validation returned invalid, retry later
|
|
252
|
+
if (!validated.isValid) {
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
if (isMountedRef.current && !cancelled) {
|
|
255
|
+
restore();
|
|
256
|
+
}
|
|
257
|
+
}, 100);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
177
261
|
// Check if the saved position is out of bounds
|
|
178
262
|
const wasOutOfBounds = Math.abs(saved.x - validated.x) > 5 || Math.abs(saved.y - validated.y) > 5;
|
|
179
263
|
if (wasOutOfBounds) {
|
|
180
264
|
// Save the corrected position
|
|
181
265
|
await savePosition(validated.x, validated.y);
|
|
182
266
|
}
|
|
183
|
-
|
|
267
|
+
finalPosition = {
|
|
268
|
+
x: validated.x,
|
|
269
|
+
y: validated.y
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Check if loaded in hidden state (bubble pushed to right edge)
|
|
273
|
+
wasHidden = validated.x >= screenWidth - visibleHandleWidth - 5;
|
|
184
274
|
} else {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const defaultY = Math.max(safeArea.top + 20, Math.min(100, screenHeight - bubbleHeight - safeArea.bottom));
|
|
191
|
-
animatedPosition.setValue({
|
|
192
|
-
x: screenWidth - bubbleWidth - 20,
|
|
193
|
-
y: defaultY // Ensure it's within safe area bounds
|
|
194
|
-
});
|
|
275
|
+
const defaultY = Math.max(safeArea.top + EDGE_PADDING, Math.min(100, screenHeight - bubbleHeight - safeArea.bottom));
|
|
276
|
+
finalPosition = {
|
|
277
|
+
x: screenWidth - bubbleWidth - EDGE_PADDING,
|
|
278
|
+
y: defaultY
|
|
279
|
+
};
|
|
195
280
|
}
|
|
196
|
-
|
|
281
|
+
|
|
282
|
+
// Final mount check before updating
|
|
283
|
+
if (cancelled || !isMountedRef.current) return;
|
|
284
|
+
animatedPosition.setValue(finalPosition);
|
|
285
|
+
setIsInitialized(true);
|
|
286
|
+
onPositionLoaded?.(finalPosition, wasHidden);
|
|
197
287
|
};
|
|
198
288
|
restore();
|
|
199
|
-
|
|
289
|
+
return () => {
|
|
290
|
+
cancelled = true;
|
|
291
|
+
};
|
|
292
|
+
}, [enabled, isInitialized, animatedPosition, loadPosition, validatePosition, savePosition, bubbleWidth, bubbleHeight, visibleHandleWidth, onPositionLoaded]);
|
|
293
|
+
|
|
294
|
+
// Listener effect - now properly depends on isInitialized state
|
|
200
295
|
(0, _react.useEffect)(() => {
|
|
201
|
-
|
|
296
|
+
// Don't attach listener until position is loaded
|
|
297
|
+
if (!enabled || !isInitialized || listenersSuspended) return;
|
|
202
298
|
const listener = animatedPosition.addListener(value => {
|
|
203
299
|
debouncedSavePosition(value.x, value.y);
|
|
204
300
|
});
|
|
205
301
|
return () => {
|
|
206
302
|
animatedPosition.removeListener(listener);
|
|
207
|
-
|
|
303
|
+
// Clear debounce timer when listener is removed
|
|
304
|
+
if (saveTimeoutRef.current) {
|
|
305
|
+
clearTimeout(saveTimeoutRef.current);
|
|
306
|
+
saveTimeoutRef.current = undefined;
|
|
307
|
+
}
|
|
208
308
|
};
|
|
209
|
-
}, [enabled, listenersSuspended, animatedPosition, debouncedSavePosition]);
|
|
309
|
+
}, [enabled, isInitialized, listenersSuspended, animatedPosition, debouncedSavePosition]);
|
|
210
310
|
return {
|
|
211
311
|
savePosition,
|
|
212
|
-
|
|
213
|
-
isInitialized: isInitialized.current
|
|
312
|
+
isInitialized
|
|
214
313
|
};
|
|
215
314
|
}
|
|
216
315
|
|
|
@@ -372,7 +471,6 @@ function FloatingTools({
|
|
|
372
471
|
}) {
|
|
373
472
|
// Animated position and drag state
|
|
374
473
|
const animatedPosition = (0, _react.useRef)(new _reactNative.Animated.ValueXY()).current;
|
|
375
|
-
const saveTimeoutRef = (0, _react.useRef)(null);
|
|
376
474
|
const [isDragging, setIsDragging] = (0, _react.useState)(false);
|
|
377
475
|
const [bubbleSize, setBubbleSize] = (0, _react.useState)({
|
|
378
476
|
width: 100,
|
|
@@ -391,26 +489,44 @@ function FloatingTools({
|
|
|
391
489
|
|
|
392
490
|
// Track previous pushToSide value to detect transitions
|
|
393
491
|
const prevPushToSideRef = (0, _react.useRef)(pushToSide);
|
|
492
|
+
|
|
493
|
+
// Track previous bubble width for auto-adjustment when tools are added/removed
|
|
494
|
+
const prevBubbleWidthRef = (0, _react.useRef)(null);
|
|
495
|
+
|
|
496
|
+
// Track if layout has settled (after initial render cycle completes)
|
|
497
|
+
const [isLayoutSettled, setIsLayoutSettled] = (0, _react.useState)(false);
|
|
394
498
|
const safeAreaInsets = (0, _sharedUi.useSafeAreaInsets)();
|
|
395
499
|
const {
|
|
396
500
|
width: screenWidth,
|
|
397
501
|
height: screenHeight
|
|
398
502
|
} = _reactNative.Dimensions.get("window");
|
|
399
503
|
|
|
504
|
+
// Callback when position is loaded - sets hidden state properly without racing
|
|
505
|
+
const handlePositionLoaded = (0, _react.useCallback)((position, wasHidden) => {
|
|
506
|
+
if (wasHidden) {
|
|
507
|
+
setIsHidden(true);
|
|
508
|
+
}
|
|
509
|
+
}, []);
|
|
510
|
+
|
|
400
511
|
// Position persistence (state/IO extracted to hook)
|
|
401
512
|
const {
|
|
402
|
-
savePosition
|
|
513
|
+
savePosition,
|
|
514
|
+
isInitialized: isPositionInitialized
|
|
403
515
|
} = useFloatingToolsPosition({
|
|
404
516
|
animatedPosition,
|
|
405
517
|
bubbleWidth: bubbleSize.width,
|
|
406
518
|
bubbleHeight: bubbleSize.height,
|
|
407
519
|
enabled: enablePositionPersistence,
|
|
408
|
-
visibleHandleWidth:
|
|
409
|
-
listenersSuspended: isDragging
|
|
520
|
+
visibleHandleWidth: VISIBLE_HANDLE_WIDTH,
|
|
521
|
+
listenersSuspended: isDragging,
|
|
522
|
+
onPositionLoaded: handlePositionLoaded
|
|
410
523
|
});
|
|
411
524
|
|
|
412
525
|
// Effect to handle pushToSide prop changes
|
|
413
526
|
(0, _react.useEffect)(() => {
|
|
527
|
+
// Don't process pushToSide until position is initialized (when persistence enabled)
|
|
528
|
+
if (enablePositionPersistence && !isPositionInitialized) return;
|
|
529
|
+
|
|
414
530
|
// Reset user override when pushToSide becomes true (dial/modal opens)
|
|
415
531
|
// This allows auto-hide to work after user manually showed the menu
|
|
416
532
|
if (!prevPushToSideRef.current && pushToSide) {
|
|
@@ -425,15 +541,15 @@ function FloatingTools({
|
|
|
425
541
|
// 4. User hasn't manually restored (userWantsVisibleRef)
|
|
426
542
|
if (pushToSide && !isHidden && !isDragging && !userWantsVisibleRef.current) {
|
|
427
543
|
// Push to side
|
|
428
|
-
const currentX = animatedPosition.x
|
|
429
|
-
const currentY = animatedPosition.y
|
|
544
|
+
const currentX = getAnimatedValue(animatedPosition.x);
|
|
545
|
+
const currentY = getAnimatedValue(animatedPosition.y);
|
|
430
546
|
|
|
431
547
|
// Save current position
|
|
432
548
|
savedPositionRef.current = {
|
|
433
549
|
x: currentX,
|
|
434
550
|
y: currentY
|
|
435
551
|
};
|
|
436
|
-
const hiddenX = screenWidth -
|
|
552
|
+
const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
|
|
437
553
|
isPushedBySideRef.current = true;
|
|
438
554
|
setIsHidden(true);
|
|
439
555
|
_reactNative.Animated.timing(animatedPosition, {
|
|
@@ -468,22 +584,92 @@ function FloatingTools({
|
|
|
468
584
|
});
|
|
469
585
|
}
|
|
470
586
|
}
|
|
471
|
-
}, [pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
|
|
587
|
+
}, [enablePositionPersistence, isPositionInitialized, pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
|
|
588
|
+
|
|
589
|
+
// Hidden state is now set via onPositionLoaded callback - no racing effect needed
|
|
472
590
|
|
|
473
|
-
//
|
|
591
|
+
// Wait for layout to settle before enabling width tracking
|
|
592
|
+
// This prevents false-positive width changes during initial render when
|
|
593
|
+
// the bubble measures multiple times as children render
|
|
474
594
|
(0, _react.useEffect)(() => {
|
|
475
|
-
if (!
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
// Delay check to ensure position is loaded
|
|
484
|
-
const timer = setTimeout(checkHiddenState, 100);
|
|
595
|
+
if (!isPositionInitialized || isLayoutSettled) return;
|
|
596
|
+
const timer = setTimeout(() => {
|
|
597
|
+
// Capture the current width as the baseline AFTER layout has settled
|
|
598
|
+
prevBubbleWidthRef.current = bubbleSize.width;
|
|
599
|
+
setIsLayoutSettled(true);
|
|
600
|
+
}, LAYOUT_SETTLE_DELAY_MS);
|
|
485
601
|
return () => clearTimeout(timer);
|
|
486
|
-
}, [
|
|
602
|
+
}, [isPositionInitialized, isLayoutSettled, bubbleSize.width]);
|
|
603
|
+
|
|
604
|
+
// Auto-adjust position when bubble width changes (tools added/removed)
|
|
605
|
+
// This keeps the bubble's right edge in the same relative position
|
|
606
|
+
(0, _react.useEffect)(() => {
|
|
607
|
+
// Skip if position not initialized yet
|
|
608
|
+
if (!isPositionInitialized) return;
|
|
609
|
+
|
|
610
|
+
// Skip until layout has settled (prevents false-positive width changes during initial render)
|
|
611
|
+
if (!isLayoutSettled) return;
|
|
612
|
+
|
|
613
|
+
// Skip if prevBubbleWidthRef hasn't been set yet
|
|
614
|
+
if (prevBubbleWidthRef.current === null) return;
|
|
615
|
+
|
|
616
|
+
// Skip if currently dragging - let drag handle position
|
|
617
|
+
if (isDragging) {
|
|
618
|
+
prevBubbleWidthRef.current = bubbleSize.width;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const prevWidth = prevBubbleWidthRef.current;
|
|
622
|
+
const newWidth = bubbleSize.width;
|
|
623
|
+
const widthDelta = newWidth - prevWidth;
|
|
624
|
+
|
|
625
|
+
// Skip if no significant change (avoid jitter)
|
|
626
|
+
if (Math.abs(widthDelta) < 2) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Update ref first
|
|
631
|
+
prevBubbleWidthRef.current = newWidth;
|
|
632
|
+
|
|
633
|
+
// If hidden, only adjust the saved position (not the current animated position)
|
|
634
|
+
if (isHidden) {
|
|
635
|
+
if (savedPositionRef.current) {
|
|
636
|
+
const adjustedX = savedPositionRef.current.x - widthDelta;
|
|
637
|
+
// Clamp to valid bounds
|
|
638
|
+
savedPositionRef.current.x = Math.max(safeAreaInsets.left, Math.min(adjustedX, screenWidth - newWidth - EDGE_PADDING));
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Get current position
|
|
644
|
+
const currentX = getAnimatedValue(animatedPosition.x);
|
|
645
|
+
const currentY = getAnimatedValue(animatedPosition.y);
|
|
646
|
+
|
|
647
|
+
// Calculate new X position
|
|
648
|
+
// If bubble grew (positive delta), move left (decrease X)
|
|
649
|
+
// If bubble shrunk (negative delta), move right (increase X)
|
|
650
|
+
const newX = currentX - widthDelta;
|
|
651
|
+
|
|
652
|
+
// Validate new position is within bounds
|
|
653
|
+
const clampedX = Math.max(safeAreaInsets.left, Math.min(newX, screenWidth - VISIBLE_HANDLE_WIDTH));
|
|
654
|
+
|
|
655
|
+
// Only animate if position actually changed
|
|
656
|
+
if (Math.abs(clampedX - currentX) > 1) {
|
|
657
|
+
_reactNative.Animated.timing(animatedPosition, {
|
|
658
|
+
toValue: {
|
|
659
|
+
x: clampedX,
|
|
660
|
+
y: currentY
|
|
661
|
+
},
|
|
662
|
+
duration: 150,
|
|
663
|
+
useNativeDriver: false
|
|
664
|
+
}).start(() => {
|
|
665
|
+
savePosition(clampedX, currentY);
|
|
666
|
+
// Update savedPositionRef so show/hide works correctly
|
|
667
|
+
if (savedPositionRef.current) {
|
|
668
|
+
savedPositionRef.current.x = clampedX;
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}, [bubbleSize.width, isPositionInitialized, isLayoutSettled, isDragging, isHidden, animatedPosition, screenWidth, safeAreaInsets.left, savePosition]);
|
|
487
673
|
|
|
488
674
|
// Default position when persistence disabled or during onboarding
|
|
489
675
|
(0, _react.useEffect)(() => {
|
|
@@ -500,29 +686,19 @@ function FloatingTools({
|
|
|
500
686
|
});
|
|
501
687
|
} else {
|
|
502
688
|
// Default right-side position
|
|
503
|
-
const defaultY = Math.max(safeAreaInsets.top +
|
|
689
|
+
const defaultY = Math.max(safeAreaInsets.top + EDGE_PADDING, Math.min(100, screenHeight - bubbleSize.height - safeAreaInsets.bottom));
|
|
504
690
|
animatedPosition.setValue({
|
|
505
|
-
x: screenWidth - bubbleSize.width -
|
|
691
|
+
x: screenWidth - bubbleSize.width - EDGE_PADDING,
|
|
506
692
|
y: defaultY
|
|
507
693
|
});
|
|
508
694
|
}
|
|
509
695
|
}
|
|
510
696
|
}, [enablePositionPersistence, centerOnboarding, animatedPosition, bubbleSize.width, bubbleSize.height, safeAreaInsets.top, safeAreaInsets.bottom, screenWidth, screenHeight]);
|
|
511
697
|
|
|
512
|
-
// Cleanup timeout on component unmount
|
|
513
|
-
(0, _react.useEffect)(() => {
|
|
514
|
-
return () => {
|
|
515
|
-
if (saveTimeoutRef.current) {
|
|
516
|
-
clearTimeout(saveTimeoutRef.current);
|
|
517
|
-
saveTimeoutRef.current = null;
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
}, []);
|
|
521
|
-
|
|
522
698
|
// Toggle hide/show function
|
|
523
699
|
const toggleHideShow = (0, _react.useCallback)(() => {
|
|
524
|
-
const currentX = animatedPosition.x
|
|
525
|
-
const currentY = animatedPosition.y
|
|
700
|
+
const currentX = getAnimatedValue(animatedPosition.x);
|
|
701
|
+
const currentY = getAnimatedValue(animatedPosition.y);
|
|
526
702
|
|
|
527
703
|
// Check if the menu is visually off-screen (more than half hidden to the right)
|
|
528
704
|
// This handles edge cases where isHidden state might be out of sync with actual position
|
|
@@ -537,7 +713,7 @@ function FloatingTools({
|
|
|
537
713
|
targetY = savedPositionRef.current.y;
|
|
538
714
|
} else {
|
|
539
715
|
// Default visible position if no saved position or saved position is off-screen
|
|
540
|
-
targetX = screenWidth - bubbleSize.width -
|
|
716
|
+
targetX = screenWidth - bubbleSize.width - EDGE_PADDING;
|
|
541
717
|
targetY = currentY;
|
|
542
718
|
}
|
|
543
719
|
|
|
@@ -564,7 +740,7 @@ function FloatingTools({
|
|
|
564
740
|
|
|
565
741
|
// User explicitly wants the menu hidden
|
|
566
742
|
userWantsVisibleRef.current = false;
|
|
567
|
-
const hiddenX = screenWidth -
|
|
743
|
+
const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
|
|
568
744
|
setIsHidden(true);
|
|
569
745
|
_reactNative.Animated.timing(animatedPosition, {
|
|
570
746
|
toValue: {
|
|
@@ -592,8 +768,20 @@ function FloatingTools({
|
|
|
592
768
|
const shouldHide = bubbleMidpoint > screenWidth;
|
|
593
769
|
if (shouldHide) {
|
|
594
770
|
// Animate to hidden position (only grabber visible)
|
|
595
|
-
const hiddenX = screenWidth -
|
|
771
|
+
const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
|
|
596
772
|
setIsHidden(true);
|
|
773
|
+
|
|
774
|
+
// Update savedPositionRef to preserve the Y position when dragging while hidden
|
|
775
|
+
// This ensures that when showing, the bubble restores to the new Y position
|
|
776
|
+
if (savedPositionRef.current) {
|
|
777
|
+
savedPositionRef.current.y = currentY;
|
|
778
|
+
} else {
|
|
779
|
+
// If no saved position exists, create one with default visible X
|
|
780
|
+
savedPositionRef.current = {
|
|
781
|
+
x: screenWidth - bubbleSize.width - EDGE_PADDING,
|
|
782
|
+
y: currentY
|
|
783
|
+
};
|
|
784
|
+
}
|
|
597
785
|
_reactNative.Animated.timing(animatedPosition, {
|
|
598
786
|
toValue: {
|
|
599
787
|
x: hiddenX,
|
|
@@ -606,7 +794,7 @@ function FloatingTools({
|
|
|
606
794
|
});
|
|
607
795
|
} else {
|
|
608
796
|
// Check if we're in hidden state and user is pulling it back
|
|
609
|
-
if (isHidden && currentX < screenWidth -
|
|
797
|
+
if (isHidden && currentX < screenWidth - VISIBLE_HANDLE_WIDTH - 10) {
|
|
610
798
|
setIsHidden(false);
|
|
611
799
|
}
|
|
612
800
|
|
|
@@ -694,6 +882,12 @@ function FloatingTools({
|
|
|
694
882
|
|
|
695
883
|
// Width for the minimized tools stack - match the drag handle width
|
|
696
884
|
const minimizedStackWidth = 32;
|
|
885
|
+
|
|
886
|
+
// Don't render until position is initialized (when persistence enabled)
|
|
887
|
+
// This prevents the bubble from appearing at {0,0} before position loads
|
|
888
|
+
if (enablePositionPersistence && !isPositionInitialized) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
697
891
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
|
|
698
892
|
style: bubbleStyle,
|
|
699
893
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
@@ -743,7 +937,7 @@ function FloatingTools({
|
|
|
743
937
|
elementSize: bubbleSize,
|
|
744
938
|
minPosition: {
|
|
745
939
|
x: safeAreaInsets.left,
|
|
746
|
-
y: safeAreaInsets.top +
|
|
940
|
+
y: safeAreaInsets.top + EDGE_PADDING
|
|
747
941
|
},
|
|
748
942
|
style: dragHandleStyle,
|
|
749
943
|
enabled: true,
|
package/lib/commonjs/index.js
CHANGED
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
var _exportNames = {
|
|
7
|
+
Buoy: true,
|
|
8
|
+
BuoyDefault: true,
|
|
7
9
|
FloatingDevTools: true,
|
|
8
10
|
autoDiscoverPresets: true,
|
|
9
11
|
autoDiscoverPresetsWithCustom: true,
|
|
@@ -27,9 +29,9 @@ var _exportNames = {
|
|
|
27
29
|
getIconSize: true,
|
|
28
30
|
MinimizedToolsStack: true,
|
|
29
31
|
LicenseManager: true,
|
|
32
|
+
DeviceRegistrationModal: true,
|
|
30
33
|
useLicense: true,
|
|
31
34
|
useFeatureAccess: true,
|
|
32
|
-
useSeats: true,
|
|
33
35
|
useIsPro: true,
|
|
34
36
|
setLicenseKey: true,
|
|
35
37
|
isPro: true,
|
|
@@ -47,6 +49,18 @@ Object.defineProperty(exports, "AppOverlay", {
|
|
|
47
49
|
return _AppHost.AppOverlay;
|
|
48
50
|
}
|
|
49
51
|
});
|
|
52
|
+
Object.defineProperty(exports, "Buoy", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
get: function () {
|
|
55
|
+
return _Buoy.Buoy;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(exports, "BuoyDefault", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
get: function () {
|
|
61
|
+
return _Buoy.default;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
50
64
|
Object.defineProperty(exports, "DevToolsSettingsModal", {
|
|
51
65
|
enumerable: true,
|
|
52
66
|
get: function () {
|
|
@@ -59,6 +73,12 @@ Object.defineProperty(exports, "DevToolsVisibilityProvider", {
|
|
|
59
73
|
return _DevToolsVisibilityContext.DevToolsVisibilityProvider;
|
|
60
74
|
}
|
|
61
75
|
});
|
|
76
|
+
Object.defineProperty(exports, "DeviceRegistrationModal", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
get: function () {
|
|
79
|
+
return _license.DeviceRegistrationModal;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
62
82
|
Object.defineProperty(exports, "FloatingDevTools", {
|
|
63
83
|
enumerable: true,
|
|
64
84
|
get: function () {
|
|
@@ -203,18 +223,13 @@ Object.defineProperty(exports, "useMinimizedTools", {
|
|
|
203
223
|
return _MinimizedToolsContext.useMinimizedTools;
|
|
204
224
|
}
|
|
205
225
|
});
|
|
206
|
-
Object.defineProperty(exports, "useSeats", {
|
|
207
|
-
enumerable: true,
|
|
208
|
-
get: function () {
|
|
209
|
-
return _license.useSeats;
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
226
|
Object.defineProperty(exports, "validateDialConfig", {
|
|
213
227
|
enumerable: true,
|
|
214
228
|
get: function () {
|
|
215
229
|
return _defaultConfig.validateDialConfig;
|
|
216
230
|
}
|
|
217
231
|
});
|
|
232
|
+
var _Buoy = _interopRequireWildcard(require("./Buoy.js"));
|
|
218
233
|
var _FloatingDevTools = require("./floatingMenu/FloatingDevTools");
|
|
219
234
|
var _autoDiscoverPresets = require("./floatingMenu/autoDiscoverPresets.js");
|
|
220
235
|
var _defaultConfig = require("./floatingMenu/defaultConfig.js");
|
|
@@ -237,4 +252,5 @@ var _DevToolsVisibilityContext = require("./floatingMenu/DevToolsVisibilityConte
|
|
|
237
252
|
var _ToggleStateManager = require("./floatingMenu/ToggleStateManager.js");
|
|
238
253
|
var _MinimizedToolsContext = require("./floatingMenu/MinimizedToolsContext.js");
|
|
239
254
|
var _MinimizedToolsStack = require("./floatingMenu/MinimizedToolsStack.js");
|
|
240
|
-
var _license = require("@buoy-gg/license");
|
|
255
|
+
var _license = require("@buoy-gg/license");
|
|
256
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|