@buoy-gg/zustand 7.0.41 → 7.0.43
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/README.md +11 -76
- package/lib/commonjs/index.js +1 -105
- package/lib/commonjs/preset.js +1 -102
- package/lib/commonjs/zustand/components/ZustandActionButton.js +1 -118
- package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +1 -48
- package/lib/commonjs/zustand/components/ZustandEventFilterView.js +1 -43
- package/lib/commonjs/zustand/components/ZustandIcon.js +1 -18
- package/lib/commonjs/zustand/components/ZustandModal.js +1 -772
- package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +1 -117
- package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +1 -372
- package/lib/commonjs/zustand/components/ZustandStateInfoView.js +1 -488
- package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +1 -204
- package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +1 -471
- package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +1 -74
- package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +1 -418
- package/lib/commonjs/zustand/types/index.js +1 -1
- package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +1 -284
- package/lib/commonjs/zustand/utils/editableState.js +1 -146
- package/lib/commonjs/zustand/utils/snapshotProvider.js +1 -209
- package/lib/commonjs/zustand/utils/testIDs.js +1 -82
- package/lib/commonjs/zustand/utils/zustandStateStore.js +1 -379
- package/lib/module/index.js +1 -90
- package/lib/module/preset.js +1 -98
- package/lib/module/zustand/components/ZustandActionButton.js +1 -114
- package/lib/module/zustand/components/ZustandDetailViewToggle.js +1 -43
- package/lib/module/zustand/components/ZustandEventFilterView.js +1 -39
- package/lib/module/zustand/components/ZustandIcon.js +1 -11
- package/lib/module/zustand/components/ZustandModal.js +1 -767
- package/lib/module/zustand/components/ZustandStateChangeItem.js +1 -113
- package/lib/module/zustand/components/ZustandStateDetailContent.js +1 -368
- package/lib/module/zustand/components/ZustandStateInfoView.js +1 -483
- package/lib/module/zustand/components/ZustandStoreBrowser.js +1 -200
- package/lib/module/zustand/components/ZustandStoreDetailContent.js +1 -469
- package/lib/module/zustand/hooks/useZustandStateChanges.js +1 -70
- package/lib/module/zustand/sync/zustandSyncAdapter.js +1 -415
- package/lib/module/zustand/types/index.js +1 -1
- package/lib/module/zustand/utils/buoyZustandMiddleware.js +1 -278
- package/lib/module/zustand/utils/editableState.js +1 -138
- package/lib/module/zustand/utils/snapshotProvider.js +1 -209
- package/lib/module/zustand/utils/testIDs.js +1 -74
- package/lib/module/zustand/utils/zustandStateStore.js +1 -375
- package/lib/web/index.mjs +2 -3
- package/package.json +4 -7
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandModal.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +0 -1
- package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +0 -1
- package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +0 -1
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/zustand/types/index.d.ts.map +0 -1
- package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +0 -1
- package/lib/typescript/zustand/utils/editableState.d.ts.map +0 -1
- package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +0 -1
- package/lib/typescript/zustand/utils/testIDs.d.ts.map +0 -1
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +0 -1
|
@@ -1,200 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ZustandStoreBrowser
|
|
5
|
-
*
|
|
6
|
-
* Browse tab — lists all registered Zustand stores. Tapping a card opens the
|
|
7
|
-
* store's detail screen (ZustandStoreDetailContent): live state editor on
|
|
8
|
-
* top, store details and the read-only tree below — the React Query
|
|
9
|
-
* interaction, replacing the old expand-in-place card.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { useMemo } from "react";
|
|
13
|
-
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
|
14
|
-
import { CompactRow, night, ToolEmptyState, resolveListEmptyState, monoFont } from "@buoy-gg/shared-ui";
|
|
15
|
-
import { zustandStateStore } from "../utils/zustandStateStore";
|
|
16
|
-
import { ZUSTAND_TEST_ID, storeRowTestID } from "../utils/testIDs";
|
|
17
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
/**
|
|
19
|
-
* Get a preview of the store's top-level keys
|
|
20
|
-
*/
|
|
21
|
-
function getKeysPreview(store) {
|
|
22
|
-
try {
|
|
23
|
-
const state = store.api.getState();
|
|
24
|
-
if (state && typeof state === "object" && state.__buoyStateOnDevice === true) {
|
|
25
|
-
return "on device";
|
|
26
|
-
}
|
|
27
|
-
if (state && typeof state === "object") {
|
|
28
|
-
const keys = Object.keys(state);
|
|
29
|
-
const dataKeys = keys.filter(k => {
|
|
30
|
-
const val = state[k];
|
|
31
|
-
return typeof val !== "function";
|
|
32
|
-
});
|
|
33
|
-
if (dataKeys.length === 0) return "no data keys";
|
|
34
|
-
if (dataKeys.length <= 3) return dataKeys.join(", ");
|
|
35
|
-
return `${dataKeys.slice(0, 2).join(", ")} +${dataKeys.length - 2}`;
|
|
36
|
-
}
|
|
37
|
-
return "";
|
|
38
|
-
} catch {
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function LoadingBrowserState() {
|
|
43
|
-
return /*#__PURE__*/_jsx(ToolEmptyState, {
|
|
44
|
-
testID: `${ZUSTAND_TEST_ID}-stores-loading`
|
|
45
|
-
// Replaces the spinner rather than joining it: two animated things
|
|
46
|
-
// stacked is clutter, and `loading` IS the indeterminate spinner in
|
|
47
|
-
// mascot form.
|
|
48
|
-
,
|
|
49
|
-
otto: "loading",
|
|
50
|
-
title: "Loading stores\u2026",
|
|
51
|
-
message: "Waiting for the device to send its registered stores."
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
function EmptyBrowserState() {
|
|
55
|
-
return /*#__PURE__*/_jsx(ToolEmptyState, {
|
|
56
|
-
testID: `${ZUSTAND_TEST_ID}-stores-empty`
|
|
57
|
-
// scanning = "scanning for targets… and finding none". The registry is
|
|
58
|
-
// discovered, not configured, so an empty one is a scan result.
|
|
59
|
-
,
|
|
60
|
-
otto: "scanning",
|
|
61
|
-
title: "No stores registered",
|
|
62
|
-
message: "Use watchStores() to register your Zustand stores.\nThey will appear here with their current state."
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
export function ZustandStoreBrowser({
|
|
66
|
-
stores,
|
|
67
|
-
searchQuery,
|
|
68
|
-
isLoading = false,
|
|
69
|
-
onOpenStore
|
|
70
|
-
}) {
|
|
71
|
-
// Filter stores by search query
|
|
72
|
-
const filteredStores = useMemo(() => {
|
|
73
|
-
if (!searchQuery) return stores;
|
|
74
|
-
const search = searchQuery.toLowerCase();
|
|
75
|
-
return stores.filter(s => s.name.toLowerCase().includes(search) || s.persistName && s.persistName.toLowerCase().includes(search));
|
|
76
|
-
}, [stores, searchQuery]);
|
|
77
|
-
if (filteredStores.length === 0 && !searchQuery) {
|
|
78
|
-
return isLoading ? /*#__PURE__*/_jsx(LoadingBrowserState, {}) : /*#__PURE__*/_jsx(EmptyBrowserState, {});
|
|
79
|
-
}
|
|
80
|
-
if (filteredStores.length === 0 && searchQuery) {
|
|
81
|
-
return /*#__PURE__*/_jsx(ToolEmptyState, {
|
|
82
|
-
testID: `${ZUSTAND_TEST_ID}-stores-no-match`,
|
|
83
|
-
...resolveListEmptyState({
|
|
84
|
-
subject: "stores",
|
|
85
|
-
verb: "registered",
|
|
86
|
-
search: searchQuery,
|
|
87
|
-
total: stores.length,
|
|
88
|
-
waiting: {
|
|
89
|
-
otto: "scanning",
|
|
90
|
-
title: "No stores registered",
|
|
91
|
-
message: "Use watchStores() to register your Zustand stores."
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
return /*#__PURE__*/_jsxs(ScrollView, {
|
|
97
|
-
style: styles.container,
|
|
98
|
-
contentContainerStyle: styles.scrollContent,
|
|
99
|
-
showsVerticalScrollIndicator: true,
|
|
100
|
-
testID: `${ZUSTAND_TEST_ID}-stores-scroll`,
|
|
101
|
-
children: [/*#__PURE__*/_jsxs(View, {
|
|
102
|
-
style: styles.sectionHeader,
|
|
103
|
-
children: [/*#__PURE__*/_jsx(Text, {
|
|
104
|
-
style: styles.sectionTitle,
|
|
105
|
-
children: "STORES"
|
|
106
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
107
|
-
style: styles.sectionCountBadge,
|
|
108
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
109
|
-
style: styles.sectionCountText,
|
|
110
|
-
testID: `${ZUSTAND_TEST_ID}-stores-count`,
|
|
111
|
-
children: filteredStores.length
|
|
112
|
-
})
|
|
113
|
-
})]
|
|
114
|
-
}), filteredStores.map(store => {
|
|
115
|
-
const keysPreview = getKeysPreview(store);
|
|
116
|
-
const storeColor = zustandStateStore.getStoreColor(store.name);
|
|
117
|
-
const rowID = storeRowTestID(store.name);
|
|
118
|
-
return /*#__PURE__*/_jsxs(View, {
|
|
119
|
-
style: styles.storeRowWrapper,
|
|
120
|
-
children: [/*#__PURE__*/_jsx(CompactRow, {
|
|
121
|
-
statusDotColor: storeColor,
|
|
122
|
-
statusLabel: store.isPersisted ? "persisted" : "in-memory",
|
|
123
|
-
primaryText: store.name,
|
|
124
|
-
secondaryText: keysPreview || undefined,
|
|
125
|
-
showChevron: true,
|
|
126
|
-
onPress: () => onOpenStore(store.name),
|
|
127
|
-
testID: rowID,
|
|
128
|
-
statusTestID: `${rowID}-backing`,
|
|
129
|
-
primaryTestID: `${rowID}-name`,
|
|
130
|
-
secondaryTestID: `${rowID}-keys`
|
|
131
|
-
}), store.stateChangeCount > 0 && /*#__PURE__*/_jsx(View, {
|
|
132
|
-
style: [styles.absCountBadge, {
|
|
133
|
-
backgroundColor: storeColor + "22",
|
|
134
|
-
borderColor: storeColor + "55"
|
|
135
|
-
}],
|
|
136
|
-
pointerEvents: "none",
|
|
137
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
138
|
-
style: [styles.absCountText, {
|
|
139
|
-
color: storeColor
|
|
140
|
-
}],
|
|
141
|
-
testID: `${rowID}-count`,
|
|
142
|
-
children: String(store.stateChangeCount)
|
|
143
|
-
})
|
|
144
|
-
})]
|
|
145
|
-
}, store.name);
|
|
146
|
-
})]
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const styles = StyleSheet.create({
|
|
150
|
-
container: {
|
|
151
|
-
flex: 1
|
|
152
|
-
},
|
|
153
|
-
storeRowWrapper: {
|
|
154
|
-
position: "relative"
|
|
155
|
-
},
|
|
156
|
-
absCountBadge: {
|
|
157
|
-
position: "absolute",
|
|
158
|
-
top: 4,
|
|
159
|
-
right: 10,
|
|
160
|
-
paddingHorizontal: 5,
|
|
161
|
-
paddingVertical: 1,
|
|
162
|
-
borderRadius: 4,
|
|
163
|
-
borderWidth: 1,
|
|
164
|
-
zIndex: 1
|
|
165
|
-
},
|
|
166
|
-
absCountText: {
|
|
167
|
-
fontSize: 9,
|
|
168
|
-
fontWeight: "700",
|
|
169
|
-
fontFamily: monoFont
|
|
170
|
-
},
|
|
171
|
-
scrollContent: {
|
|
172
|
-
paddingTop: 8,
|
|
173
|
-
paddingBottom: 20
|
|
174
|
-
},
|
|
175
|
-
sectionHeader: {
|
|
176
|
-
flexDirection: "row",
|
|
177
|
-
alignItems: "center",
|
|
178
|
-
paddingHorizontal: 16,
|
|
179
|
-
paddingVertical: 8,
|
|
180
|
-
gap: 8
|
|
181
|
-
},
|
|
182
|
-
sectionTitle: {
|
|
183
|
-
fontSize: 11,
|
|
184
|
-
fontWeight: "700",
|
|
185
|
-
letterSpacing: 0.5,
|
|
186
|
-
color: night.color.textTertiary
|
|
187
|
-
},
|
|
188
|
-
sectionCountBadge: {
|
|
189
|
-
backgroundColor: night.color.accentFill,
|
|
190
|
-
paddingHorizontal: 8,
|
|
191
|
-
paddingVertical: 2,
|
|
192
|
-
borderRadius: 4
|
|
193
|
-
},
|
|
194
|
-
sectionCountText: {
|
|
195
|
-
fontSize: 10,
|
|
196
|
-
fontWeight: "700",
|
|
197
|
-
color: night.color.accent,
|
|
198
|
-
fontFamily: monoFont
|
|
199
|
-
}
|
|
200
|
-
});
|
|
1
|
+
"use strict";import{useMemo as S}from"react";import{View as g,Text as p,StyleSheet as C,ScrollView as w}from"react-native";import{CompactRow as T,night as m,ToolEmptyState as f,resolveListEmptyState as b,monoFont as y}from"@buoy-gg/shared-ui";import{zustandStateStore as D}from"../utils/zustandStateStore";import{ZUSTAND_TEST_ID as c,storeRowTestID as x}from"../utils/testIDs";import{jsx as o,jsxs as h}from"react/jsx-runtime";function I(s){try{const t=s.api.getState();if(t&&typeof t==="object"&&t.__buoyStateOnDevice===true){return"on device"}if(t&&typeof t==="object"){const u=Object.keys(t);const r=u.filter(i=>{const e=t[i];return typeof e!=="function"});if(r.length===0)return"no data keys";if(r.length<=3)return r.join(", ");return`${r.slice(0,2).join(", ")} +${r.length-2}`}return""}catch{return""}}function v(){return o(f,{testID:`${c}-stores-loading`,otto:"loading",title:"Loading stores\u2026",message:"Waiting for the device to send its registered stores."})}function $(){return o(f,{testID:`${c}-stores-empty`,otto:"scanning",title:"No stores registered",message:"Use watchStores() to register your Zustand stores.\nThey will appear here with their current state."})}export function ZustandStoreBrowser({stores:s,searchQuery:t,isLoading:u=false,onOpenStore:r}){const i=S(()=>{if(!t)return s;const e=t.toLowerCase();return s.filter(a=>a.name.toLowerCase().includes(e)||a.persistName&&a.persistName.toLowerCase().includes(e))},[s,t]);if(i.length===0&&!t){return u?o(v,{}):o($,{})}if(i.length===0&&t){return o(f,{testID:`${c}-stores-no-match`,...b({subject:"stores",verb:"registered",search:t,total:s.length,waiting:{otto:"scanning",title:"No stores registered",message:"Use watchStores() to register your Zustand stores."}})})}return h(w,{style:n.container,contentContainerStyle:n.scrollContent,showsVerticalScrollIndicator:true,testID:`${c}-stores-scroll`,children:[h(g,{style:n.sectionHeader,children:[o(p,{style:n.sectionTitle,children:"STORES"}),o(g,{style:n.sectionCountBadge,children:o(p,{style:n.sectionCountText,testID:`${c}-stores-count`,children:i.length})})]}),i.map(e=>{const a=I(e);const d=D.getStoreColor(e.name);const l=x(e.name);return h(g,{style:n.storeRowWrapper,children:[o(T,{statusDotColor:d,statusLabel:e.isPersisted?"persisted":"in-memory",primaryText:e.name,secondaryText:a||void 0,showChevron:true,onPress:()=>r(e.name),testID:l,statusTestID:`${l}-backing`,primaryTestID:`${l}-name`,secondaryTestID:`${l}-keys`}),e.stateChangeCount>0&&o(g,{style:[n.absCountBadge,{backgroundColor:d+"22",borderColor:d+"55"}],pointerEvents:"none",children:o(p,{style:[n.absCountText,{color:d}],testID:`${l}-count`,children:String(e.stateChangeCount)})})]},e.name)})]})}const n=C.create({container:{flex:1},storeRowWrapper:{position:"relative"},absCountBadge:{position:"absolute",top:4,right:10,paddingHorizontal:5,paddingVertical:1,borderRadius:4,borderWidth:1,zIndex:1},absCountText:{fontSize:9,fontWeight:"700",fontFamily:y},scrollContent:{paddingTop:8,paddingBottom:20},sectionHeader:{flexDirection:"row",alignItems:"center",paddingHorizontal:16,paddingVertical:8,gap:8},sectionTitle:{fontSize:11,fontWeight:"700",letterSpacing:.5,color:m.color.textTertiary},sectionCountBadge:{backgroundColor:m.color.accentFill,paddingHorizontal:8,paddingVertical:2,borderRadius:4},sectionCountText:{fontSize:10,fontWeight:"700",color:m.color.accent,fontFamily:y}});
|
|
@@ -1,469 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* One store, on a screen of its own — the React Query detail-screen shape
|
|
5
|
-
* pointed at a live zustand store.
|
|
6
|
-
*
|
|
7
|
-
* Tapping a store card opens this instead of expanding in place. Top to
|
|
8
|
-
* bottom, mirroring RQ's `DataEditorMode`:
|
|
9
|
-
*
|
|
10
|
-
* 1. STATE EDITOR — the shared LiveExplorer. No draft, no Save: an edit
|
|
11
|
-
* writes into the live store as you make it and the app reacts
|
|
12
|
-
* immediately. Typing commits on a short debounce (~300ms, flushed on
|
|
13
|
-
* blur) so a word is one recorded change, not one per keystroke —
|
|
14
|
-
* zustand writes land in the change log and count against the free
|
|
15
|
-
* tier's cap, which is the one way this tool differs from RQ's
|
|
16
|
-
* unlogged cache writes.
|
|
17
|
-
* 2. STORE DETAILS — what the expanded card used to say: backing,
|
|
18
|
-
* change count (with the road to history), the store's actions.
|
|
19
|
-
* 3. STATE EXPLORER — the read-only tree of the same state.
|
|
20
|
-
*
|
|
21
|
-
* THE WRITE RULE (unchanged from the previous editor — see editableState.ts):
|
|
22
|
-
* merge-only, always. However deep the edit, it is written as
|
|
23
|
-
* `setState({ [topKey]: rewritten }, false)`, which cannot delete a top-level
|
|
24
|
-
* key and cannot touch the action functions stores keep in state. The
|
|
25
|
-
* explorer's delete affordance is therefore vetoed at depth 1 via
|
|
26
|
-
* `canRemove`, and the Raw escape hatch (whole-state JSON, for the add-key /
|
|
27
|
-
* restructure cases live editing can't express) refuses drops through
|
|
28
|
-
* `computeMergePartial`, naming the key.
|
|
29
|
-
*
|
|
30
|
-
* On a desktop mirror the same writes go over the wire: the mirrored store's
|
|
31
|
-
* `api.setState` forwards to the device's sync adapter (the time-travel
|
|
32
|
-
* path), fire-and-forget, with the next snapshot as confirmation.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
36
|
-
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
37
|
-
import { buoyColors, Database, ExpandedInfoRow, FileJson, night, PillBadge, parseValue, monoFont } from "@buoy-gg/shared-ui";
|
|
38
|
-
import { DataViewer, LiveExplorer, ValueEditModal, updateNestedDataByPath, deleteNestedDataByPath } from "@buoy-gg/shared-ui/dataViewer";
|
|
39
|
-
import { computeMergePartial, editableStateOf, isPlainObjectLike } from "../utils/editableState";
|
|
40
|
-
import { ZUSTAND_TEST_ID, storeRowTestID } from "../utils/testIDs";
|
|
41
|
-
|
|
42
|
-
/** How long typed edits coalesce before the single merge write fires. */
|
|
43
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
44
|
-
const TYPING_COMMIT_MS = 300;
|
|
45
|
-
|
|
46
|
-
/** `buoy-zustand-detail-…` — this screen's own handle root. */
|
|
47
|
-
const DETAIL_ID = `${ZUSTAND_TEST_ID}-detail`;
|
|
48
|
-
function detailExplorerTestID(dataPath) {
|
|
49
|
-
const slug = part => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
50
|
-
return dataPath.length === 0 ? `${DETAIL_ID}-explorer-root` : `${DETAIL_ID}-explorer-${dataPath.map(slug).filter(Boolean).join("-")}`;
|
|
51
|
-
}
|
|
52
|
-
export function ZustandStoreDetailContent({
|
|
53
|
-
store,
|
|
54
|
-
onViewHistory
|
|
55
|
-
}) {
|
|
56
|
-
const rowID = storeRowTestID(store.name);
|
|
57
|
-
|
|
58
|
-
// Re-render on every store write — our own edits included — so the tree,
|
|
59
|
-
// the read-only explorer, and the change count all track the live store.
|
|
60
|
-
// On a desktop mirror the stub subscribe is a no-op and the re-render
|
|
61
|
-
// arrives via the stores snapshot refreshing this component's props.
|
|
62
|
-
const [version, setVersion] = useState(0);
|
|
63
|
-
useEffect(() => store.api.subscribe(() => setVersion(v => v + 1)), [store.api]);
|
|
64
|
-
const state = store.api.getState();
|
|
65
|
-
// Recomputed per write on purpose — this screen edits LIVE state, so every
|
|
66
|
-
// view of it derives from the store's current answer, never a seed.
|
|
67
|
-
const editable = useMemo(() => editableStateOf(state),
|
|
68
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
-
[state, version]);
|
|
70
|
-
const [rawOpen, setRawOpen] = useState(false);
|
|
71
|
-
const writer = useMemo(() => {
|
|
72
|
-
if (editable.data === null) return undefined;
|
|
73
|
-
return {
|
|
74
|
-
update: (path, value) => {
|
|
75
|
-
const current = store.api.getState();
|
|
76
|
-
if (!isPlainObjectLike(current) || path.length === 0) return;
|
|
77
|
-
const topKey = path[0];
|
|
78
|
-
const nextTop = path.length === 1 ? value : updateNestedDataByPath(
|
|
79
|
-
// The live value, not the stripped view — although below the
|
|
80
|
-
// top level they are the same thing (a store with functions
|
|
81
|
-
// nested in data keys is refused editing entirely).
|
|
82
|
-
current[topKey], path.slice(1), value);
|
|
83
|
-
store.api.setState({
|
|
84
|
-
[topKey]: nextTop
|
|
85
|
-
}, false);
|
|
86
|
-
},
|
|
87
|
-
remove: path => {
|
|
88
|
-
// Depth 1 is vetoed by canRemove; this guard is the belt to that
|
|
89
|
-
// suspender — a merge write cannot delete a top-level key.
|
|
90
|
-
const current = store.api.getState();
|
|
91
|
-
if (!isPlainObjectLike(current) || path.length <= 1) return;
|
|
92
|
-
const topKey = path[0];
|
|
93
|
-
const nextTop = deleteNestedDataByPath(current[topKey], path.slice(1));
|
|
94
|
-
store.api.setState({
|
|
95
|
-
[topKey]: nextTop
|
|
96
|
-
}, false);
|
|
97
|
-
},
|
|
98
|
-
canRemove: path => path.length > 1
|
|
99
|
-
};
|
|
100
|
-
}, [store.api, editable.data === null]);
|
|
101
|
-
|
|
102
|
-
/** Raw-hatch save: whole-document edit diffed into one merge write. */
|
|
103
|
-
const applyRaw = useCallback(next => {
|
|
104
|
-
const current = editableStateOf(store.api.getState());
|
|
105
|
-
if (current.data === null) return;
|
|
106
|
-
const partial = computeMergePartial(current.data, next);
|
|
107
|
-
if (Object.keys(partial).length > 0) {
|
|
108
|
-
store.api.setState(partial, false);
|
|
109
|
-
}
|
|
110
|
-
setRawOpen(false);
|
|
111
|
-
}, [store.api]);
|
|
112
|
-
const displayState = useMemo(() => {
|
|
113
|
-
if (state && typeof state === "object") {
|
|
114
|
-
const filtered = {};
|
|
115
|
-
for (const [key, value] of Object.entries(state)) {
|
|
116
|
-
if (typeof value !== "function") filtered[key] = value;
|
|
117
|
-
}
|
|
118
|
-
return parseValue(filtered);
|
|
119
|
-
}
|
|
120
|
-
return parseValue(state);
|
|
121
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
-
}, [state, version]);
|
|
123
|
-
return /*#__PURE__*/_jsxs(View, {
|
|
124
|
-
style: styles.root,
|
|
125
|
-
children: [/*#__PURE__*/_jsxs(ScrollView, {
|
|
126
|
-
testID: `${DETAIL_ID}-scroll`,
|
|
127
|
-
style: styles.scroll,
|
|
128
|
-
contentContainerStyle: styles.scrollContent,
|
|
129
|
-
keyboardShouldPersistTaps: "handled",
|
|
130
|
-
children: [/*#__PURE__*/_jsx(View, {
|
|
131
|
-
style: styles.section,
|
|
132
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
133
|
-
style: styles.editorContainer,
|
|
134
|
-
children: [/*#__PURE__*/_jsxs(View, {
|
|
135
|
-
style: styles.sectionHeaderRow,
|
|
136
|
-
children: [/*#__PURE__*/_jsx(Text, {
|
|
137
|
-
testID: `${DETAIL_ID}-editor-header`,
|
|
138
|
-
style: styles.sectionHeaderText,
|
|
139
|
-
children: "State Editor"
|
|
140
|
-
}), editable.data !== null ? /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
141
|
-
onPress: () => setRawOpen(true),
|
|
142
|
-
testID: `${DETAIL_ID}-raw`,
|
|
143
|
-
accessibilityRole: "button",
|
|
144
|
-
accessibilityLabel: "Edit raw JSON",
|
|
145
|
-
style: styles.rawButton,
|
|
146
|
-
hitSlop: {
|
|
147
|
-
top: 6,
|
|
148
|
-
bottom: 6,
|
|
149
|
-
left: 6,
|
|
150
|
-
right: 6
|
|
151
|
-
},
|
|
152
|
-
children: [/*#__PURE__*/_jsx(FileJson, {
|
|
153
|
-
size: 13,
|
|
154
|
-
color: night.color.accent
|
|
155
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
156
|
-
style: styles.rawButtonText,
|
|
157
|
-
children: "Raw"
|
|
158
|
-
})]
|
|
159
|
-
}) : null]
|
|
160
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
161
|
-
style: styles.sectionBody,
|
|
162
|
-
children: editable.data !== null ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
163
|
-
children: [/*#__PURE__*/_jsx(LiveExplorer, {
|
|
164
|
-
editable: true,
|
|
165
|
-
label: "State",
|
|
166
|
-
value: editable.data,
|
|
167
|
-
defaultExpanded: ["State"],
|
|
168
|
-
writer: writer,
|
|
169
|
-
dataVersion: version,
|
|
170
|
-
testIDForPath: detailExplorerTestID,
|
|
171
|
-
debounceMs: TYPING_COMMIT_MS
|
|
172
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
173
|
-
style: styles.editorHint,
|
|
174
|
-
children: "Edits write into the live store as you make them \u2014 merged, so the store's actions and untouched keys survive."
|
|
175
|
-
})]
|
|
176
|
-
}) : /*#__PURE__*/_jsx(Text, {
|
|
177
|
-
style: styles.blockedText,
|
|
178
|
-
testID: `${DETAIL_ID}-edit-blocked`,
|
|
179
|
-
children: editable.blockedReason
|
|
180
|
-
})
|
|
181
|
-
})]
|
|
182
|
-
})
|
|
183
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
184
|
-
style: styles.section,
|
|
185
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
186
|
-
style: styles.infoContainer,
|
|
187
|
-
children: [/*#__PURE__*/_jsx(Text, {
|
|
188
|
-
style: styles.sectionHeaderTextPlain,
|
|
189
|
-
children: "Store Details"
|
|
190
|
-
}), /*#__PURE__*/_jsxs(View, {
|
|
191
|
-
style: styles.infoBody,
|
|
192
|
-
children: [/*#__PURE__*/_jsx(ExpandedInfoRow, {
|
|
193
|
-
label: "Type",
|
|
194
|
-
children: /*#__PURE__*/_jsx(PillBadge, {
|
|
195
|
-
color: store.color,
|
|
196
|
-
testID: `${rowID}-type`,
|
|
197
|
-
children: "ZUSTAND"
|
|
198
|
-
})
|
|
199
|
-
}), /*#__PURE__*/_jsxs(ExpandedInfoRow, {
|
|
200
|
-
label: "Backing",
|
|
201
|
-
children: [/*#__PURE__*/_jsx(PillBadge, {
|
|
202
|
-
color: buoyColors.info,
|
|
203
|
-
icon: store.isPersisted ? /*#__PURE__*/_jsx(Database, {
|
|
204
|
-
size: 9,
|
|
205
|
-
color: buoyColors.info
|
|
206
|
-
}) : undefined,
|
|
207
|
-
testID: `${rowID}-persist`,
|
|
208
|
-
children: store.isPersisted ? store.persistName || "persisted" : "in-memory"
|
|
209
|
-
}), store.isPersisted ? /*#__PURE__*/_jsx(RereadSavedValue, {
|
|
210
|
-
store: store,
|
|
211
|
-
rowID: rowID
|
|
212
|
-
}) : null]
|
|
213
|
-
}), store.stateChangeCount > 0 && /*#__PURE__*/_jsxs(ExpandedInfoRow, {
|
|
214
|
-
label: "Changes",
|
|
215
|
-
children: [/*#__PURE__*/_jsx(PillBadge, {
|
|
216
|
-
color: buoyColors.warning,
|
|
217
|
-
testID: `${rowID}-changes`,
|
|
218
|
-
children: String(store.stateChangeCount)
|
|
219
|
-
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
220
|
-
onPress: () => onViewHistory(store.name),
|
|
221
|
-
style: styles.inlineLinkButton,
|
|
222
|
-
hitSlop: {
|
|
223
|
-
top: 6,
|
|
224
|
-
bottom: 6,
|
|
225
|
-
left: 6,
|
|
226
|
-
right: 6
|
|
227
|
-
},
|
|
228
|
-
testID: `${rowID}-history`,
|
|
229
|
-
accessibilityRole: "button",
|
|
230
|
-
accessibilityLabel: `View ${store.name} history`,
|
|
231
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
232
|
-
style: [styles.inlineLinkText, {
|
|
233
|
-
color: store.color
|
|
234
|
-
}],
|
|
235
|
-
children: "view history \u2192"
|
|
236
|
-
})
|
|
237
|
-
})]
|
|
238
|
-
}), editable.fnPaths.length > 0 && /*#__PURE__*/_jsx(ExpandedInfoRow, {
|
|
239
|
-
label: "Actions",
|
|
240
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
241
|
-
style: styles.actionsText,
|
|
242
|
-
testID: `${DETAIL_ID}-actions`,
|
|
243
|
-
numberOfLines: 2,
|
|
244
|
-
children: editable.fnPaths.join(" · ")
|
|
245
|
-
})
|
|
246
|
-
})]
|
|
247
|
-
})]
|
|
248
|
-
})
|
|
249
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
250
|
-
style: styles.section,
|
|
251
|
-
children: /*#__PURE__*/_jsxs(View, {
|
|
252
|
-
style: styles.infoContainer,
|
|
253
|
-
children: [/*#__PURE__*/_jsx(Text, {
|
|
254
|
-
style: styles.sectionHeaderTextPlain,
|
|
255
|
-
children: "State Explorer"
|
|
256
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
257
|
-
style: styles.viewerBody,
|
|
258
|
-
testID: `${rowID}-state`,
|
|
259
|
-
children: displayState && typeof displayState === "object" ? /*#__PURE__*/_jsx(DataViewer, {
|
|
260
|
-
title: "",
|
|
261
|
-
data: displayState,
|
|
262
|
-
showTypeFilter: true,
|
|
263
|
-
rawMode: true,
|
|
264
|
-
initialExpanded: false
|
|
265
|
-
}) : /*#__PURE__*/_jsx(Text, {
|
|
266
|
-
style: styles.blockedText,
|
|
267
|
-
children: "Empty state"
|
|
268
|
-
})
|
|
269
|
-
})]
|
|
270
|
-
})
|
|
271
|
-
})]
|
|
272
|
-
}), rawOpen && editable.data !== null ? /*#__PURE__*/_jsx(ValueEditModal, {
|
|
273
|
-
path: [],
|
|
274
|
-
value: editable.data,
|
|
275
|
-
onSave: next => {
|
|
276
|
-
try {
|
|
277
|
-
applyRaw(next);
|
|
278
|
-
} catch {
|
|
279
|
-
// computeMergePartial refuses dropped top-level keys; the modal
|
|
280
|
-
// stays open with its own validation, so just don't close it.
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
onCancel: () => setRawOpen(false),
|
|
284
|
-
saveLabel: "Apply",
|
|
285
|
-
testIDPrefix: `${ZUSTAND_TEST_ID}-value`
|
|
286
|
-
}) : null]
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Pull this store's saved value back into memory.
|
|
292
|
-
*
|
|
293
|
-
* Lives on the store rather than in the Storage tool because here there is
|
|
294
|
-
* nothing to infer: `store.rehydrate` IS the store's own `persist.rehydrate`.
|
|
295
|
-
* The merge keeps the store's action functions, and it does not write back
|
|
296
|
-
* unless a version migration ran. A store registered before its persist
|
|
297
|
-
* middleware attached has no handle — then there is simply no button.
|
|
298
|
-
*/
|
|
299
|
-
function RereadSavedValue({
|
|
300
|
-
store,
|
|
301
|
-
rowID
|
|
302
|
-
}) {
|
|
303
|
-
const [busy, setBusy] = useState(false);
|
|
304
|
-
const [error, setError] = useState(null);
|
|
305
|
-
if (!store.rehydrate) return null;
|
|
306
|
-
const reread = () => {
|
|
307
|
-
setBusy(true);
|
|
308
|
-
setError(null);
|
|
309
|
-
void (async () => {
|
|
310
|
-
try {
|
|
311
|
-
await store.rehydrate?.();
|
|
312
|
-
} catch (e) {
|
|
313
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
314
|
-
} finally {
|
|
315
|
-
setBusy(false);
|
|
316
|
-
}
|
|
317
|
-
})();
|
|
318
|
-
};
|
|
319
|
-
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
320
|
-
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
321
|
-
onPress: reread,
|
|
322
|
-
disabled: busy,
|
|
323
|
-
style: styles.inlineLinkButton,
|
|
324
|
-
hitSlop: {
|
|
325
|
-
top: 6,
|
|
326
|
-
bottom: 6,
|
|
327
|
-
left: 6,
|
|
328
|
-
right: 6
|
|
329
|
-
},
|
|
330
|
-
testID: `${rowID}-reread`,
|
|
331
|
-
accessibilityRole: "button",
|
|
332
|
-
accessibilityLabel: `Re-read ${store.name} from storage`,
|
|
333
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
334
|
-
style: [styles.inlineLinkText, {
|
|
335
|
-
color: store.color
|
|
336
|
-
}],
|
|
337
|
-
children: busy ? "re-reading…" : "re-read saved value →"
|
|
338
|
-
})
|
|
339
|
-
}), error ? /*#__PURE__*/_jsx(Text, {
|
|
340
|
-
style: [styles.inlineLinkText, {
|
|
341
|
-
color: night.color.danger
|
|
342
|
-
}],
|
|
343
|
-
children: error
|
|
344
|
-
}) : null]
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
const styles = StyleSheet.create({
|
|
348
|
-
root: {
|
|
349
|
-
flex: 1,
|
|
350
|
-
minHeight: 0
|
|
351
|
-
},
|
|
352
|
-
scroll: {
|
|
353
|
-
flex: 1
|
|
354
|
-
},
|
|
355
|
-
scrollContent: {
|
|
356
|
-
paddingHorizontal: 8,
|
|
357
|
-
paddingTop: 8,
|
|
358
|
-
paddingBottom: 24
|
|
359
|
-
},
|
|
360
|
-
section: {
|
|
361
|
-
marginBottom: 16
|
|
362
|
-
},
|
|
363
|
-
// The editable section wears the accent, like RQ's Data Editor — the one
|
|
364
|
-
// box on this screen that changes things should read differently from the
|
|
365
|
-
// two that don't.
|
|
366
|
-
editorContainer: {
|
|
367
|
-
minWidth: 200,
|
|
368
|
-
backgroundColor: night.color.surface,
|
|
369
|
-
borderRadius: 6,
|
|
370
|
-
borderWidth: 1,
|
|
371
|
-
borderColor: night.color.accent + "4D",
|
|
372
|
-
overflow: "hidden",
|
|
373
|
-
shadowColor: night.color.accent,
|
|
374
|
-
shadowOffset: {
|
|
375
|
-
width: 0,
|
|
376
|
-
height: 0
|
|
377
|
-
},
|
|
378
|
-
shadowOpacity: 0.1,
|
|
379
|
-
shadowRadius: 6
|
|
380
|
-
},
|
|
381
|
-
sectionHeaderRow: {
|
|
382
|
-
flexDirection: "row",
|
|
383
|
-
alignItems: "center",
|
|
384
|
-
justifyContent: "space-between",
|
|
385
|
-
backgroundColor: night.color.accent + "1A",
|
|
386
|
-
paddingHorizontal: 12,
|
|
387
|
-
paddingVertical: 8,
|
|
388
|
-
borderBottomWidth: 1,
|
|
389
|
-
borderBottomColor: night.color.accent + "33"
|
|
390
|
-
},
|
|
391
|
-
sectionHeaderText: {
|
|
392
|
-
fontWeight: "600",
|
|
393
|
-
fontSize: 12,
|
|
394
|
-
color: night.color.accent,
|
|
395
|
-
letterSpacing: 0.5,
|
|
396
|
-
textTransform: "uppercase",
|
|
397
|
-
fontFamily: monoFont
|
|
398
|
-
},
|
|
399
|
-
rawButton: {
|
|
400
|
-
flexDirection: "row",
|
|
401
|
-
alignItems: "center",
|
|
402
|
-
gap: 4
|
|
403
|
-
},
|
|
404
|
-
rawButtonText: {
|
|
405
|
-
color: night.color.accent,
|
|
406
|
-
fontSize: 11,
|
|
407
|
-
fontWeight: "700"
|
|
408
|
-
},
|
|
409
|
-
sectionBody: {
|
|
410
|
-
padding: 8
|
|
411
|
-
},
|
|
412
|
-
editorHint: {
|
|
413
|
-
color: night.color.textTertiary,
|
|
414
|
-
fontSize: 10,
|
|
415
|
-
paddingTop: 8,
|
|
416
|
-
paddingHorizontal: 4,
|
|
417
|
-
lineHeight: 14
|
|
418
|
-
},
|
|
419
|
-
blockedText: {
|
|
420
|
-
color: night.color.textSecondary,
|
|
421
|
-
fontSize: 12,
|
|
422
|
-
padding: 12,
|
|
423
|
-
lineHeight: 18
|
|
424
|
-
},
|
|
425
|
-
infoContainer: {
|
|
426
|
-
minWidth: 200,
|
|
427
|
-
backgroundColor: night.color.surface,
|
|
428
|
-
borderRadius: 6,
|
|
429
|
-
borderWidth: 1,
|
|
430
|
-
borderColor: night.color.border,
|
|
431
|
-
overflow: "hidden"
|
|
432
|
-
},
|
|
433
|
-
sectionHeaderTextPlain: {
|
|
434
|
-
backgroundColor: night.color.surfaceElevated,
|
|
435
|
-
paddingHorizontal: 12,
|
|
436
|
-
paddingVertical: 8,
|
|
437
|
-
fontWeight: "600",
|
|
438
|
-
fontSize: 12,
|
|
439
|
-
color: night.color.textSecondary,
|
|
440
|
-
borderBottomWidth: 1,
|
|
441
|
-
borderBottomColor: night.color.border,
|
|
442
|
-
letterSpacing: 0.5,
|
|
443
|
-
textTransform: "uppercase",
|
|
444
|
-
fontFamily: monoFont
|
|
445
|
-
},
|
|
446
|
-
infoBody: {
|
|
447
|
-
padding: 12,
|
|
448
|
-
gap: 10
|
|
449
|
-
},
|
|
450
|
-
viewerBody: {
|
|
451
|
-
backgroundColor: night.color.bg,
|
|
452
|
-
minHeight: 60
|
|
453
|
-
},
|
|
454
|
-
inlineLinkButton: {
|
|
455
|
-
marginLeft: 4
|
|
456
|
-
},
|
|
457
|
-
inlineLinkText: {
|
|
458
|
-
fontSize: 10,
|
|
459
|
-
fontWeight: "600",
|
|
460
|
-
fontFamily: monoFont
|
|
461
|
-
},
|
|
462
|
-
actionsText: {
|
|
463
|
-
flex: 1,
|
|
464
|
-
color: night.color.textSecondary,
|
|
465
|
-
fontSize: 10,
|
|
466
|
-
fontFamily: monoFont,
|
|
467
|
-
lineHeight: 15
|
|
468
|
-
}
|
|
469
|
-
});
|
|
1
|
+
"use strict";import{useCallback as j,useEffect as O,useMemo as C,useState as S}from"react";import{ScrollView as V,StyleSheet as F,Text as s,TouchableOpacity as k,View as d}from"react-native";import{buoyColors as B,Database as W,ExpandedInfoRow as x,FileJson as M,night as i,PillBadge as I,parseValue as v,monoFont as w}from"@buoy-gg/shared-ui";import{DataViewer as _,LiveExplorer as N,ValueEditModal as A,updateNestedDataByPath as Z,deleteNestedDataByPath as J}from"@buoy-gg/shared-ui/dataViewer";import{computeMergePartial as K,editableStateOf as P,isPlainObjectLike as $}from"../utils/editableState";import{ZUSTAND_TEST_ID as H,storeRowTestID as U}from"../utils/testIDs";import{jsx as t,jsxs as l,Fragment as E}from"react/jsx-runtime";const G=300;const f=`${H}-detail`;function Y(e){const y=a=>a.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return e.length===0?`${f}-explorer-root`:`${f}-explorer-${e.map(y).filter(Boolean).join("-")}`}export function ZustandStoreDetailContent({store:e,onViewHistory:y}){const a=U(e.name);const[p,b]=S(0);O(()=>e.api.subscribe(()=>b(n=>n+1)),[e.api]);const u=e.api.getState();const r=C(()=>P(u),[u,p]);const[g,T]=S(false);const L=C(()=>{if(r.data===null)return void 0;return{update:(n,h)=>{const c=e.api.getState();if(!$(c)||n.length===0)return;const m=n[0];const z=n.length===1?h:Z(c[m],n.slice(1),h);e.api.setState({[m]:z},false)},remove:n=>{const h=e.api.getState();if(!$(h)||n.length<=1)return;const c=n[0];const m=J(h[c],n.slice(1));e.api.setState({[c]:m},false)},canRemove:n=>n.length>1}},[e.api,r.data===null]);const R=j(n=>{const h=P(e.api.getState());if(h.data===null)return;const c=K(h.data,n);if(Object.keys(c).length>0){e.api.setState(c,false)}T(false)},[e.api]);const D=C(()=>{if(u&&typeof u==="object"){const n={};for(const[h,c]of Object.entries(u)){if(typeof c!=="function")n[h]=c}return v(n)}return v(u)},[u,p]);return l(d,{style:o.root,children:[l(V,{testID:`${f}-scroll`,style:o.scroll,contentContainerStyle:o.scrollContent,keyboardShouldPersistTaps:"handled",children:[t(d,{style:o.section,children:l(d,{style:o.editorContainer,children:[l(d,{style:o.sectionHeaderRow,children:[t(s,{testID:`${f}-editor-header`,style:o.sectionHeaderText,children:"State Editor"}),r.data!==null?l(k,{onPress:()=>T(true),testID:`${f}-raw`,accessibilityRole:"button",accessibilityLabel:"Edit raw JSON",style:o.rawButton,hitSlop:{top:6,bottom:6,left:6,right:6},children:[t(M,{size:13,color:i.color.accent}),t(s,{style:o.rawButtonText,children:"Raw"})]}):null]}),t(d,{style:o.sectionBody,children:r.data!==null?l(E,{children:[t(N,{editable:true,label:"State",value:r.data,defaultExpanded:["State"],writer:L,dataVersion:p,testIDForPath:Y,debounceMs:G}),t(s,{style:o.editorHint,children:"Edits write into the live store as you make them \u2014 merged, so the store's actions and untouched keys survive."})]}):t(s,{style:o.blockedText,testID:`${f}-edit-blocked`,children:r.blockedReason})})]})}),t(d,{style:o.section,children:l(d,{style:o.infoContainer,children:[t(s,{style:o.sectionHeaderTextPlain,children:"Store Details"}),l(d,{style:o.infoBody,children:[t(x,{label:"Type",children:t(I,{color:e.color,testID:`${a}-type`,children:"ZUSTAND"})}),l(x,{label:"Backing",children:[t(I,{color:B.info,icon:e.isPersisted?t(W,{size:9,color:B.info}):void 0,testID:`${a}-persist`,children:e.isPersisted?e.persistName||"persisted":"in-memory"}),e.isPersisted?t(q,{store:e,rowID:a}):null]}),e.stateChangeCount>0&&l(x,{label:"Changes",children:[t(I,{color:B.warning,testID:`${a}-changes`,children:String(e.stateChangeCount)}),t(k,{onPress:()=>y(e.name),style:o.inlineLinkButton,hitSlop:{top:6,bottom:6,left:6,right:6},testID:`${a}-history`,accessibilityRole:"button",accessibilityLabel:`View ${e.name} history`,children:t(s,{style:[o.inlineLinkText,{color:e.color}],children:"view history \u2192"})})]}),r.fnPaths.length>0&&t(x,{label:"Actions",children:t(s,{style:o.actionsText,testID:`${f}-actions`,numberOfLines:2,children:r.fnPaths.join(" \xB7 ")})})]})]})}),t(d,{style:o.section,children:l(d,{style:o.infoContainer,children:[t(s,{style:o.sectionHeaderTextPlain,children:"State Explorer"}),t(d,{style:o.viewerBody,testID:`${a}-state`,children:D&&typeof D==="object"?t(_,{title:"",data:D,showTypeFilter:true,rawMode:true,initialExpanded:false}):t(s,{style:o.blockedText,children:"Empty state"})})]})})]}),g&&r.data!==null?t(A,{path:[],value:r.data,onSave:n=>{try{R(n)}catch{}},onCancel:()=>T(false),saveLabel:"Apply",testIDPrefix:`${H}-value`}):null]})}function q({store:e,rowID:y}){const[a,p]=S(false);const[b,u]=S(null);if(!e.rehydrate)return null;const r=()=>{p(true);u(null);void(async()=>{try{await e.rehydrate?.()}catch(g){u(g instanceof Error?g.message:String(g))}finally{p(false)}})()};return l(E,{children:[t(k,{onPress:r,disabled:a,style:o.inlineLinkButton,hitSlop:{top:6,bottom:6,left:6,right:6},testID:`${y}-reread`,accessibilityRole:"button",accessibilityLabel:`Re-read ${e.name} from storage`,children:t(s,{style:[o.inlineLinkText,{color:e.color}],children:a?"re-reading\u2026":"re-read saved value \u2192"})}),b?t(s,{style:[o.inlineLinkText,{color:i.color.danger}],children:b}):null]})}const o=F.create({root:{flex:1,minHeight:0},scroll:{flex:1},scrollContent:{paddingHorizontal:8,paddingTop:8,paddingBottom:24},section:{marginBottom:16},editorContainer:{minWidth:200,backgroundColor:i.color.surface,borderRadius:6,borderWidth:1,borderColor:i.color.accent+"4D",overflow:"hidden",shadowColor:i.color.accent,shadowOffset:{width:0,height:0},shadowOpacity:.1,shadowRadius:6},sectionHeaderRow:{flexDirection:"row",alignItems:"center",justifyContent:"space-between",backgroundColor:i.color.accent+"1A",paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:i.color.accent+"33"},sectionHeaderText:{fontWeight:"600",fontSize:12,color:i.color.accent,letterSpacing:.5,textTransform:"uppercase",fontFamily:w},rawButton:{flexDirection:"row",alignItems:"center",gap:4},rawButtonText:{color:i.color.accent,fontSize:11,fontWeight:"700"},sectionBody:{padding:8},editorHint:{color:i.color.textTertiary,fontSize:10,paddingTop:8,paddingHorizontal:4,lineHeight:14},blockedText:{color:i.color.textSecondary,fontSize:12,padding:12,lineHeight:18},infoContainer:{minWidth:200,backgroundColor:i.color.surface,borderRadius:6,borderWidth:1,borderColor:i.color.border,overflow:"hidden"},sectionHeaderTextPlain:{backgroundColor:i.color.surfaceElevated,paddingHorizontal:12,paddingVertical:8,fontWeight:"600",fontSize:12,color:i.color.textSecondary,borderBottomWidth:1,borderBottomColor:i.color.border,letterSpacing:.5,textTransform:"uppercase",fontFamily:w},infoBody:{padding:12,gap:10},viewerBody:{backgroundColor:i.color.bg,minHeight:60},inlineLinkButton:{marginLeft:4},inlineLinkText:{fontSize:10,fontWeight:"600",fontFamily:w},actionsText:{flex:1,color:i.color.textSecondary,fontSize:10,fontFamily:w,lineHeight:15}});
|