@buoy-gg/storage 7.0.30 → 7.0.32

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 (41) hide show
  1. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +92 -21
  2. package/lib/commonjs/storage/components/StorageBrowserMode.js +8 -2
  3. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +572 -0
  4. package/lib/commonjs/storage/components/StorageKeyRow.js +21 -2
  5. package/lib/commonjs/storage/components/StorageKeySection.js +44 -21
  6. package/lib/commonjs/storage/components/StorageModalWithTabs.js +25 -2
  7. package/lib/commonjs/storage/components/StorageSaveNotice.js +171 -0
  8. package/lib/commonjs/storage/components/StorageValueEditModal.js +11 -364
  9. package/lib/commonjs/storage/hooks/useKeyEditor.js +12 -85
  10. package/lib/commonjs/storage/hooks/useTreeDraft.js +7 -100
  11. package/lib/module/storage/components/GameUIStorageBrowser.js +94 -23
  12. package/lib/module/storage/components/StorageBrowserMode.js +8 -2
  13. package/lib/module/storage/components/StorageKeyDetailScreen.js +570 -0
  14. package/lib/module/storage/components/StorageKeyRow.js +20 -3
  15. package/lib/module/storage/components/StorageKeySection.js +43 -21
  16. package/lib/module/storage/components/StorageModalWithTabs.js +26 -3
  17. package/lib/module/storage/components/StorageSaveNotice.js +168 -0
  18. package/lib/module/storage/components/StorageValueEditModal.js +13 -366
  19. package/lib/module/storage/hooks/useKeyEditor.js +14 -87
  20. package/lib/module/storage/hooks/useTreeDraft.js +9 -102
  21. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +13 -2
  22. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  23. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +7 -2
  24. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  25. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts +46 -0
  26. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -0
  27. package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
  28. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  29. package/lib/typescript/storage/components/StorageKeySection.d.ts +19 -1
  30. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  31. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  32. package/lib/typescript/storage/components/StorageSaveNotice.d.ts +6 -0
  33. package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +1 -0
  34. package/lib/typescript/storage/components/StorageValueEditModal.d.ts +9 -31
  35. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
  36. package/lib/typescript/storage/hooks/useKeyEditor.d.ts +7 -31
  37. package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -1
  38. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +7 -66
  39. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
  40. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
  41. package/package.json +5 -5
@@ -5,6 +5,13 @@ import { View, Text, StyleSheet } from "react-native";
5
5
  import { StorageKeyRow } from "./StorageKeyRow";
6
6
  import { SectionHeader, night } from "@buoy-gg/shared-ui";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ /**
9
+ * Identifies a ROW, not a key name — the same name routinely exists in more
10
+ * than one backend. Shared with the browser so both sides agree on the id.
11
+ */
12
+ export function storageRowId(storageKey) {
13
+ return storageKey.instanceId ? `${storageKey.storageType}-${storageKey.instanceId}-${storageKey.key}` : `${storageKey.storageType}-${storageKey.key}`;
14
+ }
8
15
  /**
9
16
  * Storage key section component following composition principles [[rule3]]
10
17
  *
@@ -30,7 +37,9 @@ export function StorageKeySection({
30
37
  onEditKey,
31
38
  onToggleKey,
32
39
  onDeleteKey,
33
- editBlockedFor
40
+ editBlockedFor,
41
+ onRowRef,
42
+ onOpenKey
34
43
  }) {
35
44
  /**
36
45
  * Identifies a ROW, not a key name.
@@ -39,9 +48,7 @@ export function StorageKeySection({
39
48
  * `auth.refreshToken` in both MMKV and the keychain has two rows called
40
49
  * that, and they are different values in different stores.
41
50
  */
42
- const getKeyIdentifier = useCallback(storageKey => {
43
- return storageKey.instanceId ? `${storageKey.storageType}-${storageKey.instanceId}-${storageKey.key}` : `${storageKey.storageType}-${storageKey.key}`;
44
- }, []);
51
+ const getKeyIdentifier = useCallback(storageKey => storageRowId(storageKey), []);
45
52
 
46
53
  /**
47
54
  * Which ROW is expanded — by identifier, not by key name.
@@ -52,9 +59,20 @@ export function StorageKeySection({
52
59
  */
53
60
  const [expandedKey, setExpandedKey] = useState(null);
