@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,829 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { Pressable, StyleSheet, View, Dimensions, Text, Animated, Easing } from "react-native";
5
+ // Icons are provided by installedApps; no direct icon imports here.
6
+ import { DialIcon } from "./DialIcon.js";
7
+ import { safeGetItem, safeSetItem, useHintsDisabled, devToolsStorageKeys, buoyColors } from "@buoy-gg/shared-ui";
8
+ import { DevToolsSettingsModal, useDevToolsSettings } from "../DevToolsSettingsModal.js";
9
+ import { useIsPro } from "@buoy-gg/license";
10
+ import { useAppHost } from "../AppHost.js";
11
+ import { OnboardingTooltip } from "./OnboardingTooltip.js";
12
+ import { getDialLayout, MAX_DIAL_SLOTS, dialAnimationConfig, dialColors } from "@buoy-gg/floating-tools-core";
13
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
+ const {
15
+ width: SCREEN_WIDTH
16
+ } = Dimensions.get("window");
17
+
18
+ // Use shared layout calculation from core
19
+ const layout = getDialLayout({
20
+ screenWidth: SCREEN_WIDTH
21
+ });
22
+ const CIRCLE_SIZE = layout.circleSize;
23
+ const BUTTON_SIZE = layout.buttonSize;
24
+ const ONBOARDING_STORAGE_KEY = "@react_buoy_settings_tooltip_shown";
25
+ export const DialDevTools = ({
26
+ onClose,
27
+ onSettingsPress,
28
+ settings: externalSettings,
29
+ autoOpenSettings = false,
30
+ apps,
31
+ state,
32
+ actions
33
+ }) => {
34
+ const [selectedIcon, setSelectedIcon] = useState(-1);
35
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
36
+ const [settingsModalStateLoaded, setSettingsModalStateLoaded] = useState(false);
37
+ const [showOnboardingTooltip, setShowOnboardingTooltip] = useState(false);
38
+ const onboardingDismissedRef = useRef(false); // Track if onboarding was dismissed
39
+ const hintsDisabled = useHintsDisabled();
40
+ const {
41
+ settings: hookSettings,
42
+ refreshSettings
43
+ } = useDevToolsSettings();
44
+ const {
45
+ open
46
+ } = useAppHost();
47
+ const isPro = useIsPro();
48
+ // Initialize with external settings if provided, otherwise use hook settings
49
+ const [localSettings, setLocalSettings] = useState(externalSettings || hookSettings);
50
+
51
+ // Always use localSettings (which can be updated by the modal)
52
+ const settings = localSettings;
53
+
54
+ // Load persisted settings modal state on mount
55
+ useEffect(() => {
56
+ const loadSettingsModalState = async () => {
57
+ try {
58
+ const savedModalOpen = await safeGetItem(devToolsStorageKeys.settings.modalOpen());
59
+ if (savedModalOpen === "true") {
60
+ setIsSettingsModalOpen(true);
61
+ }
62
+ } catch (error) {
63
+ // Failed to load settings modal state - use default (closed)
64
+ } finally {
65
+ setSettingsModalStateLoaded(true);
66
+ }
67
+ };
68
+ loadSettingsModalState();
69
+ }, []);
70
+
71
+ // Persist settings modal state when it changes
72
+ useEffect(() => {
73
+ // Only persist after initial state is loaded to avoid overwriting with default
74
+ if (!settingsModalStateLoaded) return;
75
+ safeSetItem(devToolsStorageKeys.settings.modalOpen(), isSettingsModalOpen ? "true" : "false").catch(error => {
76
+ // Failed to save settings modal state - continue without persistence
77
+ console.warn("Failed to save settings modal state:", error);
78
+ });
79
+ }, [isSettingsModalOpen, settingsModalStateLoaded]);
80
+
81
+ // Update local settings when external settings change
82
+ useEffect(() => {
83
+ if (externalSettings) {
84
+ setLocalSettings(externalSettings);
85
+ }
86
+ }, [externalSettings]);
87
+
88
+ // Update local settings when hook settings change (if no external settings)
89
+ useEffect(() => {
90
+ if (!externalSettings) {
91
+ setLocalSettings(hookSettings);
92
+ }
93
+ }, [hookSettings, externalSettings]);
94
+
95
+ // Auto-open settings modal when prop is true
96
+ useEffect(() => {
97
+ if (autoOpenSettings && !isSettingsModalOpen) {
98
+ setIsSettingsModalOpen(true);
99
+ }
100
+ }, [autoOpenSettings, isSettingsModalOpen]);
101
+
102
+ // Check if we should show the onboarding tooltip
103
+ useEffect(() => {
104
+ // Skip onboarding if hints are disabled
105
+ if (hintsDisabled) {
106
+ return;
107
+ }
108
+ const checkOnboarding = async () => {
109
+ try {
110
+ const hasSeenTooltip = await safeGetItem(ONBOARDING_STORAGE_KEY);
111
+ if (!hasSeenTooltip) {
112
+ // Small delay to let the entrance animations play first
113
+ setTimeout(() => {
114
+ setShowOnboardingTooltip(true);
115
+ }, 1200);
116
+ }
117
+ } catch (error) {
118
+ // If there's an error reading storage, don't show the tooltip
119
+ // to avoid annoying the user repeatedly
120
+ }
121
+ };
122
+ checkOnboarding();
123
+ }, [hintsDisabled]);
124
+
125
+ // React Native Animated values
126
+ const backdropOpacity = useRef(new Animated.Value(0)).current;
127
+ const dialScale = useRef(new Animated.Value(0)).current;
128
+ const dialRotation = useRef(new Animated.Value(0)).current;
129
+ const centerButtonScale = useRef(new Animated.Value(0)).current;
130
+ const iconsProgress = useRef(new Animated.Value(0)).current;
131
+ const glitchOffset = useRef(new Animated.Value(0)).current;
132
+ const pulseScale = useRef(new Animated.Value(1)).current;
133
+ const availableApps = useMemo(() => apps.map(({
134
+ id,
135
+ name,
136
+ slot,
137
+ description
138
+ }) => ({
139
+ id,
140
+ name,
141
+ slot,
142
+ description
143
+ })), [apps]);
144
+
145
+ // Subtle animations
146
+ const floatingAnim = useRef(new Animated.Value(0)).current;
147
+ const breathingScale = useRef(new Animated.Value(1)).current;
148
+ const circuitOpacity = useRef(new Animated.Value(0)).current;
149
+
150
+ // Animation tracking refs
151
+ const glitchIntervalRef = useRef(null);
152
+ const pulseAnimationRef = useRef(null);
153
+
154
+ // Map data-driven apps to dial icons, inserting empty slots for disabled items
155
+ const dialApps = apps.filter(a => (a.slot ?? "both") !== "row");
156
+ const isDialEnabled = id => {
157
+ if (!settings) return false;
158
+ return settings.dialTools[id] ?? false;
159
+ };
160
+ const createEmptySlot = slotIndex => ({
161
+ id: `empty-${slotIndex}`,
162
+ name: `empty-${slotIndex}`,
163
+ icon: null,
164
+ color: "transparent",
165
+ onPress: () => {}
166
+ });
167
+ const enabledIcons = [];
168
+ for (const app of dialApps) {
169
+ if (!isDialEnabled(app.id)) {
170
+ continue;
171
+ }
172
+ if (enabledIcons.length >= MAX_DIAL_SLOTS) {
173
+ break;
174
+ }
175
+ enabledIcons.push({
176
+ id: app.id,
177
+ name: app.name,
178
+ // Pass both the pre-rendered icon (for non-function icons) and the component (for dynamic rendering)
179
+ icon: typeof app.icon === "function" ? null // Will be rendered dynamically by DialIcon
180
+ : app.icon,
181
+ // Cast to the expected type - the function signature is compatible at runtime
182
+ iconComponent: typeof app.icon === "function" ? app.icon : undefined,
183
+ color: app.color ?? buoyColors.primary,
184
+ onPress: () => {
185
+ // Call the app's onPress callback if provided, passing actions for toggle tools
186
+ app?.onPress?.(actions);
187
+
188
+ // Only open modal if not a toggle-only tool
189
+ if (app.launchMode !== "toggle-only") {
190
+ const resolvedIcon = typeof app.icon === "function" ? app.icon({
191
+ slot: "dial",
192
+ size: 20
193
+ }) : app.icon;
194
+ open({
195
+ id: app.id,
196
+ title: app.name,
197
+ component: app.component,
198
+ props: app.props,
199
+ launchMode: app.launchMode ?? "self-modal",
200
+ singleton: app.singleton ?? true,
201
+ icon: resolvedIcon,
202
+ color: app.color
203
+ });
204
+ }
205
+
206
+ // Close the dial
207
+ onClose?.();
208
+ }
209
+ });
210
+ }
211
+ if (__DEV__) {
212
+ const totalEnabled = dialApps.filter(app => isDialEnabled(app.id)).length;
213
+ if (totalEnabled > MAX_DIAL_SLOTS) {
214
+ // More tools enabled than can be shown - they will be hidden
215
+ }
216
+ }
217
+ const icons = [...enabledIcons];
218
+ while (icons.length < MAX_DIAL_SLOTS) {
219
+ icons.push(createEmptySlot(icons.length));
220
+ }
221
+
222
+ // Initialize animations on mount - using shared config from core
223
+ useEffect(() => {
224
+ // Fallback config in case dialAnimationConfig hasn't loaded
225
+ const config = dialAnimationConfig ?? {
226
+ entrance: {
227
+ backdrop: {
228
+ duration: 400
229
+ },
230
+ dial: {
231
+ scale: {
232
+ damping: 15,
233
+ stiffness: 150,
234
+ mass: 1
235
+ },
236
+ rotation: {
237
+ duration: 800
238
+ }
239
+ },
240
+ centerButton: {
241
+ delay: 300,
242
+ damping: 10,
243
+ stiffness: 200
244
+ },
245
+ icons: {
246
+ delay: 500,
247
+ duration: 600
248
+ },
249
+ circuitTraces: {
250
+ delay: 600,
251
+ duration: 1000
252
+ }
253
+ },
254
+ continuous: {
255
+ glitch: {
256
+ interval: 3000,
257
+ offset: 2,
258
+ stepDuration: 50
259
+ },
260
+ pulse: {
261
+ maxScale: 1.02,
262
+ minScale: 0.98,
263
+ duration: 1000
264
+ },
265
+ floating: {
266
+ maxY: -8,
267
+ minY: 0,
268
+ duration: 3000
269
+ },
270
+ breathing: {
271
+ maxScale: 1.05,
272
+ minScale: 0.98,
273
+ duration: 2500
274
+ }
275
+ }
276
+ };
277
+ const {
278
+ entrance,
279
+ continuous
280
+ } = config;
281
+
282
+ // Entrance animation sequence
283
+ Animated.timing(backdropOpacity, {
284
+ toValue: 1,
285
+ duration: entrance.backdrop.duration,
286
+ useNativeDriver: true
287
+ }).start();
288
+ Animated.spring(dialScale, {
289
+ toValue: 1,
290
+ damping: entrance.dial.scale.damping,
291
+ stiffness: entrance.dial.scale.stiffness,
292
+ mass: entrance.dial.scale.mass,
293
+ useNativeDriver: true
294
+ }).start();
295
+ Animated.sequence([Animated.timing(dialRotation, {
296
+ toValue: 1,
297
+ duration: entrance.dial.rotation.duration,
298
+ easing: Easing.out(Easing.cubic),
299
+ useNativeDriver: true
300
+ }), Animated.timing(dialRotation, {
301
+ toValue: 0,
302
+ duration: 0,
303
+ useNativeDriver: true
304
+ })]).start();
305
+ Animated.sequence([Animated.delay(entrance.centerButton.delay), Animated.spring(centerButtonScale, {
306
+ toValue: 1,
307
+ damping: entrance.centerButton.damping,
308
+ stiffness: entrance.centerButton.stiffness,
309
+ useNativeDriver: true
310
+ })]).start();
311
+ Animated.sequence([Animated.delay(entrance.icons.delay), Animated.timing(iconsProgress, {
312
+ toValue: 1,
313
+ duration: entrance.icons.duration,
314
+ easing: Easing.out(Easing.cubic),
315
+ useNativeDriver: true
316
+ })]).start();
317
+
318
+ // Subtle glitch effect - using shared config
319
+ const glitchAnimation = () => {
320
+ Animated.sequence([Animated.timing(glitchOffset, {
321
+ toValue: continuous.glitch.offset,
322
+ duration: continuous.glitch.stepDuration,
323
+ useNativeDriver: true
324
+ }), Animated.timing(glitchOffset, {
325
+ toValue: -continuous.glitch.offset,
326
+ duration: continuous.glitch.stepDuration,
327
+ useNativeDriver: true
328
+ }), Animated.timing(glitchOffset, {
329
+ toValue: 0,
330
+ duration: continuous.glitch.stepDuration,
331
+ useNativeDriver: true
332
+ })]).start();
333
+ };
334
+ glitchIntervalRef.current = setInterval(glitchAnimation, continuous.glitch.interval);
335
+
336
+ // Pulse animation - using shared config
337
+ const startPulse = () => {
338
+ pulseAnimationRef.current = Animated.loop(Animated.sequence([Animated.timing(pulseScale, {
339
+ toValue: continuous.pulse.maxScale,
340
+ duration: continuous.pulse.duration,
341
+ easing: Easing.inOut(Easing.ease),
342
+ useNativeDriver: true
343
+ }), Animated.timing(pulseScale, {
344
+ toValue: continuous.pulse.minScale,
345
+ duration: continuous.pulse.duration,
346
+ easing: Easing.inOut(Easing.ease),
347
+ useNativeDriver: true
348
+ })]));
349
+ pulseAnimationRef.current.start();
350
+ };
351
+ startPulse();
352
+
353
+ // Subtle floating animation for the dial - using shared config
354
+ Animated.loop(Animated.sequence([Animated.timing(floatingAnim, {
355
+ toValue: continuous.floating.maxY,
356
+ duration: continuous.floating.duration,
357
+ easing: Easing.inOut(Easing.ease),
358
+ useNativeDriver: true
359
+ }), Animated.timing(floatingAnim, {
360
+ toValue: continuous.floating.minY,
361
+ duration: continuous.floating.duration,
362
+ easing: Easing.inOut(Easing.ease),
363
+ useNativeDriver: true
364
+ })])).start();
365
+
366
+ // Gentle breathing effect for center button - using shared config
367
+ Animated.loop(Animated.sequence([Animated.timing(breathingScale, {
368
+ toValue: continuous.breathing.maxScale,
369
+ duration: continuous.breathing.duration,
370
+ easing: Easing.inOut(Easing.ease),
371
+ useNativeDriver: true
372
+ }), Animated.timing(breathingScale, {
373
+ toValue: continuous.breathing.minScale,
374
+ duration: continuous.breathing.duration,
375
+ easing: Easing.inOut(Easing.ease),
376
+ useNativeDriver: true
377
+ })])).start();
378
+
379
+ // Circuit traces fade in - using shared config
380
+ Animated.timing(circuitOpacity, {
381
+ toValue: 1,
382
+ duration: entrance.circuitTraces.duration,
383
+ delay: entrance.circuitTraces.delay,
384
+ useNativeDriver: true
385
+ }).start();
386
+ return () => {
387
+ if (glitchIntervalRef.current) {
388
+ clearInterval(glitchIntervalRef.current);
389
+ }
390
+ if (pulseAnimationRef.current) {
391
+ pulseAnimationRef.current.stop();
392
+ }
393
+ };
394
+ }, []);
395
+ const handleOnboardingDismiss = () => {
396
+ // Mark as dismissed immediately in ref (synchronous, no re-render needed)
397
+ onboardingDismissedRef.current = true;
398
+
399
+ // Hide the tooltip
400
+ setShowOnboardingTooltip(false);
401
+
402
+ // Save to storage asynchronously in the background
403
+ safeSetItem(ONBOARDING_STORAGE_KEY, "true").catch(error => {
404
+ // Silently fail - user already saw onboarding, just won't persist
405
+ console.warn("Failed to save dial onboarding state:", error);
406
+ });
407
+ };
408
+ const handleClose = () => {
409
+ // Stop any ongoing animations first
410
+ if (pulseAnimationRef.current) {
411
+ pulseAnimationRef.current.stop();
412
+ }
413
+ const exit = dialAnimationConfig?.exit ?? {
414
+ icons: {
415
+ duration: 300
416
+ },
417
+ centerButton: {
418
+ duration: 200
419
+ },
420
+ dialScale: {
421
+ duration: 250
422
+ },
423
+ backdrop: {
424
+ duration: 200
425
+ }
426
+ };
427
+
428
+ // Exit animation sequence - reverse order of entrance, using shared config
429
+ Animated.sequence([
430
+ // First animate icons back to center
431
+ Animated.timing(iconsProgress, {
432
+ toValue: 0,
433
+ duration: exit.icons.duration,
434
+ easing: Easing.in(Easing.cubic),
435
+ useNativeDriver: true
436
+ }),
437
+ // Then scale down center button and dial
438
+ Animated.parallel([Animated.timing(centerButtonScale, {
439
+ toValue: 0,
440
+ duration: exit.centerButton.duration,
441
+ easing: Easing.in(Easing.cubic),
442
+ useNativeDriver: true
443
+ }), Animated.timing(dialScale, {
444
+ toValue: 0,
445
+ duration: exit.dialScale.duration,
446
+ easing: Easing.in(Easing.cubic),
447
+ useNativeDriver: true
448
+ })]),
449
+ // Finally fade out backdrop
450
+ Animated.timing(backdropOpacity, {
451
+ toValue: 0,
452
+ duration: exit.backdrop.duration,
453
+ useNativeDriver: true
454
+ })]).start(() => {
455
+ // Use setTimeout to defer the state update to the next tick
456
+ // This avoids the useInsertionEffect warning
457
+ if (onClose) {
458
+ setTimeout(() => {
459
+ onClose();
460
+ }, 0);
461
+ }
462
+ });
463
+ };
464
+ const handleIconPress = index => {
465
+ setSelectedIcon(index);
466
+ const interaction = dialAnimationConfig?.interaction ?? {
467
+ iconSelect: {
468
+ pulse: [{
469
+ scale: 0.9,
470
+ damping: 15,
471
+ stiffness: 500
472
+ }, {
473
+ scale: 1,
474
+ damping: 10,
475
+ stiffness: 200
476
+ }],
477
+ actionDelay: 50
478
+ }
479
+ };
480
+ const [pulseIn, pulseOut] = interaction.iconSelect.pulse;
481
+
482
+ // Pulse animation on selection - using shared config
483
+ Animated.sequence([Animated.spring(centerButtonScale, {
484
+ toValue: pulseIn.scale,
485
+ damping: pulseIn.damping,
486
+ stiffness: pulseIn.stiffness,
487
+ useNativeDriver: true
488
+ }), Animated.spring(centerButtonScale, {
489
+ toValue: pulseOut.scale,
490
+ damping: pulseOut.damping,
491
+ stiffness: pulseOut.stiffness,
492
+ useNativeDriver: true
493
+ })]).start();
494
+
495
+ // Trigger action - using shared delay
496
+ setTimeout(() => {
497
+ icons[index].onPress();
498
+ // Only close if it's not the WiFi toggle (by id)
499
+ if (icons[index].id !== "wifi") {
500
+ handleClose();
501
+ }
502
+ }, interaction.iconSelect.actionDelay);
503
+ };
504
+
505
+ // Animated styles
506
+ const backdropAnimatedStyle = {
507
+ opacity: backdropOpacity
508
+ };
509
+ const glitchAnimatedStyle = {
510
+ transform: [{
511
+ translateX: glitchOffset
512
+ }]
513
+ };
514
+ const centerButtonAnimatedStyle = {
515
+ transform: [{
516
+ scale: Animated.multiply(centerButtonScale, breathingScale)
517
+ }]
518
+ };
519
+ const pulseAnimatedStyle = {
520
+ transform: [{
521
+ scale: selectedIcon >= 0 ? 1 : pulseScale
522
+ }]
523
+ };
524
+ return /*#__PURE__*/_jsxs(View, {
525
+ style: styles.container,
526
+ nativeID: "dial-devtools-root",
527
+ children: [/*#__PURE__*/_jsx(Animated.View, {
528
+ style: [styles.backdrop, backdropAnimatedStyle],
529
+ children: /*#__PURE__*/_jsx(Pressable, {
530
+ style: StyleSheet.absoluteFillObject,
531
+ onPress: handleClose
532
+ })
533
+ }), /*#__PURE__*/_jsxs(Animated.View, {
534
+ style: [styles.parent, {
535
+ position: "absolute",
536
+ left: (SCREEN_WIDTH - CIRCLE_SIZE) / 2,
537
+ bottom: 80,
538
+ transform: [{
539
+ translateY: floatingAnim
540
+ }, {
541
+ scale: dialScale
542
+ }, {
543
+ rotate: dialRotation.interpolate({
544
+ inputRange: [0, 1],
545
+ outputRange: ["0deg", "360deg"]
546
+ })
547
+ }]
548
+ }],
549
+ children: [/*#__PURE__*/_jsxs(Animated.View, {
550
+ style: [styles.circle, glitchAnimatedStyle],
551
+ children: [/*#__PURE__*/_jsxs(View, {
552
+ style: styles.gradientBackground,
553
+ children: [/*#__PURE__*/_jsx(View, {
554
+ style: styles.gradientLayer1
555
+ }), /*#__PURE__*/_jsx(View, {
556
+ style: styles.gradientLayer2
557
+ }), /*#__PURE__*/_jsx(View, {
558
+ style: styles.gradientLayer3
559
+ }), /*#__PURE__*/_jsx(View, {
560
+ style: styles.gridPattern,
561
+ children: Array.from({
562
+ length: 6
563
+ }).map((_, i) => /*#__PURE__*/_jsx(View, {
564
+ style: [styles.gridLine, {
565
+ transform: [{
566
+ rotate: `${i * 60}deg`
567
+ }]
568
+ }]
569
+ }, i))
570
+ })]
571
+ }), icons.map((icon, i) => /*#__PURE__*/_jsx(DialIcon, {
572
+ selectedIcon: selectedIcon,
573
+ onPress: handleIconPress,
574
+ iconsProgress: iconsProgress,
575
+ icon: icon,
576
+ index: i,
577
+ totalIcons: icons.length
578
+ }, `${i}-${icon.name}`))]
579
+ }), /*#__PURE__*/_jsx(Animated.View, {
580
+ style: [styles.buttonContainer, centerButtonAnimatedStyle],
581
+ children: /*#__PURE__*/_jsxs(View, {
582
+ style: styles.buttonGradient,
583
+ children: [/*#__PURE__*/_jsx(View, {
584
+ style: styles.buttonGradientLayer1
585
+ }), /*#__PURE__*/_jsx(View, {
586
+ style: styles.buttonGradientLayer2
587
+ }), /*#__PURE__*/_jsx(View, {
588
+ style: styles.buttonGradientLayer3
589
+ }), /*#__PURE__*/_jsx(View, {
590
+ style: styles.buttonBorder,
591
+ children: /*#__PURE__*/_jsx(Animated.View, {
592
+ style: [styles.button, pulseAnimatedStyle],
593
+ children: /*#__PURE__*/_jsx(Pressable, {
594
+ accessibilityRole: "button",
595
+ accessibilityLabel: isSettingsModalOpen ? "Close Settings" : "Open Dev Tools Settings",
596
+ onPress: () => {
597
+ if (isSettingsModalOpen) {
598
+ // Close settings modal
599
+ setIsSettingsModalOpen(false);
600
+ } else {
601
+ // Open internal settings modal
602
+ setIsSettingsModalOpen(true);
603
+ // Dismiss onboarding tooltip when user opens settings
604
+ if (showOnboardingTooltip && !onboardingDismissedRef.current) {
605
+ handleOnboardingDismiss();
606
+ }
607
+ // Also call external handler if provided
608
+ if (onSettingsPress) {
609
+ onSettingsPress();
610
+ }
611
+ }
612
+ },
613
+ style: styles.buttonPressable,
614
+ children: isSettingsModalOpen ? /*#__PURE__*/_jsxs(_Fragment, {
615
+ children: [/*#__PURE__*/_jsx(Text, {
616
+ style: [styles.centerText, styles.closeTextTop],
617
+ children: "CLOSE"
618
+ }), /*#__PURE__*/_jsx(Text, {
619
+ style: [styles.centerText, styles.closeTextBottom],
620
+ children: "SETTINGS"
621
+ })]
622
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
623
+ children: [/*#__PURE__*/_jsx(Text, {
624
+ style: styles.centerText,
625
+ children: "BUOY"
626
+ }), isPro && /*#__PURE__*/_jsx(Text, {
627
+ style: styles.proText,
628
+ children: "PRO"
629
+ })]
630
+ })
631
+ })
632
+ })
633
+ })]
634
+ })
635
+ })]
636
+ }), /*#__PURE__*/_jsx(DevToolsSettingsModal, {
637
+ visible: isSettingsModalOpen,
638
+ onClose: () => {
639
+ setIsSettingsModalOpen(false);
640
+ refreshSettings(); // Refresh from storage
641
+ },
642
+ onSettingsChange: newSettings => {
643
+ // Immediately update local settings for instant feedback
644
+ setLocalSettings(newSettings);
645
+ },
646
+ availableApps: availableApps
647
+ }), /*#__PURE__*/_jsx(OnboardingTooltip, {
648
+ visible: showOnboardingTooltip && !isSettingsModalOpen && !onboardingDismissedRef.current,
649
+ onDismiss: handleOnboardingDismiss
650
+ })]
651
+ });
652
+ };
653
+ const styles = StyleSheet.create({
654
+ container: {
655
+ ...StyleSheet.absoluteFillObject,
656
+ zIndex: 9999
657
+ },
658
+ backdrop: {
659
+ ...StyleSheet.absoluteFillObject,
660
+ backgroundColor: dialColors.dialBackdrop
661
+ },
662
+ parent: {
663
+ width: CIRCLE_SIZE,
664
+ height: CIRCLE_SIZE,
665
+ alignItems: "center",
666
+ justifyContent: "center"
667
+ },
668
+ circle: {
669
+ width: CIRCLE_SIZE,
670
+ height: CIRCLE_SIZE,
671
+ borderRadius: CIRCLE_SIZE / 2,
672
+ position: "absolute",
673
+ backgroundColor: "transparent",
674
+ borderWidth: 1,
675
+ borderColor: dialColors.dialBorder,
676
+ shadowColor: dialColors.dialShadow,
677
+ shadowOffset: {
678
+ width: 0,
679
+ height: 0
680
+ },
681
+ shadowOpacity: 0.5,
682
+ shadowRadius: 20,
683
+ elevation: 10
684
+ },
685
+ gradientBackground: {
686
+ width: "100%",
687
+ height: "100%",
688
+ borderRadius: CIRCLE_SIZE / 2,
689
+ position: "relative",
690
+ backgroundColor: dialColors.dialBackground,
691
+ overflow: "hidden"
692
+ },
693
+ gradientLayer1: {
694
+ ...StyleSheet.absoluteFillObject,
695
+ backgroundColor: dialColors.dialGradient1,
696
+ opacity: 0.6,
697
+ borderRadius: CIRCLE_SIZE / 2
698
+ },
699
+ gradientLayer2: {
700
+ ...StyleSheet.absoluteFillObject,
701
+ backgroundColor: dialColors.dialGradient2,
702
+ opacity: 0.4,
703
+ top: "30%",
704
+ left: "30%",
705
+ borderRadius: CIRCLE_SIZE / 2
706
+ },
707
+ gradientLayer3: {
708
+ ...StyleSheet.absoluteFillObject,
709
+ backgroundColor: dialColors.dialGradient3,
710
+ opacity: 0.3,
711
+ top: "50%",
712
+ left: "50%",
713
+ borderRadius: CIRCLE_SIZE / 2
714
+ },
715
+ gridPattern: {
716
+ ...StyleSheet.absoluteFillObject,
717
+ alignItems: "center",
718
+ justifyContent: "center"
719
+ },
720
+ gridLine: {
721
+ position: "absolute",
722
+ width: CIRCLE_SIZE,
723
+ height: 1,
724
+ backgroundColor: dialColors.dialGridLine
725
+ },
726
+ buttonContainer: {
727
+ zIndex: 1,
728
+ backgroundColor: "transparent",
729
+ alignItems: "center",
730
+ justifyContent: "center",
731
+ position: "absolute",
732
+ width: BUTTON_SIZE * 1.5,
733
+ height: BUTTON_SIZE * 1.5,
734
+ borderRadius: BUTTON_SIZE
735
+ },
736
+ buttonGradient: {
737
+ width: "100%",
738
+ height: "100%",
739
+ borderRadius: BUTTON_SIZE,
740
+ alignItems: "center",
741
+ justifyContent: "center",
742
+ padding: 4,
743
+ backgroundColor: dialColors.dialBackground,
744
+ position: "relative",
745
+ overflow: "hidden"
746
+ },
747
+ buttonGradientLayer1: {
748
+ ...StyleSheet.absoluteFillObject,
749
+ backgroundColor: dialColors.dialGradient1,
750
+ opacity: 0.5,
751
+ borderRadius: BUTTON_SIZE
752
+ },
753
+ buttonGradientLayer2: {
754
+ ...StyleSheet.absoluteFillObject,
755
+ backgroundColor: dialColors.dialGradient2,
756
+ opacity: 0.3,
757
+ top: "20%",
758
+ left: "20%",
759
+ borderRadius: BUTTON_SIZE
760
+ },
761
+ buttonGradientLayer3: {
762
+ ...StyleSheet.absoluteFillObject,
763
+ backgroundColor: dialColors.dialGradient3,
764
+ opacity: 0.2,
765
+ top: "40%",
766
+ left: "40%",
767
+ borderRadius: BUTTON_SIZE
768
+ },
769
+ buttonBorder: {
770
+ backgroundColor: dialColors.dialGridLine,
771
+ alignItems: "center",
772
+ justifyContent: "center",
773
+ width: BUTTON_SIZE * 1.2,
774
+ height: BUTTON_SIZE * 1.2,
775
+ borderRadius: BUTTON_SIZE * 0.6,
776
+ borderWidth: 2,
777
+ borderColor: dialColors.dialBorder
778
+ },
779
+ button: {
780
+ width: BUTTON_SIZE,
781
+ height: BUTTON_SIZE,
782
+ borderRadius: BUTTON_SIZE / 2,
783
+ justifyContent: "center",
784
+ alignItems: "center",
785
+ position: "relative",
786
+ overflow: "hidden"
787
+ },
788
+ buttonPressable: {
789
+ width: "100%",
790
+ height: "100%",
791
+ justifyContent: "center",
792
+ alignItems: "center"
793
+ },
794
+ centerText: {
795
+ color: "#FFFFFF",
796
+ fontSize: 10,
797
+ fontWeight: "900",
798
+ fontFamily: "monospace",
799
+ letterSpacing: 1,
800
+ textAlign: "center",
801
+ textTransform: "uppercase",
802
+ textShadowColor: buoyColors.primary,
803
+ textShadowOffset: {
804
+ width: 0,
805
+ height: 0
806
+ },
807
+ textShadowRadius: 4
808
+ },
809
+ closeTextTop: {
810
+ marginBottom: -2
811
+ },
812
+ closeTextBottom: {
813
+ marginTop: -2
814
+ },
815
+ proText: {
816
+ color: buoyColors.warning,
817
+ fontSize: 8,
818
+ fontWeight: "900",
819
+ fontFamily: "monospace",
820
+ letterSpacing: 2,
821
+ textAlign: "center",
822
+ textShadowColor: buoyColors.warning,
823
+ textShadowOffset: {
824
+ width: 0,
825
+ height: 0
826
+ },
827
+ textShadowRadius: 4
828
+ }
829
+ });