@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.
- package/lib/commonjs/storage/components/GameUIStorageBrowser.js +92 -21
- package/lib/commonjs/storage/components/StorageBrowserMode.js +8 -2
- package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +572 -0
- package/lib/commonjs/storage/components/StorageKeyRow.js +21 -2
- package/lib/commonjs/storage/components/StorageKeySection.js +44 -21
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +24 -2
- package/lib/commonjs/storage/components/StorageSaveNotice.js +171 -0
- package/lib/commonjs/storage/components/StorageValueEditModal.js +11 -364
- package/lib/commonjs/storage/hooks/useKeyEditor.js +12 -85
- package/lib/commonjs/storage/hooks/useTreeDraft.js +7 -100
- package/lib/module/storage/components/GameUIStorageBrowser.js +94 -23
- package/lib/module/storage/components/StorageBrowserMode.js +8 -2
- package/lib/module/storage/components/StorageKeyDetailScreen.js +570 -0
- package/lib/module/storage/components/StorageKeyRow.js +20 -3
- package/lib/module/storage/components/StorageKeySection.js +43 -21
- package/lib/module/storage/components/StorageModalWithTabs.js +24 -2
- package/lib/module/storage/components/StorageSaveNotice.js +168 -0
- package/lib/module/storage/components/StorageValueEditModal.js +13 -366
- package/lib/module/storage/hooks/useKeyEditor.js +14 -87
- package/lib/module/storage/hooks/useTreeDraft.js +9 -102
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +13 -2
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts +7 -2
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts +46 -0
- package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts +19 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageSaveNotice.d.ts +6 -0
- package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts +9 -31
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts +7 -31
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts +7 -66
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
- 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(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
})]
|
|
@@ -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, {
|
|
@@ -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
|
+
});
|