@buoy-gg/core 7.0.15 → 7.0.17

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 (133) hide show
  1. package/lib/commonjs/floatingMenu/AppHost.js +18 -0
  2. package/lib/commonjs/floatingMenu/AppHost.web.js +359 -0
  3. package/lib/commonjs/floatingMenu/AppHostLogic.js +6 -0
  4. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +787 -418
  5. package/lib/commonjs/floatingMenu/FloatingDevTools.js +1 -1
  6. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +244 -36
  7. package/lib/commonjs/floatingMenu/FloatingMenu.js +4 -3
  8. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +2 -3
  9. package/lib/commonjs/floatingMenu/MinimizedToolsStack.web.js +287 -0
  10. package/lib/commonjs/floatingMenu/ToolHost.js +264 -0
  11. package/lib/commonjs/floatingMenu/allToolsRegistry.js +7 -0
  12. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +32 -0
  13. package/lib/commonjs/floatingMenu/autoExternalSync.js +18 -0
  14. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +9 -5
  15. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +373 -231
  16. package/lib/commonjs/floatingMenu/dial/DialIcon.js +25 -3
  17. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.js +1 -1
  18. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.web.js +154 -0
  19. package/lib/commonjs/floatingMenu/dial/onboardingConstants.web.js +34 -0
  20. package/lib/commonjs/floatingMenu/floatingTools.js +3 -3
  21. package/lib/commonjs/floatingMenu/floatingTools.web.js +191 -28
  22. package/lib/commonjs/index.js +29 -2
  23. package/lib/commonjs/index.web.js +19 -0
  24. package/lib/commonjs/toolkit.js +427 -0
  25. package/lib/commonjs/utils/autoDiscoverPresets.web.js +50 -5
  26. package/lib/module/floatingMenu/AppHost.js +18 -0
  27. package/lib/module/floatingMenu/AppHost.web.js +351 -0
  28. package/lib/module/floatingMenu/AppHostLogic.js +6 -0
  29. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +792 -422
  30. package/lib/module/floatingMenu/FloatingDevTools.js +2 -2
  31. package/lib/module/floatingMenu/FloatingDevTools.web.js +247 -38
  32. package/lib/module/floatingMenu/FloatingMenu.js +4 -3
  33. package/lib/module/floatingMenu/MinimizedToolsContext.js +2 -3
  34. package/lib/module/floatingMenu/MinimizedToolsStack.web.js +284 -0
  35. package/lib/module/floatingMenu/ToolHost.js +256 -0
  36. package/lib/module/floatingMenu/allToolsRegistry.js +8 -1
  37. package/lib/module/floatingMenu/autoDiscoverPresets.js +32 -0
  38. package/lib/module/floatingMenu/autoExternalSync.js +18 -0
  39. package/lib/module/floatingMenu/dial/DialDevTools.js +10 -6
  40. package/lib/module/floatingMenu/dial/DialDevTools.web.js +375 -233
  41. package/lib/module/floatingMenu/dial/DialIcon.js +26 -4
  42. package/lib/module/floatingMenu/dial/OnboardingTooltip.js +1 -1
  43. package/lib/module/floatingMenu/dial/OnboardingTooltip.web.js +149 -0
  44. package/lib/module/floatingMenu/dial/onboardingConstants.web.js +27 -0
  45. package/lib/module/floatingMenu/floatingTools.js +3 -3
  46. package/lib/module/floatingMenu/floatingTools.web.js +190 -28
  47. package/lib/module/index.js +8 -2
  48. package/lib/module/index.web.js +5 -0
  49. package/lib/module/toolkit.js +45 -0
  50. package/lib/module/utils/autoDiscoverPresets.web.js +53 -7
  51. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts +38 -0
  53. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts +24 -1
  55. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts +5 -2
  57. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +2 -14
  59. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts +2 -20
  62. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts +21 -0
  64. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts +72 -0
  66. package/lib/typescript/commonjs/floatingMenu/ToolHost.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  71. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts +16 -2
  74. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.web.d.ts +9 -0
  77. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.web.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts +13 -0
  79. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts +29 -2
  81. package/lib/typescript/commonjs/floatingMenu/floatingTools.web.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/floatingMenu/types.d.ts +30 -1
  83. package/lib/typescript/commonjs/floatingMenu/types.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/index.d.ts +2 -0
  85. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  87. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/toolkit.d.ts +22 -0
  89. package/lib/typescript/commonjs/toolkit.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts +2 -0
  91. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  92. package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -1
  93. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts +38 -0
  94. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts.map +1 -0
  95. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts +24 -1
  96. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts.map +1 -1
  97. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts +5 -2
  98. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  99. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +2 -14
  100. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  101. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -1
  102. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts +2 -20
  103. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  104. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts +21 -0
  105. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -0
  106. package/lib/typescript/module/floatingMenu/ToolHost.d.ts +72 -0
  107. package/lib/typescript/module/floatingMenu/ToolHost.d.ts.map +1 -0
  108. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  109. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  110. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  111. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  112. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  113. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  114. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts +16 -2
  115. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  116. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  117. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.web.d.ts +9 -0
  118. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.web.d.ts.map +1 -0
  119. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts +13 -0
  120. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.web.d.ts.map +1 -0
  121. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts +29 -2
  122. package/lib/typescript/module/floatingMenu/floatingTools.web.d.ts.map +1 -1
  123. package/lib/typescript/module/floatingMenu/types.d.ts +30 -1
  124. package/lib/typescript/module/floatingMenu/types.d.ts.map +1 -1
  125. package/lib/typescript/module/index.d.ts +2 -0
  126. package/lib/typescript/module/index.d.ts.map +1 -1
  127. package/lib/typescript/module/index.web.d.ts +1 -0
  128. package/lib/typescript/module/index.web.d.ts.map +1 -1
  129. package/lib/typescript/module/toolkit.d.ts +22 -0
  130. package/lib/typescript/module/toolkit.d.ts.map +1 -0
  131. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts +2 -0
  132. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  133. package/package.json +5 -5
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * MinimizedToolsStack — Web twin of MinimizedToolsStack.tsx +
5
+ * shared-ui's ExpandablePopover (inlined: the popover is only used here on
6
+ * web). A vertical stack of minimized-tool icons fused to the top of the
7
+ * floating bubble's drag handle:
8
+ * - collapsed: a 28px "peek" tab (chevron-up + count) with top corners only
9
+ * - expanded: animated-height panel, icons newest-first, collapse chevron
10
+ * - expanded state persists under "@react_buoy_minimized_stack_expanded"
11
+ * - each icon runs the native randomized glitch loop (translateX shake +
12
+ * opacity flicker + scale pulse, 80ms), scheduled every 2-6s + index*300ms
13
+ *
14
+ * Data comes from the web AppHost (openApps with minimized === true) instead
15
+ * of the native MinimizedToolsContext — same derivation the MinimizedDock
16
+ * used before this replaced it.
17
+ */
18
+
19
+ import { useCallback, useEffect, useRef, useState } from 'react';
20
+ import { persistentStorage } from '@buoy-gg/shared-ui';
21
+ import { useAppHost } from "./AppHost.web.js";
22
+
23
+ // native MinimizedToolsStack.tsx constants
24
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+ const PEEK_HEIGHT = 28;
26
+ const TOOL_ITEM_SIZE = 32;
27
+ const ICON_GAP = 6;
28
+ const TOOLBAR_PADDING = 8;
29
+ const COLLAPSE_BUTTON_SIZE = 24;
30
+ const STORAGE_KEY_EXPANDED = '@react_buoy_minimized_stack_expanded';
31
+ const GLITCH_DURATION_MS = 80;
32
+ const MIN_GLITCH_DELAY = 2000;
33
+ const MAX_GLITCH_DELAY = 6000;
34
+
35
+ // native shared-ui buoyColors (gameUIColors.ts) — card + textMuted
36
+ const CARD = '#1A1A1A';
37
+ const TEXT_MUTED = '#888888';
38
+ function ChevronGlyph({
39
+ down = false
40
+ }) {
41
+ // native: ChevronUp size 12, strokeWidth 2, buoyColors.textMuted
42
+ return /*#__PURE__*/_jsx("svg", {
43
+ width: 12,
44
+ height: 12,
45
+ viewBox: "0 0 24 24",
46
+ "aria-hidden": "true",
47
+ style: {
48
+ transform: down ? 'rotate(180deg)' : undefined
49
+ },
50
+ children: /*#__PURE__*/_jsx("path", {
51
+ d: "M18 15l-6-6-6 6",
52
+ fill: "none",
53
+ stroke: TEXT_MUTED,
54
+ strokeWidth: "2",
55
+ strokeLinecap: "round",
56
+ strokeLinejoin: "round"
57
+ })
58
+ });
59
+ }
60
+ function GlitchToolButton({
61
+ title,
62
+ icon,
63
+ index,
64
+ onPress
65
+ }) {
66
+ const innerRef = useRef(null);
67
+ useEffect(() => {
68
+ let mounted = true;
69
+ let timer;
70
+ const runGlitch = () => {
71
+ // native: parallel translateX shake / opacity flicker / scale pulse,
72
+ // all inside GLITCH_DURATION_MS — one WAAPI animation with offsets.
73
+ innerRef.current?.animate([{
74
+ transform: 'translateX(0) scale(1)',
75
+ opacity: 1,
76
+ offset: 0
77
+ }, {
78
+ transform: 'translateX(3px) scale(1.05)',
79
+ opacity: 0.4,
80
+ offset: 0.2
81
+ }, {
82
+ transform: 'translateX(-3px) scale(1.1)',
83
+ opacity: 1,
84
+ offset: 0.4
85
+ }, {
86
+ transform: 'translateX(2px) scale(1.0)',
87
+ opacity: 0.6,
88
+ offset: 0.6
89
+ }, {
90
+ transform: 'translateX(-1px) scale(0.95)',
91
+ opacity: 1,
92
+ offset: 0.8
93
+ }, {
94
+ transform: 'translateX(0) scale(1)',
95
+ opacity: 1,
96
+ offset: 1
97
+ }], {
98
+ duration: GLITCH_DURATION_MS,
99
+ easing: 'linear'
100
+ });
101
+ };
102
+ const schedule = initial => {
103
+ const delay = initial ? 500 + Math.random() * 1500 + index * 200 : MIN_GLITCH_DELAY + Math.random() * (MAX_GLITCH_DELAY - MIN_GLITCH_DELAY) + index * 300;
104
+ timer = setTimeout(() => {
105
+ if (!mounted) return;
106
+ runGlitch();
107
+ schedule(false);
108
+ }, delay);
109
+ };
110
+ schedule(true);
111
+ return () => {
112
+ mounted = false;
113
+ clearTimeout(timer);
114
+ };
115
+ }, [index]);
116
+ return /*#__PURE__*/_jsx("button", {
117
+ type: "button",
118
+ "aria-label": `Restore ${title}`,
119
+ onClick: onPress,
120
+ style: {
121
+ width: TOOL_ITEM_SIZE,
122
+ height: TOOL_ITEM_SIZE,
123
+ display: 'flex',
124
+ alignItems: 'center',
125
+ justifyContent: 'center',
126
+ background: 'none',
127
+ border: 'none',
128
+ padding: 0,
129
+ cursor: 'pointer'
130
+ },
131
+ children: /*#__PURE__*/_jsx("div", {
132
+ ref: innerRef,
133
+ style: {
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ justifyContent: 'center'
137
+ },
138
+ children: icon
139
+ })
140
+ });
141
+ }
142
+ export function MinimizedToolsStack({
143
+ containerWidth = 32
144
+ }) {
145
+ const {
146
+ openApps,
147
+ restore
148
+ } = useAppHost();
149
+ const minimized = openApps.filter(app => app.minimized && app.launchMode !== 'toggle-only');
150
+ const [isExpanded, setIsExpanded] = useState(false);
151
+ const [isStateRestored, setIsStateRestored] = useState(false);
152
+
153
+ // Restore + persist the expanded flag (native ExpandablePopover persistState).
154
+ useEffect(() => {
155
+ let cancelled = false;
156
+ const load = async () => {
157
+ const saved = await persistentStorage.getItem(STORAGE_KEY_EXPANDED);
158
+ if (!cancelled && saved === 'true') setIsExpanded(true);
159
+ if (!cancelled) setIsStateRestored(true);
160
+ };
161
+ void load();
162
+ return () => {
163
+ cancelled = true;
164
+ };
165
+ }, []);
166
+ useEffect(() => {
167
+ if (!isStateRestored) return;
168
+ void persistentStorage.setItem(STORAGE_KEY_EXPANDED, isExpanded ? 'true' : 'false');
169
+ }, [isExpanded, isStateRestored]);
170
+ const handleRestore = useCallback(instanceId => {
171
+ restore(instanceId);
172
+ }, [restore]);
173
+ if (minimized.length === 0) return null;
174
+
175
+ // native: expandedHeight = padding + n*item + (n-1)*gap + gap + collapse + 4
176
+ const expandedHeight = TOOLBAR_PADDING + minimized.length * TOOL_ITEM_SIZE + (minimized.length - 1) * ICON_GAP + ICON_GAP + COLLAPSE_BUTTON_SIZE + 4;
177
+ const frame = {
178
+ // native peekButton/expandedContainer: card bg, top corners 6, 1px border
179
+ // on top/left/right only (fuses with the bubble below)
180
+ backgroundColor: CARD,
181
+ borderTopLeftRadius: 6,
182
+ borderTopRightRadius: 6,
183
+ borderTop: `1px solid ${TEXT_MUTED}66`,
184
+ borderLeft: `1px solid ${TEXT_MUTED}66`,
185
+ borderRight: `1px solid ${TEXT_MUTED}66`,
186
+ borderBottom: 'none',
187
+ boxSizing: 'border-box'
188
+ };
189
+ return /*#__PURE__*/_jsxs("div", {
190
+ style: {
191
+ height: PEEK_HEIGHT,
192
+ overflow: 'visible',
193
+ position: 'relative'
194
+ },
195
+ children: [/*#__PURE__*/_jsx("div", {
196
+ style: {
197
+ position: 'absolute',
198
+ bottom: 0,
199
+ left: 0,
200
+ width: containerWidth,
201
+ height: isExpanded ? expandedHeight : 0,
202
+ opacity: isExpanded ? 1 : 0,
203
+ overflow: 'hidden',
204
+ transition: 'height 200ms ease, opacity 200ms ease',
205
+ pointerEvents: isExpanded ? 'auto' : 'none',
206
+ ...frame
207
+ },
208
+ children: /*#__PURE__*/_jsxs("div", {
209
+ style: {
210
+ height: expandedHeight,
211
+ display: 'flex',
212
+ flexDirection: 'column',
213
+ alignItems: 'center',
214
+ paddingTop: TOOLBAR_PADDING,
215
+ gap: ICON_GAP,
216
+ boxSizing: 'border-box'
217
+ },
218
+ children: [[...minimized].reverse().map((app, index) => /*#__PURE__*/_jsx(GlitchToolButton, {
219
+ title: app.title || app.id,
220
+ icon: app.icon ?? /*#__PURE__*/_jsx("span", {
221
+ style: {
222
+ color: '#fff',
223
+ fontSize: 13
224
+ },
225
+ children: (app.title || app.id).slice(0, 1).toUpperCase()
226
+ }),
227
+ index: index,
228
+ onPress: () => handleRestore(app.instanceId)
229
+ }, app.instanceId)), /*#__PURE__*/_jsx("button", {
230
+ type: "button",
231
+ "aria-label": "Collapse",
232
+ onClick: () => setIsExpanded(false),
233
+ style: {
234
+ width: '100%',
235
+ height: COLLAPSE_BUTTON_SIZE,
236
+ marginTop: 3,
237
+ display: 'flex',
238
+ alignItems: 'center',
239
+ justifyContent: 'center',
240
+ background: 'none',
241
+ border: 'none',
242
+ cursor: 'pointer',
243
+ padding: 0
244
+ },
245
+ children: /*#__PURE__*/_jsx(ChevronGlyph, {
246
+ down: true
247
+ })
248
+ })]
249
+ })
250
+ }), /*#__PURE__*/_jsxs("button", {
251
+ type: "button",
252
+ "aria-label": `Show ${minimized.length} minimized tools`,
253
+ onClick: () => setIsExpanded(true),
254
+ style: {
255
+ position: 'absolute',
256
+ bottom: 0,
257
+ left: 0,
258
+ width: containerWidth,
259
+ height: PEEK_HEIGHT,
260
+ display: 'flex',
261
+ flexDirection: 'row',
262
+ alignItems: 'center',
263
+ justifyContent: 'center',
264
+ gap: 2,
265
+ cursor: 'pointer',
266
+ padding: 0,
267
+ opacity: isExpanded ? 0 : 1,
268
+ transform: isExpanded ? 'scale(0.9)' : 'scale(1)',
269
+ transition: 'opacity 200ms ease, transform 200ms ease',
270
+ pointerEvents: isExpanded ? 'none' : 'auto',
271
+ ...frame
272
+ },
273
+ children: [/*#__PURE__*/_jsx(ChevronGlyph, {}), minimized.length > 1 && /*#__PURE__*/_jsx("span", {
274
+ style: {
275
+ fontSize: 9,
276
+ fontWeight: 600,
277
+ color: TEXT_MUTED,
278
+ marginLeft: 1
279
+ },
280
+ children: minimized.length
281
+ })]
282
+ })]
283
+ });
284
+ }
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ToolHost — the declarative chrome layer for custom tools.
5
+ *
6
+ * When an `InstalledApp` declares `modal: {...}`, the AppHost renders its
7
+ * component inside `ToolModalShell` instead of raw: the shared JsModal
8
+ * (drag/resize, floating⇄bottom-sheet, minimize, dimension persistence) plus
9
+ * a managed header (title/subtitle, back navigation, search, actions) and a
10
+ * lightweight toast layer. The tool body talks back to the shell through
11
+ * `useToolHost()` / `useToolHeader()`.
12
+ */
13
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
14
+ import { Animated, StyleSheet, Text, View } from "react-native";
15
+ import { JsModal, ModalHeader, HeaderSearchButton, devToolsStorageKeys, buoyColors, gameUIColors } from "@buoy-gg/shared-ui";
16
+
17
+ /** Per-screen header overrides a tool body can apply (see `useToolHeader`). */
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ const noop = () => {};
20
+ const FALLBACK_CONTEXT = {
21
+ search: "",
22
+ isSearchActive: false,
23
+ clearSearch: noop,
24
+ close: noop,
25
+ showToast: noop,
26
+ setHeaderOverride: noop
27
+ };
28
+ const ToolHostContext = /*#__PURE__*/createContext(null);
29
+
30
+ /**
31
+ * Access the hosting shell from a custom tool body: search query, toasts,
32
+ * close. Safe to call outside the shell (returns inert no-ops) so tool
33
+ * components can also be mounted standalone.
34
+ */
35
+ export function useToolHost() {
36
+ return useContext(ToolHostContext) ?? FALLBACK_CONTEXT;
37
+ }
38
+
39
+ /**
40
+ * Declare per-screen header state (title/subtitle/actions/back) from inside a
41
+ * tool body. Applies while the calling component is mounted and reverts to
42
+ * the preset's static config on unmount — so a detail screen simply mounts
43
+ * with its own header and the list header returns when it unmounts.
44
+ *
45
+ * ```tsx
46
+ * useToolHeader(
47
+ * { title: flag.key, onBack: () => setScreen("list"), actions: <CopyButton value={flag} /> },
48
+ * [flag.key]
49
+ * );
50
+ * ```
51
+ */
52
+ export function useToolHeader(override, deps) {
53
+ const {
54
+ setHeaderOverride
55
+ } = useToolHost();
56
+ useEffect(() => {
57
+ setHeaderOverride(override);
58
+ return () => setHeaderOverride(null);
59
+ // Caller-provided deps intentionally control when the override refreshes.
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, deps);
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Toast layer
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const TOAST_DURATION_MS = 2200;
69
+ const TOAST_COLORS = {
70
+ success: gameUIColors.success,
71
+ error: gameUIColors.error,
72
+ info: gameUIColors.info
73
+ };
74
+ function ToastView({
75
+ message,
76
+ type,
77
+ nonce
78
+ }) {
79
+ const opacity = useRef(new Animated.Value(0)).current;
80
+ useEffect(() => {
81
+ opacity.setValue(0);
82
+ Animated.sequence([Animated.timing(opacity, {
83
+ toValue: 1,
84
+ duration: 150,
85
+ useNativeDriver: true
86
+ }), Animated.delay(TOAST_DURATION_MS - 450), Animated.timing(opacity, {
87
+ toValue: 0,
88
+ duration: 300,
89
+ useNativeDriver: true
90
+ })]).start();
91
+ }, [nonce, opacity]);
92
+ const color = TOAST_COLORS[type];
93
+ return /*#__PURE__*/_jsxs(Animated.View, {
94
+ pointerEvents: "none",
95
+ style: [styles.toast, {
96
+ opacity,
97
+ borderColor: color + "60"
98
+ }],
99
+ children: [/*#__PURE__*/_jsx(View, {
100
+ style: [styles.toastDot, {
101
+ backgroundColor: color
102
+ }]
103
+ }), /*#__PURE__*/_jsx(Text, {
104
+ style: styles.toastText,
105
+ numberOfLines: 2,
106
+ children: message
107
+ })]
108
+ });
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Shell
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export function ToolModalShell({
116
+ app,
117
+ config,
118
+ visible,
119
+ zIndex,
120
+ onClose,
121
+ onMinimize,
122
+ initialModalState,
123
+ enableSharedModalDimensions,
124
+ bodyProps
125
+ }) {
126
+ const Body = app.component;
127
+ const [isSearchActive, setIsSearchActive] = useState(false);
128
+ const [search, setSearch] = useState("");
129
+ const [headerOverride, setHeaderOverride] = useState(null);
130
+ const [toast, setToast] = useState(null);
131
+ const toastTimeoutRef = useRef(null);
132
+ const clearSearch = useCallback(() => {
133
+ setIsSearchActive(false);
134
+ setSearch("");
135
+ }, []);
136
+ const showToast = useCallback((message, type = "info") => {
137
+ if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
138
+ setToast(prev => ({
139
+ message,
140
+ type,
141
+ nonce: (prev?.nonce ?? 0) + 1
142
+ }));
143
+ toastTimeoutRef.current = setTimeout(() => setToast(null), TOAST_DURATION_MS);
144
+ }, []);
145
+ useEffect(() => {
146
+ return () => {
147
+ if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
148
+ };
149
+ }, []);
150
+ const contextValue = useMemo(() => ({
151
+ search: isSearchActive ? search : "",
152
+ isSearchActive,
153
+ clearSearch,
154
+ close: onClose,
155
+ showToast,
156
+ setHeaderOverride,
157
+ instanceId: app.instanceId
158
+ }), [search, isSearchActive, clearSearch, onClose, showToast, app.instanceId]);
159
+ const title = headerOverride?.title ?? config.title ?? app.title ?? app.id;
160
+ const subtitle = headerOverride?.subtitle ?? config.subtitle;
161
+ const onBack = headerOverride?.onBack;
162
+ const actions = headerOverride?.actions ?? config.headerActions;
163
+
164
+ // Tool-scoped persistence via the custom namespace unless the user has the
165
+ // shared-dimensions global setting on (all tools share one window size).
166
+ const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.custom(app.id).modal();
167
+ const header = /*#__PURE__*/_jsxs(ModalHeader, {
168
+ children: [onBack && !isSearchActive && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
169
+ onBack: onBack
170
+ }), isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
171
+ value: search,
172
+ onChangeText: setSearch,
173
+ onCancel: clearSearch,
174
+ placeholder: config.searchPlaceholder
175
+ }) : /*#__PURE__*/_jsx(ModalHeader.Content, {
176
+ title: title,
177
+ subtitle: subtitle
178
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
179
+ children: [!isSearchActive && actions, config.searchable && !isSearchActive && /*#__PURE__*/_jsx(HeaderSearchButton, {
180
+ onPress: () => setIsSearchActive(true)
181
+ })]
182
+ })]
183
+ });
184
+ return /*#__PURE__*/_jsx(ToolHostContext.Provider, {
185
+ value: contextValue,
186
+ children: /*#__PURE__*/_jsx(JsModal, {
187
+ visible: visible,
188
+ onClose: onClose,
189
+ onBack: onBack,
190
+ onMinimize: onMinimize,
191
+ initialModalState: initialModalState,
192
+ zIndex: zIndex,
193
+ persistenceKey: persistenceKey,
194
+ enablePersistence: true,
195
+ initialMode: config.initialMode ?? "bottomSheet",
196
+ header: {
197
+ customContent: header,
198
+ showToggleButton: true
199
+ }
200
+ // Tool bodies own their scrolling (ScrollView/FlatList) — the shell
201
+ // must not add a competing wrapper.
202
+ ,
203
+ disableScrollWrapper: true,
204
+ styles: {},
205
+ children: /*#__PURE__*/_jsxs(View, {
206
+ style: styles.bodyRoot,
207
+ children: [/*#__PURE__*/_jsx(Body, {
208
+ ...bodyProps
209
+ }), toast && /*#__PURE__*/_jsx(ToastView, {
210
+ message: toast.message,
211
+ type: toast.type,
212
+ nonce: toast.nonce
213
+ })]
214
+ })
215
+ })
216
+ });
217
+ }
218
+ const styles = StyleSheet.create({
219
+ bodyRoot: {
220
+ flex: 1,
221
+ minHeight: 0,
222
+ backgroundColor: buoyColors.base
223
+ },
224
+ toast: {
225
+ position: "absolute",
226
+ bottom: 16,
227
+ alignSelf: "center",
228
+ flexDirection: "row",
229
+ alignItems: "center",
230
+ gap: 8,
231
+ maxWidth: "86%",
232
+ backgroundColor: buoyColors.card,
233
+ borderWidth: 1,
234
+ borderRadius: 8,
235
+ paddingHorizontal: 12,
236
+ paddingVertical: 8,
237
+ shadowColor: "#000",
238
+ shadowOpacity: 0.4,
239
+ shadowRadius: 8,
240
+ shadowOffset: {
241
+ width: 0,
242
+ height: 2
243
+ },
244
+ elevation: 6
245
+ },
246
+ toastDot: {
247
+ width: 6,
248
+ height: 6,
249
+ borderRadius: 3
250
+ },
251
+ toastText: {
252
+ flex: 1,
253
+ fontSize: 12,
254
+ color: buoyColors.text
255
+ }
256
+ });
@@ -27,7 +27,7 @@ import { ImageOverlayIcon } from "@buoy-gg/shared-ui";
27
27
  // in shared/icons/*.json and generated into @buoy-gg/floating-tools-core, which
