@buoy-gg/zustand 7.0.41 → 7.0.42

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 (63) hide show
  1. package/README.md +11 -76
  2. package/lib/commonjs/index.js +1 -105
  3. package/lib/commonjs/preset.js +1 -102
  4. package/lib/commonjs/zustand/components/ZustandActionButton.js +1 -118
  5. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +1 -48
  6. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +1 -43
  7. package/lib/commonjs/zustand/components/ZustandIcon.js +1 -18
  8. package/lib/commonjs/zustand/components/ZustandModal.js +1 -772
  9. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +1 -117
  10. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +1 -372
  11. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +1 -488
  12. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +1 -204
  13. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +1 -471
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +1 -74
  15. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +1 -418
  16. package/lib/commonjs/zustand/types/index.js +1 -1
  17. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +1 -284
  18. package/lib/commonjs/zustand/utils/editableState.js +1 -146
  19. package/lib/commonjs/zustand/utils/snapshotProvider.js +1 -209
  20. package/lib/commonjs/zustand/utils/testIDs.js +1 -82
  21. package/lib/commonjs/zustand/utils/zustandStateStore.js +1 -379
  22. package/lib/module/index.js +1 -90
  23. package/lib/module/preset.js +1 -98
  24. package/lib/module/zustand/components/ZustandActionButton.js +1 -114
  25. package/lib/module/zustand/components/ZustandDetailViewToggle.js +1 -43
  26. package/lib/module/zustand/components/ZustandEventFilterView.js +1 -39
  27. package/lib/module/zustand/components/ZustandIcon.js +1 -11
  28. package/lib/module/zustand/components/ZustandModal.js +1 -767
  29. package/lib/module/zustand/components/ZustandStateChangeItem.js +1 -113
  30. package/lib/module/zustand/components/ZustandStateDetailContent.js +1 -368
  31. package/lib/module/zustand/components/ZustandStateInfoView.js +1 -483
  32. package/lib/module/zustand/components/ZustandStoreBrowser.js +1 -200
  33. package/lib/module/zustand/components/ZustandStoreDetailContent.js +1 -469
  34. package/lib/module/zustand/hooks/useZustandStateChanges.js +1 -70
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +1 -415
  36. package/lib/module/zustand/types/index.js +1 -1
  37. package/lib/module/zustand/utils/buoyZustandMiddleware.js +1 -278
  38. package/lib/module/zustand/utils/editableState.js +1 -138
  39. package/lib/module/zustand/utils/snapshotProvider.js +1 -209
  40. package/lib/module/zustand/utils/testIDs.js +1 -74
  41. package/lib/module/zustand/utils/zustandStateStore.js +1 -375
  42. package/lib/web/index.mjs +2 -3
  43. package/package.json +3 -3
  44. package/lib/typescript/index.d.ts.map +0 -1
  45. package/lib/typescript/preset.d.ts.map +0 -1
  46. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +0 -1
  47. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +0 -1
  48. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +0 -1
  49. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +0 -1
  50. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +0 -1
  51. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +0 -1
  52. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +0 -1
  53. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +0 -1
  54. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +0 -1
  55. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +0 -1
  56. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +0 -1
  57. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +0 -1
  58. package/lib/typescript/zustand/types/index.d.ts.map +0 -1
  59. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +0 -1
  60. package/lib/typescript/zustand/utils/editableState.d.ts.map +0 -1
  61. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +0 -1
  62. package/lib/typescript/zustand/utils/testIDs.d.ts.map +0 -1
  63. 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}});