@buoy-gg/core 1.7.8 → 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 +2 -2
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +5 -5
- 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 +218 -38
- package/lib/module/floatingMenu/AppHost.js +3 -3
- package/lib/module/floatingMenu/DevToolsSettingsModal.js +7 -7
- 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 +219 -39
- package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.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/module/floatingMenu/DevToolsSettingsModal.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/package.json +5 -5
|
@@ -92,7 +92,7 @@ const AppHostProvider = ({
|
|
|
92
92
|
(0, _react.useEffect)(() => {
|
|
93
93
|
const restoreOpenApps = async () => {
|
|
94
94
|
try {
|
|
95
|
-
const saved = await
|
|
95
|
+
const saved = await _sharedUi.persistentStorage.getItem(STORAGE_KEY_OPEN_APPS);
|
|
96
96
|
if (saved) {
|
|
97
97
|
const parsed = JSON.parse(saved);
|
|
98
98
|
// Handle both old format (string[]) and new format (PersistedAppState[])
|
|
@@ -138,7 +138,7 @@ const AppHostProvider = ({
|
|
|
138
138
|
id: app.id,
|
|
139
139
|
minimized: app.minimized ?? false
|
|
140
140
|
}));
|
|
141
|
-
|
|
141
|
+
_sharedUi.persistentStorage.setItem(STORAGE_KEY_OPEN_APPS, JSON.stringify(appStates));
|
|
142
142
|
}, PERSISTENCE_DELAY);
|
|
143
143
|
return () => {
|
|
144
144
|
if (persistenceTimeoutRef.current) {
|
|
@@ -184,7 +184,7 @@ const DevToolsSettingsModal = ({
|
|
|
184
184
|
(0, _react.useEffect)(() => {
|
|
185
185
|
const loadActiveTab = async () => {
|
|
186
186
|
try {
|
|
187
|
-
const savedTab = await
|
|
187
|
+
const savedTab = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.settings.activeTab());
|
|
188
188
|
if (savedTab && ["dial", "floating", "settings", "pro"].includes(savedTab)) {
|
|
189
189
|
setActiveTab(savedTab);
|
|
190
190
|
}
|
|
@@ -201,7 +201,7 @@ const DevToolsSettingsModal = ({
|
|
|
201
201
|
(0, _react.useEffect)(() => {
|
|
202
202
|
// Only persist after initial state is loaded to avoid overwriting with default
|
|
203
203
|
if (!activeTabLoaded) return;
|
|
204
|
-
|
|
204
|
+
_sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.settings.activeTab(), activeTab).catch(error => {
|
|
205
205
|
// Failed to save active tab - continue without persistence
|
|
206
206
|
console.warn("Failed to save settings active tab:", error);
|
|
207
207
|
});
|
|
@@ -244,7 +244,7 @@ const DevToolsSettingsModal = ({
|
|
|
244
244
|
|
|
245
245
|
const loadSettings = (0, _react.useCallback)(async () => {
|
|
246
246
|
try {
|
|
247
|
-
const savedSettings = await
|
|
247
|
+
const savedSettings = await _sharedUi.persistentStorage.getItem(STORAGE_KEY);
|
|
248
248
|
if (savedSettings) {
|
|
249
249
|
const parsed = JSON.parse(savedSettings);
|
|
250
250
|
const merged = mergeWithDefaults(defaultSettings, parsed, {
|
|
@@ -288,7 +288,7 @@ const DevToolsSettingsModal = ({
|
|
|
288
288
|
...newSettings,
|
|
289
289
|
dialTools: enforceDialLimit(newSettings.dialTools)
|
|
290
290
|
};
|
|
291
|
-
await
|
|
291
|
+
await _sharedUi.persistentStorage.setItem(STORAGE_KEY, JSON.stringify(limitedSettings));
|
|
292
292
|
setSettings(limitedSettings);
|
|
293
293
|
onSettingsChange?.(limitedSettings);
|
|
294
294
|
// Notify listeners (e.g., floating bubble) to refresh immediately
|
|
@@ -1438,7 +1438,7 @@ const useDevToolsSettings = () => {
|
|
|
1438
1438
|
const [settings, setSettings] = (0, _react.useState)(effectiveDefaults);
|
|
1439
1439
|
const loadSettings = (0, _react.useCallback)(async () => {
|
|
1440
1440
|
try {
|
|
1441
|
-
const savedSettings = await
|
|
1441
|
+
const savedSettings = await _sharedUi.persistentStorage.getItem(STORAGE_KEY);
|
|
1442
1442
|
if (savedSettings) {
|
|
1443
1443
|
const parsed = JSON.parse(savedSettings);
|
|
1444
1444
|
const merged = mergeWithDefaults(effectiveDefaults, parsed);
|
|
@@ -141,9 +141,8 @@ function FloatingDevTools({
|
|
|
141
141
|
visible: isSettingsOpen,
|
|
142
142
|
onClose: handleCloseSettings,
|
|
143
143
|
availableTools: availableTools,
|
|
144
|
-
onSettingsChange:
|
|
144
|
+
onSettingsChange: _newSettings => {
|
|
145
145
|
// Settings are already saved by the hook
|
|
146
|
-
console.log('Settings updated:', newSettings);
|
|
147
146
|
}
|
|
148
147
|
})]
|
|
149
148
|
});
|
|
@@ -53,7 +53,7 @@ const FloatingMenu = ({
|
|
|
53
53
|
(0, _react.useEffect)(() => {
|
|
54
54
|
const loadDialState = async () => {
|
|
55
55
|
try {
|
|
56
|
-
const savedDialOpen = await
|
|
56
|
+
const savedDialOpen = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.dial.isOpen());
|
|
57
57
|
if (savedDialOpen === "true") {
|
|
58
58
|
setShowDial(true);
|
|
59
59
|
}
|
|
@@ -70,7 +70,7 @@ const FloatingMenu = ({
|
|
|
70
70
|
(0, _react.useEffect)(() => {
|
|
71
71
|
// Only persist after initial state is loaded to avoid overwriting with default
|
|
72
72
|
if (!dialStateLoaded) return;
|
|
73
|
-
|
|
73
|
+
_sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.dial.isOpen(), showDial ? "true" : "false").catch(error => {
|
|
74
74
|
// Failed to save dial state - continue without persistence
|
|
75
75
|
console.warn("Failed to save dial state:", error);
|
|
76
76
|
});
|
|
@@ -97,7 +97,7 @@ const FloatingMenu = ({
|
|
|
97
97
|
}
|
|
98
98
|
const checkOnboarding = async () => {
|
|
99
99
|
try {
|
|
100
|
-
const hasSeenOnboarding = await
|
|
100
|
+
const hasSeenOnboarding = await _sharedUi.persistentStorage.getItem(FLOATING_MENU_ONBOARDING_KEY);
|
|
101
101
|
if (!hasSeenOnboarding) {
|
|
102
102
|
// Small delay to let the UI settle before showing tooltip
|
|
103
103
|
setTimeout(() => {
|
|
@@ -202,7 +202,7 @@ const FloatingMenu = ({
|
|
|
202
202
|
setOnboardingStep(null);
|
|
203
203
|
|
|
204
204
|
// Save to storage asynchronously in the background
|
|
205
|
-
|
|
205
|
+
_sharedUi.persistentStorage.setItem(FLOATING_MENU_ONBOARDING_KEY, "true").catch(error => {
|
|
206
206
|
// Silently fail - user already saw onboarding, just won't persist
|
|
207
207
|
console.warn("Failed to save onboarding state:", error);
|
|
208
208
|
});
|
|
@@ -115,7 +115,7 @@ function MinimizedToolsProvider({
|
|
|
115
115
|
(0, _react.useEffect)(() => {
|
|
116
116
|
const restoreMinimizedTools = async () => {
|
|
117
117
|
try {
|
|
118
|
-
const saved = await
|
|
118
|
+
const saved = await _sharedUi.persistentStorage.getItem(STORAGE_KEY);
|
|
119
119
|
if (saved) {
|
|
120
120
|
const serialized = JSON.parse(saved);
|
|
121
121
|
// Reconstruct tools with icons
|
|
@@ -145,7 +145,7 @@ function MinimizedToolsProvider({
|
|
|
145
145
|
icon,
|
|
146
146
|
...rest
|
|
147
147
|
}) => rest);
|
|
148
|
-
|
|
148
|
+
_sharedUi.persistentStorage.setItem(STORAGE_KEY, JSON.stringify(serialized));
|
|
149
149
|
}, PERSISTENCE_DELAY);
|
|
150
150
|
return () => {
|
|
151
151
|
if (persistenceTimeoutRef.current) {
|
|
@@ -194,6 +194,21 @@ function autoDiscoverPresets() {
|
|
|
194
194
|
return null;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
},
|
|
198
|
+
// Events Timeline
|
|
199
|
+
{
|
|
200
|
+
name: "@buoy-gg/events",
|
|
201
|
+
loader: () => {
|
|
202
|
+
try {
|
|
203
|
+
// @ts-ignore - Dynamic import that may not exist
|
|
204
|
+
const {
|
|
205
|
+
eventsToolPreset
|
|
206
|
+
} = require("@buoy-gg/events");
|
|
207
|
+
return eventsToolPreset;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
197
212
|
}];
|
|
198
213
|
|
|
199
214
|
// Attempt to load each preset
|
|
@@ -60,7 +60,7 @@ const DialDevTools = ({
|
|
|
60
60
|
(0, _react.useEffect)(() => {
|
|
61
61
|
const loadSettingsModalState = async () => {
|
|
62
62
|
try {
|
|
63
|
-
const savedModalOpen = await
|
|
63
|
+
const savedModalOpen = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.settings.modalOpen());
|
|
64
64
|
if (savedModalOpen === "true") {
|
|
65
65
|
setIsSettingsModalOpen(true);
|
|
66
66
|
}
|
|
@@ -77,7 +77,7 @@ const DialDevTools = ({
|
|
|
77
77
|
(0, _react.useEffect)(() => {
|
|
78
78
|
// Only persist after initial state is loaded to avoid overwriting with default
|
|
79
79
|
if (!settingsModalStateLoaded) return;
|
|
80
|
-
|
|
80
|
+
_sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.settings.modalOpen(), isSettingsModalOpen ? "true" : "false").catch(error => {
|
|
81
81
|
// Failed to save settings modal state - continue without persistence
|
|
82
82
|
console.warn("Failed to save settings modal state:", error);
|
|
83
83
|
});
|
|
@@ -112,7 +112,7 @@ const DialDevTools = ({
|
|
|
112
112
|
}
|
|
113
113
|
const checkOnboarding = async () => {
|
|
114
114
|
try {
|
|
115
|
-
const hasSeenTooltip = await
|
|
115
|
+
const hasSeenTooltip = await _sharedUi.persistentStorage.getItem(ONBOARDING_STORAGE_KEY);
|
|
116
116
|
if (!hasSeenTooltip) {
|
|
117
117
|
// Small delay to let the entrance animations play first
|
|
118
118
|
setTimeout(() => {
|
|
@@ -411,7 +411,7 @@ const DialDevTools = ({
|
|
|
411
411
|
setShowOnboardingTooltip(false);
|
|
412
412
|
|
|
413
413
|
// Save to storage asynchronously in the background
|
|
414
|
-
|
|
414
|
+
_sharedUi.persistentStorage.setItem(ONBOARDING_STORAGE_KEY, "true").catch(error => {
|
|
415
415
|
// Silently fail - user already saw onboarding, just won't persist
|
|
416
416
|
console.warn("Failed to save dial onboarding state:", error);
|
|
417
417
|
});
|
|
@@ -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, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { persistentStorage } from "@buoy-gg/shared-ui";
|
|
4
4
|
import React, { createContext, useCallback, useContext, useMemo, useState, useEffect, useRef } from "react";
|
|
5
5
|
import { BackHandler, Modal, StyleSheet, View } from "react-native";
|
|
6
6
|
import { resolveOpenAppsState } from "./AppHostLogic.js";
|
|
@@ -87,7 +87,7 @@ export const AppHostProvider = ({
|
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
const restoreOpenApps = async () => {
|
|
89
89
|
try {
|
|
90
|
-
const saved = await
|
|
90
|
+
const saved = await persistentStorage.getItem(STORAGE_KEY_OPEN_APPS);
|
|
91
91
|
if (saved) {
|
|
92
92
|
const parsed = JSON.parse(saved);
|
|
93
93
|
// Handle both old format (string[]) and new format (PersistedAppState[])
|
|
@@ -133,7 +133,7 @@ export const AppHostProvider = ({
|
|
|
133
133
|
id: app.id,
|
|
134
134
|
minimized: app.minimized ?? false
|
|
135
135
|
}));
|
|
136
|
-
|
|
136
|
+
persistentStorage.setItem(STORAGE_KEY_OPEN_APPS, JSON.stringify(appStates));
|
|
137
137
|
}, PERSISTENCE_DELAY);
|
|
138
138
|
return () => {
|
|
139
139
|
if (persistenceTimeoutRef.current) {
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
4
4
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions } from "react-native";
|
|
5
5
|
import { settingsBus } from "./settingsBus.js";
|
|
6
|
-
import { SentryBugIcon, WifiCircuitIcon, BenchmarkIcon, RenderCountIcon, Info, Layers, ChevronRightIcon, ChevronDown,
|
|
6
|
+
import { SentryBugIcon, WifiCircuitIcon, BenchmarkIcon, RenderCountIcon, Info, Layers, ChevronRightIcon, ChevronDown, getStorageBackendType, persistentStorage, Database, Trash2, CheckCircle2, AlertTriangle, Zap, FileText, HardDrive, Copy, FileCode, RefreshCw, Smartphone, Plus, copyToClipboard, LicenseEntryModal, SectionHeader } from "@buoy-gg/shared-ui";
|
|
7
7
|
import { EnvIcon, StorageIcon, RoutesIcon, NetworkIcon, QueryIcon, HighlighterIcon, ReduxIcon } from "@buoy-gg/floating-tools-core";
|
|
8
8
|
import { useDefaultConfig } from "./DefaultConfigContext.js";
|
|
9
|
-
import { JsModal, devToolsStorageKeys
|
|
9
|
+
import { JsModal, devToolsStorageKeys } from "@buoy-gg/shared-ui";
|
|
10
10
|
import { useSafeAreaInsets } from "@buoy-gg/shared-ui";
|
|
11
11
|
import { ModalHeader } from "@buoy-gg/shared-ui";
|
|
12
12
|
import { TabSelector } from "@buoy-gg/shared-ui";
|
|
@@ -185,7 +185,7 @@ export const DevToolsSettingsModal = ({
|
|
|
185
185
|
useEffect(() => {
|
|
186
186
|
const loadActiveTab = async () => {
|
|
187
187
|
try {
|
|
188
|
-
const savedTab = await
|
|
188
|
+
const savedTab = await persistentStorage.getItem(devToolsStorageKeys.settings.activeTab());
|
|
189
189
|
if (savedTab && ["dial", "floating", "settings", "pro"].includes(savedTab)) {
|
|
190
190
|
setActiveTab(savedTab);
|
|
191
191
|
}
|
|
@@ -202,7 +202,7 @@ export const DevToolsSettingsModal = ({
|
|
|
202
202
|
useEffect(() => {
|
|
203
203
|
// Only persist after initial state is loaded to avoid overwriting with default
|
|
204
204
|
if (!activeTabLoaded) return;
|
|
205
|
-
|
|
205
|
+
persistentStorage.setItem(devToolsStorageKeys.settings.activeTab(), activeTab).catch(error => {
|
|
206
206
|
// Failed to save active tab - continue without persistence
|
|
207
207
|
console.warn("Failed to save settings active tab:", error);
|
|
208
208
|
});
|
|
@@ -245,7 +245,7 @@ export const DevToolsSettingsModal = ({
|
|
|
245
245
|
|
|
246
246
|
const loadSettings = useCallback(async () => {
|
|
247
247
|
try {
|
|
248
|
-
const savedSettings = await
|
|
248
|
+
const savedSettings = await persistentStorage.getItem(STORAGE_KEY);
|
|
249
249
|
if (savedSettings) {
|
|
250
250
|
const parsed = JSON.parse(savedSettings);
|
|
251
251
|
const merged = mergeWithDefaults(defaultSettings, parsed, {
|
|
@@ -289,7 +289,7 @@ export const DevToolsSettingsModal = ({
|
|
|
289
289
|
...newSettings,
|
|
290
290
|
dialTools: enforceDialLimit(newSettings.dialTools)
|
|
291
291
|
};
|
|
292
|
-
await
|
|
292
|
+
await persistentStorage.setItem(STORAGE_KEY, JSON.stringify(limitedSettings));
|
|
293
293
|
setSettings(limitedSettings);
|
|
294
294
|
onSettingsChange?.(limitedSettings);
|
|
295
295
|
// Notify listeners (e.g., floating bubble) to refresh immediately
|
|
@@ -1438,7 +1438,7 @@ export const useDevToolsSettings = () => {
|
|
|
1438
1438
|
const [settings, setSettings] = useState(effectiveDefaults);
|
|
1439
1439
|
const loadSettings = useCallback(async () => {
|
|
1440
1440
|
try {
|
|
1441
|
-
const savedSettings = await
|
|
1441
|
+
const savedSettings = await persistentStorage.getItem(STORAGE_KEY);
|
|
1442
1442
|
if (savedSettings) {
|
|
1443
1443
|
const parsed = JSON.parse(savedSettings);
|
|
1444
1444
|
const merged = mergeWithDefaults(effectiveDefaults, parsed);
|
|
@@ -137,9 +137,8 @@ export function FloatingDevTools({
|
|
|
137
137
|
visible: isSettingsOpen,
|
|
138
138
|
onClose: handleCloseSettings,
|
|
139
139
|
availableTools: availableTools,
|
|
140
|
-
onSettingsChange:
|
|
140
|
+
onSettingsChange: _newSettings => {
|
|
141
141
|
// Settings are already saved by the hook
|
|
142
|
-
console.log('Settings updated:', newSettings);
|
|
143
142
|
}
|
|
144
143
|
})]
|
|
145
144
|
});
|