@buoy-gg/zustand 7.0.32 → 7.0.34

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 (35) hide show
  1. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +13 -107
  2. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +24 -288
  3. package/lib/commonjs/zustand/components/ZustandModal.js +83 -141
  4. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +8 -61
  5. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +2 -46
  6. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +29 -54
  7. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +33 -53
  8. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +24 -78
  9. package/lib/commonjs/zustand/utils/snapshotProvider.js +17 -60
  10. package/lib/commonjs/zustand/utils/zustandStateStore.js +37 -223
  11. package/lib/module/zustand/components/ZustandDetailViewToggle.js +15 -109
  12. package/lib/module/zustand/components/ZustandEventFilterView.js +28 -292
  13. package/lib/module/zustand/components/ZustandModal.js +85 -143
  14. package/lib/module/zustand/components/ZustandStateChangeItem.js +10 -63
  15. package/lib/module/zustand/components/ZustandStateInfoView.js +1 -45
  16. package/lib/module/zustand/components/ZustandStoreBrowser.js +31 -56
  17. package/lib/module/zustand/hooks/useZustandStateChanges.js +34 -54
  18. package/lib/module/zustand/sync/zustandSyncAdapter.js +25 -79
  19. package/lib/module/zustand/utils/snapshotProvider.js +18 -61
  20. package/lib/module/zustand/utils/zustandStateStore.js +37 -223
  21. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts +2 -1
  22. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -1
  23. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts +6 -0
  24. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -1
  25. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
  26. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -1
  27. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -1
  28. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -1
  29. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts +2 -1
  30. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -1
  31. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +103 -117
  32. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
  33. package/lib/typescript/zustand/utils/zustandStateStore.d.ts +6 -26
  34. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
  35. package/package.json +3 -3
@@ -1,303 +1,39 @@
1
1
  "use strict";
2
2
 
3
- import { useState } from "react";
4
- import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
5
- import { Filter, X, SectionHeader, AddFilterButton, AddFilterInput, night } from "@buoy-gg/shared-ui";
3
+ /**
4
+ * ZustandEventFilterView
5
+ *
6
+ * The shared `EntityFilterView` with zustand's nouns: registered stores you
7
+ * can tap to hide, active store-name patterns, and how it works.
8
+ */
9
+
10
+ import { useMemo } from "react";
11
+ import { EntityFilterView } from "@buoy-gg/shared-ui";
6
12
  import { zustandStateStore } from "../utils/zustandStateStore";
7
13
  import { ZUSTAND_TEST_ID, slugForTestID } from "../utils/testIDs";