28
28
  // IS a required dependency. Unavailable items therefore render the ACTUAL tool
29
29
  // icon (just dimmed by DialIcon), and it can no longer drift from the real one.
30
- import { ZustandIcon, ZUSTAND_ICON_COLOR, JotaiIcon, JOTAI_ICON_COLOR, IMAGE_OVERLAY_ICON_COLOR } from "@buoy-gg/floating-tools-core";
30
+ import { ZustandIcon, ZUSTAND_ICON_COLOR, JotaiIcon, JOTAI_ICON_COLOR, IMAGE_OVERLAY_ICON_COLOR, TimeMachineIcon, TIME_MACHINE_ICON_COLOR } from "@buoy-gg/floating-tools-core";
31
31
 
32
32
  // ─── Registry entry type ──────────────────────────────────────────────────────
33
33
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -109,6 +109,13 @@ export const ALL_TOOLS_REGISTRY = [{
109
109
  renderIcon: size => /*#__PURE__*/_jsx(EventsIcon, {
110
110
  size: size
111
111
  })
112
+ }, {
113
+ id: "time-machine",
114
+ name: "TIME MACHINE",
115
+ color: TIME_MACHINE_ICON_COLOR,
116
+ renderIcon: size => /*#__PURE__*/_jsx(TimeMachineIcon, {
117
+ size: size
118
+ })
112
119
  }, {
113
120
  id: "jotai",
114
121
  name: "JOTAI",
@@ -185,6 +185,22 @@ export function autoDiscoverPresets() {
185
185
  }
186
186
  }
187
187
  },
