@buoy-gg/core 7.0.39 → 7.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +10 -0
  2. package/bin/buoy.js +25 -31
  3. package/lib/commonjs/browserRegister.js +48 -0
  4. package/lib/commonjs/browserRegister.server.js +10 -0
  5. package/lib/commonjs/floatingMenu/AppHost.web.js +71 -59
  6. package/lib/commonjs/floatingMenu/BrowserToolRuntime.js +150 -0
  7. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +10 -1107
  8. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.shared.js +1114 -0
  9. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +28 -1028
  10. package/lib/commonjs/floatingMenu/FloatingDevTools.js +7 -2
  11. package/lib/commonjs/floatingMenu/FloatingDevTools.web.js +54 -40
  12. package/lib/commonjs/floatingMenu/MinimizedToolsStack.web.js +8 -2
  13. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +10 -0
  14. package/lib/commonjs/floatingMenu/autoExternalSync.js +9 -0
  15. package/lib/commonjs/floatingMenu/dial/DialDevTools.web.js +86 -12
  16. package/lib/commonjs/floatingMenu/dial/DialIcon.js +4 -2
  17. package/lib/commonjs/floatingMenu/settingsBus.js +6 -17
  18. package/lib/commonjs/floatingMenu/useDesktopSyncStatus.web.js +14 -0
  19. package/lib/commonjs/index.web.js +92 -1
  20. package/lib/commonjs/reactDevtoolsTypes.js +1 -0
  21. package/lib/commonjs/utils/autoDiscoverPresets.web.js +5 -178
  22. package/lib/commonjs/utils/createWebTool.js +20 -0
  23. package/lib/commonjs/utils/createWebTools.js +30 -0
  24. package/lib/commonjs/version.js +1 -1
  25. package/lib/module/browserRegister.js +44 -0
  26. package/lib/module/browserRegister.server.js +6 -0
  27. package/lib/module/floatingMenu/AppHost.web.js +71 -59
  28. package/lib/module/floatingMenu/BrowserToolRuntime.js +146 -0
  29. package/lib/module/floatingMenu/DevToolsSettingsModal.js +1 -1109
  30. package/lib/module/floatingMenu/DevToolsSettingsModal.shared.js +1110 -0
  31. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +32 -1043
  32. package/lib/module/floatingMenu/FloatingDevTools.js +7 -2
  33. package/lib/module/floatingMenu/FloatingDevTools.web.js +55 -42
  34. package/lib/module/floatingMenu/MinimizedToolsStack.web.js +8 -2
  35. package/lib/module/floatingMenu/autoDiscoverPresets.js +10 -0
  36. package/lib/module/floatingMenu/autoExternalSync.js +9 -0
  37. package/lib/module/floatingMenu/dial/DialDevTools.web.js +87 -14
  38. package/lib/module/floatingMenu/dial/DialIcon.js +4 -2
  39. package/lib/module/floatingMenu/settingsBus.js +1 -17
  40. package/lib/module/floatingMenu/useDesktopSyncStatus.web.js +10 -0
  41. package/lib/module/index.web.js +8 -1
  42. package/lib/module/reactDevtoolsTypes.js +1 -0
  43. package/lib/module/utils/autoDiscoverPresets.web.js +5 -180
  44. package/lib/module/utils/createWebTool.js +17 -0
  45. package/lib/module/utils/createWebTools.js +25 -0
  46. package/lib/module/version.js +1 -1
  47. package/lib/typescript/commonjs/browserRegister.d.ts +5 -0
  48. package/lib/typescript/commonjs/browserRegister.d.ts.map +1 -0
  49. package/lib/typescript/commonjs/browserRegister.server.d.ts +5 -0
  50. package/lib/typescript/commonjs/browserRegister.server.d.ts.map +1 -0
  51. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts +3 -3
  52. package/lib/typescript/commonjs/floatingMenu/AppHost.web.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/floatingMenu/BrowserToolRuntime.d.ts +14 -0
  54. package/lib/typescript/commonjs/floatingMenu/BrowserToolRuntime.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts +2 -67
  56. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.shared.d.ts +71 -0
  58. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.shared.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts +3 -16
  60. package/lib/typescript/commonjs/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts +12 -6
  63. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  68. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts +1 -9
  72. package/lib/typescript/commonjs/floatingMenu/settingsBus.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/floatingMenu/useDesktopSyncStatus.web.d.ts +3 -0
  74. package/lib/typescript/commonjs/floatingMenu/useDesktopSyncStatus.web.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/index.web.d.ts +6 -0
  76. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/reactDevtoolsTypes.d.ts +8 -0
  78. package/lib/typescript/commonjs/reactDevtoolsTypes.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts +3 -40
  80. package/lib/typescript/commonjs/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/utils/createWebTool.d.ts +6 -0
  82. package/lib/typescript/commonjs/utils/createWebTool.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/utils/createWebTools.d.ts +4 -0
  84. package/lib/typescript/commonjs/utils/createWebTools.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/version.d.ts +1 -1
  86. package/lib/typescript/module/browserRegister.d.ts +5 -0
  87. package/lib/typescript/module/browserRegister.d.ts.map +1 -0
  88. package/lib/typescript/module/browserRegister.server.d.ts +5 -0
  89. package/lib/typescript/module/browserRegister.server.d.ts.map +1 -0
  90. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts +3 -3
  91. package/lib/typescript/module/floatingMenu/AppHost.web.d.ts.map +1 -1
  92. package/lib/typescript/module/floatingMenu/BrowserToolRuntime.d.ts +14 -0
  93. package/lib/typescript/module/floatingMenu/BrowserToolRuntime.d.ts.map +1 -0
  94. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts +2 -67
  95. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.d.ts.map +1 -1
  96. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.shared.d.ts +71 -0
  97. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.shared.d.ts.map +1 -0
  98. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts +3 -16
  99. package/lib/typescript/module/floatingMenu/DevToolsSettingsModal.web.d.ts.map +1 -1
  100. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  101. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts +12 -6
  102. package/lib/typescript/module/floatingMenu/FloatingDevTools.web.d.ts.map +1 -1
  103. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.web.d.ts.map +1 -1
  104. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  105. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  106. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  107. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  108. package/lib/typescript/module/floatingMenu/dial/DialDevTools.web.d.ts.map +1 -1
  109. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  110. package/lib/typescript/module/floatingMenu/settingsBus.d.ts +1 -9
  111. package/lib/typescript/module/floatingMenu/settingsBus.d.ts.map +1 -1
  112. package/lib/typescript/module/floatingMenu/useDesktopSyncStatus.web.d.ts +3 -0
  113. package/lib/typescript/module/floatingMenu/useDesktopSyncStatus.web.d.ts.map +1 -0
  114. package/lib/typescript/module/index.web.d.ts +6 -0
  115. package/lib/typescript/module/index.web.d.ts.map +1 -1
  116. package/lib/typescript/module/reactDevtoolsTypes.d.ts +8 -0
  117. package/lib/typescript/module/reactDevtoolsTypes.d.ts.map +1 -0
  118. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts +3 -40
  119. package/lib/typescript/module/utils/autoDiscoverPresets.web.d.ts.map +1 -1
  120. package/lib/typescript/module/utils/createWebTool.d.ts +6 -0
  121. package/lib/typescript/module/utils/createWebTool.d.ts.map +1 -0
  122. package/lib/typescript/module/utils/createWebTools.d.ts +4 -0
  123. package/lib/typescript/module/utils/createWebTools.d.ts.map +1 -0
  124. package/lib/typescript/module/version.d.ts +1 -1
  125. package/lib/web/chunks/chunk-5NZOQR6Q.mjs +2 -0
  126. package/lib/web/chunks/chunk-CGDSLLZF.mjs +2 -0
  127. package/lib/web/chunks/chunk-DCJDUSNR.mjs +2 -0
  128. package/lib/web/chunks/chunk-IDSHFS6G.mjs +2 -0
  129. package/lib/web/chunks/chunk-JOAMJGCS.mjs +2 -0
  130. package/lib/web/chunks/chunk-KYKZ2N52.mjs +2 -0
  131. package/lib/web/chunks/chunk-NGM7JGGS.mjs +2 -0
  132. package/lib/web/chunks/chunk-QZDVCMCT.mjs +2 -0
  133. package/lib/web/chunks/chunk-R7RGRTY5.mjs +2 -0
  134. package/lib/web/chunks/chunk-Z3XWDDIO.mjs +2 -0
  135. package/lib/web/chunks/chunk-ZTGQ6TTY.mjs +2 -0
  136. package/lib/web/index.mjs +11 -0
  137. package/lib/web/register-server.mjs +2 -0
  138. package/lib/web/register.mjs +2 -0
  139. package/lib/web/toolkit.mjs +2 -0
  140. package/package.json +50 -12