8
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
- /** Returns the store color for a given pattern if it exactly matches a registered store name */
10
- function getStoreColorForPattern(pattern, stores) {
11
- const match = stores.find(s => s.name.toLowerCase() === pattern.toLowerCase());
12
- return match ? zustandStateStore.getStoreColor(match.name) : null;
13
- }
14
-
15
- /** True if any active pattern filters out this store */
16
- function isStoreFiltered(store, ignoredPatterns) {
17
- return Array.from(ignoredPatterns).some(p => store.name.toLowerCase().includes(p.toLowerCase()));
18
- }
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ const EXAMPLES = ["• auth → hides authStore from stores & events", "• temp → hides tempStore, tempUserStore", "• cart → hides cartStore from both tabs"];
19
16
  export function ZustandEventFilterView({
20
17
  ignoredPatterns,
21
18
  onTogglePattern,
22
19
  onAddPattern,
23
20
  stores
24
21
  }) {
25
- const [showAddInput, setShowAddInput] = useState(false);
26
- const [inputValue, setInputValue] = useState("");
27
- const handleAddPattern = () => {
28
- const trimmed = inputValue.trim();
29
- if (trimmed) {
30
- onAddPattern(trimmed);
31
- setInputValue("");
32
- setShowAddInput(false);
33
- }
34
- };
35
- return /*#__PURE__*/_jsxs(ScrollView, {
36
- style: styles.container,
37
- contentContainerStyle: styles.scrollContent,
38
- showsVerticalScrollIndicator: false,
39
- testID: `${ZUSTAND_TEST_ID}-filters-scroll`,
40
- children: [stores.length > 0 && /*#__PURE__*/_jsxs(View, {
41
- style: styles.section,
42
- children: [/*#__PURE__*/_jsxs(SectionHeader, {
43
- children: [/*#__PURE__*/_jsx(SectionHeader.Title, {
44
- children: "REGISTERED STORES"
45
- }), /*#__PURE__*/_jsx(SectionHeader.Badge, {
46
- count: stores.length,
47
- color: night.color.accent,
48
- testID: `${ZUSTAND_TEST_ID}-filters-store-count`
49
- })]
50
- }), /*#__PURE__*/_jsx(ScrollView, {
51
- style: styles.storeList,
52
- contentContainerStyle: styles.storeListContent,
53
- showsVerticalScrollIndicator: true,
54
- nestedScrollEnabled: true,
55
- children: stores.map(store => {
56
- const storeColor = zustandStateStore.getStoreColor(store.name);
57
- const filtered = isStoreFiltered(store, ignoredPatterns);
58
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
59
- style: [styles.storeRow, filtered && styles.storeRowFiltered],
60
- onPress: () => onAddPattern(store.name),
61
- disabled: filtered,
62
- testID: `${ZUSTAND_TEST_ID}-filters-store-${slugForTestID(store.name)}`,
63
- accessibilityRole: "button",
64
- accessibilityLabel: `Hide ${store.name}`,
65
- accessibilityState: {
66
- disabled: filtered
67
- },
68
- children: [/*#__PURE__*/_jsx(View, {
69
- style: [styles.storeDot, {
70
- backgroundColor: filtered ? night.color.textTertiary : storeColor
71
- }]
72
- }), /*#__PURE__*/_jsx(Text, {
73
- style: [styles.storeRowText, filtered && styles.storeRowTextFiltered, !filtered && {
74
- color: storeColor
75
- }],
76
- numberOfLines: 1,
77
- children: store.name
78
- }), filtered ? /*#__PURE__*/_jsx(Text, {
79
- style: styles.filteredLabel,
80
- children: "hidden"
81
- }) : /*#__PURE__*/_jsx(Text, {
82
- style: [styles.addLabel, {
83
- color: storeColor + "99"
84
- }],
85
- children: "+ hide"
86
- })]
87
- }, store.name);
88
- })
89
- })]
90
- }), /*#__PURE__*/_jsxs(View, {
91
- style: styles.section,
92
- children: [/*#__PURE__*/_jsxs(SectionHeader, {
93
- children: [/*#__PURE__*/_jsx(SectionHeader.Icon, {
94
- icon: Filter,
95
- color: night.color.accent,
96
- size: 12
97
- }), /*#__PURE__*/_jsx(SectionHeader.Title, {
98
- children: "STORE NAME FILTERS"
99
- }), ignoredPatterns.size > 0 && /*#__PURE__*/_jsx(SectionHeader.Badge, {
100
- count: ignoredPatterns.size,
101
- color: night.color.accent,
102
- testID: `${ZUSTAND_TEST_ID}-filters-count`
103
- })]
104
- }), /*#__PURE__*/_jsx(View, {
105
- style: styles.addInputWrapper,
106
- children: !showAddInput ? /*#__PURE__*/_jsx(AddFilterButton, {
107
- onPress: () => setShowAddInput(true),
108
- color: night.color.accent,
109
- label: "Add store name pattern",
110
- testID: `${ZUSTAND_TEST_ID}-filters-add-open`
111
- }) : /*#__PURE__*/_jsx(AddFilterInput, {
112
- value: inputValue,
113
- onChange: setInputValue,
114
- onSubmit: handleAddPattern,
115
- onCancel: () => {
116
- setShowAddInput(false);
117
- setInputValue("");
118
- },
119
- placeholder: "Enter store name pattern to hide...",
120
- color: night.color.accent,
121
- testID: `${ZUSTAND_TEST_ID}-filters-add-input`
122
- })
123
- }), ignoredPatterns.size > 0 && /*#__PURE__*/_jsx(View, {
124
- style: styles.patternList,
125
- children: Array.from(ignoredPatterns).map(pattern => {
126
- const storeColor = getStoreColorForPattern(pattern, stores);
127
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
128
- style: styles.patternRow,
129
- onPress: () => onTogglePattern(pattern),
130
- activeOpacity: 0.8,
131
- testID: `${ZUSTAND_TEST_ID}-filters-chip-${slugForTestID(pattern)}`,
132
- accessibilityRole: "button",
133
- accessibilityLabel: `Remove filter ${pattern}`,
134
- children: [storeColor ? /*#__PURE__*/_jsx(View, {
135
- style: [styles.storeDot, {
136
- backgroundColor: storeColor
137
- }]
138
- }) : null, /*#__PURE__*/_jsx(Text, {
139
- style: [styles.patternText, storeColor ? {
140
- color: storeColor
141
- } : null],
142
- numberOfLines: 1,
143
- children: pattern
144
- }), /*#__PURE__*/_jsx(X, {
145
- size: 12,
146
- color: night.color.accent + "80"
147
- })]
148
- }, pattern);
149
- })
150
- })]
151
- }), /*#__PURE__*/_jsxs(View, {
152
- style: styles.section,
153
- children: [/*#__PURE__*/_jsxs(SectionHeader, {
154
- children: [/*#__PURE__*/_jsx(SectionHeader.Icon, {
155
- icon: Filter,
156
- color: night.color.textSecondary,
157
- size: 12
158
- }), /*#__PURE__*/_jsx(SectionHeader.Title, {
159
- children: "HOW EVENT FILTERS WORK"
160
- })]
161
- }), /*#__PURE__*/_jsx(Text, {
162
- style: styles.howItWorksText,
163
- children: "Patterns hide matching stores from both the Stores tab and the Events tab. Tap a store above to quickly add it as a filter."
164
- }), /*#__PURE__*/_jsxs(View, {
165
- style: styles.examplesContainer,
166
- children: [/*#__PURE__*/_jsx(Text, {
167
- style: styles.examplesTitle,
168
- children: "EXAMPLES:"
169
- }), ["• auth → hides authStore from stores & events", "• temp → hides tempStore, tempUserStore", "• cart → hides cartStore from both tabs"].map(ex => /*#__PURE__*/_jsx(Text, {
170
- style: styles.exampleItem,
171
- children: ex
172
- }, ex))]
173
- })]
174
- })]
22
+ const entities = useMemo(() => stores.map(store => ({
23
+ name: store.name,
24
+ color: zustandStateStore.getStoreColor(store.name)
25
+ })), [stores]);
26
+ return /*#__PURE__*/_jsx(EntityFilterView, {
27
+ entities: entities,
28
+ ignoredPatterns: ignoredPatterns,
29
+ onTogglePattern: onTogglePattern,
30
+ onAddPattern: onAddPattern,
31
+ entityNoun: "STORES",
32
+ entitySingular: "store",
33
+ entityKey: "store",
34
+ testIDPrefix: ZUSTAND_TEST_ID,
35
+ slug: slugForTestID,
36
+ howItWorks: "Patterns hide matching stores from both the Stores tab and the Events tab. Tap a store above to quickly add it as a filter.",
37
+ examples: EXAMPLES
175
38
  });
