@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.
Files changed (49) hide show
  1. package/lib/commonjs/Buoy.js +104 -0
  2. package/lib/commonjs/floatingMenu/AppHost.js +2 -2
  3. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +18 -279
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.js +8 -1
  5. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +1 -2
  6. package/lib/commonjs/floatingMenu/FloatingMenu.js +4 -4
  7. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +2 -2
  8. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +15 -0
  9. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +4 -4
  10. package/lib/commonjs/floatingMenu/floatingTools.js +268 -74
  11. package/lib/commonjs/index.js +24 -8
  12. package/lib/module/Buoy.js +100 -0
  13. package/lib/module/floatingMenu/AppHost.js +3 -3
  14. package/lib/module/floatingMenu/DevToolsSettingsModal.js +20 -281
  15. package/lib/module/floatingMenu/FloatingDevTools.js +8 -1
  16. package/lib/module/floatingMenu/FloatingDevTools.web.js +1 -2
  17. package/lib/module/floatingMenu/FloatingMenu.js +5 -5
  18. package/lib/module/floatingMenu/MinimizedToolsContext.js +3 -3
  19. package/lib/module/floatingMenu/autoDiscoverPresets.js +15 -0
  20. package/lib/module/floatingMenu/dial/DialDevTools.js +5 -5
  21. package/lib/module/floatingMenu/floatingTools.js +269 -75
  22. package/lib/module/index.js +3 -1
  23. package/lib/typescript/commonjs/Buoy.d.ts +79 -0
  24. package/lib/typescript/commonjs/Buoy.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +25 -5
  27. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts +1 -1
  33. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/index.d.ts +4 -2
  35. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  36. package/lib/typescript/module/Buoy.d.ts +79 -0
  37. package/lib/typescript/module/Buoy.d.ts.map +1 -0
  38. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  39. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +25 -5
  40. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  41. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  42. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  43. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  44. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  45. package/lib/typescript/module/floatingMenu/floatingTools.d.ts +1 -1
  46. package/lib/typescript/module/floatingMenu/floatingTools.d.ts.map +1 -1
  47. package/lib/typescript/module/index.d.ts +4 -2
  48. package/lib/typescript/module/index.d.ts.map +1 -1
  49. 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
- // Debug logging removed for production
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
- const isInitialized = (0, _react.useRef)(false);
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([(0, _sharedUi.safeSetItem)(STORAGE_KEYS.BUBBLE_POSITION_X, x.toString()), (0, _sharedUi.safeSetItem)(STORAGE_KEYS.BUBBLE_POSITION_Y, y.toString())]);
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(() => savePosition(x, y), 500);
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([(0, _sharedUi.safeGetItem)(STORAGE_KEYS.BUBBLE_POSITION_X), (0, _sharedUi.safeGetItem)(STORAGE_KEYS.BUBBLE_POSITION_Y)]);
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)) return {
143
- x,
144
- y
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; // Respect safe area left
161
- const maxX = screenWidth - visibleHandleWidth; // no right padding, ensure handle is visible
162
- // Add small padding below the safe area top to ensure bubble doesn't go behind notch
163
- const minY = safeArea.top + 20; // Ensure bubble is below safe area
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.current) return;
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
- animatedPosition.setValue(validated);
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
- width: screenWidth,
187
- height: screenHeight
188
- } = _reactNative.Dimensions.get("window");
189
- const safeArea = (0, _sharedUi.getSafeAreaInsets)();
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
- isInitialized.current = true;
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
- }, [enabled, animatedPosition, loadPosition, validatePosition, savePosition, bubbleWidth, bubbleHeight]);
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
- if (!enabled || !isInitialized.current || listenersSuspended) return;
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
- if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
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
- loadPosition,
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: 32,
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.__getValue();
429
- const currentY = animatedPosition.y.__getValue();
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 - 32; // Show only the grabber
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
- // Check if bubble is in hidden position on load
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 (!enablePositionPersistence) return;
476
- const checkHiddenState = () => {
477
- const currentX = animatedPosition.x.__getValue();
478
- // Check if bubble is at the hidden position (showing only grabber)
479
- if (currentX >= screenWidth - 32 - 5) {
480
- setIsHidden(true);
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
- }, [enablePositionPersistence, animatedPosition, screenWidth]);
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 + 20, Math.min(100, screenHeight - bubbleSize.height - safeAreaInsets.bottom));
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 - 20,
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.__getValue();
525
- const currentY = animatedPosition.y.__getValue();
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 - 20;
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 - 32; // Only show the 32px grabber
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 - 32; // Only show the 32px grabber
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 - 32 - 10) {
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 + 20
940
+ y: safeAreaInsets.top + EDGE_PADDING
747
941
  },
748
942
  style: dragHandleStyle,
749
943
  enabled: true,
@@ -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); }