188
+ // Time Machine
189
+ {
190
+ name: "@buoy-gg/time-machine",
191
+ loader: () => {
192
+ try {
193
+ // @ts-ignore - Dynamic import that may not exist
194
+ const {
195
+ timeMachineToolPreset
196
+ } = require("@buoy-gg/time-machine");
197
+ return timeMachineToolPreset;
198
+ } catch (e) {
199
+ reportOptionalRequireError("@buoy-gg/time-machine", e);
200
+ return null;
201
+ }
202
+ }
203
+ },
188
204
  // Redux DevTools
189
205
  {
190
206
  name: "@buoy-gg/redux",
@@ -314,6 +330,22 @@ export function autoDiscoverPresets() {
314
330
  return null;
315
331
  }
316
332
  }
333
+ },
334
+ // ASSETS — bundled-asset inventory (sizes / scale coverage / unused)
335
+ {
336
+ name: "@buoy-gg/assets",
337
+ loader: () => {
338
+ try {
339
+ // @ts-ignore - Dynamic import that may not exist
340
+ const {
341
+ assetsToolPreset
342
+ } = require("@buoy-gg/assets");
343
+ return assetsToolPreset;
344
+ } catch (e) {
345
+ reportOptionalRequireError("@buoy-gg/assets", e);
346
+ return null;
347
+ }
348
+ }
317
349
  }];
