@buoy-gg/core 7.0.16 → 7.0.18

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 (77) hide show
  1. package/lib/commonjs/floatingMenu/AppHost.js +23 -3
  2. package/lib/commonjs/floatingMenu/AppHost.web.js +6 -1
  3. package/lib/commonjs/floatingMenu/AppIcon.js +91 -0
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.js +15 -9
  5. package/lib/commonjs/floatingMenu/FloatingMenu.js +14 -7
  6. package/lib/commonjs/floatingMenu/ToolHost.js +274 -0
  7. package/lib/commonjs/floatingMenu/allToolsRegistry.js +7 -0
  8. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +16 -0
  9. package/lib/commonjs/floatingMenu/autoExternalSync.js +21 -2
  10. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +15 -7
  11. package/lib/commonjs/index.js +27 -0
  12. package/lib/commonjs/toolkit.js +584 -0
  13. package/lib/commonjs/toolkitComponents.js +241 -0
  14. package/lib/module/floatingMenu/AppHost.js +23 -3
  15. package/lib/module/floatingMenu/AppHost.web.js +6 -1
  16. package/lib/module/floatingMenu/AppIcon.js +84 -0
  17. package/lib/module/floatingMenu/FloatingDevTools.js +15 -9
  18. package/lib/module/floatingMenu/FloatingMenu.js +14 -7
  19. package/lib/module/floatingMenu/ToolHost.js +266 -0
  20. package/lib/module/floatingMenu/allToolsRegistry.js +8 -1
  21. package/lib/module/floatingMenu/autoDiscoverPresets.js +16 -0
  22. package/lib/module/floatingMenu/autoExternalSync.js +21 -2
  23. package/lib/module/floatingMenu/dial/DialDevTools.js +15 -7
  24. package/lib/module/index.js +6 -0
  25. package/lib/module/toolkit.js +62 -0
  26. package/lib/module/toolkitComponents.js +230 -0
  27. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts +3 -0
  30. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/floatingMenu/AppIcon.d.ts +30 -0
  32. package/lib/typescript/commonjs/floatingMenu/AppIcon.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts +77 -0
  36. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +8 -1
  40. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  42. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/floatingMenu/types.d.ts +62 -2
  45. package/lib/typescript/commonjs/floatingMenu/types.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/index.d.ts +2 -0
  47. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/toolkit.d.ts +31 -0
  49. package/lib/typescript/commonjs/toolkit.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/toolkitComponents.d.ts +61 -0
  51. package/lib/typescript/commonjs/toolkitComponents.d.ts.map +1 -0
  52. package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -1
  53. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts.map +1 -1
  54. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts +3 -0
  55. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts.map +1 -1
  56. package/lib/typescript/module/floatingMenu/AppIcon.d.ts +30 -0
  57. package/lib/typescript/module/floatingMenu/AppIcon.d.ts.map +1 -0
  58. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  59. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  60. package/lib/typescript/module/floatingMenu/ToolHost.d.ts +77 -0
  61. package/lib/typescript/module/floatingMenu/ToolHost.d.ts.map +1 -0
  62. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  63. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  64. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +8 -1
  65. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  66. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  67. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  68. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  69. package/lib/typescript/module/floatingMenu/types.d.ts +62 -2
  70. package/lib/typescript/module/floatingMenu/types.d.ts.map +1 -1
  71. package/lib/typescript/module/index.d.ts +2 -0
  72. package/lib/typescript/module/index.d.ts.map +1 -1
  73. package/lib/typescript/module/toolkit.d.ts +31 -0
  74. package/lib/typescript/module/toolkit.d.ts.map +1 -0
  75. package/lib/typescript/module/toolkitComponents.d.ts +61 -0
  76. package/lib/typescript/module/toolkitComponents.d.ts.map +1 -0
  77. package/package.json +19 -5
@@ -8,7 +8,9 @@ var _sharedUi = require("@buoy-gg/shared-ui");
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _reactNative = require("react-native");
10
10
  var _AppHostLogic = require("./AppHostLogic.js");
11
+ var _AppIcon = require("./AppIcon.js");
11
12
  var _MinimizedToolsContext = require("./MinimizedToolsContext.js");