176
- }
177
- const styles = StyleSheet.create({
178
- container: {
179
- flex: 1,
180
- // Transparent so the night sheet's star field shows through.
181
- backgroundColor: "transparent"
182
- },
183
- scrollContent: {
184
- paddingTop: 16,
185
- paddingHorizontal: 16,
186
- paddingBottom: 24,
187
- gap: 16
188
- },
189
- section: {
190
- backgroundColor: night.color.surface,
191
- borderRadius: 16,
192
- borderWidth: 1,
193
- borderColor: night.color.border,
194
- overflow: "hidden"
195
- },
196
- storeList: {
197
- maxHeight: 180
198
- },
199
- storeListContent: {
200
- paddingHorizontal: 16,
201
- paddingTop: 8,
202
- paddingBottom: 16,
203
- gap: 10
204
- },
205
- storeRow: {
206
- flexDirection: "row",
207
- alignItems: "center",
208
- gap: 10,
209
- paddingVertical: 10,
210
- paddingHorizontal: 12,
211
- backgroundColor: night.color.surfaceElevated,
212
- borderRadius: 8,
213
- borderWidth: 1,
214
- borderColor: night.color.border
215
- },
216
- storeRowFiltered: {
217
- opacity: 0.45
218
- },
219
- storeDot: {
220
- width: 7,
221
- height: 7,
222
- borderRadius: 3.5
223
- },
224
- storeRowText: {
225
- flex: 1,
226
- fontSize: 12,
227
- fontWeight: "600",
228
- fontFamily: "monospace"
229
- },
230
- storeRowTextFiltered: {
231
- color: night.color.textTertiary,
232
- textDecorationLine: "line-through"
233
- },
234
- filteredLabel: {
235
- fontSize: 10,
236
- fontWeight: "500",
237
- color: night.color.textTertiary,
238
- fontFamily: "monospace"
239
- },
240
- addLabel: {
241
- fontSize: 10,
242
- fontWeight: "600",
243
- fontFamily: "monospace"
244
- },
245
- addInputWrapper: {
246
- paddingHorizontal: 16,
247
- paddingTop: 8,
248
- paddingBottom: 8
249
- },
250
- patternList: {
251
- paddingHorizontal: 16,
252
- paddingBottom: 16,
253
- gap: 8
254
- },
255
- patternRow: {
256
- flexDirection: "row",
257
- alignItems: "center",
258
- justifyContent: "space-between",
259
- paddingVertical: 10,
260
- paddingHorizontal: 12,
261
- backgroundColor: night.color.surfaceElevated,
262
- borderRadius: 8,
263
- borderWidth: 1,
264
- borderColor: night.color.border,
265
- gap: 8
266
- },
267
- patternText: {
268
- flex: 1,
269
- fontSize: 12,
270
- fontFamily: "monospace",
271
- color: night.color.text
272
- },
273
- howItWorksText: {
274
- fontSize: 11,
275
- color: night.color.textSecondary,
276
- lineHeight: 16,
277
- marginTop: 8,
278
- paddingHorizontal: 16,
279
- fontFamily: "monospace"
280
- },
281
- examplesContainer: {
282
- paddingTop: 8,
283
- paddingHorizontal: 16,
284
- paddingBottom: 16,
285
- borderTopWidth: 1,
286
- borderTopColor: night.color.border,
287
- marginTop: 12
288
- },
289
- examplesTitle: {
290
- fontSize: 10,
291
- fontWeight: "600",
292
- color: night.color.textTertiary,
293
- fontFamily: "monospace",
294
- letterSpacing: 0.5,
295
- marginBottom: 6
296
- },
297
- exampleItem: {
298
- fontSize: 10,
299
- color: night.color.textTertiary,
300
- fontFamily: "monospace",
301
- lineHeight: 16
302
- }
303
- });
39
+ }
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { useState, useMemo, useRef, useCallback, useEffect } from "react";
12
- import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
13
- import { JsModal, ModalHeader, TabSelector, macOSColors, night, Search, Filter, Power, X, Lock, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, windowDragProps } from "@buoy-gg/shared-ui";
12
+ import { View, Text, StyleSheet, FlatList, TouchableOpacity } from "react-native";
13
+ import { JsModal, ModalHeader, TabSelector, macOSColors, night, ToolEmptyState, resolveListEmptyState, Search, Filter, Power, Lock, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, windowDragProps, EVENT_LIST_PRESET } from "@buoy-gg/shared-ui";
14
14
  import { useIsPro } from "@buoy-gg/license";