318
350
 
319
351
  // Attempt to load each preset
@@ -39,6 +39,7 @@ let zustandModule = null;
39
39
  let jotaiModule = null;
40
40
  let eventsModule = null;
41
41
  let consoleModule = null;
42
+ let timeMachineModule = null;
42
43
  let envModule = null;
43
44
  let highlightUpdatesModule = null;
44
45
  let debugBordersModule = null;
@@ -46,6 +47,7 @@ let impersonateModule = null;
46
47
  let perfMonitorModule = null;
47
48
  let jsTopModule = null;
48
49
  let imagesModule = null;
50
+ let assetsModule = null;
49
51
  let expoConstantsModule = null;
50
52
 
51
53
  // Catch bodies report through reportOptionalRequireError: silent for a
@@ -101,6 +103,11 @@ try {
101
103
  } catch (e) {
102
104
  reportOptionalRequireError("@buoy-gg/console", e);
103
105
  }
106
+ try {
107
+ timeMachineModule = require("@buoy-gg/time-machine");
108
+ } catch (e) {
109
+ reportOptionalRequireError("@buoy-gg/time-machine", e);
110
+ }
104
111
  try {
105
112
  envModule = require("@buoy-gg/env");
106
113
  } catch (e) {
@@ -136,6 +143,11 @@ try {
136
143
  } catch (e) {
137
144
  reportOptionalRequireError("@buoy-gg/images", e);
138
145
  }
146
+ try {
147
+ assetsModule = require("@buoy-gg/assets");
148
+ } catch (e) {
149
+ reportOptionalRequireError("@buoy-gg/assets", e);
150
+ }
139
151
  try {
140
152
  expoConstantsModule = require("expo-constants");
141
153
  } catch (e) {
@@ -249,6 +261,9 @@ export function AutoExternalSync({
249
261
  if (consoleModule?.consoleSyncAdapter) {
250
262
  map.console = consoleModule.consoleSyncAdapter;
251
263
  }
264
+ if (timeMachineModule?.timeMachineSyncAdapter) {
265
+ map["time-machine"] = timeMachineModule.timeMachineSyncAdapter;
266
+ }
252
267
  if (envModule?.createEnvSyncAdapter) {
253
268
  map.env = envModule.createEnvSyncAdapter(requiredEnvVars ?? []);
254
269
  }
@@ -270,6 +285,9 @@ export function AutoExternalSync({
270
285
  if (imagesModule?.imagesSyncAdapter) {
271
286
  map.images = imagesModule.imagesSyncAdapter;
272
287
  }
288
+ if (assetsModule?.assetsSyncAdapter) {
289
+ map.assets = assetsModule.assetsSyncAdapter;
290
+ }
273
291
  return map;
274
292
  }, [queryAdapter, requiredEnvVars]);
275
293
  useExternalSync({
@@ -11,9 +11,9 @@ import { getRankedToolIds, isDialUsageLoaded, loadDialUsage, recordToolUsage } f
11
11
  import { persistentStorage, useHintsDisabled, devToolsStorageKeys, buoyColors } from "@buoy-gg/shared-ui";
12
12
  import { DevToolsSettingsModal, useDevToolsSettings } from "../DevToolsSettingsModal";
13
13
  import { useProAccess } from "@buoy-gg/license";
14
- import { useAppHost } from "../AppHost.js";
15
- import { OnboardingTooltip } from "./OnboardingTooltip.js";
16
- import { getDialLayout, MAX_DIAL_SLOTS, DIAL_BUTTON_SIZE, DIAL_ICON_SIZE, dialAnimationConfig, dialColors } from "@buoy-gg/floating-tools-core";
14
+ import { useAppHost } from "../AppHost";
15
+ import { OnboardingTooltip } from "./OnboardingTooltip";
16
+ import { getDialLayout, MAX_DIAL_SLOTS, DIAL_BUTTON_SIZE, DIAL_ICON_GLYPH_SIZE, dialAnimationConfig, dialColors } from "@buoy-gg/floating-tools-core";
17
17
 
18
18
  // The circle size depends on the live window width, so it's computed inside
19
19
  // the component via useWindowDimensions — a module-scope Dimensions.get
@@ -200,7 +200,8 @@ export const DialDevTools = ({
200
200
  launchMode: app.launchMode ?? "self-modal",
201
201
  singleton: app.singleton ?? true,
202
202
  icon: resolvedIcon,
203
- color: app.color
203
+ color: app.color,
204
+ modal: app.modal
204
205
  });
205
206
  }
206
207
 
@@ -227,8 +228,11 @@ export const DialDevTools = ({
227
228
  map.set(tool.id, {
228
229
  id: tool.id,
229
230
  name: tool.name,
230
- // Pre-render the icon at the standard dial slot size so it looks correct.
231
- icon: tool.renderIcon(DIAL_ICON_SIZE),
231
+ // Pre-render the icon at the standard GLYPH size (not the slot size —
232
+ // the slot also holds the label + "Unavailable" caption, so a glyph
233
+ // rendered at DIAL_ICON_SIZE overflows the slot and looks ~2x bigger
234
+ // than installed tools' icons).
235
+ icon: tool.renderIcon(DIAL_ICON_GLYPH_SIZE),
232
236
  iconComponent: undefined,
233
237
  color: tool.color,
234
238
  unavailable: true,