@buoy-gg/storage 7.0.4 → 7.0.5

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
@@ -3,7 +3,7 @@
3
3
  import { useCallback, useState } from "react";
4
4
  import { View, Text, StyleSheet } from "react-native";
5
5
  import { StorageKeyRow } from "./StorageKeyRow";
6
- import { SectionHeader } from "@buoy-gg/shared-ui";
6
+ import { SectionHeader, macOSColors } from "@buoy-gg/shared-ui";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  /**
9
9
  * Storage key section component following composition principles [[rule3]]
@@ -26,7 +26,10 @@ export function StorageKeySection({
26
26
  onViewHistory,
27
27
  onHideKey,
28
28
  pinnedKeys,
29
- onTogglePin
29
+ onTogglePin,
30
+ onEditKey,
31
+ onToggleKey,
32
+ editBlockedFor
30
33
  }) {
31
34
  const [expandedKey, setExpandedKey] = useState(null);
32
35
  const handleKeyPress = useCallback(storageKey => {
@@ -81,6 +84,9 @@ export function StorageKeySection({
81
84
  eventCount: eventCountByKey?.[storageKey.key],
82
85
  onViewHistory: onViewHistory ? () => onViewHistory(storageKey.key) : undefined,
83
86
  onHideKey: onHideKey,
87
+ onEditKey: onEditKey,
88
+ onToggleKey: onToggleKey,
89
+ editBlockedReason: editBlockedFor?.(storageKey),
84
90
  isPinned: pinnedKeys?.has(storageKey.key),
85
91
  onTogglePin: onTogglePin
86
92
  }, uniqueKey);
@@ -96,15 +102,15 @@ const styles = StyleSheet.create({
96
102
  // No gap needed, StorageKeyRow has its own margins
97
103
  },
98
104
  emptySection: {
99
- backgroundColor: "rgba(255, 255, 255, 0.02)",
105
+ backgroundColor: "rgba(128, 128, 128, 0.04)",
100
106
  borderRadius: 6,
101
107
  padding: 16,
102
108
  borderWidth: 1,
103
- borderColor: "rgba(255, 255, 255, 0.05)",
109
+ borderColor: "rgba(128, 128, 128, 0.1)",
104
110
  alignItems: "center"
105
111
  },
106
112
  emptySectionText: {
107
- color: "#6B7280",
113
+ color: macOSColors.text.icon,
108
114
  fontSize: 11,
109
115
  textAlign: "center"
110
116
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { View, Text, StyleSheet } from "react-native";
4
4
  import { AlertCircle, CheckCircle2, XCircle, Eye, Database, Shield } from "@buoy-gg/shared-ui";
5
+ import { macOSColors, buoyColors } from "@buoy-gg/shared-ui";
5
6
  import { getStorageTypeHexColor } from "../utils/storageQueryUtils";
6
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
8
  // Variable type configurations matching env vars design
@@ -255,15 +256,15 @@ export function StorageKeyStatsSection({
255
256
  const styles = StyleSheet.create({
256
257
  statsContainer: {
257
258
  marginBottom: 24,
258
- backgroundColor: "rgba(255, 255, 255, 0.03)",
259
+ backgroundColor: "rgba(128, 128, 128, 0.06)",
259
260
  borderRadius: 8,
260
261
  padding: 16,
261
262
  borderWidth: 1,
262
- borderColor: "rgba(255, 255, 255, 0.08)"
263
+ borderColor: "rgba(128, 128, 128, 0.16)"
263
264
  },
264
265
  // Section titles
265
266
  sectionTitle: {
266
- color: "#9CA3AF",
267
+ color: buoyColors.textSecondary,
267
268
  fontSize: 12,
268
269
  fontWeight: "500",
269
270
  marginBottom: 16,
@@ -279,11 +280,11 @@ const styles = StyleSheet.create({
279
280
  gap: 12
280
281
  },
281
282
  breakdownItem: {
282
- backgroundColor: "rgba(255, 255, 255, 0.02)",
283
+ backgroundColor: "rgba(128, 128, 128, 0.04)",
283
284
  padding: 12,
284
285
  borderRadius: 8,
285
286
  borderWidth: 1,
286
- borderColor: "rgba(255, 255, 255, 0.05)"
287
+ borderColor: "rgba(128, 128, 128, 0.1)"
287
288
  },
288
289
  breakdownItemRow: {
289
290
  flexDirection: "row",
@@ -306,12 +307,12 @@ const styles = StyleSheet.create({
306
307
  minWidth: 0
307
308
  },
308
309
  breakdownItemLabel: {
309
- color: "#FFFFFF",
310
+ color: macOSColors.text.primary,
310
311
  fontSize: 14,
311
312
  fontWeight: "500"
312
313
  },
313
314
  breakdownItemDesc: {
314
- color: "#9CA3AF",
315
+ color: buoyColors.textSecondary,
315
316
  fontSize: 12
316
317
  },
317
318
  breakdownItemRight: {
@@ -322,18 +323,18 @@ const styles = StyleSheet.create({
322
323
  fontWeight: "600"
323
324
  },
324
325
  breakdownPercentage: {
325
- color: "#6B7280",
326
+ color: macOSColors.text.icon,
326
327
  fontSize: 10
327
328
  },
328
329
  // Empty state
329
330
  emptyState: {
330
331
  padding: 16,
331
- backgroundColor: "rgba(255, 255, 255, 0.02)",
332
+ backgroundColor: "rgba(128, 128, 128, 0.04)",
332
333
  borderRadius: 6,
333
334
  alignItems: "center"
334
335
  },
335
336
  emptyStateText: {
336
- color: "#6B7280",
337
+ color: macOSColors.text.icon,
337
338
  fontSize: 11,
338
339
  textAlign: "center"
339
340
  }
@@ -3,8 +3,9 @@
3
3
  import { useState, useCallback, useEffect, useRef, useMemo } from "react";
4
4
  import { Text, View, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
5
5
  import AsyncStorage from "@react-native-async-storage/async-storage";
6
- import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, Database, Filter, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
6
+ import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, Database, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
7
7
  import { StorageBrowserMode } from "./StorageBrowserMode";
8
+ import { StorageTypeFilterBar, countByStorageType } from "./StorageTypeFilterBar";
8
9
  import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
9
10
  import { ProFeatureBanner } from "@buoy-gg/shared-ui";
10
11
  import { useStorageEvents } from "../hooks/useStorageEvents";
@@ -25,10 +26,16 @@ export function StorageModalWithTabs({
25
26
  onBack,
26
27
  onMinimize,
27
28
  enableSharedModalDimensions = false,
28
- requiredStorageKeys = []
29
+ requiredStorageKeys = [],
30
+ inline = false,
31
+ onTabChange,
32
+ onRefresh
29
33
  }) {
30
34
  const isPro = useIsPro();
31
35
  const [activeTab, setActiveTab] = useState("browser");
36
+ useEffect(() => {
37
+ onTabChange?.(activeTab);
38
+ }, [activeTab, onTabChange]);
32
39
 
33
40
  // Storage Browser state
34
41
  const [searchQuery, setSearchQuery] = useState("");
@@ -54,6 +61,19 @@ export function StorageModalWithTabs({
54
61
  const [showFilters, setShowFilters] = useState(false);
55
62
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["@react_buoy", "@RNAsyncStorage", "redux-persist", "persist:"]) // Auto-hide dev tool keys by default
56
63
  );
64
+ /**
65
+ * Which backend both tabs are looking at.
66
+ *
67
+ * Owned here rather than inside the browser because the Keys and Events tabs
68
+ * answer questions about the same three backends — filter the key list to
69
+ * Keychain, switch to Events to see what wrote to it, and the filter should
70
+ * still be Keychain.
71
+ *
72
+ * Distinct from `enabledStorageTypes` below: that's the persisted "capture
73
+ * this backend at all" setting from the Filters view. This is a view filter,
74
+ * and it never hides data the other one already dropped.
75
+ */
76
+ const [storageTypeFilter, setStorageTypeFilter] = useState("all");
57
77
  const [enabledStorageTypes, setEnabledStorageTypes] = useState(new Set(['async', 'mmkv', 'secure']) // All enabled by default
58
78
  );