15
15
  import { useZustandStateChanges } from "../hooks/useZustandStateChanges";
16
16
  import { ZustandStateChangeItem } from "./ZustandStateChangeItem";
@@ -21,33 +21,32 @@ import { ZustandEventFilterView } from "./ZustandEventFilterView";
21
21
  import { ZUSTAND_TEST_ID, buildChangeRowTestIDs } from "../utils/testIDs";
22
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
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.
24
+ * The change log's "nothing here". Everything but the nouns is the shared
25
+ * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
31
26
  */
32
- const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
33
27
  function EventsEmptyState({
34
- isEnabled
28
+ isEnabled,
29
+ searchText,
30
+ totalChanges
35
31
  }) {
36
- return /*#__PURE__*/_jsxs(View, {
37
- style: styles.emptyState,
38
- testID: `${ZUSTAND_TEST_ID}-empty`,
39
- children: [/*#__PURE__*/_jsx(Box, {
40
- size: 32,
41
- color: night.color.textTertiary
42
- }), /*#__PURE__*/_jsx(Text, {
43
- style: styles.emptyTitle,
44
- testID: `${ZUSTAND_TEST_ID}-empty-title`,
45
- children: "No state changes"
46
- }), /*#__PURE__*/_jsx(Text, {
47
- style: styles.emptyText,
48
- testID: `${ZUSTAND_TEST_ID}-empty-text`,
49
- children: isEnabled ? "State changes will appear here as stores update." : "Enable capture to start recording state changes"
50
- })]
32
+ const content = resolveListEmptyState({
33
+ subject: "changes",
34
+ search: searchText,
35
+ total: totalChanges,
36
+ capturing: isEnabled,
37
+ waiting: {
38
+ otto: "waiting",
39
+ title: "No state changes",
40
+ message: "State changes will appear here as stores update."
41
+ },
42
+ paused: {
43
+ title: "No state changes",
44
+ message: "Enable capture to start recording state changes"
45
+ }
46
+ });
47
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
48
+ ...content,
49
+ testID: `${ZUSTAND_TEST_ID}-empty`
51
50
  });
52
51
  }
