@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
@@ -26,33 +26,32 @@ var _jsxRuntime = require("react/jsx-runtime");
26
26
  */
27
27
 
28
28
  /**
29
- * Cell clipping, off in e2e bundles only.
30
- *
31
- * `removeClippedSubviews` unmounts the views of rows scrolled out of the
32
- * window, which is what makes a long change list cheap — and which makes
33
- * Detox report a perfectly rendered row as un-hittable. The env var is
34
- * inlined at bundle time, so a bundler that does NOT inline it leaves the
35
- * production behaviour exactly as it was.
29
+ * The change log's "nothing here". Everything but the nouns is the shared
30
+ * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
36
31
  */
37
- const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
38
32
  function EventsEmptyState({
39
- isEnabled
33
+ isEnabled,
34
+ searchText,
35
+ totalChanges
40
36
  }) {
41
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
42
- style: styles.emptyState,
43
- testID: `${_testIDs.ZUSTAND_TEST_ID}-empty`,
44
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
45
- size: 32,
46
- color: _sharedUi.night.color.textTertiary
47
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
48
- style: styles.emptyTitle,
49
- testID: `${_testIDs.ZUSTAND_TEST_ID}-empty-title`,
50
- children: "No state changes"
51
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
52
- style: styles.emptyText,
53
- testID: `${_testIDs.ZUSTAND_TEST_ID}-empty-text`,
54
- children: isEnabled ? "State changes will appear here as stores update." : "Enable capture to start recording state changes"
55
- })]
37
+ const content = (0, _sharedUi.resolveListEmptyState)({
38
+ subject: "changes",
39
+ search: searchText,
40
+ total: totalChanges,
41
+ capturing: isEnabled,
42
+ waiting: {
43
+ otto: "waiting",
44
+ title: "No state changes",
45
+ message: "State changes will appear here as stores update."
46
+ },
47
+ paused: {
48
+ title: "No state changes",
49
+ message: "Enable capture to start recording state changes"
50
+ }
51
+ });
52
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
53
+ ...content,
54
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-empty`
56
55
  });
57
56
  }
58
57
  function ZustandModal({
@@ -82,6 +81,7 @@ function ZustandModal({
82
81
  const [showFilters, setShowFilters] = (0, _react.useState)(false);
83
82
  const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
84
83
  const {
84
+ stateChanges,
85
85
  filteredChanges,
86
86
  filter,
87
87
  setFilter,
@@ -104,6 +104,16 @@ function ZustandModal({
104
104
  return stores.filter(s => !Array.from(ignoredPatterns).some(p => s.name.toLowerCase().includes(p.toLowerCase())));
105
105
  }, [stores, ignoredPatterns]);
106
106
 
107
+ /**
108
+ * How many changes this store has captured, before the search — the
109
+ * denominator the scoped empty state needs so "clear the search" is only
110
+ * offered when clearing it would actually show something.
111
+ */
112
+ const scopedTotalForStore = (0, _react.useMemo)(() => {
113
+ if (!selectedHistoryStore) return 0;
114
+ return stateChanges.filter(c => c.storeName === selectedHistoryStore).length;
115
+ }, [stateChanges, selectedHistoryStore]);
116
+
107
117
  // Scope to a store BEFORE the tier cap, not after.
108
118
  //
109
119
  // The other way round, the cap was applied to the whole log and a store's
@@ -369,49 +379,24 @@ function ZustandModal({
369
379
  onBack: onBack
370
380
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
371
381
  title: "",
372
- children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
373
- style: styles.headerSearchContainer,
374
- testID: `${_testIDs.ZUSTAND_TEST_ID}-search`,
375
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
376
- size: 14,
377
- color: _sharedUi.night.color.textSecondary
378
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
379
- ref: searchInputRef,
380
- testID: `${_testIDs.ZUSTAND_TEST_ID}-search-field`,
381
- style: styles.headerSearchInput,
382
- placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
383
- placeholderTextColor: _sharedUi.night.color.placeholder,
384
- value: searchText,
385
- onChangeText: handleSearch,
386
- onSubmitEditing: () => searchInputRef.current?.blur()
387
- // Stay open while the query is still narrowing the list.
388
- //
389
- // Collapsing on blur hid the reason rows were missing: the
390
- // header went back to its TABS — whose counts are of the
391
- // WHOLE capture on the stores side — while the list stayed
392
- // filtered, so the tool showed "Stores (4)" above a single
393
- // row and offered nothing to explain the gap. The only way
394
- // back was to reopen a field the user had no reason to think
395
- // was still armed.
396
- ,
397
- onBlur: () => {
398
- if (searchText.length === 0) setIsSearchActive(false);
399
- },
400
- accessibilityLabel: "Search Zustand stores and events",
401
- autoCapitalize: "none",
402
- autoCorrect: false,
403
- returnKeyType: "search"
404
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
405
- onPress: closeSearch,
406
- style: styles.headerSearchClear,
407
- testID: `${_testIDs.ZUSTAND_TEST_ID}-search-clear`,
408
- accessibilityRole: "button",
409
- accessibilityLabel: "Close search",
410
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
411
- size: 14,
412
- color: _sharedUi.night.color.textSecondary
413
- })
414
- })]
382
+ children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
383
+ value: searchText,
384
+ onChangeText: handleSearch,
385
+ onCancel: closeSearch,
386
+ placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
387
+ accessibilityLabel: "Search Zustand stores and events",
388
+ iconSize: 14,
389
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-search-field`,
390
+ containerTestID: `${_testIDs.ZUSTAND_TEST_ID}-search`,
391
+ clearTestID: `${_testIDs.ZUSTAND_TEST_ID}-search-clear`
392
+ // Stay open while the query is still narrowing the list: the
393
+ // header would otherwise go back to its TABS — whose counts are
394
+ // of the WHOLE capture — above a filtered list, with nothing to
395
+ // explain the gap.
396
+ ,
397
+ onBlur: () => {
398
+ if (searchText.length === 0) setIsSearchActive(false);
399
+ }
415
400
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
416
401
  tabs: [{
417
402
  key: "stores",
@@ -506,24 +491,25 @@ function ZustandModal({
506
491
  keyExtractor: keyExtractor,
507
492
  contentContainerStyle: styles.listContent,
508
493
  showsVerticalScrollIndicator: true,
509
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
510
- initialNumToRender: 12,
511
- maxToRenderPerBatch: 8,
512
- windowSize: 5,
513
- updateCellsBatchingPeriod: 50
514
- }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
515
- style: styles.emptyState,
494
+ ..._sharedUi.EVENT_LIST_PRESET
495
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
516
496
  testID: `${_testIDs.ZUSTAND_TEST_ID}-history-empty`,
517
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
518
- size: 32,
519
- color: _sharedUi.night.color.textTertiary
520
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
521
- style: styles.emptyTitle,
522
- children: "No history yet"
523
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
524
- style: styles.emptyText,
525
- children: ["State changes for ", selectedHistoryStore, " will appear here."]
526
- })]
497
+ ...(0, _sharedUi.resolveListEmptyState)({
498
+ subject: "changes",
499
+ search: searchText,
500
+ // Scoped to one store, so the count is this store's, not the log's.
501
+ total: searchText.trim() ? scopedTotalForStore : 0,
502
+ capturing: isEnabled,
503
+ waiting: {
504
+ otto: "waiting",
505
+ title: "No history yet",
506
+ message: `State changes for ${selectedHistoryStore} will appear here.`
507
+ },
508
+ paused: {
509
+ title: "No history yet",
510
+ message: `Capture is off, so nothing is being recorded for ${selectedHistoryStore}.`
511
+ }
512
+ })
527
513
  });
