@buoy-gg/storage 3.0.1 → 4.0.1

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 (71) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/index.js +7 -0
  3. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +25 -4
  4. package/lib/commonjs/storage/components/GameUIStorageStats.js +2 -2
  5. package/lib/commonjs/storage/components/SelectionActionBar.js +16 -3
  6. package/lib/commonjs/storage/components/StorageBrowserMode.js +6 -2
  7. package/lib/commonjs/storage/components/StorageEventDetailContent.js +2 -2
  8. package/lib/commonjs/storage/components/StorageKeyCard.js +5 -5
  9. package/lib/commonjs/storage/components/StorageKeyRow.js +97 -8
  10. package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
  11. package/lib/commonjs/storage/components/StorageModalWithTabs.js +47 -1
  12. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +15 -3
  13. package/lib/commonjs/storage/hooks/useMMKVKeys.js +20 -2
  14. package/lib/commonjs/storage/stores/storageEventStore.js +84 -0
  15. package/lib/commonjs/storage/sync/storageSyncAdapter.js +53 -0
  16. package/lib/commonjs/storage/utils/AsyncStorageListener.js +148 -160
  17. package/lib/commonjs/storage/utils/asyncStorageCompat.js +89 -0
  18. package/lib/commonjs/storage/utils/clearAllStorage.js +2 -1
  19. package/lib/commonjs/storage/utils/mmkvTypeDetection.js +20 -5
  20. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +3 -2
  21. package/lib/commonjs/storage/utils/valueType.js +41 -0
  22. package/lib/module/index.js +5 -0
  23. package/lib/module/storage/components/GameUIStorageBrowser.js +26 -4
  24. package/lib/module/storage/components/GameUIStorageStats.js +3 -2
  25. package/lib/module/storage/components/SelectionActionBar.js +17 -3
  26. package/lib/module/storage/components/StorageBrowserMode.js +6 -2
  27. package/lib/module/storage/components/StorageEventDetailContent.js +2 -2
  28. package/lib/module/storage/components/StorageKeyCard.js +5 -5
  29. package/lib/module/storage/components/StorageKeyRow.js +99 -10
  30. package/lib/module/storage/components/StorageKeySection.js +10 -4
  31. package/lib/module/storage/components/StorageModalWithTabs.js +47 -1
  32. package/lib/module/storage/hooks/useAsyncStorageKeys.js +16 -4
  33. package/lib/module/storage/hooks/useMMKVKeys.js +21 -3
  34. package/lib/module/storage/stores/storageEventStore.js +84 -0
  35. package/lib/module/storage/sync/storageSyncAdapter.js +48 -0
  36. package/lib/module/storage/utils/AsyncStorageListener.js +124 -135
  37. package/lib/module/storage/utils/asyncStorageCompat.js +81 -0
  38. package/lib/module/storage/utils/clearAllStorage.js +2 -1
  39. package/lib/module/storage/utils/mmkvTypeDetection.js +20 -5
  40. package/lib/module/storage/utils/storageTimeTravelUtils.js +3 -2
  41. package/lib/module/storage/utils/valueType.js +39 -0
  42. package/lib/typescript/index.d.ts +1 -0
  43. package/lib/typescript/index.d.ts.map +1 -1
  44. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +5 -1
  45. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  46. package/lib/typescript/storage/components/GameUIStorageStats.d.ts.map +1 -1
  47. package/lib/typescript/storage/components/SelectionActionBar.d.ts +3 -1
  48. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  49. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +3 -1
  50. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  51. package/lib/typescript/storage/components/StorageKeyRow.d.ts +7 -1
  52. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  53. package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
  54. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  55. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  56. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +1 -1
  57. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
  58. package/lib/typescript/storage/stores/storageEventStore.d.ts +8 -0
  59. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +1 -1
  60. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +31 -0
  61. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -0
  62. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +20 -0
  63. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
  64. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts +30 -0
  65. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts.map +1 -0
  66. package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +1 -1
  67. package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +1 -1
  68. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  69. package/lib/typescript/storage/utils/valueType.d.ts +13 -0
  70. package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
  71. package/package.json +6 -6
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.asyncStorageCaps = exports.asyncStorage = void 0;
7
+ exports.readMany = readMany;
8
+ exports.removeMany = removeMany;
9
+ exports.writeMany = writeMany;
10
+ var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ /**
13
+ * AsyncStorage compatibility adapter (v2 + v3)
14
+ *
15
+ * `@react-native-async-storage/async-storage` 3.x reshaped the default-export
16
+ * API. The single-item methods kept their v2 signatures, but the batch + merge
17
+ * methods changed:
18
+ *
19
+ * v2 (what this package was written against) v3
20
+ * ------------------------------------------ --------------------------------
21
+ * multiGet(keys): [key, value|null][] getMany(keys): Record<key, value|null>
22
+ * multiSet([[k, v]]): void setMany({ k: v }): void
23
+ * multiRemove(keys): void removeMany(keys): void
24
+ * mergeItem / multiMerge removed entirely
25
+ *
26
+ * This module is the single seam between the storage tool and whichever
27
+ * async-storage version the host app installed. The rest of the package always
28
+ * speaks the v2 tuple-shaped API; we translate to v3 here when needed.
29
+ *
30
+ * Reads (`readMany`) are routed through the *unswizzled* native methods, so the
31
+ * event listener can safely call them while capturing previous values without
32
+ * re-triggering itself.
33
+ */
34
+
35
+ // Loosely typed because the surface differs across major versions; callers go
36
+ // through the helpers below rather than touching this directly.
37
+
38
+ /** The live default-export object (methods may be swizzled by the listener). */
39
+ const asyncStorage = exports.asyncStorage = _asyncStorage.default;
40
+
41
+ /**
42
+ * Capability flags describing which API surface the installed version exposes.
43
+ * `multiGet` only exists on v2; `mergeItem` was removed in v3.
44
+ */
45
+ const asyncStorageCaps = exports.asyncStorageCaps = {
46
+ /** v2 exposes multiGet/multiSet/multiRemove; v3 renamed these to *Many. */
47
+ hasLegacyMultiApi: typeof asyncStorage?.multiGet === "function",
48
+ /** v3 removed mergeItem/multiMerge with no replacement. */
49
+ hasMergeApi: typeof asyncStorage?.mergeItem === "function"
50
+ };
51
+
52
+ /**
53
+ * Batch-read keys, normalized to the v2 `[key, value|null][]` tuple shape and
54
+ * preserving the requested key order regardless of installed version.
55
+ */
56
+ async function readMany(keys) {
57
+ if (typeof asyncStorage.multiGet === "function") {
58
+ return await asyncStorage.multiGet(keys);
59
+ }
60
+ // v3: getMany returns a Record; rebuild ordered tuples ("what you request is
61
+ // what you get" — missing keys come back as null).
62
+ const record = await asyncStorage.getMany(keys);
63
+ return keys.map(key => [key, record[key] ?? null]);
64
+ }
65
+
66
+ /**
67
+ * Batch-write key/value pairs. Calls the live method (which may be swizzled by
68
+ * the listener on v2's `multiSet` / v3's `setMany`) so writes still emit events.
69
+ */
70
+ async function writeMany(pairs) {
71
+ if (typeof asyncStorage.multiSet === "function") {
72
+ return asyncStorage.multiSet(pairs);
73
+ }
74
+ const record = {};
75
+ for (const [key, value] of pairs) record[key] = value;
76
+ return asyncStorage.setMany(record);
77
+ }
78
+
79
+ /**
80
+ * Batch-remove keys via the live method (swizzled on v2's `multiRemove` /
81
+ * v3's `removeMany`).
82
+ */
83
+ async function removeMany(keys) {
84
+ if (typeof asyncStorage.multiRemove === "function") {
85
+ return asyncStorage.multiRemove(keys);
86
+ }
87
+ return asyncStorage.removeMany(keys);
88
+ }
89
+ var _default = exports.default = asyncStorage;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.clearAllAppStorage = clearAllAppStorage;
7
7
  exports.clearAllStorageIncludingDevTools = clearAllStorageIncludingDevTools;
