@buoy-gg/zustand 7.0.29 → 7.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/zustand/components/ZustandActionButton.js +3 -1
- package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +7 -0
- package/lib/commonjs/zustand/components/ZustandEventFilterView.js +19 -4
- package/lib/commonjs/zustand/components/ZustandModal.js +173 -32
- package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +36 -37
- package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +22 -5
- package/lib/commonjs/zustand/components/ZustandStateInfoView.js +30 -7
- package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +23 -126
- package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -0
- package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -0
- package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +54 -0
- package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +71 -7
- package/lib/commonjs/zustand/utils/editableState.js +139 -0
- package/lib/commonjs/zustand/utils/snapshotProvider.js +10 -37
- package/lib/commonjs/zustand/utils/testIDs.js +82 -0
- package/lib/commonjs/zustand/utils/zustandStateStore.js +16 -3
- package/lib/module/zustand/components/ZustandActionButton.js +3 -1
- package/lib/module/zustand/components/ZustandDetailViewToggle.js +7 -0
- package/lib/module/zustand/components/ZustandEventFilterView.js +19 -4
- package/lib/module/zustand/components/ZustandModal.js +173 -32
- package/lib/module/zustand/components/ZustandStateChangeItem.js +36 -37
- package/lib/module/zustand/components/ZustandStateDetailContent.js +22 -5
- package/lib/module/zustand/components/ZustandStateInfoView.js +30 -7
- package/lib/module/zustand/components/ZustandStoreBrowser.js +26 -129
- package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -0
- package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -0
- package/lib/module/zustand/sync/zustandSyncAdapter.js +54 -0
- package/lib/module/zustand/utils/buoyZustandMiddleware.js +71 -7
- package/lib/module/zustand/utils/editableState.js +131 -0
- package/lib/module/zustand/utils/snapshotProvider.js +4 -30
- package/lib/module/zustand/utils/testIDs.js +74 -0
- package/lib/module/zustand/utils/zustandStateStore.js +16 -3
- package/lib/typescript/zustand/components/ZustandActionButton.d.ts +2 -1
- package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts +6 -0
- package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts +7 -9
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -1
- package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts +40 -0
- package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts +42 -0
- package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +35 -0
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/zustand/types/index.d.ts +13 -0
- package/lib/typescript/zustand/types/index.d.ts.map +1 -1
- package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts +31 -3
- package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -1
- package/lib/typescript/zustand/utils/editableState.d.ts +65 -0
- package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/testIDs.d.ts +54 -0
- package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts +1 -0
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -16,21 +16,36 @@ import { useZustandStateChanges } from "../hooks/useZustandStateChanges";
|
|
|
16
16
|
import { ZustandStateChangeItem } from "./ZustandStateChangeItem";
|
|
17
17
|
import { ZustandStateDetailContent, ZustandStateDetailFooter } from "./ZustandStateDetailContent";
|
|
18
18
|
import { ZustandStoreBrowser } from "./ZustandStoreBrowser";
|
|
19
|
+
import { ZustandStoreDetailContent } from "./ZustandStoreDetailContent";
|
|
19
20
|
import { ZustandEventFilterView } from "./ZustandEventFilterView";
|
|
21
|
+
import { ZUSTAND_TEST_ID, buildChangeRowTestIDs } from "../utils/testIDs";
|
|
20
22
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
23
|
+
/**
|
|
24
|
+
* Cell clipping, off in e2e bundles only.
|
|
25
|
+
*
|
|
26
|
+
* `removeClippedSubviews` unmounts the views of rows scrolled out of the
|
|
27
|
+
* window, which is what makes a long change list cheap — and which makes
|
|
28
|
+
* Detox report a perfectly rendered row as un-hittable. The env var is
|
|
29
|
+
* inlined at bundle time, so a bundler that does NOT inline it leaves the
|
|
30
|
+
* production behaviour exactly as it was.
|
|
31
|
+
*/
|
|
32
|
+
const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
|
|
21
33
|
function EventsEmptyState({
|
|
22
34
|
isEnabled
|
|
23
35
|
}) {
|
|
24
36
|
return /*#__PURE__*/_jsxs(View, {
|
|
25
37
|
style: styles.emptyState,
|
|
38
|
+
testID: `${ZUSTAND_TEST_ID}-empty`,
|
|
26
39
|
children: [/*#__PURE__*/_jsx(Box, {
|
|
27
40
|
size: 32,
|
|
28
41
|
color: night.color.textTertiary
|
|
29
42
|
}), /*#__PURE__*/_jsx(Text, {
|
|
30
43
|
style: styles.emptyTitle,
|
|
44
|
+
testID: `${ZUSTAND_TEST_ID}-empty-title`,
|
|
31
45
|
children: "No state changes"
|
|
32
46
|
}), /*#__PURE__*/_jsx(Text, {
|
|
33
47
|
style: styles.emptyText,
|
|
48
|
+
testID: `${ZUSTAND_TEST_ID}-empty-text`,
|
|
34
49
|
children: isEnabled ? "State changes will appear here as stores update." : "Enable capture to start recording state changes"
|
|
35
50
|
})]
|
|
36
51
|
});
|
|
@@ -54,6 +69,11 @@ export function ZustandModal({
|
|
|
54
69
|
const [showProModal, setShowProModal] = useState(false);
|
|
55
70
|
const [activeTab, setActiveTab] = useState(initialTab);
|
|
56
71
|
const [selectedHistoryStore, setSelectedHistoryStore] = useState(null);
|
|
72
|
+
/**
|
|
73
|
+
* Which store's DETAIL screen is open (live editor + details + tree —
|
|
74
|
+
* the React Query interaction). Null means the browser list.
|
|
75
|
+
*/
|
|
76
|
+
const [selectedStoreName, setSelectedStoreName] = useState(null);
|
|
57
77
|
const [showFilters, setShowFilters] = useState(false);
|
|
58
78
|
const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
|
|
59
79
|
const {
|
|
@@ -79,19 +99,26 @@ export function ZustandModal({
|
|
|
79
99
|
return stores.filter(s => !Array.from(ignoredPatterns).some(p => s.name.toLowerCase().includes(p.toLowerCase())));
|
|
80
100
|
}, [stores, ignoredPatterns]);
|
|
81
101
|
|
|
102
|
+
// Scope to a store BEFORE the tier cap, not after.
|
|
103
|
+
//
|
|
104
|
+
// The other way round, the cap was applied to the whole log and a store's
|
|
105
|
+
// history was whatever survived it — so a store with one change, made a
|
|
106
|
+
// while ago, opened an EMPTY history from a card that said "Changes: 1" and
|
|
107
|
+
// offered a "view history" link. The cap is a limit on how much of a list
|
|
108
|
+
// you can see; which list you are looking at is decided first.
|
|
109
|
+
const scopedChanges = useMemo(() => {
|
|
110
|
+
if (!selectedHistoryStore) return displayedChanges;
|
|
111
|
+
return displayedChanges.filter(c => c.storeName === selectedHistoryStore);
|
|
112
|
+
}, [displayedChanges, selectedHistoryStore]);
|
|
113
|
+
|
|
82
114
|
// Pro is Infinity here, so slice() returns everything and the locked count
|
|
83
115
|
// floors at 0 — no tier branching needed at the call site.
|
|
84
|
-
const visibleChanges = useMemo(() =>
|
|
85
|
-
const lockedChangeCount = useMemo(() => Math.max(0,
|
|
116
|
+
const visibleChanges = useMemo(() => scopedChanges.slice(0, changeLimit), [scopedChanges, changeLimit]);
|
|
117
|
+
const lockedChangeCount = useMemo(() => Math.max(0, scopedChanges.length - changeLimit), [scopedChanges.length, changeLimit]);
|
|
86
118
|
const hasLockedChanges = lockedChangeCount > 0;
|
|
87
119
|
|
|
88
|
-
|
|
89
|
-
const activeChanges =
|
|
90
|
-
if (selectedHistoryStore) {
|
|
91
|
-
return visibleChanges.filter(c => c.storeName === selectedHistoryStore);
|
|
92
|
-
}
|
|
93
|
-
return visibleChanges;
|
|
94
|
-
}, [visibleChanges, selectedHistoryStore]);
|
|
120
|
+
/** The list the detail stepper walks: already scoped, already capped. */
|
|
121
|
+
const activeChanges = visibleChanges;
|
|
95
122
|
|
|
96
123
|
// Selection state — track by ID so new events don't shift position
|
|
97
124
|
const [selectedChangeId, setSelectedChangeId] = useState(null);
|
|
@@ -141,6 +168,16 @@ export function ZustandModal({
|
|
|
141
168
|
}));
|
|
142
169
|
}
|
|
143
170
|
};
|
|
171
|
+
|
|
172
|
+
/** Drop the query AND put the header back. The X, and only the X. */
|
|
173
|
+
const closeSearch = useCallback(() => {
|
|
174
|
+
setSearchText("");
|
|
175
|
+
setFilter(prev => ({
|
|
176
|
+
...prev,
|
|
177
|
+
searchText: ""
|
|
178
|
+
}));
|
|
179
|
+
setIsSearchActive(false);
|
|
180
|
+
}, [setFilter]);
|
|
144
181
|
const handleChangePress = useCallback(change => {
|
|
145
182
|
setSelectedChangeId(change.id);
|
|
146
183
|
}, []);
|
|
@@ -164,6 +201,16 @@ export function ZustandModal({
|
|
|
164
201
|
setSelectedHistoryStore(null);
|
|
165
202
|
setSelectedChangeId(null);
|
|
166
203
|
}, []);
|
|
204
|
+
const handleOpenStore = useCallback(storeName => {
|
|
205
|
+
setSelectedStoreName(storeName);
|
|
206
|
+
// The desktop mirror hydrates a store's full state on demand — this is
|
|
207
|
+
// the moment it becomes needed (was fired on card-expand before the
|
|
208
|
+
// detail screen replaced expansion).
|
|
209
|
+
onStoreExpand?.(storeName);
|
|
210
|
+
}, [onStoreExpand]);
|
|
211
|
+
const handleBackFromStore = useCallback(() => {
|
|
212
|
+
setSelectedStoreName(null);
|
|
213
|
+
}, []);
|
|
167
214
|
const getStoresSnapshot = useCallback(() => {
|
|
168
215
|
return displayedStores.reduce((acc, store) => {
|
|
169
216
|
try {
|
|
@@ -218,6 +265,7 @@ export function ZustandModal({
|
|
|
218
265
|
const handleTabChange = useCallback(tab => {
|
|
219
266
|
setActiveTab(tab);
|
|
220
267
|
setSelectedHistoryStore(null);
|
|
268
|
+
setSelectedStoreName(null);
|
|
221
269
|
setSelectedChangeId(null);
|
|
222
270
|
setShowFilters(false);
|
|
223
271
|
if (searchText) {
|
|
@@ -230,14 +278,20 @@ export function ZustandModal({
|
|
|
230
278
|
setIsSearchActive(false);
|
|
231
279
|
}, [searchText, setFilter]);
|
|
232
280
|
const keyExtractor = useCallback(item => item.id, []);
|
|
281
|
+
|
|
282
|
+
// Built for the whole list rather than per row: the ordinal a row carries is
|
|
283
|
+
// a fact about the list around it. See ../utils/testIDs.ts for why a store
|
|
284
|
+
// name alone cannot address a change row.
|
|
285
|
+
const rowTestIDs = useMemo(() => buildChangeRowTestIDs(activeChanges), [activeChanges]);
|
|
233
286
|
const renderItem = useCallback(({
|
|
234
287
|
item
|
|
235
288
|
}) => {
|
|
236
289
|
return /*#__PURE__*/_jsx(ZustandStateChangeItem, {
|
|
237
290
|
change: item,
|
|
238
|
-
onPress: handleChangePress
|
|
291
|
+
onPress: handleChangePress,
|
|
292
|
+
testID: rowTestIDs.get(item.id)
|
|
239
293
|
});
|
|
240
|
-
}, [handleChangePress]);
|
|
294
|
+
}, [handleChangePress, rowTestIDs]);
|
|
241
295
|
|
|
242
296
|
// ---- Header rendering ----
|
|
243
297
|
|
|
@@ -250,7 +304,8 @@ export function ZustandModal({
|
|
|
250
304
|
onBack: () => setShowFilters(false)
|
|
251
305
|
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
252
306
|
title: "Filters",
|
|
253
|
-
centered: true
|
|
307
|
+
centered: true,
|
|
308
|
+
testID: `${ZUSTAND_TEST_ID}-filters-title`
|
|
254
309
|
})]
|
|
255
310
|
});
|
|
256
311
|
}
|
|
@@ -262,7 +317,8 @@ export function ZustandModal({
|
|
|
262
317
|
onBack: handleBack
|
|
263
318
|
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
264
319
|
title: `${selectedChange.storeName}/setState`,
|
|
265
|
-
centered: true
|
|
320
|
+
centered: true,
|
|
321
|
+
testID: `${ZUSTAND_TEST_ID}-detail-title`
|
|
266
322
|
})]
|
|
267
323
|
});
|
|
268
324
|
}
|
|
@@ -274,17 +330,34 @@ export function ZustandModal({
|
|
|
274
330
|
onBack: handleBackFromHistory
|
|
275
331
|
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
276
332
|
title: `${selectedHistoryStore} History`,
|
|
277
|
-
centered: true
|
|
333
|
+
centered: true,
|
|
334
|
+
testID: `${ZUSTAND_TEST_ID}-history-title`
|
|
278
335
|
}), /*#__PURE__*/_jsx(ModalHeader.Actions, {
|
|
279
336
|
children: /*#__PURE__*/_jsx(ToolbarCopyButton, {
|
|
280
337
|
value: getHistorySnapshot,
|
|
281
338
|
disabled: activeChanges.length === 0,
|
|
282
|
-
buttonStyle: styles.headerActionButton
|
|
339
|
+
buttonStyle: styles.headerActionButton,
|
|
340
|
+
testID: `${ZUSTAND_TEST_ID}-copy`
|
|
283
341
|
})
|
|
284
342
|
})]
|
|
285
343
|
});
|
|
286
344
|
}
|
|
287
345
|
|
|
346
|
+
// Store detail header — the live editor screen. AFTER history: history is
|
|
347
|
+
// reached FROM the detail screen and stacks over it, so when both are set
|
|
348
|
+
// history owns the header, and its back lands you back on the detail.
|
|
349
|
+
if (selectedStoreName) {
|
|
350
|
+
return /*#__PURE__*/_jsxs(ModalHeader, {
|
|
351
|
+
children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
352
|
+
onBack: handleBackFromStore
|
|
353
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
354
|
+
title: selectedStoreName,
|
|
355
|
+
centered: true,
|
|
356
|
+
testID: `${ZUSTAND_TEST_ID}-store-detail-title`
|
|
357
|
+
})]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
288
361
|
// Main view with tabs
|
|
289
362
|
return /*#__PURE__*/_jsxs(ModalHeader, {
|
|
290
363
|
children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
@@ -293,27 +366,42 @@ export function ZustandModal({
|
|
|
293
366
|
title: "",
|
|
294
367
|
children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
|
|
295
368
|
style: styles.headerSearchContainer,
|
|
369
|
+
testID: `${ZUSTAND_TEST_ID}-search`,
|
|
296
370
|
children: [/*#__PURE__*/_jsx(Search, {
|
|
297
371
|
size: 14,
|
|
298
372
|
color: night.color.textSecondary
|
|
299
373
|
}), /*#__PURE__*/_jsx(TextInput, {
|
|
300
374
|
ref: searchInputRef,
|
|
375
|
+
testID: `${ZUSTAND_TEST_ID}-search-field`,
|
|
301
376
|
style: styles.headerSearchInput,
|
|
302
377
|
placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
|
|
303
378
|
placeholderTextColor: night.color.placeholder,
|
|
304
379
|
value: searchText,
|
|
305
380
|
onChangeText: handleSearch,
|
|
306
|
-
onSubmitEditing: () =>
|
|
307
|
-
|
|
381
|
+
onSubmitEditing: () => searchInputRef.current?.blur()
|
|
382
|
+
// Stay open while the query is still narrowing the list.
|
|
383
|
+
//
|
|
384
|
+
// Collapsing on blur hid the reason rows were missing: the
|
|
385
|
+
// header went back to its TABS — whose counts are of the
|
|
386
|
+
// WHOLE capture on the stores side — while the list stayed
|
|
387
|
+
// filtered, so the tool showed "Stores (4)" above a single
|
|
388
|
+
// row and offered nothing to explain the gap. The only way
|
|
389
|
+
// back was to reopen a field the user had no reason to think
|
|
390
|
+
// was still armed.
|
|
391
|
+
,
|
|
392
|
+
onBlur: () => {
|
|
393
|
+
if (searchText.length === 0) setIsSearchActive(false);
|
|
394
|
+
},
|
|
395
|
+
accessibilityLabel: "Search Zustand stores and events",
|
|
308
396
|
autoCapitalize: "none",
|
|
309
397
|
autoCorrect: false,
|
|
310
398
|
returnKeyType: "search"
|
|
311
|
-
}),
|
|
312
|
-
onPress:
|
|
313
|
-
handleSearch("");
|
|
314
|
-
setIsSearchActive(false);
|
|
315
|
-
},
|
|
399
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
400
|
+
onPress: closeSearch,
|
|
316
401
|
style: styles.headerSearchClear,
|
|
402
|
+
testID: `${ZUSTAND_TEST_ID}-search-clear`,
|
|
403
|
+
accessibilityRole: "button",
|
|
404
|
+
accessibilityLabel: "Close search",
|
|
317
405
|
children: /*#__PURE__*/_jsx(X, {
|
|
318
406
|
size: 14,
|
|
319
407
|
color: night.color.textSecondary
|
|
@@ -328,12 +416,16 @@ export function ZustandModal({
|
|
|
328
416
|
label: `Events${displayedChanges.length > 0 ? ` (${displayedChanges.length})` : ""}`
|
|
329
417
|
}],
|
|
330
418
|
activeTab: activeTab,
|
|
331
|
-
onTabChange: handleTabChange
|
|
419
|
+
onTabChange: handleTabChange,
|
|
420
|
+
testIDPrefix: `${ZUSTAND_TEST_ID}-tab`
|
|
332
421
|
})
|
|
333
422
|
}), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
|
|
334
423
|
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
335
424
|
onPress: () => setIsSearchActive(true),
|
|
336
425
|
style: styles.headerActionButton,
|
|
426
|
+
testID: `${ZUSTAND_TEST_ID}-search-open`,
|
|
427
|
+
accessibilityRole: "button",
|
|
428
|
+
accessibilityLabel: "Search",
|
|
337
429
|
children: /*#__PURE__*/_jsx(Search, {
|
|
338
430
|
size: 14,
|
|
339
431
|
color: night.color.textSecondary
|
|
@@ -341,6 +433,9 @@ export function ZustandModal({
|
|
|
341
433
|
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
342
434
|
onPress: () => setShowFilters(true),
|
|
343
435
|
style: [styles.headerActionButton, hasActiveFilters && styles.headerActionButtonActive],
|
|
436
|
+
testID: `${ZUSTAND_TEST_ID}-filters-open`,
|
|
437
|
+
accessibilityRole: "button",
|
|
438
|
+
accessibilityLabel: "Filters",
|
|
344
439
|
children: /*#__PURE__*/_jsx(Filter, {
|
|
345
440
|
size: 14,
|
|
346
441
|
color: hasActiveFilters ? night.color.accent : night.color.textSecondary
|
|
@@ -348,20 +443,24 @@ export function ZustandModal({
|
|
|
348
443
|
}), activeTab === "stores" && /*#__PURE__*/_jsx(ToolbarCopyButton, {
|
|
349
444
|
value: getStoresSnapshot,
|
|
350
445
|
disabled: displayedStores.length === 0,
|
|
351
|
-
buttonStyle: styles.headerActionButton
|
|
446
|
+
buttonStyle: styles.headerActionButton,
|
|
447
|
+
testID: `${ZUSTAND_TEST_ID}-copy`
|
|
352
448
|
}), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
|
|
353
449
|
children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
|
|
354
450
|
value: getEventsSnapshot,
|
|
355
451
|
disabled: displayedChanges.length === 0,
|
|
356
|
-
buttonStyle: styles.headerActionButton
|
|
452
|
+
buttonStyle: styles.headerActionButton,
|
|
453
|
+
testID: `${ZUSTAND_TEST_ID}-copy`
|
|
357
454
|
}), /*#__PURE__*/_jsx(PowerToggleButton, {
|
|
358
455
|
isEnabled: isEnabled,
|
|
359
456
|
onToggle: toggleCapture,
|
|
360
|
-
accessibilityLabel: "Toggle Zustand state capture"
|
|
457
|
+
accessibilityLabel: "Toggle Zustand state capture",
|
|
458
|
+
testID: `${ZUSTAND_TEST_ID}-power`
|
|
361
459
|
}), /*#__PURE__*/_jsx(ToolbarClearButton, {
|
|
362
460
|
onPress: clearChanges,
|
|
363
461
|
disabled: displayedChanges.length === 0,
|
|
364
|
-
buttonStyle: styles.headerActionButton
|
|
462
|
+
buttonStyle: styles.headerActionButton,
|
|
463
|
+
testID: `${ZUSTAND_TEST_ID}-clear`
|
|
365
464
|
})]
|
|
366
465
|
})]
|
|
367
466
|
})]
|
|
@@ -396,18 +495,20 @@ export function ZustandModal({
|
|
|
396
495
|
// Store history view — filtered to one store
|
|
397
496
|
if (selectedHistoryStore) {
|
|
398
497
|
return activeChanges.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
|
|
498
|
+
testID: `${ZUSTAND_TEST_ID}-history-list`,
|
|
399
499
|
data: activeChanges,
|
|
400
500
|
renderItem: renderItem,
|
|
401
501
|
keyExtractor: keyExtractor,
|
|
402
502
|
contentContainerStyle: styles.listContent,
|
|
403
503
|
showsVerticalScrollIndicator: true,
|
|
404
|
-
removeClippedSubviews:
|
|
504
|
+
removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
|
|
405
505
|
initialNumToRender: 12,
|
|
406
506
|
maxToRenderPerBatch: 8,
|
|
407
507
|
windowSize: 5,
|
|
408
508
|
updateCellsBatchingPeriod: 50
|
|
409
509
|
}) : /*#__PURE__*/_jsxs(View, {
|
|
410
510
|
style: styles.emptyState,
|
|
511
|
+
testID: `${ZUSTAND_TEST_ID}-history-empty`,
|
|
411
512
|
children: [/*#__PURE__*/_jsx(Box, {
|
|
412
513
|
size: 32,
|
|
413
514
|
color: night.color.textTertiary
|
|
@@ -421,14 +522,41 @@ export function ZustandModal({
|
|
|
421
522
|
});
|
|
422
523
|
}
|
|
423
524
|
|
|
525
|
+
// Store detail — live editor + details + read-only tree. After history
|
|
526
|
+
// in this chain for the same reason as the header: history stacks OVER
|
|
527
|
+
// the detail screen it was opened from.
|
|
528
|
+
if (selectedStoreName) {
|
|
529
|
+
const selectedStore = stores.find(s => s.name === selectedStoreName) ?? null;
|
|
530
|
+
return selectedStore ? /*#__PURE__*/_jsx(ZustandStoreDetailContent, {
|
|
531
|
+
store: selectedStore,
|
|
532
|
+
onViewHistory: handleViewHistory
|
|
533
|
+
}) :
|
|
534
|
+
/*#__PURE__*/
|
|
535
|
+
// The store fell out of the registry mid-look (a remote mirror
|
|
536
|
+
// rebuilding, a dev reload). Say so rather than rendering nothing.
|
|
537
|
+
_jsxs(View, {
|
|
538
|
+
style: styles.emptyState,
|
|
539
|
+
testID: `${ZUSTAND_TEST_ID}-store-detail-gone`,
|
|
540
|
+
children: [/*#__PURE__*/_jsx(Box, {
|
|
541
|
+
size: 32,
|
|
542
|
+
color: night.color.textTertiary
|
|
543
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
544
|
+
style: styles.emptyTitle,
|
|
545
|
+
children: "Store no longer registered"
|
|
546
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
547
|
+
style: styles.emptyText,
|
|
548
|
+
children: [selectedStoreName, " is not in the registry any more."]
|
|
549
|
+
})]
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
424
553
|
// Stores browse tab
|
|
425
554
|
if (activeTab === "stores") {
|
|
426
555
|
return /*#__PURE__*/_jsx(ZustandStoreBrowser, {
|
|
427
556
|
stores: displayedStores,
|
|
428
557
|
searchQuery: searchText,
|
|
429
|
-
onViewHistory: handleViewHistory,
|
|
430
558
|
isLoading: storesLoading,
|
|
431
|
-
|
|
559
|
+
onOpenStore: handleOpenStore
|
|
432
560
|
});
|
|
433
561
|
}
|
|
434
562
|
|
|
@@ -436,6 +564,7 @@ export function ZustandModal({
|
|
|
436
564
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
437
565
|
children: [!isEnabled && /*#__PURE__*/_jsxs(View, {
|
|
438
566
|
style: styles.disabledBanner,
|
|
567
|
+
testID: `${ZUSTAND_TEST_ID}-banner-disabled`,
|
|
439
568
|
children: [/*#__PURE__*/_jsx(Power, {
|
|
440
569
|
size: 14,
|
|
441
570
|
color: night.color.warning
|
|
@@ -447,11 +576,14 @@ export function ZustandModal({
|
|
|
447
576
|
style: styles.lockedBanner,
|
|
448
577
|
onPress: handleUpgradePress,
|
|
449
578
|
activeOpacity: 0.8,
|
|
579
|
+
testID: `${ZUSTAND_TEST_ID}-locked`,
|
|
580
|
+
accessibilityRole: "button",
|
|
450
581
|
children: [/*#__PURE__*/_jsx(Lock, {
|
|
451
582
|
size: 14,
|
|
452
583
|
color: night.color.accent
|
|
453
584
|
}), /*#__PURE__*/_jsxs(Text, {
|
|
454
585
|
style: styles.lockedText,
|
|
586
|
+
testID: `${ZUSTAND_TEST_ID}-locked-text`,
|
|
455
587
|
children: [lockedChangeCount, " older", " ", lockedChangeCount === 1 ? "change" : "changes", " locked"]
|
|
456
588
|
}), /*#__PURE__*/_jsx(View, {
|
|
457
589
|
style: styles.upgradeBadge,
|
|
@@ -462,12 +594,13 @@ export function ZustandModal({
|
|
|
462
594
|
})]
|
|
463
595
|
}), visibleChanges.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
|
|
464
596
|
ref: flatListRef,
|
|
597
|
+
testID: `${ZUSTAND_TEST_ID}-list`,
|
|
465
598
|
data: visibleChanges,
|
|
466
599
|
renderItem: renderItem,
|
|
467
600
|
keyExtractor: keyExtractor,
|
|
468
601
|
contentContainerStyle: styles.listContent,
|
|
469
602
|
showsVerticalScrollIndicator: true,
|
|
470
|
-
removeClippedSubviews:
|
|
603
|
+
removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
|
|
471
604
|
initialNumToRender: 12,
|
|
472
605
|
maxToRenderPerBatch: 8,
|
|
473
606
|
windowSize: 5,
|
|
@@ -491,7 +624,15 @@ export function ZustandModal({
|
|
|
491
624
|
// (inside JsModal's ScrollView wrapper every row mounts — the Network
|
|
492
625
|
// tool measured 17 JS FPS / 133% CPU at max load from this exact
|
|
493
626
|
// pattern; see tool-bench batches 2026-07-06).
|
|
494
|
-
|
|
627
|
+
//
|
|
628
|
+
// The STORES tab owns its scrolling too, for a different reason: the
|
|
629
|
+
// browser hosts the store editor as an absolute-fill overlay over a
|
|
630
|
+
// flex:1 root, and inside JsModal's ScrollView wrapper that root
|
|
631
|
+
// collapses to zero height — the list still paints (iOS overflow), but
|
|
632
|
+
// the editor opens into a 0pt box and never appears. The nested-flex:1-
|
|
633
|
+
// in-JsModal trap; storage's browser disables the wrapper for the same
|
|
634
|
+
// reason.
|
|
635
|
+
const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryStore != null || activeTab === "events" || activeTab === "stores");
|
|
495
636
|
const body = /*#__PURE__*/_jsx(View, {
|
|
496
637
|
style: styles.container,
|
|
497
638
|
children: renderContent()
|
|
@@ -91,47 +91,43 @@ function getPrimaryText(change) {
|
|
|
91
91
|
// ({change, onPress} — onPress is a stable useCallback in the modal).
|
|
92
92
|
export const ZustandStateChangeItem = /*#__PURE__*/memo(function ZustandStateChangeItem({
|
|
93
93
|
change,
|
|
94
|
-
onPress
|
|
94
|
+
onPress,
|
|
95
|
+
testID
|
|
95
96
|
}) {
|
|
96
97
|
const statusColor = getCategoryColor(change.category, change.storeName);
|
|
97
98
|
const statusLabel = getStatusLabel(change.category, change.storeName);
|
|
98
99
|
const sublabel = getSublabel(change);
|
|
99
100
|
const primaryText = getPrimaryText(change);
|
|
100
101
|
const badgeText = getBadgeText(change);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
color: buoyColors.warning
|
|
131
|
-
})
|
|
132
|
-
})]
|
|
133
|
-
});
|
|
134
|
-
}
|
|
102
|
+
|
|
103
|
+
// Always a custom badge, even when there is no icon to hang off it. The
|
|
104
|
+
// alternative — letting CompactRow draw `badgeText` itself — leaves the one
|
|
105
|
+
// row whose badge matters most (a set that changed NOTHING) as the only row
|
|
106
|
+
// in the list with no handle on its badge.
|
|
107
|
+
const customBadge = /*#__PURE__*/_jsxs(View, {
|
|
108
|
+
style: styles.badgeContainer,
|
|
109
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
110
|
+
style: [styles.badgeText, {
|
|
111
|
+
color: statusColor
|
|
112
|
+
}],
|
|
113
|
+
testID: testID ? `${testID}-badge` : undefined,
|
|
114
|
+
children: badgeText
|
|
115
|
+
}), change.isSlowUpdate ? /*#__PURE__*/_jsx(View, {
|
|
116
|
+
style: styles.slowBadge,
|
|
117
|
+
testID: testID ? `${testID}-slow` : undefined,
|
|
118
|
+
children: /*#__PURE__*/_jsx(Clock, {
|
|
119
|
+
size: 12,
|
|
120
|
+
color: buoyColors.error
|
|
121
|
+
})
|
|
122
|
+
}) : change.hasStateChange ? /*#__PURE__*/_jsx(View, {
|
|
123
|
+
style: styles.changeBadge,
|
|
124
|
+
testID: testID ? `${testID}-change` : undefined,
|
|
125
|
+
children: /*#__PURE__*/_jsx(Zap, {
|
|
126
|
+
size: 12,
|
|
127
|
+
color: buoyColors.warning
|
|
128
|
+
})
|
|
129
|
+
}) : null]
|
|
130
|
+
});
|
|
135
131
|
return /*#__PURE__*/_jsx(CompactRow, {
|
|
136
132
|
statusDotColor: statusColor,
|
|
137
133
|
statusLabel: statusLabel,
|
|
@@ -141,10 +137,13 @@ export const ZustandStateChangeItem = /*#__PURE__*/memo(function ZustandStateCha
|
|
|
141
137
|
timestamp: change.timestamp
|
|
142
138
|
}),
|
|
143
139
|
customBadge: customBadge,
|
|
144
|
-
badgeText: customBadge ? undefined : badgeText,
|
|
145
140
|
badgeColor: statusColor,
|
|
146
141
|
showChevron: true,
|
|
147
|
-
onPress: () => onPress(change)
|
|
142
|
+
onPress: () => onPress(change),
|
|
143
|
+
testID: testID,
|
|
144
|
+
statusTestID: testID ? `${testID}-status` : undefined,
|
|
145
|
+
statusSublabelTestID: testID ? `${testID}-sublabel` : undefined,
|
|
146
|
+
primaryTestID: testID ? `${testID}-keys` : undefined
|
|
148
147
|
});
|
|
149
148
|
});
|
|
150
149
|
const styles = StyleSheet.create({
|
|
@@ -19,6 +19,7 @@ import { ZustandDetailViewToggle } from "./ZustandDetailViewToggle";
|
|
|
19
19
|
import { ZustandStateInfoView } from "./ZustandStateInfoView";
|
|
20
20
|
import { DiffModeTabs, CompareBar } from "@buoy-gg/shared-ui";
|
|
21
21
|
import { zustandStateStore } from "../utils/zustandStateStore";
|
|
22
|
+
import { ZUSTAND_TEST_ID } from "../utils/testIDs";
|
|
22
23
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
23
24
|
function getChangeColor(change) {
|
|
24
25
|
return zustandStateStore.getStoreColor(change.storeName);
|
|
@@ -117,6 +118,7 @@ export function ZustandStateDetailContent({
|
|
|
117
118
|
const parsed = parseValue(change.nextState);
|
|
118
119
|
return /*#__PURE__*/_jsxs(View, {
|
|
119
120
|
style: styles.contentCard,
|
|
121
|
+
testID: `${ZUSTAND_TEST_ID}-state-card`,
|
|
120
122
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
121
123
|
style: styles.valueHeader,
|
|
122
124
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
@@ -138,6 +140,7 @@ export function ZustandStateDetailContent({
|
|
|
138
140
|
style: styles.typeBadge,
|
|
139
141
|
children: /*#__PURE__*/_jsx(Text, {
|
|
140
142
|
style: styles.typeText,
|
|
143
|
+
testID: `${ZUSTAND_TEST_ID}-state-type`,
|
|
141
144
|
children: stateType.toUpperCase()
|
|
142
145
|
})
|
|
143
146
|
})]
|
|
@@ -189,7 +192,14 @@ export function ZustandStateDetailContent({
|
|
|
189
192
|
const handleFooterNext = useCallback(() => {
|
|
190
193
|
onIndexChange(Math.max(0, selectedIndex - 1));
|
|
191
194
|
}, [selectedIndex, onIndexChange]);
|
|
192
|
-
|
|
195
|
+
|
|
196
|
+
// The diff compares THIS change's prevState/nextState — both live on the
|
|
197
|
+
// change itself, so it needs no siblings. Gating on `changes.length` greyed
|
|
198
|
+
// DIFF out for every zustand event opened from the Events timeline, which
|
|
199
|
+
// hands this component a one-element array. Only a change with no captured
|
|
200
|
+
// previous state (initial creation) has nothing to compare.
|
|
201
|
+
const diffDisabled = change.prevState === undefined;
|
|
202
|
+
const showInternalFooter = !disableInternalFooter && totalChanges > 1;
|
|
193
203
|
return /*#__PURE__*/_jsxs(View, {
|
|
194
204
|
style: styles.container,
|
|
195
205
|
children: [/*#__PURE__*/_jsx(ZustandDetailViewToggle, {
|
|
@@ -197,7 +207,7 @@ export function ZustandStateDetailContent({
|
|
|
197
207
|
onViewChange: setActiveView,
|
|
198
208
|
diffDisabled: diffDisabled
|
|
199
209
|
}), /*#__PURE__*/_jsxs(View, {
|
|
200
|
-
style: styles.contentArea,
|
|
210
|
+
style: [styles.contentArea, showInternalFooter && styles.contentAreaFooterPad],
|
|
201
211
|
children: [activeView === "change" && /*#__PURE__*/_jsx(ZustandStateInfoView, {
|
|
202
212
|
change: change
|
|
203
213
|
}), activeView === "state" && /*#__PURE__*/_jsx(View, {
|
|
@@ -205,19 +215,22 @@ export function ZustandStateDetailContent({
|
|
|
205
215
|
children: renderStateView()
|
|
206
216
|
}), activeView === "diff" && /*#__PURE__*/_jsxs(View, {
|
|
207
217
|
style: styles.diffContainer,
|
|
218
|
+
testID: `${ZUSTAND_TEST_ID}-diff`,
|
|
208
219
|
children: [/*#__PURE__*/_jsx(DiffModeTabs, {
|
|
209
220
|
tabs: DIFF_MODE_TABS,
|
|
210
221
|
activeTab: diffMode,
|
|
211
|
-
onTabChange: setDiffMode
|
|
222
|
+
onTabChange: setDiffMode,
|
|
223
|
+
testIDPrefix: `${ZUSTAND_TEST_ID}-diff-tab`
|
|
212
224
|
}), /*#__PURE__*/_jsx(CompareBar, {
|
|
213
225
|
leftEvent: leftEvent,
|
|
214
226
|
rightEvent: rightEvent
|
|
215
227
|
}), /*#__PURE__*/_jsx(View, {
|
|
216
228
|
style: styles.diffContent,
|
|
229
|
+
testID: `${ZUSTAND_TEST_ID}-diff-content`,
|
|
217
230
|
children: renderDiffContent()
|
|
218
231
|
})]
|
|
219
232
|
})]
|
|
220
|
-
}),
|
|
233
|
+
}), showInternalFooter && /*#__PURE__*/_jsx(EventStepperFooter, {
|
|
221
234
|
currentIndex: chronologicalNumber - 1,
|
|
222
235
|
totalItems: totalChanges,
|
|
223
236
|
onPrevious: handleFooterPrevious,
|
|
@@ -257,7 +270,11 @@ const styles = StyleSheet.create({
|
|
|
257
270
|
backgroundColor: "transparent"
|
|
258
271
|
},
|
|
259
272
|
contentArea: {
|
|
260
|
-
flex: 1
|
|
273
|
+
flex: 1
|
|
274
|
+
},
|
|
275
|
+
// Clearance for the absolute EventStepperFooter — only when it renders,
|
|
276
|
+
// otherwise hosts like the Events tool get a dead band at the sheet bottom.
|
|
277
|
+
contentAreaFooterPad: {
|
|
261
278
|
paddingBottom: 96
|
|
262
279
|
},
|
|
263
280
|
stateContainer: {
|