528
514
  }
529
515
 
@@ -539,19 +525,15 @@ function ZustandModal({
539
525
  /*#__PURE__*/
540
526
  // The store fell out of the registry mid-look (a remote mirror
541
527
  // rebuilding, a dev reload). Say so rather than rendering nothing.
542
- (0, _jsxRuntime.jsxs)(_reactNative.View, {
543
- style: styles.emptyState,
544
- testID: `${_testIDs.ZUSTAND_TEST_ID}-store-detail-gone`,
545
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
546
- size: 32,
547
- color: _sharedUi.night.color.textTertiary
548
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
549
- style: styles.emptyTitle,
550
- children: "Store no longer registered"
551
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
552
- style: styles.emptyText,
553
- children: [selectedStoreName, " is not in the registry any more."]
554
- })]
528
+ (0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
529
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-store-detail-gone`
530
+ // ghost = "stale or detached: an unmounted component, an old
531
+ // snapshot" — exactly what the store you are looking at has become.
532
+ // No other tool has this cause, so it skips the resolver.
533
+ ,
534
+ otto: "ghost",
535
+ title: "Store no longer registered",
536
+ message: `${selectedStoreName} is not in the registry any more.`
555
537
  });
556
538
  }
557
539
 
@@ -605,13 +587,11 @@ function ZustandModal({
605
587
  keyExtractor: keyExtractor,
606
588
  contentContainerStyle: styles.listContent,
607
589
  showsVerticalScrollIndicator: true,
608
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
609
- initialNumToRender: 12,
610
- maxToRenderPerBatch: 8,
611
- windowSize: 5,
612
- updateCellsBatchingPeriod: 50
590
+ ..._sharedUi.EVENT_LIST_PRESET
613
591
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EventsEmptyState, {
614
- isEnabled: isEnabled
592
+ isEnabled: isEnabled,
593
+ searchText: searchText,
594
+ totalChanges: stats.totalChanges
615
595
  })]
616
596
  });
617
597
  };
@@ -720,27 +700,6 @@ const styles = _reactNative.StyleSheet.create({
720
700
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
721
701
  backgroundColor: "transparent"
722
702
  },
723
- headerSearchContainer: {
724
- flexDirection: "row",
725
- alignItems: "center",
726
- backgroundColor: _sharedUi.night.color.surfaceElevated,
727
- borderRadius: _sharedUi.night.radius.row,
728
- borderWidth: 1,
729
- borderColor: _sharedUi.night.color.border,
730
- paddingHorizontal: 12,
731
- paddingVertical: 5
732
- },
733
- headerSearchInput: {
734
- flex: 1,
735
- color: _sharedUi.night.color.text,
736
- fontSize: 13,
737
- marginLeft: 6,
738
- paddingVertical: 2
739
- },
740
- headerSearchClear: {
741
- marginLeft: 6,
742
- padding: 4
743
- },
744
703
  headerActionButton: {
745
704
  width: 32,
746
705
  height: 32,
@@ -779,23 +738,6 @@ const styles = _reactNative.StyleSheet.create({
779
738
  paddingTop: 8,
780
739
  paddingBottom: 20
781
740
  },
782
- emptyState: {
783
- alignItems: "center",
784
- paddingVertical: 40
785
- },
786
- emptyTitle: {
787
- color: _sharedUi.night.color.text,
788
- fontSize: 14,
789
- fontWeight: "600",
790
- marginTop: 12,
791
- marginBottom: 6
792
- },
793
- emptyText: {
794
- color: _sharedUi.night.color.textTertiary,
795
- fontSize: 12,
796
- textAlign: "center",
797
- lineHeight: 18
798
- },
799
741
  lockedBanner: {
800
742
  flexDirection: "row",
801
743
  alignItems: "center",
@@ -5,7 +5,6 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.ZustandStateChangeItem = void 0;
7
7
  var _react = require("react");
8
- var _reactNative = require("react-native");
9
8
  var _sharedUi = require("@buoy-gg/shared-ui");
10
9
  var _zustandStateStore = require("../utils/zustandStateStore");
11
10
  var _jsxRuntime = require("react/jsx-runtime");
@@ -103,68 +102,16 @@ const ZustandStateChangeItem = exports.ZustandStateChangeItem = /*#__PURE__*/(0,
103
102
  const sublabel = getSublabel(change);
104
103
  const primaryText = getPrimaryText(change);
105
104
  const badgeText = getBadgeText(change);
106
-
107
- // Always a custom badge, even when there is no icon to hang off it. The
108
- // alternative — letting CompactRow draw `badgeText` itself — leaves the one
109
- // row whose badge matters most (a set that changed NOTHING) as the only row
110
- // in the list with no handle on its badge.
111
- const customBadge = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
112
- style: styles.badgeContainer,
113
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
114
- style: [styles.badgeText, {
115
- color: statusColor
116
- }],
117
- testID: testID ? `${testID}-badge` : undefined,
118
- children: badgeText
119
- }), change.isSlowUpdate ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
120
- style: styles.slowBadge,
121
- testID: testID ? `${testID}-slow` : undefined,
122
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Clock, {
123
- size: 12,
124
- color: _sharedUi.buoyColors.error
125
- })
126
- }) : change.hasStateChange ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
127
- style: styles.changeBadge,
128
- testID: testID ? `${testID}-change` : undefined,
129
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
130
- size: 12,
131
- color: _sharedUi.buoyColors.warning
132
- })
133
- }) : null]
134
- });
135
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow, {
136
- statusDotColor: statusColor,
105
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChangeRow, {
106
+ statusColor: statusColor,
137
107
  statusLabel: statusLabel,
138
- statusSublabel: sublabel,
108
+ sublabel: sublabel,
139
109
  primaryText: primaryText,
140
- bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
141
- timestamp: change.timestamp
142
- }),
143
- customBadge: customBadge,
144
- badgeColor: statusColor,
145
- showChevron: true,
110
+ timestamp: change.timestamp,
111
+ badgeText: badgeText,
112
+ changed: change.hasStateChange,
113
+ slow: change.isSlowUpdate,
146
114
  onPress: () => onPress(change),
147
- testID: testID,
148
- statusTestID: testID ? `${testID}-status` : undefined,
149
- statusSublabelTestID: testID ? `${testID}-sublabel` : undefined,
150
- primaryTestID: testID ? `${testID}-keys` : undefined
115
+ testID: testID
151
116
  });
152
- });
153
- const styles = _reactNative.StyleSheet.create({
154
- badgeContainer: {
155
- flexDirection: "row",
156
- alignItems: "center",
157
- gap: 4
158
- },
159
- badgeText: {
160
- fontSize: 11,
161
- fontWeight: "600",
162
- fontFamily: "monospace"
163
- },
164
- changeBadge: {
165
- paddingHorizontal: 4
166
- },
167
- slowBadge: {
168
- paddingHorizontal: 4
169
- }
170
117
  });
@@ -79,29 +79,6 @@ function SectionHeader({
79
79
  }), badge]
80
80
  });
81
81
  }
82
- function InfoRow({
83
- label,
84
- value,
85
- valueColor,
86
- mono = false,
87
- testID
88
- }) {
89
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
90
- style: styles.infoRow,
91
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
92
- style: styles.infoLabel,
93
- children: label
94
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
95
- testID: testID,
96
- style: [styles.infoValue, mono && styles.infoValueMono, valueColor ? {
97
- color: valueColor
98
- } : undefined],
99
- numberOfLines: 1,
100
- ellipsizeMode: "middle",
101
- children: value
102
- })]
103
- });
104
- }
105
82
  function formatTime(timestamp) {
106
83
  const date = new Date(timestamp);
107
84
  const hours = date.getHours().toString().padStart(2, "0");
@@ -157,13 +134,13 @@ const ZustandStateInfoView = exports.ZustandStateInfoView = /*#__PURE__*/(0, _re
157
134
  title: "CHANGE INFO"
158
135
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
159
136
  style: styles.cardContent,
160
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
137
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InfoRow, {
161
138
  label: "Store",
162
139
  value: change.storeName,
163
140
  mono: true,
164
141
  valueColor: storeColor,
165
142
  testID: `${_testIDs.ZUSTAND_TEST_ID}-info-store`
166
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(InfoRow, {
143
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.InfoRow, {
167
144
  label: "Time",
168
145
  value: formatTime(change.timestamp),
169
146
  mono: true,
@@ -366,27 +343,6 @@ const styles = _reactNative.StyleSheet.create({
366
343
  padding: 14,
367
344
  gap: 10
368
345
  },
369
- infoRow: {
370
- flexDirection: "row",
371
- alignItems: "center",
372
- justifyContent: "space-between"
373
- },
374
- infoLabel: {
375
- fontSize: 11,
376
- color: _sharedUi.night.color.textTertiary,
377
- fontWeight: "500"
378
- },
379
- infoValue: {
380
- fontSize: 11,
381
- color: _sharedUi.night.color.text,
382
- fontWeight: "500",
383
- flex: 1,
384
- textAlign: "right",
385
- marginLeft: 12
386
- },
387
- infoValueMono: {
388
- fontFamily: "monospace"
389
- },
390
346
  badgeRow: {
391
347
  flexDirection: "row",
392
348
  flexWrap: "wrap",
@@ -44,36 +44,26 @@ function getKeysPreview(store) {
44
44
  }
45
45
  }
46
46
  function LoadingBrowserState() {
47
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
48
- style: styles.emptyState,
49
- testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-loading`,
50
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
51
- size: "small",
52
- color: _sharedUi.night.color.textTertiary
53
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
54
- style: styles.emptyTitle,
55
- children: "Loading stores\u2026"
56
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
57
- style: styles.emptyText,
58
- children: "Waiting for the device to send its registered stores."
59
- })]
47
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
48
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-loading`
49
+ // Replaces the spinner rather than joining it: two animated things
50
+ // stacked is clutter, and `loading` IS the indeterminate spinner in
51
+ // mascot form.
52
+ ,
53
+ otto: "loading",
54
+ title: "Loading stores\u2026",
55
+ message: "Waiting for the device to send its registered stores."
60
56
  });
61
57
  }
62
58
  function EmptyBrowserState() {
63
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
64
- style: styles.emptyState,
65
- testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-empty`,
66
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
67
- size: 32,
68
- color: _sharedUi.night.color.textTertiary
69
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
70
- style: styles.emptyTitle,
71
- testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-empty-title`,
72
- children: "No stores registered"
73
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
74
- style: styles.emptyText,
75
- children: "Use watchStores() to register your Zustand stores.\nThey will appear here with their current state."
76
- })]
59
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
60
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-empty`
61
+ // scanning = "scanning for targets… and finding none". The registry is
62
+ // discovered, not configured, so an empty one is a scan result.
63
+ ,
64
+ otto: "scanning",
65
+ title: "No stores registered",
66
+ message: "Use watchStores() to register your Zustand stores.\nThey will appear here with their current state."
77
67
  });