54
61
  const handleKeyPress = useCallback(storageKey => {
62
+ // Fiber-driven automation can invoke the ROW's onPress prop directly,
63
+ // handing this a GestureResponderEvent instead of the key the row's own
64
+ // touchable wrapper binds. Rendering an event as a key produced a
65
+ // convincing all-"undefined" detail screen; refuse anything that isn't
66
+ // a key.
67
+ if (!storageKey || typeof storageKey.key !== "string") return;
68
+ if (onOpenKey) {
69
+ // Navigate-to-detail hosts: the card is a door, not a drawer.
70
+ onOpenKey(storageKey);
71
+ return;
72
+ }
55
73
  const id = getKeyIdentifier(storageKey);
56
74
  setExpandedKey(prev => prev === id ? null : id);
57
- }, [getKeyIdentifier]);
75
+ }, [getKeyIdentifier, onOpenKey]);
58
76
  if (keys.length === 0 && title === "Required Keys") {
59
77
  return /*#__PURE__*/_jsxs(View, {
60
78
  style: styles.sectionContainer,
@@ -89,22 +107,26 @@ export function StorageKeySection({
89
107
  children: keys.map(storageKey => {
90
108
  // Create unique key by combining storage type, instance ID (if present), and key name
91
109
  const uniqueKey = getKeyIdentifier(storageKey);
92
- return /*#__PURE__*/_jsx(StorageKeyRow, {
93
- storageKey: storageKey,
94
- isExpanded: !isSelectMode && expandedKey === uniqueKey,
95
- onPress: handleKeyPress,
96
- isSelectMode: isSelectMode,
97
- isSelected: selectedKeys.has(uniqueKey),
98
- onSelectionChange: onSelectionChange,
99
- eventCount: eventCountByKey?.[storageKey.key],
100
- onViewHistory: onViewHistory ? () => onViewHistory(storageKey.key) : undefined,
101
- onHideKey: onHideKey,
102
- onEditKey: onEditKey,
103
- onToggleKey: onToggleKey,
104
- onDeleteKey: onDeleteKey,
105
- editBlockedReason: editBlockedFor?.(storageKey),
106
- isPinned: pinnedKeys?.has(storageKey.key),
107
- onTogglePin: onTogglePin
110
+ return /*#__PURE__*/_jsx(View, {
111
+ ref: onRowRef ? node => onRowRef(uniqueKey, node) : undefined,
112
+ collapsable: false,
113
+ children: /*#__PURE__*/_jsx(StorageKeyRow, {
114
+ storageKey: storageKey,
115
+ isExpanded: !isSelectMode && expandedKey === uniqueKey,
116
+ onPress: handleKeyPress,
117
+ isSelectMode: isSelectMode,
118
+ isSelected: selectedKeys.has(uniqueKey),
119
+ onSelectionChange: onSelectionChange,
120
+ eventCount: eventCountByKey?.[storageKey.key],
121
+ onViewHistory: onViewHistory ? () => onViewHistory(storageKey.key) : undefined,
122
+ onHideKey: onHideKey,
123
+ onEditKey: onEditKey,
124
+ onToggleKey: onToggleKey,
125
+ onDeleteKey: onDeleteKey,
126
+ editBlockedReason: editBlockedFor?.(storageKey),
127
+ isPinned: pinnedKeys?.has(storageKey.key),
128
+ onTogglePin: onTogglePin
129
+ })
108
130
  }, uniqueKey);
109
131
  })
110
132
  })]
@@ -3,7 +3,7 @@
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, night, Database, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS } from "@buoy-gg/shared-ui";
6
+ import { JsModal, ModalHeader, TabSelector, parseValue, devToolsStorageKeys, macOSColors, night, Database, Filter, RefreshCw, Search, SearchBar, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, TIER_LIMITS, windowDragProps } from "@buoy-gg/shared-ui";
7
7
  import { StorageBrowserMode } from "./StorageBrowserMode";
8
8
  import { StorageTypeFilterBar, countByStorageType } from "./StorageTypeFilterBar";
9
9
  import { clearAllAppStorage, clearAllStorageIncludingDevTools } from "../utils/clearAllStorage";
@@ -45,6 +45,12 @@ export function StorageModalWithTabs({
45
45
 
46
46
  // Storage Browser state
47
47
  const [searchQuery, setSearchQuery] = useState("");
48
+ /**
49
+ * The key whose detail screen (live editor + info) is open. Lives at the
50
+ * MODAL level because the modal's header becomes back + the key's name
51
+ * while it's open — the React Query interaction, same as the Zustand tool.
52
+ */
53
+ const [detailKey, setDetailKey] = useState(null);
48
54
  const [isSearchActive, setIsSearchActive] = useState(false);
49
55
 
50
56
  // Local state to control subscription (true = subscribed, false = unsubscribed)
@@ -630,7 +636,10 @@ export function StorageModalWithTabs({
630
636
  ,
631
637
  applySafeAreaInset: modalMode === "bottomSheet",
632
638
  storageTypeFilter: storageTypeFilter,
633
- onStorageTypeFilterChange: setStorageTypeFilter
639
+ onStorageTypeFilterChange: setStorageTypeFilter,
640
+ detailKey: detailKey,
641
+ onOpenKey: setDetailKey,
642
+ onCloseDetail: () => setDetailKey(null)
634
643
  });
635
644
  }
636
645
 
@@ -745,6 +754,19 @@ export function StorageModalWithTabs({
745
754
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
746
755
  title: selectedConversation.key
747
756
  })]
757
+ }) : detailKey ?
758
+ /*#__PURE__*/
759
+ // The key detail screen — back + the key's name, the React Query
760
+ // interaction. AFTER history in this chain: history is reached FROM
761
+ // the detail and stacks over it, so its back returns here.
762
+ _jsxs(ModalHeader, {
763
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
764
+ onBack: () => setDetailKey(null)
765
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
766
+ title: detailKey.key,
767
+ centered: true,
768
+ testID: "buoy-storage-detail-title"
769
+ })]
748
770
  }) : /*#__PURE__*/_jsxs(ModalHeader, {
749
771
  children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
750
772
  onBack: onBack
@@ -762,7 +784,6 @@ export function StorageModalWithTabs({
762
784
  autoFocus: true,
763
785
  onSubmitEditing: () => setIsSearchActive(false),
764
786
  returnKeyType: "search",
765
- suggestions: allStorageKeys,
766
787
  containerStyle: styles.headerSearchContainer
767
788
  }) : /*#__PURE__*/_jsx(TabSelector, {
768
789
  tabs: [{
@@ -780,6 +801,7 @@ export function StorageModalWithTabs({
780
801
  setSelectedHistoryEventIndex(null);
781
802
  setSelectedConversationKey(null);
782
803
  setSelectedEventIndex(0);
804
+ setDetailKey(null);
783
805
  }
784
806
  })
785
807
  }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
@@ -848,6 +870,7 @@ export function StorageModalWithTabs({
848
870
  style: styles.inlineRoot,
849
871
  children: [/*#__PURE__*/_jsx(View, {
850
872
  style: styles.inlineHeader,
873
+ ...windowDragProps,
851
874
  children: headerContent
852
875
  }), renderContent(), footerNode]
853
876
  })
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * What a storage edit actually did, as a toast.
5
+ *
6
+ * ONE SENTENCE, EVERY TIME, NO DETECTION. Saving used to report success and
7
+ * stop, which is misleading for any key an app has already read into memory:
8
+ * the write lands on disk, the screen does not move, and the next time the
9
+ * owner saves it writes its own copy back over the edit.
10
+ *
11
+ * The obvious fix — work out which live store owns the key and offer to make it
12
+ * re-read — was built and then removed on purpose. It required knowing what is
13
+ * linked to what, and every route to that knowledge is a guess: key names
14
+ * collide across backends, only zustand stores register with Buoy at all, and a
15
+ * store can register after the lookup ran. A tool that is right most of the
16
+ * time is the problem here, not the fix. There is also no general mechanism to
17
+ * reach for — the app owns its cache, and only the app can invalidate it.
18
+ *
19
+ * So it promises the one thing true for every key, every backend and every
20
+ * persistence library, in dev and in release — "applies on next launch" — and
21
+ * says it in five words, because a toast is read at a glance or not at all.
22
+ * Making a change live NOW is a different operation and lives in the state
23
+ * tools, where the store you mean is the one you are already looking at.
24
+ *
25
+ * WHY IT FLOATS. It first shipped inline, above the filter cards and INSIDE the
26
+ * scroller — so it scrolled away with the content and you had to go looking for
27
+ * the thing that was meant to catch your eye. It is pinned over the top of the
28
+ * tool now, and takes itself away, because a message about a save you just made
29
+ * has a short useful life and should not need dismissing.
30
+ */
31
+ import { useEffect, useRef } from "react";
32
+ import { Animated, Easing, StyleSheet, Text, TouchableOpacity, View } from "react-native";
33
+ import { Check, macOSColors } from "@buoy-gg/shared-ui";
34
+
35
+ /** Five words is one glance, so it does not need to linger like a sentence. */
36
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
37
+ const VISIBLE_MS = 2600;
38
+ const FADE_MS = 180;
39
+ export function StorageSaveNotice({
40
+ onDone
41
+ }) {
42
+ const progress = useRef(new Animated.Value(0)).current;
43
+
44
+ // Latest-ref: the host passes an inline arrow, and depending on it directly
45
+ // would restart the animation on every parent render — the toast would never
46
+ // reach its own fade-out.
47
+ const onDoneRef = useRef(onDone);
48
+ onDoneRef.current = onDone;
49
+ useEffect(() => {
50
+ const animation = Animated.sequence([Animated.timing(progress, {
51
+ toValue: 1,
52
+ duration: FADE_MS,
53
+ easing: Easing.out(Easing.quad),
54
+ useNativeDriver: true
55
+ }), Animated.delay(VISIBLE_MS), Animated.timing(progress, {
56
+ toValue: 0,
57
+ duration: FADE_MS,
58
+ easing: Easing.in(Easing.quad),
59
+ useNativeDriver: true
60
+ })]);
61
+ animation.start(({
62
+ finished
63
+ }) => {
64
+ // Only when it ran to the end: a stop() from unmount must not call back
65
+ // into a host that is already tearing this down.
66
+ if (finished) onDoneRef.current();
67
+ });
68
+ return () => animation.stop();
69
+ }, [progress]);
70
+ return /*#__PURE__*/_jsx(Animated.View, {
71
+ pointerEvents: "box-none",
72
+ style: [styles.wrap, {
73
+ opacity: progress,
74
+ transform: [{
75
+ translateY: progress.interpolate({
76
+ inputRange: [0, 1],
77
+ outputRange: [-10, 0]
78
+ })
79
+ }]
80
+ }],
81
+ testID: "buoy-storage-save-notice",
82
+ children: /*#__PURE__*/_jsxs(TouchableOpacity, {
83
+ activeOpacity: 0.85,
84
+ onPress: () => onDoneRef.current(),
85
+ style: styles.toast,
86
+ accessibilityRole: "button"
87
+ // The visible text is a fragment; the label is the whole thought, for
88
+ // anyone who gets this read aloud rather than glanced at.
89
+ ,
90
+ accessibilityLabel: "Saved. Applies on the app's next launch. Tap to dismiss.",
91
+ testID: "buoy-storage-save-notice-dismiss",
92
+ children: [/*#__PURE__*/_jsx(View, {
93
+ style: styles.tick,
94
+ children: /*#__PURE__*/_jsx(Check, {
95
+ size: 10,
96
+ color: macOSColors.semantic.success
97
+ })
98
+ }), /*#__PURE__*/_jsx(Text, {
99
+ style: styles.saved,
100
+ children: "Saved"
101
+ }), /*#__PURE__*/_jsx(View, {
102
+ style: styles.divider
103
+ }), /*#__PURE__*/_jsx(Text, {
104
+ style: styles.detail,
105
+ children: "applies on next launch"
106
+ })]
107
+ })
108
+ });
109
+ }
110
+ const styles = StyleSheet.create({
111
+ wrap: {
112
+ // Pinned over the tool rather than laid out in it, so the scroller cannot
113
+ // carry it away. `box-none` keeps the untouched margins clickable.
114
+ position: "absolute",
115
+ top: 10,
116
+ left: 12,
117
+ right: 12,
118
+ // Above the list, the filter cards and the sticky headers.
119
+ zIndex: 100,
120
+ elevation: 100,
121
+ alignItems: "center"
122
+ },
123
+ toast: {
124
+ // Sized to its content: a five-word toast that spans the tool reads as a
125
+ // banner, which is the thing this stopped being.
126
+ flexDirection: "row",
127
+ alignItems: "center",
128
+ paddingVertical: 7,
129
+ paddingLeft: 8,
130
+ paddingRight: 12,
131
+ borderRadius: 999,
132
+ backgroundColor: macOSColors.background.card,
133
+ borderWidth: StyleSheet.hairlineWidth,
134
+ borderColor: macOSColors.border.default,
135
+ shadowColor: "#000",
136
+ shadowOpacity: 0.4,
137
+ shadowRadius: 14,
138
+ shadowOffset: {
139
+ width: 0,
140
+ height: 5
141
+ }
142
+ },
143
+ tick: {
144
+ width: 16,
145
+ height: 16,
146
+ borderRadius: 8,
147
+ marginRight: 7,
148
+ alignItems: "center",
149
+ justifyContent: "center",
150
+ backgroundColor: macOSColors.semantic.successBackground
151
+ },
152
+ saved: {
153
+ color: macOSColors.text.primary,
154
+ fontSize: 12,
155
+ fontWeight: "600"
156
+ },
157
+ divider: {
158
+ width: StyleSheet.hairlineWidth,
159
+ alignSelf: "stretch",
160
+ marginHorizontal: 8,
161
+ backgroundColor: macOSColors.border.default
162
+ },
163
+ // The consequence is the part people already half-know, so it recedes.
164
+ detail: {
165
+ color: macOSColors.text.muted,
166
+ fontSize: 12
167
+ }
168
+ });