@buoy-gg/core 7.0.40 → 7.0.41

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 (140) hide show
  1. package/README.md +10 -0
  2. package/bin/buoy.js +25 -31
  3. package/lib/commonjs/browserRegister.js +48 -0
  4. package/lib/commonjs/browserRegister.server.js +10 -0
  5. package/lib/commonjs/floatingMenu/AppHost.web.js +71 -59
  6. package/lib/commonjs/floatingMenu/BrowserToolRuntime.js +150 -0
  7. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +10 -1111
  8. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.shared.js +1114 -0
  9. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +28 -1028
  10. package/lib/commonjs/floatingMenu/FloatingDevTools.js +7 -2
  11. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +54 -40
  12. package/lib/commonjs/floatingMenu/MinimizedToolsStack.web.js +8 -2
  13. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +10 -0
  14. package/lib/commonjs/floatingMenu/autoExternalSync.js +9 -0
  15. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +86 -12
  16. package/lib/commonjs/floatingMenu/dial/DialIcon.js +4 -2
  17. package/lib/commonjs/floatingMenu/settingsBus.js +6 -17
  18. package/lib/commonjs/floatingMenu/useDesktopSyncStatus.web.js +14 -0
  19. package/lib/commonjs/index.web.js +92 -1
  20. package/lib/commonjs/reactDevtoolsTypes.js +1 -0
  21. package/lib/commonjs/utils/autoDiscoverPresets.web.js +5 -178
  22. package/lib/commonjs/utils/createWebTool.js +20 -0
  23. package/lib/commonjs/utils/createWebTools.js +30 -0
  24. package/lib/commonjs/version.js +1 -1
  25. package/lib/module/browserRegister.js +44 -0
  26. package/lib/module/browserRegister.server.js +6 -0
  27. package/lib/module/floatingMenu/AppHost.web.js +71 -59
  28. package/lib/module/floatingMenu/BrowserToolRuntime.js +146 -0
  29. package/lib/module/floatingMenu/DevToolsSettingsModal.js +1 -1113
  30. package/lib/module/floatingMenu/DevToolsSettingsModal.shared.js +1110 -0
  31. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +32 -1043
  32. package/lib/module/floatingMenu/FloatingDevTools.js +7 -2
  33. package/lib/module/floatingMenu/FloatingDevTools.web.js +55 -42
  34. package/lib/module/floatingMenu/MinimizedToolsStack.web.js +8 -2
  35. package/lib/module/floatingMenu/autoDiscoverPresets.js +10 -0
  36. package/lib/module/floatingMenu/autoExternalSync.js +9 -0
  37. package/lib/module/floatingMenu/dial/DialDevTools.web.js +87 -14
  38. package/lib/module/floatingMenu/dial/DialIcon.js +4 -2
  39. package/lib/module/floatingMenu/settingsBus.js +1 -17
  40. package/lib/module/floatingMenu/useDesktopSyncStatus.web.js +10 -0
  41. package/lib/module/index.web.js +8 -1
  42. package/lib/module/reactDevtoolsTypes.js +1 -0
  43. package/lib/module/utils/autoDiscoverPresets.web.js +5 -180
  44. package/lib/module/utils/createWebTool.js +17 -0
  45. package/lib/module/utils/createWebTools.js +25 -0
  46. package/lib/module/version.js +1 -1
  47. package/lib/typescript/commonjs/browserRegister.d.ts +5 -0
  48. package/lib/typescript/commonjs/browserRegister.d.ts.map +1 -0
  49. package/lib/typescript/commonjs/browserRegister.server.d.ts +5 -0
  50. package/lib/typescript/commonjs/browserRegister.server.d.ts.map +1 -0
  51. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts +3 -3
  52. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/floatingMenu/BrowserToolRuntime.d.ts +14 -0
  54. package/lib/typescript/commonjs/floatingMenu/BrowserToolRuntime.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts +2 -67
  56. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.shared.d.ts +71 -0
  58. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.shared.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts +3 -16
  60. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +12 -6
  63. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  68. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts +1 -9
  72. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/floatingMenu/useDesktopSyncStatus.web.d.ts +3 -0
  74. package/lib/typescript/commonjs/floatingMenu/useDesktopSyncStatus.web.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/index.web.d.ts +6 -0
  76. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/reactDevtoolsTypes.d.ts +8 -0
  78. package/lib/typescript/commonjs/reactDevtoolsTypes.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts +3 -40
  80. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/utils/createWebTool.d.ts +6 -0
  82. package/lib/typescript/commonjs/utils/createWebTool.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/utils/createWebTools.d.ts +4 -0
  84. package/lib/typescript/commonjs/utils/createWebTools.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/version.d.ts +1 -1
  86. package/lib/typescript/module/browserRegister.d.ts +5 -0
  87. package/lib/typescript/module/browserRegister.d.ts.map +1 -0
  88. package/lib/typescript/module/browserRegister.server.d.ts +5 -0
  89. package/lib/typescript/module/browserRegister.server.d.ts.map +1 -0
  90. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts +3 -3
  91. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts.map +1 -1
  92. package/lib/typescript/module/floatingMenu/BrowserToolRuntime.d.ts +14 -0
  93. package/lib/typescript/module/floatingMenu/BrowserToolRuntime.d.ts.map +1 -0
  94. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts +2 -67
  95. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  96. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.shared.d.ts +71 -0
  97. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.shared.d.ts.map +1 -0
  98. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts +3 -16
  99. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  100. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  101. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +12 -6
  102. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  103. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  104. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  105. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  106. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  107. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  108. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  109. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  110. package/lib/typescript/module/floatingMenu/settingsBus.d.ts +1 -9
  111. package/lib/typescript/module/floatingMenu/settingsBus.d.ts.map +1 -1
  112. package/lib/typescript/module/floatingMenu/useDesktopSyncStatus.web.d.ts +3 -0
  113. package/lib/typescript/module/floatingMenu/useDesktopSyncStatus.web.d.ts.map +1 -0
  114. package/lib/typescript/module/index.web.d.ts +6 -0
  115. package/lib/typescript/module/index.web.d.ts.map +1 -1
  116. package/lib/typescript/module/reactDevtoolsTypes.d.ts +8 -0
  117. package/lib/typescript/module/reactDevtoolsTypes.d.ts.map +1 -0
  118. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts +3 -40
  119. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  120. package/lib/typescript/module/utils/createWebTool.d.ts +6 -0
  121. package/lib/typescript/module/utils/createWebTool.d.ts.map +1 -0
  122. package/lib/typescript/module/utils/createWebTools.d.ts +4 -0
  123. package/lib/typescript/module/utils/createWebTools.d.ts.map +1 -0
  124. package/lib/typescript/module/version.d.ts +1 -1
  125. package/lib/web/chunks/chunk-5NZOQR6Q.mjs +2 -0
  126. package/lib/web/chunks/chunk-CGDSLLZF.mjs +2 -0
  127. package/lib/web/chunks/chunk-DCJDUSNR.mjs +2 -0
  128. package/lib/web/chunks/chunk-IDSHFS6G.mjs +2 -0
  129. package/lib/web/chunks/chunk-JOAMJGCS.mjs +2 -0
  130. package/lib/web/chunks/chunk-KYKZ2N52.mjs +2 -0
  131. package/lib/web/chunks/chunk-NGM7JGGS.mjs +2 -0
  132. package/lib/web/chunks/chunk-QZDVCMCT.mjs +2 -0
  133. package/lib/web/chunks/chunk-R7RGRTY5.mjs +2 -0
  134. package/lib/web/chunks/chunk-Z3XWDDIO.mjs +2 -0
  135. package/lib/web/chunks/chunk-ZTGQ6TTY.mjs +2 -0
  136. package/lib/web/index.mjs +11 -0
  137. package/lib/web/register-server.mjs +2 -0
  138. package/lib/web/register.mjs +2 -0
  139. package/lib/web/toolkit.mjs +2 -0
  140. package/package.json +50 -12
