@buoy-gg/zustand 7.0.38 → 7.0.40
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 +105 -1
- package/lib/commonjs/preset.js +102 -1
- package/lib/commonjs/zustand/components/ZustandActionButton.js +118 -1
- package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +48 -1
- package/lib/commonjs/zustand/components/ZustandEventFilterView.js +43 -1
- package/lib/commonjs/zustand/components/ZustandIcon.js +18 -1
- package/lib/commonjs/zustand/components/ZustandModal.js +772 -1
- package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +117 -1
- package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +372 -1
- package/lib/commonjs/zustand/components/ZustandStateInfoView.js +488 -1
- package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +204 -1
- package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -1
- package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -1
- package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +74 -1
- package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +382 -1
- package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +284 -1
- package/lib/commonjs/zustand/utils/editableState.js +146 -1
- package/lib/commonjs/zustand/utils/snapshotProvider.js +209 -1
- package/lib/commonjs/zustand/utils/testIDs.js +82 -1
- package/lib/commonjs/zustand/utils/zustandStateStore.js +379 -1
- package/lib/module/index.js +90 -1
- package/lib/module/preset.js +98 -1
- package/lib/module/zustand/components/ZustandActionButton.js +114 -1
- package/lib/module/zustand/components/ZustandDetailViewToggle.js +43 -1
- package/lib/module/zustand/components/ZustandEventFilterView.js +39 -1
- package/lib/module/zustand/components/ZustandIcon.js +11 -1
- package/lib/module/zustand/components/ZustandModal.js +767 -1
- package/lib/module/zustand/components/ZustandStateChangeItem.js +113 -1
- package/lib/module/zustand/components/ZustandStateDetailContent.js +368 -1
- package/lib/module/zustand/components/ZustandStateInfoView.js +483 -1
- package/lib/module/zustand/components/ZustandStoreBrowser.js +200 -1
- package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -1
- package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -1
- package/lib/module/zustand/hooks/useZustandStateChanges.js +70 -1
- package/lib/module/zustand/sync/zustandSyncAdapter.js +380 -1
- package/lib/module/zustand/utils/buoyZustandMiddleware.js +278 -1
- package/lib/module/zustand/utils/editableState.js +138 -1
- package/lib/module/zustand/utils/snapshotProvider.js +209 -1
- package/lib/module/zustand/utils/testIDs.js +74 -1
- package/lib/module/zustand/utils/zustandStateStore.js +375 -1
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
- package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/zustand/types/index.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
- package/package.json +3 -3
|
@@ -1 +1,389 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Editing a store, on a screen of its own — the Storage key editor's shape,
|
|
5
|
+
* pointed at a live zustand store.
|
|
6
|
+
*
|
|
7
|
+
* Same reasoning as storage's editor screen: a tree, six structural actions
|
|
8
|
+
* and a keyboard don't fit inside an expanded card, so the card offers ONE
|
|
9
|
+
* edit button and the work happens here with the whole viewport. The tree,
|
|
10
|
+
* dock, draft and value modal are the shared-ui editor — one implementation,
|
|
11
|
+
* two tools.
|
|
12
|
+
*
|
|
13
|
+
* What is zustand-specific is the WRITE. Storage saves by replacing one key's
|
|
14
|
+
* serialized value; a store has no such luxury — its state holds the app's
|
|
15
|
+
* action functions, and a full `setState(draft, true)` replace would delete
|
|
16
|
+
* them (see `editableState.ts`). So Save diffs the draft against the state it
|
|
17
|
+
* was seeded from and writes ONE top-level merge:
|
|
18
|
+
*
|
|
19
|
+
* store.api.setState(changedTopLevelKeys, false)
|
|
20
|
+
*
|
|
21
|
+
* which by construction cannot remove a key or touch a function. The two
|
|
22
|
+
* edits a merge cannot express — removing or renaming a top-level key — are
|
|
23
|
+
* disabled in the dock (`rootEntriesLocked`) and refused at save if they
|
|
24
|
+
* arrive through the raw-JSON escape hatch.
|
|
25
|
+
*
|
|
26
|
+
* On a desktop mirror the same line goes over the wire: the mirrored store's
|
|
27
|
+
* `api.setState` forwards to the device's sync adapter (the time-travel
|
|
28
|
+
* path). That write has no ack, so a remote save resolves optimistically and
|
|
29
|
+
* the next device snapshot is the confirmation.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useCallback, useMemo, useState } from "react";
|
|
33
|
+
import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
34
|
+
import { buoyColors, Database, FileJson, night, PillBadge, ToolBackground, useSafeAreaInsets, X, monoFont } from "@buoy-gg/shared-ui";
|
|
35
|
+
import { DataTreeActionDock, DataViewer, getAtPath, useTreeEditor, ValueEditModal } from "@buoy-gg/shared-ui/dataViewer";
|
|
36
|
+
import { computeMergePartial, editableStateOf } from "../utils/editableState";
|
|
37
|
+
import { ZUSTAND_TEST_ID } from "../utils/testIDs";
|
|
38
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
39
|
+
const EDITOR_ID = `${ZUSTAND_TEST_ID}-editor`;
|
|
40
|
+
export function ZustandStoreEditorScreen({
|
|
41
|
+
store,
|
|
42
|
+
onClose,
|
|
43
|
+
applySafeAreaInset
|
|
44
|
+
}) {
|
|
45
|
+
const insets = useSafeAreaInsets();
|
|
46
|
+
const bottomInset = applySafeAreaInset ? insets.bottom : 0;
|
|
47
|
+
|
|
48
|
+
// Seeded ONCE, when the editor opens — the draft is diffed against this at
|
|
49
|
+
// save, so the partial carries exactly what the user changed and a key the
|
|
50
|
+
// app wrote meanwhile (untouched here) keeps the app's value.
|
|
51
|
+
const [seed] = useState(() => editableStateOf(store.api.getState()));
|
|
52
|
+
const save = useCallback(async draft => {
|
|
53
|
+
if (seed.data === null) throw new Error("Store is not editable.");
|
|
54
|
+
const partial = computeMergePartial(seed.data, draft);
|
|
55
|
+
// A merge, never a replace — the entire reason this editor can exist.
|
|
56
|
+
// Empty partial = the draft circled back to the seed; nothing to write.
|
|
57
|
+
if (Object.keys(partial).length > 0) {
|
|
58
|
+
store.api.setState(partial, false);
|
|
59
|
+
}
|
|
60
|
+
}, [seed, store]);
|
|
61
|
+
const editor = useTreeEditor({
|
|
62
|
+
value: seed.data,
|
|
63
|
+
save,
|
|
64
|
+
onClose,
|
|
65
|
+
confirmDiscard: choices => Alert.alert("Unsaved changes", `"${store.name}" has edits that haven't been written to the store.`, [{
|
|
66
|
+
text: "Keep editing",
|
|
67
|
+
style: "cancel"
|
|
68
|
+
}, {
|
|
69
|
+
text: "Discard",
|
|
70
|
+
style: "destructive",
|
|
71
|
+
onPress: choices.discard
|
|
72
|
+
}, {
|
|
73
|
+
text: "Save",
|
|
74
|
+
onPress: choices.save
|
|
75
|
+
}])
|
|
76
|
+
});
|
|
77
|
+
const {
|
|
78
|
+
draft,
|
|
79
|
+
selection,
|
|
80
|
+
valuePath
|
|
81
|
+
} = editor;
|
|
82
|
+
const keyCount = useMemo(() => {
|
|
83
|
+
const v = draft.value;
|
|
84
|
+
return v !== null && typeof v === "object" ? Object.keys(v).length : 0;
|
|
85
|
+
}, [draft.value]);
|
|
86
|
+
|
|
87
|
+
// The card's gating means this can't normally be reached un-editable, but a
|
|
88
|
+
// remote mirror can theoretically drop hydration between tap and mount.
|
|
89
|
+
if (seed.data === null) {
|
|
90
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
91
|
+
style: styles.screen,
|
|
92
|
+
children: [/*#__PURE__*/_jsx(ToolBackground, {}), /*#__PURE__*/_jsx(Text, {
|
|
93
|
+
style: styles.blockedText,
|
|
94
|
+
children: seed.blockedReason
|
|
95
|
+
})]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
99
|
+
style: styles.screen,
|
|
100
|
+
children: [/*#__PURE__*/_jsx(ToolBackground, {}), /*#__PURE__*/_jsxs(View, {
|
|
101
|
+
style: styles.header,
|
|
102
|
+
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
103
|
+
onPress: editor.requestClose,
|
|
104
|
+
testID: `${EDITOR_ID}-close`,
|
|
105
|
+
accessibilityRole: "button",
|
|
106
|
+
accessibilityLabel: "Close editor",
|
|
107
|
+
style: styles.headerButton,
|
|
108
|
+
hitSlop: {
|
|
109
|
+
top: 8,
|
|
110
|
+
bottom: 8,
|
|
111
|
+
left: 8,
|
|
112
|
+
right: 8
|
|
113
|
+
},
|
|
114
|
+
children: /*#__PURE__*/_jsx(X, {
|
|
115
|
+
size: 16,
|
|
116
|
+
color: night.color.textSecondary
|
|
117
|
+
})
|
|
118
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
119
|
+
style: styles.headerTitle,
|
|
120
|
+
children: [/*#__PURE__*/_jsx(Text
|
|
121
|
+
// Which store this editor is pointed at — opening it on the wrong
|
|
122
|
+
// one looks exactly like the right one until a write lands
|
|
123
|
+
// somewhere unexpected.
|
|
124
|
+
, {
|
|
125
|
+
testID: `${EDITOR_ID}-store`,
|
|
126
|
+
style: styles.headerKey,
|
|
127
|
+
numberOfLines: 1,
|
|
128
|
+
children: store.name
|
|
129
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
130
|
+
style: [styles.headerState, draft.isDirty && styles.headerDirty],
|
|
131
|
+
children: draft.isSaving ? "Saving…" : draft.isDirty ? "Draft · unsaved" : "In sync with store"
|
|
132
|
+
})]
|
|
133
|
+
}), /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
134
|
+
onPress: editor.openRawEditor,
|
|
135
|
+
testID: `${EDITOR_ID}-raw`,
|
|
136
|
+
accessibilityRole: "button",
|
|
137
|
+
accessibilityLabel: "Edit raw JSON",
|
|
138
|
+
style: styles.headerButton,
|
|
139
|
+
hitSlop: {
|
|
140
|
+
top: 8,
|
|
141
|
+
bottom: 8,
|
|
142
|
+
left: 8,
|
|
143
|
+
right: 8
|
|
144
|
+
},
|
|
145
|
+
children: [/*#__PURE__*/_jsx(FileJson, {
|
|
146
|
+
size: 16,
|
|
147
|
+
color: night.color.accent
|
|
148
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
149
|
+
style: styles.headerRawText,
|
|
150
|
+
children: "Raw"
|
|
151
|
+
})]
|
|
152
|
+
})]
|
|
153
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
154
|
+
style: styles.metaRow,
|
|
155
|
+
children: [/*#__PURE__*/_jsx(PillBadge, {
|
|
156
|
+
color: buoyColors.info,
|
|
157
|
+
icon: store.isPersisted ? /*#__PURE__*/_jsx(Database, {
|
|
158
|
+
size: 9,
|
|
159
|
+
color: buoyColors.info
|
|
160
|
+
}) : undefined,
|
|
161
|
+
children: store.isPersisted ? store.persistName || "persisted" : "in-memory"
|
|
162
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
163
|
+
style: styles.metaText,
|
|
164
|
+
children: [`object · ${keyCount} key${keyCount === 1 ? "" : "s"}`, seed.fnPaths.length > 0 ? ` · ${seed.fnPaths.length} action${seed.fnPaths.length === 1 ? "" : "s"} untouched` : ""]
|
|
165
|
+
})]
|
|
166
|
+
}), /*#__PURE__*/_jsxs(ScrollView, {
|
|
167
|
+
testID: `${EDITOR_ID}-scroll`,
|
|
168
|
+
style: styles.treeScroll,
|
|
169
|
+
contentContainerStyle: styles.treeContent,
|
|
170
|
+
keyboardShouldPersistTaps: "handled",
|
|
171
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
172
|
+
style: styles.treeCard,
|
|
173
|
+
children: /*#__PURE__*/_jsx(DataViewer, {
|
|
174
|
+
data: draft.value,
|
|
175
|
+
title: "",
|
|
176
|
+
rawMode: true,
|
|
177
|
+
initialExpanded: true
|
|
178
|
+
// Type filtering is a READING aid: the filtered view is a
|
|
179
|
+
// projection whose row paths don't address the real document, so
|
|
180
|
+
// the explorer stops reporting selection while one is active. An
|
|
181
|
+
// editor whose actions silently go dead isn't worth the filter.
|
|
182
|
+
,
|
|
183
|
+
showTypeFilter: false,
|
|
184
|
+
onSelectionChange: editor.setSelection,
|
|
185
|
+
selectPath: draft.selectPath
|
|
186
|
+
// Its own handle root: the card that opened this editor is still
|
|
187
|
+
// mounted underneath, rendering the same value in its preview tree.
|
|
188
|
+
,
|
|
189
|
+
testIDPrefix: `${EDITOR_ID}-row`,
|
|
190
|
+
onRequestInlineEdit: sel => editor.openValueEditor(sel.path)
|
|
191
|
+
})
|
|
192
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
193
|
+
style: styles.treeHint,
|
|
194
|
+
children: "Tap a row to select it \xB7 double-tap to edit its value \xB7 saves merge into the live store, so actions and untouched keys survive"
|
|
195
|
+
})]
|
|
196
|
+
}), draft.error ? /*#__PURE__*/_jsx(Text, {
|
|
197
|
+
style: styles.error,
|
|
198
|
+
numberOfLines: 3,
|
|
199
|
+
testID: `${EDITOR_ID}-error`,
|
|
200
|
+
children: draft.error
|
|
201
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
202
|
+
style: [styles.footer, {
|
|
203
|
+
paddingBottom: bottomInset
|
|
204
|
+
}],
|
|
205
|
+
children: [/*#__PURE__*/_jsx(DataTreeActionDock, {
|
|
206
|
+
root: draft.value,
|
|
207
|
+
selection: selection,
|
|
208
|
+
onEdit: draft.apply,
|
|
209
|
+
onSelectPath: draft.selectAt
|
|
210
|
+
// Booleans never reach this — the dock applies their flip directly.
|
|
211
|
+
,
|
|
212
|
+
onEditValue: () => editor.openValueEditor(selection?.path ?? [])
|
|
213
|
+
// A merge write can rewrite a top-level key but never delete one, so
|
|
214
|
+
// Remove is disabled at depth 1 — see `editableState.ts`.
|
|
215
|
+
,
|
|
216
|
+
rootEntriesLocked: true
|
|
217
|
+
}), draft.isDirty ? /*#__PURE__*/_jsxs(View, {
|
|
218
|
+
style: styles.commitRow,
|
|
219
|
+
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
220
|
+
onPress: draft.discard,
|
|
221
|
+
disabled: draft.isSaving,
|
|
222
|
+
testID: `${EDITOR_ID}-discard`,
|
|
223
|
+
accessibilityRole: "button",
|
|
224
|
+
accessibilityLabel: "Discard changes",
|
|
225
|
+
style: styles.discardButton,
|
|
226
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
227
|
+
style: styles.discardText,
|
|
228
|
+
children: "Discard"
|
|
229
|
+
})
|
|
230
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
231
|
+
onPress: editor.saveAndClose,
|
|
232
|
+
disabled: draft.isSaving,
|
|
233
|
+
testID: `${EDITOR_ID}-save`,
|
|
234
|
+
accessibilityRole: "button",
|
|
235
|
+
accessibilityLabel: "Save changes",
|
|
236
|
+
style: [styles.saveButton, draft.isSaving && styles.saveDisabled],
|
|
237
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
238
|
+
style: styles.saveText,
|
|
239
|
+
children: draft.isSaving ? "Saving…" : "Save changes"
|
|
240
|
+
})
|
|
241
|
+
})]
|
|
242
|
+
}) : null]
|
|
243
|
+
}), valuePath ? /*#__PURE__*/_jsx(ValueEditModal, {
|
|
244
|
+
path: valuePath,
|
|
245
|
+
value: getAtPath(draft.value, valuePath),
|
|
246
|
+
onSave: editor.applyValue,
|
|
247
|
+
onCancel: editor.closeValueEditor
|
|
248
|
+
// It only reaches the draft here — the single write happens on Save.
|
|
249
|
+
,
|
|
250
|
+
saveLabel: "Apply",
|
|
251
|
+
testIDPrefix: `${ZUSTAND_TEST_ID}-value`
|
|
252
|
+
}) : null]
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const styles = StyleSheet.create({
|
|
256
|
+
screen: {
|
|
257
|
+
flex: 1,
|
|
258
|
+
// Opaque on purpose: transparency here showed the whole store list
|
|
259
|
+
// through the editor, not just the sheet's backdrop. The screen paints
|
|
260
|
+
// its own ToolBackground instead — see the mount site above.
|
|
261
|
+
backgroundColor: night.color.bg
|
|
262
|
+
},
|
|
263
|
+
blockedText: {
|
|
264
|
+
color: night.color.textTertiary,
|
|
265
|
+
fontSize: 12,
|
|
266
|
+
padding: 20,
|
|
267
|
+
textAlign: "center",
|
|
268
|
+
lineHeight: 18
|
|
269
|
+
},
|
|
270
|
+
header: {
|
|
271
|
+
flexDirection: "row",
|
|
272
|
+
alignItems: "center",
|
|
273
|
+
gap: 8,
|
|
274
|
+
paddingHorizontal: 10,
|
|
275
|
+
paddingVertical: 8,
|
|
276
|
+
borderBottomWidth: 1,
|
|
277
|
+
borderBottomColor: night.color.border
|
|
278
|
+
},
|
|
279
|
+
headerButton: {
|
|
280
|
+
flexDirection: "row",
|
|
281
|
+
alignItems: "center",
|
|
282
|
+
gap: 4,
|
|
283
|
+
paddingVertical: 6,
|
|
284
|
+
paddingHorizontal: 4
|
|
285
|
+
},
|
|
286
|
+
headerRawText: {
|
|
287
|
+
color: night.color.accent,
|
|
288
|
+
fontSize: 12,
|
|
289
|
+
fontWeight: "700"
|
|
290
|
+
},
|
|
291
|
+
headerTitle: {
|
|
292
|
+
flex: 1,
|
|
293
|
+
alignItems: "center",
|
|
294
|
+
gap: 2
|
|
295
|
+
},
|
|
296
|
+
headerKey: {
|
|
297
|
+
color: night.color.text,
|
|
298
|
+
fontSize: 13,
|
|
299
|
+
fontWeight: "600",
|
|
300
|
+
fontFamily: monoFont
|
|
301
|
+
},
|
|
302
|
+
headerState: {
|
|
303
|
+
color: night.color.textTertiary,
|
|
304
|
+
fontSize: 10
|
|
305
|
+
},
|
|
306
|
+
headerDirty: {
|
|
307
|
+
color: night.color.warning
|
|
308
|
+
},
|
|
309
|
+
metaRow: {
|
|
310
|
+
flexDirection: "row",
|
|
311
|
+
alignItems: "center",
|
|
312
|
+
gap: 8,
|
|
313
|
+
paddingHorizontal: 12,
|
|
314
|
+
paddingVertical: 8
|
|
315
|
+
},
|
|
316
|
+
metaText: {
|
|
317
|
+
color: night.color.textTertiary,
|
|
318
|
+
fontSize: 11,
|
|
319
|
+
fontFamily: monoFont
|
|
320
|
+
},
|
|
321
|
+
treeScroll: {
|
|
322
|
+
flex: 1
|
|
323
|
+
},
|
|
324
|
+
treeContent: {
|
|
325
|
+
paddingHorizontal: 12,
|
|
326
|
+
paddingBottom: 16
|
|
327
|
+
},
|
|
328
|
+
treeCard: {
|
|
329
|
+
backgroundColor: night.color.surface,
|
|
330
|
+
borderRadius: 10,
|
|
331
|
+
borderWidth: 1,
|
|
332
|
+
borderColor: night.color.border,
|
|
333
|
+
padding: 6
|
|
334
|
+
},
|
|
335
|
+
treeHint: {
|
|
336
|
+
color: night.color.textTertiary,
|
|
337
|
+
fontSize: 10,
|
|
338
|
+
paddingTop: 8,
|
|
339
|
+
paddingHorizontal: 4
|
|
340
|
+
},
|
|
341
|
+
error: {
|
|
342
|
+
color: night.color.danger,
|
|
343
|
+
fontSize: 11,
|
|
344
|
+
fontFamily: monoFont,
|
|
345
|
+
paddingHorizontal: 12,
|
|
346
|
+
paddingBottom: 6
|
|
347
|
+
},
|
|
348
|
+
footer: {
|
|
349
|
+
backgroundColor: night.color.surface
|
|
350
|
+
},
|
|
351
|
+
commitRow: {
|
|
352
|
+
flexDirection: "row",
|
|
353
|
+
gap: 8,
|
|
354
|
+
paddingHorizontal: 12,
|
|
355
|
+
paddingVertical: 10,
|
|
356
|
+
borderTopWidth: 1,
|
|
357
|
+
borderTopColor: night.color.border
|
|
358
|
+
},
|
|
359
|
+
discardButton: {
|
|
360
|
+
flex: 1,
|
|
361
|
+
minHeight: 44,
|
|
362
|
+
alignItems: "center",
|
|
363
|
+
justifyContent: "center",
|
|
364
|
+
borderRadius: 10,
|
|
365
|
+
borderWidth: 1,
|
|
366
|
+
borderColor: night.color.border
|
|
367
|
+
},
|
|
368
|
+
discardText: {
|
|
369
|
+
color: night.color.textSecondary,
|
|
370
|
+
fontSize: 14,
|
|
371
|
+
fontWeight: "600"
|
|
372
|
+
},
|
|
373
|
+
saveButton: {
|
|
374
|
+
flex: 1.4,
|
|
375
|
+
minHeight: 44,
|
|
376
|
+
alignItems: "center",
|
|
377
|
+
justifyContent: "center",
|
|
378
|
+
borderRadius: 10,
|
|
379
|
+
backgroundColor: night.color.accent
|
|
380
|
+
},
|
|
381
|
+
saveDisabled: {
|
|
382
|
+
opacity: 0.5
|
|
383
|
+
},
|
|
384
|
+
saveText: {
|
|
385
|
+
color: night.color.onAccent,
|
|
386
|
+
fontSize: 15,
|
|
387
|
+
fontWeight: "700"
|
|
388
|
+
}
|
|
389
|
+
});
|
|
@@ -1 +1,70 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for consuming Zustand state changes from the store
|
|
5
|
+
*
|
|
6
|
+
* The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
|
|
7
|
+
* this file owns the zustand-specific filter, stats and store registry.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useMemo } from "react";
|
|
11
|
+
import { useChangeLog, useSubscribedValue } from "@buoy-gg/shared-ui";
|
|
12
|
+
import { zustandStateStore } from "../utils/zustandStateStore";
|
|
13
|
+
function filterChanges(changes, filter) {
|
|
14
|
+
let filtered = changes;
|
|
15
|
+
if (filter.searchText) {
|
|
16
|
+
const search = filter.searchText.toLowerCase();
|
|
17
|
+
filtered = filtered.filter(c => c.storeName.toLowerCase().includes(search) || c.partialPreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
|
|
18
|
+
}
|
|
19
|
+
if (filter.storeNames && filter.storeNames.length > 0) {
|
|
20
|
+
filtered = filtered.filter(c => filter.storeNames.includes(c.storeName));
|
|
21
|
+
}
|
|
22
|
+
if (filter.onlyWithChanges) {
|
|
23
|
+
filtered = filtered.filter(c => c.hasStateChange);
|
|
24
|
+
}
|
|
25
|
+
return filtered;
|
|
26
|
+
}
|
|
27
|
+
const readStores = () => zustandStateStore.getStores();
|
|
28
|
+
const subscribeToStores = listener => zustandStateStore.subscribeToStores(listener);
|
|
29
|
+
export function useZustandStateChanges() {
|
|
30
|
+
const log = useChangeLog(zustandStateStore, {
|
|
31
|
+
filterItems: filterChanges
|
|
32
|
+
});
|
|
33
|
+
const stores = useSubscribedValue(readStores, subscribeToStores);
|
|
34
|
+
|
|
35
|
+
// Get stats — derive directly from state
|
|
36
|
+
const stats = useMemo(() => {
|
|
37
|
+
const total = log.items.length;
|
|
38
|
+
const withChanges = log.items.filter(c => c.hasStateChange).length;
|
|
39
|
+
return {
|
|
40
|
+
totalChanges: total,
|
|
41
|
+
changesWithStateChange: withChanges,
|
|
42
|
+
changesWithoutStateChange: total - withChanges,
|
|
43
|
+
storeCount: stores.length,
|
|
44
|
+
averageDuration: 0
|
|
45
|
+
};
|
|
46
|
+
}, [log.items, stores]);
|
|
47
|
+
|
|
48
|
+
// Get unique store names
|
|
49
|
+
const storeNames = useMemo(() => {
|
|
50
|
+
return zustandStateStore.getUniqueStoreNames();
|
|
51
|
+
}, [stores]);
|
|
52
|
+
const getChangeById = useCallback(id => {
|
|
53
|
+
return zustandStateStore.getStateChangeById(id);
|
|
54
|
+
}, []);
|
|
55
|
+
return {
|
|
56
|
+
stateChanges: log.items,
|
|
57
|
+
filteredChanges: log.filteredItems,
|
|
58
|
+
filter: log.filter,
|
|
59
|
+
setFilter: log.setFilter,
|
|
60
|
+
stats,
|
|
61
|
+
stores,
|
|
62
|
+
// Not the shared log.clear: the store also owns each store's change
|
|
63
|
+
// count, and only clearStateChanges resets those with the list.
|
|
64
|
+
clearChanges: zustandStateStore.clearStateChanges.bind(zustandStateStore),
|
|
65
|
+
isEnabled: log.isEnabled,
|
|
66
|
+
toggleCapture: log.toggleCapture,
|
|
67
|
+
storeNames,
|
|
68
|
+
getChangeById
|
|
69
|
+
};
|
|
70
|
+
}
|