@buoy-gg/core 1.7.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 (132) hide show
  1. package/README.md +43 -0
  2. package/lib/commonjs/floatingMenu/AppHost.js +410 -0
  3. package/lib/commonjs/floatingMenu/AppHostLogic.js +44 -0
  4. package/lib/commonjs/floatingMenu/DefaultConfigContext.js +45 -0
  5. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +2274 -0
  6. package/lib/commonjs/floatingMenu/DevToolsVisibilityContext.js +49 -0
  7. package/lib/commonjs/floatingMenu/DraggableHeader.js +114 -0
  8. package/lib/commonjs/floatingMenu/FloatingDevTools.js +254 -0
  9. package/lib/commonjs/floatingMenu/FloatingMenu.js +364 -0
  10. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +247 -0
  11. package/lib/commonjs/floatingMenu/MinimizedToolsStack.js +206 -0
  12. package/lib/commonjs/floatingMenu/ToggleStateManager.js +36 -0
  13. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +241 -0
  14. package/lib/commonjs/floatingMenu/defaultConfig.js +160 -0
  15. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +835 -0
  16. package/lib/commonjs/floatingMenu/dial/DialIcon.js +246 -0
  17. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.js +249 -0
  18. package/lib/commonjs/floatingMenu/dial/onboardingConstants.js +70 -0
  19. package/lib/commonjs/floatingMenu/floatingTools.js +771 -0
  20. package/lib/commonjs/floatingMenu/settingsBus.js +23 -0
  21. package/lib/commonjs/floatingMenu/types.js +5 -0
  22. package/lib/commonjs/index.js +240 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/module/floatingMenu/AppHost.js +402 -0
  25. package/lib/module/floatingMenu/AppHostLogic.js +39 -0
  26. package/lib/module/floatingMenu/DefaultConfigContext.js +39 -0
  27. package/lib/module/floatingMenu/DevToolsSettingsModal.js +2273 -0
  28. package/lib/module/floatingMenu/DevToolsVisibilityContext.js +44 -0
  29. package/lib/module/floatingMenu/DraggableHeader.js +110 -0
  30. package/lib/module/floatingMenu/FloatingDevTools.js +249 -0
  31. package/lib/module/floatingMenu/FloatingMenu.js +358 -0
  32. package/lib/module/floatingMenu/MinimizedToolsContext.js +239 -0
  33. package/lib/module/floatingMenu/MinimizedToolsStack.js +202 -0
  34. package/lib/module/floatingMenu/ToggleStateManager.js +32 -0
  35. package/lib/module/floatingMenu/autoDiscoverPresets.js +236 -0
  36. package/lib/module/floatingMenu/defaultConfig.js +151 -0
  37. package/lib/module/floatingMenu/dial/DialDevTools.js +829 -0
  38. package/lib/module/floatingMenu/dial/DialIcon.js +241 -0
  39. package/lib/module/floatingMenu/dial/OnboardingTooltip.js +244 -0
  40. package/lib/module/floatingMenu/dial/onboardingConstants.js +64 -0
  41. package/lib/module/floatingMenu/floatingTools.js +767 -0
  42. package/lib/module/floatingMenu/settingsBus.js +19 -0
  43. package/lib/module/floatingMenu/types.js +3 -0
  44. package/lib/module/index.js +29 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts +39 -0
  47. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts +37 -0
  49. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/floatingMenu/DefaultConfigContext.d.ts +27 -0
  51. package/lib/typescript/commonjs/floatingMenu/DefaultConfigContext.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts +57 -0
  53. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/floatingMenu/DevToolsVisibilityContext.d.ts +25 -0
  55. package/lib/typescript/commonjs/floatingMenu/DevToolsVisibilityContext.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/floatingMenu/DraggableHeader.d.ts +30 -0
  57. package/lib/typescript/commonjs/floatingMenu/DraggableHeader.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +226 -0
  59. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts +39 -0
  61. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts +95 -0
  63. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts +10 -0
  65. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/floatingMenu/ToggleStateManager.d.ts +21 -0
  67. package/lib/typescript/commonjs/floatingMenu/ToggleStateManager.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts +75 -0
  69. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +120 -0
  71. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +35 -0
  73. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts +14 -0
  75. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.d.ts +12 -0
  77. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts +30 -0
  79. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts +56 -0
  81. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts +10 -0
  83. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/floatingMenu/types.d.ts +56 -0
  85. package/lib/typescript/commonjs/floatingMenu/types.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/index.d.ts +18 -0
  87. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/package.json +1 -0
  89. package/lib/typescript/module/floatingMenu/AppHost.d.ts +39 -0
  90. package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -0
  91. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts +37 -0
  92. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts.map +1 -0
  93. package/lib/typescript/module/floatingMenu/DefaultConfigContext.d.ts +27 -0
  94. package/lib/typescript/module/floatingMenu/DefaultConfigContext.d.ts.map +1 -0
  95. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts +57 -0
  96. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -0
  97. package/lib/typescript/module/floatingMenu/DevToolsVisibilityContext.d.ts +25 -0
  98. package/lib/typescript/module/floatingMenu/DevToolsVisibilityContext.d.ts.map +1 -0
  99. package/lib/typescript/module/floatingMenu/DraggableHeader.d.ts +30 -0
  100. package/lib/typescript/module/floatingMenu/DraggableHeader.d.ts.map +1 -0
  101. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +226 -0
  102. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -0
  103. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts +39 -0
  104. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -0
  105. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts +95 -0
  106. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -0
  107. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts +10 -0
  108. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts.map +1 -0
  109. package/lib/typescript/module/floatingMenu/ToggleStateManager.d.ts +21 -0
  110. package/lib/typescript/module/floatingMenu/ToggleStateManager.d.ts.map +1 -0
  111. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts +75 -0
  112. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -0
  113. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +120 -0
  114. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -0
  115. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +35 -0
  116. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -0
  117. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts +14 -0
  118. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -0
  119. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.d.ts +12 -0
  120. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -0
  121. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts +30 -0
  122. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts.map +1 -0
  123. package/lib/typescript/module/floatingMenu/floatingTools.d.ts +56 -0
  124. package/lib/typescript/module/floatingMenu/floatingTools.d.ts.map +1 -0
  125. package/lib/typescript/module/floatingMenu/settingsBus.d.ts +10 -0
  126. package/lib/typescript/module/floatingMenu/settingsBus.d.ts.map +1 -0
  127. package/lib/typescript/module/floatingMenu/types.d.ts +56 -0
  128. package/lib/typescript/module/floatingMenu/types.d.ts.map +1 -0
  129. package/lib/typescript/module/index.d.ts +18 -0
  130. package/lib/typescript/module/index.d.ts.map +1 -0
  131. package/lib/typescript/module/package.json +1 -0
  132. package/package.json +79 -0
