@buoy-gg/shared-ui 7.0.36 → 7.0.39

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 (284) hide show
  1. package/README.md +33 -26
  2. package/floating.d.ts +1 -0
  3. package/floating.js +2 -0
  4. package/lib/commonjs/JsModal.js +16 -1
  5. package/lib/commonjs/JsModal.web.js +16 -1
  6. package/lib/commonjs/dataViewer/CyberpunkInput.js +6 -1
  7. package/lib/commonjs/dataViewer/LiveExplorer.js +28 -16
  8. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  9. package/lib/commonjs/icons/ReactQueryIcon.js +9 -161
  10. package/lib/commonjs/icons/SentryBugIcon.js +9 -171
  11. package/lib/commonjs/index.js +57 -6
  12. package/lib/commonjs/license/AccountSetup.js +56 -0
  13. package/lib/commonjs/license/AccountSetup.web.js +81 -0
  14. package/lib/commonjs/license/FeatureGate.js +10 -43
  15. package/lib/commonjs/license/FreeAccountPrompt.js +35 -12
  16. package/lib/commonjs/license/index.js +0 -6
  17. package/lib/commonjs/license/licenseModule.js +56 -36
  18. package/lib/commonjs/license/tierLimits.js +12 -63
  19. package/lib/commonjs/storage/devToolsStorageKeys.js +8 -1
  20. package/lib/commonjs/stores/BaseEventStore.js +21 -5
  21. package/lib/commonjs/stores/createToolStore.js +21 -3
  22. package/lib/commonjs/stores/floatingToolStore.js +71 -0
  23. package/lib/commonjs/stores/floatingToolStore.test.js +112 -0
  24. package/lib/commonjs/sync/toolActionRegistry.js +85 -2
  25. package/lib/commonjs/ui/backgrounds/README.md +52 -0
  26. package/lib/commonjs/ui/backgrounds/ToolBackground.js +4 -2
  27. package/lib/commonjs/ui/backgrounds/index.js +8 -1
  28. package/lib/commonjs/ui/backgrounds/registry.js +8 -0
  29. package/lib/commonjs/ui/backgrounds/variants/wormhole/motion.js +48 -0
  30. package/lib/commonjs/ui/backgrounds/variants/wormhole/textures.js +3 -0
  31. package/lib/commonjs/ui/backgrounds/variants/wormhole.js +133 -0
  32. package/lib/commonjs/ui/components/ClipboardHintBanner.js +5 -6
  33. package/lib/commonjs/ui/components/CompactRow.js +94 -74
  34. package/lib/commonjs/ui/components/DraggableHeader.js +16 -1
  35. package/lib/commonjs/ui/components/HeaderSearchButton.js +17 -4
  36. package/lib/commonjs/ui/floating/FloatingTool.js +1010 -0
  37. package/lib/commonjs/ui/floating/FloatingTool.web.js +591 -0
  38. package/lib/commonjs/ui/floating/FloatingToolHost.js +32 -0
  39. package/lib/commonjs/ui/floating/FloatingToolHost.web.js +28 -0
  40. package/lib/commonjs/ui/floating/index.js +105 -0
  41. package/lib/commonjs/ui/floating/index.web.js +105 -0
  42. package/lib/commonjs/ui/floating/onboardingConstants.js +70 -0
  43. package/lib/commonjs/ui/floating/onboardingConstants.web.js +34 -0
  44. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +3 -1
  45. package/lib/commonjs/ui/index.js +118 -2
  46. package/lib/commonjs/ui/night/nightTheme.js +10 -0
  47. package/lib/commonjs/ui/night/primitives.js +22 -5
  48. package/lib/commonjs/ui/otto/OttoSceneView.js +69 -0
  49. package/lib/commonjs/ui/otto/index.js +22 -0
  50. package/lib/commonjs/ui/otto/ottoEngine.js +10 -7
  51. package/lib/commonjs/ui/otto/ottoScenes.js +949 -0
  52. package/lib/commonjs/utils/minimalSuperJSON.js +13 -3
  53. package/lib/module/JsModal.js +16 -1
  54. package/lib/module/JsModal.web.js +16 -1
  55. package/lib/module/dataViewer/CyberpunkInput.js +6 -1
  56. package/lib/module/dataViewer/LiveExplorer.js +28 -16
  57. package/lib/module/hooks/safe-area-impl.js +1 -1
  58. package/lib/module/icons/ReactQueryIcon.js +9 -161
  59. package/lib/module/icons/SentryBugIcon.js +9 -171
  60. package/lib/module/index.js +4 -1
  61. package/lib/module/license/AccountSetup.js +52 -0
  62. package/lib/module/license/AccountSetup.web.js +77 -0
  63. package/lib/module/license/FeatureGate.js +10 -43
  64. package/lib/module/license/FreeAccountPrompt.js +35 -12
  65. package/lib/module/license/index.js +1 -1
  66. package/lib/module/license/licenseModule.js +50 -35
  67. package/lib/module/license/tierLimits.js +13 -63
  68. package/lib/module/storage/devToolsStorageKeys.js +8 -1
  69. package/lib/module/stores/BaseEventStore.js +21 -5
  70. package/lib/module/stores/createToolStore.js +21 -3
  71. package/lib/module/stores/floatingToolStore.js +61 -0
  72. package/lib/module/stores/floatingToolStore.test.js +111 -0
  73. package/lib/module/sync/toolActionRegistry.js +84 -2
  74. package/lib/module/ui/backgrounds/README.md +52 -0
  75. package/lib/module/ui/backgrounds/ToolBackground.js +4 -2
  76. package/lib/module/ui/backgrounds/index.js +2 -1
  77. package/lib/module/ui/backgrounds/registry.js +8 -0
  78. package/lib/module/ui/backgrounds/variants/wormhole/motion.js +44 -0
  79. package/lib/module/ui/backgrounds/variants/wormhole/textures.js +3 -0
  80. package/lib/module/ui/backgrounds/variants/wormhole.js +129 -0
  81. package/lib/module/ui/components/ClipboardHintBanner.js +5 -6
  82. package/lib/module/ui/components/CompactRow.js +94 -74
  83. package/lib/module/ui/components/DraggableHeader.js +16 -1
  84. package/lib/module/ui/components/HeaderSearchButton.js +17 -4
  85. package/lib/module/ui/floating/FloatingTool.js +1003 -0
  86. package/lib/module/ui/floating/FloatingTool.web.js +590 -0
  87. package/lib/module/ui/floating/FloatingToolHost.js +29 -0
  88. package/lib/module/ui/floating/FloatingToolHost.web.js +25 -0
  89. package/lib/module/ui/floating/index.js +6 -0
  90. package/lib/module/ui/floating/index.web.js +6 -0
  91. package/lib/module/ui/floating/onboardingConstants.js +64 -0
  92. package/lib/module/ui/floating/onboardingConstants.web.js +27 -0
  93. package/lib/module/ui/gameUI/constants/gameUIColors.js +3 -1
  94. package/lib/module/ui/index.js +5 -1
  95. package/lib/module/ui/night/nightTheme.js +10 -0
  96. package/lib/module/ui/night/primitives.js +22 -5
  97. package/lib/module/ui/otto/OttoSceneView.js +64 -0
  98. package/lib/module/ui/otto/index.js +3 -1
  99. package/lib/module/ui/otto/ottoEngine.js +10 -7
  100. package/lib/module/ui/otto/ottoScenes.js +942 -0
  101. package/lib/module/utils/minimalSuperJSON.js +13 -3
  102. package/lib/typescript/commonjs/JsModal.d.ts +43 -2
  103. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/JsModal.web.d.ts +5 -2
  105. package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/dataViewer/CyberpunkInput.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  109. package/lib/typescript/commonjs/icons/ReactQueryIcon.d.ts +1 -2
  110. package/lib/typescript/commonjs/icons/ReactQueryIcon.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/icons/SentryBugIcon.d.ts +1 -2
  112. package/lib/typescript/commonjs/icons/SentryBugIcon.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/index.d.ts +4 -1
  114. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/license/AccountSetup.d.ts +4 -0
  116. package/lib/typescript/commonjs/license/AccountSetup.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/license/AccountSetup.web.d.ts +2 -0
  118. package/lib/typescript/commonjs/license/AccountSetup.web.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/license/FeatureGate.d.ts +3 -19
  120. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts +8 -11
  122. package/lib/typescript/commonjs/license/FreeAccountPrompt.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/license/index.d.ts +1 -1
  124. package/lib/typescript/commonjs/license/index.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/license/licenseModule.d.ts +18 -31
  126. package/lib/typescript/commonjs/license/licenseModule.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/license/tierLimits.d.ts +9 -52
  128. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +6 -0
  130. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/stores/BaseEventStore.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/stores/createToolStore.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/stores/floatingToolStore.d.ts +27 -0
  134. package/lib/typescript/commonjs/stores/floatingToolStore.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/stores/floatingToolStore.test.d.ts +2 -0
  136. package/lib/typescript/commonjs/stores/floatingToolStore.test.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts +4 -32
  138. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts +7 -1
  140. package/lib/typescript/commonjs/ui/backgrounds/ToolBackground.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts +2 -1
  142. package/lib/typescript/commonjs/ui/backgrounds/index.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/ui/backgrounds/registry.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts +11 -1
  145. package/lib/typescript/commonjs/ui/backgrounds/types.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole/motion.d.ts +24 -0
  147. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole/motion.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole/textures.d.ts +13 -0
  149. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole/textures.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole.d.ts +3 -0
  151. package/lib/typescript/commonjs/ui/backgrounds/variants/wormhole.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +8 -1
  154. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts +4 -1
  156. package/lib/typescript/commonjs/ui/components/DraggableHeader.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/ui/components/HeaderSearchButton.d.ts +7 -1
  158. package/lib/typescript/commonjs/ui/components/HeaderSearchButton.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/ui/floating/FloatingTool.d.ts +79 -0
  160. package/lib/typescript/commonjs/ui/floating/FloatingTool.d.ts.map +1 -0
  161. package/lib/typescript/commonjs/ui/floating/FloatingTool.web.d.ts +64 -0
  162. package/lib/typescript/commonjs/ui/floating/FloatingTool.web.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/ui/floating/FloatingToolHost.d.ts +3 -0
  164. package/lib/typescript/commonjs/ui/floating/FloatingToolHost.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/ui/floating/FloatingToolHost.web.d.ts +3 -0
  166. package/lib/typescript/commonjs/ui/floating/FloatingToolHost.web.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/ui/floating/index.d.ts +7 -0
  168. package/lib/typescript/commonjs/ui/floating/index.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/ui/floating/index.web.d.ts +7 -0
  170. package/lib/typescript/commonjs/ui/floating/index.web.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/ui/floating/onboardingConstants.d.ts +30 -0
  172. package/lib/typescript/commonjs/ui/floating/onboardingConstants.d.ts.map +1 -0
  173. package/lib/typescript/commonjs/ui/floating/onboardingConstants.web.d.ts +13 -0
  174. package/lib/typescript/commonjs/ui/floating/onboardingConstants.web.d.ts.map +1 -0
  175. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +2 -0
  176. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/ui/index.d.ts +6 -0
  178. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/ui/night/nightTheme.d.ts +10 -0
  180. package/lib/typescript/commonjs/ui/night/nightTheme.d.ts.map +1 -1
  181. package/lib/typescript/commonjs/ui/night/primitives.d.ts +10 -3
  182. package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/ui/otto/OttoSceneView.d.ts +30 -0
  184. package/lib/typescript/commonjs/ui/otto/OttoSceneView.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/ui/otto/index.d.ts +2 -0
  186. package/lib/typescript/commonjs/ui/otto/index.d.ts.map +1 -1
  187. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts +1 -1
  188. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts.map +1 -1
  189. package/lib/typescript/commonjs/ui/otto/ottoScenes.d.ts +75 -0
  190. package/lib/typescript/commonjs/ui/otto/ottoScenes.d.ts.map +1 -0
  191. package/lib/typescript/commonjs/utils/minimalSuperJSON.d.ts.map +1 -1
  192. package/lib/typescript/module/JsModal.d.ts +43 -2
  193. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  194. package/lib/typescript/module/JsModal.web.d.ts +5 -2
  195. package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
  196. package/lib/typescript/module/dataViewer/CyberpunkInput.d.ts.map +1 -1
  197. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -1
  198. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  199. package/lib/typescript/module/icons/ReactQueryIcon.d.ts +1 -2
  200. package/lib/typescript/module/icons/ReactQueryIcon.d.ts.map +1 -1
  201. package/lib/typescript/module/icons/SentryBugIcon.d.ts +1 -2
  202. package/lib/typescript/module/icons/SentryBugIcon.d.ts.map +1 -1
  203. package/lib/typescript/module/index.d.ts +4 -1
  204. package/lib/typescript/module/index.d.ts.map +1 -1
  205. package/lib/typescript/module/license/AccountSetup.d.ts +4 -0
  206. package/lib/typescript/module/license/AccountSetup.d.ts.map +1 -0
  207. package/lib/typescript/module/license/AccountSetup.web.d.ts +2 -0
  208. package/lib/typescript/module/license/AccountSetup.web.d.ts.map +1 -0
  209. package/lib/typescript/module/license/FeatureGate.d.ts +3 -19
  210. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  211. package/lib/typescript/module/license/FreeAccountPrompt.d.ts +8 -11
  212. package/lib/typescript/module/license/FreeAccountPrompt.d.ts.map +1 -1
  213. package/lib/typescript/module/license/index.d.ts +1 -1
  214. package/lib/typescript/module/license/index.d.ts.map +1 -1
  215. package/lib/typescript/module/license/licenseModule.d.ts +18 -31
  216. package/lib/typescript/module/license/licenseModule.d.ts.map +1 -1
  217. package/lib/typescript/module/license/tierLimits.d.ts +9 -52
  218. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -1
  219. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +6 -0
  220. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  221. package/lib/typescript/module/stores/BaseEventStore.d.ts.map +1 -1
  222. package/lib/typescript/module/stores/createToolStore.d.ts.map +1 -1
  223. package/lib/typescript/module/stores/floatingToolStore.d.ts +27 -0
  224. package/lib/typescript/module/stores/floatingToolStore.d.ts.map +1 -0
  225. package/lib/typescript/module/stores/floatingToolStore.test.d.ts +2 -0
  226. package/lib/typescript/module/stores/floatingToolStore.test.d.ts.map +1 -0
  227. package/lib/typescript/module/sync/toolActionRegistry.d.ts +4 -32
  228. package/lib/typescript/module/sync/toolActionRegistry.d.ts.map +1 -1
  229. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts +7 -1
  230. package/lib/typescript/module/ui/backgrounds/ToolBackground.d.ts.map +1 -1
  231. package/lib/typescript/module/ui/backgrounds/index.d.ts +2 -1
  232. package/lib/typescript/module/ui/backgrounds/index.d.ts.map +1 -1
  233. package/lib/typescript/module/ui/backgrounds/registry.d.ts.map +1 -1
  234. package/lib/typescript/module/ui/backgrounds/types.d.ts +11 -1
  235. package/lib/typescript/module/ui/backgrounds/types.d.ts.map +1 -1
  236. package/lib/typescript/module/ui/backgrounds/variants/wormhole/motion.d.ts +24 -0
  237. package/lib/typescript/module/ui/backgrounds/variants/wormhole/motion.d.ts.map +1 -0
  238. package/lib/typescript/module/ui/backgrounds/variants/wormhole/textures.d.ts +13 -0
  239. package/lib/typescript/module/ui/backgrounds/variants/wormhole/textures.d.ts.map +1 -0
  240. package/lib/typescript/module/ui/backgrounds/variants/wormhole.d.ts +3 -0
  241. package/lib/typescript/module/ui/backgrounds/variants/wormhole.d.ts.map +1 -0
  242. package/lib/typescript/module/ui/components/ClipboardHintBanner.d.ts.map +1 -1
  243. package/lib/typescript/module/ui/components/CompactRow.d.ts +8 -1
  244. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  245. package/lib/typescript/module/ui/components/DraggableHeader.d.ts +4 -1
  246. package/lib/typescript/module/ui/components/DraggableHeader.d.ts.map +1 -1
  247. package/lib/typescript/module/ui/components/HeaderSearchButton.d.ts +7 -1
  248. package/lib/typescript/module/ui/components/HeaderSearchButton.d.ts.map +1 -1
  249. package/lib/typescript/module/ui/floating/FloatingTool.d.ts +79 -0
  250. package/lib/typescript/module/ui/floating/FloatingTool.d.ts.map +1 -0
  251. package/lib/typescript/module/ui/floating/FloatingTool.web.d.ts +64 -0
  252. package/lib/typescript/module/ui/floating/FloatingTool.web.d.ts.map +1 -0
  253. package/lib/typescript/module/ui/floating/FloatingToolHost.d.ts +3 -0
  254. package/lib/typescript/module/ui/floating/FloatingToolHost.d.ts.map +1 -0
  255. package/lib/typescript/module/ui/floating/FloatingToolHost.web.d.ts +3 -0
  256. package/lib/typescript/module/ui/floating/FloatingToolHost.web.d.ts.map +1 -0
  257. package/lib/typescript/module/ui/floating/index.d.ts +7 -0
  258. package/lib/typescript/module/ui/floating/index.d.ts.map +1 -0
  259. package/lib/typescript/module/ui/floating/index.web.d.ts +7 -0
  260. package/lib/typescript/module/ui/floating/index.web.d.ts.map +1 -0
  261. package/lib/typescript/module/ui/floating/onboardingConstants.d.ts +30 -0
  262. package/lib/typescript/module/ui/floating/onboardingConstants.d.ts.map +1 -0
  263. package/lib/typescript/module/ui/floating/onboardingConstants.web.d.ts +13 -0
  264. package/lib/typescript/module/ui/floating/onboardingConstants.web.d.ts.map +1 -0
  265. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +2 -0
  266. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  267. package/lib/typescript/module/ui/index.d.ts +6 -0
  268. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  269. package/lib/typescript/module/ui/night/nightTheme.d.ts +10 -0
  270. package/lib/typescript/module/ui/night/nightTheme.d.ts.map +1 -1
  271. package/lib/typescript/module/ui/night/primitives.d.ts +10 -3
  272. package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -1
  273. package/lib/typescript/module/ui/otto/OttoSceneView.d.ts +30 -0
  274. package/lib/typescript/module/ui/otto/OttoSceneView.d.ts.map +1 -0
  275. package/lib/typescript/module/ui/otto/index.d.ts +2 -0
  276. package/lib/typescript/module/ui/otto/index.d.ts.map +1 -1
  277. package/lib/typescript/module/ui/otto/ottoEngine.d.ts +1 -1
  278. package/lib/typescript/module/ui/otto/ottoEngine.d.ts.map +1 -1
  279. package/lib/typescript/module/ui/otto/ottoScenes.d.ts +75 -0
  280. package/lib/typescript/module/ui/otto/ottoScenes.d.ts.map +1 -0
  281. package/lib/typescript/module/utils/minimalSuperJSON.d.ts.map +1 -1
  282. package/package.json +25 -5
  283. package/scripts/generate-wormhole-textures.cjs +261 -0
  284. package/scripts/test-wormhole-motion.cjs +59 -0
