@buoy-gg/events 2.1.10 → 2.1.13

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 (28) hide show
  1. package/lib/commonjs/components/EventsCopySettingsView.js +3 -1
  2. package/lib/commonjs/components/ReactQueryEventDetail.js +7 -3
  3. package/lib/commonjs/components/UnifiedEventDetail.js +68 -2
  4. package/lib/commonjs/components/UnifiedEventItem.js +59 -1
  5. package/lib/commonjs/hooks/useUnifiedEvents.js +23 -1
  6. package/lib/commonjs/stores/unifiedEventStore.js +59 -1
  7. package/lib/commonjs/utils/autoDiscoverEventSources.js +146 -1
  8. package/lib/commonjs/utils/eventExportFormatter.js +2 -0
  9. package/lib/module/components/EventsCopySettingsView.js +3 -1
  10. package/lib/module/components/ReactQueryEventDetail.js +7 -3
  11. package/lib/module/components/UnifiedEventDetail.js +68 -2
  12. package/lib/module/components/UnifiedEventItem.js +59 -1
  13. package/lib/module/hooks/useUnifiedEvents.js +24 -2
  14. package/lib/module/stores/unifiedEventStore.js +54 -0
  15. package/lib/module/utils/autoDiscoverEventSources.js +146 -1
  16. package/lib/module/utils/eventExportFormatter.js +2 -0
  17. package/lib/typescript/stores/unifiedEventStore.d.ts +20 -0
  18. package/lib/typescript/types/index.d.ts +1 -1
  19. package/package.json +3 -3
  20. package/src/components/EventsCopySettingsView.tsx +3 -1
  21. package/src/components/ReactQueryEventDetail.tsx +7 -3
  22. package/src/components/UnifiedEventDetail.tsx +79 -1
  23. package/src/components/UnifiedEventItem.tsx +66 -0
  24. package/src/hooks/useUnifiedEvents.ts +28 -0
  25. package/src/stores/unifiedEventStore.ts +54 -0
  26. package/src/types/index.ts +3 -1
  27. package/src/utils/autoDiscoverEventSources.ts +165 -0
  28. package/src/utils/eventExportFormatter.ts +2 -0
@@ -48,6 +48,20 @@ interface OptionalDetailComponents {
48
48
  RenderDetailView: ComponentType<{
49
49
  render: unknown;
50
50
  }> | null;
51
+ ZustandStateDetailContent: ComponentType<{
52
+ change: unknown;
53
+ changes: unknown[];
54
+ selectedIndex: number;
55
+ onIndexChange: (index: number) => void;
56
+ disableInternalFooter?: boolean;
57
+ }> | null;
58
+ JotaiAtomDetailContent: ComponentType<{
59
+ change: unknown;
60
+ changes: unknown[];
61
+ selectedIndex: number;
62
+ onIndexChange: (index: number) => void;
63
+ disableInternalFooter?: boolean;
64
+ }> | null;
51
65
  }
52
66
 
53
67
  function tryLoadOptionalDetailComponents(): OptionalDetailComponents {
@@ -56,6 +70,8 @@ function tryLoadOptionalDetailComponents(): OptionalDetailComponents {
56
70
  ReduxActionDetailContent: null,
57
71
  NetworkEventDetailView: null,
58
72
  RenderDetailView: null,
73
+ ZustandStateDetailContent: null,
74
+ JotaiAtomDetailContent: null,
59
75
  };
60
76
 
61
77
  // Try to load storage detail component
@@ -94,6 +110,24 @@ function tryLoadOptionalDetailComponents(): OptionalDetailComponents {
94
110
  // Optional dependency not installed
95
111
  }
96
112
 
113
+ // Try to load zustand detail component
114
+ try {
115
+ // @ts-ignore - Dynamic import that may not exist
116
+ const zustand = require("@buoy-gg/zustand");
117
+ components.ZustandStateDetailContent = zustand.ZustandStateDetailContent;
118
+ } catch {
119
+ // Optional dependency not installed
120
+ }
121
+
122
+ // Try to load jotai detail component
123
+ try {
124
+ // @ts-ignore - Dynamic import that may not exist
125
+ const jotai = require("@buoy-gg/jotai");
126
+ components.JotaiAtomDetailContent = jotai.JotaiAtomDetailContent;
127
+ } catch {
128
+ // Optional dependency not installed
129
+ }
130
+
97
131
  return components;
98
132
  }