78
68
  }
79
69
  function ZustandStoreBrowser({
@@ -92,17 +82,19 @@ function ZustandStoreBrowser({
92
82
  return isLoading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(LoadingBrowserState, {}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyBrowserState, {});
93
83
  }
94
84
  if (filteredStores.length === 0 && searchQuery) {
95
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
96
- style: styles.emptyState,
85
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
97
86
  testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-no-match`,
98
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
99
- style: styles.emptyTitle,
100
- testID: `${_testIDs.ZUSTAND_TEST_ID}-stores-no-match-title`,
101
- children: "No matching stores"
102
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
103
- style: styles.emptyText,
104
- children: ["No stores match \"", searchQuery, "\""]
105
- })]
87
+ ...(0, _sharedUi.resolveListEmptyState)({
88
+ subject: "stores",
89
+ verb: "registered",
90
+ search: searchQuery,
91
+ total: stores.length,
92
+ waiting: {
93
+ otto: "scanning",
94
+ title: "No stores registered",
95
+ message: "Use watchStores() to register your Zustand stores."
96
+ }
97
+ })
106
98
  });
107
99
  }
108
100
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
@@ -208,22 +200,5 @@ const styles = _reactNative.StyleSheet.create({
208
200
  fontWeight: "700",
209
201
  color: _sharedUi.night.color.accent,
210
202
  fontFamily: "monospace"
211
- },
212
- emptyState: {
213
- alignItems: "center",
214
- paddingVertical: 40
215
- },
216
- emptyTitle: {
217
- color: _sharedUi.night.color.text,
218
- fontSize: 14,
219
- fontWeight: "600",
220
- marginTop: 12,
221
- marginBottom: 6
222
- },
223
- emptyText: {
224
- color: _sharedUi.night.color.textTertiary,
225
- fontSize: 12,
226
- textAlign: "center",
227
- lineHeight: 18
228
203
  }
229
204
  });