@@ -0,0 +1,767 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useMemo, useRef, useState, useContext, createContext, useCallback, Children } from "react";
4
+ import { Animated, Dimensions, View, Text, TouchableOpacity } from "react-native";
5
+ import { getSafeAreaInsets, safeGetItem, safeSetItem, buoyColors } from "@buoy-gg/shared-ui";
6
+ import { DraggableHeader } from "./DraggableHeader.js";
7
+ import { useSafeAreaInsets } from "@buoy-gg/shared-ui";
8
+ import { calculateTargetPosition } from "./dial/onboardingConstants.js";
9
+ import { MinimizedToolsStack } from "./MinimizedToolsStack.js";
10
+ import { useMinimizedTools } from "./MinimizedToolsContext.js";
11
+ import { useAppHost } from "./AppHost.js";
12
+ import { EnvironmentSelectorInline } from "@buoy-gg/shared-ui";
13
+
14
+ // Shared utilities from floating-tools-core
15
+ import { getUserStatusConfig as getStatusConfig, getGripIconLayout, filterValidChildren, floatingToolsColors } from "@buoy-gg/floating-tools-core";
16
+
17
+ // Re-export UserRole type for consumers
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ // Using Views to render grip dots; no react-native-svg dependency
20
+
21
+ // =============================
22
+ // Icons (using shared layout calculation)
23
+ // =============================
24
+ /**
25
+ * Grip icon component for draggable areas
26
+ *
27
+ * Renders a vertical grip pattern using View components to avoid SVG dependencies.
28
+ * Uses shared getGripIconLayout for consistent sizing across platforms.
29
+ */
30
+ function GripVerticalIcon({
31
+ size = 24,
32
+ color = buoyColors.textSecondary + "CC"
33
+ }) {
34
+ // Use shared layout calculation from floating-tools-core
35
+ const layout = getGripIconLayout(size);
36
+ const containerStyle = {
37
+ width: size,
38
+ height: size,
39
+ flexDirection: "row",
40
+ alignItems: "center",
41
+ justifyContent: "center"
42
+ };
43
+ const columnStyle = {
44
+ flexDirection: "column",
45
+ alignItems: "center",
46
+ justifyContent: "center",
47
+ marginHorizontal: layout.columnGap / 2
48
+ };
49
+ const dotStyle = {
50
+ width: layout.dotSize,
51
+ height: layout.dotSize,
52
+ borderRadius: layout.dotSize / 2,
53
+ backgroundColor: color,
54
+ marginVertical: layout.rowGap / 2
55
+ };
56
+ return /*#__PURE__*/_jsxs(View, {
57
+ style: containerStyle,
58
+ children: [/*#__PURE__*/_jsxs(View, {
59
+ style: columnStyle,
60
+ children: [/*#__PURE__*/_jsx(View, {
61
+ style: dotStyle
62
+ }), /*#__PURE__*/_jsx(View, {
63
+ style: dotStyle
64
+ }), /*#__PURE__*/_jsx(View, {
65
+ style: dotStyle
66
+ })]
67
+ }), /*#__PURE__*/_jsxs(View, {
68
+ style: columnStyle,
69
+ children: [/*#__PURE__*/_jsx(View, {
70
+ style: dotStyle
71
+ }), /*#__PURE__*/_jsx(View, {
72
+ style: dotStyle
73
+ }), /*#__PURE__*/_jsx(View, {
74
+ style: dotStyle
75
+ })]
76
+ })]
77
+ });
78
+ }
79
+ const STORAGE_KEYS = {
80
+ BUBBLE_POSITION_X: "@react_buoy_bubble_position_x",
81
+ BUBBLE_POSITION_Y: "@react_buoy_bubble_position_y"
82
+ };
83
+
84
+ // Debug logging removed for production
85
+
86
+ // =============================
87
+ // Position persistence hook
88
+ // Extracted logic dedicated to state/IO
89
+ // =============================
90
+ /**
91
+ * Custom hook for managing floating tools position persistence
92
+ *
93
+ * Handles loading, saving, and validating the position of the floating tools bubble
94
+ * with automatic boundary checking and storage management.
95
+ *
96
+ * @param props - Configuration for position management
97
+ * @param props.animatedPosition - Animated.ValueXY for position updates
98
+ * @param props.bubbleWidth - Width of the bubble for boundary calculations
99
+ * @param props.bubbleHeight - Height of the bubble for boundary calculations
100
+ * @param props.enabled - Whether position persistence is enabled
101
+ * @param props.visibleHandleWidth - Width of visible handle when bubble is hidden
102
+ * @param props.listenersSuspended - Pause automatic listeners without disabling manual saves
103
+ *
104
+ * @returns Object containing position management functions
105
+ *
106
+ * @performance Uses debounced saving to avoid excessive storage operations
107
+ * @performance Validates positions against screen boundaries and safe areas
108
+ */
109
+ function useFloatingToolsPosition({
110
+ animatedPosition,
111
+ bubbleWidth = 100,
112
+ bubbleHeight = 32,
113
+ enabled = true,
114
+ visibleHandleWidth = 32,
115
+ listenersSuspended = false
116
+ }) {
117
+ const isInitialized = useRef(false);
118
+ const saveTimeoutRef = useRef(undefined);
119
+ const savePosition = useCallback(async (x, y) => {
120
+ if (!enabled) return;
121
+ try {
122
+ await Promise.all([safeSetItem(STORAGE_KEYS.BUBBLE_POSITION_X, x.toString()), safeSetItem(STORAGE_KEYS.BUBBLE_POSITION_Y, y.toString())]);
123
+ } catch (error) {
124
+ // Failed to save position - continue without persistence
125
+ }
126
+ }, [enabled]);
127
+ const debouncedSavePosition = useCallback((x, y) => {
128
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
129
+ saveTimeoutRef.current = setTimeout(() => savePosition(x, y), 500);
130
+ }, [savePosition]);
131
+ const loadPosition = useCallback(async () => {
132
+ if (!enabled) return null;
133
+ try {
134
+ const [xStr, yStr] = await Promise.all([safeGetItem(STORAGE_KEYS.BUBBLE_POSITION_X), safeGetItem(STORAGE_KEYS.BUBBLE_POSITION_Y)]);
135
+ if (xStr !== null && yStr !== null) {
136
+ const x = parseFloat(xStr);
137
+ const y = parseFloat(yStr);
138
+ if (!Number.isNaN(x) && !Number.isNaN(y)) return {
139
+ x,
140
+ y
141
+ };
142
+ }
143
+ } catch (error) {
144
+ // Failed to load position - use default
145
+ }
146
+ return null;
147
+ }, [enabled]);
148
+ const validatePosition = useCallback(position => {
149
+ const {
150
+ width: screenWidth,
151
+ height: screenHeight
152
+ } = Dimensions.get("window");
153
+ const safeArea = getSafeAreaInsets();
154
+ // Prevent going off left, top, and bottom edges with safe area
155
+ // Allow pushing off-screen to the right so only the grab handle remains visible
156
+ const minX = safeArea.left; // Respect safe area left
157
+ const maxX = screenWidth - visibleHandleWidth; // no right padding, ensure handle is visible
158
+ // Add small padding below the safe area top to ensure bubble doesn't go behind notch
159
+ const minY = safeArea.top + 20; // Ensure bubble is below safe area
160
+ const maxY = screenHeight - bubbleHeight - safeArea.bottom; // Respect safe area bottom
161
+ const clamped = {
162
+ x: Math.max(minX, Math.min(position.x, maxX)),
163
+ y: Math.max(minY, Math.min(position.y, maxY))
164
+ };
165
+ return clamped;
166
+ }, [visibleHandleWidth, bubbleHeight]);
167
+ useEffect(() => {
168
+ if (!enabled || isInitialized.current) return;
169
+ const restore = async () => {
170
+ const saved = await loadPosition();
171
+ if (saved) {
172
+ const validated = validatePosition(saved);
173
+ // Check if the saved position is out of bounds
174
+ const wasOutOfBounds = Math.abs(saved.x - validated.x) > 5 || Math.abs(saved.y - validated.y) > 5;
175
+ if (wasOutOfBounds) {
176
+ // Save the corrected position
177
+ await savePosition(validated.x, validated.y);
178
+ }
179
+ animatedPosition.setValue(validated);
180
+ } else {
181
+ const {
182
+ width: screenWidth,
183
+ height: screenHeight
184
+ } = Dimensions.get("window");
185
+ const safeArea = getSafeAreaInsets();
186
+ const defaultY = Math.max(safeArea.top + 20, Math.min(100, screenHeight - bubbleHeight - safeArea.bottom));
187
+ animatedPosition.setValue({
188
+ x: screenWidth - bubbleWidth - 20,
189
+ y: defaultY // Ensure it's within safe area bounds
190
+ });
191
+ }
192
+ isInitialized.current = true;
193
+ };
194
+ restore();
195
+ }, [enabled, animatedPosition, loadPosition, validatePosition, savePosition, bubbleWidth, bubbleHeight]);
196
+ useEffect(() => {
197
+ if (!enabled || !isInitialized.current || listenersSuspended) return;
198
+ const listener = animatedPosition.addListener(value => {
199
+ debouncedSavePosition(value.x, value.y);
200
+ });
201
+ return () => {
202
+ animatedPosition.removeListener(listener);
203
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
204
+ };
205
+ }, [enabled, listenersSuspended, animatedPosition, debouncedSavePosition]);
206
+ return {
207
+ savePosition,
208
+ loadPosition,
209
+ isInitialized: isInitialized.current
210
+ };
211
+ }
212
+
213
+ // =============================
214
+ // UI-only leaf components
215
+ // =============================
216
+ export function Divider() {
217
+ const dividerStyle = {
218
+ width: 1,
219
+ height: 12,
220
+ backgroundColor: buoyColors.textMuted + "66",
221
+ flexShrink: 0
222
+ };
223
+ return /*#__PURE__*/_jsx(View, {
224
+ style: dividerStyle
225
+ });
226
+ }
227
+
228
+ // Use shared getUserStatusConfig from floating-tools-core
229
+ // Wraps the core function to use our buoyColors (mapped to gameUIColors format)
230
+ const gameUIColorsCompat = {
231
+ success: "#20C997",
232
+ // teal
233
+ warning: "#FFA94D",
234
+ error: "#EF4444",
235
+ muted: buoyColors.textMuted,
236
+ secondary: buoyColors.textSecondary,
237
+ info: "#20C997",
238
+ // teal
239
+ primary: "#20C997",
240
+ optional: buoyColors.textSecondary // optional status color
241
+ };
242
+ function getUserStatusConfig(userRole) {
243
+ return getStatusConfig(userRole, gameUIColorsCompat);
244
+ }
245
+
246
+ // Context to avoid brittle prop threading and keep API composable
247
+ const FloatingToolsContext = /*#__PURE__*/createContext({
248
+ isDragging: false
249
+ });
250
+ export function UserStatus({
251
+ userRole,
252
+ onPress
253
+ }) {
254
+ const {
255
+ isDragging
256
+ } = useContext(FloatingToolsContext);
257
+ const config = getUserStatusConfig(userRole);
258
+ const containerStyle = {
259
+ flexDirection: "row",
260
+ alignItems: "center",
261
+ paddingVertical: 6,
262
+ paddingHorizontal: 8,
263
+ flexShrink: 0
264
+ };
265
+ const dotStyle = {
266
+ width: 6,
267
+ height: 6,
268
+ borderRadius: 3,
269
+ backgroundColor: config.dotColor,
270
+ marginRight: 4
271
+ };
272
+ const textStyle = {
273
+ fontSize: 10,
274
+ fontWeight: "500",
275
+ color: config.textColor,
276
+ letterSpacing: 0.3
277
+ };
278
+ if (!onPress) {
279
+ return /*#__PURE__*/_jsxs(View, {
280
+ style: containerStyle,
281
+ accessibilityLabel: `User status: ${config.label}`,
282
+ children: [/*#__PURE__*/_jsx(View, {
283
+ style: dotStyle
284
+ }), /*#__PURE__*/_jsx(Text, {
285
+ style: textStyle,
286
+ children: config.label
287
+ })]
288
+ });
289
+ }
290
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
291
+ accessibilityRole: "button",
292
+ accessibilityLabel: "Open Dev Tools Menu",
293
+ testID: "open-dev-tools-button",
294
+ onPress: onPress,
295
+ hitSlop: {
296
+ top: 10,
297
+ bottom: 10,
298
+ left: 0,
299
+ right: 10
300
+ },
301
+ disabled: isDragging,
302
+ activeOpacity: 0.85,
303
+ style: containerStyle,
304
+ children: [/*#__PURE__*/_jsx(View, {
305
+ style: dotStyle
306
+ }), /*#__PURE__*/_jsx(Text, {
307
+ style: textStyle,
308
+ children: config.label
309
+ })]
310
+ });
311
+ }
312
+
313
+ // =============================
314
+ // Helpers (using shared filterValidChildren from core)
315
+ // =============================
316
+ function interleaveWithDividers(childrenArray) {
317
+ const validChildren = filterValidChildren(childrenArray);
318
+ const result = [];
319
+ validChildren.forEach((child, index) => {
320
+ result.push(child);
321
+ if (index < validChildren.length - 1) result.push(/*#__PURE__*/_jsx(Divider, {}, `divider-${index}`));
322
+ });
323
+ return result;
324
+ }
325
+
326
+ // =============================
327
+ // Main Component (presentation only)
328
+ // =============================
329
+
330
+ /**
331
+ * FloatingTools - A draggable, resizable bubble for development tools
332
+ *
333
+ * This component provides a floating bubble interface that can contain various
334
+ * development tools and controls. It features:
335
+ * - Drag and drop positioning with boundary constraints
336
+ * - Hide/show functionality by dragging to screen edge
337
+ * - Position persistence across app restarts
338
+ * - Safe area aware positioning
339
+ * - Automatic divider insertion between child components
340
+ *
341
+ * @param props - Configuration for the floating tools
342
+ * @param props.enablePositionPersistence - Whether to save/restore position (default: true)
343
+ * @param props.children - Child components to render in the bubble
344
+ *
345
+ * @returns JSX.Element representing the floating tools bubble
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * <FloatingTools enablePositionPersistence={true}>
350
+ * <UserStatus userRole="admin" onPress={handleUserPress} />
351
+ * <ToolButton onPress={openSettings} />
352
+ * </FloatingTools>
353
+ * ```
354
+ *
355
+ * @performance Uses native driver animations for smooth positioning
356
+ * @performance Implements efficient boundary checking and position validation
357
+ * @performance Includes debounced position saving for optimal storage performance
358
+ */
359
+ export function FloatingTools({
360
+ enablePositionPersistence = true,
361
+ pushToSide = false,
362
+ centerOnboarding = false,
363
+ children,
364
+ environment,
365
+ availableEnvironments,
366
+ onEnvironmentSwitch,
367
+ showEnvironmentSelector = false
368
+ }) {
369
+ // Animated position and drag state
370
+ const animatedPosition = useRef(new Animated.ValueXY()).current;
371
+ const saveTimeoutRef = useRef(null);
372
+ const [isDragging, setIsDragging] = useState(false);
373
+ const [bubbleSize, setBubbleSize] = useState({
374
+ width: 100,
375
+ height: 32
376
+ });
377
+ const [isHidden, setIsHidden] = useState(false);
378
+
379
+ // Store the position before hiding to restore when showing
380
+ const savedPositionRef = useRef(null);
381
+
382
+ // Track if the hide state was triggered by pushToSide prop vs user toggle
383
+ const isPushedBySideRef = useRef(false);
384
+
385
+ // Track if user has explicitly chosen to show the menu (overriding pushToSide)
386
+ const userWantsVisibleRef = useRef(false);
387
+
388
+ // Track previous pushToSide value to detect transitions
389
+ const prevPushToSideRef = useRef(pushToSide);
390
+ const safeAreaInsets = useSafeAreaInsets();
391
+ const {
392
+ width: screenWidth,
393
+ height: screenHeight
394
+ } = Dimensions.get("window");
395
+
396
+ // Position persistence (state/IO extracted to hook)
397
+ const {
398
+ savePosition
399
+ } = useFloatingToolsPosition({
400
+ animatedPosition,
401
+ bubbleWidth: bubbleSize.width,
402
+ bubbleHeight: bubbleSize.height,
403
+ enabled: enablePositionPersistence,
404
+ visibleHandleWidth: 32,
405
+ listenersSuspended: isDragging
406
+ });
407
+
408
+ // Effect to handle pushToSide prop changes
409
+ useEffect(() => {
410
+ // Reset user override when pushToSide becomes true (dial/modal opens)
411
+ // This allows auto-hide to work after user manually showed the menu
412
+ if (!prevPushToSideRef.current && pushToSide) {
413
+ userWantsVisibleRef.current = false;
414
+ }
415
+ prevPushToSideRef.current = pushToSide;
416
+
417
+ // Only push to side if:
418
+ // 1. pushToSide is true
419
+ // 2. Not currently hidden
420
+ // 3. Not being dragged
421
+ // 4. User hasn't manually restored (userWantsVisibleRef)
422
+ if (pushToSide && !isHidden && !isDragging && !userWantsVisibleRef.current) {
423
+ // Push to side
424
+ const currentX = animatedPosition.x.__getValue();
425
+ const currentY = animatedPosition.y.__getValue();
426
+
427
+ // Save current position
428
+ savedPositionRef.current = {
429
+ x: currentX,
430
+ y: currentY
431
+ };
432
+ const hiddenX = screenWidth - 32; // Show only the grabber
433
+ isPushedBySideRef.current = true;
434
+ setIsHidden(true);
435
+ Animated.timing(animatedPosition, {
436
+ toValue: {
437
+ x: hiddenX,
438
+ y: currentY
439
+ },
440
+ duration: 200,
441
+ useNativeDriver: false
442
+ }).start(() => {
443
+ savePosition(hiddenX, currentY);
444
+ });
445
+ } else if (!pushToSide && isHidden && isPushedBySideRef.current) {
446
+ // Restore from side when pushToSide becomes false
447
+ if (savedPositionRef.current) {
448
+ const {
449
+ x: targetX,
450
+ y: targetY
451
+ } = savedPositionRef.current;
452
+ isPushedBySideRef.current = false;
453
+ userWantsVisibleRef.current = false; // Reset user override when tools close
454
+ setIsHidden(false);
455
+ Animated.timing(animatedPosition, {
456
+ toValue: {
457
+ x: targetX,
458
+ y: targetY
459
+ },
460
+ duration: 200,
461
+ useNativeDriver: false
462
+ }).start(() => {
463
+ savePosition(targetX, targetY);
464
+ });
465
+ }
466
+ }
467
+ }, [pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
468
+
469
+ // Check if bubble is in hidden position on load
470
+ useEffect(() => {
471
+ if (!enablePositionPersistence) return;
472
+ const checkHiddenState = () => {
473
+ const currentX = animatedPosition.x.__getValue();
474
+ // Check if bubble is at the hidden position (showing only grabber)
475
+ if (currentX >= screenWidth - 32 - 5) {
476
+ setIsHidden(true);
477
+ }
478
+ };
479
+ // Delay check to ensure position is loaded
480
+ const timer = setTimeout(checkHiddenState, 100);
481
+ return () => clearTimeout(timer);
482
+ }, [enablePositionPersistence, animatedPosition, screenWidth]);
483
+
484
+ // Default position when persistence disabled or during onboarding
485
+ useEffect(() => {
486
+ if (!enablePositionPersistence) {
487
+ if (centerOnboarding) {
488
+ // Center the bubble for onboarding - position under tooltip arrow
489
+ // Use shared calculation to ensure perfect alignment across all screens
490
+ const bottomOffset = calculateTargetPosition();
491
+ const centerX = (screenWidth - bubbleSize.width) / 2;
492
+ const centerY = screenHeight - bubbleSize.height - bottomOffset;
493
+ animatedPosition.setValue({
494
+ x: centerX,
495
+ y: centerY
496
+ });
497
+ } else {
498
+ // Default right-side position
499
+ const defaultY = Math.max(safeAreaInsets.top + 20, Math.min(100, screenHeight - bubbleSize.height - safeAreaInsets.bottom));
500
+ animatedPosition.setValue({
501
+ x: screenWidth - bubbleSize.width - 20,
502
+ y: defaultY
503
+ });
504
+ }
505
+ }
506
+ }, [enablePositionPersistence, centerOnboarding, animatedPosition, bubbleSize.width, bubbleSize.height, safeAreaInsets.top, safeAreaInsets.bottom, screenWidth, screenHeight]);
507
+
508
+ // Cleanup timeout on component unmount
509
+ useEffect(() => {
510
+ return () => {
511
+ if (saveTimeoutRef.current) {
512
+ clearTimeout(saveTimeoutRef.current);
513
+ saveTimeoutRef.current = null;
514
+ }
515
+ };
516
+ }, []);
517
+
518
+ // Toggle hide/show function
519
+ const toggleHideShow = useCallback(() => {
520
+ const currentX = animatedPosition.x.__getValue();
521
+ const currentY = animatedPosition.y.__getValue();
522
+
523
+ // Check if the menu is visually off-screen (more than half hidden to the right)
524
+ // This handles edge cases where isHidden state might be out of sync with actual position
525
+ const isVisuallyOffScreen = currentX > screenWidth - bubbleSize.width / 2;
526
+ if (isHidden || isVisuallyOffScreen) {
527
+ // Show the bubble - restore to saved position or default visible position
528
+ let targetX;
529
+ let targetY;
530
+ if (savedPositionRef.current && savedPositionRef.current.x < screenWidth - bubbleSize.width / 2) {
531
+ // Restore to the saved position only if it's a valid visible position
532
+ targetX = savedPositionRef.current.x;
533
+ targetY = savedPositionRef.current.y;
534
+ } else {
535
+ // Default visible position if no saved position or saved position is off-screen
536
+ targetX = screenWidth - bubbleSize.width - 20;
537
+ targetY = currentY;
538
+ }
539
+
540
+ // User explicitly wants the menu visible (overrides pushToSide)
541
+ isPushedBySideRef.current = false;
542
+ userWantsVisibleRef.current = true;
543
+ setIsHidden(false);
544
+ Animated.timing(animatedPosition, {
545
+ toValue: {
546
+ x: targetX,
547
+ y: targetY
548
+ },
549
+ duration: 200,
550
+ useNativeDriver: false
551
+ }).start(() => {
552
+ savePosition(targetX, targetY);
553
+ });
554
+ } else {
555
+ // Hide the bubble - save current position before hiding
556
+ savedPositionRef.current = {
557
+ x: currentX,
558
+ y: currentY
559
+ };
560
+
561
+ // User explicitly wants the menu hidden
562
+ userWantsVisibleRef.current = false;
563
+ const hiddenX = screenWidth - 32; // Only show the 32px grabber
564
+ setIsHidden(true);
565
+ Animated.timing(animatedPosition, {
566
+ toValue: {
567
+ x: hiddenX,
568
+ y: currentY
569
+ },
570
+ duration: 200,
571
+ useNativeDriver: false
572
+ }).start(() => {
573
+ savePosition(hiddenX, currentY);
574
+ });
575
+ }
576
+ }, [animatedPosition, isHidden, bubbleSize.width, savePosition, screenWidth]);
577
+ const handleDragStart = useCallback(() => {
578
+ setIsDragging(true);
579
+ }, []);
580
+ const handleDragEnd = useCallback(finalPosition => {
581
+ let {
582
+ x: currentX,
583
+ y: currentY
584
+ } = finalPosition;
585
+
586
+ // Check if bubble is more than 50% over the right edge
587
+ const bubbleMidpoint = currentX + bubbleSize.width / 2;
588
+ const shouldHide = bubbleMidpoint > screenWidth;
589
+ if (shouldHide) {
590
+ // Animate to hidden position (only grabber visible)
591
+ const hiddenX = screenWidth - 32; // Only show the 32px grabber
592
+ setIsHidden(true);
593
+ Animated.timing(animatedPosition, {
594
+ toValue: {
595
+ x: hiddenX,
596
+ y: currentY
597
+ },
598
+ duration: 200,
599
+ useNativeDriver: false
600
+ }).start(() => {
601
+ savePosition(hiddenX, currentY);
602
+ });
603
+ } else {
604
+ // Check if we're in hidden state and user is pulling it back
605
+ if (isHidden && currentX < screenWidth - 32 - 10) {
606
+ setIsHidden(false);
607
+ }
608
+
609
+ // Update saved position if bubble is in visible area (not hidden)
610
+ if (currentX < screenWidth - bubbleSize.width / 2) {
611
+ savedPositionRef.current = {
612
+ x: currentX,
613
+ y: currentY
614
+ };
615
+ }
616
+ savePosition(currentX, currentY);
617
+ }
618
+ setIsDragging(false);
619
+ }, [animatedPosition, bubbleSize.width, isHidden, savePosition, screenWidth]);
620
+
621
+ // Stable styles
622
+ const bubbleStyle = useMemo(() => ({
623
+ position: "absolute",
624
+ zIndex: 1001,
625
+ transform: animatedPosition.getTranslateTransform()
626
+ }), [animatedPosition]);
627
+
628
+ // Get minimized tools context and app host for restoration
629
+ const {
630
+ minimizedTools
631
+ } = useMinimizedTools();
632
+ const {
633
+ restore: restoreInAppHost
634
+ } = useAppHost();
635
+
636
+ // Check if minimized tools are showing (for seamless connection)
637
+ const hasMinimizedTools = minimizedTools.length > 0;
638
+ const containerStyle = {
639
+ flexDirection: "row",
640
+ alignItems: "center",
641
+ backgroundColor: buoyColors.card,
642
+ // When minimized tools showing, remove top-left radius so they connect seamlessly
643
+ borderTopLeftRadius: hasMinimizedTools ? 0 : 6,
644
+ borderTopRightRadius: 6,
645
+ borderBottomLeftRadius: 6,
646
+ borderBottomRightRadius: 6,
647
+ // Use individual border widths when minimized tools are showing
648
+ // Always show top border when dragging for visual feedback
649
+ borderLeftWidth: isDragging ? 2 : 1,
650
+ borderRightWidth: isDragging ? 2 : 1,
651
+ borderBottomWidth: isDragging ? 2 : 1,
652
+ borderTopWidth: isDragging ? 2 : hasMinimizedTools ? 0 : 1,
653
+ borderColor: isDragging ? floatingToolsColors.dragActive : buoyColors.textMuted + "66",
654
+ overflow: "visible",
655
+ elevation: 8,
656
+ shadowColor: isDragging ? floatingToolsColors.dragActive + "99" : "#000",
657
+ shadowOffset: {
658
+ width: 0,
659
+ height: isDragging ? 6 : 4
660
+ },
661
+ shadowOpacity: isDragging ? 0.6 : 0.3,
662
+ shadowRadius: isDragging ? 12 : 8
663
+ };
664
+ const dragHandleStyle = {
665
+ paddingHorizontal: 6,
666
+ paddingVertical: 6,
667
+ backgroundColor: buoyColors.textMuted + "1A",
668
+ alignItems: "center",
669
+ justifyContent: "center",
670
+ width: 32,
671
+ borderRightWidth: 1,
672
+ borderRightColor: buoyColors.textMuted + "66",
673
+ // Remove top-left radius when minimized tools are showing
674
+ borderTopLeftRadius: hasMinimizedTools ? 0 : undefined
675
+ };
676
+ const contentStyle = {
677
+ flexDirection: "row",
678
+ alignItems: "center",
679
+ gap: 6,
680
+ paddingRight: 8
681
+ };
682
+
683
+ // Compose actions row with automatic dividers
684
+ const actions = useMemo(() => interleaveWithDividers(Children.toArray(children)), [children]);
685
+
686
+ // Handle restore from minimized tools stack
687
+ const handleMinimizedRestore = useCallback(tool => {
688
+ restoreInAppHost(tool.instanceId, tool.modalState);
689
+ }, [restoreInAppHost]);
690
+
691
+ // Width for the minimized tools stack - match the drag handle width
692
+ const minimizedStackWidth = 32;
693
+ return /*#__PURE__*/_jsx(Animated.View, {
694
+ style: bubbleStyle,
695
+ children: /*#__PURE__*/_jsxs(View, {
696
+ style: {
697
+ overflow: "visible"
698
+ },
699
+ children: [minimizedTools.length > 0 && /*#__PURE__*/_jsx(View, {
700
+ style: {
701
+ position: "absolute",
702
+ bottom: "100%",
703
+ left: 0,
704
+ width: minimizedStackWidth,
705
+ overflow: "visible",
706
+ zIndex: 1001
707
+ },
708
+ children: /*#__PURE__*/_jsx(MinimizedToolsStack, {
709
+ onRestore: handleMinimizedRestore,
710
+ containerWidth: minimizedStackWidth
711
+ })
712
+ }), /*#__PURE__*/_jsxs(View, {
713
+ hitSlop: {
714
+ top: 10,
715
+ bottom: 10,
716
+ left: 10,
717
+ right: 10
718
+ },
719
+ style: containerStyle,
720
+ onLayout: event => {
721
+ const {
722
+ width,
723
+ height
724
+ } = event.nativeEvent.layout;
725
+ setBubbleSize({
726
+ width,
727
+ height
728
+ });
729
+ },
730
+ children: [/*#__PURE__*/_jsx(DraggableHeader, {
731
+ position: animatedPosition,
732
+ onDragStart: handleDragStart,
733
+ onDragEnd: handleDragEnd,
734
+ onTap: toggleHideShow,
735
+ containerBounds: {
736
+ width: screenWidth,
737
+ height: screenHeight
738
+ },
739
+ elementSize: bubbleSize,
740
+ minPosition: {
741
+ x: safeAreaInsets.left,
742
+ y: safeAreaInsets.top + 20
743
+ },
744
+ style: dragHandleStyle,
745
+ enabled: true,
746
+ maxOverflowX: bubbleSize.width,
747
+ children: /*#__PURE__*/_jsx(GripVerticalIcon, {
748
+ size: 12,
749
+ color: buoyColors.textSecondary + "CC"
750
+ })
751
+ }), /*#__PURE__*/_jsx(FloatingToolsContext.Provider, {
752
+ value: {
753
+ isDragging
754
+ },
755
+ children: /*#__PURE__*/_jsxs(View, {
756
+ style: contentStyle,
757
+ children: [showEnvironmentSelector && environment && availableEnvironments && onEnvironmentSwitch && /*#__PURE__*/_jsx(EnvironmentSelectorInline, {
758
+ currentEnvironment: environment,
759
+ availableEnvironments: availableEnvironments,
760
+ onSelect: onEnvironmentSwitch
761
+ }), actions]
762
+ })
763
+ })]
764
+ })]
765
+ })
766
+ });
767
+ }