99
133
 
@@ -116,6 +150,8 @@ const SOURCE_CONFIG: Record<EventSource, { label: string; color: string }> = {
116
150
  "react-query-query": { label: "Query", color: "#EC4899" },
117
151
  "react-query-mutation": { label: "Mutation", color: "#F97316" },
118
152
  route: { label: "Route", color: "#06B6D4" },
153
+ zustand: { label: "Zustand", color: "#764ABC" },
154
+ jotai: { label: "Jotai", color: "#14B8A6" },
119
155
  render: { label: "Render", color: "#F472B6" },
120
156
  };
121
157
 
@@ -236,6 +272,42 @@ export const UnifiedEventDetail = memo(function UnifiedEventDetail({
236
272
  );
237
273
  }
238
274
 
275
+ // For Zustand events, use the shared ZustandStateDetailContent if available
276
+ if (event.source === "zustand" && optionalComponents.ZustandStateDetailContent) {
277
+ const { ZustandStateDetailContent } = optionalComponents;
278
+ const zustandChange = event.originalEvent;
279
+
280
+ return (
281
+ <View style={styles.container}>
282
+ <ZustandStateDetailContent
283
+ change={zustandChange}
284
+ changes={[zustandChange]}
285
+ selectedIndex={0}
286
+ onIndexChange={() => {}}
287
+ disableInternalFooter={true}
288
+ />
289
+ </View>
290
+ );
291
+ }
292
+
293
+ // For Jotai events, use the shared JotaiAtomDetailContent if available
294
+ if (event.source === "jotai" && optionalComponents.JotaiAtomDetailContent) {
295
+ const { JotaiAtomDetailContent } = optionalComponents;
296
+ const jotaiChange = event.originalEvent;
297
+
298
+ return (
299
+ <View style={styles.container}>
300
+ <JotaiAtomDetailContent
301
+ change={jotaiChange}
302
+ changes={[jotaiChange]}
303
+ selectedIndex={0}
304
+ onIndexChange={() => {}}
305
+ disableInternalFooter={true}
306
+ />
307
+ </View>
308
+ );
309
+ }
310
+
239
311
  // For render events, use the RenderDetailView if available
240
312
  if (event.source === "render" && optionalComponents.RenderDetailView) {
241
313
  const { RenderDetailView } = optionalComponents;
@@ -307,7 +379,13 @@ export const UnifiedEventDetail = memo(function UnifiedEventDetail({
307
379
  <View style={styles.dataSection}>
308
380
  <Text style={styles.sectionLabel}>Event Data</Text>
309
381
  <View style={styles.dataContainer}>
310
- <DataViewer data={getEventData()} title="" showTypeFilter={false} />
382
+ <DataViewer
383
+ data={getEventData()}
384
+ title=""
385
+ showTypeFilter={true}
386
+ rawMode={true}
387
+ initialExpanded={true}
388
+ />
311
389
  </View>
312
390
  </View>
313
391
  </ScrollView>
@@ -43,6 +43,14 @@ interface OptionalComponents {
43
43
  onPress: () => void;
44
44
  onNavigate?: (pathname: string) => void;
45
45
  }> | null;
46
+ ZustandStateChangeItem: ComponentType<{
47
+ change: unknown;
48
+ onPress: (change: unknown) => void;
49
+ }> | null;
50
+ JotaiAtomChangeItem: ComponentType<{
51
+ change: unknown;
52
+ onPress: (change: unknown) => void;
53
+ }> | null;
46
54
  }
47
55
 
48
56
  function tryLoadOptionalComponents(): OptionalComponents {
@@ -52,6 +60,8 @@ function tryLoadOptionalComponents(): OptionalComponents {
52
60
  ReduxActionItem: null,
53
61
  NetworkEventItemCompact: null,
54
62
  RouteEventItemCompact: null,
63
+ ZustandStateChangeItem: null,
64
+ JotaiAtomChangeItem: null,
55
65
  };
56
66
 
57
67
  // Try to load storage components
@@ -91,6 +101,24 @@ function tryLoadOptionalComponents(): OptionalComponents {
91
101
  // Optional dependency not installed
92
102
  }
93
103
 
104
+ // Try to load zustand components
105
+ try {
106
+ // @ts-ignore - Dynamic import that may not exist
107
+ const zustand = require("@buoy-gg/zustand");
108
+ components.ZustandStateChangeItem = zustand.ZustandStateChangeItem;
109
+ } catch {
110
+ // Optional dependency not installed
111
+ }
112
+
113
+ // Try to load jotai components
114
+ try {
115
+ // @ts-ignore - Dynamic import that may not exist
116
+ const jotai = require("@buoy-gg/jotai");
117
+ components.JotaiAtomChangeItem = jotai.JotaiAtomChangeItem;
118
+ } catch {
119
+ // Optional dependency not installed
120
+ }
121
+
94
122
  return components;
95
123
  }
96
124
 
@@ -140,6 +168,14 @@ const SOURCE_CONFIG: Record<
140
168
  label: "Route",
141
169
  color: "#06B6D4", // Cyan
142
170
  },
171
+ zustand: {
172
+ label: "Zustand",
173
+ color: "#764ABC", // Purple
174
+ },
175
+ jotai: {
176
+ label: "Jotai",
177
+ color: "#14B8A6", // Teal
178
+ },
143
179
  render: {
144
180
  label: "Render",
145
181
  color: "#F472B6", // Pink
@@ -220,6 +256,8 @@ const SOURCE_BADGE_LABELS: Record<EventSource, string> = {
220
256
  "react-query-query": "QUERY",
221
257
  "react-query-mutation": "MUTATION",
222
258
  route: "ROUTE",
259
+ zustand: "ZUSTAND",
260
+ jotai: "JOTAI",
223
261
  render: "RENDER",
224
262
  };
225
263
 
@@ -342,6 +380,34 @@ export const UnifiedEventItem = memo(function UnifiedEventItem({
342
380
  );
343
381
  }
344
382
 
383
+ // For Zustand events, try to use the shared ZustandStateChangeItem component
384
+ if (event.source === "zustand" && optionalComponents.ZustandStateChangeItem) {
385
+ const { ZustandStateChangeItem } = optionalComponents;
386
+
387
+ return (
388
+ <SourceBadgeWrapper source={event.source}>
389
+ <ZustandStateChangeItem
390
+ change={event.originalEvent}
391
+ onPress={() => onPress()}
392
+ />
393
+ </SourceBadgeWrapper>
394
+ );
395
+ }
396
+
397
+ // For Jotai events, try to use the shared JotaiAtomChangeItem component
398
+ if (event.source === "jotai" && optionalComponents.JotaiAtomChangeItem) {
399
+ const { JotaiAtomChangeItem } = optionalComponents;
400
+
401
+ return (
402
+ <SourceBadgeWrapper source={event.source}>
403
+ <JotaiAtomChangeItem
404
+ change={event.originalEvent}
405
+ onPress={() => onPress()}
406
+ />
407
+ </SourceBadgeWrapper>
408
+ );
409
+ }
410
+
345
411
  // Fallback: Use CompactRow for all events when specific components aren't available
346
412
  const customBadge =
347
413
  correlationLabel && correlationColor ? (
@@ -15,12 +15,16 @@ import {
15
15
  subscribeToNetwork,
16
16
  subscribeToReactQuery,
17
17
  subscribeToRoutes,
18
+ subscribeToZustand,
19
+ subscribeToJotai,
18
20
  subscribeToRender,
19
21
  unsubscribeFromStorage,
20
22
  unsubscribeFromRedux,
21
23
  unsubscribeFromNetwork,
22
24
  unsubscribeFromReactQuery,
23
25
  unsubscribeFromRoutes,
26
+ unsubscribeFromZustand,
27
+ unsubscribeFromJotai,
24
28
  unsubscribeFromRender,
25
29
  unsubscribeAll,
26
30
  getSourceCounts,
@@ -82,6 +86,8 @@ const ALL_DISPLAY_SOURCES: EventSource[] = [
82
86
  "react-query-query",
83
87
  "react-query-mutation",
84
88
  "route",
89
+ "zustand",
90
+ "jotai",
85
91
  "render",
86
92
  ];
87
93
 
@@ -106,6 +112,8 @@ const SOURCE_TO_EVENT_SOURCES: Record<EventSource, EventSource[]> = {
106
112
  "react-query-query": ["react-query", "react-query-query"],
107
113
  "react-query-mutation": ["react-query-mutation"],
108
114
  route: ["route"],
115
+ zustand: ["zustand"],
116
+ jotai: ["jotai"],
109
117
  render: ["render"],
110
118
  };
111
119
 
@@ -121,6 +129,8 @@ const EVENT_SOURCE_TO_DISCOVERY_ID: Record<EventSource, string> = {
121
129
  "react-query-query": "react-query",
122
130
  "react-query-mutation": "react-query",
123
131
  route: "route-events",
132
+ zustand: "zustand",
133
+ jotai: "jotai",
124
134
  render: "render",
125
135
  };
126
136
 
@@ -231,6 +241,8 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
231
241
  subscribeToReactQuery();
232
242
  }
233
243
  if (sourcesToEnable.has("route")) subscribeToRoutes();
244
+ if (sourcesToEnable.has("zustand")) subscribeToZustand();
245
+ if (sourcesToEnable.has("jotai")) subscribeToJotai();
234
246
  if (sourcesToEnable.has("render")) subscribeToRender();
235
247
  }
236
248
 
@@ -314,6 +326,8 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
314
326
  "react-query-query": "react-query",
315
327
  "react-query-mutation": "react-query",
316
328
  route: "route-events",
329
+ zustand: "zustand",
330
+ jotai: "jotai",
317
331
  render: "render",
318
332
  };
319
333
 
@@ -370,6 +384,12 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
370
384
  case "route":
371
385
  unsubscribeFromRoutes();
372
386
  break;
387
+ case "zustand":
388
+ unsubscribeFromZustand();
389
+ break;
390
+ case "jotai":
391
+ unsubscribeFromJotai();
392
+ break;
373
393
  case "render":
374
394
  unsubscribeFromRender();
375
395
  break;
@@ -395,6 +415,12 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
395
415
  case "route":
396
416
  subscribeToRoutes();
397
417
  break;
418
+ case "zustand":
419
+ subscribeToZustand();
420
+ break;
421
+ case "jotai":
422
+ subscribeToJotai();
423
+ break;
398
424
  case "render":
399
425
  subscribeToRender();
400
426
  break;
@@ -423,6 +449,8 @@ export function useUnifiedEvents(): UseUnifiedEventsResult {
423
449
  subscribeToReactQuery();
424
450
  }
425
451
  if (enabledSources.has("route")) subscribeToRoutes();
452
+ if (enabledSources.has("zustand")) subscribeToZustand();
453
+ if (enabledSources.has("jotai")) subscribeToJotai();
426
454
  if (enabledSources.has("render")) subscribeToRender();
427
455
  setIsCapturing(true);
428
456
  }, [enabledSources]);
@@ -195,6 +195,44 @@ class UnifiedEventStore {
195
195
  this.activeSources.delete("route");
196
196
  }
197
197
 
198
+ /**
199
+ * Subscribe to Zustand state changes (if @buoy-gg/zustand is installed)
200
+ */
201
+ subscribeToZustand(): void {
202
+ const { sources } = getCachedDiscovery();
203
+ const zustandSource = sources.find((s) => s.id === "zustand");
204
+ if (zustandSource) {
205
+ this.subscribeToSource(zustandSource);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Unsubscribe from Zustand state changes
211
+ */
212
+ unsubscribeFromZustand(): void {
213
+ this.unsubscribeFromSource("zustand");
214
+ this.activeSources.delete("zustand");
215
+ }
216
+
217
+ /**
218
+ * Subscribe to Jotai atom changes (if @buoy-gg/jotai is installed)
219
+ */
220
+ subscribeToJotai(): void {
221
+ const { sources } = getCachedDiscovery();
222
+ const jotaiSource = sources.find((s) => s.id === "jotai");
223
+ if (jotaiSource) {
224
+ this.subscribeToSource(jotaiSource);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Unsubscribe from Jotai atom changes
230
+ */
231
+ unsubscribeFromJotai(): void {
232
+ this.unsubscribeFromSource("jotai");
233
+ this.activeSources.delete("jotai");
234
+ }
235
+
198
236
  /**
199
237
  * Subscribe to render events (if @buoy-gg/highlight-updates is installed)
200
238
  */
@@ -283,6 +321,8 @@ class UnifiedEventStore {
283
321
  "react-query-query": 0,
284
322
  "react-query-mutation": 0,
285
323
  route: 0,
324
+ zustand: 0,
325
+ jotai: 0,
286
326
  render: 0,
287
327
  };
288
328
 
@@ -366,6 +406,10 @@ class UnifiedEventStore {
366
406
  return this.sourceUnsubscribers.has("react-query");
367
407
  case "route":
368
408
  return this.sourceUnsubscribers.has("route-events");
409
+ case "zustand":
410
+ return this.sourceUnsubscribers.has("zustand");
411
+ case "jotai":
412
+ return this.sourceUnsubscribers.has("jotai");
369
413
  case "render":
370
414
  return this.sourceUnsubscribers.has("render");
371
415
  default:
@@ -386,6 +430,8 @@ class UnifiedEventStore {
386
430
  "react-query-query": this.sourceUnsubscribers.has("react-query"),
387
431
  "react-query-mutation": this.sourceUnsubscribers.has("react-query"),
388
432
  route: this.sourceUnsubscribers.has("route-events"),
433
+ zustand: this.sourceUnsubscribers.has("zustand"),
434
+ jotai: this.sourceUnsubscribers.has("jotai"),
389
435
  render: this.sourceUnsubscribers.has("render"),
390
436
  };
391
437
  }
@@ -423,6 +469,14 @@ export const subscribeToRoutes = () =>
423
469
  unifiedEventStore.subscribeToRoutes();
424
470
  export const unsubscribeFromRoutes = () =>
425
471
  unifiedEventStore.unsubscribeFromRoutes();
472
+ export const subscribeToZustand = () =>
473
+ unifiedEventStore.subscribeToZustand();
474
+ export const unsubscribeFromZustand = () =>
475
+ unifiedEventStore.unsubscribeFromZustand();
476
+ export const subscribeToJotai = () =>
477
+ unifiedEventStore.subscribeToJotai();
478
+ export const unsubscribeFromJotai = () =>
479
+ unifiedEventStore.unsubscribeFromJotai();
426
480
  export const subscribeToRender = () =>
427
481
  unifiedEventStore.subscribeToRender();
428
482
  export const unsubscribeFromRender = () =>
@@ -16,7 +16,9 @@ export type EventSource =
16
16
  | "react-query-query"
17
17
  | "react-query-mutation"
18
18
  | "route"
19
- | "render";
19
+ | "render"
20
+ | "zustand"
21
+ | "jotai";
20
22
 
21
23
  /**
22
24
  * Event status for visual indicators
@@ -596,6 +596,167 @@ function transformRouteEvent(event: unknown): UnifiedEvent {
596
596
  };
597
597
  }
598
598
 
599
+ // ============================================================================
600
+ // Zustand Event Source Discovery
601
+ // ============================================================================
602
+
603
+ function tryLoadZustandSource(): DiscoveredEventSource | null {
604
+ try {
605
+ // @ts-ignore - Dynamic import that may not exist
606
+ const { zustandStateStore } = require("@buoy-gg/zustand");
607
+
608
+ let lastChangeId: string | null = null;
609
+
610
+ return {
611
+ id: "zustand",
612
+ name: "Zustand",
613
+ eventSources: ["zustand"],
614
+ available: true,
615
+ subscribe: (onEvent) => {
616
+ return zustandStateStore.subscribe((changes: unknown[]) => {
617
+ if (changes.length > 0) {
618
+ const latestChange = changes[0] as { id: string };
619
+ if (latestChange.id !== lastChangeId) {
620
+ lastChangeId = latestChange.id;
621
+ const unifiedEvent = transformZustandChange(latestChange);
622
+ onEvent(unifiedEvent);
623
+ }
624
+ }
625
+ });
626
+ },
627
+ };
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+
633
+ function transformZustandChange(change: unknown): UnifiedEvent {
634
+ const c = change as {
635
+ id: string;
636
+ storeName: string;
637
+ timestamp: number;
638
+ hasStateChange?: boolean;
639
+ category?: "setState" | "replace";
640
+ changedKeys?: string[];
641
+ changedKeysCount?: number;
642
+ diffSummary?: string;
643
+ partialPreview?: string;
644
+ duration?: number;
645
+ isSlowUpdate?: boolean;
646
+ };
647
+
648
+ // Get status
649
+ let status: "success" | "error" | "pending" | "neutral" = "neutral";
650
+ if (c.isSlowUpdate) {
651
+ status = "pending"; // Yellow for slow updates
652
+ } else if (c.hasStateChange) {
653
+ status = "success";
654
+ }
655
+
656
+ // Get subtitle
657
+ const parts: string[] = [];
658
+ if (c.changedKeys && c.changedKeys.length > 0) {
659
+ const keys = c.changedKeys.slice(0, 3).join(", ");
660
+ const more = c.changedKeys.length > 3 ? ` +${c.changedKeys.length - 3}` : "";
661
+ parts.push(`${keys}${more}`);
662
+ } else if (c.diffSummary) {
663
+ parts.push(c.diffSummary);
664
+ }
665
+ if (c.duration !== undefined) {
666
+ parts.push(`${c.duration.toFixed(1)}ms`);
667
+ }
668
+ if (c.category === "replace") {
669
+ parts.push("replace");
670
+ }
671
+
672
+ return {
673
+ id: generateEventId("zustand"),
674
+ source: "zustand",
675
+ timestamp: c.timestamp,
676
+ title: c.storeName,
677
+ subtitle: parts.join(" · ") || (c.hasStateChange ? "state changed" : "no change"),
678
+ status,
679
+ originalEvent: change,
680
+ };
681
+ }
682
+
683
+ // ============================================================================
684
+ // Jotai Event Source Discovery
685
+ // ============================================================================
686
+
687
+ function tryLoadJotaiSource(): DiscoveredEventSource | null {
688
+ try {
689
+ // @ts-ignore - Dynamic import that may not exist
690
+ const { jotaiStateStore } = require("@buoy-gg/jotai");
691
+
692
+ let lastChangeId: string | null = null;
693
+
694
+ return {
695
+ id: "jotai",
696
+ name: "Jotai",
697
+ eventSources: ["jotai"],
698
+ available: true,
699
+ subscribe: (onEvent) => {
700
+ return jotaiStateStore.subscribe((changes: unknown[]) => {
701
+ if (changes.length > 0) {
702
+ const latestChange = changes[0] as { id: string };
703
+ if (latestChange.id !== lastChangeId) {
704
+ lastChangeId = latestChange.id;
705
+ const unifiedEvent = transformJotaiChange(latestChange);
706
+ onEvent(unifiedEvent);
707
+ }
708
+ }
709
+ });
710
+ },
711
+ };
712
+ } catch {
713
+ return null;
714
+ }
715
+ }
716
+
717
+ function transformJotaiChange(change: unknown): UnifiedEvent {
718
+ const c = change as {
719
+ id: string;
720
+ atomLabel: string;
721
+ timestamp: number;
722
+ hasValueChange?: boolean;
723
+ category?: "write" | "read";
724
+ changedKeys?: string[];
725
+ changedKeysCount?: number;
726
+ diffSummary?: string;
727
+ valuePreview?: string;
728
+ };
729
+
730
+ // Get status
731
+ let status: "success" | "error" | "pending" | "neutral" = "neutral";
732
+ if (c.category === "write" && c.hasValueChange) {
733
+ status = "success";
734
+ } else if (c.category === "read") {
735
+ status = "neutral";
736
+ }
737
+
738
+ // Get subtitle
739
+ const parts: string[] = [];
740
+ if (c.valuePreview) {
741
+ parts.push(c.valuePreview);
742
+ } else if (c.diffSummary) {
743
+ parts.push(c.diffSummary);
744
+ }
745
+ if (c.category) {
746
+ parts.push(c.category);
747
+ }
748
+
749
+ return {
750
+ id: generateEventId("jotai"),
751
+ source: "jotai",
752
+ timestamp: c.timestamp,
753
+ title: c.atomLabel,
754
+ subtitle: parts.join(" · ") || (c.hasValueChange ? "value changed" : "no change"),
755
+ status,
756
+ originalEvent: change,
757
+ };
758
+ }
759
+
599
760
  // ============================================================================
600
761
  // Render Events Source Discovery (Highlight Updates)
601
762
  // ============================================================================
@@ -776,6 +937,8 @@ export function autoDiscoverEventSources(): DiscoveryResult {
776
937
  tryLoadNetworkSource,
777
938
  tryLoadReactQuerySource,
778
939
  tryLoadRouteEventsSource,
940
+ tryLoadZustandSource,
941
+ tryLoadJotaiSource,
779
942
  tryLoadHighlightUpdatesSource,
780
943
  ];
781
944
 
@@ -808,6 +971,8 @@ export function getSourceDisplayConfig(source: EventSource): {
808
971
  "react-query-query": { label: "Query", color: "#EC4899", icon: "sync-outline" },
809
972
  "react-query-mutation": { label: "Mutation", color: "#F97316", icon: "flash-outline" },
810
973
  route: { label: "Route", color: "#06B6D4", icon: "navigate-outline" },
974
+ zustand: { label: "Zustand", color: "#764ABC", icon: "cube-outline" },
975
+ jotai: { label: "Jotai", color: "#14B8A6", icon: "ellipse-outline" },
811
976
  render: { label: "Render", color: "#F472B6", icon: "layers-outline" },
812
977
  };
813
978
 
@@ -53,6 +53,8 @@ const SOURCE_LABELS: Record<EventSource, string> = {
53
53
  "react-query-query": "Query",
54
54
  "react-query-mutation": "Mutation",
55
55
  route: "Route",
56
+ zustand: "Zustand",
57
+ jotai: "Jotai",
56
58
  render: "Render",
57
59
  };
58
60