@buoy-gg/storage 7.0.30 → 7.0.31

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