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