13
+ var _ToolHost = require("./ToolHost.js");
12
14
  var _DevToolsSettingsModal = require("./DevToolsSettingsModal");
13
15
  var _jsxRuntime = require("react/jsx-runtime");
14
16
  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); }
@@ -68,10 +70,11 @@ const AppHostProvider = ({
68
70
  }) => {
69
71
  const appDef = installedAppsRef.current.find(app => app.id === appId);
70
72
  if (appDef) {
71
- const resolvedIcon = typeof appDef.icon === "function" ? appDef.icon({
73
+ const resolvedIcon = (0, _AppIcon.renderAppIcon)(appDef.icon, {
72
74
  slot: "dial",
73
- size: 20
74
- }) : appDef.icon;
75
+ size: 20,
76
+ color: appDef.color
77
+ });
75
78
  open({
76
79
  id: appDef.id,
77
80
  title: appDef.name,
@@ -81,6 +84,7 @@ const AppHostProvider = ({
81
84
  singleton: appDef.singleton,
82
85
  icon: resolvedIcon,
83
86
  color: appDef.color,
87
+ modal: appDef.modal,
84
88
  minimized: minimized // Preserve minimized state
85
89
  });
86
90
  }
@@ -257,6 +261,22 @@ const AppRenderer = ({
257
261
  enableSharedModalDimensions: true
258
262
  } : {})
259
263
  };
264
+
265
+ // Declarative chrome: the component is a plain body — wrap it in the shared
266
+ // modal shell (JsModal + managed header + search + toasts).
267
+ if (app.modal && (app.launchMode === "self-modal" || !app.launchMode)) {
268
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ToolHost.ToolModalShell, {
269
+ app: app,
270
+ config: app.modal,
271
+ visible: isVisible,
272
+ zIndex: zIndex,
273
+ onClose: onClose,
274
+ onMinimize: onMinimize,
275
+ initialModalState: app.restoreState,
276
+ enableSharedModalDimensions: globalEnableSharedModalDimensions === true,
277
+ bodyProps: mergedProps
278
+ });
279
+ }
260
280
  if (app.launchMode === "self-modal") {
261
281
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(Comp, {
262
282
  ...mergedProps,
@@ -7,6 +7,7 @@ exports.useAppHost = exports.AppOverlay = exports.AppHostProvider = void 0;
7
7
  var _sharedUi = require("@buoy-gg/shared-ui");
8
8
  var _react = require("react");
9
9
  var _AppHostLogic = require("./AppHostLogic.js");
10
+ var _AppIcon = require("./AppIcon.js");
10
11
  var _floatingToolsWeb = require("./floatingTools.web.js");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  /**
@@ -86,7 +87,11 @@ const AppHostProvider = ({
86
87
  }) => {
87
88
  const appDef = installedAppsRef.current.find(app => app.id === appId);
88
89
  if (appDef && appDef.component) {
89
- const resolvedIcon = typeof appDef.icon === "function" ? appDef.icon(20) : appDef.icon;
90
+ const resolvedIcon = (0, _AppIcon.renderAppIcon)(appDef.icon, {
91
+ slot: "dial",
92
+ size: 20,
93
+ color: appDef.color
94
+ });
90
95
  open({
91
96
  id: appDef.id,
92
97
  title: appDef.name,
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.AppTextIcon = AppTextIcon;
7
+ exports.renderAppIcon = renderAppIcon;
8
+ var _react = _interopRequireDefault(require("react"));
9
+ var _reactNative = require("react-native");
10
+ var _sharedUi = require("@buoy-gg/shared-ui");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ /**
14
+ * Icon resolution for `InstalledApp.icon`.
15
+ *
16
+ * Custom tools may provide an icon three ways:
17
+ * - a component `({ slot, size }) => node` (dynamic, hook-capable)
18
+ * - a pre-built ReactNode
19
+ * - a PLAIN STRING — "AUTH", "KIT", "🗑️" — rendered as an auto-fitting text
20
+ * icon so simple tools never need an icon library.
21
+ *
22
+ * Every menu surface (dial, row, minimize stack, restore path) resolves icons
23
+ * through `renderAppIcon` so all three forms behave identically.
24
+ */
25
+
26
+ /** Visible glyphs, not UTF-16 units — "🗑️" is one glyph, not three. */
27
+ function glyphCount(label) {
28
+ // Strip variation selectors + zero-width joiners before counting.
29
+ const stripped = label.replace(/[\uFE00-\uFE0F\u200D]/g, "");
30
+ return Math.max(Array.from(stripped).length, 1);
31
+ }
32
+ function AppTextIcon({
33
+ label,
34
+ size,
35
+ color = _sharedUi.toolTheme.colors.text
36
+ }) {
37
+ const chars = glyphCount(label);
38
+ // Short labels stay square; longer ones ("AUTH") get a wider box — the
39
+ // dial/row layouts have breathing room and the label reads better than a
40
+ // squashed square would. The computed font size is what fits the box on
41
+ // web; adjustsFontSizeToFit covers native font-metric drift.
42
+ const maxWidth = chars <= 2 ? size : size * 1.6;
43
+ const fontSize = Math.max(7, Math.min(size * (chars === 1 ? 0.7 : 0.62), maxWidth / (0.62 * chars)));
44
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
45
+ style: {
46
+ minWidth: size,
47
+ maxWidth,
48
+ height: size,
49
+ alignItems: "center",
50
+ justifyContent: "center"
51
+ },
52
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
53
+ numberOfLines: 1,
54
+ adjustsFontSizeToFit: true,
55
+ minimumFontScale: 0.5,
56
+ style: {
57
+ color,
58
+ fontSize,
59
+ fontWeight: "800",
60
+ letterSpacing: 0.5,
61
+ textAlign: "center"
62
+ },
63
+ children: label
64
+ })
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Resolve any `InstalledApp.icon` form to a renderable node. Function icons
70
+ * become elements (createElement — hooks keep working); strings become
71
+ * auto-fitting text icons; nodes pass through.
72
+ */
73
+ function renderAppIcon(icon, ctx) {
74
+ if (icon == null) return icon;
75
+ if (typeof icon === "function") {
76
+ return /*#__PURE__*/_react.default.createElement(icon, {
77
+ slot: ctx.slot,
78
+ size: ctx.size,
79
+ state: ctx.state,
80
+ actions: ctx.actions
81
+ });
82
+ }
83
+ if (typeof icon === "string" || typeof icon === "number") {
84
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(AppTextIcon, {
85
+ label: String(icon),
86
+ size: ctx.size,
87
+ color: ctx.color
88
+ });
89
+ }
90
+ return icon;
91
+ }
@@ -9,6 +9,7 @@ var _reactNative = require("react-native");
9
9
  var _AppHost = require("./AppHost");
10
10
  var _FloatingMenu = require("./FloatingMenu.js");
11
11
  var _autoDiscoverPresets = require("./autoDiscoverPresets.js");
12
+ var _AppIcon = require("./AppIcon.js");
12
13
  var _DevToolsVisibilityContext = require("./DevToolsVisibilityContext.js");
13
14
  var _optionalRequire = require("./optionalRequire.js");
14
15
  var _sharedUi = require("@buoy-gg/shared-ui");
@@ -230,6 +231,11 @@ const FloatingDevTools = ({
230
231
  // identity (and re-ran discovery) on every parent render.
231
232
  const finalApps = (0, _react.useMemo)(() => userApps ? (0, _autoDiscoverPresets.autoDiscoverPresetsWithCustom)(userApps) : (0, _autoDiscoverPresets.autoDiscoverPresets)(), [userApps]);
232
233
 
234
+ // Custom tools that declared a sync adapter — registered with the broker as
235
+ // `custom:<id>` by AutoExternalSync. Memoized so the sync tools map (keyed
236
+ // by identity) doesn't rebuild every render.
237
+ const syncApps = (0, _react.useMemo)(() => finalApps.filter(app => app.sync), [finalApps]);
238
+
233
239
  // Check if debug-borders is installed and auto-render the overlay
234
240
  const DebugBordersOverlay = (0, _react.useMemo)(() => {
235
241
  try {
@@ -366,13 +372,11 @@ const FloatingDevTools = ({
366
372
  const getToolIcon = (0, _react.useCallback)(id => {
367
373
  const tool = finalApps.find(app => app.id === id);
368
374
  if (!tool) return null;
369
- if (typeof tool.icon === "function") {
370
- return tool.icon({
371
- slot: "dial",
372
- size: 20
373
- });
374
- }
375
- return tool.icon;
375
+ return (0, _AppIcon.renderAppIcon)(tool.icon, {
376
+ slot: "dial",
377
+ size: 20,
378
+ color: tool.color
379
+ });
376
380
  }, [finalApps]);
377
381
 
378
382
  // Gate: Free tier only works in development mode
@@ -394,7 +398,8 @@ const FloatingDevTools = ({
394
398
  options: typeof externalSync === "object" ? externalSync : undefined,
395
399
  requiredEnvVars: requiredEnvVars,
396
400
  isPro: isLicensed,
397
- licenseKey: licenseKey
401
+ licenseKey: licenseKey,
402
+ customApps: syncApps
398
403
  })]
399
404
  });
400
405
  }
@@ -434,7 +439,8 @@ const FloatingDevTools = ({
434
439
  options: typeof externalSync === "object" ? externalSync : undefined,
435
440
  requiredEnvVars: requiredEnvVars,
436
441
  isPro: isLicensed,
437
- licenseKey: licenseKey
442
+ licenseKey: licenseKey,
443
+ customApps: syncApps
438
444
  })]
439
445
  })
440
446
  })
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _floatingTools = require("./floatingTools");
11
+ var _AppIcon = require("./AppIcon.js");
11
12
  var _DialDevTools = require("./dial/DialDevTools");
12
13
  var _DevToolsSettingsModal = require("./DevToolsSettingsModal");
13
14
  var _AppHost = require("./AppHost");
@@ -172,12 +173,13 @@ const FloatingMenu = ({
172
173
 
173
174
  // Only open modal if not a toggle-only tool
174
175
  if (app.launchMode !== "toggle-only") {
175
- // Resolve the icon for minimize stack display
176
- // IMPORTANT: Use React.createElement for function components to preserve hooks
177
- const resolvedIcon = typeof app.icon === "function" ? /*#__PURE__*/_react.default.createElement(app.icon, {
176
+ // Resolve the icon for minimize stack display (createElement keeps
177
+ // function-icon hooks working; strings become text icons).
178
+ const resolvedIcon = (0, _AppIcon.renderAppIcon)(app.icon, {
178
179
  slot: "dial",
179
- size: 20
180
- }) : app.icon;
180
+ size: 20,
181
+ color: app.color
182
+ });
181
183
  open({
182
184
  id: app.id,
183
185
  title: app.name,
@@ -186,7 +188,8 @@ const FloatingMenu = ({
186
188
  launchMode: app.launchMode ?? "self-modal",
187
189
  singleton: app.singleton ?? true,
188
190
  icon: resolvedIcon,
189
- color: app.color
191
+ color: app.color,
192
+ modal: app.modal
190
193
  });
191
194
  }
192
195
  };
@@ -284,7 +287,11 @@ const FloatingMenu = ({
284
287
  actions: mergedActions
285
288
  });
286
289
  }
287
- return app.icon;
290
+ return (0, _AppIcon.renderAppIcon)(app.icon, {
291
+ slot: "row",
292
+ size: 16,
293
+ color: app.color
294
+ });
288
295
  })()
289
296
  }, `row-${app.id}`))]
