@buoy-gg/storage 7.0.4 → 7.0.6

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 (148) hide show
  1. package/lib/commonjs/index.js +77 -1
  2. package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
  3. package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
  4. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +263 -115
  5. package/lib/commonjs/storage/components/GameUIStorageStats.js +7 -7
  6. package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -1
  7. package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -1
  8. package/lib/commonjs/storage/components/SelectionActionBar.js +2 -36
  9. package/lib/commonjs/storage/components/StorageActions.js +6 -6
  10. package/lib/commonjs/storage/components/StorageBrowserMode.js +16 -2
  11. package/lib/commonjs/storage/components/StorageEventDetailContent.js +106 -149
  12. package/lib/commonjs/storage/components/StorageEventListener.js +13 -13
  13. package/lib/commonjs/storage/components/StorageFilterCards.js +13 -135
  14. package/lib/commonjs/storage/components/StorageKeyCard.js +1 -1
  15. package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +392 -0
  16. package/lib/commonjs/storage/components/StorageKeyRow.js +53 -15
  17. package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
  18. package/lib/commonjs/storage/components/StorageKeyStats.js +10 -10
  19. package/lib/commonjs/storage/components/StorageModalWithTabs.js +272 -129
  20. package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +46 -0
  21. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +141 -0
  22. package/lib/commonjs/storage/components/StorageValueEditModal.js +361 -0
  23. package/lib/commonjs/storage/hooks/useKeyEditor.js +104 -0
  24. package/lib/commonjs/storage/hooks/useMeasuredWidth.js +74 -0
  25. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +70 -15
  26. package/lib/commonjs/storage/hooks/useTreeDraft.js +107 -0
  27. package/lib/commonjs/storage/index.js +29 -0
  28. package/lib/commonjs/storage/stores/storageEventStore.js +4 -2
  29. package/lib/commonjs/storage/sync/storageSyncAdapter.js +125 -3
  30. package/lib/commonjs/storage/utils/AsyncStorageListener.js +76 -12
  31. package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +10 -9
  32. package/lib/commonjs/storage/utils/MMKVListener.js +40 -4
  33. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +52 -1
  34. package/lib/commonjs/storage/utils/deleteStorageKeys.js +53 -0
  35. package/lib/commonjs/storage/utils/index.js +13 -0
  36. package/lib/commonjs/storage/utils/lineDiff.js +21 -354
  37. package/lib/commonjs/storage/utils/setStorageValue.js +218 -0
  38. package/lib/commonjs/storage/utils/storageBrowserRefresh.js +49 -0
  39. package/lib/commonjs/storage/utils/storageOps.js +53 -0
  40. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +60 -38
  41. package/lib/commonjs/storage/utils/valueType.js +24 -0
  42. package/lib/module/index.js +18 -1
  43. package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
  44. package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
  45. package/lib/module/storage/components/GameUIStorageBrowser.js +266 -118
  46. package/lib/module/storage/components/GameUIStorageStats.js +7 -7
  47. package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -1
  48. package/lib/module/storage/components/MMKVInstanceSelector.js +1 -1
  49. package/lib/module/storage/components/SelectionActionBar.js +2 -36
  50. package/lib/module/storage/components/StorageActions.js +7 -7
  51. package/lib/module/storage/components/StorageBrowserMode.js +16 -2
  52. package/lib/module/storage/components/StorageEventDetailContent.js +110 -151
  53. package/lib/module/storage/components/StorageEventListener.js +14 -14
  54. package/lib/module/storage/components/StorageFilterCards.js +13 -135
  55. package/lib/module/storage/components/StorageKeyCard.js +1 -1
  56. package/lib/module/storage/components/StorageKeyEditorScreen.js +388 -0
  57. package/lib/module/storage/components/StorageKeyRow.js +55 -17
  58. package/lib/module/storage/components/StorageKeySection.js +11 -5
  59. package/lib/module/storage/components/StorageKeyStats.js +11 -10
  60. package/lib/module/storage/components/StorageModalWithTabs.js +272 -130
  61. package/lib/module/storage/components/StorageScalarKeyEditor.js +42 -0
  62. package/lib/module/storage/components/StorageTypeFilterBar.js +136 -0
  63. package/lib/module/storage/components/StorageValueEditModal.js +357 -0
  64. package/lib/module/storage/hooks/useKeyEditor.js +100 -0
  65. package/lib/module/storage/hooks/useMeasuredWidth.js +71 -0
  66. package/lib/module/storage/hooks/useSecureStoreKeys.js +71 -16
  67. package/lib/module/storage/hooks/useTreeDraft.js +103 -0
  68. package/lib/module/storage/index.js +3 -0
  69. package/lib/module/storage/stores/storageEventStore.js +4 -2
  70. package/lib/module/storage/sync/storageSyncAdapter.js +125 -3
  71. package/lib/module/storage/utils/AsyncStorageListener.js +76 -12
  72. package/lib/module/storage/utils/MMKVInstanceRegistry.js +8 -7
  73. package/lib/module/storage/utils/MMKVListener.js +40 -4
  74. package/lib/module/storage/utils/SecureStoreRegistry.js +52 -1
  75. package/lib/module/storage/utils/deleteStorageKeys.js +49 -0
  76. package/lib/module/storage/utils/index.js +3 -0
  77. package/lib/module/storage/utils/lineDiff.js +7 -354
  78. package/lib/module/storage/utils/setStorageValue.js +209 -0
  79. package/lib/module/storage/utils/storageBrowserRefresh.js +44 -0
  80. package/lib/module/storage/utils/storageOps.js +46 -0
  81. package/lib/module/storage/utils/storageTimeTravelUtils.js +61 -40
  82. package/lib/module/storage/utils/valueType.js +23 -0
  83. package/lib/typescript/index.d.ts +12 -1
  84. package/lib/typescript/index.d.ts.map +1 -1
  85. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts +4 -1
  86. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +1 -1
  87. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -1
  88. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
  89. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +27 -1
  90. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  91. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  92. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +17 -1
  93. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  94. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts +4 -1
  95. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -1
  96. package/lib/typescript/storage/components/StorageFilterCards.d.ts +9 -3
  97. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
  98. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts +42 -0
  99. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -0
  100. package/lib/typescript/storage/components/StorageKeyRow.d.ts +17 -0
  101. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  102. package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
  103. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  104. package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +1 -1
  105. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +28 -1
  106. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  107. package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts +24 -0
  108. package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts.map +1 -0
  109. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts +29 -0
  110. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -0
  111. package/lib/typescript/storage/components/StorageValueEditModal.d.ts +36 -0
  112. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -0
  113. package/lib/typescript/storage/hooks/useKeyEditor.d.ts +51 -0
  114. package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -0
  115. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts +37 -0
  116. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts.map +1 -0
  117. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +8 -1
  118. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -1
  119. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +67 -0
  120. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -0
  121. package/lib/typescript/storage/index.d.ts +2 -0
  122. package/lib/typescript/storage/index.d.ts.map +1 -1
  123. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +41 -0
  124. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  125. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +15 -0
  126. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
  127. package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +1 -1
  128. package/lib/typescript/storage/utils/MMKVListener.d.ts +7 -0
  129. package/lib/typescript/storage/utils/MMKVListener.d.ts.map +1 -1
  130. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +20 -0
  131. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -1
  132. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +27 -0
  133. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -0
  134. package/lib/typescript/storage/utils/index.d.ts +1 -0
  135. package/lib/typescript/storage/utils/index.d.ts.map +1 -1
  136. package/lib/typescript/storage/utils/lineDiff.d.ts +7 -31
  137. package/lib/typescript/storage/utils/lineDiff.d.ts.map +1 -1
  138. package/lib/typescript/storage/utils/setStorageValue.d.ts +64 -0
  139. package/lib/typescript/storage/utils/setStorageValue.d.ts.map +1 -0
  140. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts +28 -0
  141. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +1 -0
  142. package/lib/typescript/storage/utils/storageOps.d.ts +25 -0
  143. package/lib/typescript/storage/utils/storageOps.d.ts.map +1 -0
  144. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts +15 -5
  145. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  146. package/lib/typescript/storage/utils/valueType.d.ts +13 -0
  147. package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
  148. package/package.json +5 -5
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  import { useMemo, useCallback, useState, useEffect } from "react";
4
- import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from "react-native";
4
+ import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Alert } from "react-native";
5
5
  import { absoluteFill } from "@buoy-gg/shared-ui";