@@ -0,0 +1,1003 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useMemo, useRef, useState, useContext, createContext, useCallback, Children } from "react";
4
+ import { Animated, Dimensions, useWindowDimensions, View, Text, TouchableOpacity } from "react-native";
5
+ import { night } from "../night/nightTheme.js";
6
+ import { DraggableHeader } from "../components/DraggableHeader.js";
7
+ import { EnvironmentSelectorInline } from "../../env/EnvironmentSelectorInline";
8
+ import { GripVerticalIcon } from "../components/GripVerticalIcon.js";
9
+ import { buoyColors } from "../gameUI/constants/gameUIColors.js";
10
+ import { getSafeAreaInsets, persistentStorage } from "../../utils/index.js";
11
+ import { useSafeAreaInsets } from "../../hooks/index.js";
12
+ import { calculateTargetPosition } from "./onboardingConstants";
13
+
14
+ // Shared utilities from floating-tools-core
15
+ import { getUserStatusConfig as getStatusConfig, 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
+ const STORAGE_KEYS = {
22
+ BUBBLE_POSITION_X: "@react_buoy_bubble_position_x",
23
+ BUBBLE_POSITION_Y: "@react_buoy_bubble_position_y"
24
+ };
25
+
26
+ // Position constants
27
+ const VISIBLE_HANDLE_WIDTH = 32;
28
+ const EDGE_PADDING = 20;
29
+ const LAYOUT_SETTLE_DELAY_MS = 150;
30
+
31
+ /** Helper to get current value from Animated.Value without triggering updates */
32
+ const getAnimatedValue = value => value.__getValue();
33
+
34
+ // =============================
35
+ // Position persistence hook
36
+ // Extracted logic dedicated to state/IO
37
+ // =============================
38
+ /**
39
+ * Custom hook for managing floating tools position persistence
40
+ *
41
+ * Handles loading, saving, and validating the position of the floating tools bubble
42
+ * with automatic boundary checking and storage management.
43
+ *
44
+ * @param props - Configuration for position management
45
+ * @param props.animatedPosition - Animated.ValueXY for position updates
46
+ * @param props.bubbleWidth - Width of the bubble for boundary calculations
47
+ * @param props.bubbleHeight - Height of the bubble for boundary calculations
48
+ * @param props.enabled - Whether position persistence is enabled
49
+ * @param props.visibleHandleWidth - Width of visible handle when bubble is hidden
50
+ * @param props.listenersSuspended - Pause automatic listeners without disabling manual saves
51
+ * @param props.onPositionLoaded - Callback fired once position is loaded and set
52
+ *
53
+ * @returns Object containing position management functions and loading state
54
+ *
55
+ * @performance Uses debounced saving to avoid excessive storage operations
56
+ * @performance Validates positions against screen boundaries and safe areas
57
+ */
58
+ function useFloatingToolsPosition({
59
+ animatedPosition,
60
+ bubbleWidth = 100,
61
+ bubbleHeight = 32,
62
+ enabled = true,
63
+ visibleHandleWidth = 32,
64
+ listenersSuspended = false,
65
+ defaultPlacement = "top",
66
+ onPositionLoaded,
67
+ storageKey
68
+ }) {
69
+ // Use state instead of ref so we can react to changes and prevent races
70
+ const [isInitialized, setIsInitialized] = useState(false);
71
+ const saveTimeoutRef = useRef(undefined);
72
+ // Track if component is mounted to prevent state updates after unmount
73
+ const isMountedRef = useRef(true);
74
+
75
+ // Cleanup on unmount - cancel any pending saves
76
+ useEffect(() => {
77
+ isMountedRef.current = true;
78
+ return () => {
79
+ isMountedRef.current = false;
80
+ // Clear any pending debounced saves on unmount to prevent saving stale/invalid positions
81
+ if (saveTimeoutRef.current) {
82
+ clearTimeout(saveTimeoutRef.current);
83
+ saveTimeoutRef.current = undefined;
84
+ }
85
+ };
86
+ }, []);
87
+ const savePosition = useCallback(async (x, y) => {
88
+ if (!enabled) return;
89
+ // Don't save if component unmounted or position is clearly invalid
90
+ if (!isMountedRef.current) return;
91
+ // Guard against saving invalid positions (e.g., during HMR with stale dimensions)
92
+ const {
93
+ width: screenWidth
94
+ } = Dimensions.get("window");
95
+ if (screenWidth <= 0 || x < 0 || y < 0) return;
96
+ try {
97
+ await Promise.all([persistentStorage.setItem(storageKey ? `${storageKey}:x` : STORAGE_KEYS.BUBBLE_POSITION_X, x.toString()), persistentStorage.setItem(storageKey ? `${storageKey}:y` : STORAGE_KEYS.BUBBLE_POSITION_Y, y.toString())]);
98
+ } catch (error) {
99
+ // Failed to save position - continue without persistence
100
+ }
101
+ }, [enabled, storageKey]);
102
+ const debouncedSavePosition = useCallback((x, y) => {
103
+ // Don't schedule saves if unmounted
104
+ if (!isMountedRef.current) return;
105
+ if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
106
+ saveTimeoutRef.current = setTimeout(() => {
107
+ // Double-check mount status before saving
108
+ if (isMountedRef.current) {
109
+ savePosition(x, y);
110
+ }
111
+ }, 500);
112
+ }, [savePosition]);
113
+ const loadPosition = useCallback(async () => {
114
+ if (!enabled) return null;
115
+ try {
116
+ const [xStr, yStr] = await Promise.all([persistentStorage.getItem(storageKey ? `${storageKey}:x` : STORAGE_KEYS.BUBBLE_POSITION_X), persistentStorage.getItem(storageKey ? `${storageKey}:y` : STORAGE_KEYS.BUBBLE_POSITION_Y)]);
117
+ if (xStr !== null && yStr !== null) {
118
+ const x = parseFloat(xStr);
119
+ const y = parseFloat(yStr);
120
+ if (!Number.isNaN(x) && !Number.isNaN(y)) {
121
+ return {
122
+ x,
123
+ y
124
+ };
125
+ }
126
+ }
127
+ } catch (error) {
128
+ // Failed to load position - use default
129
+ }
130
+ return null;
131
+ }, [enabled, storageKey]);
132
+ const validatePosition = useCallback(position => {
133
+ const {
134
+ width: screenWidth,
135
+ height: screenHeight
136
+ } = Dimensions.get("window");
137
+ const safeArea = getSafeAreaInsets();
138
+
139
+ // Guard against invalid screen dimensions (can happen during HMR)
140
+ // Return the position as-is but mark as invalid so caller knows not to trust it
141
+ if (screenWidth <= 0 || screenHeight <= 0) {
142
+ return {
143
+ ...position,
144
+ isValid: false
145
+ };
146
+ }
147
+
148
+ // Prevent going off left, top, and bottom edges with safe area
149
+ // Allow pushing off-screen to the right so only the grab handle remains visible
150
+ const minX = safeArea.left;
151
+ const maxX = screenWidth - visibleHandleWidth;
152
+ // Add padding below the safe area top to ensure bubble doesn't go behind notch
153
+ const minY = safeArea.top + EDGE_PADDING;
154
+ const maxY = screenHeight - bubbleHeight - safeArea.bottom; // Respect safe area bottom
155
+ const clamped = {
156
+ x: Math.max(minX, Math.min(position.x, maxX)),
157
+ y: Math.max(minY, Math.min(position.y, maxY)),
158
+ isValid: true
159
+ };
160
+ return clamped;
161
+ }, [visibleHandleWidth, bubbleHeight]);
162
+ useEffect(() => {
163
+ if (!enabled || isInitialized) return;
164
+ let cancelled = false;
165
+ const restore = async () => {
166
+ const saved = await loadPosition();
167
+
168
+ // Check if cancelled or unmounted before updating state
169
+ if (cancelled || !isMountedRef.current) return;
170
+ const {
171
+ width: screenWidth,
172
+ height: screenHeight
173
+ } = Dimensions.get("window");
174
+ const safeArea = getSafeAreaInsets();
175
+
176
+ // If dimensions are invalid (HMR scenario), wait and retry
177
+ if (screenWidth <= 0 || screenHeight <= 0) {
178
+ // Schedule a retry after a short delay
179
+ setTimeout(() => {
180
+ if (isMountedRef.current && !cancelled) {
181
+ restore();
182
+ }
183
+ }, 100);
184
+ return;
185
+ }
186
+ let finalPosition;
187
+ let wasHidden = false;
188
+ if (saved) {
189
+ const validated = validatePosition(saved);
190
+
191
+ // If validation returned invalid, retry later
192
+ if (!validated.isValid) {
193
+ setTimeout(() => {
194
+ if (isMountedRef.current && !cancelled) {
195
+ restore();
196
+ }
197
+ }, 100);
198
+ return;
199
+ }
200
+
201
+ // Check if the saved position is out of bounds
202
+ const wasOutOfBounds = Math.abs(saved.x - validated.x) > 5 || Math.abs(saved.y - validated.y) > 5;
203
+ if (wasOutOfBounds) {
204
+ // Save the corrected position
205
+ await savePosition(validated.x, validated.y);
206
+ }
207
+ finalPosition = {
208
+ x: validated.x,
209
+ y: validated.y
210
+ };
211
+
212
+ // Check if loaded in hidden state (bubble pushed to right edge)
213
+ wasHidden = validated.x >= screenWidth - visibleHandleWidth - 5;
214
+ } else {
215
+ const defaultY = defaultPlacement === "bottom" ? Math.max(safeArea.top + EDGE_PADDING, screenHeight - bubbleHeight - safeArea.bottom - EDGE_PADDING) : Math.max(safeArea.top + EDGE_PADDING, Math.min(100, screenHeight - bubbleHeight - safeArea.bottom));
216
+ finalPosition = {
217
+ x: screenWidth - bubbleWidth - EDGE_PADDING,
218
+ y: defaultY
219
+ };
220
+ }
221
+
222
+ // Final mount check before updating
223
+ if (cancelled || !isMountedRef.current) return;
224
+ animatedPosition.setValue(finalPosition);
225
+ setIsInitialized(true);
226
+ onPositionLoaded?.(finalPosition, wasHidden);
227
+ };
228
+ restore();
229
+ return () => {
230
+ cancelled = true;
231
+ };
232
+ }, [enabled, isInitialized, animatedPosition, loadPosition, validatePosition, savePosition, bubbleWidth, bubbleHeight, visibleHandleWidth, onPositionLoaded]);
233
+
234
+ // Listener effect - now properly depends on isInitialized state
235
+ useEffect(() => {
236
+ // Don't attach listener until position is loaded
237
+ if (!enabled || !isInitialized || listenersSuspended) return;
238
+ const listener = animatedPosition.addListener(value => {
239
+ debouncedSavePosition(value.x, value.y);
240
+ });
241
+ return () => {
242
+ animatedPosition.removeListener(listener);
243
+ // Clear debounce timer when listener is removed
244
+ if (saveTimeoutRef.current) {
245
+ clearTimeout(saveTimeoutRef.current);
246
+ saveTimeoutRef.current = undefined;
247
+ }
248
+ };
249
+ }, [enabled, isInitialized, listenersSuspended, animatedPosition, debouncedSavePosition]);
250
+ return {
251
+ savePosition,
252
+ isInitialized
253
+ };
254
+ }
255
+
256
+ // =============================
257
+ // UI-only leaf components
258
+ // =============================
259
+ export function Divider() {
260
+ const dividerStyle = {
261
+ width: 1,
262
+ height: 12,
263
+ backgroundColor: buoyColors.textMuted + "66",
264
+ flexShrink: 0
265
+ };
266
+ return /*#__PURE__*/_jsx(View, {
267
+ style: dividerStyle
268
+ });
269
+ }
270
+
271
+ // Use shared getUserStatusConfig from floating-tools-core
272
+ // Wraps the core function to use our buoyColors (mapped to gameUIColors format)
273
+ const gameUIColorsCompat = {
274
+ success: "#20C997",
275
+ // teal
276
+ warning: "#FFA94D",
277
+ error: "#EF4444",
278
+ muted: buoyColors.textMuted,
279
+ secondary: buoyColors.textSecondary,
280
+ info: "#20C997",
281
+ // teal
282
+ primary: "#20C997",
283
+ optional: buoyColors.textSecondary // optional status color
284
+ };
285
+ function getUserStatusConfig(userRole) {
286
+ return getStatusConfig(userRole, gameUIColorsCompat);
287
+ }
288
+
289
+ // Context to avoid brittle prop threading and keep API composable
290
+ const FloatingToolsContext = /*#__PURE__*/createContext({
291
+ isDragging: false
292
+ });
293
+ export function UserStatus({
294
+ userRole,
295
+ onPress
296
+ }) {
297
+ const {
298
+ isDragging
299
+ } = useContext(FloatingToolsContext);
300
+ const config = getUserStatusConfig(userRole);
301
+ const containerStyle = {
302
+ flexDirection: "row",
303
+ alignItems: "center",
304
+ paddingVertical: 6,
305
+ paddingHorizontal: 8,
306
+ flexShrink: 0
307
+ };
308
+ const dotStyle = {
309
+ width: 6,
310
+ height: 6,
311
+ borderRadius: 3,
312
+ backgroundColor: config.dotColor,
313
+ marginRight: 4
314
+ };
315
+ const textStyle = {
316
+ fontSize: 10,
317
+ fontWeight: "500",
318
+ color: config.textColor,
319
+ letterSpacing: 0.3
320
+ };
321
+ if (!onPress) {
322
+ return /*#__PURE__*/_jsxs(View, {
323
+ style: containerStyle,
324
+ accessibilityLabel: `User status: ${config.label}`,
325
+ children: [/*#__PURE__*/_jsx(View, {
326
+ style: dotStyle
327
+ }), /*#__PURE__*/_jsx(Text, {
328
+ style: textStyle,
329
+ children: config.label
330
+ })]
331
+ });
332
+ }
333
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
334
+ accessibilityRole: "button",
335
+ accessibilityLabel: "Open Dev Tools Menu",
336
+ testID: "open-dev-tools-button",
337
+ onPress: onPress,
338
+ hitSlop: {
339
+ top: 10,
340
+ bottom: 10,
341
+ left: 0,
342
+ right: 10
343
+ },
344
+ disabled: isDragging,
345
+ activeOpacity: 0.85,
346
+ style: containerStyle,
347
+ children: [/*#__PURE__*/_jsx(View, {
348
+ style: dotStyle
349
+ }), /*#__PURE__*/_jsx(Text, {
350
+ style: textStyle,
351
+ children: config.label
352
+ })]
353
+ });
354
+ }
355
+
356
+ // =============================
357
+ // Helpers (using shared filterValidChildren from core)
358
+ // =============================
359
+ function interleaveWithDividers(childrenArray) {
360
+ const validChildren = filterValidChildren(childrenArray);
361
+ const result = [];
362
+ validChildren.forEach((child, index) => {
363
+ result.push(child);
364
+ if (index < validChildren.length - 1) result.push(/*#__PURE__*/_jsx(Divider, {}, `divider-${index}`));
365
+ });
366
+ return result;
367
+ }
368
+
369
+ // =============================
370
+ // Main Component (presentation only)
371
+ // =============================
372
+
373
+ /**
374
+ * FloatingTools - A draggable, resizable bubble for development tools
375
+ *
376
+ * This component provides a floating bubble interface that can contain various
377
+ * development tools and controls. It features:
378
+ * - Drag and drop positioning with boundary constraints
379
+ * - Hide/show functionality by dragging to screen edge
380
+ * - Position persistence across app restarts
381
+ * - Safe area aware positioning
382
+ * - Automatic divider insertion between child components
383
+ *
384
+ * @param props - Configuration for the floating tools
385
+ * @param props.enablePositionPersistence - Whether to save/restore position (default: true)
386
+ * @param props.children - Child components to render in the bubble
387
+ *
388
+ * @returns JSX.Element representing the floating tools bubble
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * <FloatingTools enablePositionPersistence={true}>
393
+ * <UserStatus userRole="admin" onPress={handleUserPress} />
394
+ * <ToolButton onPress={openSettings} />
395
+ * </FloatingTools>
396
+ * ```
397
+ *
398
+ * @performance Uses native driver animations for smooth positioning
399
+ * @performance Implements efficient boundary checking and position validation
400
+ * @performance Includes debounced position saving for optimal storage performance
401
+ */
402
+ export function FloatingTool({
403
+ id,
404
+ suspendPointerEvents = false,
405
+ revealToken,
406
+ variant = "bubble",
407
+ dragFromSurface = false,
408
+ accessibilityLabel = "Move dev tools bubble",
409
+ minimizedStack,
410
+ hasMinimizedStack = false,
411
+ accessory,
412
+ handleAdornment,
413
+ enablePositionPersistence = true,
414
+ defaultPlacement = "top",
415
+ pushToSide = false,
416
+ centerOnboarding = false,
417
+ children,
418
+ environment,
419
+ availableEnvironments,
420
+ onEnvironmentSwitch,
421
+ showEnvironmentSelector = false
422
+ }) {
423
+ // Animated position and drag state
424
+ const animatedPosition = useRef(new Animated.ValueXY()).current;
425
+ const [isDragging, setIsDragging] = useState(false);
426
+ const [bubbleSize, setBubbleSize] = useState({
427
+ width: 100,
428
+ height: 32
429
+ });
430
+ const [isHidden, setIsHidden] = useState(false);
431
+ const [accessoryHeight, setAccessoryHeight] = useState(0);
432
+ const [accessoryAbove, setAccessoryAbove] = useState(false);
433
+
434
+ // Store the position before hiding to restore when showing
435
+ const savedPositionRef = useRef(null);
436
+
437
+ // Track if the hide state was triggered by pushToSide prop vs user toggle
438
+ const isPushedBySideRef = useRef(false);
439
+
440
+ // Track if user has explicitly chosen to show the menu (overriding pushToSide)
441
+ const userWantsVisibleRef = useRef(false);
442
+
443
+ // Track previous pushToSide value to detect transitions
444
+ const prevPushToSideRef = useRef(pushToSide);
445
+
446
+ // Track previous bubble width for auto-adjustment when tools are added/removed
447
+ const prevBubbleWidthRef = useRef(null);
448
+
449
+ // Track if layout has settled (after initial render cycle completes)
450
+ const [isLayoutSettled, setIsLayoutSettled] = useState(false);
451
+ const safeAreaInsets = useSafeAreaInsets();
452
+ const {
453
+ width: screenWidth,
454
+ height: screenHeight
455
+ } = useWindowDimensions();
456
+
457
+ // Callback when position is loaded - sets hidden state properly without racing
458
+ const handlePositionLoaded = useCallback((position, wasHidden) => {
459
+ if (wasHidden) {
460
+ setIsHidden(true);
461
+ }
462
+ }, []);
463
+
464
+ // Position persistence (state/IO extracted to hook)
465
+ const {
466
+ savePosition,
467
+ isInitialized: isPositionInitialized
468
+ } = useFloatingToolsPosition({
469
+ animatedPosition,
470
+ bubbleWidth: bubbleSize.width,
471
+ bubbleHeight: bubbleSize.height,
472
+ enabled: enablePositionPersistence,
473
+ visibleHandleWidth: VISIBLE_HANDLE_WIDTH,
474
+ listenersSuspended: isDragging,
475
+ defaultPlacement,
476
+ onPositionLoaded: handlePositionLoaded,
477
+ storageKey: id ? `@buoy/floating-tool/${id}` : undefined
478
+ });
479
+
480
+ // Effect to handle pushToSide prop changes
481
+ useEffect(() => {
482
+ // Don't process pushToSide until position is initialized (when persistence enabled)
483
+ if (enablePositionPersistence && !isPositionInitialized) return;
484
+
485
+ // Reset user override when pushToSide becomes true (dial/modal opens)
486
+ // This allows auto-hide to work after user manually showed the menu
487
+ if (!prevPushToSideRef.current && pushToSide) {
488
+ userWantsVisibleRef.current = false;
489
+ }
490
+ prevPushToSideRef.current = pushToSide;
491
+
492
+ // Only push to side if:
493
+ // 1. pushToSide is true
494
+ // 2. Not currently hidden
495
+ // 3. Not being dragged
496
+ // 4. User hasn't manually restored (userWantsVisibleRef)
497
+ if (pushToSide && !isHidden && !isDragging && !userWantsVisibleRef.current) {
498
+ // Push to side
499
+ const currentX = getAnimatedValue(animatedPosition.x);
500
+ const currentY = getAnimatedValue(animatedPosition.y);
501
+
502
+ // Save current position
503
+ savedPositionRef.current = {
504
+ x: currentX,
505
+ y: currentY
506
+ };
507
+ const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
508
+ isPushedBySideRef.current = true;
509
+ setIsHidden(true);
510
+ Animated.timing(animatedPosition, {
511
+ toValue: {
512
+ x: hiddenX,
513
+ y: currentY
514
+ },
515
+ duration: 200,
516
+ useNativeDriver: false
517
+ }).start(() => {
518
+ savePosition(hiddenX, currentY);
519
+ });
520
+ } else if (!pushToSide && isHidden && isPushedBySideRef.current) {
521
+ // Restore from side when pushToSide becomes false
522
+ if (savedPositionRef.current) {
523
+ const {
524
+ x: targetX,
525
+ y: targetY
526
+ } = savedPositionRef.current;
527
+ isPushedBySideRef.current = false;
528
+ userWantsVisibleRef.current = false; // Reset user override when tools close
529
+ setIsHidden(false);
530
+ Animated.timing(animatedPosition, {
531
+ toValue: {
532
+ x: targetX,
533
+ y: targetY
534
+ },
535
+ duration: 200,
536
+ useNativeDriver: false
537
+ }).start(() => {
538
+ savePosition(targetX, targetY);
539
+ });
540
+ }
541
+ }
542
+ }, [enablePositionPersistence, isPositionInitialized, pushToSide, isHidden, isDragging, screenWidth, animatedPosition, savePosition]);
543
+
544
+ // Hidden state is now set via onPositionLoaded callback - no racing effect needed
545
+
546
+ // Wait for layout to settle before enabling width tracking
547
+ // This prevents false-positive width changes during initial render when
548
+ // the bubble measures multiple times as children render
549
+ useEffect(() => {
550
+ if (!isPositionInitialized || isLayoutSettled) return;
551
+ const timer = setTimeout(() => {
552
+ // Capture the current width as the baseline AFTER layout has settled
553
+ prevBubbleWidthRef.current = bubbleSize.width;
554
+ setIsLayoutSettled(true);
555
+ }, LAYOUT_SETTLE_DELAY_MS);
556
+ return () => clearTimeout(timer);
557
+ }, [isPositionInitialized, isLayoutSettled, bubbleSize.width]);
558
+
559
+ // Auto-adjust position when bubble width changes (tools added/removed)
560
+ // This keeps the bubble's right edge in the same relative position
561
+ useEffect(() => {
562
+ // Skip if position not initialized yet
563
+ if (!isPositionInitialized) return;
564
+
565
+ // Skip until layout has settled (prevents false-positive width changes during initial render)
566
+ if (!isLayoutSettled) return;
567
+
568
+ // Skip if prevBubbleWidthRef hasn't been set yet
569
+ if (prevBubbleWidthRef.current === null) return;
570
+
571
+ // Skip if currently dragging - let drag handle position
572
+ if (isDragging) {
573
+ prevBubbleWidthRef.current = bubbleSize.width;
574
+ return;
575
+ }
576
+ const prevWidth = prevBubbleWidthRef.current;
577
+ const newWidth = bubbleSize.width;
578
+ const widthDelta = newWidth - prevWidth;
579
+
580
+ // Skip if no significant change (avoid jitter)
581
+ if (Math.abs(widthDelta) < 2) {
582
+ return;
583
+ }
584
+
585
+ // Update ref first
586
+ prevBubbleWidthRef.current = newWidth;
587
+
588
+ // If hidden, only adjust the saved position (not the current animated position)
589
+ if (isHidden) {
590
+ if (savedPositionRef.current) {
591
+ const adjustedX = savedPositionRef.current.x - widthDelta;
592
+ // Clamp to valid bounds
593
+ savedPositionRef.current.x = Math.max(safeAreaInsets.left, Math.min(adjustedX, screenWidth - newWidth - EDGE_PADDING));
594
+ }
595
+ return;
596
+ }
597
+
598
+ // Get current position
599
+ const currentX = getAnimatedValue(animatedPosition.x);
600
+ const currentY = getAnimatedValue(animatedPosition.y);
601
+
602
+ // Calculate new X position
603
+ // If bubble grew (positive delta), move left (decrease X)
604
+ // If bubble shrunk (negative delta), move right (increase X)
605
+ const newX = currentX - widthDelta;
606
+
607
+ // Validate new position is within bounds
608
+ const clampedX = Math.max(safeAreaInsets.left, Math.min(newX, screenWidth - VISIBLE_HANDLE_WIDTH));
609
+
610
+ // Only animate if position actually changed
611
+ if (Math.abs(clampedX - currentX) > 1) {
612
+ Animated.timing(animatedPosition, {
613
+ toValue: {
614
+ x: clampedX,
615
+ y: currentY
616
+ },
617
+ duration: 150,
618
+ useNativeDriver: false
619
+ }).start(() => {
620
+ savePosition(clampedX, currentY);
621
+ // Update savedPositionRef so show/hide works correctly
622
+ if (savedPositionRef.current) {
623
+ savedPositionRef.current.x = clampedX;
624
+ }
625
+ });
626
+ }
627
+ }, [bubbleSize.width, isPositionInitialized, isLayoutSettled, isDragging, isHidden, animatedPosition, screenWidth, safeAreaInsets.left, savePosition]);
628
+
629
+ // Default position when persistence disabled or during onboarding
630
+ useEffect(() => {
631
+ if (!enablePositionPersistence) {
632
+ if (centerOnboarding) {
633
+ // Center the bubble for onboarding - position under tooltip arrow
634
+ // Use shared calculation to ensure perfect alignment across all screens
635
+ const bottomOffset = calculateTargetPosition();
636
+ const centerX = (screenWidth - bubbleSize.width) / 2;
637
+ const centerY = screenHeight - bubbleSize.height - bottomOffset;
638
+ animatedPosition.setValue({
639
+ x: centerX,
640
+ y: centerY
641
+ });
642
+ } else {
643
+ // Default right-side position
644
+ const defaultY = defaultPlacement === "bottom" ? Math.max(safeAreaInsets.top + EDGE_PADDING, screenHeight - bubbleSize.height - safeAreaInsets.bottom - EDGE_PADDING) : Math.max(safeAreaInsets.top + EDGE_PADDING, Math.min(100, screenHeight - bubbleSize.height - safeAreaInsets.bottom));
645
+ animatedPosition.setValue({
646
+ x: screenWidth - bubbleSize.width - EDGE_PADDING,
647
+ y: defaultY
648
+ });
649
+ }
650
+ }
651
+ }, [enablePositionPersistence, centerOnboarding, animatedPosition, bubbleSize.width, bubbleSize.height, safeAreaInsets.top, safeAreaInsets.bottom, screenWidth, screenHeight]);
652
+
653
+ // Toggle hide/show function
654
+ const toggleHideShow = useCallback(() => {
655
+ const currentX = getAnimatedValue(animatedPosition.x);
656
+ const currentY = getAnimatedValue(animatedPosition.y);
657
+
658
+ // Check if the menu is visually off-screen (more than half hidden to the right)
659
+ // This handles edge cases where isHidden state might be out of sync with actual position
660
+ const isVisuallyOffScreen = currentX > screenWidth - bubbleSize.width / 2;
661
+ if (isHidden || isVisuallyOffScreen) {
662
+ // Show the bubble - restore to saved position or default visible position
663
+ let targetX;
664
+ let targetY;
665
+ if (savedPositionRef.current && savedPositionRef.current.x < screenWidth - bubbleSize.width / 2) {
666
+ // Restore to the saved position only if it's a valid visible position
667
+ targetX = savedPositionRef.current.x;
668
+ targetY = savedPositionRef.current.y;
669
+ } else {
670
+ // Default visible position if no saved position or saved position is off-screen
671
+ targetX = screenWidth - bubbleSize.width - EDGE_PADDING;
672
+ targetY = currentY;
673
+ }
674
+
675
+ // User explicitly wants the menu visible (overrides pushToSide)
676
+ isPushedBySideRef.current = false;
677
+ userWantsVisibleRef.current = true;
678
+ setIsHidden(false);
679
+ Animated.timing(animatedPosition, {
680
+ toValue: {
681
+ x: targetX,
682
+ y: targetY
683
+ },
684
+ duration: 200,
685
+ useNativeDriver: false
686
+ }).start(() => {
687
+ savePosition(targetX, targetY);
688
+ });
689
+ } else {
690
+ // Hide the bubble - save current position before hiding
691
+ savedPositionRef.current = {
692
+ x: currentX,
693
+ y: currentY
694
+ };
695
+
696
+ // User explicitly wants the menu hidden
697
+ userWantsVisibleRef.current = false;
698
+ const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
699
+ setIsHidden(true);
700
+ Animated.timing(animatedPosition, {
701
+ toValue: {
702
+ x: hiddenX,
703
+ y: currentY
704
+ },
705
+ duration: 200,
706
+ useNativeDriver: false
707
+ }).start(() => {
708
+ savePosition(hiddenX, currentY);
709
+ });
710
+ }
711
+ }, [animatedPosition, isHidden, bubbleSize.width, savePosition, screenWidth]);
712
+ const handleDragStart = useCallback(() => {
713
+ setIsDragging(true);
714
+ }, []);
715
+ const handleDragEnd = useCallback(finalPosition => {
716
+ let {
717
+ x: currentX,
718
+ y: currentY
719
+ } = finalPosition;
720
+
721
+ // Check if bubble is more than 50% over the right edge
722
+ const bubbleMidpoint = currentX + bubbleSize.width / 2;
723
+ const shouldHide = bubbleMidpoint > screenWidth;
724
+ if (shouldHide) {
725
+ // Animate to hidden position (only grabber visible)
726
+ const hiddenX = screenWidth - VISIBLE_HANDLE_WIDTH;
727
+ setIsHidden(true);
728
+
729
+ // Update savedPositionRef to preserve the Y position when dragging while hidden
730
+ // This ensures that when showing, the bubble restores to the new Y position
731
+ if (savedPositionRef.current) {
732
+ savedPositionRef.current.y = currentY;
733
+ } else {
734
+ // If no saved position exists, create one with default visible X
735
+ savedPositionRef.current = {
736
+ x: screenWidth - bubbleSize.width - EDGE_PADDING,
737
+ y: currentY
738
+ };
739
+ }
740
+ Animated.timing(animatedPosition, {
741
+ toValue: {
742
+ x: hiddenX,
743
+ y: currentY
744
+ },
745
+ duration: 200,
746
+ useNativeDriver: false
747
+ }).start(() => {
748
+ savePosition(hiddenX, currentY);
749
+ });
750
+ } else {
751
+ // Check if we're in hidden state and user is pulling it back
752
+ if (isHidden && currentX < screenWidth - VISIBLE_HANDLE_WIDTH - 10) {
753
+ setIsHidden(false);
754
+ }
755
+
756
+ // Update saved position if bubble is in visible area (not hidden)
757
+ if (currentX < screenWidth - bubbleSize.width / 2) {
758
+ savedPositionRef.current = {
759
+ x: currentX,
760
+ y: currentY
761
+ };
762
+ }
763
+ savePosition(currentX, currentY);
764
+ }
765
+ setIsDragging(false);
766
+ }, [animatedPosition, bubbleSize.width, isHidden, savePosition, screenWidth]);
767
+ const lastViewport = useRef({
768
+ width: screenWidth,
769
+ height: screenHeight
770
+ });
771
+ useEffect(() => {
772
+ if (enablePositionPersistence && !isPositionInitialized) return;
773
+ if (lastViewport.current.width === screenWidth && lastViewport.current.height === screenHeight) return;
774
+ lastViewport.current = {
775
+ width: screenWidth,
776
+ height: screenHeight
777
+ };
778
+ const clamp = point => ({
779
+ x: Math.max(safeAreaInsets.left, Math.min(point.x, screenWidth - bubbleSize.width - EDGE_PADDING)),
780
+ y: Math.max(safeAreaInsets.top + EDGE_PADDING, Math.min(point.y, screenHeight - bubbleSize.height - safeAreaInsets.bottom))
781
+ });
782
+ const current = clamp({
783
+ x: getAnimatedValue(animatedPosition.x),
784
+ y: getAnimatedValue(animatedPosition.y)
785
+ });
786
+ if (isHidden) current.x = screenWidth - VISIBLE_HANDLE_WIDTH;
787
+ if (savedPositionRef.current) savedPositionRef.current = clamp(savedPositionRef.current);
788
+ animatedPosition.stopAnimation();
789
+ animatedPosition.setValue(current);
790
+ void savePosition(current.x, current.y);
791
+ }, [screenWidth, screenHeight, isPositionInitialized, enablePositionPersistence, bubbleSize, safeAreaInsets, isHidden, animatedPosition, savePosition]);
792
+ const hasAccessory = Boolean(accessory);
793
+ useEffect(() => {
794
+ if (!hasAccessory) return;
795
+ const update = ({
796
+ y
797
+ }) => setAccessoryAbove(y + bubbleSize.height + accessoryHeight > screenHeight - safeAreaInsets.bottom);
798
+ update({
799
+ y: getAnimatedValue(animatedPosition.y)
800
+ });
801
+ const listener = animatedPosition.addListener(update);
802
+ return () => animatedPosition.removeListener(listener);
803
+ }, [animatedPosition, bubbleSize.height, accessoryHeight, screenHeight, safeAreaInsets.bottom, hasAccessory]);
804
+
805
+ // Stable styles
806
+ const bubbleStyle = useMemo(() => ({
807
+ position: "absolute",
808
+ zIndex: 1001,
809
+ transform: animatedPosition.getTranslateTransform()
810
+ }), [animatedPosition]);
811
+ const previousReveal = useRef(undefined);
812
+ useEffect(() => {
813
+ if (enablePositionPersistence && !isPositionInitialized) return;
814
+ if (previousReveal.current === revealToken) return;
815
+ previousReveal.current = revealToken;
816
+ if (revealToken !== undefined && revealToken > 0 && isHidden) toggleHideShow();
817
+ }, [revealToken, isPositionInitialized, enablePositionPersistence, isHidden, toggleHideShow]);
818
+ const hasMinimizedTools = hasMinimizedStack;
819
+ const containerStyle = {
820
+ flexDirection: "row",
821
+ alignItems: "center",
822
+ backgroundColor: buoyColors.card,
823
+ // When minimized tools showing, remove top-left radius so they connect seamlessly
824
+ borderTopLeftRadius: hasMinimizedTools ? 0 : 6,
825
+ borderTopRightRadius: 6,
826
+ borderBottomLeftRadius: 6,
827
+ borderBottomRightRadius: 6,
828
+ // Use individual border widths when minimized tools are showing
829
+ // Always show top border when dragging for visual feedback
830
+ borderLeftWidth: isDragging ? 2 : 1,
831
+ borderRightWidth: isDragging ? 2 : 1,
832
+ borderBottomWidth: isDragging ? 2 : 1,
833
+ borderTopWidth: isDragging ? 2 : hasMinimizedTools ? 0 : 1,
834
+ borderColor: isDragging ? floatingToolsColors.dragActive : buoyColors.textMuted + "66",
835
+ overflow: "visible",
836
+ elevation: 8,
837
+ shadowColor: isDragging ? floatingToolsColors.dragActive + "99" : "#000",
838
+ shadowOffset: {
839
+ width: 0,
840
+ height: isDragging ? 6 : 4
841
+ },
842
+ shadowOpacity: isDragging ? 0.6 : 0.3,
843
+ shadowRadius: isDragging ? 12 : 8,
844
+ ...(variant === "strip" ? {
845
+ borderRadius: night.radius.card,
846
+ borderTopLeftRadius: night.radius.card,
847
+ borderTopRightRadius: night.radius.card,
848
+ borderBottomLeftRadius: night.radius.card,
849
+ borderBottomRightRadius: night.radius.card,
850
+ minHeight: 48,
851
+ backgroundColor: night.color.surface,
852
+ borderColor: isDragging ? night.color.accentBorderStrong : night.color.separator,
853
+ shadowOpacity: 0.24,
854
+ shadowRadius: 16
855
+ } : {})
856
+ };
857
+ const dragHandleStyle = {
858
+ paddingHorizontal: 6,
859
+ paddingVertical: 6,
860
+ backgroundColor: buoyColors.textMuted + "1A",
861
+ alignItems: "center",
862
+ justifyContent: "center",
863
+ width: 32,
864
+ ...(variant === "strip" ? {
865
+ minHeight: 48
866
+ } : {}),
867
+ borderRightWidth: 1,
868
+ borderRightColor: buoyColors.textMuted + "66",
869
+ // Remove top-left radius when minimized tools are showing
870
+ borderTopLeftRadius: hasMinimizedTools ? 0 : undefined,
871
+ ...(variant === "strip" ? {
872
+ backgroundColor: "transparent",
873
+ borderRightWidth: 0
874
+ } : {})
875
+ };
876
+ const contentStyle = {
877
+ flexDirection: "row",
878
+ alignItems: "center",
879
+ gap: 6,
880
+ paddingRight: 8
881
+ };
882
+
883
+ // Compose actions row with automatic dividers
884
+ const actions = useMemo(() => variant === "strip" ? Children.toArray(children) : interleaveWithDividers(Children.toArray(children)), [children, variant]);
885
+
886
+ // Width for the minimized tools stack - match the drag handle width
887
+ const minimizedStackWidth = 32;
888
+
889
+ // Don't render until position is initialized (when persistence enabled)
890
+ // This prevents the bubble from appearing at {0,0} before position loads
891
+ if (enablePositionPersistence && !isPositionInitialized) {
892
+ return null;
893
+ }
894
+ return /*#__PURE__*/_jsx(Animated.View, {
895
+ pointerEvents: suspendPointerEvents ? "none" : "box-none",
896
+ testID: id ?? "buoy-floating-tool",
897
+ style: bubbleStyle,
898
+ children: /*#__PURE__*/_jsxs(View, {
899
+ style: {
900
+ overflow: "visible"
901
+ },
902
+ children: [minimizedStack && /*#__PURE__*/_jsx(View, {
903
+ style: {
904
+ position: "absolute",
905
+ bottom: "100%",
906
+ left: 0,
907
+ width: minimizedStackWidth,
908
+ overflow: "visible",
909
+ zIndex: 1001
910
+ },
911
+ children: minimizedStack
912
+ }), /*#__PURE__*/_jsxs(DraggableHeader, {
913
+ position: animatedPosition,
914
+ enabled: dragFromSurface,
915
+ claimOnMove: false,
916
+ onDragStart: handleDragStart,
917
+ onDragEnd: handleDragEnd,
918
+ containerBounds: {
919
+ width: screenWidth,
920
+ height: screenHeight
921
+ },
922
+ elementSize: bubbleSize,
923
+ minPosition: {
924
+ x: safeAreaInsets.left,
925
+ y: safeAreaInsets.top + EDGE_PADDING
926
+ },
927
+ maxOverflowX: bubbleSize.width,
928
+ hitSlop: {
929
+ top: 10,
930
+ bottom: 10,
931
+ left: 10,
932
+ right: 10
933
+ },
934
+ style: containerStyle,
935
+ onLayout: event => {
936
+ const {
937
+ width,
938
+ height
939
+ } = event.nativeEvent.layout;
940
+ setBubbleSize({
941
+ width,
942
+ height
943
+ });
944
+ },
945
+ children: [/*#__PURE__*/_jsxs(DraggableHeader, {
946
+ position: animatedPosition,
947
+ testID: id ? `${id}-grip` : "buoy-floating-grip",
948
+ accessibilityLabel: accessibilityLabel,
949
+ onDragStart: handleDragStart,
950
+ onDragEnd: handleDragEnd,
951
+ onTap: toggleHideShow,
952
+ containerBounds: {
953
+ width: screenWidth,
954
+ height: screenHeight
955
+ },
956
+ elementSize: bubbleSize,
957
+ minPosition: {
958
+ x: safeAreaInsets.left,
959
+ y: safeAreaInsets.top + EDGE_PADDING
960
+ },
961
+ style: dragHandleStyle,
962
+ enabled: true,
963
+ maxOverflowX: bubbleSize.width,
964
+ children: [/*#__PURE__*/_jsx(GripVerticalIcon, {
965
+ size: variant === "strip" ? 20 : 12,
966
+ color: buoyColors.textSecondary + "CC"
967
+ }), handleAdornment]
968
+ }), /*#__PURE__*/_jsx(FloatingToolsContext.Provider, {
969
+ value: {
970
+ isDragging
971
+ },
972
+ children: /*#__PURE__*/_jsxs(View, {
973
+ style: contentStyle,
974
+ children: [showEnvironmentSelector && environment && availableEnvironments && onEnvironmentSwitch && /*#__PURE__*/_jsx(EnvironmentSelectorInline, {
975
+ currentEnvironment: environment,
976
+ availableEnvironments: availableEnvironments,
977
+ onSelect: onEnvironmentSwitch
978
+ }), actions]
979
+ })
980
+ })]
981
+ }), !isHidden && accessory && /*#__PURE__*/_jsx(View, {
982
+ onLayout: event => setAccessoryHeight(event.nativeEvent.layout.height),
983
+ style: {
984
+ position: "absolute",
985
+ ...(accessoryAbove ? {
986
+ bottom: "100%"
987
+ } : {
988
+ top: "100%"
989
+ }),
990
+ left: 0,
991
+ width: "100%",
992
+ zIndex: 1002
993
+ },
994
+ children: accessory
995
+ })]
996
+ })
997
+ });
998
+ }
999
+
1000
+ /** Native AppHost gets its anchor from the minimized-tools provider. */
1001
+ export function getBubbleAnchorPosition() {
1002
+ return null;
1003
+ }