@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.
Files changed (62) hide show
  1. package/lib/commonjs/index.js +105 -1
  2. package/lib/commonjs/preset.js +102 -1
  3. package/lib/commonjs/zustand/components/ZustandActionButton.js +118 -1
  4. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +48 -1
  5. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +43 -1
  6. package/lib/commonjs/zustand/components/ZustandIcon.js +18 -1
  7. package/lib/commonjs/zustand/components/ZustandModal.js +772 -1
  8. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +117 -1
  9. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +372 -1
  10. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +488 -1
  11. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +204 -1
  12. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -1
  13. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -1
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +74 -1
  15. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +382 -1
  16. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +284 -1
  17. package/lib/commonjs/zustand/utils/editableState.js +146 -1
  18. package/lib/commonjs/zustand/utils/snapshotProvider.js +209 -1
  19. package/lib/commonjs/zustand/utils/testIDs.js +82 -1
  20. package/lib/commonjs/zustand/utils/zustandStateStore.js +379 -1
  21. package/lib/module/index.js +90 -1
  22. package/lib/module/preset.js +98 -1
  23. package/lib/module/zustand/components/ZustandActionButton.js +114 -1
  24. package/lib/module/zustand/components/ZustandDetailViewToggle.js +43 -1
  25. package/lib/module/zustand/components/ZustandEventFilterView.js +39 -1
  26. package/lib/module/zustand/components/ZustandIcon.js +11 -1
  27. package/lib/module/zustand/components/ZustandModal.js +767 -1
  28. package/lib/module/zustand/components/ZustandStateChangeItem.js +113 -1
  29. package/lib/module/zustand/components/ZustandStateDetailContent.js +368 -1
  30. package/lib/module/zustand/components/ZustandStateInfoView.js +483 -1
  31. package/lib/module/zustand/components/ZustandStoreBrowser.js +200 -1
  32. package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -1
  33. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -1
  34. package/lib/module/zustand/hooks/useZustandStateChanges.js +70 -1
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +380 -1
  36. package/lib/module/zustand/utils/buoyZustandMiddleware.js +278 -1
  37. package/lib/module/zustand/utils/editableState.js +138 -1
  38. package/lib/module/zustand/utils/snapshotProvider.js +209 -1
  39. package/lib/module/zustand/utils/testIDs.js +74 -1
  40. package/lib/module/zustand/utils/zustandStateStore.js +375 -1
  41. package/lib/typescript/index.d.ts.map +1 -0
  42. package/lib/typescript/preset.d.ts.map +1 -0
  43. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
  44. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
  45. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
  46. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
  47. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
  48. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
  49. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
  50. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
  51. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
  52. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
  53. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
  54. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
  55. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
  56. package/lib/typescript/zustand/types/index.d.ts.map +1 -0
  57. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
  58. package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
  59. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
  60. package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
  61. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
  62. package/package.json +3 -3