59
79
  const [pinnedKeys, setPinnedKeys] = useState(new Set());
@@ -65,8 +85,13 @@ export function StorageModalWithTabs({
65
85
 
66
86
  // isListening = enabled and capturing (has subscribers)
67
87
  const isListening = isListeningEnabled && isCapturing;
68
- const handleModeChange = useCallback(_mode => {
69
- // Mode changes handled by JsModal
88
+
89
+ // Track the modal's presentation mode so the stepper footer only reserves
90
+ // safe-area bottom padding while docked as a bottom sheet. In floating mode
91
+ // the footer isn't pinned to the screen edge, so that inset is dead space.
92
+ const [modalMode, setModalMode] = useState("bottomSheet");
93
+ const handleModeChange = useCallback(mode => {
94
+ setModalMode(mode);
70
95
  }, []);
71
96
 
72
97
  // Timer removed - using useTickEveryMinute hook instead
@@ -412,14 +437,34 @@ export function StorageModalWithTabs({
412
437
  return counts;
413
438
  }, [conversations]);
414
439
 
440
+ /**
441
+ * When each key was last written, for the wide table's "Last write" column.
442
+ * Derived from the recorded event stream, NOT from the key read: a storage
443
+ * read has no timestamp of its own, so a key we never witnessed being written
444
+ * has no honest answer here and is simply absent.
445
+ */
446
+ const lastWriteByKey = useMemo(() => {
447
+ const at = {};
448
+ conversations.forEach(conv => {
449
+ at[conv.key] = conv.lastEvent.timestamp.getTime();
450
+ });
451
+ return at;
452
+ }, [conversations]);
453
+
415
454
  // Selected history conversation (from browser tab "view history")
416
455
  const selectedHistoryConversation = useMemo(() => {
417
456
  if (!selectedHistoryKey) return null;
418
457
  return conversations.find(c => c.key === selectedHistoryKey) || null;
419
458
  }, [selectedHistoryKey, conversations]);
420
459
 
421
- // Filtered raw events for the events tab (chronological stream, newest-first)
422
- const filteredEvents = useMemo(() => {
460
+ /**
461
+ * Events passing everything EXCEPT the backend pills.
462
+ *
463
+ * Split out so the pills can show what each one would reveal. Counting the
464
+ * already-filtered list would zero out the three types you didn't pick, which
465
+ * tells you nothing about where to look next.
466
+ */
467
+ const eventsBeforeTypeFilter = useMemo(() => {
423
468
  return events.filter(event => {
424
469
  if (!event.data?.key) return false;
425
470
  if (!enabledStorageTypes.has(event.storageType)) return false;
@@ -427,6 +472,13 @@ export function StorageModalWithTabs({
427
472
  return !Array.from(ignoredPatterns).some(p => key.includes(p));
428
473
  });
429
474
  }, [events, ignoredPatterns, enabledStorageTypes]);
475
+ const eventTypeCounts = useMemo(() => countByStorageType(eventsBeforeTypeFilter), [eventsBeforeTypeFilter]);
476
+
477
+ // Filtered raw events for the events tab (chronological stream, newest-first)
478
+ const filteredEvents = useMemo(() => {
479
+ if (storageTypeFilter === "all") return eventsBeforeTypeFilter;
480
+ return eventsBeforeTypeFilter.filter(event => event.storageType === storageTypeFilter);
481
+ }, [eventsBeforeTypeFilter, storageTypeFilter]);
430
482
  const getEventsSnapshot = useCallback(() => {
431
483
  return filteredEvents.map(event => ({
432
484
  key: event.data?.key,
@@ -547,10 +599,17 @@ export function StorageModalWithTabs({
547
599
  searchQuery: searchQuery,
548
600
  storageDataRef: storageDataRef,
549
601
  eventCountByKey: eventCountByKey,
602
+ lastWriteByKey: lastWriteByKey,
550
603
  onViewHistory: handleViewHistoryFromBrowser,
551
604
  enabledStorageTypes: enabledStorageTypes,
552
605
  pinnedKeys: pinnedKeys,
553
606
  onTogglePin: handleTogglePin
607
+ // Only a docked sheet sits on the screen's bottom edge; a floating
608
+ // window's own bottom is well above the home indicator.
609
+ ,
610
+ applySafeAreaInset: modalMode === "bottomSheet",
611
+ storageTypeFilter: storageTypeFilter,
612
+ onStorageTypeFilterChange: setStorageTypeFilter
554
613
  });
555
614
  }
556
615
 
@@ -566,48 +625,75 @@ export function StorageModalWithTabs({
566
625
  })
567
626
  });
568
627
  }
628
+
629
+ // The pills render ABOVE the empty state as well as the list. Filtering to
630
+ // a backend that happens to have no events would otherwise hide the only
631
+ // control that undoes it, leaving the tab looking permanently broken.
632
+ const eventTypeFilterBar = /*#__PURE__*/_jsx(View, {
633
+ style: styles.eventFilterBar,
634
+ children: /*#__PURE__*/_jsx(StorageTypeFilterBar, {
635
+ active: storageTypeFilter,
636
+ onChange: setStorageTypeFilter,
637
+ counts: eventTypeCounts
638
+ })
639
+ });
569
640
  if (filteredEvents.length === 0) {
641
+ const filtered = storageTypeFilter !== "all";
570
642
  return /*#__PURE__*/_jsxs(View, {
571
- style: styles.emptyState,
572
- children: [/*#__PURE__*/_jsx(Database, {
573
- size: 48,
574
- color: macOSColors.text.muted
575
- }), /*#__PURE__*/_jsx(Text, {
576
- style: styles.emptyTitle,
577
- children: isListening ? "No storage events yet" : "Event listener is paused"
578
- }), /*#__PURE__*/_jsx(Text, {
579
- style: styles.emptySubtitle,
580
- children: isListening ? "Storage operations will appear here" : "Press play to start monitoring"
643
+ style: styles.flexFill,
644
+ children: [eventTypeFilterBar, /*#__PURE__*/_jsxs(View, {
645
+ style: styles.emptyState,
646
+ children: [/*#__PURE__*/_jsx(Database, {
647
+ size: 48,
648
+ color: macOSColors.text.muted
649
+ }), /*#__PURE__*/_jsx(Text, {
650
+ style: styles.emptyTitle,
651
+ children: !isListening ? "Event listener is paused" : filtered ? `No ${storageTypeFilter} events` : "No storage events yet"
652
+ }), /*#__PURE__*/_jsx(Text, {
653
+ style: styles.emptySubtitle,
654
+ children: !isListening ? "Press play to start monitoring" : filtered ? "Other backends have events — pick All Types to see them" : "Storage operations will appear here"
655
+ })]
581
656
  })]
582
657
  });
583
658
  }
584
- return /*#__PURE__*/_jsx(FlatList, {
659
+ return /*#__PURE__*/_jsxs(View, {
585
660
  style: styles.flexFill,
586
- data: visibleEvents,
587
- renderItem: renderEventItem,
588
- keyExtractor: eventKeyExtractor,
589
- onEndReachedThreshold: END_REACHED_THRESHOLD,
590
- contentContainerStyle: styles.listContent,
591
- ListFooterComponent: lockedEventCount > 0 ? /*#__PURE__*/_jsx(View, {
592
- style: styles.lockedBannerContainer,
593
- children: /*#__PURE__*/_jsx(ProFeatureBanner, {
594
- featureName: `${lockedEventCount} event${lockedEventCount > 1 ? 's' : ''} locked`,
595
- description: "Upgrade to Pro to unlock full event history"
596
- })
597
- }) : null,
598
- initialNumToRender: 10,
599
- maxToRenderPerBatch: 10,
600
- windowSize: 10
661
+ children: [eventTypeFilterBar, /*#__PURE__*/_jsx(FlatList, {
662
+ style: styles.flexFill,
663
+ data: visibleEvents,
664
+ renderItem: renderEventItem,
665
+ keyExtractor: eventKeyExtractor,
666
+ onEndReachedThreshold: END_REACHED_THRESHOLD,
667
+ contentContainerStyle: styles.listContent,
668
+ ListFooterComponent: lockedEventCount > 0 ? /*#__PURE__*/_jsx(View, {
669
+ style: styles.lockedBannerContainer,
670
+ children: /*#__PURE__*/_jsx(ProFeatureBanner, {
671
+ featureName: `${lockedEventCount} event${lockedEventCount > 1 ? 's' : ''} locked`,
672
+ description: "Upgrade to Pro to unlock full event history"
673
+ })
674
+ }) : null,
675
+ initialNumToRender: 10,
676
+ maxToRenderPerBatch: 10,
677
+ windowSize: 10
678
+ })]
601
679
  });
602
680
  };
603
- const footerNode = selectedConversation ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
681
+
682
+ // The stepper renders nothing for a single-event conversation
683
+ // (EventStepperFooter bails at totalItems <= 1), but an element is truthy
684
+ // either way — so `footerHeight={footerNode ? 68 : 0}` below used to reserve
685
+ // 68px for a footer that drew nothing, leaving a dead band under the content.
686
+ // Gate the node on the event count so the reserve matches what's painted.
687
+ const footerNode = selectedConversation && selectedConversation.events.length > 1 ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
604
688
  conversation: selectedConversation,
605
689
  selectedEventIndex: selectedEventIndex,
606
- onEventIndexChange: setSelectedEventIndex
607
- }) : selectedHistoryConversation && selectedHistoryEventIndex !== null ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
690
+ onEventIndexChange: setSelectedEventIndex,
691
+ applySafeAreaInset: modalMode === "bottomSheet"
692
+ }) : selectedHistoryConversation && selectedHistoryEventIndex !== null && selectedHistoryConversation.events.length > 1 ? /*#__PURE__*/_jsx(StorageEventDetailFooter, {
608
693
  conversation: selectedHistoryConversation,
609
694
  selectedEventIndex: selectedHistoryEventIndex,
610
- onEventIndexChange: setSelectedHistoryEventIndex
695
+ onEventIndexChange: setSelectedHistoryEventIndex,
696
+ applySafeAreaInset: modalMode === "bottomSheet"
611
697
  }) : null;
612
698
 
613
699
  // Determine the appropriate back handler based on current view state
@@ -615,6 +701,122 @@ export function StorageModalWithTabs({
615
701
  setSelectedConversationKey(null);
616
702
  setSelectedEventIndex(0);
617
703
  } : onBack;
704
+ const headerContent = showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
705
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
706
+ onBack: () => setShowFilters(false)
707
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
708
+ title: "Filters"
709
+ })]
710
+ }) : selectedHistoryKey ? /*#__PURE__*/_jsxs(ModalHeader, {
711
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
712
+ onBack: selectedHistoryEventIndex !== null ? () => setSelectedHistoryEventIndex(null) : handleBackFromHistory
713
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
714
+ title: `${selectedHistoryKey} History`,
715
+ centered: true
716
+ })]
717
+ }) : selectedConversation ? /*#__PURE__*/_jsxs(ModalHeader, {
718
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
719
+ onBack: () => {
720
+ setSelectedConversationKey(null);
721
+ setSelectedEventIndex(0);
722
+ }
723
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
724
+ title: selectedConversation.key
725
+ })]
726
+ }) : /*#__PURE__*/_jsxs(ModalHeader, {
727
+ children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
728
+ onBack: onBack
729
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
730
+ title: "",
731
+ children: isSearchActive ? /*#__PURE__*/_jsx(SearchBar, {
732
+ value: searchQuery,
733
+ onChange: setSearchQuery,
734
+ onClear: () => {
735
+ setSearchQuery("");
736
+ setIsSearchActive(false);
737
+ },
738
+ placeholder: "Search storage keys...",
739
+ autoFocus: true,
740
+ onSubmitEditing: () => setIsSearchActive(false),
741
+ returnKeyType: "search",
742
+ suggestions: allStorageKeys,
743
+ containerStyle: styles.headerSearchContainer
744
+ }) : /*#__PURE__*/_jsx(TabSelector, {
745
+ tabs: [{
746
+ key: "browser",
747
+ label: "Storage"
748
+ }, {
749
+ key: "events",
750
+ label: `Events${filteredEvents.length > 0 ? ` (${filteredEvents.length})` : ""}`
751
+ }],
752
+ activeTab: activeTab,
753
+ onTabChange: tab => {
754
+ setActiveTab(tab);
755
+ setSelectedHistoryKey(null);
756
+ setSelectedHistoryEventIndex(null);
757
+ setSelectedConversationKey(null);
758
+ setSelectedEventIndex(0);
759
+ }
760
+ })
761
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
762
+ children: [onRefresh && !isSearchActive ? /*#__PURE__*/_jsx(TouchableOpacity, {
763
+ onPress: onRefresh,
764
+ style: styles.iconButton,
765
+ accessibilityLabel: "Refresh storage",
766
+ children: /*#__PURE__*/_jsx(RefreshCw, {
767
+ size: 14,
768
+ color: macOSColors.text.secondary
769
+ })
770
+ }) : null, /*#__PURE__*/_jsx(TouchableOpacity, {
771
+ onPress: handleToggleFilters,
772
+ style: [styles.iconButton, (ignoredPatterns.size > 0 || enabledStorageTypes.size < 3) && styles.activeFilterButton],
773
+ children: /*#__PURE__*/_jsx(Filter, {
774
+ size: 14,
775
+ color: ignoredPatterns.size > 0 || enabledStorageTypes.size < 3 ? macOSColors.semantic.debug : macOSColors.text.secondary
776
+ })
777
+ }), activeTab === "browser" && !isSearchActive && /*#__PURE__*/_jsxs(_Fragment, {
778
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
779
+ onPress: () => setIsSearchActive(true),
780
+ style: styles.iconButton,
781
+ children: /*#__PURE__*/_jsx(Search, {
782
+ size: 14,
783
+ color: macOSColors.text.secondary
784
+ })
785
+ }), /*#__PURE__*/_jsx(ToolbarCopyButton, {
786
+ value: getStorageSnapshot,
787
+ disabled: storageDataRef.current.length === 0,
788
+ buttonStyle: styles.iconButton
789
+ })]
790
+ }), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
791
+ children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
792
+ value: getEventsSnapshot,
793
+ disabled: filteredEvents.length === 0,
794
+ buttonStyle: styles.iconButton
795
+ }), /*#__PURE__*/_jsx(PowerToggleButton, {
796
+ isEnabled: isListening,
797
+ onToggle: handleToggleListening,
798
+ accessibilityLabel: "Toggle storage event monitoring"
799
+ }), /*#__PURE__*/_jsx(ToolbarClearButton, {
800
+ onPress: handleClearEvents,
801
+ buttonStyle: styles.iconButton
802
+ })]
803
+ })]
804
+ })]
805
+ });
806
+
807
+ // The ONLY difference between the phone and the desktop dashboard is which
808
+ // shell the header, body and footer go in. Everything above this is shared.
809
+ if (inline) {
810
+ return /*#__PURE__*/_jsx(TickProvider, {
811
+ children: /*#__PURE__*/_jsxs(View, {
812
+ style: styles.inlineRoot,
813
+ children: [/*#__PURE__*/_jsx(View, {
814
+ style: styles.inlineHeader,
815
+ children: headerContent
816
+ }), renderContent(), footerNode]
817
+ })
818
+ });
819
+ }
618
820
  return /*#__PURE__*/_jsx(TickProvider, {
619
821
  children: /*#__PURE__*/_jsx(JsModal, {
620
822
  visible: visible,
@@ -624,100 +826,7 @@ export function StorageModalWithTabs({
624
826
  persistenceKey: persistenceKey,
625
827
  header: {
626
828
  showToggleButton: true,
627
- customContent: showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
628
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
629
- onBack: () => setShowFilters(false)
630
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
631
- title: "Filters"
632
- })]
633
- }) : selectedHistoryKey ? /*#__PURE__*/_jsxs(ModalHeader, {
634
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
635
- onBack: selectedHistoryEventIndex !== null ? () => setSelectedHistoryEventIndex(null) : handleBackFromHistory
636
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
637
- title: `${selectedHistoryKey} History`,
638
- centered: true
639
- })]
640
- }) : selectedConversation ? /*#__PURE__*/_jsxs(ModalHeader, {
641
- children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
642
- onBack: () => {
643
- setSelectedConversationKey(null);
644
- setSelectedEventIndex(0);
645
- }
646
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
647
- title: selectedConversation.key
648
- })]
649
- }) : /*#__PURE__*/_jsxs(ModalHeader, {
650
- children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
651
- onBack: onBack
652
- }), /*#__PURE__*/_jsx(ModalHeader.Content, {
653
- title: "",
654
- children: isSearchActive ? /*#__PURE__*/_jsx(SearchBar, {
655
- value: searchQuery,
656
- onChange: setSearchQuery,
657
- onClear: () => {
658
- setSearchQuery("");
659
- setIsSearchActive(false);
660
- },
661
- placeholder: "Search storage keys...",
662
- autoFocus: true,
663
- onSubmitEditing: () => setIsSearchActive(false),
664
- returnKeyType: "search",
665
- suggestions: allStorageKeys,
666
- containerStyle: styles.headerSearchContainer
667
- }) : /*#__PURE__*/_jsx(TabSelector, {
668
- tabs: [{
669
- key: "browser",
670
- label: "Storage"
671
- }, {
672
- key: "events",
673
- label: `Events${filteredEvents.length > 0 ? ` (${filteredEvents.length})` : ""}`
674
- }],
675
- activeTab: activeTab,
676
- onTabChange: tab => {
677
- setActiveTab(tab);
678
- setSelectedHistoryKey(null);
679
- setSelectedHistoryEventIndex(null);
680
- setSelectedConversationKey(null);
681
- setSelectedEventIndex(0);
682
- }
683
- })
684
- }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
685
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
686
- onPress: handleToggleFilters,
687
- style: [styles.iconButton, (ignoredPatterns.size > 0 || enabledStorageTypes.size < 3) && styles.activeFilterButton],
688
- children: /*#__PURE__*/_jsx(Filter, {
689
- size: 14,
690
- color: ignoredPatterns.size > 0 || enabledStorageTypes.size < 3 ? macOSColors.semantic.debug : macOSColors.text.secondary
691
- })
692
- }), activeTab === "browser" && !isSearchActive && /*#__PURE__*/_jsxs(_Fragment, {
693
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
694
- onPress: () => setIsSearchActive(true),
695
- style: styles.iconButton,
696
- children: /*#__PURE__*/_jsx(Search, {
697
- size: 14,
698
- color: macOSColors.text.secondary
699
- })
700
- }), /*#__PURE__*/_jsx(ToolbarCopyButton, {
701
- value: getStorageSnapshot,
702
- disabled: storageDataRef.current.length === 0,
703
- buttonStyle: styles.iconButton
704
- })]
705
- }), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
706
- children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
707
- value: getEventsSnapshot,
708
- disabled: filteredEvents.length === 0,
709
- buttonStyle: styles.iconButton
710
- }), /*#__PURE__*/_jsx(PowerToggleButton, {
711
- isEnabled: isListening,
712
- onToggle: handleToggleListening,
713
- accessibilityLabel: "Toggle storage event monitoring"
714
- }), /*#__PURE__*/_jsx(ToolbarClearButton, {
715
- onPress: handleClearEvents,
716
- buttonStyle: styles.iconButton
717
- })]
718
- })]
719
- })]
720
- })
829
+ customContent: headerContent
721
830
  },