@@ -0,0 +1,1110 @@
1
+ "use strict";
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from "react";
4
+ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions, Platform } from "react-native";
5
+ import { settingsBus } from "./settingsBus.js";
6
+ import { useDesktopSyncStatus } from "./useDesktopSyncStatus";
7
+ import { SentryIcon, WifiCircuitIcon, ImageOverlayIcon, RenderCountIcon, Info, Layers, ChevronRightIcon, ChevronDown, getStorageBackendType, getUseLicense, persistentStorage, Trash2, CheckCircle2, LicenseEntryModal, BackgroundSwitcher, night, NightCard, NightRows, NightSectionLabel, NightFootnote, NightSwitch, NightButton, NightSegmented, NightBadge, NightSeparator, monoFont } from "@buoy-gg/shared-ui";
8
+ import { EnvIcon, StorageIcon, RoutesIcon, NetworkIcon, QueryIcon, HighlighterIcon, ReduxIcon, EventsIcon } from "@buoy-gg/floating-tools-core";
9
+ import { useDefaultConfig } from "./DefaultConfigContext.js";
10
+ import { renderAppIcon } from "./AppIcon";
11
+ import { JsModal, devToolsStorageKeys } from "@buoy-gg/shared-ui";
12
+ import { useSafeAreaInsets } from "@buoy-gg/shared-ui";
13
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
+ const STORAGE_KEY = "@react_buoy_dev_tools_settings";
15
+ const MAX_DIAL_SLOTS = 6;
16
+ const enforceDialLimit = dialTools => {
17
+ let remaining = MAX_DIAL_SLOTS;
18
+ const limited = {};
19
+ for (const [id, enabled] of Object.entries(dialTools)) {
20
+ if (enabled && remaining > 0) {
21
+ limited[id] = true;
22
+ remaining -= 1;
23
+ } else {
24
+ limited[id] = false;
25
+ }
26
+ }
27
+ return limited;
28
+ };
29
+ const sanitizeFloating = (floating, allowedKeys) => {
30
+ const {
31
+ userStatus,
32
+ environment,
33
+ ...rest
34
+ } = floating;
35
+ const filteredEntries = allowedKeys ? Object.entries(rest).filter(([key]) => allowedKeys.includes(key)) : Object.entries(rest);
36
+ return {
37
+ ...Object.fromEntries(filteredEntries),
38
+ environment: environment ?? false
39
+ };
40
+ };
41
+ const mergeWithDefaults = (defaults, stored, options) => {
42
+ if (!stored) return defaults;
43
+ const combinedDial = {
44
+ ...defaults.dialTools,
45
+ ...(stored.dialTools ?? {})
46
+ };
47
+ const dialEntries = options?.allowedDialKeys ? Object.entries(combinedDial).filter(([key]) => options.allowedDialKeys?.includes(key)) : Object.entries(combinedDial);
48
+ const mergedDial = enforceDialLimit(Object.fromEntries(dialEntries));
49
+ return {
50
+ dialTools: mergedDial,
51
+ floatingTools: sanitizeFloating({
52
+ ...defaults.floatingTools,
53
+ ...(stored.floatingTools ?? {})
54
+ }, options?.allowedFloatingKeys),
55
+ globalSettings: {
56
+ enableSharedModalDimensions: false,
57
+ // Default to false
58
+ expandableWindowControls: true,
59
+ // Default to true
60
+ ...(stored.globalSettings ?? {})
61
+ }
62
+ };
63
+ };
64
+
65
+ /**
66
+ * Global settings that apply to all dev tools.
67
+ * These settings override individual tool configurations.
68
+ */
69
+
70
+ /**
71
+ * Serialized preferences that power the floating dev tools UI. Values persist across sessions
72
+ * via AsyncStorage so teams can tailor which tools appear in the dial or floating row.
73
+ */
74
+
75
+ /**
76
+ * The subset of InstalledApp the settings modal needs per tool. `icon`/`color`
77
+ * let rows fall back to the tool's own glyph when getToolIcon() has no
78
+ * night-tuned override for it.
79
+ */
80
+
81
+ /**
82
+ * Generate default settings based on available apps and optional team default configuration.
83
+ *
84
+ * @param availableApps - List of available apps from auto-discovery
85
+ * @param defaultFloatingTools - Optional array of tool IDs to enable by default in floating bubble
86
+ * @param defaultDialTools - Optional array of tool IDs to enable by default in dial menu
87
+ * If not provided, all dial-compatible tools are enabled by default (up to 6)
88
+ */
89
+ const generateDefaultSettings = (availableApps = [], defaultFloatingTools, defaultDialTools) => {
90
+ const dialDefaults = {};
91
+ const floatingDefaults = {};
92
+
93
+ // Create sets for quick lookup of default-enabled tools
94
+ // Cast to Set<string> to allow comparison with any tool ID (including custom tools)
95
+ const enabledFloatingSet = new Set(defaultFloatingTools ?? []);
96
+ const enabledDialSet = new Set(defaultDialTools ?? []);
97
+
98
+ // If no defaultDialTools specified, auto-enable all dial-compatible tools
99
+ const autoEnableDialTools = !defaultDialTools || defaultDialTools.length === 0;
100
+ for (const app of availableApps) {
101
+ const {
102
+ id,
103
+ slot = "both"
104
+ } = app;
105
+ if (slot === "dial" || slot === "both") {
106
+ // Auto-enable if no defaults specified, otherwise check the set
107
+ dialDefaults[id] = autoEnableDialTools ? true : enabledDialSet.has(id);
108
+ }
109
+ if (slot === "row" || slot === "both") {
110
+ // Enable if in defaultFloatingTools, otherwise false
111
+ floatingDefaults[id] = enabledFloatingSet.has(id);
112
+ }
113
+ }
114
+ return {
115
+ dialTools: enforceDialLimit(dialDefaults),
116
+ floatingTools: {
117
+ ...floatingDefaults,
118
+ // Special: environment badge - check if 'environment' is in the floating defaults
119
+ environment: enabledFloatingSet.has('environment')
120
+ },
121
+ globalSettings: {
122
+ enableSharedModalDimensions: false,
123
+ // Default to false - each modal has its own persistence
124
+ expandableWindowControls: true // Default to true - expandable on touch devices
125
+ }
126
+ };
127
+ };
128
+
129
+ /**
130
+ * Configurable modal surface that lets engineers pick which dev tools appear in the dial and
131
+ * floating row. Persists preferences and enforces slot limits for a consistent UX.
132
+ *
133
+ * Styled in the "night" language (Everlights port — see shared-ui's
134
+ * ui/night): section labels outside near-black cards, hairline-separated
135
+ * rows, real switches, and a transparent header the star field runs through.
136
+ */
137
+ export const DevToolsSettingsModal = ({
138
+ visible,
139
+ onClose,
140
+ onSettingsChange,
141
+ initialSettings,
142
+ availableApps = [],
143
+ storage = persistentStorage,
144
+ zIndex
145
+ }) => {
146
+ // Get team default configuration from context
147
+ const {
148
+ defaultFloatingTools,
149
+ defaultDialTools
150
+ } = useDefaultConfig();
151
+ const defaultSettings = useMemo(() => generateDefaultSettings(availableApps, defaultFloatingTools, defaultDialTools), [availableApps, defaultFloatingTools, defaultDialTools]);
152
+ const allowedDialKeys = useMemo(() => Object.keys(defaultSettings.dialTools), [defaultSettings]);
153
+ const allowedFloatingKeys = useMemo(() => Object.keys(defaultSettings.floatingTools).filter(key => key !== "environment"), [defaultSettings]);
154
+ const [settings, setSettings] = useState(initialSettings || cachedSettings || defaultSettings);
155
+ const [activeTab, setActiveTab] = useState(cachedActiveTab ?? "floating");
156
+ const [activeTabLoaded, setActiveTabLoaded] = useState(cachedActiveTab !== null);
157
+ const [expandedSettings, setExpandedSettings] = useState(new Set());
158
+ const [showLicenseModal, setShowLicenseModal] = useState(false);
159
+ /**
160
+ * Tool icons are the expensive half of this modal: ~20 of them, each a
161
+ * data-driven glyph that mounts one view per shape. Rendering them in the
162
+ * first pass pushed the modal's first paint out to ~240ms, so the tap felt
163
+ * dead. Holding them back a single frame lets the chrome and the tool rows
164
+ * paint at ~100ms and the glyphs land during the entrance fade.
165
+ *
166
+ * There is no layout shift: `styles.toolIconSlot` is a fixed box, so it
167
+ * reads as the icon's placeholder until filled.
168
+ */
169
+ const [iconsReady, setIconsReady] = useState(false);
170
+ useEffect(() => {
171
+ const frame = requestAnimationFrame(() => setIconsReady(true));
172
+ return () => cancelAnimationFrame(frame);
173
+ }, []);
174
+
175
+ // null when external-sync isn't installed (or predates the status store);
176
+ // "idle" until AutoExternalSync initializes — the row hides in both cases.
177
+ const desktopSyncStatus = useDesktopSyncStatus();
178
+
179
+ // Load persisted active tab on mount
180
+ useEffect(() => {
181
+ const loadActiveTab = async () => {
182
+ try {
183
+ const savedTab = await persistentStorage.getItem(devToolsStorageKeys.settings.activeTab());
184
+ if (savedTab && ["floating", "settings", "pro"].includes(savedTab)) {
185
+ const tab = savedTab;
186
+ cachedActiveTab = tab;
187
+ setActiveTab(prev => prev === tab ? prev : tab);
188
+ }
189
+ } catch (error) {
190
+ // Failed to load active tab - use default
191
+ } finally {
192
+ setActiveTabLoaded(true);
193
+ }
194
+ };
195
+ loadActiveTab();
196
+ }, []);
197
+
198
+ // Persist active tab when it changes
199
+ useEffect(() => {
200
+ // Only persist after initial state is loaded to avoid overwriting with default
201
+ if (!activeTabLoaded) return;
202
+ persistentStorage.setItem(devToolsStorageKeys.settings.activeTab(), activeTab).catch(error => {
203
+ // Failed to save active tab - continue without persistence
204
+ console.warn("Failed to save settings active tab:", error);
205
+ });
206
+ }, [activeTab, activeTabLoaded]);
207
+
208
+ // License hooks
209
+ const useLicenseHook = getUseLicense();
210
+ const license = useLicenseHook?.();
211
+ const isPro = license?.isPro ?? false;
212
+ // A key is set but validation is failing. Without this the tab renders a bare
213
+ // "Free" badge, which reads as "your key was rejected" when what actually
214
+ // happened is that we never reached the license server. `errorKind` decides
215
+ // which story to tell: only a transport failure gets the VPN/proxy advice — a
216
+ // real verdict (suspended, expired) with that advice would send the user off
217
+ // to fight their network over a key that was genuinely rejected.
218
+ const licenseError = !isPro && license?.licenseKey ? license.error : null;
219
+ const licenseErrorIsTransport = license?.errorKind === "transport";
220
+ const [storageBackend, setStorageBackend] = useState(cachedStorageBackend);
221
+ const [isClearing, setIsClearing] = useState(false);
222
+ const [clearSuccess, setClearSuccess] = useState(false);
223
+ const [isStorageExpanded, setIsStorageExpanded] = useState(false);
224
+ const [savedKeys, setSavedKeys] = useState([]);
225
+ const [savedKeysLoading, setSavedKeysLoading] = useState(false);
226
+ const insets = useSafeAreaInsets();
227
+ const screenHeight = Dimensions.get("window").height;
228
+ const screenWidth = Dimensions.get("window").width;
229
+ const modalHeight = Math.floor(screenHeight * 0.33); // 1/3 of screen height
230
+ const modalWidth = Math.min(screenWidth - 32, 400); // Modal width with padding
231
+
232
+ const loadSettings = useCallback(async () => {
233
+ try {
234
+ const savedSettings = await storage.getItem(STORAGE_KEY);
235
+ if (savedSettings) {
236
+ const parsed = JSON.parse(savedSettings);
237
+ const merged = mergeWithDefaults(defaultSettings, parsed, {
238
+ allowedDialKeys,
239
+ allowedFloatingKeys
240
+ });
241
+ cachedSettings = merged;
242
+ setSettings(prev => sameSettings(prev, merged) ? prev : merged);
243
+ return;
244
+ }
245
+ cachedSettings = defaultSettings;
246
+ setSettings(prev => sameSettings(prev, defaultSettings) ? prev : defaultSettings);
247
+ } catch (error) {
248
+ console.error("Failed to load dev tools settings:", error);
249
+ setSettings(defaultSettings);
250
+ }
251
+ }, [defaultSettings, allowedDialKeys, allowedFloatingKeys, storage]);
252
+ useEffect(() => {
253
+ loadSettings();
254
+ }, [loadSettings]);
255
+
256
+ // Load storage backend type
257
+ useEffect(() => {
258
+ getStorageBackendType().then(backend => {
259
+ cachedStorageBackend = backend;
260
+ setStorageBackend(prev => prev === backend ? prev : backend);
261
+ });
262
+ }, []);
263
+
264
+ // Load saved keys when storage card is expanded
265
+ useEffect(() => {
266
+ if (isStorageExpanded) {
267
+ setSavedKeysLoading(true);
268
+ persistentStorage.getAllKeys().then(keys => {
269
+ setSavedKeys(keys.sort());
270
+ }).catch(() => {
271
+ setSavedKeys([]);
272
+ }).finally(() => {
273
+ setSavedKeysLoading(false);
274
+ });
275
+ }
276
+ }, [isStorageExpanded]);
277
+ const saveSettings = async newSettings => {
278
+ try {
279
+ const limitedSettings = {
280
+ ...newSettings,
281
+ dialTools: enforceDialLimit(newSettings.dialTools)
282
+ };
283
+ await storage.setItem(STORAGE_KEY, JSON.stringify(limitedSettings));
284
+ cachedSettings = limitedSettings;
285
+ setSettings(limitedSettings);
286
+ onSettingsChange?.(limitedSettings);
287
+ // Notify listeners (e.g., floating bubble) to refresh immediately
288
+ settingsBus.emit(limitedSettings);
289
+ } catch (error) {
290
+ console.error("Failed to save dev tools settings:", error);
291
+ }
292
+ };
293
+ const toggleFloatingTool = tool => {
294
+ const newSettings = {
295
+ ...settings,
296
+ floatingTools: {
297
+ ...settings.floatingTools,
298
+ [tool]: !settings.floatingTools[tool]
299
+ }
300
+ };
301
+ saveSettings(newSettings);
302
+ };
303
+ const toggleGlobalSetting = setting => {
304
+ const newSettings = {
305
+ ...settings,
306
+ globalSettings: {
307
+ ...settings.globalSettings,
308
+ [setting]: !settings.globalSettings?.[setting]
309
+ }
310
+ };
311
+ saveSettings(newSettings);
312
+ };
313
+ const handleClearStorage = async () => {
314
+ setIsClearing(true);
315
+ setClearSuccess(false);
316
+ try {
317
+ await persistentStorage.clear();
318
+ setClearSuccess(true);
319
+ // Reset settings to defaults after clearing
320
+ setSettings(defaultSettings);
321
+ if (storage !== persistentStorage) await storage.setItem(STORAGE_KEY, JSON.stringify(defaultSettings));
322
+ onSettingsChange?.(defaultSettings);
323
+ settingsBus.emit(defaultSettings);
324
+ // Reset success state after 2 seconds
325
+ setTimeout(() => setClearSuccess(false), 2000);
326
+ } catch (error) {
327
+ console.error("Failed to clear storage:", error);
328
+ } finally {
329
+ setIsClearing(false);
330
+ }
331
+ };
332
+
333
+ // Modal is fixed to bottom sheet mode
334
+ const handleModeChange = useCallback(_mode => {
335
+ // Mode changes handled by JsModal
336
+ }, []);
337
+ const getToolDescription = tool => {
338
+ // Get description from availableApps
339
+ const app = availableApps.find(a => a.id === tool);
340
+ if (app?.description) {
341
+ return app.description;
342
+ }
343
+ if (tool === "environment") {
344
+ return "Environment badge.";
345
+ }
346
+ return "";
347
+ };
348
+ const getToolLabel = tool => {
349
+ if (tool === "environment") {
350
+ return "Env Badge";
351
+ }
352
+ const app = availableApps.find(a => a.id === tool);
353
+ return app?.name ?? tool.toUpperCase().replace(/_/g, " ");
354
+ };
355
+ const getToolIcon = tool => {
356
+ if (!iconsReady) return null;
357
+ switch (tool) {
358
+ case "query":
359
+ return /*#__PURE__*/_jsx(QueryIcon, {
360
+ size: 18
361
+ });
362
+ case "env":
363
+ return /*#__PURE__*/_jsx(EnvIcon, {
364
+ size: 18
365
+ });
366
+ case "sentry":
367
+ return /*#__PURE__*/_jsx(SentryIcon, {
368
+ size: 18,
369
+ color: "#9945FF"
370
+ });
371
+ case "storage":
372
+ return /*#__PURE__*/_jsx(StorageIcon, {
373
+ size: 18,
374
+ bgColor: night.color.surface
375
+ });
376
+ case "wifi":
377
+ case "query-wifi-toggle":
378
+ // Support both IDs for wifi toggle
379
+ return /*#__PURE__*/_jsx(WifiCircuitIcon, {
380
+ size: 18,
381
+ colorPreset: "green",
382
+ strength: 4,
383
+ noBackground: true
384
+ });
385
+ case "route-events":
386
+ return /*#__PURE__*/_jsx(RoutesIcon, {
387
+ size: 18,
388
+ bgColor: night.color.surface
389
+ });
390
+ case "network":
391
+ return /*#__PURE__*/_jsx(NetworkIcon, {
392
+ size: 18
393
+ });
394
+ case "environment":
395
+ return /*#__PURE__*/_jsx(EnvIcon, {
396
+ size: 18
397
+ });
398
+ case "debug-borders":
399
+ return /*#__PURE__*/_jsx(Layers, {
400
+ size: 18,
401
+ color: night.color.accent
402
+ });
403
+ case "highlight-updates":
404
+ return /*#__PURE__*/_jsx(RenderCountIcon, {
405
+ size: 18,
406
+ color: night.color.accent
407
+ });
408
+ case "highlight-updates-modal":
409
+ return /*#__PURE__*/_jsx(HighlighterIcon, {
410
+ size: 18
411
+ });
412
+ case "redux":
413
+ return /*#__PURE__*/_jsx(ReduxIcon, {
414
+ size: 18
415
+ });
416
+ case "image-overlay":
417
+ return /*#__PURE__*/_jsx(ImageOverlayIcon, {
418
+ size: 18
419
+ });
420
+ case "events":
421
+ return /*#__PURE__*/_jsx(EventsIcon, {
422
+ size: 18
423
+ });
424
+ case "zustand":
425
+ try {
426
+ // @ts-ignore - Optional package
427
+ const {
428
+ ZustandIcon
429
+ } = require("@buoy-gg/zustand");
430
+ return /*#__PURE__*/_jsx(ZustandIcon, {
431
+ size: 18
432
+ });
433
+ } catch {
434
+ return /*#__PURE__*/_jsx(Info, {
435
+ size: 18,
436
+ color: night.color.textSecondary
437
+ });
438
+ }
439
+ case "jotai":
440
+ try {
441
+ // @ts-ignore - Optional package
442
+ const {
443
+ JotaiIcon
444
+ } = require("@buoy-gg/jotai");
445
+ return /*#__PURE__*/_jsx(JotaiIcon, {
446
+ size: 18
447
+ });
448
+ } catch {
449
+ return /*#__PURE__*/_jsx(Info, {
450
+ size: 18,
451
+ color: night.color.textSecondary
452
+ });
453
+ }
454
+ default:
455
+ {
456
+ // Tools without a night-tuned override (console, images, time
457
+ // machine, perf monitor, custom toolkit tools, …) render their own
458
+ // app icon — the same artwork the dial shows.
459
+ const app = availableApps.find(a => a.id === tool);
460
+ if (app?.icon != null) {
461
+ return renderAppIcon(app.icon, {
462
+ slot: "row",
463
+ size: 18,
464
+ color: app.color
465
+ });
466
+ }
467
+ return /*#__PURE__*/_jsx(Info, {
468
+ size: 18,
469
+ color: night.color.textSecondary
470
+ });
471
+ }
472
+ }
473
+ };
474
+
475
+ /**
476
+ * One tool row: glyph, name + one-line description, switch. Lives inside a
477
+ * NightCard's hairline-separated rows.
478
+ */
479
+ const renderToolRow = (keyName, value, disabled, onToggle) => {
480
+ const description = getToolDescription(keyName);
481
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
482
+ activeOpacity: disabled ? 1 : 0.7,
483
+ onPress: () => !disabled && onToggle(),
484
+ accessibilityLabel: getToolLabel(keyName),
485
+ accessibilityRole: "button",
486
+ style: [styles.toolRow, disabled && styles.rowDisabled],
487
+ children: [/*#__PURE__*/_jsx(View, {
488
+ style: styles.toolIconSlot,
489
+ children: getToolIcon(keyName)
490
+ }), /*#__PURE__*/_jsxs(View, {
491
+ style: styles.rowInfo,
492
+ children: [/*#__PURE__*/_jsxs(Text, {
493
+ style: styles.rowLabel,
494
+ children: [getToolLabel(keyName), disabled ? " · max 6" : ""]
495
+ }), description ? /*#__PURE__*/_jsx(Text, {
496
+ style: styles.rowCaption,
497
+ numberOfLines: 1,
498
+ children: description
499
+ }) : null]
500
+ }), /*#__PURE__*/_jsx(NightSwitch, {
501
+ value: value,
502
+ disabled: disabled,
503
+ onValueChange: () => onToggle()
504
+ })]
505
+ }, keyName);
506
+ };
507
+
508
+ // Toggle expanded state for a setting card
509
+ const toggleSettingExpanded = settingKey => {
510
+ setExpandedSettings(prev => {
511
+ const newSet = new Set(prev);
512
+ if (newSet.has(settingKey)) {
513
+ newSet.delete(settingKey);
514
+ } else {
515
+ newSet.add(settingKey);
516
+ }
517
+ return newSet;
518
+ });
519
+ };
520
+
521
+ /**
522
+ * One global-setting row: name + one-line hint, switch, and a tap-to-expand
523
+ * body holding the full description and recommendation.
524
+ */
525
+ const renderGlobalSettingRow = (settingKey, label, shortDescription, fullDescription, recommendation) => {
526
+ const value = settings.globalSettings?.[settingKey] ?? false;
527
+ const isExpanded = expandedSettings.has(settingKey);
528
+ return /*#__PURE__*/_jsxs(View, {
529
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
530
+ activeOpacity: 0.7,
531
+ onPress: () => toggleSettingExpanded(settingKey),
532
+ accessibilityLabel: label,
533
+ style: styles.settingRow,
534
+ children: [/*#__PURE__*/_jsxs(View, {
535
+ style: styles.rowInfo,
536
+ children: [/*#__PURE__*/_jsx(Text, {
537
+ style: styles.rowLabel,
538
+ children: label
539
+ }), /*#__PURE__*/_jsx(Text, {
540
+ style: styles.rowCaption,
541
+ numberOfLines: isExpanded ? 0 : 1,
542
+ children: shortDescription
543
+ })]
544
+ }), /*#__PURE__*/_jsx(NightSwitch, {
545
+ value: value,
546
+ onValueChange: () => toggleGlobalSetting(settingKey)
547
+ })]
548
+ }), isExpanded && /*#__PURE__*/_jsxs(View, {
549
+ style: styles.settingRowBody,
550
+ children: [/*#__PURE__*/_jsx(Text, {
551
+ style: styles.settingRowBodyText,
552
+ children: fullDescription
553
+ }), /*#__PURE__*/_jsx(Text, {
554
+ style: styles.settingRowBodyHint,
555
+ children: recommendation
556
+ })]
557
+ })]
558
+ }, settingKey);
559
+ };
560
+
561
+ /** Section = uppercase label, card, optional footnote(s) under it. */
562
+ const renderSection = (label, card, footnotes) => /*#__PURE__*/_jsxs(View, {
563
+ children: [/*#__PURE__*/_jsx(NightSectionLabel, {
564
+ children: label
565
+ }), card, footnotes]
566
+ });
567
+ const storageBadge = storageBackend === "filesystem" ? /*#__PURE__*/_jsx(NightBadge, {
568
+ label: "File System",
569
+ tone: "accent"
570
+ }) : storageBackend === "asyncstorage" ? /*#__PURE__*/_jsx(NightBadge, {
571
+ label: "Async Storage",
572
+ tone: "warning"
573
+ }) : storageBackend === "localstorage" ? /*#__PURE__*/_jsx(NightBadge, {
574
+ label: "Local Storage",
575
+ tone: "accent"
576
+ }) : storageBackend === "memory" ? /*#__PURE__*/_jsx(NightBadge, {
577
+ label: "Memory",
578
+ tone: "danger"
579
+ }) : /*#__PURE__*/_jsx(NightBadge, {
580
+ label: "Loading\u2026",
581
+ tone: "neutral"
582
+ });
583
+ const renderContent = () => /*#__PURE__*/_jsxs(View, {
584
+ style: styles.container,
585
+ children: [/*#__PURE__*/_jsxs(ScrollView, {
586
+ style: styles.scrollContent,
587
+ showsVerticalScrollIndicator: false,
588
+ contentContainerStyle: styles.scrollContainer,
589
+ children: [activeTab === "floating" && /*#__PURE__*/_jsx(_Fragment, {
590
+ children: renderSection("Floating Tools", /*#__PURE__*/_jsx(NightCard, {
591
+ glass: true,
592
+ children: /*#__PURE__*/_jsx(NightRows, {
593
+ children: Object.entries(settings.floatingTools).map(([key, value]) => renderToolRow(key, value, false, () => toggleFloatingTool(key)))
594
+ })
595
+ }), /*#__PURE__*/_jsx(NightFootnote, {
596
+ children: "Tools switched on here appear in the floating bubble row."
597
+ }))
598
+ }), activeTab === "settings" && /*#__PURE__*/_jsxs(_Fragment, {
599
+ children: [renderSection("Background", /*#__PURE__*/_jsx(NightCard, {
600
+ glass: true,
601
+ padded: true,
602
+ children: /*#__PURE__*/_jsx(BackgroundSwitcher, {
603
+ embedded: true
604
+ })
605
+ })), renderSection("Storage", /*#__PURE__*/_jsxs(NightCard, {
606
+ glass: true,
607
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
608
+ activeOpacity: 0.7,
609
+ onPress: () => setIsStorageExpanded(!isStorageExpanded),
610
+ accessibilityLabel: "Storage Type",
611
+ style: styles.settingRow,
612
+ children: [/*#__PURE__*/_jsx(View, {
613
+ style: styles.rowInfo,
614
+ children: /*#__PURE__*/_jsx(Text, {
615
+ style: styles.rowLabel,
616
+ children: "Storage Type"
617
+ })
618
+ }), storageBadge, isStorageExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
619
+ size: 16,
620
+ color: night.color.textTertiary
621
+ }) : /*#__PURE__*/_jsx(ChevronRightIcon, {
622
+ size: 16,
623
+ color: night.color.textTertiary
624
+ })]
625
+ }), isStorageExpanded && /*#__PURE__*/_jsxs(_Fragment, {
626
+ children: [/*#__PURE__*/_jsxs(View, {
627
+ style: styles.settingRowBody,
628
+ children: [/*#__PURE__*/_jsx(Text, {
629
+ style: styles.settingRowBodyText,
630
+ children: storageBackend === "filesystem" ? "Settings persist independently and survive AsyncStorage.clear() calls during logout." : storageBackend === "asyncstorage" ? "Settings may be lost if AsyncStorage is cleared during logout." : storageBackend === "localstorage" ? "Settings are saved in this browser. Clearing site data removes them." : storageBackend === "memory" ? "Settings are stored in memory only and will be lost on app restart." : "Checking storage backend…"
631
+ }), storageBackend === "asyncstorage" && /*#__PURE__*/_jsx(Text, {
632
+ style: [styles.settingRowBodyText, styles.bodyWarning],
633
+ children: "Install expo-file-system to persist settings through logout flows."
634
+ }), storageBackend === "memory" && /*#__PURE__*/_jsx(Text, {
635
+ style: [styles.settingRowBodyText, styles.bodyDanger],
636
+ children: "No persistent storage available \u2014 install expo-file-system (best) or @react-native-async-storage/async-storage."
637
+ })]
638
+ }), /*#__PURE__*/_jsx(NightSeparator, {}), /*#__PURE__*/_jsxs(View, {
639
+ style: styles.storageExpanded,
640
+ children: [/*#__PURE__*/_jsxs(View, {
641
+ style: styles.storageExpandedHeader,
642
+ children: [/*#__PURE__*/_jsx(Text, {
643
+ style: styles.storageExpandedTitle,
644
+ children: "SAVED SETTINGS"
645
+ }), /*#__PURE__*/_jsx(Text, {
646
+ style: styles.storageExpandedCount,
647
+ children: savedKeysLoading ? "…" : `${savedKeys.length} keys`
648
+ })]
649
+ }), savedKeysLoading ? /*#__PURE__*/_jsx(Text, {
650
+ style: styles.storageKeyEmpty,
651
+ children: "Loading\u2026"
652
+ }) : savedKeys.length === 0 ? /*#__PURE__*/_jsx(Text, {
653
+ style: styles.storageKeyEmpty,
654
+ children: "No settings saved yet"
655
+ }) : /*#__PURE__*/_jsx(ScrollView, {
656
+ style: styles.storageKeysList,
657
+ nestedScrollEnabled: true,
658
+ showsVerticalScrollIndicator: true,
659
+ children: savedKeys.map(key => /*#__PURE__*/_jsx(View, {
660
+ style: styles.storageKeyItem,
661
+ children: /*#__PURE__*/_jsx(Text, {
662
+ style: styles.storageKeyText,
663
+ numberOfLines: 1,
664
+ children: key.replace("@react_buoy_", "")
665
+ })
666
+ }, key))
667
+ })]
668
+ }), /*#__PURE__*/_jsx(View, {
669
+ style: styles.cardButtonWrap,
670
+ children: /*#__PURE__*/_jsx(NightButton, {
671
+ label: clearSuccess ? "Cleared" : isClearing ? "Clearing…" : "Clear All Settings",
672
+ variant: clearSuccess ? "secondary" : "destructive",
673
+ size: "md",
674
+ disabled: isClearing,
675
+ onPress: handleClearStorage,
676
+ icon: clearSuccess ? /*#__PURE__*/_jsx(CheckCircle2, {
677
+ size: 15,
678
+ color: night.color.accent
679
+ }) : /*#__PURE__*/_jsx(Trash2, {
680
+ size: 15,
681
+ color: isClearing ? night.color.textTertiary : night.color.danger
682
+ })
683
+ })
684
+ })]
685
+ })]
686
+ })), desktopSyncStatus && desktopSyncStatus.state !== "idle" && renderSection("Desktop Sync", /*#__PURE__*/_jsxs(NightCard, {
687
+ glass: true,
688
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
689
+ activeOpacity: 0.7,
690
+ onPress: () => toggleSettingExpanded("desktopSync"),
691
+ accessibilityLabel: "Buoy Desktop",
692
+ style: styles.settingRow,
693
+ children: [/*#__PURE__*/_jsx(View, {
694
+ style: styles.rowInfo,
695
+ children: /*#__PURE__*/_jsx(Text, {
696
+ style: styles.rowLabel,
697
+ children: "Buoy Desktop"
698
+ })
699
+ }), desktopSyncStatus.state === "connected" ? /*#__PURE__*/_jsx(NightBadge, {
700
+ label: "Connected",
701
+ tone: "accent"
702
+ }) : desktopSyncStatus.state === "connecting" ? /*#__PURE__*/_jsx(NightBadge, {
703
+ label: "Connecting\u2026",
704
+ tone: "neutral"
705
+ }) : /*#__PURE__*/_jsx(NightBadge, {
706
+ label: "Retrying",
707
+ tone: "warning"
708
+ }), expandedSettings.has("desktopSync") ? /*#__PURE__*/_jsx(ChevronDown, {
709
+ size: 16,
710
+ color: night.color.textTertiary
711
+ }) : /*#__PURE__*/_jsx(ChevronRightIcon, {
712
+ size: 16,
713
+ color: night.color.textTertiary
714
+ })]
715
+ }), expandedSettings.has("desktopSync") && /*#__PURE__*/_jsxs(View, {
716
+ style: styles.settingRowBody,
717
+ children: [/*#__PURE__*/_jsxs(Text, {
718
+ style: styles.settingRowBodyText,
719
+ children: [desktopSyncStatus.state === "connected" ? "Streaming tool data to Buoy Desktop at " : "Trying to reach Buoy Desktop at ", /*#__PURE__*/_jsx(Text, {
720
+ style: styles.footnoteMono,
721
+ children: desktopSyncStatus.targetURL ?? "(unknown URL)"
722
+ }), desktopSyncStatus.state !== "connected" && " — is the desktop app running? Override with the externalSync socketURL option if it runs elsewhere."]
723
+ }), desktopSyncStatus.state === "retrying" && desktopSyncStatus.lastError && /*#__PURE__*/_jsxs(Text, {
724
+ style: [styles.settingRowBodyText, styles.bodyWarning],
725
+ children: ["Last error: ", desktopSyncStatus.lastError]
726
+ })]
727
+ })]
728
+ })), renderSection("Modal Behavior", /*#__PURE__*/_jsx(NightCard, {
729
+ glass: true,
730
+ children: /*#__PURE__*/_jsxs(NightRows, {
731
+ children: [renderGlobalSettingRow("enableSharedModalDimensions", "Shared Modal Size", "Sync dimensions across all tools", "When enabled, all tool modals will share the same size and position. Resizing one modal will affect all others. When disabled, each tool remembers its own size and position independently.", "Keep OFF for the best experience. This allows you to customize each tool's modal size separately."), Platform.OS !== "web" ? renderGlobalSettingRow("expandableWindowControls", "Expand Controls", "iPad-style expandable window buttons", "When enabled, the window control buttons (minimize, toggle mode, close) start as small dots and expand into larger, easy-to-tap buttons when pressed. When disabled, buttons are directly tappable at their small size.", "Keep ON for touch devices where the small buttons are hard to press. Turn OFF if you prefer direct single-tap access.") : null]
732
+ })
733
+ }))]
734
+ }), activeTab === "pro" && /*#__PURE__*/_jsxs(_Fragment, {
735
+ children: [renderSection("License", /*#__PURE__*/_jsx(NightCard, {
736
+ glass: true,
737
+ children: /*#__PURE__*/_jsxs(View, {
738
+ style: styles.settingRow,
739
+ children: [/*#__PURE__*/_jsx(View, {
740
+ style: styles.rowInfo,
741
+ children: /*#__PURE__*/_jsx(Text, {
742
+ style: styles.rowLabel,
743
+ children: "License Status"
744
+ })
745
+ }), /*#__PURE__*/_jsx(View, {
746
+ testID: "buoy-settings-license-status",
747
+ accessibilityLabel: isPro ? "License: Active" : "License: Free",
748
+ children: isPro ? /*#__PURE__*/_jsx(NightBadge, {
749
+ label: "Active",
750
+ tone: "accent"
751
+ }) : /*#__PURE__*/_jsx(NightBadge, {
752
+ label: "Free",
753
+ tone: "neutral"
754
+ })
755
+ })]
756
+ })
757
+ }), /*#__PURE__*/_jsx(NightFootnote, {
758
+ children: isPro ? "You have full access to all Buoy DevTools features." : licenseError ? licenseErrorIsTransport ? `Couldn't reach the license server (${licenseError}). A VPN or corporate proxy can block buoy.gg — turn it off and reload. Your key hasn't been rejected.` : `License check failed: ${licenseError}.` : "Upgrade to Pro to unlock all features and support development."
759
+ })), !isPro && /*#__PURE__*/_jsxs(_Fragment, {
760
+ children: [renderSection("Pro Features", /*#__PURE__*/_jsx(NightCard, {
761
+ glass: true,
762
+ children: /*#__PURE__*/_jsx(NightRows, {
763
+ children: ["Advanced Settings", "Priority Support"].map(feature => /*#__PURE__*/_jsxs(View, {
764
+ style: styles.featureRow,
765
+ children: [/*#__PURE__*/_jsx(CheckCircle2, {
766
+ size: 16,
767
+ color: night.color.accent
768
+ }), /*#__PURE__*/_jsx(Text, {
769
+ style: styles.rowLabel,
770
+ children: feature
771
+ })]
772
+ }, feature))
773
+ })
774
+ })), /*#__PURE__*/_jsx(NightButton, {
775
+ label: "Upgrade to Pro",
776
+ variant: "primary",
777
+ size: "lg",
778
+ onPress: () => setShowLicenseModal(true)
779
+ })]
780
+ })]
781
+ })]
782
+ }), /*#__PURE__*/_jsx(LicenseEntryModal, {
783
+ visible: showLicenseModal,
784
+ onClose: () => setShowLicenseModal(false),
785
+ onSuccess: () => setShowLicenseModal(false)
786
+ })]
787
+ });
788
+ return /*#__PURE__*/_jsx(JsModal, {
789
+ visible: visible,
790
+ onClose: onClose,
791
+ zIndex: zIndex,
792
+ variant: "night",
793
+ header: {
794
+ showToggleButton: false,
795
+ customContent: /*#__PURE__*/_jsx(View, {
796
+ style: styles.headerRow,
797
+ children: /*#__PURE__*/_jsx(NightSegmented, {
798
+ glass: true,
799
+ tabs: [{
800
+ key: "floating",
801
+ label: "Floating"
802
+ }, {
803
+ key: "settings",
804
+ label: "Settings"
805
+ }, {
806
+ key: "pro",
807
+ label: "Pro"
808
+ }],
809
+ activeKey: activeTab,
810
+ onChange: tab => setActiveTab(tab)
811
+ })
812
+ })
813
+ },
814
+ initialMode: "bottomSheet",
815
+ onModeChange: handleModeChange,
816
+ persistenceKey: devToolsStorageKeys.settings.root(),
817
+ enablePersistence: true,
818
+ maxHeight: screenHeight - insets.top,
819
+ initialHeight: modalHeight,
820
+ initialFloatingPosition: {
821
+ x: (screenWidth - modalWidth) / 2,
822
+ y: insets.top + 20
823
+ },
824
+ enableGlitchEffects: true,
825
+ children: renderContent()
826
+ });
827
+ };
828
+
829
+ // Basic default settings for the hook (when apps are not available and no defaults configured)
830
+ const basicDefaultSettings = {
831
+ dialTools: {},
832
+ floatingTools: {
833
+ environment: false
834
+ },
835
+ globalSettings: {
836
+ enableSharedModalDimensions: false
837
+ }
838
+ };
839
+
840
+ /**
841
+ * Creates default settings from the team configuration arrays.
842
+ * Used by the hook when no stored settings exist.
843
+ *
844
+ * Note: This function doesn't have access to availableApps, so it can only
845
+ * enable tools that are explicitly listed in the config arrays.
846
+ * For auto-enabling all dial tools, the generateDefaultSettings function
847
+ * (which has access to availableApps) handles that case.
848
+ */
849
+ const createDefaultsFromConfig = (defaultFloatingTools, defaultDialTools) => {
850
+ const floatingSet = new Set(defaultFloatingTools ?? []);
851
+ const dialSet = new Set(defaultDialTools ?? []);
852
+
853
+ // Build dial tools record from defaults
854
+ // Note: If no defaults specified, dialTools will be empty here
855
+ // but generateDefaultSettings will auto-enable all tools when it has access to availableApps
856
+ const dialTools = {};
857
+ for (const id of dialSet) {
858
+ dialTools[id] = true;
859
+ }
860
+
861
+ // Build floating tools record from defaults
862
+ const floatingTools = {
863
+ environment: floatingSet.has('environment')
864
+ };
865
+ for (const id of floatingSet) {
866
+ if (id !== 'environment') {
867
+ floatingTools[id] = true;
868
+ }
869
+ }
870
+ return {
871
+ dialTools: enforceDialLimit(dialTools),
872
+ floatingTools,
873
+ globalSettings: {
874
+ enableSharedModalDimensions: false
875
+ }
876
+ };
877
+ };
878
+
879
+ /**
880
+ * Convenience hook for accessing persisted dev tools settings. Subscribes to the internal
881
+ * event bus so all surfaces stay in sync when the modal saves new preferences.
882
+ *
883
+ * When no saved settings exist, the hook uses the default configuration from
884
+ * the DefaultConfigProvider (if available) to determine initial tool states.
885
+ */
886
+ /**
887
+ * Session cache for the values the settings modal loads asynchronously on mount.
888
+ *
889
+ * Without it every open renders three times: once with defaults, then again as
890
+ * each of the persisted reads (settings, active tab, storage backend) resolves —
891
+ * and each of those re-renders rebuilds the whole tool list. The values cannot
892
+ * change behind our back (writes go through this module, and settingsBus
893
+ * broadcasts them), so after the first open we can render the real values
894
+ * immediately and only re-render if a read actually came back different.
895
+ */
896
+ let cachedSettings = null;
897
+ let cachedActiveTab = null;
898
+ let cachedStorageBackend = null;
899
+
900
+ /** Cheap structural equality — these objects are small and JSON-safe. */
901
+ const sameSettings = (a, b) => JSON.stringify(a) === JSON.stringify(b);
902
+
903
+ // Every surface that changes settings broadcasts on the bus, so listening here
904
+ // keeps the cache authoritative rather than merely last-seen — otherwise a
905
+ // change made while the modal was unmounted would show stale for one frame.
906
+ // Registered for the app's lifetime on purpose; there is nothing to unsubscribe.
907
+ settingsBus.addListener(payload => {
908
+ if (payload) cachedSettings = payload;
909
+ });
910
+ export const useDevToolsSettings = () => {
911
+ // Get team default configuration from context
912
+ const {
913
+ defaultFloatingTools,
914
+ defaultDialTools
915
+ } = useDefaultConfig();
916
+
917
+ // Compute the effective defaults based on team configuration
918
+ const effectiveDefaults = useMemo(() => {
919
+ if (!defaultFloatingTools && !defaultDialTools) {
920
+ return basicDefaultSettings;
921
+ }
922
+ return createDefaultsFromConfig(defaultFloatingTools, defaultDialTools);
923
+ }, [defaultFloatingTools, defaultDialTools]);
924
+ const [settings, setSettings] = useState(effectiveDefaults);
925
+ const loadSettings = useCallback(async () => {
926
+ try {
927
+ const savedSettings = await persistentStorage.getItem(STORAGE_KEY);
928
+ if (savedSettings) {
929
+ const parsed = JSON.parse(savedSettings);
930
+ const merged = mergeWithDefaults(effectiveDefaults, parsed);
931
+ setSettings(merged);
932
+ return;
933
+ }
934
+ // No saved settings - use the effective defaults (including team config)
935
+ setSettings(effectiveDefaults);
936
+ } catch (error) {
937
+ console.error("Failed to load dev tools settings:", error);
938
+ setSettings(effectiveDefaults);
939
+ }
940
+ }, [effectiveDefaults]);
941
+ useEffect(() => {
942
+ loadSettings();
943
+ // Subscribe to settings changes
944
+ const unsub = settingsBus.addListener(payload => {
945
+ try {
946
+ if (payload) {
947
+ setSettings(payload);
948
+ }
949
+ } catch (err) {
950
+ // Listener errors are intentionally swallowed to avoid breaking subscribers
951
+ }
952
+ });
953
+ return () => {
954
+ unsub();
955
+ };
956
+ }, [loadSettings]);
957
+
958
+ // Refresh settings when component using this hook becomes visible
959
+ const refreshSettings = useCallback(() => {
960
+ loadSettings();
961
+ }, [loadSettings]);
962
+ return {
963
+ settings,
964
+ refreshSettings
965
+ };
966
+ };
967
+ const styles = StyleSheet.create({
968
+ container: {
969
+ flex: 1,
970
+ // Transparent so JsModal's night star field shows through everything.
971
+ backgroundColor: "transparent"
972
+ },
973
+ // Header — the segmented control. The window controls are cleared
974
+ // vertically by JsModal's drag-indicator strip, so this runs full width.
975
+ headerRow: {
976
+ flexDirection: "row",
977
+ alignItems: "center",
978
+ paddingLeft: night.gutter,
979
+ paddingRight: night.gutter,
980
+ paddingBottom: 8
981
+ },
982
+ // Scroll content — labeled groups on a 24pt rhythm (Everlights create screen)
983
+ scrollContent: {
984
+ flex: 1
985
+ },
986
+ scrollContainer: {
987
+ paddingHorizontal: night.gutter,
988
+ paddingTop: 8,
989
+ paddingBottom: 24,
990
+ gap: night.groupGap
991
+ },
992
+ // Rows
993
+ toolRow: {
994
+ flexDirection: "row",
995
+ alignItems: "center",
996
+ gap: 12,
997
+ paddingHorizontal: night.rowPadH,
998
+ paddingVertical: 12
999
+ },
1000
+ settingRow: {
1001
+ flexDirection: "row",
1002
+ alignItems: "center",
1003
+ gap: 10,
1004
+ paddingHorizontal: night.rowPadH,
1005
+ paddingVertical: night.rowPadV,
1006
+ minHeight: 52
1007
+ },
1008
+ featureRow: {
1009
+ flexDirection: "row",
1010
+ alignItems: "center",
1011
+ gap: 10,
1012
+ paddingHorizontal: night.rowPadH,
1013
+ paddingVertical: 12
1014
+ },
1015
+ rowDisabled: {
1016
+ opacity: 0.4
1017
+ },
1018
+ toolIconSlot: {
1019
+ width: 24,
1020
+ alignItems: "center"
1021
+ },
1022
+ rowInfo: {
1023
+ flex: 1,
1024
+ gap: 2
1025
+ },
1026
+ rowLabel: {
1027
+ color: night.color.text,
1028
+ fontSize: night.font.row,
1029
+ fontWeight: "500"
1030
+ },
1031
+ rowCaption: {
1032
+ color: night.color.textTertiary,
1033
+ fontSize: night.font.label
1034
+ },
1035
+ // Expanded body under a global-setting row
1036
+ settingRowBody: {
1037
+ paddingHorizontal: night.rowPadH,
1038
+ paddingBottom: night.rowPadV,
1039
+ gap: 6
1040
+ },
1041
+ settingRowBodyText: {
1042
+ color: night.color.textSecondary,
1043
+ fontSize: night.font.caption,
1044
+ lineHeight: night.font.caption * 1.45
1045
+ },
1046
+ settingRowBodyHint: {
1047
+ color: night.color.textTertiary,
1048
+ fontSize: night.font.caption,
1049
+ lineHeight: night.font.caption * 1.45
1050
+ },
1051
+ bodyWarning: {
1052
+ color: night.color.warning
1053
+ },
1054
+ bodyDanger: {
1055
+ color: night.color.danger
1056
+ },
1057
+ // Buttons that live inside a card
1058
+ cardButtonWrap: {
1059
+ paddingHorizontal: night.cardPad,
1060
+ paddingTop: 10,
1061
+ paddingBottom: night.cardPad
1062
+ },
1063
+ // Storage expanded key list
1064
+ storageExpanded: {
1065
+ paddingHorizontal: night.rowPadH,
1066
+ paddingVertical: 12,
1067
+ gap: 8
1068
+ },
1069
+ storageExpandedHeader: {
1070
+ flexDirection: "row",
1071
+ alignItems: "center"
1072
+ },
1073
+ storageExpandedTitle: {
1074
+ flex: 1,
1075
+ color: night.color.textSecondary,
1076
+ fontSize: night.font.micro,
1077
+ fontWeight: "600",
1078
+ letterSpacing: 0.5
1079
+ },
1080
+ storageExpandedCount: {
1081
+ color: night.color.textTertiary,
1082
+ fontSize: night.font.micro
1083
+ },
1084
+ storageKeysList: {
1085
+ maxHeight: 150
1086
+ },
1087
+ storageKeyItem: {
1088
+ backgroundColor: night.color.glassElevated,
1089
+ borderRadius: night.radius.segment,
1090
+ paddingHorizontal: 10,
1091
+ paddingVertical: 7,
1092
+ marginBottom: 4
1093
+ },
1094
+ storageKeyText: {
1095
+ color: night.color.textSecondary,
1096
+ fontSize: night.font.micro,
1097
+ fontFamily: monoFont
1098
+ },
1099
+ storageKeyEmpty: {
1100
+ color: night.color.textTertiary,
1101
+ fontSize: night.font.caption,
1102
+ fontStyle: "italic"
1103
+ },
1104
+ // Footnote inline code
1105
+ footnoteMono: {
1106
+ fontFamily: monoFont,
1107
+ fontSize: night.font.label,
1108
+ color: night.color.textSecondary
1109
+ }
1110
+ });