290
297
  })
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ToolModalShell = ToolModalShell;
7
+ exports.useToolHeader = useToolHeader;
8
+ exports.useToolHost = useToolHost;
9
+ var _react = _interopRequireWildcard(require("react"));
10
+ var _reactNative = require("react-native");
11
+ var _sharedUi = require("@buoy-gg/shared-ui");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ 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); }
14
+ /**
15
+ * ToolHost — the declarative chrome layer for custom tools.
16
+ *
17
+ * When an `InstalledApp` declares `modal: {...}`, the AppHost renders its
18
+ * component inside `ToolModalShell` instead of raw: the shared JsModal
19
+ * (drag/resize, floating⇄bottom-sheet, minimize, dimension persistence) plus
20
+ * a managed header (title/subtitle, back navigation, search, actions) and a
21
+ * lightweight toast layer. The tool body talks back to the shell through
22
+ * `useToolHost()` / `useToolHeader()`.
23
+ */
24
+
25
+ /** Per-screen header overrides a tool body can apply (see `useToolHeader`). */
26
+
27
+ const noop = () => {};
28
+ const FALLBACK_CONTEXT = {
29
+ search: "",
30
+ isSearchActive: false,
31
+ clearSearch: noop,
32
+ close: noop,
33
+ showToast: noop,
34
+ setHeaderOverride: noop
35
+ };
36
+ const ToolHostContext = /*#__PURE__*/(0, _react.createContext)(null);
37
+
38
+ /**
39
+ * Access the hosting shell from a custom tool body: search query, toasts,
40
+ * close. Safe to call outside the shell (returns inert no-ops) so tool
41
+ * components can also be mounted standalone.
42
+ */
43
+ function useToolHost() {
44
+ return (0, _react.useContext)(ToolHostContext) ?? FALLBACK_CONTEXT;
45
+ }
46
+
47
+ /**
48
+ * Declare per-screen header state (title/subtitle/actions/back) from inside a
49
+ * tool body. Applies while the calling component is mounted and reverts to
50
+ * the preset's static config on unmount — so a detail screen simply mounts
51
+ * with its own header and the list header returns when it unmounts.
52
+ *
53
+ * ```tsx
54
+ * useToolHeader(
55
+ * { title: flag.key, onBack: () => setScreen("list"), actions: <CopyButton value={flag} /> },
56
+ * [flag.key]
57
+ * );
58
+ * ```
59
+ */
60
+ function useToolHeader(override, deps) {
61
+ const {
62
+ setHeaderOverride
63
+ } = useToolHost();
64
+ (0, _react.useEffect)(() => {
65
+ setHeaderOverride(override);
66
+ return () => setHeaderOverride(null);
67
+ // Caller-provided deps intentionally control when the override refreshes.
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, deps);
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Toast layer
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const TOAST_DURATION_MS = 2200;
77
+ const TOAST_COLORS = {
78
+ success: _sharedUi.gameUIColors.success,
79
+ error: _sharedUi.gameUIColors.error,
80
+ info: _sharedUi.gameUIColors.info
81
+ };
82
+ function ToastView({
83
+ message,
84
+ type,
85
+ nonce
86
+ }) {
87
+ const opacity = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
88
+ (0, _react.useEffect)(() => {
89
+ opacity.setValue(0);
90
+ _reactNative.Animated.sequence([_reactNative.Animated.timing(opacity, {
91
+ toValue: 1,
92
+ duration: 150,
93
+ useNativeDriver: true
94
+ }), _reactNative.Animated.delay(TOAST_DURATION_MS - 450), _reactNative.Animated.timing(opacity, {
95
+ toValue: 0,
96
+ duration: 300,
97
+ useNativeDriver: true
98
+ })]).start();
99
+ }, [nonce, opacity]);
100
+ const color = TOAST_COLORS[type];
101
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
102
+ pointerEvents: "none",
103
+ style: [styles.toast, {
104
+ opacity,
105
+ borderColor: color + "60"
106
+ }],
107
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
108
+ style: [styles.toastDot, {
109
+ backgroundColor: color
110
+ }]
111
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
112
+ style: styles.toastText,
113
+ numberOfLines: 2,
114
+ children: message
115
+ })]
116
+ });
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Shell
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function ToolModalShell({
124
+ app,
125
+ config,
126
+ visible,
127
+ zIndex,
128
+ onClose,
129
+ onMinimize,
130
+ initialModalState,
131
+ enableSharedModalDimensions,
132
+ bodyProps
133
+ }) {
134
+ const Body = app.component;
135
+ const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
136
+ const [search, setSearch] = (0, _react.useState)("");
137
+ const [headerOverride, setHeaderOverride] = (0, _react.useState)(null);
138
+ const [toast, setToast] = (0, _react.useState)(null);
139
+ const toastTimeoutRef = (0, _react.useRef)(null);
140
+ const clearSearch = (0, _react.useCallback)(() => {
141
+ setIsSearchActive(false);
142
+ setSearch("");
143
+ }, []);
144
+ const showToast = (0, _react.useCallback)((message, type = "info") => {
145
+ if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
146
+ setToast(prev => ({
147
+ message,
148
+ type,
149
+ nonce: (prev?.nonce ?? 0) + 1
150
+ }));
151
+ toastTimeoutRef.current = setTimeout(() => setToast(null), TOAST_DURATION_MS);
152
+ }, []);
153
+ (0, _react.useEffect)(() => {
154
+ return () => {
155
+ if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
156
+ };
157
+ }, []);
158
+ const contextValue = (0, _react.useMemo)(() => ({
159
+ search: isSearchActive ? search : "",
160
+ isSearchActive,
161
+ clearSearch,
162
+ close: onClose,
163
+ showToast,
164
+ setHeaderOverride,
165
+ instanceId: app.instanceId
166
+ }), [search, isSearchActive, clearSearch, onClose, showToast, app.instanceId]);
167
+ const title = headerOverride?.title ?? config.title ?? app.title ?? app.id;
168
+ const subtitle = headerOverride?.subtitle ?? config.subtitle;
169
+ const onBack = headerOverride?.onBack;
170
+ const actions = headerOverride?.actions ?? config.headerActions;
171
+ const searchable = headerOverride?.searchable ?? config.searchable;
172
+
173
+ // A screen that hides search while it's open would strand the input with
174
+ // no way to dismiss it — close it on entry instead.
175
+ (0, _react.useEffect)(() => {
176
+ if (!searchable && isSearchActive) clearSearch();
177
+ }, [searchable, isSearchActive, clearSearch]);
178
+
179
+ // Tool-scoped persistence via the custom namespace unless the user has the
180
+ // shared-dimensions global setting on (all tools share one window size).
181
+ const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : _sharedUi.devToolsStorageKeys.custom(app.id).modal();
182
+ const header = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
183
+ children: [onBack && !isSearchActive && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
184
+ onBack: onBack
185
+ }), isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
186
+ value: search,
187
+ onChangeText: setSearch,
188
+ onCancel: clearSearch,
189
+ placeholder: config.searchPlaceholder
190
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
191
+ title: title,
192
+ subtitle: subtitle
193
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
194
+ children: [!isSearchActive && actions, searchable && !isSearchActive && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.HeaderSearchButton, {
195
+ testID: `tool-search-${app.id}`,
196
+ onPress: () => setIsSearchActive(true)
197
+ })]
198
+ })]
199
+ });
200
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(ToolHostContext.Provider, {
201
+ value: contextValue,
202
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
203
+ visible: visible,
204
+ onClose: onClose,
205
+ onBack: onBack,
206
+ onMinimize: onMinimize,
207
+ initialModalState: initialModalState,
208
+ zIndex: zIndex,
209
+ persistenceKey: persistenceKey,
210
+ enablePersistence: true,
211
+ initialMode: config.initialMode ?? "bottomSheet",
212
+ header: {
213
+ customContent: header,
214
+ showToggleButton: true
215
+ }
216
+ // Tool bodies own their scrolling (ScrollView/FlatList) — the shell
217
+ // must not add a competing wrapper.
218
+ ,
219
+ disableScrollWrapper: true,
220
+ styles: {},
221
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
222
+ style: styles.bodyRoot,
223
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TickProvider, {
224
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Body, {
225
+ ...bodyProps
226
+ })
227
+ }), toast && /*#__PURE__*/(0, _jsxRuntime.jsx)(ToastView, {
228
+ message: toast.message,
229
+ type: toast.type,
230
+ nonce: toast.nonce
231
+ })]
232
+ })
233
+ })
234
+ });
235
+ }
236
+ const styles = _reactNative.StyleSheet.create({
237
+ bodyRoot: {
238
+ flex: 1,
239
+ minHeight: 0,
240
+ backgroundColor: _sharedUi.buoyColors.base
241
+ },
242
+ toast: {
243
+ position: "absolute",
244
+ bottom: 16,
245
+ alignSelf: "center",
246
+ flexDirection: "row",
247
+ alignItems: "center",
248
+ gap: 8,
249
+ maxWidth: "86%",
250
+ backgroundColor: _sharedUi.buoyColors.card,
251
+ borderWidth: 1,
252
+ borderRadius: 8,
253
+ paddingHorizontal: 12,
254
+ paddingVertical: 8,
255
+ shadowColor: "#000",
256
+ shadowOpacity: 0.4,
257
+ shadowRadius: 8,
258
+ shadowOffset: {
259
+ width: 0,
260
+ height: 2
261
+ },
262
+ elevation: 6
263
+ },
264
+ toastDot: {
265
+ width: 6,
266
+ height: 6,
267
+ borderRadius: 3
268
+ },
269
+ toastText: {
270
+ flex: 1,
271
+ fontSize: 12,
272
+ color: _sharedUi.buoyColors.text
273
+ }
274
+ });
@@ -113,6 +113,13 @@ const ALL_TOOLS_REGISTRY = exports.ALL_TOOLS_REGISTRY = [{
113
113
  renderIcon: size => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.EventsIcon, {
114
114
  size: size
115
115
  })
116
+ }, {
117
+ id: "time-machine",
118
+ name: "TIME MACHINE",
119
+ color: _floatingToolsCore.TIME_MACHINE_ICON_COLOR,
120
+ renderIcon: size => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.TimeMachineIcon, {
121
+ size: size
122
+ })
116
123
  }, {
117
124
  id: "jotai",
118
125
  name: "JOTAI",
@@ -189,6 +189,22 @@ function autoDiscoverPresets() {
189
189
  }
190
190
  }
