@buoy-gg/storage 7.0.4 → 7.0.6
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/index.js +77 -1
- package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
- package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
- package/lib/commonjs/storage/components/GameUIStorageBrowser.js +263 -115
- package/lib/commonjs/storage/components/GameUIStorageStats.js +7 -7
- package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -1
- package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -1
- package/lib/commonjs/storage/components/SelectionActionBar.js +2 -36
- package/lib/commonjs/storage/components/StorageActions.js +6 -6
- package/lib/commonjs/storage/components/StorageBrowserMode.js +16 -2
- package/lib/commonjs/storage/components/StorageEventDetailContent.js +106 -149
- package/lib/commonjs/storage/components/StorageEventListener.js +13 -13
- package/lib/commonjs/storage/components/StorageFilterCards.js +13 -135
- package/lib/commonjs/storage/components/StorageKeyCard.js +1 -1
- package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +392 -0
- package/lib/commonjs/storage/components/StorageKeyRow.js +53 -15
- package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
- package/lib/commonjs/storage/components/StorageKeyStats.js +10 -10
- package/lib/commonjs/storage/components/StorageModalWithTabs.js +272 -129
- package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +46 -0
- package/lib/commonjs/storage/components/StorageTypeFilterBar.js +141 -0
- package/lib/commonjs/storage/components/StorageValueEditModal.js +361 -0
- package/lib/commonjs/storage/hooks/useKeyEditor.js +104 -0
- package/lib/commonjs/storage/hooks/useMeasuredWidth.js +74 -0
- package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +70 -15
- package/lib/commonjs/storage/hooks/useTreeDraft.js +107 -0
- package/lib/commonjs/storage/index.js +29 -0
- package/lib/commonjs/storage/stores/storageEventStore.js +4 -2
- package/lib/commonjs/storage/sync/storageSyncAdapter.js +125 -3
- package/lib/commonjs/storage/utils/AsyncStorageListener.js +76 -12
- package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +10 -9
- package/lib/commonjs/storage/utils/MMKVListener.js +40 -4
- package/lib/commonjs/storage/utils/SecureStoreRegistry.js +52 -1
- package/lib/commonjs/storage/utils/deleteStorageKeys.js +53 -0
- package/lib/commonjs/storage/utils/index.js +13 -0
- package/lib/commonjs/storage/utils/lineDiff.js +21 -354
- package/lib/commonjs/storage/utils/setStorageValue.js +218 -0
- package/lib/commonjs/storage/utils/storageBrowserRefresh.js +49 -0
- package/lib/commonjs/storage/utils/storageOps.js +53 -0
- package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +60 -38
- package/lib/commonjs/storage/utils/valueType.js +24 -0
- package/lib/module/index.js +18 -1
- package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
- package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
- package/lib/module/storage/components/GameUIStorageBrowser.js +266 -118
- package/lib/module/storage/components/GameUIStorageStats.js +7 -7
- package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -1
- package/lib/module/storage/components/MMKVInstanceSelector.js +1 -1
- package/lib/module/storage/components/SelectionActionBar.js +2 -36
- package/lib/module/storage/components/StorageActions.js +7 -7
- package/lib/module/storage/components/StorageBrowserMode.js +16 -2
- package/lib/module/storage/components/StorageEventDetailContent.js +110 -151
- package/lib/module/storage/components/StorageEventListener.js +14 -14
- package/lib/module/storage/components/StorageFilterCards.js +13 -135
- package/lib/module/storage/components/StorageKeyCard.js +1 -1
- package/lib/module/storage/components/StorageKeyEditorScreen.js +388 -0
- package/lib/module/storage/components/StorageKeyRow.js +55 -17
- package/lib/module/storage/components/StorageKeySection.js +11 -5
- package/lib/module/storage/components/StorageKeyStats.js +11 -10
- package/lib/module/storage/components/StorageModalWithTabs.js +272 -130
- package/lib/module/storage/components/StorageScalarKeyEditor.js +42 -0
- package/lib/module/storage/components/StorageTypeFilterBar.js +136 -0
- package/lib/module/storage/components/StorageValueEditModal.js +357 -0
- package/lib/module/storage/hooks/useKeyEditor.js +100 -0
- package/lib/module/storage/hooks/useMeasuredWidth.js +71 -0
- package/lib/module/storage/hooks/useSecureStoreKeys.js +71 -16
- package/lib/module/storage/hooks/useTreeDraft.js +103 -0
- package/lib/module/storage/index.js +3 -0
- package/lib/module/storage/stores/storageEventStore.js +4 -2
- package/lib/module/storage/sync/storageSyncAdapter.js +125 -3
- package/lib/module/storage/utils/AsyncStorageListener.js +76 -12
- package/lib/module/storage/utils/MMKVInstanceRegistry.js +8 -7
- package/lib/module/storage/utils/MMKVListener.js +40 -4
- package/lib/module/storage/utils/SecureStoreRegistry.js +52 -1
- package/lib/module/storage/utils/deleteStorageKeys.js +49 -0
- package/lib/module/storage/utils/index.js +3 -0
- package/lib/module/storage/utils/lineDiff.js +7 -354
- package/lib/module/storage/utils/setStorageValue.js +209 -0
- package/lib/module/storage/utils/storageBrowserRefresh.js +44 -0
- package/lib/module/storage/utils/storageOps.js +46 -0
- package/lib/module/storage/utils/storageTimeTravelUtils.js +61 -40
- package/lib/module/storage/utils/valueType.js +23 -0
- package/lib/typescript/index.d.ts +12 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts +4 -1
- package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -1
- package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +27 -1
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts +17 -1
- package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts +4 -1
- package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageFilterCards.d.ts +9 -3
- package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts +42 -0
- package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +17 -0
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
- package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +28 -1
- package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts +24 -0
- package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts +29 -0
- package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts +36 -0
- package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts +51 -0
- package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts +37 -0
- package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +8 -1
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts +67 -0
- package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -0
- package/lib/typescript/storage/index.d.ts +2 -0
- package/lib/typescript/storage/index.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +41 -0
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +15 -0
- package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
- package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +1 -1
- package/lib/typescript/storage/utils/MMKVListener.d.ts +7 -0
- package/lib/typescript/storage/utils/MMKVListener.d.ts.map +1 -1
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +20 -0
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -1
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +27 -0
- package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -0
- package/lib/typescript/storage/utils/index.d.ts +1 -0
- package/lib/typescript/storage/utils/index.d.ts.map +1 -1
- package/lib/typescript/storage/utils/lineDiff.d.ts +7 -31
- package/lib/typescript/storage/utils/lineDiff.d.ts.map +1 -1
- package/lib/typescript/storage/utils/setStorageValue.d.ts +64 -0
- package/lib/typescript/storage/utils/setStorageValue.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts +28 -0
- package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageOps.d.ts +25 -0
- package/lib/typescript/storage/utils/storageOps.d.ts.map +1 -0
- package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts +15 -5
- package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
- package/lib/typescript/storage/utils/valueType.d.ts +13 -0
- package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.StorageTypeFilterBar = StorageTypeFilterBar;
|
|
7
|
+
exports.countByStorageType = countByStorageType;
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
10
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
11
|
+
/**
|
|
12
|
+
* "Which backend?" — the pill row shared by the Keys and Events tabs.
|
|
13
|
+
*
|
|
14
|
+
* Both tabs answer questions about the same three backends, so asking the
|
|
15
|
+
* question two different ways (or, as it was, only on one tab) is a gap rather
|
|
16
|
+
* than a simplification: you filter the key list to Keychain, switch to Events
|
|
17
|
+
* to see what wrote to it, and you're looking at everything again.
|
|
18
|
+
*
|
|
19
|
+
* The selection is owned by the modal and passed to both, so switching tabs
|
|
20
|
+
* keeps the filter you set.
|
|
21
|
+
*
|
|
22
|
+
* Each pill carries the count it WOULD show, computed before the type filter is
|
|
23
|
+
* applied — a count that collapsed to zero the moment you picked another type
|
|
24
|
+
* would tell you nothing about where to look next.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One entry per pill. This was four near-identical blocks of JSX differing only
|
|
29
|
+
* in three colours and a label, which is how "All Types" ended up with a
|
|
30
|
+
* hover-grey active state while the others used their own hue.
|
|
31
|
+
*/
|
|
32
|
+
const SEGMENTS = [{
|
|
33
|
+
id: "all",
|
|
34
|
+
label: "All Types",
|
|
35
|
+
color: null
|
|
36
|
+
}, {
|
|
37
|
+
id: "async",
|
|
38
|
+
label: "Async",
|
|
39
|
+
color: _sharedUi.macOSColors.semantic.warning
|
|
40
|
+
}, {
|
|
41
|
+
id: "mmkv",
|
|
42
|
+
label: "MMKV",
|
|
43
|
+
color: _sharedUi.macOSColors.semantic.info
|
|
44
|
+
}, {
|
|
45
|
+
id: "secure",
|
|
46
|
+
label: "Secure",
|
|
47
|
+
color: _sharedUi.macOSColors.semantic.success
|
|
48
|
+
}];
|
|
49
|
+
function StorageTypeFilterBar({
|
|
50
|
+
active,
|
|
51
|
+
onChange,
|
|
52
|
+
counts
|
|
53
|
+
}) {
|
|
54
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
55
|
+
style: styles.row,
|
|
56
|
+
children: SEGMENTS.map(segment => {
|
|
57
|
+
const isActive = active === segment.id;
|
|
58
|
+
const accent = segment.color ?? _sharedUi.macOSColors.text.primary;
|
|
59
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
60
|
+
accessibilityLabel: `Filter by ${segment.label}`,
|
|
61
|
+
accessibilityState: {
|
|
62
|
+
selected: isActive
|
|
63
|
+
},
|
|
64
|
+
activeOpacity: 0.8,
|
|
65
|
+
onPress: () => onChange(segment.id),
|
|
66
|
+
style: [styles.pill, {
|
|
67
|
+
borderColor: _sharedUi.macOSColors.border.default
|
|
68
|
+
}, isActive && [styles.pillActive, segment.color ? {
|
|
69
|
+
backgroundColor: segment.color + "15",
|
|
70
|
+
borderColor: segment.color + "40"
|
|
71
|
+
} : {
|
|
72
|
+
backgroundColor: _sharedUi.macOSColors.background.hover,
|
|
73
|
+
borderColor: _sharedUi.macOSColors.border.hover
|
|
74
|
+
}]],
|
|
75
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
76
|
+
style: [styles.label, isActive && {
|
|
77
|
+
color: accent,
|
|
78
|
+
fontWeight: "600"
|
|
79
|
+
}],
|
|
80
|
+
children: segment.label
|
|
81
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
82
|
+
style: [styles.value, {
|
|
83
|
+
color: isActive ? accent : _sharedUi.macOSColors.text.muted
|
|
84
|
+
}],
|
|
85
|
+
children: counts[segment.id]
|
|
86
|
+
})]
|
|
87
|
+
}, segment.id);
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Tally a list of things that each know which backend they came from. */
|
|
93
|
+
function countByStorageType(items) {
|
|
94
|
+
const counts = {
|
|
95
|
+
all: items.length,
|
|
96
|
+
async: 0,
|
|
97
|
+
mmkv: 0,
|
|
98
|
+
secure: 0
|
|
99
|
+
};
|
|
100
|
+
for (const item of items) counts[item.storageType] += 1;
|
|
101
|
+
return counts;
|
|
102
|
+
}
|
|
103
|
+
const styles = _reactNative.StyleSheet.create({
|
|
104
|
+
row: {
|
|
105
|
+
flexDirection: "row",
|
|
106
|
+
gap: 8,
|
|
107
|
+
marginTop: 2
|
|
108
|
+
},
|
|
109
|
+
pill: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
backgroundColor: _sharedUi.macOSColors.background.input + "80",
|
|
112
|
+
borderRadius: 8,
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderColor: _sharedUi.macOSColors.border.default + "30",
|
|
115
|
+
paddingVertical: 6,
|
|
116
|
+
paddingHorizontal: 8,
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
justifyContent: "center",
|
|
119
|
+
minHeight: 32
|
|
120
|
+
},
|
|
121
|
+
pillActive: {
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
transform: [{
|
|
124
|
+
scale: 1.005
|
|
125
|
+
}]
|
|
126
|
+
},
|
|
127
|
+
label: {
|
|
128
|
+
fontSize: 9,
|
|
129
|
+
color: _sharedUi.macOSColors.text.secondary,
|
|
130
|
+
textTransform: "uppercase",
|
|
131
|
+
letterSpacing: 0.4,
|
|
132
|
+
fontWeight: "500",
|
|
133
|
+
marginBottom: 1
|
|
134
|
+
},
|
|
135
|
+
value: {
|
|
136
|
+
fontSize: 12,
|
|
137
|
+
fontWeight: "600",
|
|
138
|
+
fontFamily: "monospace",
|
|
139
|
+
lineHeight: 14
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.StorageValueEditModal = StorageValueEditModal;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
10
|
+
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
11
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
12
|
+
/**
|
|
13
|
+
* Editing one value: a panel at the top of the screen, with Cancel and Save.
|
|
14
|
+
*
|
|
15
|
+
* This started as a pushed full screen and that was more ceremony than the task
|
|
16
|
+
* deserves — changing one string is the most ordinary thing in this tool, and
|
|
17
|
+
* it shouldn't cost a navigation. A modal is what people expect, and the reason
|
|
18
|
+
* it's pinned to the TOP rather than centred is the keyboard: a centred panel
|
|
19
|
+
* gets shoved up or covered the moment the field focuses, and a bottom sheet is
|
|
20
|
+
* simply underneath it. At the top it stays put and stays readable.
|
|
21
|
+
*
|
|
22
|
+
* The field is multiline for anything textual. Stored values are routinely
|
|
23
|
+
* tokens, URLs and serialized blobs, and a single line shows you roughly forty
|
|
24
|
+
* characters of them, scrolling sideways — which is how you edit the wrong part
|
|
25
|
+
* of a string without noticing.
|
|
26
|
+
*
|
|
27
|
+
* Type is preserved by construction: the ORIGINAL value's type picks the editor
|
|
28
|
+
* AND decides how the draft is read back, so editing a number can only produce
|
|
29
|
+
* a number. Raw JSON — the escape hatch for containers — re-checks the parsed
|
|
30
|
+
* result and refuses a different shape.
|
|
31
|
+
*
|
|
32
|
+
* Nothing here writes. `onSave` hands the value to whoever owns it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How this value gets edited. Driven by the value's own type, which is also
|
|
37
|
+
* what makes the result type-safe without asking the user to declare anything.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
function modeFor(value) {
|
|
41
|
+
if (typeof value === "boolean") return "boolean";
|
|
42
|
+
if (typeof value === "number") return "number";
|
|
43
|
+
if (value !== null && typeof value === "object") return "raw";
|
|
44
|
+
return "text";
|
|
45
|
+
}
|
|
46
|
+
function initialText(value, mode) {
|
|
47
|
+
if (mode === "raw") return JSON.stringify(value, null, 2);
|
|
48
|
+
if (value === null || value === undefined) return "";
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
function hintFor(mode, type) {
|
|
52
|
+
switch (mode) {
|
|
53
|
+
case "number":
|
|
54
|
+
return "Numeric only — anything else is refused, so this stays a number.";
|
|
55
|
+
case "raw":
|
|
56
|
+
return `Raw JSON. Must stay ${type === "array" ? "an array" : "an object"} — a different shape is refused.`;
|
|
57
|
+
default:
|
|
58
|
+
return "Saves as a string. The type never changes here.";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function StorageValueEditModal({
|
|
62
|
+
path,
|
|
63
|
+
value,
|
|
64
|
+
onSave,
|
|
65
|
+
onCancel,
|
|
66
|
+
saveLabel = "Save",
|
|
67
|
+
busy = false
|
|
68
|
+
}) {
|
|
69
|
+
const mode = modeFor(value);
|
|
70
|
+
const type = (0, _dataViewer.typeLabel)(value);
|
|
71
|
+
const [text, setText] = (0, _react.useState)(() => initialText(value, mode));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Advisory only — {@link commit} re-derives it, so a disabled button and a
|
|
75
|
+
* refused save can never disagree about what's valid.
|
|
76
|
+
*/
|
|
77
|
+
const error = validate(text, mode, type);
|
|
78
|
+
const blocked = busy || error != null;
|
|
79
|
+
const commit = () => {
|
|
80
|
+
if (busy || validate(text, mode, type) != null) return;
|
|
81
|
+
onSave(parse(text, mode, value));
|
|
82
|
+
};
|
|
83
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
84
|
+
style: styles.overlay,
|
|
85
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
86
|
+
style: styles.scrim,
|
|
87
|
+
activeOpacity: 1,
|
|
88
|
+
onPress: busy ? undefined : onCancel,
|
|
89
|
+
accessibilityLabel: "Dismiss editor"
|
|
90
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
91
|
+
style: styles.panel,
|
|
92
|
+
pointerEvents: "box-none",
|
|
93
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
94
|
+
style: styles.card,
|
|
95
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
96
|
+
style: styles.headerRow,
|
|
97
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
98
|
+
style: styles.title,
|
|
99
|
+
children: "Edit value"
|
|
100
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
101
|
+
style: styles.typeBadge,
|
|
102
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
103
|
+
style: styles.typeBadgeText,
|
|
104
|
+
children: type
|
|
105
|
+
})
|
|
106
|
+
})]
|
|
107
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
108
|
+
style: styles.path,
|
|
109
|
+
numberOfLines: 1,
|
|
110
|
+
children: (0, _dataViewer.breadcrumb)(path)
|
|
111
|
+
}), mode === "boolean" ? /*#__PURE__*/(0, _jsxRuntime.jsx)(BooleanChoice, {
|
|
112
|
+
current: value === true,
|
|
113
|
+
onPick: onSave
|
|
114
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
115
|
+
style: styles.fieldScroll,
|
|
116
|
+
keyboardShouldPersistTaps: "handled",
|
|
117
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
|
|
118
|
+
value: text,
|
|
119
|
+
onChangeText: setText,
|
|
120
|
+
autoFocus: true,
|
|
121
|
+
autoCapitalize: "none",
|
|
122
|
+
autoCorrect: false,
|
|
123
|
+
spellCheck: false,
|
|
124
|
+
editable: !busy
|
|
125
|
+
// Multiline for anything textual: stored values are long more
|
|
126
|
+
// often than they're short, and a single line hides that.
|
|
127
|
+
,
|
|
128
|
+
multiline: mode !== "number",
|
|
129
|
+
keyboardType: mode === "number" ? "numeric" : "default",
|
|
130
|
+
onSubmitEditing: mode === "number" ? commit : undefined,
|
|
131
|
+
placeholder: mode === "raw" ? "{ }" : "value",
|
|
132
|
+
placeholderTextColor: _sharedUi.macOSColors.text.muted,
|
|
133
|
+
style: [styles.input, mode === "raw" && styles.inputRaw, mode !== "number" && styles.inputMultiline, error != null && styles.inputInvalid]
|
|
134
|
+
})
|
|
135
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
136
|
+
style: error != null ? styles.error : styles.hint,
|
|
137
|
+
children: error ?? hintFor(mode, type)
|
|
138
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
139
|
+
style: styles.actions,
|
|
140
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
141
|
+
onPress: onCancel,
|
|
142
|
+
disabled: busy,
|
|
143
|
+
accessibilityLabel: "Cancel edit",
|
|
144
|
+
style: styles.cancelButton,
|
|
145
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
146
|
+
style: styles.cancelText,
|
|
147
|
+
children: "Cancel"
|
|
148
|
+
})
|
|
149
|
+
}), mode === "boolean" ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
150
|
+
onPress: commit,
|
|
151
|
+
disabled: blocked,
|
|
152
|
+
accessibilityLabel: saveLabel,
|
|
153
|
+
style: [styles.saveButton, blocked && styles.saveDisabled],
|
|
154
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
155
|
+
style: styles.saveText,
|
|
156
|
+
children: busy ? "Saving…" : saveLabel
|
|
157
|
+
})
|
|
158
|
+
})]
|
|
159
|
+
})]
|
|
160
|
+
})
|
|
161
|
+
})]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function BooleanChoice({
|
|
165
|
+
current,
|
|
166
|
+
onPick
|
|
167
|
+
}) {
|
|
168
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
169
|
+
style: styles.boolRow,
|
|
170
|
+
children: [true, false].map(option => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
171
|
+
onPress: () => onPick(option),
|
|
172
|
+
accessibilityLabel: `Set ${option}`,
|
|
173
|
+
style: [styles.boolButton, current === option && styles.boolActive],
|
|
174
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
175
|
+
style: [styles.boolText, current === option && styles.boolTextActive],
|
|
176
|
+
children: String(option)
|
|
177
|
+
})
|
|
178
|
+
}, String(option)))
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Null when the draft is applicable, else why it isn't. */
|
|
183
|
+
function validate(text, mode, type) {
|
|
184
|
+
if (mode === "number") {
|
|
185
|
+
if (text.trim() === "") return "Enter a number.";
|
|
186
|
+
if (Number.isNaN(Number(text))) return `"${text}" isn't a number.`;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (mode === "raw") {
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(text);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return e instanceof Error ? e.message : "Invalid JSON.";
|
|
195
|
+
}
|
|
196
|
+
if ((0, _dataViewer.typeLabel)(parsed) !== type) {
|
|
197
|
+
return `Must stay ${type} — that parses as ${(0, _dataViewer.typeLabel)(parsed)}.`;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function parse(text, mode, original) {
|
|
204
|
+
if (mode === "number") return Number(text);
|
|
205
|
+
if (mode === "raw") return JSON.parse(text);
|
|
206
|
+
// An emptied field on a null keeps it null rather than silently minting an
|
|
207
|
+
// empty string, which would change the type this promised to preserve.
|
|
208
|
+
if (original === null && text === "") return null;
|
|
209
|
+
return text;
|
|
210
|
+
}
|
|
211
|
+
const styles = _reactNative.StyleSheet.create({
|
|
212
|
+
overlay: {
|
|
213
|
+
..._sharedUi.absoluteFill,
|
|
214
|
+
zIndex: 20
|
|
215
|
+
},
|
|
216
|
+
scrim: {
|
|
217
|
+
..._sharedUi.absoluteFill,
|
|
218
|
+
backgroundColor: "rgba(0,0,0,0.55)"
|
|
219
|
+
},
|
|
220
|
+
/** Pinned to the top so the keyboard can't reach it. */
|
|
221
|
+
panel: {
|
|
222
|
+
padding: 12
|
|
223
|
+
},
|
|
224
|
+
card: {
|
|
225
|
+
backgroundColor: _sharedUi.macOSColors.background.card,
|
|
226
|
+
borderRadius: 14,
|
|
227
|
+
borderWidth: 1,
|
|
228
|
+
borderColor: _sharedUi.macOSColors.border.default,
|
|
229
|
+
padding: 14,
|
|
230
|
+
gap: 8
|
|
231
|
+
},
|
|
232
|
+
headerRow: {
|
|
233
|
+
flexDirection: "row",
|
|
234
|
+
alignItems: "center",
|
|
235
|
+
gap: 8
|
|
236
|
+
},
|
|
237
|
+
title: {
|
|
238
|
+
flex: 1,
|
|
239
|
+
color: _sharedUi.macOSColors.text.primary,
|
|
240
|
+
fontSize: 14,
|
|
241
|
+
fontWeight: "700"
|
|
242
|
+
},
|
|
243
|
+
typeBadge: {
|
|
244
|
+
paddingHorizontal: 8,
|
|
245
|
+
paddingVertical: 2,
|
|
246
|
+
borderRadius: 6,
|
|
247
|
+
backgroundColor: _sharedUi.macOSColors.background.hover
|
|
248
|
+
},
|
|
249
|
+
typeBadgeText: {
|
|
250
|
+
color: _sharedUi.macOSColors.semantic.warning,
|
|
251
|
+
fontSize: 10,
|
|
252
|
+
fontWeight: "700",
|
|
253
|
+
fontFamily: "monospace"
|
|
254
|
+
},
|
|
255
|
+
path: {
|
|
256
|
+
color: _sharedUi.macOSColors.text.muted,
|
|
257
|
+
fontSize: 11,
|
|
258
|
+
fontFamily: "monospace"
|
|
259
|
+
},
|
|
260
|
+
/** Caps the panel's height so a huge blob can't push the buttons off-screen. */
|
|
261
|
+
fieldScroll: {
|
|
262
|
+
maxHeight: 240
|
|
263
|
+
},
|
|
264
|
+
input: {
|
|
265
|
+
color: _sharedUi.macOSColors.text.primary,
|
|
266
|
+
backgroundColor: _sharedUi.macOSColors.background.input,
|
|
267
|
+
borderWidth: 1,
|
|
268
|
+
borderColor: _sharedUi.macOSColors.border.input,
|
|
269
|
+
borderRadius: 10,
|
|
270
|
+
paddingHorizontal: 12,
|
|
271
|
+
paddingVertical: 10,
|
|
272
|
+
fontSize: 14,
|
|
273
|
+
fontFamily: "monospace"
|
|
274
|
+
},
|
|
275
|
+
inputMultiline: {
|
|
276
|
+
minHeight: 110,
|
|
277
|
+
lineHeight: 20,
|
|
278
|
+
textAlignVertical: "top"
|
|
279
|
+
},
|
|
280
|
+
inputRaw: {
|
|
281
|
+
minHeight: 180,
|
|
282
|
+
fontSize: 12,
|
|
283
|
+
lineHeight: 18
|
|
284
|
+
},
|
|
285
|
+
inputInvalid: {
|
|
286
|
+
borderColor: _sharedUi.macOSColors.semantic.error
|
|
287
|
+
},
|
|
288
|
+
hint: {
|
|
289
|
+
color: _sharedUi.macOSColors.text.muted,
|
|
290
|
+
fontSize: 11,
|
|
291
|
+
lineHeight: 16
|
|
292
|
+
},
|
|
293
|
+
error: {
|
|
294
|
+
color: _sharedUi.macOSColors.semantic.error,
|
|
295
|
+
fontSize: 11,
|
|
296
|
+
lineHeight: 16,
|
|
297
|
+
fontFamily: "monospace"
|
|
298
|
+
},
|
|
299
|
+
actions: {
|
|
300
|
+
flexDirection: "row",
|
|
301
|
+
gap: 8,
|
|
302
|
+
paddingTop: 2
|
|
303
|
+
},
|
|
304
|
+
cancelButton: {
|
|
305
|
+
flex: 1,
|
|
306
|
+
minHeight: 44,
|
|
307
|
+
alignItems: "center",
|
|
308
|
+
justifyContent: "center",
|
|
309
|
+
borderRadius: 10,
|
|
310
|
+
borderWidth: 1,
|
|
311
|
+
borderColor: _sharedUi.macOSColors.border.default
|
|
312
|
+
},
|
|
313
|
+
cancelText: {
|
|
314
|
+
color: _sharedUi.macOSColors.text.secondary,
|
|
315
|
+
fontSize: 14,
|
|
316
|
+
fontWeight: "600"
|
|
317
|
+
},
|
|
318
|
+
saveButton: {
|
|
319
|
+
flex: 1.4,
|
|
320
|
+
minHeight: 44,
|
|
321
|
+
alignItems: "center",
|
|
322
|
+
justifyContent: "center",
|
|
323
|
+
borderRadius: 10,
|
|
324
|
+
backgroundColor: _sharedUi.macOSColors.semantic.success
|
|
325
|
+
},
|
|
326
|
+
saveDisabled: {
|
|
327
|
+
opacity: 0.35
|
|
328
|
+
},
|
|
329
|
+
saveText: {
|
|
330
|
+
color: _sharedUi.macOSColors.background.base,
|
|
331
|
+
fontSize: 15,
|
|
332
|
+
fontWeight: "700"
|
|
333
|
+
},
|
|
334
|
+
boolRow: {
|
|
335
|
+
flexDirection: "row",
|
|
336
|
+
gap: 10
|
|
337
|
+
},
|
|
338
|
+
boolButton: {
|
|
339
|
+
flex: 1,
|
|
340
|
+
minHeight: 64,
|
|
341
|
+
alignItems: "center",
|
|
342
|
+
justifyContent: "center",
|
|
343
|
+
borderRadius: 12,
|
|
344
|
+
borderWidth: 1,
|
|
345
|
+
borderColor: _sharedUi.macOSColors.border.default,
|
|
346
|
+
backgroundColor: _sharedUi.macOSColors.background.base
|
|
347
|
+
},
|
|
348
|
+
boolActive: {
|
|
349
|
+
borderColor: _sharedUi.macOSColors.semantic.warning,
|
|
350
|
+
backgroundColor: _sharedUi.macOSColors.semantic.warning + "18"
|
|
351
|
+
},
|
|
352
|
+
boolText: {
|
|
353
|
+
color: _sharedUi.macOSColors.text.muted,
|
|
354
|
+
fontSize: 16,
|
|
355
|
+
fontWeight: "700",
|
|
356
|
+
fontFamily: "monospace"
|
|
357
|
+
},
|
|
358
|
+
boolTextActive: {
|
|
359
|
+
color: _sharedUi.macOSColors.semantic.warning
|
|
360
|
+
}
|
|
361
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useKeyEditor = useKeyEditor;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
|
|
9
|
+
var _useTreeDraft = require("./useTreeDraft");
|
|
10
|
+
/**
|
|
11
|
+
* Everything editing a key involves, minus how it looks.
|
|
12
|
+
*
|
|
13
|
+
* The phone and the dashboard run the same editor: one buffered draft, one
|
|
14
|
+
* write on save, a selection the structural actions operate on, a modal for
|
|
15
|
+
* typing a value, and a guard so closing can't silently drop unsaved work.
|
|
16
|
+
* What differs is presentation — thumb-sized tiles under a full screen versus
|
|
17
|
+
* chips docked in a panel — which is a rendering concern and nothing more.
|
|
18
|
+
*
|
|
19
|
+
* Keeping the rules here means the two surfaces can't drift on the things that
|
|
20
|
+
* would actually hurt: whether a boolean opens an editor, whether a failed
|
|
21
|
+
* write closes the editor anyway, what "dirty" means.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function useKeyEditor({
|
|
25
|
+
value,
|
|
26
|
+
save,
|
|
27
|
+
onClose,
|
|
28
|
+
confirmDiscard
|
|
29
|
+
}) {
|
|
30
|
+
const draft = (0, _useTreeDraft.useTreeDraft)(value, save);
|
|
31
|
+
const [selection, setSelection] = (0, _react.useState)(null);
|
|
32
|
+
const [valuePath, setValuePath] = (0, _react.useState)(null);
|
|
33
|
+
const {
|
|
34
|
+
begin
|
|
35
|
+
} = draft;
|
|
36
|
+
(0, _react.useEffect)(() => {
|
|
37
|
+
begin();
|
|
38
|
+
// Seed the draft ONCE, when the editor opens. `begin` closes over the live
|
|
39
|
+
// value, whose identity changes every time the key list refreshes
|
|
40
|
+
// underneath us (a parsed JSON string is a new object each pass), so
|
|
41
|
+
// re-running would reset the draft mid-edit and discard the user's work.
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* "Change this node". For a boolean that's a flip rather than a modal — the
|
|
47
|
+
* same rule `treeActionsFor` applies to the action controls, so the gesture
|
|
48
|
+
* and the button can't mean different things.
|
|
49
|
+
*/
|
|
50
|
+
const openValueEditor = (0, _react.useCallback)(path => {
|
|
51
|
+
const current = (0, _dataViewer.getAtPath)(draft.value, path);
|
|
52
|
+
if (typeof current === "boolean") {
|
|
53
|
+
draft.apply({
|
|
54
|
+
kind: "replace",
|
|
55
|
+
path,
|
|
56
|
+
value: !current
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setValuePath(path);
|
|
61
|
+
}, [draft]);
|
|
62
|
+
const applyValue = (0, _react.useCallback)(next => {
|
|
63
|
+
draft.apply({
|
|
64
|
+
kind: "replace",
|
|
65
|
+
path: valuePath ?? [],
|
|
66
|
+
value: next
|
|
67
|
+
});
|
|
68
|
+
setValuePath(null);
|
|
69
|
+
}, [draft, valuePath]);
|
|
70
|
+
|
|
71
|
+
// Only leave once the write actually landed — `commit` resolves false and
|
|
72
|
+
// keeps the draft on screen when the device refuses it, and closing anyway
|
|
73
|
+
// would throw the work away while showing an error nobody ever sees.
|
|
74
|
+
const saveAndClose = (0, _react.useCallback)(() => {
|
|
75
|
+
void draft.commit().then(saved => saved && onClose());
|
|
76
|
+
}, [draft, onClose]);
|
|
77
|
+
const requestClose = (0, _react.useCallback)(() => {
|
|
78
|
+
if (!draft.isDirty) {
|
|
79
|
+
onClose();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// The draft is the only copy of this work, so closing over it silently is
|
|
83
|
+
// the one way this editor can lose something the user can't get back.
|
|
84
|
+
confirmDiscard({
|
|
85
|
+
save: saveAndClose,
|
|
86
|
+
discard: () => {
|
|
87
|
+
draft.discard();
|
|
88
|
+
onClose();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}, [confirmDiscard, draft, onClose, saveAndClose]);
|
|
92
|
+
return {
|
|
93
|
+
draft,
|
|
94
|
+
selection,
|
|
95
|
+
setSelection,
|
|
96
|
+
valuePath,
|
|
97
|
+
openValueEditor,
|
|
98
|
+
closeValueEditor: () => setValuePath(null),
|
|
99
|
+
applyValue,
|
|
100
|
+
openRawEditor: () => setValuePath([]),
|
|
101
|
+
requestClose,
|
|
102
|
+
saveAndClose
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useMeasuredWidth = useMeasuredWidth;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Measure a container's width, for the layouts that switch between a card list
|
|
10
|
+
* and a wide table.
|
|
11
|
+
*
|
|
12
|
+
* Three things went wrong before this landed on a callback ref; all three are
|
|
13
|
+
* worth knowing, because the same trap is waiting in any other tool that wants
|
|
14
|
+
* a width-driven layout.
|
|
15
|
+
*
|
|
16
|
+
* 1. `onLayout` ALONE IS NOT ENOUGH. On react-native-web it never delivered its
|
|
17
|
+
* first callback for these subtrees. The node carries RNW's
|
|
18
|
+
* `__reactLayoutHandler`, and a ResizeObserver attached by hand fires on that
|
|
19
|
+
* very same node — so the handler is wired and the observer works; RNW's
|
|
20
|
+
* shared observer just never calls it. `width` stayed 0 and the table never
|
|
21
|
+
* appeared, silently. `onLayout` is still wired here because it works on
|
|
22
|
+
* native.
|
|
23
|
+
*
|
|
24
|
+
* 2. RE-MEASURING EVERY RENDER DEADLOCKS THE RENDERER. Filtering a list changes
|
|
25
|
+
* whether its scrollbar is present, which changes the container width, which
|
|
26
|
+
* re-renders, which re-measures — with no fixed point. An observer only fires
|
|
27
|
+
* when the size genuinely changes, so layout settles.
|
|
28
|
+
*
|
|
29
|
+
* 3. A MOUNT-ONLY EFFECT MISSES THE NODE. These components early-return a
|
|
30
|
+
* loading state before the measured container exists, so `ref.current` is
|
|
31
|
+
* null when a `useEffect(…, [])` runs, and it never runs again. A callback
|
|
32
|
+
* ref fires whenever the node actually attaches — including the second time.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** The bits of a DOM node we need. This package has no `dom` lib. */
|
|
36
|
+
|
|
37
|
+
function getResizeObserver() {
|
|
38
|
+
return globalThis.ResizeObserver;
|
|
39
|
+
}
|
|
40
|
+
function isMeasurable(node) {
|
|
41
|
+
return typeof node?.getBoundingClientRect === "function";
|
|
42
|
+
}
|
|
43
|
+
function useMeasuredWidth() {
|
|
44
|
+
const [width, setWidth] = (0, _react.useState)(0);
|
|
45
|
+
const observerRef = (0, _react.useRef)(null);
|
|
46
|
+
|
|
47
|
+
// A sub-pixel wobble is not a layout change; anything larger is.
|
|
48
|
+
const apply = (0, _react.useCallback)(measured => {
|
|
49
|
+
setWidth(prev => Math.abs(prev - measured) > 1 ? measured : prev);
|
|
50
|
+
}, []);
|
|
51
|
+
const ref = (0, _react.useCallback)(node => {
|
|
52
|
+
observerRef.current?.disconnect();
|
|
53
|
+
observerRef.current = null;
|
|
54
|
+
|
|
55
|
+
// Detach (node === null) or a platform without DOM measurement: leave the
|
|
56
|
+
// last width in place so a remount doesn't flash the wrong layout.
|
|
57
|
+
if (!isMeasurable(node)) return;
|
|
58
|
+
apply(node.getBoundingClientRect().width);
|
|
59
|
+
const Observer = getResizeObserver();
|
|
60
|
+
if (!Observer) return;
|
|
61
|
+
const observer = new Observer(entries => {
|
|
62
|
+
const measured = entries[0]?.contentRect?.width;
|
|
63
|
+
if (typeof measured === "number") apply(measured);
|
|
64
|
+
});
|
|
65
|
+
observer.observe(node);
|
|
66
|
+
observerRef.current = observer;
|
|
67
|
+
}, [apply]);
|
|
68
|
+
const onLayout = (0, _react.useCallback)(event => apply(event.nativeEvent.layout.width), [apply]);
|
|
69
|
+
return {
|
|
70
|
+
ref,
|
|
71
|
+
width,
|
|
72
|
+
onLayout
|
|
73
|
+
};
|
|
74
|
+
}
|