6
6
  import { Database, Zap, ProBadge, ProUpgradeModal } from "@buoy-gg/shared-ui";
7
7
  import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
8
8
  import { StorageKeySection } from "./StorageKeySection";
9
+ import { StorageKeyEditorScreen } from "./StorageKeyEditorScreen";
9
10
  import { StorageFilterCards } from "./StorageFilterCards";
10
11
  import { useAsyncStorageKeys } from "../hooks/useAsyncStorageKeys";
11
12
  import { addListener } from "../utils/AsyncStorageListener";
@@ -17,13 +18,15 @@ import { SelectionActionBar } from "./SelectionActionBar";
17
18
  // Pure JS (no react-native-mmkv dependency) — works with any registered
18
19
  // instance, including remote-backed wrappers on the desktop dashboard.
19
20
  import { addMMKVListener } from "../utils/MMKVListener";
21
+ import { onStorageBrowserRefresh } from "../utils/storageBrowserRefresh";
22
+ import { setStorageValue, storageEditBlockedReason } from "../utils/setStorageValue";
20
23
 
21
24
  // Import shared Game UI components
22
- import { gameUIColors, macOSColors, HardDrive } from "@buoy-gg/shared-ui";
25
+ import { gameUIColors, macOSColors, buoyColors, HardDrive } from "@buoy-gg/shared-ui";
23
26
  import { useIsPro } from "@buoy-gg/license";
24
27
 
25
28
  // MMKV Instance color palette - consistent colors per instance
