@buoy-gg/core 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +43 -0
  2. package/lib/commonjs/floatingMenu/AppHost.js +410 -0
  3. package/lib/commonjs/floatingMenu/AppHostLogic.js +44 -0
  4. package/lib/commonjs/floatingMenu/DefaultConfigContext.js +45 -0
  5. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +2274 -0
  6. package/lib/commonjs/floatingMenu/DevToolsVisibilityContext.js +49 -0
  7. package/lib/commonjs/floatingMenu/DraggableHeader.js +114 -0
  8. package/lib/commonjs/floatingMenu/FloatingDevTools.js +254 -0
  9. package/lib/commonjs/floatingMenu/FloatingMenu.js +364 -0
  10. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +247 -0
  11. package/lib/commonjs/floatingMenu/MinimizedToolsStack.js +206 -0
  12. package/lib/commonjs/floatingMenu/ToggleStateManager.js +36 -0
  13. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +241 -0
  14. package/lib/commonjs/floatingMenu/defaultConfig.js +160 -0
  15. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +835 -0
  16. package/lib/commonjs/floatingMenu/dial/DialIcon.js +246 -0
  17. package/lib/commonjs/floatingMenu/dial/OnboardingTooltip.js +249 -0
  18. package/lib/commonjs/floatingMenu/dial/onboardingConstants.js +70 -0
  19. package/lib/commonjs/floatingMenu/floatingTools.js +771 -0
  20. package/lib/commonjs/floatingMenu/settingsBus.js +23 -0
  21. package/lib/commonjs/floatingMenu/types.js +5 -0
  22. package/lib/commonjs/index.js +240 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/module/floatingMenu/AppHost.js +402 -0
  25. package/lib/module/floatingMenu/AppHostLogic.js +39 -0
  26. package/lib/module/floatingMenu/DefaultConfigContext.js +39 -0
  27. package/lib/module/floatingMenu/DevToolsSettingsModal.js +2273 -0
  28. package/lib/module/floatingMenu/DevToolsVisibilityContext.js +44 -0
  29. package/lib/module/floatingMenu/DraggableHeader.js +110 -0
  30. package/lib/module/floatingMenu/FloatingDevTools.js +249 -0
  31. package/lib/module/floatingMenu/FloatingMenu.js +358 -0
  32. package/lib/module/floatingMenu/MinimizedToolsContext.js +239 -0
  33. package/lib/module/floatingMenu/MinimizedToolsStack.js +202 -0
  34. package/lib/module/floatingMenu/ToggleStateManager.js +32 -0
  35. package/lib/module/floatingMenu/autoDiscoverPresets.js +236 -0
  36. package/lib/module/floatingMenu/defaultConfig.js +151 -0
  37. package/lib/module/floatingMenu/dial/DialDevTools.js +829 -0
  38. package/lib/module/floatingMenu/dial/DialIcon.js +241 -0
  39. package/lib/module/floatingMenu/dial/OnboardingTooltip.js +244 -0
  40. package/lib/module/floatingMenu/dial/onboardingConstants.js +64 -0
  41. package/lib/module/floatingMenu/floatingTools.js +767 -0
  42. package/lib/module/floatingMenu/settingsBus.js +19 -0
  43. package/lib/module/floatingMenu/types.js +3 -0
  44. package/lib/module/index.js +29 -0
  45. package/lib/module/package.json +1 -0
  46. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts +39 -0
  47. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts +37 -0
  49. package/lib/typescript/commonjs/floatingMenu/AppHostLogic.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/floatingMenu/DefaultConfigContext.d.ts +27 -0
  51. package/lib/typescript/commonjs/floatingMenu/DefaultConfigContext.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts +57 -0
  53. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/floatingMenu/DevToolsVisibilityContext.d.ts +25 -0
  55. package/lib/typescript/commonjs/floatingMenu/DevToolsVisibilityContext.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/floatingMenu/DraggableHeader.d.ts +30 -0
  57. package/lib/typescript/commonjs/floatingMenu/DraggableHeader.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +226 -0
  59. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts +39 -0
  61. package/lib/typescript/commonjs/floatingMenu/FloatingMenu.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts +95 -0
  63. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -0
  64. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts +10 -0
  65. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts.map +1 -0
  66. package/lib/typescript/commonjs/floatingMenu/ToggleStateManager.d.ts +21 -0
  67. package/lib/typescript/commonjs/floatingMenu/ToggleStateManager.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts +75 -0
  69. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -0
  70. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +120 -0
  71. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +35 -0
  73. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts +14 -0
  75. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.d.ts +12 -0
  77. package/lib/typescript/commonjs/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts +30 -0
  79. package/lib/typescript/commonjs/floatingMenu/dial/onboardingConstants.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts +56 -0
  81. package/lib/typescript/commonjs/floatingMenu/floatingTools.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts +10 -0
  83. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/floatingMenu/types.d.ts +56 -0
  85. package/lib/typescript/commonjs/floatingMenu/types.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/index.d.ts +18 -0
  87. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/package.json +1 -0
  89. package/lib/typescript/module/floatingMenu/AppHost.d.ts +39 -0
  90. package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -0
  91. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts +37 -0
  92. package/lib/typescript/module/floatingMenu/AppHostLogic.d.ts.map +1 -0
  93. package/lib/typescript/module/floatingMenu/DefaultConfigContext.d.ts +27 -0
  94. package/lib/typescript/module/floatingMenu/DefaultConfigContext.d.ts.map +1 -0
  95. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts +57 -0
  96. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -0
  97. package/lib/typescript/module/floatingMenu/DevToolsVisibilityContext.d.ts +25 -0
  98. package/lib/typescript/module/floatingMenu/DevToolsVisibilityContext.d.ts.map +1 -0
  99. package/lib/typescript/module/floatingMenu/DraggableHeader.d.ts +30 -0
  100. package/lib/typescript/module/floatingMenu/DraggableHeader.d.ts.map +1 -0
  101. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +226 -0
  102. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -0
  103. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts +39 -0
  104. package/lib/typescript/module/floatingMenu/FloatingMenu.d.ts.map +1 -0
  105. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts +95 -0
  106. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -0
  107. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts +10 -0
  108. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts.map +1 -0
  109. package/lib/typescript/module/floatingMenu/ToggleStateManager.d.ts +21 -0
  110. package/lib/typescript/module/floatingMenu/ToggleStateManager.d.ts.map +1 -0
  111. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts +75 -0
  112. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -0
  113. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +120 -0
  114. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -0
  115. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +35 -0
  116. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -0
  117. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts +14 -0
  118. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -0
  119. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.d.ts +12 -0
  120. package/lib/typescript/module/floatingMenu/dial/OnboardingTooltip.d.ts.map +1 -0
  121. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts +30 -0
  122. package/lib/typescript/module/floatingMenu/dial/onboardingConstants.d.ts.map +1 -0
  123. package/lib/typescript/module/floatingMenu/floatingTools.d.ts +56 -0
  124. package/lib/typescript/module/floatingMenu/floatingTools.d.ts.map +1 -0
  125. package/lib/typescript/module/floatingMenu/settingsBus.d.ts +10 -0
  126. package/lib/typescript/module/floatingMenu/settingsBus.d.ts.map +1 -0
  127. package/lib/typescript/module/floatingMenu/types.d.ts +56 -0
  128. package/lib/typescript/module/floatingMenu/types.d.ts.map +1 -0
  129. package/lib/typescript/module/index.d.ts +18 -0
  130. package/lib/typescript/module/index.d.ts.map +1 -0
  131. package/lib/typescript/module/package.json +1 -0
  132. package/package.json +79 -0
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import { TouchableOpacity, StyleSheet, View, Dimensions } from "react-native";
5
+ import { FloatingTools, UserStatus } from "./floatingTools.js";
6
+ import { DialDevTools } from "./dial/DialDevTools.js";
7
+ import { EnvironmentIndicator, safeGetItem, safeSetItem, useHintsDisabled, devToolsStorageKeys, buoyColors } from "@buoy-gg/shared-ui";
8
+ import { useDevToolsSettings } from "./DevToolsSettingsModal.js";
9
+ import { useAppHost } from "./AppHost.js";
10
+ import { useDevToolsVisibility } from "./DevToolsVisibilityContext.js";
11
+ import { toggleStateManager } from "./ToggleStateManager.js";
12
+ import { OnboardingTooltip } from "./dial/OnboardingTooltip.js";
13
+
14
+ /**
15
+ * Props for the floating developer tools launcher. Controls which apps are shown and
16
+ * how the menu integrates with the current host environment.
17
+ */
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ /**
20
+ * FloatingMenu renders the persistent developer tools entry point. It handles visibility,
21
+ * integrates with the AppHost, and presents available tools as floating shortcuts and a dial.
22
+ */
23
+ const FLOATING_MENU_ONBOARDING_KEY = "@react_buoy_floating_menu_tooltip_shown";
24
+ const ONBOARDING_STEP_KEY = "@react_buoy_onboarding_step";
25
+ const {
26
+ width: SCREEN_WIDTH,
27
+ height: SCREEN_HEIGHT
28
+ } = Dimensions.get("window");
29
+ export const FloatingMenu = ({
30
+ apps,
31
+ state,
32
+ actions,
33
+ hidden,
34
+ environment,
35
+ userRole,
36
+ availableEnvironments,
37
+ onEnvironmentSwitch
38
+ }) => {
39
+ const [internalHidden, setInternalHidden] = useState(false);
40
+ const [showDial, setShowDial] = useState(false);
41
+ const [dialStateLoaded, setDialStateLoaded] = useState(false);
42
+ const [onboardingStep, setOnboardingStep] = useState(null);
43
+ const [, forceUpdate] = useState(0); // Used to force re-render when toggle states change
44
+ const onboardingDismissedRef = useRef(false); // Track if onboarding was dismissed
45
+ const hintsDisabled = useHintsDisabled();
46
+
47
+ // Load persisted dial state on mount
48
+ useEffect(() => {
49
+ const loadDialState = async () => {
50
+ try {
51
+ const savedDialOpen = await safeGetItem(devToolsStorageKeys.dial.isOpen());
52
+ if (savedDialOpen === "true") {
53
+ setShowDial(true);
54
+ }
55
+ } catch (error) {
56
+ // Failed to load dial state - use default (closed)
57
+ } finally {
58
+ setDialStateLoaded(true);
59
+ }
60
+ };
61
+ loadDialState();
62
+ }, []);
63
+
64
+ // Persist dial state when it changes
65
+ useEffect(() => {
66
+ // Only persist after initial state is loaded to avoid overwriting with default
67
+ if (!dialStateLoaded) return;
68
+ safeSetItem(devToolsStorageKeys.dial.isOpen(), showDial ? "true" : "false").catch(error => {
69
+ // Failed to save dial state - continue without persistence
70
+ console.warn("Failed to save dial state:", error);
71
+ });
72
+ }, [showDial, dialStateLoaded]);
73
+
74
+ // Determine if environment selector should be shown
75
+ const showEnvironmentSelector = Boolean(availableEnvironments?.length && onEnvironmentSwitch);
76
+ const {
77
+ isAnyOpen,
78
+ open,
79
+ registerApps
80
+ } = useAppHost();
81
+ const wasAppOpenRef = useRef(isAnyOpen);
82
+ const {
83
+ setDialOpen,
84
+ setToolOpen
85
+ } = useDevToolsVisibility();
86
+
87
+ // Check onboarding status on first load
88
+ useEffect(() => {
89
+ // Skip onboarding if hints are disabled
90
+ if (hintsDisabled) {
91
+ return;
92
+ }
93
+ const checkOnboarding = async () => {
94
+ try {
95
+ const hasSeenOnboarding = await safeGetItem(FLOATING_MENU_ONBOARDING_KEY);
96
+ if (!hasSeenOnboarding) {
97
+ // Small delay to let the UI settle before showing tooltip
98
+ setTimeout(() => {
99
+ setOnboardingStep("positioning");
100
+ }, 1000);
101
+ }
102
+ } catch (error) {
103
+ // If there's an error reading storage, don't show onboarding
104
+ }
105
+ };
106
+ checkOnboarding();
107
+ }, [hintsDisabled]);
108
+
109
+ // Subscribe to toggle state changes to update icon colors
110
+ useEffect(() => {
111
+ const unsubscribe = toggleStateManager.subscribe(() => {
112
+ forceUpdate(prev => prev + 1);
113
+ });
114
+ return unsubscribe;
115
+ }, []);
116
+
117
+ // Sync dial state with visibility context
118
+ useEffect(() => {
119
+ setDialOpen(showDial);
120
+ }, [showDial, setDialOpen]);
121
+
122
+ // Sync tool open state with visibility context
123
+ useEffect(() => {
124
+ setToolOpen(isAnyOpen);
125
+ }, [isAnyOpen, setToolOpen]);
126
+
127
+ // When an app is open or dial is shown, push the menu to the side instead of hiding completely
128
+ const shouldPushToSide = useMemo(() => Boolean(showDial || isAnyOpen), [showDial, isAnyOpen]);
129
+
130
+ // Use external hidden prop or internal hidden state for complete hiding
131
+ const isCompletelyHidden = useMemo(() => Boolean(hidden ?? internalHidden), [hidden, internalHidden]);
132
+ const {
133
+ settings: devToolsSettings
134
+ } = useDevToolsSettings();
135
+
136
+ // Register apps with AppHost for persistence
137
+ useEffect(() => {
138
+ if (registerApps) {
139
+ registerApps(apps);
140
+ }
141
+ }, [apps, registerApps]);
142
+ const mergedActions = useMemo(() => {
143
+ return {
144
+ ...(actions ?? {}),
145
+ closeMenu: () => setShowDial(false),
146
+ hideFloatingRow: () => setInternalHidden(true),
147
+ showFloatingRow: () => setInternalHidden(false),
148
+ notifyToggleChange: () => forceUpdate(prev => prev + 1)
149
+ };
150
+ }, [actions]);
151
+ useEffect(() => {
152
+ if (wasAppOpenRef.current && !isAnyOpen) {
153
+ setInternalHidden(false);
154
+ setShowDial(false);
155
+ }
156
+ wasAppOpenRef.current = isAnyOpen;
157
+ }, [isAnyOpen]);
158
+
159
+ // Filter function for floating tools based on settings
160
+ const isFloatingEnabled = id => {
161
+ if (!devToolsSettings) return false;
162
+ // Default to disabled for tools without explicit preferences
163
+ return devToolsSettings.floatingTools[id] ?? false;
164
+ };
165
+
166
+ // Dial is the default/only layout
167
+
168
+ const handlePress = app => {
169
+ // Call the app's onPress callback if provided, passing actions for toggle tools
170
+ app?.onPress?.(mergedActions);
171
+
172
+ // Only open modal if not a toggle-only tool
173
+ if (app.launchMode !== "toggle-only") {
174
+ // Resolve the icon for minimize stack display
175
+ // IMPORTANT: Use React.createElement for function components to preserve hooks
176
+ const resolvedIcon = typeof app.icon === "function" ? /*#__PURE__*/React.createElement(app.icon, {
177
+ slot: "dial",
178
+ size: 20
179
+ }) : app.icon;
180
+ open({
181
+ id: app.id,
182
+ title: app.name,
183
+ component: app.component,
184
+ props: app.props,
185
+ launchMode: app.launchMode ?? "self-modal",
186
+ singleton: app.singleton ?? true,
187
+ icon: resolvedIcon,
188
+ color: app.color
189
+ });
190
+ }
191
+ };
192
+ const handleOnboardingDismiss = () => {
193
+ // Mark as dismissed immediately in ref (synchronous, no re-render needed)
194
+ onboardingDismissedRef.current = true;
195
+
196
+ // Update state to hide tooltip
197
+ setOnboardingStep(null);
198
+
199
+ // Save to storage asynchronously in the background
200
+ safeSetItem(FLOATING_MENU_ONBOARDING_KEY, "true").catch(error => {
201
+ // Silently fail - user already saw onboarding, just won't persist
202
+ console.warn("Failed to save onboarding state:", error);
203
+ });
204
+ };
205
+ const handleDialOpen = () => {
206
+ // If user opens dial during onboarding, mark onboarding as complete
207
+ if (isOnboarding) {
208
+ handleOnboardingDismiss();
209
+ }
210
+ setShowDial(true);
211
+ };
212
+
213
+ // Determine if we're in onboarding mode (only when explicitly set to positioning AND not dismissed AND hints not disabled)
214
+ const isOnboarding = onboardingStep === "positioning" && !onboardingDismissedRef.current && !hintsDisabled;
215
+
216
+ // During onboarding, disable position persistence and use centered position
217
+ const shouldEnablePositionPersistence = !isOnboarding;
218
+ return /*#__PURE__*/_jsxs(_Fragment, {
219
+ children: [isOnboarding && !showDial && !isAnyOpen && /*#__PURE__*/_jsxs(View, {
220
+ style: styles.onboardingContainer,
221
+ children: [/*#__PURE__*/_jsx(View, {
222
+ style: styles.onboardingBackdrop
223
+ }), /*#__PURE__*/_jsx(OnboardingTooltip, {
224
+ visible: true,
225
+ onDismiss: handleOnboardingDismiss,
226
+ title: "Welcome to Buoy Dev Tools!",
227
+ message: "Grab and position this menu wherever you want, then tap the icon to open your dev tools."
228
+ })]
229
+ }), /*#__PURE__*/_jsx(View, {
230
+ nativeID: "floating-devtools-root",
231
+ pointerEvents: isCompletelyHidden ? "none" : "box-none",
232
+ style: {
233
+ position: "absolute",
234
+ top: 0,
235
+ left: 0,
236
+ right: 0,
237
+ bottom: 0,
238
+ zIndex: isOnboarding ? 10001 : 9999,
239
+ // Higher z-index during onboarding to show above backdrop
240
+ opacity: isCompletelyHidden ? 0 : 1
241
+ },
242
+ children: /*#__PURE__*/_jsxs(FloatingTools, {
243
+ enablePositionPersistence: shouldEnablePositionPersistence,
244
+ pushToSide: shouldPushToSide,
245
+ centerOnboarding: isOnboarding,
246
+ environment: environment,
247
+ availableEnvironments: availableEnvironments,
248
+ onEnvironmentSwitch: onEnvironmentSwitch,
249
+ showEnvironmentSelector: showEnvironmentSelector && devToolsSettings?.floatingTools?.environment,
250
+ children: [devToolsSettings?.floatingTools?.environment && environment && !showEnvironmentSelector ? /*#__PURE__*/_jsx(EnvironmentIndicator, {
251
+ environment: environment
252
+ }) : null, userRole ? /*#__PURE__*/_jsx(UserStatus, {
253
+ userRole: userRole,
254
+ onPress: handleDialOpen
255
+ }) :
256
+ /*#__PURE__*/
257
+ // Fallback: small launcher icon to ensure settings are always accessible
258
+ _jsx(TouchableOpacity, {
259
+ accessibilityLabel: "Open Dev Tools Menu",
260
+ onPress: handleDialOpen,
261
+ style: styles.fab,
262
+ children: /*#__PURE__*/_jsx(View, {
263
+ style: styles.menuButton,
264
+ children: /*#__PURE__*/_jsx(MenuLauncherIcon, {
265
+ size: 14
266
+ })
267
+ })
268
+ }), apps.filter(a => (a.slot ?? "both") !== "dial" && isFloatingEnabled(a.id)).map(app => /*#__PURE__*/_jsx(TouchableOpacity, {
269
+ accessibilityLabel: app.name,
270
+ onPress: () => handlePress(app),
271
+ style: styles.fab,
272
+ children: (() => {
273
+ if (typeof app.icon === "function") {
274
+ const IconComponent = app.icon;
275
+ return /*#__PURE__*/_jsx(IconComponent, {
276
+ slot: "row",
277
+ size: 16,
278
+ state: state,
279
+ actions: mergedActions
280
+ });
281
+ }
282
+ return app.icon;
283
+ })()
284
+ }, `row-${app.id}`))]
285
+ })
286
+ }), showDial && /*#__PURE__*/_jsx(DialDevTools, {
287
+ apps: apps,
288
+ state: state,
289
+ actions: mergedActions,
290
+ onClose: () => {
291
+ setShowDial(false);
292
+ }
293
+ })]
294
+ });
295
+ };
296
+ const styles = StyleSheet.create({
297
+ fab: {
298
+ paddingHorizontal: 6,
299
+ paddingVertical: 4,
300
+ borderRadius: 6,
301
+ marginRight: 4,
302
+ alignItems: "center",
303
+ justifyContent: "center",
304
+ minWidth: 0,
305
+ minHeight: 0,
306
+ backgroundColor: "transparent"
307
+ },
308
+ menuButton: {
309
+ paddingHorizontal: 4,
310
+ paddingVertical: 2,
311
+ minWidth: 16,
312
+ alignItems: "center",
313
+ justifyContent: "center"
314
+ },
315
+ menuDots: {
316
+ color: "#8CA2C8",
317
+ fontSize: 14,
318
+ fontWeight: "900"
319
+ },
320
+ onboardingContainer: {
321
+ ...StyleSheet.absoluteFillObject,
322
+ zIndex: 10000
323
+ },
324
+ onboardingBackdrop: {
325
+ ...StyleSheet.absoluteFillObject,
326
+ backgroundColor: "rgba(0, 0, 0, 0.85)"
327
+ }
328
+ });
329
+ /** Minimal 3x3 dot icon used when the user status badge is unavailable. */
330
+ const MenuLauncherIcon = ({
331
+ size = 14,
332
+ color = buoyColors.primary
333
+ }) => {
334
+ const dotSize = Math.max(2, Math.floor(size / 4));
335
+ const gap = Math.max(1, Math.floor(size / 16));
336
+ const items = Array.from({
337
+ length: 9
338
+ });
339
+ return /*#__PURE__*/_jsx(View, {
340
+ style: {
341
+ width: size,
342
+ height: size,
343
+ flexDirection: "row",
344
+ flexWrap: "wrap",
345
+ alignContent: "center",
346
+ justifyContent: "center"
347
+ },
348
+ children: items.map((_, i) => /*#__PURE__*/_jsx(View, {
349
+ style: {
350
+ width: dotSize,
351
+ height: dotSize,
352
+ margin: gap,
353
+ borderRadius: 2,
354
+ backgroundColor: color
355
+ }
356
+ }, i))
357
+ });
358
+ };
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+
3
+ import React, { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo } from "react";
4
+ import { Dimensions } from "react-native";
5
+ import { safeGetItem, safeSetItem, getSafeAreaInsets } from "@buoy-gg/shared-ui";
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Modal state to restore when reopening a minimized tool
13
+ */
14
+
15
+ /**
16
+ * Represents a minimized tool that can be restored
17
+ */
18
+
19
+ /**
20
+ * Position for a minimized tool icon in the stack
21
+ */
22
+ import { jsx as _jsx } from "react/jsx-runtime";
23
+ // ============================================================================
24
+ // Constants
25
+ // ============================================================================
26
+
27
+ const ICON_SIZE = 44;
28
+ const ICON_GAP = 12;
29
+ const BOTTOM_OFFSET = 100; // Distance from safe area bottom
30
+ const RIGHT_OFFSET = 16; // Distance from right edge
31
+
32
+ /**
33
+ * Calculate the position of a minimized tool icon in the stack
34
+ * Icons stack upward from bottom-right corner
35
+ */
36
+ export function getIconPosition(index) {
37
+ const {
38
+ width: screenWidth,
39
+ height: screenHeight
40
+ } = Dimensions.get("window");
41
+ const safeArea = getSafeAreaInsets();
42
+
43
+ // X position: right edge minus icon size and offset
44
+ const x = screenWidth - ICON_SIZE - RIGHT_OFFSET;
45
+
46
+ // Y position: bottom up, starting at safe area bottom + offset
47
+ const baseY = screenHeight - safeArea.bottom - BOTTOM_OFFSET - ICON_SIZE;
48
+ const y = baseY - index * (ICON_SIZE + ICON_GAP);
49
+ return {
50
+ x,
51
+ y
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Get the icon size constant
57
+ */
58
+ export function getIconSize() {
59
+ return ICON_SIZE;
60
+ }
61
+
62
+ /**
63
+ * Context value for minimized tools management
64
+ */
65
+
66
+ // ============================================================================
67
+ // Storage
68
+ // ============================================================================
69
+
70
+ const STORAGE_KEY = "@react_buoy_minimized_tools";
71
+ const PERSISTENCE_DELAY = 500;
72
+
73
+ /**
74
+ * Serializable version of MinimizedTool for storage
75
+ * (icon is not serializable, so we only store the id to reconstruct)
76
+ */
77
+
78
+ // ============================================================================
79
+ // Context
80
+ // ============================================================================
81
+
82
+ const MinimizedToolsContext = /*#__PURE__*/createContext(null);
83
+
84
+ // ============================================================================
85
+ // Provider
86
+ // ============================================================================
87
+
88
+ /**
89
+ * Provider component for minimized tools management.
90
+ * Handles state, persistence, and provides context to children.
91
+ */
92
+ export function MinimizedToolsProvider({
93
+ children,
94
+ getToolIcon
95
+ }) {
96
+ const [minimizedTools, setMinimizedTools] = useState([]);
97
+ const [isRestored, setIsRestored] = useState(false);
98
+ const persistenceTimeoutRef = useRef(null);
99
+ const getToolIconRef = useRef(getToolIcon);
100
+
101
+ // Keep ref updated
102
+ useEffect(() => {
103
+ getToolIconRef.current = getToolIcon;
104
+ }, [getToolIcon]);
105
+
106
+ // Restore minimized tools from storage on mount
107
+ useEffect(() => {
108
+ const restoreMinimizedTools = async () => {
109
+ try {
110
+ const saved = await safeGetItem(STORAGE_KEY);
111
+ if (saved) {
112
+ const serialized = JSON.parse(saved);
113
+ // Reconstruct tools with icons
114
+ const restored = serialized.map(tool => ({
115
+ ...tool,
116
+ icon: getToolIconRef.current?.(tool.id) ?? null
117
+ }));
118
+ setMinimizedTools(restored);
119
+ }
120
+ } catch (error) {
121
+ // Failed to restore minimized tools - continue with empty state
122
+ }
123
+ setIsRestored(true);
124
+ };
125
+ restoreMinimizedTools();
126
+ }, []);
127
+
128
+ // Persist minimized tools to storage with debounce
129
+ useEffect(() => {
130
+ if (!isRestored) return;
131
+ if (persistenceTimeoutRef.current) {
132
+ clearTimeout(persistenceTimeoutRef.current);
133
+ }
134
+ persistenceTimeoutRef.current = setTimeout(() => {
135
+ // Serialize tools (exclude icon which is not serializable)
136
+ const serialized = minimizedTools.map(({
137
+ icon,
138
+ ...rest
139
+ }) => rest);
140
+ safeSetItem(STORAGE_KEY, JSON.stringify(serialized));
141
+ }, PERSISTENCE_DELAY);
142
+ return () => {
143
+ if (persistenceTimeoutRef.current) {
144
+ clearTimeout(persistenceTimeoutRef.current);
145
+ }
146
+ };
147
+ }, [minimizedTools, isRestored]);
148
+ const minimize = useCallback(tool => {
149
+ setMinimizedTools(current => {
150
+ // Check if already minimized (by id for singleton behavior)
151
+ const existing = current.find(t => t.id === tool.id);
152
+ if (existing) {
153
+ // Update existing minimized tool
154
+ return current.map(t => t.id === tool.id ? {
155
+ ...tool,
156
+ minimizedAt: Date.now()
157
+ } : t);
158
+ }
159
+ // Add new minimized tool
160
+ return [...current, {
161
+ ...tool,
162
+ minimizedAt: Date.now()
163
+ }];
164
+ });
165
+ }, []);
166
+ const restore = useCallback(instanceId => {
167
+ let restoredTool = null;
168
+ setMinimizedTools(current => {
169
+ const index = current.findIndex(t => t.instanceId === instanceId);
170
+ if (index === -1) return current;
171
+ restoredTool = current[index];
172
+ return current.filter(t => t.instanceId !== instanceId);
173
+ });
174
+ return restoredTool;
175
+ }, []);
176
+ const isMinimized = useCallback(id => {
177
+ return minimizedTools.some(t => t.id === id);
178
+ }, [minimizedTools]);
179
+ const getMinimizedTool = useCallback(instanceId => {
180
+ return minimizedTools.find(t => t.instanceId === instanceId);
181
+ }, [minimizedTools]);
182
+ const clearAll = useCallback(() => {
183
+ setMinimizedTools([]);
184
+ }, []);
185
+ const getNextIconPosition = useCallback(() => {
186
+ // Next icon will be at the current count index
187
+ return getIconPosition(minimizedTools.length);
188
+ }, [minimizedTools.length]);
189
+ const getToolIconPosition = useCallback(instanceId => {
190
+ const index = minimizedTools.findIndex(t => t.instanceId === instanceId);
191
+ if (index === -1) return null;
192
+ return getIconPosition(index);
193
+ }, [minimizedTools]);
194
+ const value = useMemo(() => ({
195
+ minimizedTools,
196
+ minimize,
197
+ restore,
198
+ isMinimized,
199
+ getMinimizedTool,
200
+ clearAll,
201
+ count: minimizedTools.length,
202
+ getNextIconPosition,
203
+ getToolIconPosition
204
+ }), [minimizedTools, minimize, restore, isMinimized, getMinimizedTool, clearAll, getNextIconPosition, getToolIconPosition]);
205
+ return /*#__PURE__*/_jsx(MinimizedToolsContext.Provider, {
206
+ value: value,
207
+ children: children
208
+ });
209
+ }
210
+
211
+ // ============================================================================
212
+ // Hook
213
+ // ============================================================================
214
+
215
+ /**
216
+ * Hook to access minimized tools context.
217
+ * Must be used within a MinimizedToolsProvider.
218
+ */
219
+ export function useMinimizedTools() {
220
+ const context = useContext(MinimizedToolsContext);
221
+ if (!context) {
222
+ // Return a no-op implementation when outside provider
223
+ return {
224
+ minimizedTools: [],
225
+ minimize: () => {},
226
+ restore: () => null,
227
+ isMinimized: () => false,
228
+ getMinimizedTool: () => undefined,
229
+ clearAll: () => {},
230
+ count: 0,
231
+ getNextIconPosition: () => ({
232
+ x: 0,
233
+ y: 0
234
+ }),
235
+ getToolIconPosition: () => null
236
+ };
237
+ }
238
+ return context;
239
+ }