8
8
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
9
+ var _asyncStorageCompat = require("./asyncStorageCompat");
9
10
  var _sharedUi = require("@buoy-gg/shared-ui");
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
12
  /**
@@ -30,7 +31,7 @@ async function clearAllAppStorage() {
30
31
  // Clearing ${keysToRemove.length} app storage keys
31
32
 
32
33
  // Remove all non-dev-tool keys
33
- await _asyncStorage.default.multiRemove(keysToRemove);
34
+ await (0, _asyncStorageCompat.removeMany)(keysToRemove);
34
35
 
35
36
  // Successfully cleared app storage
36
37
  }
@@ -42,16 +42,25 @@ exports.isTypeMatch = isTypeMatch;
42
42
  * ```
43
43
  */
44
44
  function detectMMKVType(instance, key) {
45
- // Try string (most common)
45
+ // MMKV stores no type metadata, so the native getters can cross-read: in
46
+ // particular `getString` returns "" (an empty string, NOT undefined) for many
47
+ // non-string values like numbers and `false`. Trying getString first and
48
+ // trusting its result therefore mis-detects those as empty strings — e.g.
49
+ // a key set to 42 or false would render as "". Probe the getters and only let
50
+ // getString win when it returns a NON-empty string; treat an empty result as
51
+ // ambiguous and prefer the number/boolean getters.
46
52
  const stringValue = instance.getString(key);
47
- if (stringValue !== undefined) {
53
+
54
+ // A non-empty string is unambiguously a string.
55
+ if (stringValue !== undefined && stringValue !== '') {
48
56
  return {
49
57
  value: stringValue,
50
58
  type: 'string'
51
59
  };
52
60
  }
53
61
 
54
- // Try number
62
+ // Empty getString result is ambiguous (genuine "" vs a number/boolean the
63
+ // native layer decoded to ""). Prefer typed getters when they have a value.
55
64
  const numberValue = instance.getNumber(key);
56
65
  if (numberValue !== undefined) {
57
66
  return {
@@ -59,8 +68,6 @@ function detectMMKVType(instance, key) {
59
68
  type: 'number'
60
69
  };
61
70
  }
62
-
63
- // Try boolean
64
71
  const booleanValue = instance.getBoolean(key);
65
72
  if (booleanValue !== undefined) {
66
73
  return {
@@ -69,6 +76,14 @@ function detectMMKVType(instance, key) {
69
76
  };
70
77
  }
71
78
 
79
+ // getString returned "" and nothing else matched -> a genuine empty string.
80
+ if (stringValue === '') {
81
+ return {
82
+ value: '',
83
+ type: 'string'
84
+ };
85
+ }
86
+
72
87
  // Try buffer (least common)
73
88
  const bufferValue = instance.getBuffer(key);
74
89
  if (bufferValue !== undefined) {
@@ -7,6 +7,7 @@ exports.canUndo = canUndo;
7
7
  exports.jumpToState = jumpToState;
8
8
  exports.undoOperation = undoOperation;
9
9
  var _asyncStorage = _interopRequireDefault(require("@react-native-async-storage/async-storage"));
10
+ var _asyncStorageCompat = require("./asyncStorageCompat");
10
11
  var _AsyncStorageListener = require("./AsyncStorageListener");
11
12
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
13
  /**
@@ -79,7 +80,7 @@ async function undoOperation(event) {
79
80
  // Restore all cleared key-value pairs
80
81
  const pairsToRestore = data.prevPairs.filter(([, value]) => value !== null);
81
82
  if (pairsToRestore.length > 0) {
82
- await _asyncStorage.default.multiSet(pairsToRestore);
83
+ await (0, _asyncStorageCompat.writeMany)(pairsToRestore);
83
84
  }
84
85
  }
85
86
  break;
@@ -131,7 +132,7 @@ async function jumpToState(events, targetEventIndex) {
131
132
  }
132
133
  }
133
134
  if (pairsToSet.length > 0) {
134
- await _asyncStorage.default.multiSet(pairsToSet);
135
+ await (0, _asyncStorageCompat.writeMany)(pairsToSet);
135
136
  }
136
137
  } finally {
137
138
  // Always resume capture
@@ -3,7 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.getValuePreview = getValuePreview;
6
7
  exports.getValueTypeLabel = getValueTypeLabel;
8
+ exports.getValueTypeWithPreview = getValueTypeWithPreview;
7
9
  /** Return a concise string describing the JavaScript type of the provided value. */
8
10
  function getValueTypeLabel(value) {
9
11
  if (value === null) return "null";
@@ -15,4 +17,43 @@ function getValueTypeLabel(value) {
15
17
  if (type === "number") return "number";
16
18
  if (type === "string") return "string";
17
19
  return "unknown";
20
+ }
21
+
22
+ /** Longest value preview rendered inline next to the type label. */
23
+ const MAX_INLINE_PREVIEW = 40;
24
+
25
+ /**
26
+ * Short, human-readable preview of a value for inline display next to the type
27
+ * label, or `null` when the value is too large/complex to preview (long
28
+ * strings, objects, arrays). Lets the user read simple values without opening
29
+ * the card.
30
+ */
31
+ function getValuePreview(value) {
32
+ switch (typeof value) {
33
+ case "boolean":
34
+ return value ? "true" : "false";
35
+ case "number":
36
+ return Number.isFinite(value) ? String(value) : null;
37
+ case "string":
38
+ {
39
+ if (value.length === 0) return '""';
40
+ if (value.length > MAX_INLINE_PREVIEW) return null;
41
+ // Collapse whitespace so multi-line/tabbed values stay on one line.
42
+ return `"${value.replace(/\s+/g, " ")}"`;
43
+ }
44
+ default:
45
+ return null;
46
+ // objects, arrays, null, undefined
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Type label optionally followed by a short value preview — e.g. `boolean · true`,
52
+ * `number · 42`, `string · "hello"`. Falls back to just the type label when the
53
+ * value is too large to preview.
54
+ */
55
+ function getValueTypeWithPreview(value) {
56
+ const label = getValueTypeLabel(value);
57
+ const preview = getValuePreview(value);
58
+ return preview ? `${label} · ${preview}` : label;
18
59
  }
@@ -55,6 +55,11 @@ export { undoOperation, jumpToState, canUndo } from "./storage/utils/storageTime
55
55
  // =============================================================================
56
56
  export { registerMMKVInstance, unregisterMMKVInstance } from "./storage/utils/MMKVInstanceRegistry";
57
57
 
58
+ // =============================================================================
59
+ // EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
60
+ // =============================================================================
61
+ export { storageSyncAdapter } from "./storage/sync/storageSyncAdapter";
62
+
58
63
  // =============================================================================
59
64
  // INTERNAL EXPORTS (For @buoy-gg/* packages only - not part of public API)
60
65
  // =============================================================================
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useMemo, useCallback, useState, useEffect } from "react";
4
4
  import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from "react-native";
5
+ import { absoluteFill } from "@buoy-gg/shared-ui";
5
6
  import { Database, Zap, ProBadge, ProUpgradeModal } from "@buoy-gg/shared-ui";
6
7
  import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
7
8
  import { StorageKeySection } from "./StorageKeySection";
@@ -61,7 +62,9 @@ export function GameUIStorageBrowser({
61
62
  storageDataRef,
62
63
  eventCountByKey,
63
64
  onViewHistory,
64
- enabledStorageTypes
65
+ enabledStorageTypes,
66
+ pinnedKeys,
67
+ onTogglePin
65
68
  }) {
66
69
  const isPro = useIsPro();
67
70
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
@@ -171,6 +174,15 @@ export function GameUIStorageBrowser({
171
174
  refresh();
172
175
  }, [refresh]);
173
176
 
177
+ // Hide the selected keys by adding each to the ignored-pattern filter store,
178
+ // then exit selection mode. Non-destructive — keys can be unhidden from the
179
+ // filters UI.
180
+ const handleHideKeys = useCallback(keys => {
181
+ keys.forEach(k => onAddPattern?.(k.key));
182
+ setSelectedKeyIds(new Set());
183
+ setIsSelectMode(false);
184
+ }, [onAddPattern]);
185
+
174
186
  // Memoized export data for copy functionality
175
187
  const copyExportData = useMemo(() => {
176
188
  const allKeys = allStorageKeys;
@@ -393,8 +405,14 @@ export function GameUIStorageBrowser({
393
405
  // NOTE: MMKV instance filtering is now handled in mmkvStorageKeys memo (line 128)
394
406
  // No need to filter again here
395
407
 
408
+ // Float pinned keys to the top (stable — preserves existing order otherwise)
409
+ if (pinnedKeys && pinnedKeys.size > 0) {
410
+ const pinned = keys.filter(k => pinnedKeys.has(k.key));
411
+ const rest = keys.filter(k => !pinnedKeys.has(k.key));
412
+ keys = [...pinned, ...rest];
413
+ }
396
414
  return keys;
397
- }, [sortedKeys, activeFilter, activeStorageType, ignoredPatterns, searchQuery, selectedMMKVInstance, enabledStorageTypes]);
415
+ }, [sortedKeys, activeFilter, activeStorageType, ignoredPatterns, searchQuery, selectedMMKVInstance, enabledStorageTypes, pinnedKeys]);
398
416
 
399
417
  // For free users, limit visible keys to FREE_TIER_KEY_LIMIT
400
418
  const visibleKeys = useMemo(() => {
@@ -616,6 +634,7 @@ export function GameUIStorageBrowser({
616
634
  instance: inst.instance
617
635
  })),
618
636
  onDeleteComplete: handleDeleteComplete,
637
+ onHideKeys: handleHideKeys,
619
638
  onSelectAll: handleSelectAll,
620
639
  onClearSelection: handleClearSelection,
621
640
  totalVisibleKeys: visibleKeys.length
@@ -628,7 +647,10 @@ export function GameUIStorageBrowser({
628
647
  selectedKeys: selectedKeyIds,
629
648
  onSelectionChange: handleSelectionChange,
630
649
  eventCountByKey: eventCountByKey,
631
- onViewHistory: onViewHistory
650
+ onViewHistory: onViewHistory,
651
+ onHideKey: key => handleHideKeys([key]),
652
+ pinnedKeys: pinnedKeys,
653
+ onTogglePin: onTogglePin
632
654
  }), hasLockedKeys && /*#__PURE__*/_jsxs(TouchableOpacity, {
633
655
  style: styles.limitBanner,
634
656
  onPress: () => setShowUpgradeModal(true),
@@ -682,7 +704,7 @@ const styles = StyleSheet.create({
682
704
  paddingBottom: 32
683
705
  },
684
706
  backgroundGrid: {
685
- ...StyleSheet.absoluteFillObject,
707
+ ...absoluteFill,
686
708
  opacity: 0.006,
687
709
  backgroundColor: gameUIColors.info
688
710
  },
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useRef } from "react";
4
4
  import { StyleSheet, View, Text, Animated } from "react-native";
5
+ import { absoluteFill } from "@buoy-gg/shared-ui";
5
6
  import { Database, Shield, AlertCircle, CheckCircle2, XCircle, Eye, Zap, gameUIColors, macOSColors } from "@buoy-gg/shared-ui";
6
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
8
  // Use macOS colors
@@ -513,7 +514,7 @@ const styles = StyleSheet.create({
513
514
  shadowRadius: 8
514
515
  },
515
516
  healthGridOverlay: {
516
- ...StyleSheet.absoluteFillObject,
517
+ ...absoluteFill,
517
518
  opacity: 0.1,
518
519
  borderWidth: 1,
519
520
  borderColor: "rgba(255, 255, 255, 0.1)",
@@ -536,7 +537,7 @@ const styles = StyleSheet.create({
536
537
  backgroundColor: "rgba(255, 0, 0, 0.02)"
537
538
  },
538
539
  cardGlow: {
539
- ...StyleSheet.absoluteFillObject,
540
+ ...absoluteFill,
540
541
  opacity: 0.3
541
542
  },
542
543
  cardHeader: {
@@ -2,15 +2,16 @@
2
2
 
3
3
  import { useState, useEffect } from "react";
4
4
  import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
5
- import { Trash2, CopyButton, CheckSquare, ProUpgradeModal } from "@buoy-gg/shared-ui";
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 AsyncStorage from "@react-native-async-storage/async-storage";
8
+ import { removeMany } from "../utils/asyncStorageCompat";
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  export function SelectionActionBar({
11
11
  selectedKeys,
12
12
  mmkvInstances,
13
13
  onDeleteComplete,
14
+ onHideKeys,
14
15
  onSelectAll,
15
16
  onClearSelection,
16
17
  totalVisibleKeys
@@ -41,6 +42,12 @@ export function SelectionActionBar({
41
42
  };
42
43
  };
43
44
 
45
+ // Hide selected keys (adds them to the filter store; non-destructive)
46
+ const handleHideSelected = () => {
47
+ if (selectedCount === 0) return;
48
+ onHideKeys?.(selectedKeys);
49
+ };
50
+
44
51
  // Handle delete selected keys
45
52
  const handleDeleteSelected = () => {
46
53
  if (selectedCount === 0) return;
@@ -64,7 +71,7 @@ export function SelectionActionBar({
64
71
 
65
72
  // Delete AsyncStorage keys
66
73
  if (asyncKeys.length > 0) {
67
- await AsyncStorage.multiRemove(asyncKeys.map(k => k.key));
74
+ await removeMany(asyncKeys.map(k => k.key));
68
75
  }
69
76
 
70
77
  // Delete MMKV keys
@@ -127,6 +134,13 @@ export function SelectionActionBar({
127
134
  success: macOSColors.semantic.success,
128
135
  error: macOSColors.semantic.error
129
136
  }
137
+ }), onHideKeys && /*#__PURE__*/_jsx(TouchableOpacity, {
138
+ style: styles.actionButton,
139
+ onPress: handleHideSelected,
140
+ children: /*#__PURE__*/_jsx(EyeOff, {
141
+ size: 14,
142
+ color: macOSColors.semantic.warning
143
+ })
130
144
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
131
145
  style: styles.actionButton,
132
146
  onPress: handleDeleteSelected,
@@ -16,7 +16,9 @@ export function StorageBrowserMode({
16
16
  storageDataRef,
17
17
  eventCountByKey,
18
18
  onViewHistory,
19
- enabledStorageTypes
19
+ enabledStorageTypes,
20
+ pinnedKeys,
21
+ onTogglePin
20
22
  }) {
21
23
  return /*#__PURE__*/_jsx(GameUIStorageBrowser, {
22
24
  requiredStorageKeys: requiredStorageKeys,
@@ -28,6 +30,8 @@ export function StorageBrowserMode({
28
30
  storageDataRef: storageDataRef,
29
31
  eventCountByKey: eventCountByKey,
30
32
  onViewHistory: onViewHistory,
31
- enabledStorageTypes: enabledStorageTypes
33
+ enabledStorageTypes: enabledStorageTypes,
34
+ pinnedKeys: pinnedKeys,
35
+ onTogglePin: onTogglePin
32
36
  });
33
37
  }
@@ -235,7 +235,7 @@ export function StorageEventDetailContent({
235
235
  children: "CURRENT VALUE"
236
236
  }), /*#__PURE__*/_jsxs(View, {
237
237
  style: styles.valueHeaderBadges,
238
- children: [action && /*#__PURE__*/_jsx(View, {
238
+ children: [action ? /*#__PURE__*/_jsx(View, {
239
239
  style: [styles.actionBadge, {
240
240
  backgroundColor: `${actionColor}20`
241
241
  }],
@@ -245,7 +245,7 @@ export function StorageEventDetailContent({
245
245
  }],
246
246
  children: translateStorageAction(action)
247
247
  })
248
- }), /*#__PURE__*/_jsx(View, {
248
+ }) : null, /*#__PURE__*/_jsx(View, {
249
249
  style: styles.typeBadge,
250
250
  children: /*#__PURE__*/_jsx(Text, {
251
251
  style: styles.typeText,
@@ -147,10 +147,10 @@ export function StorageKeyCard({
147
147
  children: [/*#__PURE__*/_jsx(Text, {
148
148
  style: styles.storageKeyText,
149
149
  children: storageKey.key
150
- }), storageKey.description && /*#__PURE__*/_jsx(Text, {
150
+ }), storageKey.description ? /*#__PURE__*/_jsx(Text, {
151
151
  style: styles.descriptionText,
152
152
  children: storageKey.description
153
- }), /*#__PURE__*/_jsxs(View, {
153
+ }) : null, /*#__PURE__*/_jsxs(View, {
154
154
  style: styles.cardHeaderMeta,
155
155
  children: [/*#__PURE__*/_jsx(View, {
156
156
  style: [styles.statusBadge, {
@@ -279,13 +279,13 @@ export function StorageKeyCard({
279
279
  style: styles.typeHelperText,
280
280
  children: ["Current type: ", getValueTypeLabel(storageKey.value)]
281
281
  })]
282
- }), storageKey.lastUpdated && /*#__PURE__*/_jsx(View, {
282
+ }), storageKey.lastUpdated ? /*#__PURE__*/_jsx(View, {
283
283
  style: styles.metaInfo,
284
284
  children: /*#__PURE__*/_jsxs(Text, {
285
285
  style: styles.metaLabel,
286
- children: ["Last updated123: ", storageKey.lastUpdated.toLocaleString()]
286
+ children: ["Last updated: ", storageKey.lastUpdated.toLocaleString()]
287
287
  })
288
- })]
288
+ }) : null]
289
289
  }), isExpanded && !hasValue && /*#__PURE__*/_jsxs(View, {
290
290
  style: styles.cardBody,
291
291
  children: [/*#__PURE__*/_jsxs(View, {
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
4
- import { macOSColors, CompactRow, TypeBadge, HardDrive, Square, CheckSquare, ExpandedInfoRow, PillBadge } from "@buoy-gg/shared-ui";
4
+ import { macOSColors, CompactRow, TypeBadge, HardDrive, Square, CheckSquare, EyeOff, Pin, ExpandedInfoRow, PillBadge } from "@buoy-gg/shared-ui";
5
5
  import { getStorageTypeLabel } from "../utils/storageQueryUtils";
6
- import { getValueTypeLabel } from "../utils/valueType";
6
+ import { getValueTypeLabel, getValueTypeWithPreview } from "../utils/valueType";
7
7
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
8
8
 
9
9
  // MMKV Instance color palette - consistent colors per instance
@@ -78,11 +78,18 @@ export function StorageKeyRow({
78
78
  isSelected = false,
79
79
  onSelectionChange,
80
80
  eventCount,
81
- onViewHistory
81
+ onViewHistory,
82
+ onHideKey,
83
+ isPinned = false,
84
+ onTogglePin
82
85
  }) {
83
86
  const config = getStatusConfig(storageKey.status);
84
87
  const hasValue = storageKey.value !== undefined && storageKey.value !== null;
85
88
 
89
+ // Booleans get a dedicated colored badge instead of an inline text preview so
90
+ // their value is scannable at a glance.
91
+ const isBoolean = hasValue && typeof storageKey.value === "boolean";
92
+
86
93
  // Format primary text - show the key
87
94
  const primaryText = storageKey.key;
88
95
 
@@ -186,7 +193,7 @@ export function StorageKeyRow({
186
193
  style: styles.expandedExpected,
187
194
  children: String(storageKey.expectedValue)
188
195
  })]
189
- }), storageKey.description && /*#__PURE__*/_jsxs(View, {
196
+ }), storageKey.description ? /*#__PURE__*/_jsxs(View, {
190
197
  style: styles.expandedRow,
191
198
  children: [/*#__PURE__*/_jsx(Text, {
192
199
  style: styles.expandedLabel,
@@ -195,7 +202,46 @@ export function StorageKeyRow({
195
202
  style: styles.expandedDescription,
196
203
  children: storageKey.description
197
204
  })]
198
- })]
205
+ }) : null, onTogglePin || onHideKey ? /*#__PURE__*/_jsxs(View, {
206
+ style: styles.actionRow,
207
+ children: [onTogglePin ? /*#__PURE__*/_jsxs(TouchableOpacity, {
208
+ style: [styles.actionChip, isPinned && styles.actionChipActive],
209
+ onPress: () => onTogglePin(storageKey.key),
210
+ hitSlop: {
211
+ top: 6,
212
+ bottom: 6,
213
+ left: 6,
214
+ right: 6
215
+ },
216
+ children: [/*#__PURE__*/_jsx(Pin, {
217
+ size: 12,
218
+ color: macOSColors.semantic.info
219
+ }), /*#__PURE__*/_jsx(Text, {
220
+ style: [styles.actionChipText, {
221
+ color: macOSColors.semantic.info
222
+ }],
223
+ children: isPinned ? "Unpin" : "Pin to top"
224
+ })]
225
+ }) : null, onHideKey ? /*#__PURE__*/_jsxs(TouchableOpacity, {
226
+ style: styles.actionChip,
227
+ onPress: () => onHideKey(storageKey),
228
+ hitSlop: {
229
+ top: 6,
230
+ bottom: 6,
231
+ left: 6,
232
+ right: 6
233
+ },
234
+ children: [/*#__PURE__*/_jsx(EyeOff, {
235
+ size: 12,
236
+ color: macOSColors.semantic.warning
237
+ }), /*#__PURE__*/_jsx(Text, {
238
+ style: [styles.actionChipText, {
239
+ color: macOSColors.semantic.warning
240
+ }],
241
+ children: "Hide from list"
242
+ })]
243
+ }) : null]
244
+ }) : null]
199
245
  });
200
246
 
201
247
  // Handle checkbox press in select mode
@@ -230,17 +276,28 @@ export function StorageKeyRow({
230
276
  statusLabel: config.label,
231
277
  statusSublabel: config.sublabel,
232
278
  primaryText: primaryText,
233
- secondaryText: hasValue ? getValueTypeLabel(storageKey.value) : undefined,
279
+ secondaryText: hasValue ? isBoolean ? getValueTypeLabel(storageKey.value) : getValueTypeWithPreview(storageKey.value) : undefined,
280
+ secondaryAccessory: isBoolean ? /*#__PURE__*/_jsx(PillBadge, {
281
+ size: "sm",
282
+ color: storageKey.value ? macOSColors.semantic.success : macOSColors.semantic.error,
283
+ children: storageKey.value ? "true" : "false"
284
+ }) : undefined,
234
285
  expandedContent: expandedContent,
235
286
  isExpanded: isExpanded,
236
287
  expandedGlowColor: config.color,
237
- customBadge: /*#__PURE__*/_jsx(PillBadge, {
238
- color: getStorageTypeColor(storageKey.storageType),
239
- children: storageTypeLabel
288
+ customBadge: /*#__PURE__*/_jsxs(View, {
289
+ style: styles.badgeRow,
290
+ children: [isPinned && /*#__PURE__*/_jsx(Pin, {
291
+ size: 12,
292
+ color: macOSColors.semantic.info
293
+ }), /*#__PURE__*/_jsx(PillBadge, {
294
+ color: getStorageTypeColor(storageKey.storageType),
295
+ children: storageTypeLabel
296
+ })]
240
297
  }),
241
298
  showChevron: !isSelectMode,
242
299
  onPress: isSelectMode ? handleCheckboxPress : onPress ? () => onPress(storageKey) : undefined
243
- }), eventCount != null && eventCount > 0 && /*#__PURE__*/_jsx(View, {
300
+ }), eventCount != null && eventCount > 1 && /*#__PURE__*/_jsx(View, {
244
301
  style: [styles.absCountBadge, {
245
302
  backgroundColor: macOSColors.semantic.warning + "22",
246
303
  borderColor: macOSColors.semantic.warning + "55"
@@ -269,6 +326,11 @@ const getStorageTypeColor = storageType => {
269
326
  }
270
327
  };
271
328
  const styles = StyleSheet.create({
329
+ badgeRow: {
330
+ flexDirection: "row",
331
+ alignItems: "center",
332
+ gap: 6
333
+ },
272
334
  expandedContainer: {
273
335
  gap: 8
274
336
  },
@@ -342,6 +404,33 @@ const styles = StyleSheet.create({
342
404
  fontWeight: "700",
343
405
  fontFamily: "monospace"
344
406
  },
407
+ actionRow: {
408
+ flexDirection: "row",
409
+ alignItems: "center",
410
+ flexWrap: "wrap",
411
+ gap: 8,
412
+ marginTop: 4
413
+ },
414
+ actionChip: {
415
+ flexDirection: "row",
416
+ alignItems: "center",
417
+ gap: 6,
418
+ paddingVertical: 6,
419
+ paddingHorizontal: 10,
420
+ borderRadius: 6,
421
+ backgroundColor: macOSColors.background.card,
422
+ borderWidth: 1,
423
+ borderColor: macOSColors.border.default
424
+ },
425
+ actionChipActive: {
426
+ backgroundColor: macOSColors.semantic.info + "15",
427
+ borderColor: macOSColors.semantic.info + "44"
428
+ },
429
+ actionChipText: {
430
+ fontSize: 11,
431
+ fontWeight: "600",
432
+ fontFamily: "monospace"
433
+ },
345
434
  viewHistoryButton: {
346
435
  marginLeft: 4
347
436
  },