package/README.md CHANGED
@@ -97,3 +97,13 @@ Proprietary software. © Buoy LLC. All rights reserved. See the [Terms of Servic
97
97
  ### Custom floating controls
98
98
 
99
99
  The DevTools bubble uses the shared `FloatingTool` shell. Custom tools can use the same shell with `openFloatingTool`; `FloatingDevTools` already mounts the host. See the [integration guide](https://buoy.gg/buoy/latest/docs/custom-tools/floating-tools).
100
+
101
+ ## Web support (unreleased)
102
+
103
+ Import `FloatingDevTools` from `@buoy-gg/core/web` and pass the tool namespaces through `modules`. Settings, account status, and modal persistence share their implementation with native. The browser build is available in this checkout and has not been published yet. See the [web setup guide](https://buoy.gg/buoy/latest/docs/web-preview) for registration, dependencies, and browser boundaries.
104
+
105
+ In a Vite app, run `pnpm exec buoy login` from the app directory. The CLI writes `VITE_BUOY_KEY` to `.env.development.local` for Free accounts or `.env.local` for paid accounts. Pass `import.meta.env.VITE_BUOY_KEY` to the host's `licenseKey` prop.
106
+
107
+ The browser dial supports Tab, arrow keys between tools, Enter to activate controls, and Escape to close. Its center button opens settings. Drag panel backgrounds or handles to move them; tabs and inputs keep their normal mouse behavior. Minimized tools stay above the floating bar and scroll when needed. They open below only when there is not enough room above for one row.
108
+
109
+ The web dial follows the shared Background selection in Settings, including changes made while the dial is open.
package/bin/buoy.js CHANGED
@@ -61,20 +61,16 @@ function openBrowser(url) {
61
61
  }
62
62
  }
63
63
 
64
- /**
65
- * Which env var name this project can actually read.
66
- *
67
- * Expo only inlines `EXPO_PUBLIC_`-prefixed vars into the bundle, so writing a
68
- * bare `BUOY_KEY` there produces a file that looks correct and silently does
69
- * nothing — the most likely way this whole flow could waste someone's evening.
70
- */
64
+ /** Select the public variable prefix supported by the app's bundler. */
71
65
  function detectEnvVarName(root) {
66
+ let webVarName = null;
72
67
  try {
73
68
  const pkgPath = path.join(root, 'package.json');
74
69
  if (fs.existsSync(pkgPath)) {
75
70
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
76
71
  const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
77
72
  if (deps.expo) return 'EXPO_PUBLIC_BUOY_KEY';
73
+ if (deps.vite) webVarName = 'VITE_BUOY_KEY';
78
74
  }
79
75
  if (
80
76
  fs.existsSync(path.join(root, 'app.config.js')) ||
@@ -88,27 +84,19 @@ function detectEnvVarName(root) {
88
84
  if (parsed && parsed.expo) return 'EXPO_PUBLIC_BUOY_KEY';
89
85
  }
90
86
  } catch {
91
- /* fall through to the generic name */
87
+ /* Use the detected bundler or the generic name. */
92
88
  }
93
- return 'BUOY_KEY';
89
+ return webVarName || 'BUOY_KEY';
94
90
  }
95
91
 
96
92
  /**
97
- * Which env file the key belongs in.
98
- *
99
- * Expo inlines `.env.local` into EVERY bundle, release builds included, and
100
- * `npx expo export` forces NODE_ENV=production — so a free key written there
101
- * ships inside the app to every phone that installs it. Each of those phones
102
- * then validates against the license server, which is how one free account
103
- * spent the whole daily quota in a morning. `.env.development.local` is only
104
- * loaded under NODE_ENV=development, so a free key there can only ever reach
105
- * a developer's own device.
106
- *
107
- * Paid keys keep `.env.local`: a Pro customer may want the tools in a
108
- * TestFlight or QA build, and that is their call to make.
93
+ * Expo and Vite include `.env.local` in production builds. Free keys belong
94
+ * in `.env.development.local` to keep them in development builds and avoid
95
+ * account verification from every production client. Paid keys can also be
96
+ * used in TestFlight or QA builds, so they keep `.env.local`.
109
97
  */
110
98
  function envFileFor(varName, tier) {
111
- if (varName !== 'EXPO_PUBLIC_BUOY_KEY') return '.env.local';
99
+ if (varName !== 'EXPO_PUBLIC_BUOY_KEY' && varName !== 'VITE_BUOY_KEY') return '.env.local';
112
100
  return tier === 'pro' || tier === 'business' ? '.env.local' : '.env.development.local';
113
101
  }
114
102
 
@@ -296,16 +284,22 @@ async function login() {
296
284
  log(` Wrote ${cyan(varName)} to ${cyan(relative)}`);
297
285
  if (moved) log(dim(` Removed the old ${varName} line from .env.local`));
298
286
  if (fileName === '.env.development.local') {
299
- log(dim(' Expo loads this file for `expo start` only, so the key stays out of release builds.'));
287
+ log(dim(' This file is loaded in development, keeping the key out of production builds.'));
300
288
  }
301
289
  if (addedIgnore) log(dim(` Added ${fileName} to .gitignore`));
302
290
  log('');
303
291
  log(' Use it in your app:');
304
292
  log('');
305
- log(cyan(` Buoy.init({ licenseKey: ${varName} });`));
306
- log('');
307
- log(dim(' Swift: pass the key through BuoyConfig.licenseKey in your development build.'));
308
- log(dim(' Bare React Native does not load .env.local automatically; wire your existing env loader.'));
293
+ if (varName === 'VITE_BUOY_KEY') {
294
+ log(cyan(' <FloatingDevTools modules={modules} licenseKey={import.meta.env.VITE_BUOY_KEY} />'));
295
+ log('');
296
+ log(dim(' Keep your existing tool modules and providers, then reload the web app.'));
297
+ } else {
298
+ log(cyan(` Buoy.init({ licenseKey: ${varName} });`));
299
+ log('');
300
+ log(dim(' Swift: pass the key through BuoyConfig.licenseKey in your development build.'));
301
+ log(dim(' Bare React Native does not load .env.local automatically; wire your existing env loader.'));
302
+ }
309
303
  log('');
310
304
  }
311
305
 
@@ -324,10 +318,10 @@ function help() {
324
318
 
325
319
  ${bold('Notes')}
326
320
  Run login from your app directory. Complete browser sign-in to receive
327
- your account key. On Expo, a free key goes to .env.development.local so
328
- it never ships in a release build; a paid key goes to .env.local. Other
329
- projects receive BUOY_KEY in .env.local and must configure their own
330
- environment loader and variable mapping.
321
+ your account key. Expo uses EXPO_PUBLIC_BUOY_KEY; Vite uses VITE_BUOY_KEY.
322
+ For both, a free key goes to .env.development.local and a paid key goes
323
+ to .env.local. Other projects receive BUOY_KEY in .env.local and must
324
+ configure their own environment loader and variable mapping.
331
325
  This command does not install tools or verify a Desktop connection.
332
326
 
333
327
  Docs: ${SITE}/buoy/latest/docs/installation
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.registerBrowserDevTools = registerBrowserDevTools;
7
+ var ReactDevtools = _interopRequireWildcard(require("react-devtools-core"));
8
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
9
+ /** Call before importing React DOM so render and element inspectors can see app roots. */
10
+ function registerBrowserDevTools(options = {}) {
11
+ if (typeof window === 'undefined' || typeof document === 'undefined') return false;
12
+ const dev = typeof __DEV__ !== 'undefined' ? __DEV__ : process.env.NODE_ENV !== 'production';
13
+ if (!dev && !options.enableInRelease) return false;
14
+ const target = window;
15
+ const previous = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
16
+ // Fast Refresh installs a minimal hook before the application's entry runs.
17
+ // Preserve its callbacks and renderer IDs when installing the full backend.
18
+ const refreshHook = previous && previous.supportsFiber && !previous.getFiberRoots && !previous.sub && Object.getOwnPropertyDescriptor(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__')?.configurable;
19
+ if (refreshHook) delete target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
20
+ if (!target.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
21
+ const initialize = ReactDevtools.initialize ?? ReactDevtools.default?.initialize;
22
+ initialize({
23
+ appendComponentStack: false,
24
+ breakOnConsoleErrors: false,
25
+ showInlineWarningsAndErrors: false,
26
+ hideConsoleLogsInStrictMode: false
27
+ });
28
+ if (refreshHook) {
29
+ const hook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
30
+ const inject = hook.inject;
31
+ const ids = new Map();
32
+ hook.inject = renderer => {
33
+ const id = inject(renderer);
34
+ ids.set(id, previous.inject(renderer));
35
+ return id;
36
+ };
37
+ for (const name of ['onScheduleFiberRoot', 'onCommitFiberRoot', 'onCommitFiberUnmount']) {
38
+ const current = hook[name];
39
+ hook[name] = (id, ...args) => {
40
+ previous[name]?.(ids.get(id) ?? id, ...args);
41
+ return current?.(id, ...args);
42
+ };
43
+ }
44
+ }
45
+ }
46
+ return Boolean(target.__REACT_DEVTOOLS_GLOBAL_HOOK__);
47
+ }
48
+ registerBrowserDevTools();
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.registerBrowserDevTools = registerBrowserDevTools;
7
+ /** The server entry does not load the browser's React DevTools backend. */
8
+ function registerBrowserDevTools(_options = {}) {
9
+ return false;
10
+ }
@@ -8,34 +8,18 @@ var _sharedUi = require("@buoy-gg/shared-ui");
8
8
  var _react = require("react");
9
9
  var _AppHostLogic = require("./AppHostLogic.js");
10
10
  var _AppIcon = require("./AppIcon");
11
+ var _ToolHost = require("./ToolHost.js");
12
+ var _DevToolsSettingsModalShared = require("./DevToolsSettingsModal.shared.js");
11
13
  var _floatingToolsWeb = require("./floatingTools.web.js");
12
14
  var _jsxRuntime = require("react/jsx-runtime");
13
15
  /**
14
- * AppHost.web.tsx — DOM port of AppHost.tsx (React Native).
15
- *
16
- * Same state machine (shared `AppHostLogic.ts`), same `@react_buoy_open_apps`
17
- * persistence key + JSON shape (`{id, minimized}[]`, 500ms debounce), same
18
- * launch modes and z-order model (base 9000 + stack index).
19
- *
20
- * Intentional deviations from the native file (all logged):
21
- * - No `MinimizedToolsContext`/`MinimizedToolsStack` second store: the
22
- * minimized dock derives directly from `openApps` (`minimized === true`),
23
- * the same simplification the Flutter port made deliberately
24
- * (packages-flutter/buoy_core — see FLUTTER_TOOLS_PORT/MASTER_PLAN.md).
25
- * `@react_buoy_minimized_tools` is therefore not written on web.
26
- * - `BackHandler` (Android hardware back) → `Escape` keydown closes the
27
- * top-most open tool.
28
- * - No `setExpandableWindowControls` / global settings bridge — the web
29
- * settings modal has no global-settings tab yet.
30
- * - `minimizeTargetPosition` is computed from the dock layout below instead
31
- * of `MinimizedToolsContext.getNextIconPosition()`.
32
- *
33
- * @platform React DOM Web
16
+ * Browser tool host using the shared open-app state machine and persistence.
17
+ * Minimized tools derive from openApps. Escape closes the top visible tool;
18
+ * the minimized stack remains attached to the floating menu.
34
19
  */
35
20
 
36
21
  const AppHostContext = /*#__PURE__*/(0, _react.createContext)(null);
37
22
  const STORAGE_KEY_OPEN_APPS = "@react_buoy_open_apps";
38
- const PERSISTENCE_DELAY = 500;
39
23
 
40
24
  // Minimized dock geometry — mirrors MinimizedToolsContext.tsx constants.
41
25
  const ICON_SIZE = 44;
@@ -57,9 +41,11 @@ const AppHostProvider = ({
57
41
  }) => {
58
42
  const [openApps, setOpenApps] = (0, _react.useState)([]);
59
43
  const [isRestored, setIsRestored] = (0, _react.useState)(false);
60
- const persistenceTimeoutRef = (0, _react.useRef)(null);
61
44
  const installedAppsRef = (0, _react.useRef)([]);
62
45
  const pendingRestoreRef = (0, _react.useRef)(null);
46
+ const restoredRef = (0, _react.useRef)(false);
47
+ const storageReadRef = (0, _react.useRef)(false);
48
+ const registeredRef = (0, _react.useRef)(false);
63
49
  const open = (0, _react.useCallback)(def => {
64
50
  let resolvedId = "";
65
51
  setOpenApps(current => {
@@ -73,8 +59,8 @@ const AppHostProvider = ({
73
59
  return resolvedId;
74
60
  }, []);
75
61
  const tryRestorePending = (0, _react.useCallback)(() => {
76
- if (isRestored) return;
77
- if (!installedAppsRef.current.length) return;
62
+ if (restoredRef.current || !storageReadRef.current || !registeredRef.current) return;
63
+ restoredRef.current = true;
78
64
  const pendingApps = pendingRestoreRef.current;
79
65
  if (!pendingApps || pendingApps.length === 0) {
80
66
  setIsRestored(true);
@@ -97,8 +83,9 @@ const AppHostProvider = ({
97
83
  title: appDef.name,
98
84
  component: appDef.component,
99
85
  props: appDef.props,
86
+ modal: appDef.modal,
100
87
  launchMode: appDef.launchMode || "self-modal",
101
- singleton: appDef.singleton,
88
+ singleton: appDef.singleton ?? true,
102
89
  icon: resolvedIcon,
103
90
  color: appDef.color,
104
91
  minimized
@@ -106,14 +93,16 @@ const AppHostProvider = ({
106
93
  }
107
94
  });
108
95
  setIsRestored(true);
109
- }, [isRestored, open]);
96
+ }, [open]);
110
97
 
111
98
  // Restore open apps on mount. Handles both the legacy `string[]` shape and
112
99
  // the current `{id, minimized}[]` shape — same tolerance as native.
113
100
  (0, _react.useEffect)(() => {
101
+ let cancelled = false;
114
102
  const restoreOpenApps = async () => {
115
103
  try {
116
104
  const saved = await _sharedUi.persistentStorage.getItem(STORAGE_KEY_OPEN_APPS);
105
+ if (cancelled) return;
117
106
  if (saved) {
118
107
  const parsed = JSON.parse(saved);
119
108
  let savedApps;
@@ -124,46 +113,40 @@ const AppHostProvider = ({
124
113
  minimized: false
125
114
  }));
126
115
  } else {
127
- savedApps = parsed;
116
+ savedApps = parsed.filter(item => typeof item === "object" && item !== null && typeof item.id === "string");
128
117
  }
129
118
  if (savedApps.length) {
130
119
  pendingRestoreRef.current = savedApps;
131
- tryRestorePending();
132
- return;
133
120
  }
134
121
  }
135
122
  }
136
123
  } catch {
137
124
  // Failed to restore open apps - continue with fresh state
138
125
  }
139
- setIsRestored(true);
126
+ if (cancelled) return;
127
+ storageReadRef.current = true;
128
+ tryRestorePending();
129
+ };
130
+ void restoreOpenApps();
131
+ return () => {
132
+ cancelled = true;
140
133
  };
141
- restoreOpenApps();
142
134
  }, [tryRestorePending]);
143
135
 
144
- // Save open apps with debounce
136
+ // Save discrete open/minimize/close changes before a reload can lose them.
145
137
  (0, _react.useEffect)(() => {
146
138
  if (!isRestored) return;
147
- if (persistenceTimeoutRef.current) {
148
- clearTimeout(persistenceTimeoutRef.current);
149
- }
150
- persistenceTimeoutRef.current = setTimeout(() => {
151
- const appStates = openApps.map(app => ({
152
- id: app.id,
153
- minimized: app.minimized ?? false
154
- }));
155
- _sharedUi.persistentStorage.setItem(STORAGE_KEY_OPEN_APPS, JSON.stringify(appStates));
156
- }, PERSISTENCE_DELAY);
157
- return () => {
158
- if (persistenceTimeoutRef.current) {
159
- clearTimeout(persistenceTimeoutRef.current);
160
- }
161
- };
139
+ const appStates = openApps.map(app => ({
140
+ id: app.id,
141
+ minimized: app.minimized ?? false
142
+ }));
143
+ void _sharedUi.persistentStorage.setItem(STORAGE_KEY_OPEN_APPS, JSON.stringify(appStates));
162
144
  }, [openApps, isRestored]);
163
145
 
164
146
  // Store reference to installed apps for restoration
165
147
  const registerApps = (0, _react.useCallback)(apps => {
166
148
  installedAppsRef.current = apps;
149
+ registeredRef.current = true;
167
150
  tryRestorePending();
168
151
  }, [tryRestorePending]);
169
152
  const close = (0, _react.useCallback)(instanceId => {
@@ -174,16 +157,18 @@ const AppHostProvider = ({
174
157
  });
175
158
  }, []);
176
159
  const closeAll = (0, _react.useCallback)(() => setOpenApps([]), []);
177
- const minimize = (0, _react.useCallback)(instanceId => {
160
+ const minimize = (0, _react.useCallback)((instanceId, restoreState) => {
178
161
  setOpenApps(s => s.map(app => app.instanceId === instanceId ? {
179
162
  ...app,
180
- minimized: true
163
+ minimized: true,
164
+ restoreState
181
165
  } : app));
182
166
  }, []);
183
- const restore = (0, _react.useCallback)(instanceId => {
167
+ const restore = (0, _react.useCallback)((instanceId, restoreState) => {
184
168
  setOpenApps(s => s.map(app => app.instanceId === instanceId ? {
185
169
  ...app,
186
- minimized: false
170
+ minimized: false,
171
+ restoreState: restoreState ?? app.restoreState
187
172
  } : app));
188
173
  }, []);
189
174
  const isMinimized = (0, _react.useCallback)(instanceId => {
@@ -198,11 +183,13 @@ const AppHostProvider = ({
198
183
  if (!hasVisibleApps) return;
199
184
  const handler = event => {
200
185
  if (event.key !== "Escape") return;
201
- close();
186
+ if (event.defaultPrevented) return;
187
+ const top = [...openApps].reverse().find(app => app.launchMode !== "toggle-only" && !app.minimized);
188
+ if (top) close(top.instanceId);
202
189
  };
203
190
  window.addEventListener("keydown", handler);
204
191
  return () => window.removeEventListener("keydown", handler);
205
- }, [hasVisibleApps, close]);
192
+ }, [hasVisibleApps, close, openApps]);
206
193
  const value = (0, _react.useMemo)(() => ({
207
194
  openApps,
208
195
  isAnyOpen: hasVisibleApps,
@@ -274,20 +261,41 @@ const AppRenderer = ({
274
261
  zIndex,
275
262
  onClose,
276
263
  onMinimize,
277
- minimizeTargetPosition
264
+ minimizeTargetPosition,
265
+ enableSharedModalDimensions
278
266
  }) => {
279
267
  const Comp = app.component;
280
268
  const isVisible = !app.minimized;
269
+ const bodyProps = {
270
+ ...(app.props ?? {}),
271
+ ...(enableSharedModalDimensions ? {
272
+ enableSharedModalDimensions: true
273
+ } : {})
274
+ };
275
+ if (app.modal && (!app.launchMode || app.launchMode === "self-modal")) {
276
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ToolHost.ToolModalShell, {
277
+ app: app,
278
+ config: app.modal,
279
+ visible: isVisible,
280
+ zIndex: zIndex,
281
+ onClose: onClose,
282
+ onMinimize: onMinimize,
283
+ initialModalState: app.restoreState,
284
+ enableSharedModalDimensions: enableSharedModalDimensions,
285
+ bodyProps: bodyProps
286
+ });
287
+ }
281
288
  if (app.launchMode === "self-modal") {
282
289
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
283
- ...(app.props ?? {}),
290
+ ...bodyProps,
284
291
  visible: isVisible,
285
292
  onClose: onClose,
286
293
  onRequestClose: onClose,
287
294
  onMinimize: onMinimize,
288
295
  minimizeTargetPosition: minimizeTargetPosition,
289
296
  instanceId: app.instanceId,
290
- zIndex: zIndex
297
+ zIndex: zIndex,
298
+ initialModalState: app.restoreState
291
299
  });
292
300
  }
293
301
  if (app.launchMode === "inline") {
@@ -303,7 +311,7 @@ const AppRenderer = ({
303
311
  pointerEvents: "none"
304
312
  },
305
313
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
306
- ...(app.props ?? {}),
314
+ ...bodyProps,
307
315
  onClose: onClose,
308
316
  onMinimize: onMinimize,
309
317
  minimizeTargetPosition: minimizeTargetPosition,
@@ -322,7 +330,7 @@ const AppRenderer = ({
322
330
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
323
331
  style: cardStyle,
324
332
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
325
- ...(app.props ?? {}),
333
+ ...bodyProps,
326
334
  onClose: onClose,
327
335
  onMinimize: onMinimize,
328
336
  minimizeTargetPosition: minimizeTargetPosition,
@@ -344,6 +352,9 @@ const AppOverlay = () => {
344
352
  close,
345
353
  minimize
346
354
  } = useAppHost();
355
+ const {
356
+ settings
357
+ } = (0, _DevToolsSettingsModalShared.useDevToolsSettings)();
347
358
  const renderableApps = openApps.filter(app => app.launchMode !== "toggle-only");
348
359
  if (renderableApps.length === 0) return null;
349
360
  const minimizedCount = renderableApps.filter(app => app.minimized).length;
@@ -356,7 +367,8 @@ const AppOverlay = () => {
356
367
  app: app,
357
368
  zIndex: BASE_ZINDEX + index,
358
369
  onClose: () => close(app.instanceId),
359
- onMinimize: () => minimize(app.instanceId),
370
+ onMinimize: state => minimize(app.instanceId, state),
371
+ enableSharedModalDimensions: settings.globalSettings?.enableSharedModalDimensions === true,
360
372
  minimizeTargetPosition: minimizeTarget
361
373
  }, app.instanceId))
362
374
  });
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.BrowserToolRuntime = BrowserToolRuntime;
7
+ var _react = require("react");
8
+ var _license = require("@buoy-gg/license");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
+ var _externalSyncGate = require("./externalSyncGate.js");
11
+ var _deviceIdentity = require("./deviceIdentity.js");
12
+ var _appSyncAdapter = require("./sync/appSyncAdapter.js");
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ const roots = ['ConsoleRoot', 'RouteTracker', 'ReactQueryTracker', 'ReduxAutoInstrument', 'PerfMonitorRoot', 'AutomationResumer', 'ImagesRoot'];
15
+ const overlays = ['DebugBordersStandaloneOverlay', 'HighlightUpdatesOverlay', 'ImageOverlayStandalone', 'PerfMonitorOverlay', 'JsTopHudOverlay', 'ImpersonateOverlay', 'ScenariosOverlay'];
16
+ function BrowserToolRuntime(props) {
17
+ const modules = (0, _react.useMemo)(() => Object.fromEntries(Object.entries(props.modules).map(([id, module]) => [id.replace(/^@buoy-gg\//, '').replace(/\/web$/, ''), module])), [props.modules]);
18
+ const query = modules['react-query'];
19
+ return query?.useReactQuerySyncAdapter ? /*#__PURE__*/(0, _jsxRuntime.jsx)(QueryRuntime, {
20
+ ...props,
21
+ modules: modules,
22
+ query: query
23
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(ToolRuntime, {
24
+ ...props,
25
+ modules: modules
26
+ });
27
+ }
28
+ function QueryRuntime(props) {
29
+ const adapter = props.query.useReactQuerySyncAdapter();
30
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(ToolRuntime, {
31
+ ...props,
32
+ queryAdapter: adapter
33
+ });
34
+ }
35
+ function ToolRuntime({
36
+ modules,
37
+ apps,
38
+ requiredEnvVars,
39
+ externalSync,
40
+ headless,
41
+ sentryGetClient,
42
+ queryAdapter
43
+ }) {
44
+ const {
45
+ isPro,
46
+ tier
47
+ } = (0, _license.useLicense)();
48
+ const adapters = (0, _react.useMemo)(() => {
49
+ const result = {
50
+ app: _appSyncAdapter.appSyncAdapter
51
+ };
52
+ for (const [id, module] of Object.entries(modules)) {
53
+ for (const [name, adapter] of Object.entries(module)) {
54
+ if (name.endsWith('SyncAdapter') && adapter && typeof adapter === 'object') result[id] = adapter;
55
+ }
56
+ }
57
+ if (queryAdapter) result.query = queryAdapter;
58
+ if (modules.env?.createEnvSyncAdapter) result.env = modules.env.createEnvSyncAdapter(requiredEnvVars ?? []);
59
+ for (const app of apps ?? []) if (app.sync) result[`custom:${app.id}`] = app.sync;
60
+ return result;
61
+ }, [modules, apps, requiredEnvVars, queryAdapter]);
62
+ const ref = (0, _react.useRef)(adapters);
63
+ ref.current = adapters;
64
+ (0, _react.useEffect)(() => (0, _sharedUi.registerToolAdapters)(() => ref.current), []);
65
+ (0, _react.useEffect)(() => {
66
+ const cleanups = [];
67
+ const start = fn => {
68
+ const stop = fn?.();
69
+ if (typeof stop === 'function') cleanups.push(stop);
70
+ };
71
+ start(modules.network?.startNetworkCapture);
72
+ start(modules.storage?.startBrowserStorageCapture);
73
+ start(modules.assets?.startBrowserAssetCapture);
74
+ if (modules.storage?.storageSyncAdapter?.subscribe) start(() => modules.storage.storageSyncAdapter.subscribe(() => {}));
75
+ if (modules['focus-inspector']?.focusInspectorSyncAdapter?.subscribe) start(() => modules['focus-inspector'].focusInspectorSyncAdapter.subscribe(() => {}));
76
+ return () => {
77
+ for (const stop of cleanups.reverse()) stop();
78
+ };
79
+ }, [modules]);
80
+ const sync = modules['external-sync'];
81
+ const dev = typeof __DEV__ !== 'undefined' ? __DEV__ : process.env.NODE_ENV !== 'production';
82
+ const connect = (0, _externalSyncGate.shouldMountExternalSync)({
83
+ isDevelopment: dev,
84
+ isLicensed: isPro,
85
+ externalSync,
86
+ isAvailable: Boolean(sync)
87
+ });
88
+ const SentryRoot = modules.sentry?.SentryRoot;
89
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
90
+ children: [Object.entries(modules).flatMap(([id, module]) => roots.map(name => {
91
+ const Root = module[name];
92
+ return Root ? /*#__PURE__*/(0, _jsxRuntime.jsx)(Root, {}, `${id}:${name}`) : null;
93
+ })), SentryRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(SentryRoot, {
94
+ getClient: sentryGetClient
95
+ }), Object.entries(modules).flatMap(([id, module]) => overlays.map(name => {
96
+ const Overlay = module[name];
97
+ // Simulation indicators remain visible when a remote driver changes app state.
98
+ return Overlay && (!headless || name === 'ScenariosOverlay' || name === 'ImpersonateOverlay') ? /*#__PURE__*/(0, _jsxRuntime.jsx)(Overlay, {}, `${id}:${name}`) : null;
99
+ })), connect && /*#__PURE__*/(0, _jsxRuntime.jsx)(BrowserExternalSync, {
100
+ module: sync,
101
+ tools: adapters,
102
+ options: (0, _externalSyncGate.externalSyncOptions)(externalSync),
103
+ isPro: isPro,
104
+ tier: tier
105
+ })]
106
+ });
107
+ }
108
+ function BrowserExternalSync({
109
+ module,
110
+ tools,
111
+ options,
112
+ isPro,
113
+ tier
114
+ }) {
115
+ const identity = (0, _deviceIdentity.useDeviceIdentity)(options, {
116
+ appName: typeof document === 'undefined' ? null : document.title || location.hostname,
117
+ hardwareName: 'Browser',
118
+ platform: 'web'
119
+ });
120
+ const {
121
+ socket
122
+ } = module.useExternalSyncSocket({
123
+ deviceName: identity?.deviceName ?? 'Buoy Browser',
124
+ socketURL: options?.socketURL ?? module.getDefaultSocketURL(),
125
+ persistentDeviceId: identity?.deviceId ?? null,
126
+ platform: 'web',
127
+ enableLogs: options?.enableLogs
128
+ });
129
+ (0, _react.useEffect)(() => {
130
+ if (!socket) return;
131
+ const emit = () => {
132
+ if (socket.connected && _license.LicenseManager.hasAccountAccess()) socket.emit('device-license', {
133
+ isPro: isPro ? '1' : '0',
134
+ tier
135
+ });
136
+ };
137
+ emit();
138
+ socket.on('connect', emit);
139
+ return () => {
140
+ socket.off('connect', emit);
141
+ };
142
+ }, [socket, isPro, tier]);
143
+ module.useExternalSync({
144
+ tools,
145
+ socket,
146
+ deviceId: identity?.deviceId ?? null,
147
+ enableLogs: options?.enableLogs
148
+ });
149
+ return null;
150
+ }