191
191
  },
192
+ // Time Machine
193
+ {
194
+ name: "@buoy-gg/time-machine",
195
+ loader: () => {
196
+ try {
197
+ // @ts-ignore - Dynamic import that may not exist
198
+ const {
199
+ timeMachineToolPreset
200
+ } = require("@buoy-gg/time-machine");
201
+ return timeMachineToolPreset;
202
+ } catch (e) {
203
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/time-machine", e);
204
+ return null;
205
+ }
206
+ }
207
+ },
192
208
  // Redux DevTools
193
209
  {
194
210
  name: "@buoy-gg/redux",
@@ -44,6 +44,7 @@ let zustandModule = null;
44
44
  let jotaiModule = null;
45
45
  let eventsModule = null;
46
46
  let consoleModule = null;
47
+ let timeMachineModule = null;
47
48
  let envModule = null;
48
49
  let highlightUpdatesModule = null;
49
50
  let debugBordersModule = null;
@@ -107,6 +108,11 @@ try {
107
108
  } catch (e) {
108
109
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/console", e);
109
110
  }
111
+ try {
112
+ timeMachineModule = require("@buoy-gg/time-machine");
113
+ } catch (e) {
114
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/time-machine", e);
115
+ }
110
116
  try {
111
117
  envModule = require("@buoy-gg/env");
112
118
  } catch (e) {
@@ -174,7 +180,8 @@ function AutoExternalSync({
174
180
  options,
175
181
  requiredEnvVars,
176
182
  isPro,
177
- licenseKey
183
+ licenseKey,
184
+ customApps
178
185
  }) {
179
186
  const {
180
187
  useExternalSyncSocket,
@@ -260,6 +267,9 @@ function AutoExternalSync({
260
267
  if (consoleModule?.consoleSyncAdapter) {
261
268
  map.console = consoleModule.consoleSyncAdapter;
262
269
  }
270
+ if (timeMachineModule?.timeMachineSyncAdapter) {
271
+ map["time-machine"] = timeMachineModule.timeMachineSyncAdapter;
272
+ }
263
273
  if (envModule?.createEnvSyncAdapter) {
264
274
  map.env = envModule.createEnvSyncAdapter(requiredEnvVars ?? []);
265
275
  }
@@ -284,8 +294,17 @@ function AutoExternalSync({
284
294
  if (assetsModule?.assetsSyncAdapter) {
285
295
  map.assets = assetsModule.assetsSyncAdapter;
286
296
  }
297
+
298
+ // Third-party custom tools that declared InstalledApp.sync. Namespaced so
299
+ // a custom tool named "network" can't shadow the first-party adapter on
300
+ // the broker, in MCP call_action, or in desktop watch state.
301
+ for (const app of customApps ?? []) {
302
+ if (app?.sync) {
303
+ map[`custom:${app.id}`] = app.sync;
304
+ }
305
+ }
287
306
  return map;
288
- }, [queryAdapter, requiredEnvVars]);
307
+ }, [queryAdapter, requiredEnvVars, customApps]);
289
308
  useExternalSync({
290
309
  tools,
291
310
  socket,