53
52
  export function ZustandModal({
@@ -77,6 +76,7 @@ export function ZustandModal({
77
76
  const [showFilters, setShowFilters] = useState(false);
78
77
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
79
78
  const {
79
+ stateChanges,
80
80
  filteredChanges,
81
81
  filter,
82
82
  setFilter,
@@ -99,6 +99,16 @@ export function ZustandModal({
99
99
  return stores.filter(s => !Array.from(ignoredPatterns).some(p => s.name.toLowerCase().includes(p.toLowerCase())));
100
100
  }, [stores, ignoredPatterns]);
101
101
 
102
+ /**
103
+ * How many changes this store has captured, before the search — the
104
+ * denominator the scoped empty state needs so "clear the search" is only
105
+ * offered when clearing it would actually show something.
106
+ */
107
+ const scopedTotalForStore = useMemo(() => {
108
+ if (!selectedHistoryStore) return 0;
109
+ return stateChanges.filter(c => c.storeName === selectedHistoryStore).length;
110
+ }, [stateChanges, selectedHistoryStore]);
111
+
102
112
  // Scope to a store BEFORE the tier cap, not after.
103
113
  //
104
114
  // The other way round, the cap was applied to the whole log and a store's
@@ -364,49 +374,24 @@ export function ZustandModal({
364
374
  onBack: onBack
365
375
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
366
376
  title: "",
367
- children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
368
- style: styles.headerSearchContainer,
369
- testID: `${ZUSTAND_TEST_ID}-search`,
370
- children: [/*#__PURE__*/_jsx(Search, {
371
- size: 14,
372
- color: night.color.textSecondary
373
- }), /*#__PURE__*/_jsx(TextInput, {
374
- ref: searchInputRef,
375
- testID: `${ZUSTAND_TEST_ID}-search-field`,
376
- style: styles.headerSearchInput,
377
- placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
378
- placeholderTextColor: night.color.placeholder,
379
- value: searchText,
380
- onChangeText: handleSearch,
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",
396
- autoCapitalize: "none",
397
- autoCorrect: false,
398
- returnKeyType: "search"
399
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
400
- onPress: closeSearch,
401
- style: styles.headerSearchClear,
402
- testID: `${ZUSTAND_TEST_ID}-search-clear`,
403
- accessibilityRole: "button",
404
- accessibilityLabel: "Close search",
405
- children: /*#__PURE__*/_jsx(X, {
406
- size: 14,
407
- color: night.color.textSecondary
408
- })
409
- })]
377
+ children: isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
378
+ value: searchText,
379
+ onChangeText: handleSearch,
380
+ onCancel: closeSearch,
381
+ placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
382
+ accessibilityLabel: "Search Zustand stores and events",
383
+ iconSize: 14,
384
+ testID: `${ZUSTAND_TEST_ID}-search-field`,
385
+ containerTestID: `${ZUSTAND_TEST_ID}-search`,
386
+ clearTestID: `${ZUSTAND_TEST_ID}-search-clear`
387
+ // Stay open while the query is still narrowing the list: the
388
+ // header would otherwise go back to its TABS — whose counts are
389
+ // of the WHOLE capture — above a filtered list, with nothing to
390
+ // explain the gap.
391
+ ,
392
+ onBlur: () => {
393
+ if (searchText.length === 0) setIsSearchActive(false);
394
+ }
410
395
  }) : /*#__PURE__*/_jsx(TabSelector, {
411
396
  tabs: [{
412
397
  key: "stores",
@@ -501,24 +486,25 @@ export function ZustandModal({
501
486
  keyExtractor: keyExtractor,
502
487
  contentContainerStyle: styles.listContent,
503
488
  showsVerticalScrollIndicator: true,
504
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
505
- initialNumToRender: 12,
506
- maxToRenderPerBatch: 8,
507
- windowSize: 5,
508
- updateCellsBatchingPeriod: 50
509
- }) : /*#__PURE__*/_jsxs(View, {
510
- style: styles.emptyState,
489
+ ...EVENT_LIST_PRESET
490
+ }) : /*#__PURE__*/_jsx(ToolEmptyState, {
511
491
  testID: `${ZUSTAND_TEST_ID}-history-empty`,
512
- children: [/*#__PURE__*/_jsx(Box, {
513
- size: 32,
514
- color: night.color.textTertiary
515
- }), /*#__PURE__*/_jsx(Text, {
516
- style: styles.emptyTitle,
517
- children: "No history yet"
518
- }), /*#__PURE__*/_jsxs(Text, {
519
- style: styles.emptyText,
520
- children: ["State changes for ", selectedHistoryStore, " will appear here."]
521
- })]
492
+ ...resolveListEmptyState({
493
+ subject: "changes",
494
+ search: searchText,
495
+ // Scoped to one store, so the count is this store's, not the log's.
496
+ total: searchText.trim() ? scopedTotalForStore : 0,
497
+ capturing: isEnabled,
498
+ waiting: {
499
+ otto: "waiting",
500
+ title: "No history yet",
501
+ message: `State changes for ${selectedHistoryStore} will appear here.`
502
+ },
503
+ paused: {
504
+ title: "No history yet",
505
+ message: `Capture is off, so nothing is being recorded for ${selectedHistoryStore}.`
506
+ }
507
+ })
522
508
  });
523
509
  }
524
510
 
@@ -534,19 +520,15 @@ export function ZustandModal({
534
520
  /*#__PURE__*/
535
521
  // The store fell out of the registry mid-look (a remote mirror
536
522
  // 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
- })]
523
+ _jsx(ToolEmptyState, {
524
+ testID: `${ZUSTAND_TEST_ID}-store-detail-gone`
525
+ // ghost = "stale or detached: an unmounted component, an old
526
+ // snapshot" — exactly what the store you are looking at has become.
527
+ // No other tool has this cause, so it skips the resolver.
528
+ ,
529
+ otto: "ghost",
530
+ title: "Store no longer registered",
531
+ message: `${selectedStoreName} is not in the registry any more.`
550
532
  });
551
533
  }
552
534
 
@@ -600,13 +582,11 @@ export function ZustandModal({
600
582
  keyExtractor: keyExtractor,
601
583
  contentContainerStyle: styles.listContent,
602
584
  showsVerticalScrollIndicator: true,
603
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
604
- initialNumToRender: 12,
605
- maxToRenderPerBatch: 8,
606
- windowSize: 5,
607
- updateCellsBatchingPeriod: 50
585
+ ...EVENT_LIST_PRESET
608
586
  }) : /*#__PURE__*/_jsx(EventsEmptyState, {
609
- isEnabled: isEnabled
587
+ isEnabled: isEnabled,
588
+ searchText: searchText,
589
+ totalChanges: stats.totalChanges
610
590
  })]
611
591
  });
612
592
  };
@@ -715,27 +695,6 @@ const styles = StyleSheet.create({
715
695
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
716
696
  backgroundColor: "transparent"
717
697
  },
718
- headerSearchContainer: {
719
- flexDirection: "row",
720
- alignItems: "center",
721
- backgroundColor: night.color.surfaceElevated,
722
- borderRadius: night.radius.row,
723
- borderWidth: 1,
724
- borderColor: night.color.border,
725
- paddingHorizontal: 12,
726
- paddingVertical: 5
727
- },
728
- headerSearchInput: {
729
- flex: 1,
730
- color: night.color.text,
731
- fontSize: 13,
732
- marginLeft: 6,
733
- paddingVertical: 2
734
- },
735
- headerSearchClear: {
736
- marginLeft: 6,
737
- padding: 4
738
- },
739
698
  headerActionButton: {
740
699
  width: 32,
741
700
  height: 32,
@@ -774,23 +733,6 @@ const styles = StyleSheet.create({
774
733
  paddingTop: 8,
775
734
  paddingBottom: 20
776
735
  },
777
- emptyState: {
778
- alignItems: "center",
779
- paddingVertical: 40
780
- },
781
- emptyTitle: {
782
- color: night.color.text,
783
- fontSize: 14,
784
- fontWeight: "600",
785
- marginTop: 12,
786
- marginBottom: 6
787
- },
788
- emptyText: {
789
- color: night.color.textTertiary,
790
- fontSize: 12,
791
- textAlign: "center",
792
- lineHeight: 18
793
- },
794
736
  lockedBanner: {
795
737
  flexDirection: "row",
796
738
  alignItems: "center",