26
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
29
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
27
30
  const INSTANCE_COLORS = [macOSColors.semantic.info,
28
31
  // Blue
29
32
  macOSColors.semantic.success,
@@ -57,10 +60,17 @@ export function GameUIStorageBrowser({
57
60
  searchQuery = "",
58
61
  storageDataRef,
59
62
  eventCountByKey,
63
+ lastWriteByKey,
60
64
  onViewHistory,
61
65
  enabledStorageTypes,
66
+ statusFilter,
67
+ onStatusFilterChange,
68
+ onStatusCountsChange,
62
69
  pinnedKeys,
63
- onTogglePin
70
+ onTogglePin,
71
+ storageTypeFilter,
72
+ onStorageTypeFilterChange,
73
+ applySafeAreaInset
64
74
  }) {
65
75
  const isPro = useIsPro();
66
76
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
@@ -71,10 +81,29 @@ export function GameUIStorageBrowser({
71
81
  setShowUpgradeModal(false);
72
82
  }
73
83
  }, [showUpgradeModal, isPro]);
74
- const [activeFilter, setActiveFilter] = useState("all");
75
- const [activeStorageType, setActiveStorageType] = useState("all");
84
+ const [internalFilter, setInternalFilter] = useState("all");
85
+ // Controlled when the host supplies both halves; uncontrolled otherwise.
86
+ const isFilterControlled = statusFilter != null && onStatusFilterChange != null;
87
+ const activeFilter = isFilterControlled ? statusFilter : internalFilter;
88
+ const setActiveFilter = isFilterControlled ? onStatusFilterChange : setInternalFilter;
89
+ const [internalStorageType, setInternalStorageType] = useState("all");
90
+ // Controlled when the host supplies both halves — the modal does, so the
91
+ // Events tab shows the same backend you picked here.
92
+ const isStorageTypeControlled = storageTypeFilter != null && onStorageTypeFilterChange != null;
93
+ const activeStorageType = isStorageTypeControlled ? storageTypeFilter : internalStorageType;
94
+ const setActiveStorageType = isStorageTypeControlled ? onStorageTypeFilterChange : setInternalStorageType;
76
95
  const [selectedMMKVInstance, setSelectedMMKVInstance] = useState(null);
77
96
 
97
+ /**
98
+ * The key the full-screen editor is open on, if any.
99
+ *
100
+ * Held here rather than in the row because the editor covers the whole
101
+ * browser, and because the write it eventually performs is `handleSaveValue`
102
+ * — which needs the MMKV instance registry and the refresh fan-out that live
103
+ * at this level.
104
+ */
105
+ const [editingKey, setEditingKey] = useState(null);
106
+
78
107
  // Selection mode state
79
108
  const [isSelectMode, setIsSelectMode] = useState(false);
80
109
  const [selectedKeyIds, setSelectedKeyIds] = useState(new Set());
@@ -188,6 +217,40 @@ export function GameUIStorageBrowser({
188
217
  setIsSelectMode(false);
189
218
  }, [onAddPattern]);
190
219
 
220
+ /**
221
+ * Write an edited value back to the device.
222
+ *
223
+ * Pro is checked here rather than on the Edit button because the technical
224
+ * blocks (`storageEditBlockedReason`) and the licence are different kinds of
225
+ * "no": the former means this key can never be edited and the row says so
226
+ * permanently, the latter is an upsell. Throwing lets the editor keep the
227
+ * draft on screen instead of discarding what was typed.
228
+ */
229
+ const handleSaveValue = useCallback(async (storageKey, raw) => {
230
+ if (!isPro) {
231
+ setShowUpgradeModal(true);
232
+ throw new Error("Editing storage values is a Pro feature.");
233
+ }
234
+ await setStorageValue(storageKey, raw, mmkvInstances);
235
+ refresh();
236
+ }, [isPro, mmkvInstances, refresh]);
237
+
238
+ /**
239
+ * Flip a boolean key straight from its card.
240
+ *
241
+ * Goes through `handleSaveValue` like every other write, so the Pro gate and
242
+ * the type preservation are the same ones the editor uses — this is only a
243
+ * shortcut past the UI, not past the rules.
244
+ */
245
+ const handleToggleKey = useCallback(storageKey => {
246
+ void handleSaveValue(storageKey, JSON.stringify(!storageKey.value)).catch(e => {
247
+ Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
248
+ });
249
+ }, [handleSaveValue]);
250
+
251
+ /** Permanent, per-key reasons a value can't be edited (never the licence). */
252
+ const editBlockedFor = useCallback(storageKey => storageEditBlockedReason(storageKey, mmkvInstances), [mmkvInstances]);
253
+
191
254
  // Memoized export data for copy functionality
192
255
  const copyExportData = useMemo(() => {
193
256
  const allKeys = allStorageKeys;
@@ -253,12 +316,34 @@ export function GameUIStorageBrowser({
253
316
  };
254
317
  }, [refreshAsync]);
255
318
 
319
+ // Auto-refresh when something outside this component knows the underlying
320
+ // storage changed. On device the listeners above cover it; in remote-mirror
321
+ // mode (desktop dashboard) local capture is suppressed and they never fire,
322
+ // so the dashboard signals each incoming device snapshot this way — without
323
+ // it the browse tab is frozen at whatever it read on mount.
324
+ useEffect(() => {
325
+ let timeoutId;
326
+ const unsubscribe = onStorageBrowserRefresh(() => {
327
+ clearTimeout(timeoutId);
328
+ timeoutId = setTimeout(refresh, 100);
329
+ });
330
+ return () => {
331
+ clearTimeout(timeoutId);
332
+ unsubscribe();
333
+ };
334
+ }, [refresh]);
335
+
256
336
  // Auto-refresh on MMKV storage events
257
337
  useEffect(() => {
258
338
  let timeoutId;
259
339
  const unsubscribe = addMMKVListener(event => {
260
- // Only refresh if event is for the selected instance
261
- if (event.instanceId === selectedMMKVInstance) {
340
+ // Refresh when the changed instance is one we're actually showing.
341
+ // `selectedMMKVInstance` is null by default ("All instances"), so
342
+ // comparing it to event.instanceId directly was never true for the view
343
+ // every user starts on — MMKV values sat stale in the browse list until
344
+ // it remounted, even though the write was recorded in Events.
345
+ const affectsView = selectedMMKVInstance === null || event.instanceId === selectedMMKVInstance;
346
+ if (affectsView) {
262
347
  clearTimeout(timeoutId);
263
348
  timeoutId = setTimeout(() => {
264
349
  refreshMMKV(); // Auto-refresh MMKV when it changes
@@ -343,6 +428,15 @@ export function GameUIStorageBrowser({
343
428
  };
344
429
  }, [allStorageKeys, activeFilter, activeStorageType]);
345
430
 
431
+ // Push the status counts to a host toolbar that renders them (desktop).
432
+ useEffect(() => {
433
+ onStatusCountsChange?.({
434
+ validCount: tabStats.validCount,
435
+ missingCount: tabStats.missingCount,
436
+ issuesCount: tabStats.issuesCount
437
+ });
438
+ }, [onStatusCountsChange, tabStats.validCount, tabStats.missingCount, tabStats.issuesCount]);
439
+
346
440
  // Sort all keys by priority (issues first)
347
441
  const sortedKeys = useMemo(() => {
348
442
  // Sort by status priority: errors first, then warnings, then valid
@@ -464,6 +558,11 @@ export function GameUIStorageBrowser({
464
558
  setSelectedKeyIds(new Set());
465
559
  }, []);
466
560
 
561
+ /**
562
+ * Serialized size of everything in storage, for the table's footer summary.
563
+ * Keyed on `allStorageKeys` rather than the filtered set: it answers "how much
564
+ * is in here", which a filter should not change.
565
+ */
467
566
  // Calculate health percentage
468
567
  const healthPercentage = stats.requiredCount > 0 ? Math.round(stats.presentRequiredCount / stats.requiredCount * 100) : stats.totalCount > 0 ? 100 : 0;
469
568
  const healthStatus = healthPercentage >= 90 ? "OPTIMAL" : healthPercentage >= 70 ? "WARNING" : "CRITICAL";
@@ -501,23 +600,15 @@ export function GameUIStorageBrowser({
501
600
  })]
502
601
  });
503
602
  }
504
- return /*#__PURE__*/_jsxs(ScrollView, {
505
- style: styles.scrollContainer,
506
- contentContainerStyle: styles.container,
507
- showsVerticalScrollIndicator: false,
508
- children: [/*#__PURE__*/_jsx(View, {
509
- style: styles.backgroundGrid
510
- }), /*#__PURE__*/_jsx(StorageFilterCards, {
511
- stats: stats,
512
- tabStats: tabStats,
513
- healthPercentage: healthPercentage,
514
- healthStatus: healthStatus,
515
- healthColor: healthColor,
516
- activeFilter: activeFilter,
517
- onFilterChange: setActiveFilter,
518
- activeStorageType: activeStorageType,
519
- onStorageTypeChange: setActiveStorageType
520
- }), activeStorageType === "mmkv" && mmkvInstances.length === 0 && /*#__PURE__*/_jsxs(View, {
603
+
604
+ /* ---------------------------------------------------------------- *
605
+ * Subtrees the card list composes. Extracted rather than inlined so Pro
606
+ * gating and MMKV
607
+ * instance selection or the empty-state copy.
608
+ * ---------------------------------------------------------------- */
609
+
610
+ const mmkvSection = /*#__PURE__*/_jsxs(_Fragment, {
611
+ children: [activeStorageType === "mmkv" && mmkvInstances.length === 0 && /*#__PURE__*/_jsxs(View, {
521
612
  style: styles.emptyMMKVState,
522
613
  children: [/*#__PURE__*/_jsx(Text, {
523
614
  style: styles.emptyMMKVTitle,
@@ -601,102 +692,159 @@ export function GameUIStorageBrowser({
601
692
  }, inst.id);
602
693
  })]
603
694
  })]
604
- }), filteredKeys.length > 0 ? /*#__PURE__*/_jsxs(View, {
605
- style: styles.keysSection,
695
+ })]
696
+ });
697
+ const actionButtonsNode = /*#__PURE__*/_jsx(StorageActionButtons, {
698
+ copyValue: copyExportData,
699
+ mmkvInstances: mmkvInstances.map(inst => ({
700
+ id: inst.id,
701
+ instance: inst.instance
702
+ })),
703
+ activeStorageType: activeStorageType,
704
+ onClearComplete: refresh,
705
+ isSelectMode: isSelectMode,
706
+ onToggleSelectMode: handleToggleSelectMode,
707
+ selectedCount: selectedKeyIds.size
708
+ });
709
+
710
+ /* Selection Action Bar - Only show in select mode with selections */
711
+ const selectionBarNode = isSelectMode ? /*#__PURE__*/_jsx(SelectionActionBar, {
712
+ selectedKeys: selectedKeysInfo,
713
+ mmkvInstances: mmkvInstances.map(inst => ({
714
+ id: inst.id,
715
+ instance: inst.instance
716
+ })),
717
+ onDeleteComplete: handleDeleteComplete,
718
+ onHideKeys: handleHideKeys,
719
+ onSelectAll: handleSelectAll,
720
+ onClearSelection: handleClearSelection,
721
+ totalVisibleKeys: visibleKeys.length
722
+ }) : null;
723
+ const lockedBannerNode = hasLockedKeys ? /*#__PURE__*/_jsxs(TouchableOpacity, {
724
+ style: styles.limitBanner,
725
+ onPress: () => setShowUpgradeModal(true),
726
+ activeOpacity: 0.8,
727
+ children: [/*#__PURE__*/_jsxs(View, {
728
+ style: styles.limitBannerContent,
606
729
  children: [/*#__PURE__*/_jsxs(View, {
607
- style: styles.sectionHeader,
608
- children: [/*#__PURE__*/_jsxs(View, {
609
- style: styles.sectionHeaderLeft,
610
- children: [/*#__PURE__*/_jsxs(Text, {
611
- style: styles.sectionTitle,
612
- children: [activeFilter === "all" ? "KEYS" : activeFilter === "missing" ? "MISSING KEYS" : "ISSUES TO FIX", activeStorageType !== "all" && ` (${activeStorageType.toUpperCase()})`]
613
- }), /*#__PURE__*/_jsx(View, {
614
- style: styles.countBadge,
615
- children: /*#__PURE__*/_jsx(Text, {
616
- style: styles.countText,
617
- children: visibleKeys.length
618
- })
619
- })]
620
- }), /*#__PURE__*/_jsx(StorageActionButtons, {
621
- copyValue: copyExportData,
622
- mmkvInstances: mmkvInstances.map(inst => ({
623
- id: inst.id,
624
- instance: inst.instance
625
- })),
626
- activeStorageType: activeStorageType,
627
- onClearComplete: refresh,
628
- isSelectMode: isSelectMode,
629
- onToggleSelectMode: handleToggleSelectMode,
630
- selectedCount: selectedKeyIds.size
631
- })]
632
- }), isSelectMode && /*#__PURE__*/_jsx(SelectionActionBar, {
633
- selectedKeys: selectedKeysInfo,
634
- mmkvInstances: mmkvInstances.map(inst => ({
635
- id: inst.id,
636
- instance: inst.instance
637
- })),
638
- onDeleteComplete: handleDeleteComplete,
639
- onHideKeys: handleHideKeys,
640
- onSelectAll: handleSelectAll,
641
- onClearSelection: handleClearSelection,
642
- totalVisibleKeys: visibleKeys.length
643
- }), /*#__PURE__*/_jsx(StorageKeySection, {
644
- title: "",
645
- count: -1,
646
- keys: visibleKeys,
647
- emptyMessage: "",
648
- isSelectMode: isSelectMode,
649
- selectedKeys: selectedKeyIds,
650
- onSelectionChange: handleSelectionChange,
651
- eventCountByKey: eventCountByKey,
652
- onViewHistory: onViewHistory,
653
- onHideKey: key => handleHideKeys([key]),
654
- pinnedKeys: pinnedKeys,
655
- onTogglePin: onTogglePin
656
- }), hasLockedKeys && /*#__PURE__*/_jsxs(TouchableOpacity, {
657
- style: styles.limitBanner,
658
- onPress: () => setShowUpgradeModal(true),
659
- activeOpacity: 0.8,
660
- children: [/*#__PURE__*/_jsxs(View, {
661
- style: styles.limitBannerContent,
662
- children: [/*#__PURE__*/_jsxs(View, {
663
- style: styles.limitBannerLeft,
664
- children: [/*#__PURE__*/_jsx(Zap, {
665
- size: 16,
666
- color: "#20C997"
667
- }), /*#__PURE__*/_jsx(Text, {
668
- style: styles.limitBannerText,
669
- children: searchQuery ? `${lockedKeyCount} matching ${lockedKeyCount === 1 ? 'key' : 'keys'} in locked storage` : `${lockedKeyCount} keys locked`
670
- })]
671
- }), /*#__PURE__*/_jsx(ProBadge, {})]
730
+ style: styles.limitBannerLeft,
731
+ children: [/*#__PURE__*/_jsx(Zap, {
732
+ size: 16,
733
+ color: buoyColors.primary
672
734
  }), /*#__PURE__*/_jsx(Text, {
673
- style: styles.limitBannerSubtext,
674
- children: searchQuery ? "Upgrade to Pro to search your full storage" : "Upgrade to Pro to unlock full storage access"
735
+ style: styles.limitBannerText,
736
+ children: searchQuery ? `${lockedKeyCount} matching ${lockedKeyCount === 1 ? 'key' : 'keys'} in locked storage` : `${lockedKeyCount} keys locked`
675
737
  })]
676
- })]
677
- }) : /*#__PURE__*/_jsxs(View, {
678
- style: styles.emptyState,
679
- children: [/*#__PURE__*/_jsx(Database, {
680
- size: 32,
681
- color: macOSColors.text.muted
682
- }), /*#__PURE__*/_jsx(Text, {
683
- style: styles.emptyTitle,
684
- children: searchQuery ? "No results found" : activeFilter === "all" ? "No valid keys found" : activeFilter === "missing" ? "No missing keys" : "No issues found"
685
- }), /*#__PURE__*/_jsx(Text, {
686
- style: styles.emptySubtitle,
687
- children: searchQuery ? `No keys matching "${searchQuery}"` : activeFilter === "all" ? "All keys have issues or are missing" : activeFilter === "missing" ? "All required keys are present" : "All storage keys are correctly configured"
688
- })]
738
+ }), /*#__PURE__*/_jsx(ProBadge, {})]
689
739
  }), /*#__PURE__*/_jsx(Text, {
690
- style: styles.techFooter,
691
- children: "ASYNC STORAGE | MMKV | SECURE STORAGE BACKENDS"
692
- }), /*#__PURE__*/_jsx(ProUpgradeModal, {
693
- visible: showUpgradeModal,
694
- onClose: () => setShowUpgradeModal(false),
695
- featureName: "Storage Keys"
740
+ style: styles.limitBannerSubtext,
741
+ children: searchQuery ? "Upgrade to Pro to search your full storage" : "Upgrade to Pro to unlock full storage access"
742
+ })]
743
+ }) : null;
744
+ const emptyStateNode = /*#__PURE__*/_jsxs(View, {
745
+ style: styles.emptyState,
746
+ children: [/*#__PURE__*/_jsx(Database, {
747
+ size: 32,
748
+ color: macOSColors.text.muted
749
+ }), /*#__PURE__*/_jsx(Text, {
750
+ style: styles.emptyTitle,
751
+ children: searchQuery ? "No results found" : activeFilter === "all" ? "No valid keys found" : activeFilter === "missing" ? "No missing keys" : "No issues found"
752
+ }), /*#__PURE__*/_jsx(Text, {
753
+ style: styles.emptySubtitle,
754
+ children: searchQuery ? `No keys matching "${searchQuery}"` : activeFilter === "all" ? "All keys have issues or are missing" : activeFilter === "missing" ? "All required keys are present" : "All storage keys are correctly configured"
696
755
  })]
697
756
  });
757
+ const upgradeModalNode = /*#__PURE__*/_jsx(ProUpgradeModal, {
758
+ visible: showUpgradeModal,
759
+ onClose: () => setShowUpgradeModal(false),
760
+ featureName: "Storage Keys"
761
+ });
762
+ return /*#__PURE__*/_jsxs(View, {
763
+ style: styles.root,
764
+ children: [/*#__PURE__*/_jsxs(ScrollView, {
765
+ style: styles.scrollContainer,
766
+ contentContainerStyle: styles.container,
767
+ showsVerticalScrollIndicator: false,
768
+ children: [/*#__PURE__*/_jsx(View, {
769
+ style: styles.backgroundGrid
770
+ }), /*#__PURE__*/_jsx(StorageFilterCards, {
771
+ stats: stats,
772
+ tabStats: tabStats,
773
+ healthPercentage: healthPercentage,
774
+ healthStatus: healthStatus,
775
+ healthColor: healthColor,
776
+ activeFilter: activeFilter,
777
+ onFilterChange: setActiveFilter,
778
+ activeStorageType: activeStorageType,
779
+ onStorageTypeChange: setActiveStorageType
780
+ // Same rule the wide bar follows: when a host toolbar owns the status
781
+ // filter it also renders the counters, so ours would be a second copy.
782
+ ,
783
+ showStatus: !isFilterControlled
784
+ }), mmkvSection, filteredKeys.length > 0 ? /*#__PURE__*/_jsxs(View, {
785
+ style: styles.keysSection,
786
+ children: [/*#__PURE__*/_jsxs(View, {
787
+ style: styles.sectionHeader,
788
+ children: [/*#__PURE__*/_jsxs(View, {
789
+ style: styles.sectionHeaderLeft,
790
+ children: [/*#__PURE__*/_jsxs(Text, {
791
+ style: styles.sectionTitle,
792
+ children: [activeFilter === "all" ? "KEYS" : activeFilter === "missing" ? "MISSING KEYS" : "ISSUES TO FIX", activeStorageType !== "all" && ` (${activeStorageType.toUpperCase()})`]
793
+ }), /*#__PURE__*/_jsx(View, {
794
+ style: styles.countBadge,
795
+ children: /*#__PURE__*/_jsx(Text, {
796
+ style: styles.countText,
797
+ children: visibleKeys.length
798
+ })
799
+ })]
800
+ }), actionButtonsNode]
801
+ }), selectionBarNode, /*#__PURE__*/_jsx(StorageKeySection, {
802
+ title: "",
803
+ count: -1,
804
+ keys: visibleKeys,
805
+ emptyMessage: "",
806
+ isSelectMode: isSelectMode,
807
+ selectedKeys: selectedKeyIds,
808
+ onSelectionChange: handleSelectionChange,
809
+ eventCountByKey: eventCountByKey,
810
+ onViewHistory: onViewHistory,
811
+ onHideKey: key => handleHideKeys([key]),
812
+ onEditKey: setEditingKey,
813
+ onToggleKey: handleToggleKey,
814
+ editBlockedFor: editBlockedFor,
815
+ pinnedKeys: pinnedKeys,
816
+ onTogglePin: onTogglePin
817
+ }), lockedBannerNode]
818
+ }) : emptyStateNode, /*#__PURE__*/_jsx(Text, {
819
+ style: styles.techFooter,
820
+ children: "ASYNC STORAGE | MMKV | SECURE STORAGE BACKENDS"
821
+ }), upgradeModalNode]
822
+ }), editingKey ? /*#__PURE__*/_jsx(View, {
823
+ style: styles.editorOverlay,
824
+ children: /*#__PURE__*/_jsx(StorageKeyEditorScreen, {
825
+ storageKey: editingKey,
826
+ onSave: raw => handleSaveValue(editingKey, raw),
827
+ onClose: () => setEditingKey(null),
828
+ applySafeAreaInset: applySafeAreaInset
829
+ })
830
+ }) : null]
831
+ });
698
832
  }
699
833
  const styles = StyleSheet.create({
834
+ /** Measures the space the columns actually get; `flex: 1` so it fills the
835
+ * parent exactly as the bare ScrollView it wraps used to. */
836
+ root: {
837
+ flex: 1,
838
+ minHeight: 0
839
+ },
840
+ /**
841
+ * No background of its own: the tree editor paints an opaque screen, while a
842
+ * scalar key shows only a modal and wants the list visible behind its scrim.
843
+ */
844
+ editorOverlay: {
845
+ ...absoluteFill,
846
+ zIndex: 10
847
+ },
700
848
  scrollContainer: {
701
849
  flex: 1,
702
850
  backgroundColor: gameUIColors.background
@@ -842,7 +990,7 @@ const styles = StyleSheet.create({
842
990
  fontSize: 9,
843
991
  fontWeight: '700',
844
992
  color: macOSColors.text.muted,
845
- textTransform: 'uppercase',
993
+ textTransform: "uppercase",
846
994
  letterSpacing: 1
847
995
  },
848
996
  instanceNavbarScroll: {
@@ -912,10 +1060,10 @@ const styles = StyleSheet.create({
912
1060
  },
913
1061
  // Free tier limit banner
914
1062
  limitBanner: {
915
- backgroundColor: "#1A1A1A",
1063
+ backgroundColor: buoyColors.card,
916
1064
  borderRadius: 10,
917
1065
  borderWidth: 1,
918
- borderColor: "#20C997" + "40",
1066
+ borderColor: buoyColors.primary + "40",
919
1067
  padding: 12,
920
1068
  marginTop: 12
921
1069
  },
@@ -931,12 +1079,12 @@ const styles = StyleSheet.create({
931
1079
  gap: 8
932
1080
  },
933
1081
  limitBannerText: {
934
- color: "#E0E0E0",
1082
+ color: buoyColors.text,
935
1083
  fontSize: 13,
936
1084
  fontWeight: "600"
937
1085
  },
938
1086
  limitBannerSubtext: {
939
- color: "#888888",
1087
+ color: buoyColors.textMuted,
940
1088
  fontSize: 11
941
1089
  }
942
1090
  });
@@ -428,7 +428,7 @@ const styles = StyleSheet.create({
428
428
  marginBottom: 20,
429
429
  paddingBottom: 12,
430
430
  borderBottomWidth: 1,
431
- borderBottomColor: "rgba(255, 255, 255, 0.1)"
431
+ borderBottomColor: "rgba(128, 128, 128, 0.2)"
432
432
  },
433
433
  headerLeft: {
434
434
  gap: 2
@@ -499,7 +499,7 @@ const styles = StyleSheet.create({
499
499
  },
500
500
  healthBarBg: {
501
501
  height: 6,
502
- backgroundColor: "rgba(255, 255, 255, 0.05)",
502
+ backgroundColor: "rgba(128, 128, 128, 0.1)",
503
503
  borderRadius: 3,
504
504
  overflow: "hidden"
505
505
  },
@@ -517,7 +517,7 @@ const styles = StyleSheet.create({
517
517
  ...absoluteFill,
518
518
  opacity: 0.1,
519
519
  borderWidth: 1,
520
- borderColor: "rgba(255, 255, 255, 0.1)",
520
+ borderColor: "rgba(128, 128, 128, 0.2)",
521
521
  borderRadius: 3
522
522
  },
523
523
  // Stats grid
@@ -634,11 +634,11 @@ const styles = StyleSheet.create({
634
634
  backendItem: {
635
635
  flexDirection: "row",
636
636
  alignItems: "center",
637
- backgroundColor: "rgba(255, 255, 255, 0.02)",
637
+ backgroundColor: "rgba(128, 128, 128, 0.04)",
638
638
  borderRadius: 8,
639
639
  padding: 10,
640
640
  borderWidth: 1,
641
- borderColor: "rgba(255, 255, 255, 0.05)"
641
+ borderColor: "rgba(128, 128, 128, 0.1)"
642
642
  },
643
643
  backendIcon: {
644
644
  width: 24,
@@ -681,7 +681,7 @@ const styles = StyleSheet.create({
681
681
  alignItems: "center",
682
682
  paddingTop: 12,
683
683
  borderTopWidth: 1,
684
- borderTopColor: "rgba(255, 255, 255, 0.1)"
684
+ borderTopColor: "rgba(128, 128, 128, 0.2)"
685
685
  },
686
686
  bottomStats: {
687
687
  flex: 1,
@@ -703,7 +703,7 @@ const styles = StyleSheet.create({
703
703
  bottomDivider: {
704
704
  width: 1,
705
705
  height: 20,
706
- backgroundColor: "rgba(255, 255, 255, 0.1)"
706
+ backgroundColor: "rgba(128, 128, 128, 0.2)"
707
707
  },
708
708
  // Empty state
709
709
  emptyState: {
@@ -189,7 +189,7 @@ const styles = StyleSheet.create({
189
189
  fontSize: 11,
190
190
  fontWeight: '600',
191
191
  color: macOSColors.text.primary,
192
- textTransform: 'uppercase',
192
+ textTransform: "uppercase",
193
193
  letterSpacing: 0.5
194
194
  },
195
195
  // Info grid
@@ -408,7 +408,7 @@ const styles = StyleSheet.create({
408
408
  keyCountLabel: {
409
409
  fontSize: 9,
410
410
  color: macOSColors.text.muted,
411
- textTransform: 'uppercase',
411
+ textTransform: "uppercase",
412
412
  letterSpacing: 0.5
413
413
  }
414
414
  });
@@ -5,8 +5,7 @@ import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
5
5
  import { Trash2, CopyButton, CheckSquare, EyeOff, ProUpgradeModal } from "@buoy-gg/shared-ui";
6
6
  import { macOSColors } from "@buoy-gg/shared-ui";
7
7
  import { useIsPro } from "@buoy-gg/license";
8
- import { removeMany } from "../utils/asyncStorageCompat";
9
- import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
8
+ import { deleteStorageKeys } from "../utils/deleteStorageKeys";
10
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
10
  export function SelectionActionBar({
12
11
  selectedKeys,
@@ -66,40 +65,7 @@ export function SelectionActionBar({
66
65
  style: "destructive",
67
66
  onPress: async () => {
68
67
  try {
69
- // Group keys by storage type
70
- const asyncKeys = selectedKeys.filter(k => k.storageType === "async");
71
- const mmkvKeys = selectedKeys.filter(k => k.storageType === "mmkv");
72
- const secureKeys = selectedKeys.filter(k => k.storageType === "secure");
73
-
74
- // Delete AsyncStorage keys
75
- if (asyncKeys.length > 0) {
76
- await removeMany(asyncKeys.map(k => k.key));
77
- }
78
-
79
- // Delete SecureStore keys (via the registry, which knows each
80
- // key's keychainService options)
81
- for (const k of secureKeys) {
82
- await secureStoreRegistry.deleteValue(k.key);
83
- }
84
-
85
- // Delete MMKV keys
86
- if (mmkvKeys.length > 0) {
87
- // Group by instance
88
- const byInstance = {};
89
- mmkvKeys.forEach(k => {
90
- const instanceId = k.instanceId || "default";
91
- if (!byInstance[instanceId]) byInstance[instanceId] = [];
92
- byInstance[instanceId].push(k.key);
93
- });
94
-
95
- // Delete from each instance
96
- Object.entries(byInstance).forEach(([instanceId, keys]) => {
97
- const inst = mmkvInstances.find(i => i.id === instanceId);
98
- if (inst) {
99
- keys.forEach(key => inst.instance.delete(key));
100
- }
101
- });
102
- }
68
+ await deleteStorageKeys(selectedKeys, mmkvInstances);
103
69
  onDeleteComplete?.();
104
70
  } catch (error) {
105
71
  Alert.alert("Error", `Failed to delete keys: ${error}`);