@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.
- 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 +25 -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 +26 -3
- 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
|
@@ -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)(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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, {
|
|
@@ -853,6 +875,7 @@ function StorageModalWithTabs({
|
|
|
853
875
|
style: styles.inlineRoot,
|
|
854
876
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
855
877
|
style: styles.inlineHeader,
|
|
878
|
+
..._sharedUi.windowDragProps,
|
|
856
879
|
children: headerContent
|
|
857
880
|
}), renderContent(), footerNode]
|
|
858
881
|
})
|
|
@@ -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
|
+
});
|