@buoy-gg/core 1.7.7 → 2.1.1
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/floatingMenu/AppHost.js +3 -3
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +25 -13
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +746 -0
- package/lib/commonjs/floatingMenu/FloatingDevTools.js +16 -4
- package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +153 -0
- package/lib/commonjs/floatingMenu/FloatingMenu.js +8 -8
- package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +2 -2
- package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +30 -0
- package/lib/commonjs/floatingMenu/defaultConfig.js +14 -7
- package/lib/commonjs/floatingMenu/dial/DialDevTools.js +12 -6
- package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +593 -0
- package/lib/commonjs/floatingMenu/floatingTools.js +218 -38
- package/lib/commonjs/floatingMenu/floatingTools.web.js +357 -0
- package/lib/commonjs/index.js +2 -2
- package/lib/commonjs/index.web.js +131 -0
- package/lib/commonjs/utils/autoDiscoverPresets.web.js +181 -0
- package/lib/module/floatingMenu/AppHost.js +4 -4
- package/lib/module/floatingMenu/DevToolsSettingsModal.js +28 -16
- package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +756 -0
- package/lib/module/floatingMenu/FloatingDevTools.js +17 -5
- package/lib/module/floatingMenu/FloatingDevTools.web.js +149 -0
- package/lib/module/floatingMenu/FloatingMenu.js +9 -9
- package/lib/module/floatingMenu/MinimizedToolsContext.js +3 -3
- package/lib/module/floatingMenu/autoDiscoverPresets.js +30 -0
- package/lib/module/floatingMenu/defaultConfig.js +14 -7
- package/lib/module/floatingMenu/dial/DialDevTools.js +13 -7
- package/lib/module/floatingMenu/dial/DialDevTools.web.js +590 -0
- package/lib/module/floatingMenu/floatingTools.js +219 -39
- package/lib/module/floatingMenu/floatingTools.web.js +357 -0
- package/lib/module/index.js +2 -2
- package/lib/module/index.web.js +24 -0
- package/lib/module/utils/autoDiscoverPresets.web.js +174 -0
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts +23 -0
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +48 -0
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -0
- 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/defaultConfig.d.ts +8 -7
- package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts +26 -0
- package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -0
- 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/floatingMenu/floatingTools.web.d.ts +27 -0
- package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.web.d.ts +20 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts +58 -0
- package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts +23 -0
- package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +48 -0
- package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -0
- 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/defaultConfig.d.ts +8 -7
- package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts +26 -0
- package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -0
- 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/floatingMenu/floatingTools.web.d.ts +27 -0
- package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts.map +1 -0
- package/lib/typescript/module/index.web.d.ts +20 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -0
- package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts +58 -0
- package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts.map +1 -0
- package/package.json +5 -4
|
@@ -104,8 +104,9 @@ const STORAGE_KEYS = {
|
|
|
104
104
|
* @param props.enabled - Whether position persistence is enabled
|
|
105
105
|
* @param props.visibleHandleWidth - Width of visible handle when bubble is hidden
|
|
106
106
|
* @param props.listenersSuspended - Pause automatic listeners without disabling manual saves
|
|
107
|
+
* @param props.onPositionLoaded - Callback fired once position is loaded and set
|
|
107
108
|
*
|
|
108
|
-
* @returns Object containing position management functions
|
|
109
|
+
* @returns Object containing position management functions and loading state
|
|
109
110
|
*
|
|
110
111
|
* @performance Uses debounced saving to avoid excessive storage operations
|
|
111
112
|
* @performance Validates positions against screen boundaries and safe areas
|
|
@@ -116,26 +117,57 @@ function useFloatingToolsPosition({
|
|
|
116
117
|
bubbleHeight = 32,
|
|
117
118
|
enabled = true,
|
|
118
119
|
visibleHandleWidth = 32,
|
|
119
|
-
listenersSuspended = false
|
|
120
|
+
listenersSuspended = false,
|
|
121
|
+
onPositionLoaded
|
|
120
122
|
}) {
|
|
121
|
-
|
|
123
|
+
// Use state instead of ref so we can react to changes and prevent races
|
|
124
|
+
const [isInitialized, setIsInitialized] = (0, _react.useState)(false);
|
|
122
125
|
const saveTimeoutRef = (0, _react.useRef)(undefined);
|
|
126
|
+
// Track if component is mounted to prevent state updates after unmount
|
|
127
|
+
const isMountedRef = (0, _react.useRef)(true);
|
|
128
|
+
|
|
129
|
+
// Cleanup on unmount - cancel any pending saves
|
|
130
|
+
(0, _react.useEffect)(() => {
|
|
131
|
+
isMountedRef.current = true;
|
|
132
|
+
return () => {
|
|
133
|
+
isMountedRef.current = false;
|
|
134
|
+
// Clear any pending debounced saves on unmount to prevent saving stale/invalid positions
|
|
135
|
+
if (saveTimeoutRef.current) {
|
|
136
|
+
clearTimeout(saveTimeoutRef.current);
|
|
137
|
+
saveTimeoutRef.current = undefined;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
123
141
|
const savePosition = (0, _react.useCallback)(async (x, y) => {
|
|
124
142
|
if (!enabled) return;
|
|
143
|
+
// Don't save if component unmounted or position is clearly invalid
|
|
144
|
+
if (!isMountedRef.current) return;
|
|
145
|
+
// Guard against saving invalid positions (e.g., during HMR with stale dimensions)
|
|
146
|
+
const {
|
|
147
|
+
width: screenWidth
|
|
148
|
+
} = _reactNative.Dimensions.get("window");
|
|
149
|
+
if (screenWidth <= 0 || x < 0 || y < 0) return;
|
|
125
150
|
try {
|
|
126
|
-
await Promise.all([
|
|
151
|
+
await Promise.all([_sharedUi.persistentStorage.setItem(STORAGE_KEYS.BUBBLE_POSITION_X, x.toString()), _sharedUi.persistentStorage.setItem(STORAGE_KEYS.BUBBLE_POSITION_Y, y.toString())]);
|
|
127
152
|
} catch (error) {
|
|
128
153
|
// Failed to save position - continue without persistence
|
|
129
154
|
}
|
|
130
155
|
}, [enabled]);
|
|
131
156
|
const debouncedSavePosition = (0, _react.useCallback)((x, y) => {
|
|
157
|
+
// Don't schedule saves if unmounted
|
|
158
|
+
if (!isMountedRef.current) return;
|
|
132
159
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
|
133
|
-
saveTimeoutRef.current = setTimeout(() =>
|
|
160
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
161
|
+
// Double-check mount status before saving
|
|
162
|
+
if (isMountedRef.current) {
|
|
163
|
+
savePosition(x, y);
|
|
164
|
+
}
|
|
165
|
+
}, 500);
|
|
134
166
|
}, [savePosition]);
|
|
135
167
|
const loadPosition = (0, _react.useCallback)(async () => {
|
|
136
168
|
if (!enabled) return null;
|
|
137
169
|
try {
|
|
138
|
-
const [xStr, yStr] = await Promise.all([
|
|
170
|
+
const [xStr, yStr] = await Promise.all([_sharedUi.persistentStorage.getItem(STORAGE_KEYS.BUBBLE_POSITION_X), _sharedUi.persistentStorage.getItem(STORAGE_KEYS.BUBBLE_POSITION_Y)]);
|
|
139
171
|
if (xStr !== null && yStr !== null) {
|
|
140
172
|
const x = parseFloat(xStr);
|
|
141
173
|
const y = parseFloat(yStr);
|
|
@@ -155,6 +187,16 @@ function useFloatingToolsPosition({
|
|
|
155
187
|
height: screenHeight
|
|
156
188
|
} = _reactNative.Dimensions.get("window");
|
|
157
189
|
const safeArea = (0, _sharedUi.getSafeAreaInsets)();
|
|
190
|
+
|
|
191
|
+
// Guard against invalid screen dimensions (can happen during HMR)
|
|
192
|
+
// Return the position as-is but mark as invalid so caller knows not to trust it
|
|
193
|
+
if (screenWidth <= 0 || screenHeight <= 0) {
|
|
194
|
+
return {
|
|
195
|
+
...position,
|
|
196
|
+
isValid: false
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
158
200
|
// Prevent going off left, top, and bottom edges with safe area
|
|
159
201
|
// Allow pushing off-screen to the right so only the grab handle remains visible
|
|
160
202
|
const minX = safeArea.left; // Respect safe area left
|
|
@@ -164,53 +206,103 @@ function useFloatingToolsPosition({
|
|
|
164
206
|
const maxY = screenHeight - bubbleHeight - safeArea.bottom; // Respect safe area bottom
|
|
165
207
|
const clamped = {
|
|
166
208
|
x: Math.max(minX, Math.min(position.x, maxX)),
|
|
167
|
-
y: Math.max(minY, Math.min(position.y, maxY))
|
|
209
|
+
y: Math.max(minY, Math.min(position.y, maxY)),
|
|
210
|
+
isValid: true
|
|
168
211
|
};
|
|
169
212
|
return clamped;
|
|
170
213
|
}, [visibleHandleWidth, bubbleHeight]);
|
|
171
214
|
(0, _react.useEffect)(() => {
|
|
172
|
-
if (!enabled || isInitialized
|
|
215
|
+
if (!enabled || isInitialized) return;
|
|
216
|
+
let cancelled = false;
|
|
173
217
|
const restore = async () => {
|
|
174
218
|
const saved = await loadPosition();
|
|
219
|
+
|
|
220
|
+
// Check if cancelled or unmounted before updating state
|
|
221
|
+
if (cancelled || !isMountedRef.current) return;
|
|
222
|
+
const {
|
|
223
|
+
width: screenWidth,
|
|
224
|
+
height: screenHeight
|
|
225
|
+
} = _reactNative.Dimensions.get("window");
|
|
226
|
+
const safeArea = (0, _sharedUi.getSafeAreaInsets)();
|
|
227
|
+
|
|
228
|
+
// If dimensions are invalid (HMR scenario), wait and retry
|
|
229
|
+
if (screenWidth <= 0 || screenHeight <= 0) {
|
|
230
|
+
// Schedule a retry after a short delay
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
if (isMountedRef.current && !cancelled) {
|
|
233
|
+
restore();
|
|
234
|
+
}
|
|
235
|
+
}, 100);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let finalPosition;
|
|
239
|
+
let wasHidden = false;
|
|
175
240
|
if (saved) {
|
|
176
241
|
const validated = validatePosition(saved);
|
|
242
|
+
|
|
243
|
+
// If validation returned invalid, retry later
|
|
244
|
+
if (!validated.isValid) {
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
if (isMountedRef.current && !cancelled) {
|
|
247
|
+
restore();
|
|
248
|
+
}
|
|
249
|
+
}, 100);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
177
253
|
// Check if the saved position is out of bounds
|
|
178
254
|
const wasOutOfBounds = Math.abs(saved.x - validated.x) > 5 || Math.abs(saved.y - validated.y) > 5;
|
|
179
255
|
if (wasOutOfBounds) {
|
|
180
256
|
// Save the corrected position
|
|
181
257
|
await savePosition(validated.x, validated.y);
|
|
182
258
|
}
|
|
183
|
-
|
|
259
|
+
finalPosition = {
|
|
260
|
+
x: validated.x,
|
|
261
|
+
y: validated.y
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Check if loaded in hidden state (bubble pushed to right edge)
|
|
265
|
+
wasHidden = validated.x >= screenWidth - visibleHandleWidth - 5;
|
|
184
266
|
} else {
|
|
185
|
-
const {
|
|
186
|
-
width: screenWidth,
|
|
187
|
-
height: screenHeight
|
|
188
|
-
} = _reactNative.Dimensions.get("window");
|
|
189
|
-
const safeArea = (0, _sharedUi.getSafeAreaInsets)();
|
|
190
267
|
const defaultY = Math.max(safeArea.top + 20, Math.min(100, screenHeight - bubbleHeight - safeArea.bottom));
|
|
191
|
-
|
|
268
|
+
finalPosition = {
|
|
192
269
|
x: screenWidth - bubbleWidth - 20,
|
|
193
|
-
y: defaultY
|
|
194
|
-
}
|
|
270
|
+
y: defaultY
|
|
271
|
+
};
|
|
195
272
|
}
|
|
196
|
-
|
|
273
|
+
|
|
274
|
+
// Final mount check before updating
|
|
275
|
+
if (cancelled || !isMountedRef.current) return;
|
|
276
|
+
animatedPosition.setValue(finalPosition);
|
|
277
|
+
setIsInitialized(true);
|
|
278
|
+
onPositionLoaded?.(finalPosition, wasHidden);
|
|
197
279
|
};
|
|
198
280
|
restore();
|
|
199
|
-
|
|
281
|
+
return () => {
|
|
282
|
+
cancelled = true;
|
|
283
|
+
};
|
|
284
|
+
}, [enabled, isInitialized, animatedPosition, loadPosition, validatePosition, savePosition, bubbleWidth, bubbleHeight, visibleHandleWidth, onPositionLoaded]);
|
|
285
|
+
|
|
286
|
+
// Listener effect - now properly depends on isInitialized state
|
|
200
287
|
(0, _react.useEffect)(() => {
|
|
201
|
-
|
|
288
|
+
// Don't attach listener until position is loaded
|
|
289
|
+
if (!enabled || !isInitialized || listenersSuspended) return;
|
|
202
290
|
const listener = animatedPosition.addListener(value => {
|
|
203
291
|
debouncedSavePosition(value.x, value.y);
|
|
204
292
|
});
|
|
205
293
|
return () => {
|
|
206
294
|
animatedPosition.removeListener(listener);
|
|
207
|
-
|
|
295
|
+
// Clear debounce timer when listener is removed
|
|
296
|
+
if (saveTimeoutRef.current) {
|
|
297
|
+
clearTimeout(saveTimeoutRef.current);
|
|
298
|
+
saveTimeoutRef.current = undefined;
|
|
299
|
+
}
|
|
208
300
|
};
|
|
209
|
-
}, [enabled, listenersSuspended, animatedPosition, debouncedSavePosition]);
|
|
301
|
+
}, [enabled, isInitialized, listenersSuspended, animatedPosition, debouncedSavePosition]);
|
|
210
302
|
return {
|
|
211
303
|
savePosition,
|
|
212
304
|
loadPosition,
|
|
213
|
-
isInitialized
|
|
305
|
+
isInitialized
|
|
214
306
|
};
|
|
215
307
|
}
|
|
216
308
|
|
|
@@ -391,26 +483,41 @@ function FloatingTools({
|
|
|
391
483
|
|
|
392
484
|
// Track previous pushToSide value to detect transitions
|
|
393
485
|
const prevPushToSideRef = (0, _react.useRef)(pushToSide);
|
|
486
|
+
|
|
487
|
+
// Track previous bubble width for auto-adjustment when tools are added/removed
|
|
488
|
+
const prevBubbleWidthRef = (0, _react.useRef)(null);
|
|
394
489
|
const safeAreaInsets = (0, _sharedUi.useSafeAreaInsets)();
|
|
395
490
|
const {
|
|
396
491
|
width: screenWidth,
|
|
397
492
|
height: screenHeight
|
|
398
493
|
} = _reactNative.Dimensions.get("window");
|
|
399
494
|
|
|
495
|
+
// Callback when position is loaded - sets hidden state properly without racing
|
|
496
|
+
const handlePositionLoaded = (0, _react.useCallback)((position, wasHidden) => {
|
|
497
|
+
if (wasHidden) {
|
|
498
|
+
setIsHidden(true);
|
|
499
|
+
}
|
|
500
|
+
}, []);
|
|
501
|
+
|
|
400
502
|
// Position persistence (state/IO extracted to hook)
|
|
401
503
|
const {
|
|
402
|
-
savePosition
|
|
504
|
+
savePosition,
|
|
505
|
+
isInitialized: isPositionInitialized
|
|
403
506
|
} = useFloatingToolsPosition({
|
|
404
507
|
animatedPosition,
|
|
405
508
|
bubbleWidth: bubbleSize.width,
|
|
406
509
|
bubbleHeight: bubbleSize.height,
|
|
407
510
|
enabled: enablePositionPersistence,
|
|
408
511
|
visibleHandleWidth: 32,
|
|
409
|
-
listenersSuspended: isDragging
|
|
512
|
+
listenersSuspended: isDragging,
|
|
513
|
+
onPositionLoaded: handlePositionLoaded
|
|
410
514
|
});
|
|
411
515
|
|
|
412
516
|
// Effect to handle pushToSide prop changes
|
|
413
517
|
(0, _react.useEffect)(() => {
|
|
518
|
+
// Don't process pushToSide until position is initialized (when persistence enabled)
|
|
519
|
+
if (enablePositionPersistence && !isPositionInitialized) return;
|
|
520
|
+
|
|
414
521
|
// Reset user override when pushToSide becomes true (dial/modal opens)
|
|
415
522
|
// This allows auto-hide to work after user manually showed the menu
|
|
416
523
|
if (!prevPushToSideRef.current && pushToSide) {
|
|
@@ -468,22 +575,77 @@ function FloatingTools({
|
|
|
468
575
|
});
|
|
469
576
|
}
|
|
470
577
|
}
|
|
471
|
-
}, [pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
|
|
578
|
+
}, [enablePositionPersistence, isPositionInitialized, pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
|
|
579
|
+
|
|
580
|
+
// Hidden state is now set via onPositionLoaded callback - no racing effect needed
|
|
472
581
|
|
|
473
|
-
//
|
|
582
|
+
// Auto-adjust position when bubble width changes (tools added/removed)
|
|
583
|
+
// This keeps the bubble's right edge in the same relative position
|
|
474
584
|
(0, _react.useEffect)(() => {
|
|
475
|
-
if
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
585
|
+
// Skip if not initialized or first measurement
|
|
586
|
+
if (!isPositionInitialized || prevBubbleWidthRef.current === null) {
|
|
587
|
+
prevBubbleWidthRef.current = bubbleSize.width;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Skip if currently dragging - let drag handle position
|
|
592
|
+
if (isDragging) {
|
|
593
|
+
prevBubbleWidthRef.current = bubbleSize.width;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const prevWidth = prevBubbleWidthRef.current;
|
|
597
|
+
const newWidth = bubbleSize.width;
|
|
598
|
+
const widthDelta = newWidth - prevWidth;
|
|
599
|
+
|
|
600
|
+
// Skip if no significant change (avoid jitter)
|
|
601
|
+
if (Math.abs(widthDelta) < 2) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Update ref first
|
|
606
|
+
prevBubbleWidthRef.current = newWidth;
|
|
607
|
+
|
|
608
|
+
// If hidden, only adjust the saved position (not the current animated position)
|
|
609
|
+
if (isHidden) {
|
|
610
|
+
if (savedPositionRef.current) {
|
|
611
|
+
const adjustedX = savedPositionRef.current.x - widthDelta;
|
|
612
|
+
// Clamp to valid bounds
|
|
613
|
+
savedPositionRef.current.x = Math.max(safeAreaInsets.left, Math.min(adjustedX, screenWidth - newWidth - 20));
|
|
481
614
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Get current position
|
|
619
|
+
const currentX = animatedPosition.x.__getValue();
|
|
620
|
+
const currentY = animatedPosition.y.__getValue();
|
|
621
|
+
|
|
622
|
+
// Calculate new X position
|
|
623
|
+
// If bubble grew (positive delta), move left (decrease X)
|
|
624
|
+
// If bubble shrunk (negative delta), move right (increase X)
|
|
625
|
+
const newX = currentX - widthDelta;
|
|
626
|
+
|
|
627
|
+
// Validate new position is within bounds
|
|
628
|
+
const clampedX = Math.max(safeAreaInsets.left, Math.min(newX, screenWidth - 32) // Ensure handle still visible
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
// Only animate if position actually changed
|
|
632
|
+
if (Math.abs(clampedX - currentX) > 1) {
|
|
633
|
+
_reactNative.Animated.timing(animatedPosition, {
|
|
634
|
+
toValue: {
|
|
635
|
+
x: clampedX,
|
|
636
|
+
y: currentY
|
|
637
|
+
},
|
|
638
|
+
duration: 150,
|
|
639
|
+
useNativeDriver: false
|
|
640
|
+
}).start(() => {
|
|
641
|
+
savePosition(clampedX, currentY);
|
|
642
|
+
// Update savedPositionRef so show/hide works correctly
|
|
643
|
+
if (savedPositionRef.current) {
|
|
644
|
+
savedPositionRef.current.x = clampedX;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}, [bubbleSize.width, isPositionInitialized, isDragging, isHidden, animatedPosition, screenWidth, safeAreaInsets.left, savePosition]);
|
|
487
649
|
|
|
488
650
|
// Default position when persistence disabled or during onboarding
|
|
489
651
|
(0, _react.useEffect)(() => {
|
|
@@ -594,6 +756,18 @@ function FloatingTools({
|
|
|
594
756
|
// Animate to hidden position (only grabber visible)
|
|
595
757
|
const hiddenX = screenWidth - 32; // Only show the 32px grabber
|
|
596
758
|
setIsHidden(true);
|
|
759
|
+
|
|
760
|
+
// Update savedPositionRef to preserve the Y position when dragging while hidden
|
|
761
|
+
// This ensures that when showing, the bubble restores to the new Y position
|
|
762
|
+
if (savedPositionRef.current) {
|
|
763
|
+
savedPositionRef.current.y = currentY;
|
|
764
|
+
} else {
|
|
765
|
+
// If no saved position exists, create one with default visible X
|
|
766
|
+
savedPositionRef.current = {
|
|
767
|
+
x: screenWidth - bubbleSize.width - 20,
|
|
768
|
+
y: currentY
|
|
769
|
+
};
|
|
770
|
+
}
|
|
597
771
|
_reactNative.Animated.timing(animatedPosition, {
|
|
598
772
|
toValue: {
|
|
599
773
|
x: hiddenX,
|
|
@@ -694,6 +868,12 @@ function FloatingTools({
|
|
|
694
868
|
|
|
695
869
|
// Width for the minimized tools stack - match the drag handle width
|
|
696
870
|
const minimizedStackWidth = 32;
|
|
871
|
+
|
|
872
|
+
// Don't render until position is initialized (when persistence enabled)
|
|
873
|
+
// This prevents the bubble from appearing at {0,0} before position loads
|
|
874
|
+
if (enablePositionPersistence && !isPositionInitialized) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
697
877
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
|
|
698
878
|
style: bubbleStyle,
|
|
699
879
|
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|