@buoy-gg/storage 7.0.41 → 7.0.43

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 (180) hide show
  1. package/README.md +11 -90
  2. package/lib/commonjs/index.js +1 -353
  3. package/lib/commonjs/preset.js +1 -85
  4. package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +1 -36
  5. package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +1 -12
  6. package/lib/commonjs/storage/components/DiffViewer/themes/diffThemes.js +1 -24
  7. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +3 -1259
  8. package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -257
  9. package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -419
  10. package/lib/commonjs/storage/components/SelectionActionBar.js +1 -207
  11. package/lib/commonjs/storage/components/StorageActionButtons.js +1 -242
  12. package/lib/commonjs/storage/components/StorageBrowserMode.js +1 -61
  13. package/lib/commonjs/storage/components/StorageEventActionButton.js +1 -121
  14. package/lib/commonjs/storage/components/StorageEventCard.js +1 -118
  15. package/lib/commonjs/storage/components/StorageEventDetailContent.js +1 -494
  16. package/lib/commonjs/storage/components/StorageEventFilterView.js +1 -160
  17. package/lib/commonjs/storage/components/StorageEventsSection.js +1 -24
  18. package/lib/commonjs/storage/components/StorageFilterCards.js +1 -231
  19. package/lib/commonjs/storage/components/StorageKeyCard.js +1 -520
  20. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +1 -579
  21. package/lib/commonjs/storage/components/StorageKeyRow.js +1 -559
  22. package/lib/commonjs/storage/components/StorageKeySection.js +1 -160
  23. package/lib/commonjs/storage/components/StorageKeyStats.js +1 -345
  24. package/lib/commonjs/storage/components/StorageModalWithTabs.js +1 -954
  25. package/lib/commonjs/storage/components/StorageSaveNotice.js +1 -171
  26. package/lib/commonjs/storage/components/StorageSection.js +1 -43
  27. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +1 -144
  28. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +1 -141
  29. package/lib/commonjs/storage/hooks/useMMKVInstances.js +1 -210
  30. package/lib/commonjs/storage/hooks/useMMKVKeys.js +1 -376
  31. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +1 -202
  32. package/lib/commonjs/storage/hooks/useStorageEvents.js +1 -61
  33. package/lib/commonjs/storage/stores/storageEventStore.js +1 -312
  34. package/lib/commonjs/storage/sync/storageSyncAdapter.js +1 -492
  35. package/lib/commonjs/storage/types.js +1 -5
  36. package/lib/commonjs/storage/utils/AsyncStorageListener.js +1 -691
  37. package/lib/commonjs/storage/utils/AsyncStorageListener.web.js +1 -89
  38. package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +1 -203
  39. package/lib/commonjs/storage/utils/MMKVListener.js +1 -416
  40. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +1 -200
  41. package/lib/commonjs/storage/utils/asyncStorageCompat.js +1 -89
  42. package/lib/commonjs/storage/utils/browserStorageLifecycle.js +1 -10
  43. package/lib/commonjs/storage/utils/browserStorageLifecycle.web.js +1 -70
  44. package/lib/commonjs/storage/utils/clearAllStorage.js +1 -48
  45. package/lib/commonjs/storage/utils/deleteStorageKeys.js +1 -82
  46. package/lib/commonjs/storage/utils/lineDiff.js +1 -30
  47. package/lib/commonjs/storage/utils/mmkvAvailability.js +1 -62
  48. package/lib/commonjs/storage/utils/mmkvAvailability.web.js +1 -21
  49. package/lib/commonjs/storage/utils/mmkvTypeDetection.js +1 -154
  50. package/lib/commonjs/storage/utils/requiredStorageKeyScope.js +1 -17
  51. package/lib/commonjs/storage/utils/setStorageValue.js +1 -218
  52. package/lib/commonjs/storage/utils/snapshotProvider.js +1 -382
  53. package/lib/commonjs/storage/utils/storageActionHelpers.js +1 -46
  54. package/lib/commonjs/storage/utils/storageBrowserRefresh.js +1 -49
  55. package/lib/commonjs/storage/utils/storageLabels.js +1 -13
  56. package/lib/commonjs/storage/utils/storageLabels.web.js +1 -13
  57. package/lib/commonjs/storage/utils/storageOps.js +1 -53
  58. package/lib/commonjs/storage/utils/storageQueryUtils.js +1 -35
  59. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +1 -288
  60. package/lib/commonjs/storage/utils/valueType.js +1 -83
  61. package/lib/module/index.js +1 -114
  62. package/lib/module/preset.js +1 -82
  63. package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +1 -32
  64. package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +1 -9
  65. package/lib/module/storage/components/DiffViewer/themes/diffThemes.js +1 -10
  66. package/lib/module/storage/components/GameUIStorageBrowser.js +3 -1257
  67. package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -253
  68. package/lib/module/storage/components/MMKVInstanceSelector.js +1 -415
  69. package/lib/module/storage/components/SelectionActionBar.js +1 -204
  70. package/lib/module/storage/components/StorageActionButtons.js +1 -238
  71. package/lib/module/storage/components/StorageBrowserMode.js +1 -57
  72. package/lib/module/storage/components/StorageEventActionButton.js +1 -118
  73. package/lib/module/storage/components/StorageEventCard.js +1 -113
  74. package/lib/module/storage/components/StorageEventDetailContent.js +1 -490
  75. package/lib/module/storage/components/StorageEventFilterView.js +1 -156
  76. package/lib/module/storage/components/StorageEventsSection.js +1 -20
  77. package/lib/module/storage/components/StorageFilterCards.js +1 -227
  78. package/lib/module/storage/components/StorageKeyCard.js +1 -517
  79. package/lib/module/storage/components/StorageKeyDetailScreen.js +1 -577
  80. package/lib/module/storage/components/StorageKeyRow.js +1 -554
  81. package/lib/module/storage/components/StorageKeySection.js +1 -155
  82. package/lib/module/storage/components/StorageKeyStats.js +1 -342
  83. package/lib/module/storage/components/StorageModalWithTabs.js +1 -949
  84. package/lib/module/storage/components/StorageSaveNotice.js +1 -168
  85. package/lib/module/storage/components/StorageSection.js +1 -40
  86. package/lib/module/storage/components/StorageTypeFilterBar.js +1 -139
  87. package/lib/module/storage/hooks/useAsyncStorageKeys.js +1 -136
  88. package/lib/module/storage/hooks/useMMKVInstances.js +1 -204
  89. package/lib/module/storage/hooks/useMMKVKeys.js +1 -371
  90. package/lib/module/storage/hooks/useSecureStoreKeys.js +1 -199
  91. package/lib/module/storage/hooks/useStorageEvents.js +1 -58
  92. package/lib/module/storage/stores/storageEventStore.js +1 -301
  93. package/lib/module/storage/sync/storageSyncAdapter.js +1 -486
  94. package/lib/module/storage/types.js +1 -3
  95. package/lib/module/storage/utils/AsyncStorageListener.js +1 -679
  96. package/lib/module/storage/utils/AsyncStorageListener.web.js +1 -76
  97. package/lib/module/storage/utils/MMKVInstanceRegistry.js +1 -197
  98. package/lib/module/storage/utils/MMKVListener.js +1 -403
  99. package/lib/module/storage/utils/SecureStoreRegistry.js +1 -194
  100. package/lib/module/storage/utils/asyncStorageCompat.js +1 -81
  101. package/lib/module/storage/utils/browserStorageLifecycle.js +1 -6
  102. package/lib/module/storage/utils/browserStorageLifecycle.web.js +1 -66
  103. package/lib/module/storage/utils/clearAllStorage.js +1 -43
  104. package/lib/module/storage/utils/deleteStorageKeys.js +1 -78
  105. package/lib/module/storage/utils/lineDiff.js +1 -12
  106. package/lib/module/storage/utils/mmkvAvailability.js +1 -56
  107. package/lib/module/storage/utils/mmkvAvailability.web.js +1 -15
  108. package/lib/module/storage/utils/mmkvTypeDetection.js +1 -148
  109. package/lib/module/storage/utils/requiredStorageKeyScope.js +1 -13
  110. package/lib/module/storage/utils/setStorageValue.js +1 -209
  111. package/lib/module/storage/utils/snapshotProvider.js +1 -380
  112. package/lib/module/storage/utils/storageActionHelpers.js +1 -42
  113. package/lib/module/storage/utils/storageBrowserRefresh.js +1 -44
  114. package/lib/module/storage/utils/storageLabels.js +1 -9
  115. package/lib/module/storage/utils/storageLabels.web.js +1 -9
  116. package/lib/module/storage/utils/storageOps.js +1 -46
  117. package/lib/module/storage/utils/storageQueryUtils.js +1 -30
  118. package/lib/module/storage/utils/storageTimeTravelUtils.js +1 -281
  119. package/lib/module/storage/utils/valueType.js +1 -76
  120. package/lib/web/index.mjs +2 -3
  121. package/package.json +6 -10
  122. package/lib/typescript/index.d.ts.map +0 -1
  123. package/lib/typescript/preset.d.ts.map +0 -1
  124. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +0 -1
  125. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +0 -1
  126. package/lib/typescript/storage/components/DiffViewer/themes/diffThemes.d.ts.map +0 -1
  127. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +0 -1
  128. package/lib/typescript/storage/components/MMKVInstanceInfoPanel.d.ts.map +0 -1
  129. package/lib/typescript/storage/components/MMKVInstanceSelector.d.ts.map +0 -1
  130. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +0 -1
  131. package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +0 -1
  132. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +0 -1
  133. package/lib/typescript/storage/components/StorageEventActionButton.d.ts.map +0 -1
  134. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +0 -1
  135. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +0 -1
  136. package/lib/typescript/storage/components/StorageEventFilterView.d.ts.map +0 -1
  137. package/lib/typescript/storage/components/StorageEventsSection.d.ts.map +0 -1
  138. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +0 -1
  139. package/lib/typescript/storage/components/StorageKeyCard.d.ts.map +0 -1
  140. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +0 -1
  141. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +0 -1
  142. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +0 -1
  143. package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +0 -1
  144. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +0 -1
  145. package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +0 -1
  146. package/lib/typescript/storage/components/StorageSection.d.ts.map +0 -1
  147. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +0 -1
  148. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +0 -1
  149. package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +0 -1
  150. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +0 -1
  151. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +0 -1
  152. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +0 -1
  153. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +0 -1
  154. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +0 -1
  155. package/lib/typescript/storage/types.d.ts.map +0 -1
  156. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +0 -1
  157. package/lib/typescript/storage/utils/AsyncStorageListener.web.d.ts.map +0 -1
  158. package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +0 -1
  159. package/lib/typescript/storage/utils/MMKVListener.d.ts.map +0 -1
  160. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +0 -1
  161. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts.map +0 -1
  162. package/lib/typescript/storage/utils/browserStorageLifecycle.d.ts.map +0 -1
  163. package/lib/typescript/storage/utils/browserStorageLifecycle.web.d.ts.map +0 -1
  164. package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +0 -1
  165. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +0 -1
  166. package/lib/typescript/storage/utils/lineDiff.d.ts.map +0 -1
  167. package/lib/typescript/storage/utils/mmkvAvailability.d.ts.map +0 -1
  168. package/lib/typescript/storage/utils/mmkvAvailability.web.d.ts.map +0 -1
  169. package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +0 -1
  170. package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts.map +0 -1
  171. package/lib/typescript/storage/utils/setStorageValue.d.ts.map +0 -1
  172. package/lib/typescript/storage/utils/snapshotProvider.d.ts.map +0 -1
  173. package/lib/typescript/storage/utils/storageActionHelpers.d.ts.map +0 -1
  174. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +0 -1
  175. package/lib/typescript/storage/utils/storageLabels.d.ts.map +0 -1
  176. package/lib/typescript/storage/utils/storageLabels.web.d.ts.map +0 -1
  177. package/lib/typescript/storage/utils/storageOps.d.ts.map +0 -1
  178. package/lib/typescript/storage/utils/storageQueryUtils.d.ts.map +0 -1
  179. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +0 -1
  180. package/lib/typescript/storage/utils/valueType.d.ts.map +0 -1