722
831
  onModeChange: handleModeChange,
723
832
  enablePersistence: true,
@@ -731,7 +840,34 @@ export function StorageModalWithTabs({
731
840
  })
732
841
  });
733
842
  }
843
+
844
+ /**
845
+ * The same storage tool as `StorageModalWithTabs`, rendered INLINE for hosts
846
+ * that place tools in a panel rather than a modal. Exists so there is exactly
847
+ * one implementation — the desktop dashboard used to ship its own copy of this
848
+ * component's header, tabs and screen router.
849
+ */
850
+ export function StorageToolView(props = {}) {
851
+ return /*#__PURE__*/_jsx(StorageModalWithTabs, {
852
+ ...props,
853
+ inline: true,
854
+ visible: true,
855
+ onClose: () => {}
856
+ });
857
+ }
734
858
  const styles = StyleSheet.create({
859
+ inlineRoot: {
860
+ flex: 1,
861
+ minHeight: 0,
862
+ backgroundColor: macOSColors.background.base
863
+ },
864
+ inlineHeader: {
865
+ paddingHorizontal: 12,
866
+ paddingVertical: 8,
867
+ borderBottomWidth: 1,
868
+ borderBottomColor: macOSColors.border.default,
869
+ backgroundColor: macOSColors.background.card
870
+ },
735
871
  headerSearchContainer: {
736
872
  flex: 1
737
873
  },
@@ -761,6 +897,12 @@ const styles = StyleSheet.create({
761
897
  marginTop: 8,
762
898
  marginHorizontal: 16
763
899
  },
900
+ /** Gutter matching the event cards below it, so the pills line up with them. */
901
+ eventFilterBar: {
902
+ paddingHorizontal: 12,
903
+ paddingTop: 8,
904
+ paddingBottom: 4
905
+ },
764
906
  emptyState: {
765
907
  flex: 1,
766
908
  justifyContent: "center",
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Editing a key whose value is a single string, number or boolean.
5
+ *
6
+ * A tree of one row is not a tree, so there's nothing here to select, reorder
7
+ * or append to — which means none of the structural editor applies. And with
8
+ * one edit producing one write, there's nothing to buffer either: a draft would
9
+ * only add a step between typing the value and storing it.
10
+ *
11
+ * So this is just the value modal over whatever the user was looking at, on
12
+ * both the phone and the dashboard. Changing one string shouldn't black out the
13
+ * screen it came from.
14
+ */
15
+
16
+ import { useCallback, useState } from "react";
17
+ import { Alert } from "react-native";
18
+ import { StorageValueEditModal } from "./StorageValueEditModal";
19
+ import { jsx as _jsx } from "react/jsx-runtime";
20
+ export function StorageScalarKeyEditor({
21
+ storageKey,
22
+ value,
23
+ onSave,
24
+ onClose
25
+ }) {
26
+ const [saving, setSaving] = useState(false);
27
+ const save = useCallback(next => {
28
+ if (saving) return;
29
+ setSaving(true);
30
+ onSave(JSON.stringify(next)).then(onClose).catch(e => {
31
+ setSaving(false);
32
+ Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
33
+ });
34
+ }, [onClose, onSave, saving]);
35
+ return /*#__PURE__*/_jsx(StorageValueEditModal, {
36
+ path: [storageKey.key],
37
+ value: value,
38
+ onSave: save,
39
+ onCancel: onClose,
40
+ busy: saving
41
+ });
42
+ }