@@ -1 +1,389 @@
1
- "use strict";import{useCallback,useMemo,useState}from"react";import{Alert,ScrollView,StyleSheet,Text,TouchableOpacity,View}from"react-native";import{buoyColors,Database,FileJson,night,PillBadge,ToolBackground,useSafeAreaInsets,X,monoFont}from"@buoy-gg/shared-ui";import{DataTreeActionDock,DataViewer,getAtPath,useTreeEditor,ValueEditModal}from"@buoy-gg/shared-ui/dataViewer";import{computeMergePartial,editableStateOf}from"../utils/editableState";import{ZUSTAND_TEST_ID}from"../utils/testIDs";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";const EDITOR_ID=`${ZUSTAND_TEST_ID}-editor`;export function ZustandStoreEditorScreen({store:e,onClose:t,applySafeAreaInset:o}){const s=useSafeAreaInsets(),r=o?s.bottom:0,[i]=useState(()=>editableStateOf(e.api.getState())),a=useCallback(async t=>{if(null===i.data)throw new Error("Store is not editable.");const o=computeMergePartial(i.data,t);Object.keys(o).length>0&&e.api.setState(o,!1)},[i,e]),l=useTreeEditor({value:i.data,save:a,onClose:t,confirmDiscard:t=>Alert.alert("Unsaved changes",`"${e.name}" has edits that haven't been written to the store.`,[{text:"Keep editing",style:"cancel"},{text:"Discard",style:"destructive",onPress:t.discard},{text:"Save",onPress:t.save}])}),{draft:n,selection:c,valuePath:d}=l,h=useMemo(()=>{const e=n.value;return null!==e&&"object"==typeof e?Object.keys(e).length:0},[n.value]);return null===i.data?_jsxs(View,{style:styles.screen,children:[_jsx(ToolBackground,{}),_jsx(Text,{style:styles.blockedText,children:i.blockedReason})]}):_jsxs(View,{style:styles.screen,children:[_jsx(ToolBackground,{}),_jsxs(View,{style:styles.header,children:[_jsx(TouchableOpacity,{onPress:l.requestClose,testID:`${EDITOR_ID}-close`,accessibilityRole:"button",accessibilityLabel:"Close editor",style:styles.headerButton,hitSlop:{top:8,bottom:8,left:8,right:8},children:_jsx(X,{size:16,color:night.color.textSecondary})}),_jsxs(View,{style:styles.headerTitle,children:[_jsx(Text,{testID:`${EDITOR_ID}-store`,style:styles.headerKey,numberOfLines:1,children:e.name}),_jsx(Text,{style:[styles.headerState,n.isDirty&&styles.headerDirty],children:n.isSaving?"Saving…":n.isDirty?"Draft · unsaved":"In sync with store"})]}),_jsxs(TouchableOpacity,{onPress:l.openRawEditor,testID:`${EDITOR_ID}-raw`,accessibilityRole:"button",accessibilityLabel:"Edit raw JSON",style:styles.headerButton,hitSlop:{top:8,bottom:8,left:8,right:8},children:[_jsx(FileJson,{size:16,color:night.color.accent}),_jsx(Text,{style:styles.headerRawText,children:"Raw"})]})]}),_jsxs(View,{style:styles.metaRow,children:[_jsx(PillBadge,{color:buoyColors.info,icon:e.isPersisted?_jsx(Database,{size:9,color:buoyColors.info}):void 0,children:e.isPersisted?e.persistName||"persisted":"in-memory"}),_jsxs(Text,{style:styles.metaText,children:[`object · ${h} key${1===h?"":"s"}`,i.fnPaths.length>0?` · ${i.fnPaths.length} action${1===i.fnPaths.length?"":"s"} untouched`:""]})]}),_jsxs(ScrollView,{testID:`${EDITOR_ID}-scroll`,style:styles.treeScroll,contentContainerStyle:styles.treeContent,keyboardShouldPersistTaps:"handled",children:[_jsx(View,{style:styles.treeCard,children:_jsx(DataViewer,{data:n.value,title:"",rawMode:!0,initialExpanded:!0,showTypeFilter:!1,onSelectionChange:l.setSelection,selectPath:n.selectPath,testIDPrefix:`${EDITOR_ID}-row`,onRequestInlineEdit:e=>l.openValueEditor(e.path)})}),_jsx(Text,{style:styles.treeHint,children:"Tap a row to select it · double-tap to edit its value · saves merge into the live store, so actions and untouched keys survive"})]}),n.error?_jsx(Text,{style:styles.error,numberOfLines:3,testID:`${EDITOR_ID}-error`,children:n.error}):null,_jsxs(View,{style:[styles.footer,{paddingBottom:r}],children:[_jsx(DataTreeActionDock,{root:n.value,selection:c,onEdit:n.apply,onSelectPath:n.selectAt,onEditValue:()=>l.openValueEditor(c?.path??[]),rootEntriesLocked:!0}),n.isDirty?_jsxs(View,{style:styles.commitRow,children:[_jsx(TouchableOpacity,{onPress:n.discard,disabled:n.isSaving,testID:`${EDITOR_ID}-discard`,accessibilityRole:"button",accessibilityLabel:"Discard changes",style:styles.discardButton,children:_jsx(Text,{style:styles.discardText,children:"Discard"})}),_jsx(TouchableOpacity,{onPress:l.saveAndClose,disabled:n.isSaving,testID:`${EDITOR_ID}-save`,accessibilityRole:"button",accessibilityLabel:"Save changes",style:[styles.saveButton,n.isSaving&&styles.saveDisabled],children:_jsx(Text,{style:styles.saveText,children:n.isSaving?"Saving…":"Save changes"})})]}):null]}),d?_jsx(ValueEditModal,{path:d,value:getAtPath(n.value,d),onSave:l.applyValue,onCancel:l.closeValueEditor,saveLabel:"Apply",testIDPrefix:`${ZUSTAND_TEST_ID}-value`}):null]})}const styles=StyleSheet.create({screen:{flex:1,backgroundColor:night.color.bg},blockedText:{color:night.color.textTertiary,fontSize:12,padding:20,textAlign:"center",lineHeight:18},header:{flexDirection:"row",alignItems:"center",gap:8,paddingHorizontal:10,paddingVertical:8,borderBottomWidth:1,borderBottomColor:night.color.border},headerButton:{flexDirection:"row",alignItems:"center",gap:4,paddingVertical:6,paddingHorizontal:4},headerRawText:{color:night.color.accent,fontSize:12,fontWeight:"700"},headerTitle:{flex:1,alignItems:"center",gap:2},headerKey:{color:night.color.text,fontSize:13,fontWeight:"600",fontFamily:monoFont},headerState:{color:night.color.textTertiary,fontSize:10},headerDirty:{color:night.color.warning},metaRow:{flexDirection:"row",alignItems:"center",gap:8,paddingHorizontal:12,paddingVertical:8},metaText:{color:night.color.textTertiary,fontSize:11,fontFamily:monoFont},treeScroll:{flex:1},treeContent:{paddingHorizontal:12,paddingBottom:16},treeCard:{backgroundColor:night.color.surface,borderRadius:10,borderWidth:1,borderColor:night.color.border,padding:6},treeHint:{color:night.color.textTertiary,fontSize:10,paddingTop:8,paddingHorizontal:4},error:{color:night.color.danger,fontSize:11,fontFamily:monoFont,paddingHorizontal:12,paddingBottom:6},footer:{backgroundColor:night.color.surface},commitRow:{flexDirection:"row",gap:8,paddingHorizontal:12,paddingVertical:10,borderTopWidth:1,borderTopColor:night.color.border},discardButton:{flex:1,minHeight:44,alignItems:"center",justifyContent:"center",borderRadius:10,borderWidth:1,borderColor:night.color.border},discardText:{color:night.color.textSecondary,fontSize:14,fontWeight:"600"},saveButton:{flex:1.4,minHeight:44,alignItems:"center",justifyContent:"center",borderRadius:10,backgroundColor:night.color.accent},saveDisabled:{opacity:.5},saveText:{color:night.color.onAccent,fontSize:15,fontWeight:"700"}});
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";import{useCallback,useMemo}from"react";import{useChangeLog,useSubscribedValue}from"@buoy-gg/shared-ui";import{zustandStateStore}from"../utils/zustandStateStore";function filterChanges(e,t){let s=e;if(t.searchText){const e=t.searchText.toLowerCase();s=s.filter(t=>t.storeName.toLowerCase().includes(e)||t.partialPreview.toLowerCase().includes(e)||t.changedKeys.some(t=>t.toLowerCase().includes(e)))}return t.storeNames&&t.storeNames.length>0&&(s=s.filter(e=>t.storeNames.includes(e.storeName))),t.onlyWithChanges&&(s=s.filter(e=>e.hasStateChange)),s}const readStores=()=>zustandStateStore.getStores(),subscribeToStores=e=>zustandStateStore.subscribeToStores(e);export function useZustandStateChanges(){const e=useChangeLog(zustandStateStore,{filterItems:filterChanges}),t=useSubscribedValue(readStores,subscribeToStores),s=useMemo(()=>{const s=e.items.length,a=e.items.filter(e=>e.hasStateChange).length;return{totalChanges:s,changesWithStateChange:a,changesWithoutStateChange:s-a,storeCount:t.length,averageDuration:0}},[e.items,t]),a=useMemo(()=>zustandStateStore.getUniqueStoreNames(),[t]),r=useCallback(e=>zustandStateStore.getStateChangeById(e),[]);return{stateChanges:e.items,filteredChanges:e.filteredItems,filter:e.filter,setFilter:e.setFilter,stats:s,stores:t,clearChanges:zustandStateStore.clearStateChanges.bind(zustandStateStore),isEnabled:e.isEnabled,toggleCapture:e.toggleCapture,storeNames:a,getChangeById:r}}
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
+ }