@@ -1,949 +1 @@
1
- "use strict";
2
-
3
- import { useState, useCallback, useEffect, useRef, useMemo } from "react";
4
- import { View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
5
- import AsyncStorage from "@react-native-async-storage/async-storage";
6
- import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, ToolEmptyState, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps, usePersistedState, monoFont } from "@buoy-gg/shared-ui";
7
- import { StorageBrowserMode } from "./StorageBrowserMode";
8
- import { StorageTypeFilterBar, countByStorageType } from "./StorageTypeFilterBar";
9
- import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
10
- import { ProFeatureBanner } from "@buoy-gg/shared-ui";
11
- import { useStorageEvents } from "../hooks/useStorageEvents";
12
- import { StorageEventDetailContent, StorageEventDetailFooter } from "./StorageEventDetailContent";
13
- import { StorageEventFilterView } from "./StorageEventFilterView";
14
- import { StorageEventCard } from "./StorageEventCard";
15
- // Note: StorageEvent type is now imported from useStorageEvents hook
16
- // which handles both AsyncStorage and MMKV events via the centralized store
17
-
18
- import { useIsPro } from "@buoy-gg/license";
19
-
20
- /**
21
- * @deprecated Read the live limit with `useLimit("storage.events")` — this only
22
- * reports the FREE tier's value and is blind to the anonymous tier. Kept
23
- * because it is part of the package's public exports.
24
- */
25
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
26
- export const FREE_TIER_EVENT_LIMIT = TIER_LIMITS["storage.events"].free;
27
- /**
28
- * Persistence recipes. These keys were always written straight to
29
- * AsyncStorage (not the FileSystem-first persistentStorage), so they keep
30
- * that backend — a user's saved tab, filters and pins survive the upgrade.
31
- */
32
- const ASYNC_STORAGE_BACKEND = {
33
- getItem: key => AsyncStorage.getItem(key),
34
- setItem: (key, value) => AsyncStorage.setItem(key, value)
35
- };
36
- const TAB_PERSISTENCE = {
37
- storage: ASYNC_STORAGE_BACKEND,
38
- // Stored as the bare tab name, not JSON.
39
- parse: raw => raw === "browser" || raw === "events" ? raw : undefined,
40
- serialize: tab => tab
41
- };
42
- const BOOLEAN_PERSISTENCE = {
43
- storage: ASYNC_STORAGE_BACKEND,
44
- // Stored as "true" / "false".
45
- parse: raw => raw === "true",
46
- serialize: value => value.toString()
47
- };
48
- const STRING_SET_PERSISTENCE = {
49
- storage: ASYNC_STORAGE_BACKEND,
50
- parse: raw => new Set(JSON.parse(raw)),
51
- serialize: set => JSON.stringify(Array.from(set))
52
- };
53
- const IGNORED_PATTERNS_PERSISTENCE = {
54
- ...STRING_SET_PERSISTENCE,
55
- parse: raw => {
56
- const filters = JSON.parse(raw);
57
- // Always ensure @react_buoy is included to hide dev tool keys
58
- if (!filters.includes("@react_buoy")) filters.push("@react_buoy");
59
- return new Set(filters);
60
- }
61
- };
62
- export function StorageModalWithTabs({
63
- visible,
64
- onClose,
65
- onBack,
66
- onMinimize,
67
- enableSharedModalDimensions = false,
68
- requiredStorageKeys = [],
69
- inline = false,
70
- onTabChange,
71
- onRefresh,
72
- onEventSelect
73
- }) {
74
- const isPro = useIsPro();
75
- const eventLimit = useLimit("storage.events");
76
- const [activeTab, setActiveTab] = usePersistedState(devToolsStorageKeys.storage.activeTab(), "browser", TAB_PERSISTENCE);
77
- useEffect(() => {
78
- onTabChange?.(activeTab);
79
- }, [activeTab, onTabChange]);
80
-
81
- // Storage Browser state
82
- const [searchQuery, setSearchQuery] = useState("");
83
- /**
84
- * The key whose detail screen (live editor + info) is open. Lives at the
85
- * MODAL level because the modal's header becomes back + the key's name
86
- * while it's open — the React Query interaction, same as the Zustand tool.
87
- */
88
- const [detailKey, setDetailKey] = useState(null);
89
- const [isSearchActive, setIsSearchActive] = useState(false);
90
-
91
- // Local state to control subscription (true = subscribed, false = unsubscribed)
92
- const [isListeningEnabled, setIsListeningEnabled] = usePersistedState(devToolsStorageKeys.storage.isMonitoring(), true, BOOLEAN_PERSISTENCE);
93
-
94
- // Event Listener state - using centralized store via hook
95
- // Pass enabled option to control subscription - this allows events tool to
96
- // continue receiving events while storage UI has toggled off
97
- const {
98
- events,
99
- clearEvents: clearStorageEvents,
100
- isCapturing
101
- } = useStorageEvents({
102
- enabled: isListeningEnabled
103
- });
104
- const [selectedConversationKey, setSelectedConversationKey] = useState(null);
105
- const [selectedHistoryKey, setSelectedHistoryKey] = useState(null);
106
- const [selectedHistoryEventIndex, setSelectedHistoryEventIndex] = useState(null);
107
- const [selectedEventIndex, setSelectedEventIndex] = useState(0);
108
- const [showFilters, setShowFilters] = useState(false);
109
- const [ignoredPatterns, setIgnoredPatterns] = usePersistedState(devToolsStorageKeys.storage.eventFilters(),
110
- // Auto-hide dev tool keys by default
111
- new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]), IGNORED_PATTERNS_PERSISTENCE);
112
- /**
113
- * Which backend both tabs are looking at.
114
- *
115
- * Owned here rather than inside the browser because the Keys and Events tabs
116
- * answer questions about the same three backends — filter the key list to
117
- * Keychain, switch to Events to see what wrote to it, and the filter should
118
- * still be Keychain.
119
- *
120
- * Distinct from `enabledStorageTypes` below: that's the persisted "capture
121
- * this backend at all" setting from the Filters view. This is a view filter,
122
- * and it never hides data the other one already dropped.
123
- */
124
- const [storageTypeFilter, setStorageTypeFilter] = useState("all");
125
- const [enabledStorageTypes, setEnabledStorageTypes] = useState(new Set(['async', 'mmkv', 'secure']) // All enabled by default
126
- );
127
- const [pinnedKeys, setPinnedKeys] = usePersistedState(devToolsStorageKeys.storage.pinnedKeys(), new Set(), STRING_SET_PERSISTENCE);
128
- const lastEventRef = useRef(null);
129
-
130
- // isListening = enabled and capturing (has subscribers)
131
- const isListening = isListeningEnabled && isCapturing;
132
-
133
- // Track the modal's presentation mode so the stepper footer only reserves
134
- // safe-area bottom padding while docked as a bottom sheet. In floating mode
135
- // the footer isn't pinned to the screen edge, so that inset is dead space.
136
- const [modalMode, setModalMode] = useState("bottomSheet");
137
- const handleModeChange = useCallback(mode => {
138
- setModalMode(mode);
139
- }, []);
140
-
141
- // Timer removed - using useTickEveryMinute hook instead
142
-
143
- // Event listener setup - now handled by useStorageEvents hook
144
- // The hook subscribes to the centralized storageEventStore
145
-
146
- const handleToggleListening = useCallback(() => {
147
- // Toggle subscription - this actually subscribes/unsubscribes from the store
148
- setIsListeningEnabled(prev => !prev);
149
- }, []);
150
- const handleClearEvents = useCallback(() => {
151
- clearStorageEvents();
152
- setSelectedConversationKey(null);
153
- setSelectedEventIndex(0);
154
- }, [clearStorageEvents]);
155
- const handleViewHistoryFromBrowser = useCallback(key => {
156
- setSelectedHistoryKey(key);
157
- setSelectedHistoryEventIndex(null);
158
- }, []);
159
- const handleBackFromHistory = useCallback(() => {
160
- setSelectedHistoryKey(null);
161
- setSelectedHistoryEventIndex(null);
162
- }, []);
163
- const handleTogglePattern = useCallback(pattern => {
164
- setIgnoredPatterns(prev => {
165
- const next = new Set(prev);
166
- if (next.has(pattern)) {
167
- next.delete(pattern);
168
- } else {
169
- next.add(pattern);
170
- }
171
- return next;
172
- });
173
- }, []);
174
- const handleAddPattern = useCallback(pattern => {
175
- setIgnoredPatterns(prev => new Set([...prev, pattern]));
176
- }, []);
177
- const handleTogglePin = useCallback(key => {
178
- setPinnedKeys(prev => {
179
- const next = new Set(prev);
180
- if (next.has(key)) {
181
- next.delete(key);
182
- } else {
183
- next.add(key);
184
- }
185
- return next;
186
- });
187
- }, []);
188
- const handleToggleFilters = useCallback(() => {
189
- setShowFilters(!showFilters);
190
- }, [showFilters]);
191
- const handleToggleStorageType = useCallback(type => {
192
- setEnabledStorageTypes(prev => {
193
- const next = new Set(prev);
194
- if (next.has(type)) {
195
- next.delete(type);
196
- } else {
197
- next.add(type);
198
- }
199
- return next;
200
- });
201
- }, []);
202
-
203
- // Storage Browser handlers
204
- const storageDataRef = useRef([]);
205
- const getStorageSnapshot = useCallback(() => {
206
- const keys = storageDataRef.current;
207
- return {
208
- timestamp: new Date().toISOString(),
209
- totalKeys: keys.length,
210
- asyncStorage: keys.filter(k => k.storageType === 'async').reduce((acc, k) => {
211
- acc[k.key] = truncatePayload(k.value);
212
- return acc;
213
- }, {}),
214
- mmkv: keys.filter(k => k.storageType === 'mmkv').reduce((acc, k) => {
215
- const id = k.instanceId || 'default';
216
- if (!acc[id]) acc[id] = {};
217
- acc[id][k.key] = truncatePayload(k.value);
218
- return acc;
219
- }, {}),
220
- secure: keys.filter(k => k.storageType === 'secure').reduce((acc, k) => {
221
- acc[k.key] = truncatePayload(k.value);
222
- return acc;
223
- }, {})
224
- };
225
- }, []);
226
- const handlePurgeStorage = useCallback(async () => {
227
- Alert.alert("Clear Storage", "Choose what to clear:", [{
228
- text: "Cancel",
229
- style: "cancel"
230
- }, {
231
- text: "Clear App Storage",
232
- onPress: async () => {
233
- try {
234
- await clearAllAppStorage();
235
- // Refresh will be handled by GameUIStorageBrowser
236
- } catch (error) {
237
- Alert.alert("Error", "Failed to clear app storage");
238
- }
239
- }
240
- }, {
241
- text: "Clear All (Including Dev Tools)",
242
- style: "destructive",
243
- onPress: async () => {
244
- try {
245
- await clearAllStorageIncludingDevTools();
246
- // Refresh will be handled by GameUIStorageBrowser
247
- } catch (error) {
248
- Alert.alert("Error", "Failed to clear all storage");
249
- }
250
- }
251
- }]);
252
- }, []);
253
- const getValueType = value => {
254
- const parsed = parseValue(value);
255
- if (parsed === null) return "null";
256
- if (parsed === undefined) return "undefined";
257
- if (Array.isArray(parsed)) return "array";
258
- if (typeof parsed === "boolean") return "boolean";
259
- if (typeof parsed === "number") return "number";
260
- if (typeof parsed === "string") return "string";
261
- if (typeof parsed === "object") return "object";
262
- return "undefined";
263
- };
264
-
265
- // Get all unique keys from events AND from AsyncStorage for filter view
266
- const [allStorageKeys, setAllStorageKeys] = useState([]);
267
- useEffect(() => {
268
- const fetchAllKeys = async () => {
269
- try {
270
- const keys = await AsyncStorage.getAllKeys();
271
- setAllStorageKeys([...keys].sort());
272
- } catch (error) {
273
- // Failed to fetch keys
274
- }
275
- };
276
-
277
- // Fetch all keys for both tabs when visible
278
- if (visible) {
279
- fetchAllKeys();
280
- }
281
- }, [visible]);
282
- const allEventKeys = useMemo(() => {
283
- // Combine storage keys with event keys for comprehensive filter view
284
- const keys = new Set(allStorageKeys);
285
- events.forEach(event => {
286
- if (event.data?.key) {
287
- keys.add(event.data.key);
288
- }
289
- });
290
- return Array.from(keys).sort();
291
- }, [events, allStorageKeys]);
292
-
293
- // Group events by key and create conversations
294
- const conversations = useMemo(() => {
295
- const keyMap = new Map();
296
- events.forEach(event => {
297
- if (!event.data?.key) return;
298
- const key = event.data.key;
299
-
300
- // Filter by enabled storage types
301
- if (!enabledStorageTypes.has(event.storageType)) return;
302
-
303
- // Filter out keys that match ignored patterns
304
- const shouldIgnore = Array.from(ignoredPatterns).some(pattern => key.includes(pattern));
305
- if (shouldIgnore) return;
306
- const existing = keyMap.get(key);
307
- if (!existing) {
308
- keyMap.set(key, {
309
- key,
310
- lastEvent: event,
311
- events: [event],
312
- totalOperations: 1,
313
- currentValue: event.data.value,
314
- valueType: getValueType(event.data.value),
315
- storageTypes: new Set([event.storageType])
316
- });
317
- } else {
318
- existing.events.push(event);
319
- existing.totalOperations++;
320
- existing.storageTypes.add(event.storageType);
321
-
322
- // Update last event if this one is newer
323
- if (event.timestamp > existing.lastEvent.timestamp) {
324
- existing.lastEvent = event;
325
- existing.currentValue = event.data.value;
326
- existing.valueType = getValueType(event.data.value);
327
- }
328
- }
329
- });
330
-
331
- // Convert to array and sort by last updated
332
- return Array.from(keyMap.values()).sort((a, b) => b.lastEvent.timestamp.getTime() - a.lastEvent.timestamp.getTime());
333
- }, [events, ignoredPatterns, enabledStorageTypes]);
334
-
335
- // Get the live selected conversation from the current conversations array
336
- const selectedConversation = useMemo(() => {
337
- if (!selectedConversationKey) return null;
338
- return conversations.find(c => c.key === selectedConversationKey) || null;
339
- }, [selectedConversationKey, conversations]);
340
-
341
- // Event count per key for storage browser badges
342
- const eventCountByKey = useMemo(() => {
343
- const counts = {};
344
- conversations.forEach(conv => {
345
- counts[conv.key] = conv.totalOperations;
346
- });
347
- return counts;
348
- }, [conversations]);
349
-
350
- /**
351
- * When each key was last written, for the wide table's "Last write" column.
352
- * Derived from the recorded event stream, NOT from the key read: a storage
353
- * read has no timestamp of its own, so a key we never witnessed being written
354
- * has no honest answer here and is simply absent.
355
- */
356
- const lastWriteByKey = useMemo(() => {
357
- const at = {};
358
- conversations.forEach(conv => {
359
- at[conv.key] = conv.lastEvent.timestamp.getTime();
360
- });
361
- return at;
362
- }, [conversations]);
363
-
364
- // Selected history conversation (from browser tab "view history")
365
- const selectedHistoryConversation = useMemo(() => {
366
- if (!selectedHistoryKey) return null;
367
- return conversations.find(c => c.key === selectedHistoryKey) || null;
368
- }, [selectedHistoryKey, conversations]);
369
-
370
- // Wire-form snapshots omit oversized values. Tell a remote host which
371
- // event(s) are on screen so it can fetch them — selected + previous, because
372
- // the detail pane diffs those two.
373
- useEffect(() => {
374
- if (!onEventSelect) return;
375
- const conversation = selectedConversation ? selectedConversation : selectedHistoryEventIndex !== null ? selectedHistoryConversation : null;
376
- const index = selectedConversation ? selectedEventIndex : selectedHistoryEventIndex;
377
- if (!conversation || index == null) return;
378
- const sorted = [...conversation.events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
379
- const selected = sorted[index];
380
- const previous = sorted[index - 1];
381
- if (selected) onEventSelect(selected);
382
- if (previous) onEventSelect(previous);
383
- }, [onEventSelect, selectedConversation, selectedEventIndex, selectedHistoryConversation, selectedHistoryEventIndex]);
384
-
385
- /**
386
- * Events passing everything EXCEPT the backend pills.
387
- *
388
- * Split out so the pills can show what each one would reveal. Counting the
389
- * already-filtered list would zero out the three types you didn't pick, which
390
- * tells you nothing about where to look next.
391
- */
392
- const eventsBeforeTypeFilter = useMemo(() => {
393
- return events.filter(event => {
394
- if (!event.data?.key) return false;
395
- if (!enabledStorageTypes.has(event.storageType)) return false;
396
- const key = event.data.key;
397
- return !Array.from(ignoredPatterns).some(p => key.includes(p));
398
- });
399
- }, [events, ignoredPatterns, enabledStorageTypes]);
400
- const eventTypeCounts = useMemo(() => countByStorageType(eventsBeforeTypeFilter), [eventsBeforeTypeFilter]);
401
-
402
- // Filtered raw events for the events tab (chronological stream, newest-first)
403
- const filteredEvents = useMemo(() => {
404
- if (storageTypeFilter === "all") return eventsBeforeTypeFilter;
405
- return eventsBeforeTypeFilter.filter(event => event.storageType === storageTypeFilter);
406
- }, [eventsBeforeTypeFilter, storageTypeFilter]);
407
- const getEventsSnapshot = useCallback(() => {
408
- return filteredEvents.map(event => ({
409
- key: event.data?.key,
410
- action: event.action,
411
- value: truncatePayload(event.data?.value),
412
- storageType: event.storageType,
413
- timestamp: event.timestamp.toISOString()
414
- }));
415
- }, [filteredEvents]);
416
-
417
- // Pro is Infinity here, so slice() returns everything and the locked count
418
- // floors at 0 — no tier branching needed at the call site.
419
- const visibleEvents = useMemo(() => filteredEvents.slice(0, eventLimit), [filteredEvents, eventLimit]);
420
- const lockedEventCount = useMemo(() => Math.max(0, filteredEvents.length - eventLimit), [filteredEvents.length, eventLimit]);
421
-
422
- // FlatList optimization constants
423
- const END_REACHED_THRESHOLD = 0.8;
424
-
425
- // keyExtractor for raw event FlatList
426
- const eventKeyExtractor = useCallback(item => item.id, []);
427
-
428
- // When a raw event card is pressed, open that key's conversation detail at the matching event
429
- const handleRawEventPress = useCallback(item => {
430
- const conv = conversations.find(c => c.key === item.data?.key);
431
- if (!conv) return;
432
- const sortedEvents = [...conv.events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
433
- const eventIndex = sortedEvents.findIndex(e => e.timestamp.getTime() === item.timestamp.getTime());
434
- setSelectedConversationKey(conv.key);
435
- setSelectedEventIndex(eventIndex >= 0 ? eventIndex : 0);
436
- }, [conversations]);
437
-
438
- // Render a single raw event card
439
- const renderEventItem = useCallback(({
440
- item
441
- }) => {
442
- const cardData = {
443
- key: item.data?.key || "",
444
- lastAction: item.action,
445
- totalOperations: 1,
446
- lastEventTimestamp: item.timestamp,
447
- storageTypes: new Set([item.storageType]),
448
- valueType: getValueType(item.data?.value)
449
- };
450
- return /*#__PURE__*/_jsx(StorageEventCard, {
451
- data: cardData,
452
- onPress: () => handleRawEventPress(item),
453
- testID: `buoy-storage-event-${item.id}`
454
- });
455
- }, [handleRawEventPress]);
456
- if (!visible) return null;
457
- const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.storage.modal();
458
- const renderContent = () => {
459
- // Filters overlay — applies to both tabs
460
- if (showFilters) {
461
- return /*#__PURE__*/_jsx(StorageEventFilterView, {
462
- ignoredPatterns: ignoredPatterns,
463
- onTogglePattern: handleTogglePattern,
464
- onAddPattern: handleAddPattern,
465
- availableKeys: allEventKeys,
466
- enabledStorageTypes: enabledStorageTypes,
467
- onToggleStorageType: handleToggleStorageType
468
- });
469
- }
470
- if (activeTab === "browser") {
471
- // History flow for a specific key (from "view history" in browser tab)
472
- if (selectedHistoryKey && selectedHistoryConversation) {
473
- // Level 2: detail view for a specific event
474
- if (selectedHistoryEventIndex !== null) {
475
- return /*#__PURE__*/_jsx(View, {
476
- style: styles.contentWrapper,
477
- children: /*#__PURE__*/_jsx(StorageEventDetailContent, {
478
- conversation: selectedHistoryConversation,
479
- selectedEventIndex: selectedHistoryEventIndex,
480
- onEventIndexChange: setSelectedHistoryEventIndex,
481
- disableInternalFooter: true
482
- })
483
- });
484
- }
485
-
486
- // Level 1: event list for this key (newest-first)
487
- const ascSortedEvents = [...selectedHistoryConversation.events].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
488
- const newestFirstEvents = [...ascSortedEvents].reverse();
489
- return /*#__PURE__*/_jsx(FlatList, {
490
- testID: "buoy-storage-history-list",
491
- style: styles.flexFill,
492
- data: newestFirstEvents,
493
- keyExtractor: item => item.id,
494
- renderItem: ({
495
- item
496
- }) => {
497
- const ascIdx = ascSortedEvents.findIndex(e => e.timestamp.getTime() === item.timestamp.getTime());
498
- const cardData = {
499
- key: item.data?.key || "",
500
- lastAction: item.action,
501
- totalOperations: 1,
502
- lastEventTimestamp: item.timestamp,
503
- storageTypes: new Set([item.storageType]),
504
- valueType: getValueType(item.data?.value)
505
- };
506
- return /*#__PURE__*/_jsx(StorageEventCard, {
507
- data: cardData,
508
- onPress: () => setSelectedHistoryEventIndex(ascIdx >= 0 ? ascIdx : 0),
509
- testID: `buoy-storage-history-event-${item.id}`
510
- });
511
- },
512
- contentContainerStyle: styles.listContent,
513
- ItemSeparatorComponent: () => /*#__PURE__*/_jsx(View, {
514
- style: styles.separator
515
- }),
516
- initialNumToRender: 20
517
- });
518
- }
519
- return /*#__PURE__*/_jsx(StorageBrowserMode, {
520
- requiredStorageKeys: requiredStorageKeys,
521
- ignoredPatterns: ignoredPatterns,
522
- onTogglePattern: handleTogglePattern,
523
- onAddPattern: handleAddPattern,
524
- searchQuery: searchQuery,
525
- storageDataRef: storageDataRef,
526
- eventCountByKey: eventCountByKey,
527
- lastWriteByKey: lastWriteByKey,
528
- onViewHistory: handleViewHistoryFromBrowser,
529
- enabledStorageTypes: enabledStorageTypes,
530
- pinnedKeys: pinnedKeys,
531
- onTogglePin: handleTogglePin
532
- // Only a docked sheet sits on the screen's bottom edge; a floating
533
- // window's own bottom is well above the home indicator.
534
- ,
535
- applySafeAreaInset: modalMode === "bottomSheet",
536
- storageTypeFilter: storageTypeFilter,
537
- onStorageTypeFilterChange: setStorageTypeFilter,
538
- detailKey: detailKey,
539
- onOpenKey: setDetailKey,
540
- onCloseDetail: () => setDetailKey(null)
541
- });
542
- }
543
-
544
- // Events tab content
545
- if (selectedConversation) {
546
- return /*#__PURE__*/_jsx(View, {
547
- style: styles.contentWrapper,
548
- children: /*#__PURE__*/_jsx(StorageEventDetailContent, {
549
- conversation: selectedConversation,
550
- selectedEventIndex: selectedEventIndex,
551
- onEventIndexChange: setSelectedEventIndex,
552
- disableInternalFooter: true
553
- })
554
- });
555
- }
556
-
557
- // The pills render ABOVE the empty state as well as the list. Filtering to
558
- // a backend that happens to have no events would otherwise hide the only
559
- // control that undoes it, leaving the tab looking permanently broken.
560
- const eventTypeFilterBar = /*#__PURE__*/_jsx(View, {
561
- style: styles.eventFilterBar,
562
- children: /*#__PURE__*/_jsx(StorageTypeFilterBar, {
563
- active: storageTypeFilter,
564
- onChange: setStorageTypeFilter,
565
- counts: eventTypeCounts
566
- })
567
- });
568
- if (filteredEvents.length === 0) {
569
- const filtered = storageTypeFilter !== "all";
570
- return /*#__PURE__*/_jsxs(View, {
571
- style: styles.flexFill,
572
- children: [eventTypeFilterBar, /*#__PURE__*/_jsx(ToolEmptyState, {
573
- testID: "buoy-storage-events-empty"
574
- // Three literal causes, no search on this tab — so it renders
575
- // through the shared shell without the shared resolver. The
576
- // backend pills need their own sentence naming the pill that
577
- // undoes them, which the generic "Filtered Out" would drop.
578
- ,
579
- ...(!isListening ? {
580
- otto: "paused",
581
- title: "Event listener is paused",
582
- message: "Press play to start monitoring"
583
- } : filtered ? {
584
- otto: "filtering",
585
- title: `No ${storageTypeFilter} events`,
586
- message: "Other backends have events — pick All Types to see them"
587
- } : {
588
- otto: "listening",
589
- title: "No storage events yet",
590
- message: "Storage operations will appear here"
591
- })
592
- })]
593
- });
594
- }
595
- return /*#__PURE__*/_jsxs(View, {
596
- style: styles.flexFill,
597
- children: [eventTypeFilterBar, /*#__PURE__*/_jsx(FlatList, {
598
- testID: "buoy-storage-events-list",
599
- style: styles.flexFill,
600
- data: visibleEvents,
601
- renderItem: renderEventItem,
602
- keyExtractor: eventKeyExtractor,
603
- onEndReachedThreshold: END_REACHED_THRESHOLD,
604
- contentContainerStyle: styles.listContent,
605
- ListFooterComponent: lockedEventCount > 0 ? /*#__PURE__*/_jsx(View, {
606
- style: styles.lockedBannerContainer,
607
- children: /*#__PURE__*/_jsx(ProFeatureBanner, {
608
- featureName: `${lockedEventCount} event${lockedEventCount > 1 ? 's' : ''} locked`,
609
- description: "Upgrade to Pro to unlock full event history"
610
- })
611
- }) : null,
612
- initialNumToRender: 10,
613
- maxToRenderPerBatch: 10,
614
- windowSize: 10
615
- })]
616
- });
617
- };
618
-
619
- // The stepper renders nothing for a single-event conversation
620
- // (EventStepperFooter bails at totalItems <= 1), but an element is truthy
621
- // either way — so `footerHeight={footerNode ? 68 : 0}` below used to reserve
622
- // 68px for a footer that drew nothing, leaving a dead band under the content.
623
- // Gate the node on the event count so the reserve matches what's painted.
624
- const footerNode = selectedConversation && selectedConversation.events.length > 1 ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
625
- conversation: selectedConversation,
626
- selectedEventIndex: selectedEventIndex,
627
- onEventIndexChange: setSelectedEventIndex,
628
- applySafeAreaInset: modalMode === "bottomSheet"
629
- }) : selectedHistoryConversation && selectedHistoryEventIndex !== null && selectedHistoryConversation.events.length > 1 ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
630
- conversation: selectedHistoryConversation,
631
- selectedEventIndex: selectedHistoryEventIndex,
632
- onEventIndexChange: setSelectedHistoryEventIndex,
633
- applySafeAreaInset: modalMode === "bottomSheet"
634
- }) : null;
635
-
636
- // Determine the appropriate back handler based on current view state
637
- const currentBackHandler = showFilters ? () => setShowFilters(false) : selectedHistoryEventIndex !== null ? () => setSelectedHistoryEventIndex(null) : selectedHistoryKey ? handleBackFromHistory : selectedConversation ? () => {
638
- setSelectedConversationKey(null);
639
- setSelectedEventIndex(0);
640
- } : onBack;
641
- const headerContent = showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
642
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
643
- onBack: () => setShowFilters(false)
644
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
645
- title: "Filters"
646
- })]
647
- }) : selectedHistoryKey ? /*#__PURE__*/_jsxs(ModalHeader, {
648
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
649
- onBack: selectedHistoryEventIndex !== null ? () => setSelectedHistoryEventIndex(null) : handleBackFromHistory
650
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
651
- title: `${selectedHistoryKey} History`,
652
- centered: true
653
- })]
654
- }) : selectedConversation ? /*#__PURE__*/_jsxs(ModalHeader, {
655
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
656
- onBack: () => {
657
- setSelectedConversationKey(null);
658
- setSelectedEventIndex(0);
659
- }
660
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
661
- title: selectedConversation.key
662
- })]
663
- }) : detailKey ?
664
- /*#__PURE__*/
665
- // The key detail screen — back + the key's name, the React Query
666
- // interaction. AFTER history in this chain: history is reached FROM
667
- // the detail and stacks over it, so its back returns here.
668
- _jsxs(ModalHeader, {
669
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
670
- onBack: () => setDetailKey(null)
671
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
672
- title: detailKey.key,
673
- centered: true,
674
- testID: "buoy-storage-detail-title"
675
- })]
676
- }) : /*#__PURE__*/_jsxs(ModalHeader, {
677
- children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
678
- onBack: onBack
679
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
680
- title: "",
681
- children: isSearchActive ? /*#__PURE__*/_jsx(SearchBar, {
682
- value: searchQuery,
683
- onChange: setSearchQuery,
684
- onClear: () => {
685
- setSearchQuery("");
686
- setIsSearchActive(false);
687
- },
688
- placeholder: "Search storage keys...",
689
- testID: "buoy-storage-search",
690
- autoFocus: true,
691
- onSubmitEditing: () => setIsSearchActive(false),
692
- returnKeyType: "search",
693
- containerStyle: styles.headerSearchContainer
694
- }) : /*#__PURE__*/_jsx(TabSelector, {
695
- tabs: [{
696
- key: "browser",
697
- label: "Storage"
698
- }, {
699
- key: "events",
700
- label: `Events${filteredEvents.length > 0 ? ` (${filteredEvents.length})` : ""}`
701
- }],
702
- activeTab: activeTab,
703
- testIDPrefix: "buoy-storage-tab",
704
- onTabChange: tab => {
705
- setActiveTab(tab);
706
- setSelectedHistoryKey(null);
707
- setSelectedHistoryEventIndex(null);
708
- setSelectedConversationKey(null);
709
- setSelectedEventIndex(0);
710
- setDetailKey(null);
711
- }
712
- })
713
- }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
714
- children: [onRefresh && !isSearchActive ? /*#__PURE__*/_jsx(TouchableOpacity, {
715
- onPress: onRefresh,
716
- style: styles.iconButton,
717
- testID: "buoy-storage-refresh",
718
- accessibilityLabel: "Refresh storage",
719
- accessibilityRole: "button",
720
- children: /*#__PURE__*/_jsx(RefreshCw, {
721
- size: 14,
722
- color: night.color.textSecondary
723
- })
724
- }) : null, /*#__PURE__*/_jsx(TouchableOpacity, {
725
- onPress: handleToggleFilters,
726
- testID: "buoy-storage-filters",
727
- accessibilityLabel: "Storage filters",
728
- accessibilityRole: "button",
729
- style: [styles.iconButton, (ignoredPatterns.size > 0 || enabledStorageTypes.size < 3) && styles.activeFilterButton],
730
- children: /*#__PURE__*/_jsx(Filter, {
731
- size: 14,
732
- color: ignoredPatterns.size > 0 || enabledStorageTypes.size < 3 ? night.color.accent : night.color.textSecondary
733
- })
734
- }), activeTab === "browser" && !isSearchActive && /*#__PURE__*/_jsxs(_Fragment, {
735
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
736
- onPress: () => setIsSearchActive(true),
737
- testID: "buoy-storage-search-open",
738
- accessibilityLabel: "Open search",
739
- accessibilityRole: "button",
740
- style: styles.iconButton,
741
- children: /*#__PURE__*/_jsx(Search, {
742
- size: 14,
743
- color: night.color.textSecondary
744
- })
745
- }), /*#__PURE__*/_jsx(ToolbarCopyButton, {
746
- value: getStorageSnapshot,
747
- disabled: storageDataRef.current.length === 0,
748
- buttonStyle: styles.iconButton,
749
- testID: "buoy-storage-copy"
750
- })]
751
- }), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
752
- children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
753
- value: getEventsSnapshot,
754
- disabled: filteredEvents.length === 0,
755
- buttonStyle: styles.iconButton,
756
- testID: "buoy-storage-events-copy"
757
- }), /*#__PURE__*/_jsx(PowerToggleButton, {
758
- isEnabled: isListening,
759
- onToggle: handleToggleListening,
760
- accessibilityLabel: "Toggle storage event monitoring",
761
- testID: "buoy-storage-power"
762
- }), /*#__PURE__*/_jsx(ToolbarClearButton, {
763
- onPress: handleClearEvents,
764
- buttonStyle: styles.iconButton,
765
- testID: "buoy-storage-clear-events"
766
- })]
767
- })]
768
- })]
769
- });
770
-
771
- // The ONLY difference between the phone and the desktop dashboard is which
772
- // shell the header, body and footer go in. Everything above this is shared.
773
- if (inline) {
774
- return /*#__PURE__*/_jsx(TickProvider, {
775
- children: /*#__PURE__*/_jsxs(View, {
776
- style: styles.inlineRoot,
777
- children: [/*#__PURE__*/_jsx(View, {
778
- style: styles.inlineHeader,
779
- ...windowDragProps,
780
- children: headerContent
781
- }), renderContent(), footerNode]
782
- })
783
- });
784
- }
785
- return /*#__PURE__*/_jsx(TickProvider, {
786
- children: /*#__PURE__*/_jsx(JsModal, {
787
- visible: visible,
788
- onClose: onClose,
789
- onBack: currentBackHandler,
790
- onMinimize: onMinimize,
791
- persistenceKey: persistenceKey,
792
- header: {
793
- showToggleButton: true,
794
- customContent: headerContent
795
- },
796
- onModeChange: handleModeChange,
797
- enablePersistence: true,
798
- initialMode: "bottomSheet",
799
- enableGlitchEffects: true,
800
- styles: {},
801
- footer: footerNode,
802
- footerHeight: footerNode ? 68 : 0,
803
- disableScrollWrapper: true,
804
- children: renderContent()
805
- })
806
- });
807
- }
808
-
809
- /**
810
- * The same storage tool as `StorageModalWithTabs`, rendered INLINE for hosts
811
- * that place tools in a panel rather than a modal. Exists so there is exactly
812
- * one implementation — the desktop dashboard used to ship its own copy of this
813
- * component's header, tabs and screen router.
814
- */
815
- export function StorageToolView(props = {}) {
816
- return /*#__PURE__*/_jsx(StorageModalWithTabs, {
817
- ...props,
818
- inline: true,
819
- visible: true,
820
- onClose: () => {}
821
- });
822
- }
823
- const styles = StyleSheet.create({
824
- inlineRoot: {
825
- flex: 1,
826
- minHeight: 0,
827
- // Transparent so the desktop workbench's hoisted sky (one field spanning
828
- // sidebar + panel) shows through — the shell paints its base beneath it.
829
- backgroundColor: "transparent"
830
- },
831
- inlineHeader: {
832
- paddingHorizontal: 12,
833
- paddingVertical: 8,
834
- borderBottomWidth: 1,
835
- borderBottomColor: macOSColors.border.default,
836
- // Clear, like Bench — the host's hoisted sky shows through the header.
837
- backgroundColor: "transparent"
838
- },
839
- headerSearchContainer: {
840
- flex: 1
841
- },
842
- flexFill: {
843
- flex: 1
844
- },
845
- // Night chrome — the header is glass over the star field, so the action
846
- // chips sit on the Everlights dark-glass fill.
847
- iconButton: {
848
- width: 32,
849
- height: 32,
850
- borderRadius: 8,
851
- backgroundColor: night.color.buttonSurface,
852
- borderWidth: 1,
853
- borderColor: night.color.border,
854
- alignItems: "center",
855
- justifyContent: "center"
856
- },
857
- activeFilterButton: {
858
- backgroundColor: night.color.accentSoft,
859
- borderColor: night.color.accentBorderStrong
860
- },
861
- separator: {
862
- height: 8
863
- },
864
- listContent: {
865
- paddingVertical: 6
866
- },
867
- lockedBannerContainer: {
868
- marginTop: 8,
869
- marginHorizontal: 16
870
- },
871
- /** Gutter matching the event cards below it, so the pills line up with them. */
872
- eventFilterBar: {
873
- paddingHorizontal: 12,
874
- paddingTop: 8,
875
- paddingBottom: 4
876
- },
877
- emptyState: {
878
- flex: 1,
879
- justifyContent: "center",
880
- alignItems: "center",
881
- padding: 32
882
- },
883
- emptyTitle: {
884
- color: night.color.text,
885
- fontSize: 16,
886
- fontWeight: "600",
887
- marginTop: 16,
888
- marginBottom: 8,
889
- fontFamily: monoFont,
890
- letterSpacing: 0.5,
891
- textTransform: "uppercase"
892
- },
893
- emptySubtitle: {
894
- color: night.color.textSecondary,
895
- fontSize: 14,
896
- textAlign: "center",
897
- fontFamily: monoFont
898
- },
899
- eventNavigation: {
900
- flexDirection: "row",
901
- alignItems: "center",
902
- gap: 8,
903
- paddingHorizontal: 8
904
- },
905
- navButton: {
906
- padding: 4,
907
- borderRadius: 4
908
- },
909
- navButtonDisabled: {
910
- opacity: 0.3
911
- },
912
- eventCounter: {
913
- color: night.color.text,
914
- fontSize: 12,
915
- fontWeight: "600",
916
- fontFamily: monoFont,
917
- paddingHorizontal: 8
918
- },
919
- headerTopRow: {
920
- flexDirection: "row",
921
- alignItems: "center",
922
- gap: 8,
923
- marginBottom: 12
924
- },
925
- keyNameContainer: {
926
- flex: 1,
927
- flexDirection: "row",
928
- alignItems: "center",
929
- gap: 6,
930
- backgroundColor: night.color.surfaceElevated,
931
- paddingHorizontal: 10,
932
- paddingVertical: 4,
933
- borderRadius: 6,
934
- borderWidth: 1,
935
- borderColor: night.color.border,
936
- height: 28
937
- },
938
- keyNameText: {
939
- flex: 1,
940
- fontSize: 13,
941
- fontWeight: "600",
942
- color: night.color.accent,
943
- fontFamily: monoFont,
944
- letterSpacing: 0.5
945
- },
946
- contentWrapper: {
947
- flex: 1
948
- }
949
- });
1
+ "use strict";import{useState as g,useCallback as i,useEffect as ee,useRef as be,useMemo as y}from"react";import{View as S,TouchableOpacity as te,StyleSheet as lt,FlatList as Ee,Alert as ne}from"react-native";import oe from"@react-native-async-storage/async-storage";import{JsModal as it,ModalHeader as c,TabSelector as ct,parseValue as dt,devToolsStorageKeys as P,macOSColors as ut,night as d,ToolEmptyState as gt,Filter as yt,RefreshCw as pt,Search as mt,SearchBar as ft,PowerToggleButton as ht,ToolbarCopyButton as Ce,ToolbarClearButton as St,truncatePayload as _,TickProvider as Ie,useLimit as vt,TIER_LIMITS as Tt,windowDragProps as bt,usePersistedState as M,monoFont as L}from"@buoy-gg/shared-ui";import{StorageBrowserMode as Et}from"./StorageBrowserMode";import{StorageTypeFilterBar as Ct,countByStorageType as It}from"./StorageTypeFilterBar";import{clearAllAppStorage as xt,clearAllStorageIncludingDevTools as kt}from"../utils/clearAllStorage";import{ProFeatureBanner as wt}from"@buoy-gg/shared-ui";import{useStorageEvents as Bt}from"../hooks/useStorageEvents";import{StorageEventDetailContent as xe,StorageEventDetailFooter as ke}from"./StorageEventDetailContent";import{StorageEventFilterView as Ft}from"./StorageEventFilterView";import{StorageEventCard as we}from"./StorageEventCard";import{useIsPro as Pt}from"@buoy-gg/license";import{jsx as n,jsxs as p,Fragment as Be}from"react/jsx-runtime";export const FREE_TIER_EVENT_LIMIT=Tt["storage.events"].free;const re={getItem:r=>oe.getItem(r),setItem:(r,C)=>oe.setItem(r,C)};const At={storage:re,parse:r=>r==="browser"||r==="events"?r:void 0,serialize:r=>r};const Dt={storage:re,parse:r=>r==="true",serialize:r=>r.toString()};const Fe={storage:re,parse:r=>new Set(JSON.parse(r)),serialize:r=>JSON.stringify(Array.from(r))};const Rt={...Fe,parse:r=>{const C=JSON.parse(r);if(!C.includes("@react_buoy"))C.push("@react_buoy");return new Set(C)}};export function StorageModalWithTabs({visible:r,onClose:C,onBack:V,onMinimize:Pe,enableSharedModalDimensions:Ae=false,requiredStorageKeys:De=[],inline:Re=false,onTabChange:ae,onRefresh:se,onEventSelect:K}){const Kt=Pt();const N=vt("storage.events");const[B,Ke]=M(P.storage.activeTab(),"browser",At);ee(()=>{ae?.(B)},[B,ae]);const[le,ie]=g("");const[W,H]=g(null);const[j,$]=g(false);const[ce,Ne]=M(P.storage.isMonitoring(),true,Dt);const{events:F,clearEvents:de,isCapturing:He}=Bt({enabled:ce});const[G,A]=g(null);const[I,J]=g(null);const[h,v]=g(null);const[z,x]=g(0);const[D,Q]=g(false);const[T,ue]=M(P.storage.eventFilters(),new Set(["@react_buoy","@RNAsyncStorage","redux-persist","persist:"]),Rt);const[k,ge]=g("all");const[b,ze]=g(new Set(["async","mmkv","secure"]));const[Oe,_e]=M(P.storage.pinnedKeys(),new Set,Fe);const Nt=be(null);const ye=ce&&He;const[q,Me]=g("bottomSheet");const Le=i(e=>{Me(e)},[]);const Ve=i(()=>{Ne(e=>!e)},[]);const We=i(()=>{de();A(null);x(0)},[de]);const je=i(e=>{J(e);v(null)},[]);const pe=i(()=>{J(null);v(null)},[]);const me=i(e=>{ue(t=>{const o=new Set(t);if(o.has(e)){o.delete(e)}else{o.add(e)}return o})},[]);const fe=i(e=>{ue(t=>new Set([...t,e]))},[]);const $e=i(e=>{_e(t=>{const o=new Set(t);if(o.has(e)){o.delete(e)}else{o.add(e)}return o})},[]);const Ge=i(()=>{Q(!D)},[D]);const Je=i(e=>{ze(t=>{const o=new Set(t);if(o.has(e)){o.delete(e)}else{o.add(e)}return o})},[]);const U=be([]);const Qe=i(()=>{const e=U.current;return{timestamp:new Date().toISOString(),totalKeys:e.length,asyncStorage:e.filter(t=>t.storageType==="async").reduce((t,o)=>{t[o.key]=_(o.value);return t},{}),mmkv:e.filter(t=>t.storageType==="mmkv").reduce((t,o)=>{const a=o.instanceId||"default";if(!t[a])t[a]={};t[a][o.key]=_(o.value);return t},{}),secure:e.filter(t=>t.storageType==="secure").reduce((t,o)=>{t[o.key]=_(o.value);return t},{})}},[]);const Ht=i(async()=>{ne.alert("Clear Storage","Choose what to clear:",[{text:"Cancel",style:"cancel"},{text:"Clear App Storage",onPress:async()=>{try{await xt()}catch(e){ne.alert("Error","Failed to clear app storage")}}},{text:"Clear All (Including Dev Tools)",style:"destructive",onPress:async()=>{try{await kt()}catch(e){ne.alert("Error","Failed to clear all storage")}}}])},[]);const O=e=>{const t=dt(e);if(t===null)return"null";if(t===void 0)return"undefined";if(Array.isArray(t))return"array";if(typeof t==="boolean")return"boolean";if(typeof t==="number")return"number";if(typeof t==="string")return"string";if(typeof t==="object")return"object";return"undefined"};const[he,qe]=g([]);ee(()=>{const e=async()=>{try{const t=await oe.getAllKeys();qe([...t].sort())}catch(t){}};if(r){e()}},[r]);const Ue=y(()=>{const e=new Set(he);F.forEach(t=>{if(t.data?.key){e.add(t.data.key)}});return Array.from(e).sort()},[F,he]);const m=y(()=>{const e=new Map;F.forEach(t=>{if(!t.data?.key)return;const o=t.data.key;if(!b.has(t.storageType))return;const a=Array.from(T).some(w=>o.includes(w));if(a)return;const s=e.get(o);if(!s){e.set(o,{key:o,lastEvent:t,events:[t],totalOperations:1,currentValue:t.data.value,valueType:O(t.data.value),storageTypes:new Set([t.storageType])})}else{s.events.push(t);s.totalOperations++;s.storageTypes.add(t.storageType);if(t.timestamp>s.lastEvent.timestamp){s.lastEvent=t;s.currentValue=t.data.value;s.valueType=O(t.data.value)}}});return Array.from(e.values()).sort((t,o)=>o.lastEvent.timestamp.getTime()-t.lastEvent.timestamp.getTime())},[F,T,b]);const u=y(()=>{if(!G)return null;return m.find(e=>e.key===G)||null},[G,m]);const Ye=y(()=>{const e={};m.forEach(t=>{e[t.key]=t.totalOperations});return e},[m]);const Xe=y(()=>{const e={};m.forEach(t=>{e[t.key]=t.lastEvent.timestamp.getTime()});return e},[m]);const E=y(()=>{if(!I)return null;return m.find(e=>e.key===I)||null},[I,m]);ee(()=>{if(!K)return;const e=u?u:h!==null?E:null;const t=u?z:h;if(!e||t==null)return;const o=[...e.events].sort((w,Z)=>w.timestamp.getTime()-Z.timestamp.getTime());const a=o[t];const s=o[t-1];if(a)K(a);if(s)K(s)},[K,u,z,E,h]);const R=y(()=>{return F.filter(e=>{if(!e.data?.key)return false;if(!b.has(e.storageType))return false;const t=e.data.key;return!Array.from(T).some(o=>t.includes(o))})},[F,T,b]);const Ze=y(()=>It(R),[R]);const f=y(()=>{if(k==="all")return R;return R.filter(e=>e.storageType===k)},[R,k]);const et=i(()=>{return f.map(e=>({key:e.data?.key,action:e.action,value:_(e.data?.value),storageType:e.storageType,timestamp:e.timestamp.toISOString()}))},[f]);const tt=y(()=>f.slice(0,N),[f,N]);const Y=y(()=>Math.max(0,f.length-N),[f.length,N]);const nt=.8;const ot=i(e=>e.id,[]);const Se=i(e=>{const t=m.find(s=>s.key===e.data?.key);if(!t)return;const o=[...t.events].sort((s,w)=>s.timestamp.getTime()-w.timestamp.getTime());const a=o.findIndex(s=>s.timestamp.getTime()===e.timestamp.getTime());A(t.key);x(a>=0?a:0)},[m]);const rt=i(({item:e})=>{const t={key:e.data?.key||"",lastAction:e.action,totalOperations:1,lastEventTimestamp:e.timestamp,storageTypes:new Set([e.storageType]),valueType:O(e.data?.value)};return n(we,{data:t,onPress:()=>Se(e),testID:`buoy-storage-event-${e.id}`})},[Se]);if(!r)return null;const at=Ae?P.modal.root():P.storage.modal();const ve=()=>{if(D){return n(Ft,{ignoredPatterns:T,onTogglePattern:me,onAddPattern:fe,availableKeys:Ue,enabledStorageTypes:b,onToggleStorageType:Je})}if(B==="browser"){if(I&&E){if(h!==null){return n(S,{style:l.contentWrapper,children:n(xe,{conversation:E,selectedEventIndex:h,onEventIndexChange:v,disableInternalFooter:true})})}const t=[...E.events].sort((a,s)=>a.timestamp.getTime()-s.timestamp.getTime());const o=[...t].reverse();return n(Ee,{testID:"buoy-storage-history-list",style:l.flexFill,data:o,keyExtractor:a=>a.id,renderItem:({item:a})=>{const s=t.findIndex(Z=>Z.timestamp.getTime()===a.timestamp.getTime());const w={key:a.data?.key||"",lastAction:a.action,totalOperations:1,lastEventTimestamp:a.timestamp,storageTypes:new Set([a.storageType]),valueType:O(a.data?.value)};return n(we,{data:w,onPress:()=>v(s>=0?s:0),testID:`buoy-storage-history-event-${a.id}`})},contentContainerStyle:l.listContent,ItemSeparatorComponent:()=>n(S,{style:l.separator}),initialNumToRender:20})}return n(Et,{requiredStorageKeys:De,ignoredPatterns:T,onTogglePattern:me,onAddPattern:fe,searchQuery:le,storageDataRef:U,eventCountByKey:Ye,lastWriteByKey:Xe,onViewHistory:je,enabledStorageTypes:b,pinnedKeys:Oe,onTogglePin:$e,applySafeAreaInset:q==="bottomSheet",storageTypeFilter:k,onStorageTypeFilterChange:ge,detailKey:W,onOpenKey:H,onCloseDetail:()=>H(null)})}if(u){return n(S,{style:l.contentWrapper,children:n(xe,{conversation:u,selectedEventIndex:z,onEventIndexChange:x,disableInternalFooter:true})})}const e=n(S,{style:l.eventFilterBar,children:n(Ct,{active:k,onChange:ge,counts:Ze})});if(f.length===0){const t=k!=="all";return p(S,{style:l.flexFill,children:[e,n(gt,{testID:"buoy-storage-events-empty",...!ye?{otto:"paused",title:"Event listener is paused",message:"Press play to start monitoring"}:t?{otto:"filtering",title:`No ${k} events`,message:"Other backends have events \u2014 pick All Types to see them"}:{otto:"listening",title:"No storage events yet",message:"Storage operations will appear here"}})]})}return p(S,{style:l.flexFill,children:[e,n(Ee,{testID:"buoy-storage-events-list",style:l.flexFill,data:tt,renderItem:rt,keyExtractor:ot,onEndReachedThreshold:nt,contentContainerStyle:l.listContent,ListFooterComponent:Y>0?n(S,{style:l.lockedBannerContainer,children:n(wt,{featureName:`${Y} event${Y>1?"s":""} locked`,description:"Upgrade to Pro to unlock full event history"})}):null,initialNumToRender:10,maxToRenderPerBatch:10,windowSize:10})]})};const X=u&&u.events.length>1?n(ke,{conversation:u,selectedEventIndex:z,onEventIndexChange:x,applySafeAreaInset:q==="bottomSheet"}):E&&h!==null&&E.events.length>1?n(ke,{conversation:E,selectedEventIndex:h,onEventIndexChange:v,applySafeAreaInset:q==="bottomSheet"}):null;const st=D?()=>Q(false):h!==null?()=>v(null):I?pe:u?()=>{A(null);x(0)}:V;const Te=D?p(c,{children:[n(c.Navigation,{onBack:()=>Q(false)}),n(c.Content,{title:"Filters"})]}):I?p(c,{children:[n(c.Navigation,{onBack:h!==null?()=>v(null):pe}),n(c.Content,{title:`${I} History`,centered:true})]}):u?p(c,{children:[n(c.Navigation,{onBack:()=>{A(null);x(0)}}),n(c.Content,{title:u.key})]}):W?p(c,{children:[n(c.Navigation,{onBack:()=>H(null)}),n(c.Content,{title:W.key,centered:true,testID:"buoy-storage-detail-title"})]}):p(c,{children:[V&&n(c.Navigation,{onBack:V}),n(c.Content,{title:"",children:j?n(ft,{value:le,onChange:ie,onClear:()=>{ie("");$(false)},placeholder:"Search storage keys...",testID:"buoy-storage-search",autoFocus:true,onSubmitEditing:()=>$(false),returnKeyType:"search",containerStyle:l.headerSearchContainer}):n(ct,{tabs:[{key:"browser",label:"Storage"},{key:"events",label:`Events${f.length>0?` (${f.length})`:""}`}],activeTab:B,testIDPrefix:"buoy-storage-tab",onTabChange:e=>{Ke(e);J(null);v(null);A(null);x(0);H(null)}})}),p(c.Actions,{children:[se&&!j?n(te,{onPress:se,style:l.iconButton,testID:"buoy-storage-refresh",accessibilityLabel:"Refresh storage",accessibilityRole:"button",children:n(pt,{size:14,color:d.color.textSecondary})}):null,n(te,{onPress:Ge,testID:"buoy-storage-filters",accessibilityLabel:"Storage filters",accessibilityRole:"button",style:[l.iconButton,(T.size>0||b.size<3)&&l.activeFilterButton],children:n(yt,{size:14,color:T.size>0||b.size<3?d.color.accent:d.color.textSecondary})}),B==="browser"&&!j&&p(Be,{children:[n(te,{onPress:()=>$(true),testID:"buoy-storage-search-open",accessibilityLabel:"Open search",accessibilityRole:"button",style:l.iconButton,children:n(mt,{size:14,color:d.color.textSecondary})}),n(Ce,{value:Qe,disabled:U.current.length===0,buttonStyle:l.iconButton,testID:"buoy-storage-copy"})]}),B==="events"&&p(Be,{children:[n(Ce,{value:et,disabled:f.length===0,buttonStyle:l.iconButton,testID:"buoy-storage-events-copy"}),n(ht,{isEnabled:ye,onToggle:Ve,accessibilityLabel:"Toggle storage event monitoring",testID:"buoy-storage-power"}),n(St,{onPress:We,buttonStyle:l.iconButton,testID:"buoy-storage-clear-events"})]})]})]});if(Re){return n(Ie,{children:p(S,{style:l.inlineRoot,children:[n(S,{style:l.inlineHeader,...bt,children:Te}),ve(),X]})})}return n(Ie,{children:n(it,{visible:r,onClose:C,onBack:st,onMinimize:Pe,persistenceKey:at,header:{showToggleButton:true,customContent:Te},onModeChange:Le,enablePersistence:true,initialMode:"bottomSheet",enableGlitchEffects:true,styles:{},footer:X,footerHeight:X?68:0,disableScrollWrapper:true,children:ve()})})}export function StorageToolView(r={}){return n(StorageModalWithTabs,{...r,inline:true,visible:true,onClose:()=>{}})}const l=lt.create({inlineRoot:{flex:1,minHeight:0,backgroundColor:"transparent"},inlineHeader:{paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:ut.border.default,backgroundColor:"transparent"},headerSearchContainer:{flex:1},flexFill:{flex:1},iconButton:{width:32,height:32,borderRadius:8,backgroundColor:d.color.buttonSurface,borderWidth:1,borderColor:d.color.border,alignItems:"center",justifyContent:"center"},activeFilterButton:{backgroundColor:d.color.accentSoft,borderColor:d.color.accentBorderStrong},separator:{height:8},listContent:{paddingVertical:6},lockedBannerContainer:{marginTop:8,marginHorizontal:16},eventFilterBar:{paddingHorizontal:12,paddingTop:8,paddingBottom:4},emptyState:{flex:1,justifyContent:"center",alignItems:"center",padding:32},emptyTitle:{color:d.color.text,fontSize:16,fontWeight:"600",marginTop:16,marginBottom:8,fontFamily:L,letterSpacing:.5,textTransform:"uppercase"},emptySubtitle:{color:d.color.textSecondary,fontSize:14,textAlign:"center",fontFamily:L},eventNavigation:{flexDirection:"row",alignItems:"center",gap:8,paddingHorizontal:8},navButton:{padding:4,borderRadius:4},navButtonDisabled:{opacity:.3},eventCounter:{color:d.color.text,fontSize:12,fontWeight:"600",fontFamily:L,paddingHorizontal:8},headerTopRow:{flexDirection:"row",alignItems:"center",gap:8,marginBottom:12},keyNameContainer:{flex:1,flexDirection:"row",alignItems:"center",gap:6,backgroundColor:d.color.surfaceElevated,paddingHorizontal:10,paddingVertical:4,borderRadius:6,borderWidth:1,borderColor:d.color.border,height:28},keyNameText:{flex:1,fontSize:13,fontWeight:"600",color:d.color.accent,fontFamily:L,letterSpacing:.5},contentWrapper:{flex:1}});