@buoy-gg/zustand 2.1.15 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/commonjs/index.js +98 -1
  2. package/lib/commonjs/preset.js +102 -1
  3. package/lib/commonjs/zustand/components/ZustandActionButton.js +116 -1
  4. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +134 -1
  5. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +291 -1
  6. package/lib/commonjs/zustand/components/ZustandIcon.js +35 -1
  7. package/lib/commonjs/zustand/components/ZustandModal.js +603 -1
  8. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +165 -1
  9. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +352 -1
  10. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +508 -1
  11. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +307 -1
  12. package/lib/commonjs/zustand/components/index.js +73 -1
  13. package/lib/commonjs/zustand/hooks/index.js +12 -1
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +92 -1
  15. package/lib/commonjs/zustand/index.js +99 -1
  16. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +48 -0
  17. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +220 -1
  18. package/lib/commonjs/zustand/utils/index.js +31 -1
  19. package/lib/commonjs/zustand/utils/zustandStateStore.js +457 -1
  20. package/lib/module/index.js +85 -1
  21. package/lib/module/preset.js +98 -1
  22. package/lib/module/zustand/components/ZustandActionButton.js +112 -1
  23. package/lib/module/zustand/components/ZustandDetailViewToggle.js +129 -1
  24. package/lib/module/zustand/components/ZustandEventFilterView.js +287 -1
  25. package/lib/module/zustand/components/ZustandIcon.js +32 -1
  26. package/lib/module/zustand/components/ZustandModal.js +599 -1
  27. package/lib/module/zustand/components/ZustandStateChangeItem.js +161 -1
  28. package/lib/module/zustand/components/ZustandStateDetailContent.js +348 -1
  29. package/lib/module/zustand/components/ZustandStateInfoView.js +503 -1
  30. package/lib/module/zustand/components/ZustandStoreBrowser.js +303 -1
  31. package/lib/module/zustand/components/index.js +10 -1
  32. package/lib/module/zustand/hooks/index.js +3 -1
  33. package/lib/module/zustand/hooks/useZustandStateChanges.js +88 -1
  34. package/lib/module/zustand/index.js +12 -1
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +44 -0
  36. package/lib/module/zustand/utils/buoyZustandMiddleware.js +214 -1
  37. package/lib/module/zustand/utils/index.js +4 -1
  38. package/lib/module/zustand/utils/zustandStateStore.js +453 -1
  39. package/lib/typescript/index.d.ts +2 -1
  40. package/lib/typescript/index.d.ts.map +1 -0
  41. package/lib/typescript/preset.d.ts.map +1 -0
  42. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
  43. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
  44. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
  45. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
  46. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
  47. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
  48. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
  49. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
  50. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
  51. package/lib/typescript/zustand/components/index.d.ts.map +1 -0
  52. package/lib/typescript/zustand/hooks/index.d.ts.map +1 -0
  53. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
  54. package/lib/typescript/zustand/index.d.ts.map +1 -0
  55. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +26 -0
  56. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
  57. package/lib/typescript/zustand/types/index.d.ts +12 -0
  58. package/lib/typescript/zustand/types/index.d.ts.map +1 -0
  59. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
  60. package/lib/typescript/zustand/utils/index.d.ts.map +1 -0
  61. package/lib/typescript/zustand/utils/zustandStateStore.d.ts +35 -1
  62. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
  63. package/package.json +3 -3
@@ -1 +1,214 @@
1
- "use strict";import{zustandStateStore}from"./zustandStateStore";const WATCHED_SYMBOL=Symbol.for("@@buoy-zustand/watched");function detectPersist(t){const e=t.persist;if(e&&"function"==typeof e.getOptions){const t=e.getOptions();return{isPersisted:!0,persistName:t?.name}}return{isPersisted:!1}}export function watchStores(t){const e=[];for(const[s,r]of Object.entries(t)){const t=r;if(t[WATCHED_SYMBOL])continue;t[WATCHED_SYMBOL]=!0;const{isPersisted:n,persistName:a}=detectPersist(r);zustandStateStore.registerStore(s,{getState:r.getState,getInitialState:r.getInitialState,setState:r.setState,subscribe:r.subscribe},{isPersisted:n,persistName:a});const i=r.subscribe((t,e)=>{try{zustandStateStore.addStateChange({storeName:s,partial:void 0,replace:!1,prevState:e,nextState:t,duration:void 0,isPersisted:n})}catch{}});e.push(()=>{i(),delete r[WATCHED_SYMBOL],zustandStateStore.unregisterStore(s)})}return()=>{e.forEach(t=>t())}}let unnamedCounter=0;export function buoyDevTools(t,e){return(s,r,n)=>{const a=e?.name??"store-"+ ++unnamedCounter,i=!1!==e?.enabled,o=t((t,e)=>{if(!i)return s(t,e);const n=performance.now(),o=r();s(t,e);const S=r(),u=performance.now()-n,c="function"==typeof t?"(updater fn)":t;zustandStateStore.addStateChange({storeName:a,partial:c,replace:e??!1,prevState:o,nextState:S,duration:u,isPersisted:!1})},r,n);return setTimeout(()=>{if(n[WATCHED_SYMBOL])return;n[WATCHED_SYMBOL]=!0;const{isPersisted:t,persistName:e}=detectPersist(n);zustandStateStore.registerStore(a,{getState:r,getInitialState:n.getInitialState,setState:s,subscribe:n.subscribe},{isPersisted:t,persistName:e})},0),o}}export function isStoreInstrumented(t){return!0===t[WATCHED_SYMBOL]}
1
+ "use strict";
2
+
3
+ /**
4
+ * Buoy Zustand DevTools — Store instrumentation
5
+ *
6
+ * Two approaches, from easiest to most detailed:
7
+ *
8
+ * 1. watchStores() — RECOMMENDED. One line, zero store modifications.
9
+ * Uses store.subscribe() externally. Never touches setState.
10
+ * Safe to add/remove — cannot break your stores even if our code has bugs.
11
+ *
12
+ * 2. buoyDevTools() — Opt-in middleware for advanced use.
13
+ * Wraps setState to capture the partial argument and timing.
14
+ * Requires modifying each store's create() call.
15
+ */
16
+
17
+ import { zustandStateStore } from "./zustandStateStore";
18
+ /** Symbol to mark stores that have been watched/instrumented */
19
+ const WATCHED_SYMBOL = Symbol.for("@@buoy-zustand/watched");
20
+
21
+ /**
22
+ * Detect if a store is using the persist middleware by checking for persist API
23
+ */
24
+ function detectPersist(store) {
25
+ const persist = store.persist;
26
+ if (persist && typeof persist.getOptions === "function") {
27
+ const options = persist.getOptions();
28
+ return {
29
+ isPersisted: true,
30
+ persistName: options?.name
31
+ };
32
+ }
33
+ return {
34
+ isPersisted: false
35
+ };
36
+ }
37
+
38
+ // =============================================================================
39
+ // watchStores — Primary, non-intrusive approach
40
+ // =============================================================================
41
+
42
+ /** Minimal store shape — what Zustand's create() returns */
43
+
44
+ /**
45
+ * Watch multiple Zustand stores for state changes.
46
+ *
47
+ * This is the recommended, non-intrusive approach. It uses each store's
48
+ * `.subscribe()` method to observe changes from the outside — it never
49
+ * modifies `setState` or any store internals.
50
+ *
51
+ * **Safe by design:** Even if our listener code has a bug, it cannot break
52
+ * your stores. All listener code is wrapped in try/catch.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * // In your _layout.tsx or App.tsx — ONE line:
57
+ * import { watchStores } from '@buoy-gg/zustand';
58
+ * import { useCounterStore } from './stores/counter';
59
+ * import { useAuthStore } from './stores/auth';
60
+ * import { useCartStore } from './stores/cart';
61
+ *
62
+ * watchStores({
63
+ * counterStore: useCounterStore,
64
+ * authStore: useAuthStore,
65
+ * cartStore: useCartStore,
66
+ * });
67
+ * ```
68
+ *
69
+ * @param stores - Object mapping store names to Zustand store hooks
70
+ * @returns Cleanup function that removes all subscriptions
71
+ */
72
+ export function watchStores(stores) {
73
+ const cleanups = [];
74
+ for (const [name, store] of Object.entries(stores)) {
75
+ // Skip if already watched
76
+ const storeAny = store;
77
+ if (storeAny[WATCHED_SYMBOL]) continue;
78
+ storeAny[WATCHED_SYMBOL] = true;
79
+ const {
80
+ isPersisted,
81
+ persistName
82
+ } = detectPersist(store);
83
+
84
+ // Register the store for overview
85
+ zustandStateStore.registerStore(name, {
86
+ getState: store.getState,
87
+ getInitialState: store.getInitialState,
88
+ setState: store.setState,
89
+ subscribe: store.subscribe
90
+ }, {
91
+ isPersisted,
92
+ persistName
93
+ });
94
+
95
+ // Subscribe to state changes — completely external, never touches setState
96
+ const unsubscribe = store.subscribe((state, prevState) => {
97
+ try {
98
+ zustandStateStore.addStateChange({
99
+ storeName: name,
100
+ partial: undefined,
101
+ // Not available in subscribe-only mode
102
+ replace: false,
103
+ prevState,
104
+ nextState: state,
105
+ duration: undefined,
106
+ // Not available in subscribe-only mode
107
+ isPersisted
108
+ });
109
+ } catch {
110
+ // Silently catch — our bug must never propagate into the store
111
+ }
112
+ });
113
+ cleanups.push(() => {
114
+ unsubscribe();
115
+ delete store[WATCHED_SYMBOL];
116
+ zustandStateStore.unregisterStore(name);
117
+ });
118
+ }
119
+ return () => {
120
+ cleanups.forEach(fn => fn());
121
+ };
122
+ }
123
+
124
+ // =============================================================================
125
+ // buoyDevTools — Opt-in middleware (advanced, more detail)
126
+ // =============================================================================
127
+
128
+ /** Auto-incrementing store name counter for unnamed stores */
129
+ let unnamedCounter = 0;
130
+
131
+ /**
132
+ * Zustand middleware that instruments a store for Buoy DevTools.
133
+ *
134
+ * This is the advanced approach — it wraps setState to capture:
135
+ * - The partial argument passed to setState
136
+ * - Precise timing of each setState call
137
+ *
138
+ * Use this when you want maximum detail. For most cases, `watchStores()`
139
+ * is simpler and safer.
140
+ *
141
+ * @example
142
+ * ```tsx
143
+ * import { create } from 'zustand';
144
+ * import { buoyDevTools } from '@buoy-gg/zustand';
145
+ *
146
+ * const useCounterStore = create(
147
+ * buoyDevTools(
148
+ * (set) => ({
149
+ * count: 0,
150
+ * increment: () => set((s) => ({ count: s.count + 1 })),
151
+ * }),
152
+ * { name: 'counterStore' }
153
+ * )
154
+ * );
155
+ * ```
156
+ */
157
+ export function buoyDevTools(config, options) {
158
+ return (set, get, api) => {
159
+ const storeName = options?.name ?? `store-${++unnamedCounter}`;
160
+ const enabled = options?.enabled !== false;
161
+ const instrumentedSet = (partial, replace) => {
162
+ if (!enabled) {
163
+ return set(partial, replace);
164
+ }
165
+ const startTime = performance.now();
166
+ const prevState = get();
167
+ set(partial, replace);
168
+ const nextState = get();
169
+ const duration = performance.now() - startTime;
170
+ const resolvedPartial = typeof partial === "function" ? "(updater fn)" : partial;
171
+ zustandStateStore.addStateChange({
172
+ storeName,
173
+ partial: resolvedPartial,
174
+ replace: replace ?? false,
175
+ prevState,
176
+ nextState,
177
+ duration,
178
+ isPersisted: false
179
+ });
180
+ };
181
+ const result = config(instrumentedSet, get, api);
182
+
183
+ // Register the store after creation
184
+ setTimeout(() => {
185
+ if (api[WATCHED_SYMBOL]) return;
186
+ api[WATCHED_SYMBOL] = true;
187
+ const {
188
+ isPersisted,
189
+ persistName
190
+ } = detectPersist(api);
191
+ zustandStateStore.registerStore(storeName, {
192
+ getState: get,
193
+ getInitialState: api.getInitialState,
194
+ setState: set,
195
+ subscribe: api.subscribe
196
+ }, {
197
+ isPersisted,
198
+ persistName
199
+ });
200
+ }, 0);
201
+ return result;
202
+ };
203
+ }
204
+
205
+ // =============================================================================
206
+ // Utility
207
+ // =============================================================================
208
+
209
+ /**
210
+ * Check if a store is being watched/instrumented
211
+ */
212
+ export function isStoreInstrumented(store) {
213
+ return store[WATCHED_SYMBOL] === true;
214
+ }
@@ -1 +1,4 @@
1
- "use strict";export{zustandStateStore}from"./zustandStateStore";export{watchStores,buoyDevTools,isStoreInstrumented}from"./buoyZustandMiddleware";
1
+ "use strict";
2
+
3
+ export { zustandStateStore } from "./zustandStateStore";
4
+ export { watchStores, buoyDevTools, isStoreInstrumented } from "./buoyZustandMiddleware";
@@ -1 +1,453 @@
1
- "use strict";const STORE_COLORS={counter:"#10B981",auth:"#8B5CF6",user:"#3B82F6",cart:"#EC4899",app:"#6366F1",ui:"#F59E0B",settings:"#14B8A6",theme:"#06B6D4",navigation:"#F97316",form:"#EF4444"};function getStoreColor(e){const t=e.toLowerCase();if(STORE_COLORS[t])return STORE_COLORS[t];for(const[e,s]of Object.entries(STORE_COLORS))if(t.includes(e))return s;const s=137*e.split("").reduce((e,t)=>e+t.charCodeAt(0),0)%360,n=.7*(1-Math.abs(1.2-1)),r=n*(1-Math.abs(s/60%2-1)),a=.6-n/2;let i=0,o=0,h=0;s<60?(i=n,o=r):s<120?(i=r,o=n):s<180?(o=n,h=r):s<240?(o=r,h=n):s<300?(i=r,h=n):(i=n,h=r);const g=e=>Math.round(255*(e+a)).toString(16).padStart(2,"0");return`#${g(i)}${g(o)}${g(h)}`}function formatPartialPreview(e,t=40){if(void 0===e)return"";if(null===e)return"null";try{if("function"==typeof e)return"(updater fn)";if("string"==typeof e)return e.length>t?`"${e.slice(0,t-3)}..."`:`"${e}"`;if("number"==typeof e||"boolean"==typeof e)return String(e);if(Array.isArray(e)){if(0===e.length)return"[]";const s=JSON.stringify(e);return s.length>t?`[${e.length} items]`:s}if("object"==typeof e){const s=Object.keys(e);if(0===s.length)return"{}";const n=JSON.stringify(e);return n.length<=t?n:`{ ${s.length} keys }`}return String(e).slice(0,t)}catch{return"[complex]"}}function getStateDiffSummary(e,t){if(e===t)return{summary:"no change",changedKeys:[],changedCount:0};if("object"!=typeof e||"object"!=typeof t||null===e||null===t)return{summary:"changed",changedKeys:[],changedCount:1};const s=Object.keys(e),n=Object.keys(t),r=n.filter(e=>!s.includes(e)),a=s.filter(e=>!n.includes(e)),i=[];for(const r of s)n.includes(r)&&e[r]!==t[r]&&i.push(r);const o=[...r,...a,...i],h=[];r.length>0&&h.push(`+${r.length}`),a.length>0&&h.push(`-${a.length}`),i.length>0&&h.push(`~${i.length}`);const g=o.length;return 0===h.length?{summary:"nested change",changedKeys:[],changedCount:1}:{summary:`${h.join(" ")} ${1===g?"key":"keys"}`,changedKeys:o,changedCount:g}}class ZustandStateStore{stateChanges=[];stores=new Map;listeners=new Set;storeListeners=new Set;maxChanges=200;idCounter=0;isEnabled=!0;addStateChange(e){if(!this.isEnabled)return;const{storeName:t,partial:s,replace:n,prevState:r,nextState:a,duration:i,category:o,isPersisted:h=!1}=e,g=r!==a,{summary:u,changedKeys:l,changedCount:c}=getStateDiffSummary(r,a),d=formatPartialPreview(s),f=(i??0)>16,C=o??(n?"replace":"setState"),S={id:`${Date.now()}-${++this.idCounter}`,storeName:t,timestamp:Date.now(),prevState:r,nextState:a,partial:s,duration:i,hasStateChange:g,replace:n,category:C,changedKeys:l,changedKeysCount:c,diffSummary:u,partialPreview:d,isSlowUpdate:f,isPersisted:h};this.stateChanges=[S,...this.stateChanges].slice(0,this.maxChanges);const y=this.stores.get(t);y&&y.stateChangeCount++,this.notifyListeners()}getStateChanges(){return[...this.stateChanges]}getStateChangeById(e){return this.stateChanges.find(t=>t.id===e)}clearStateChanges(){this.stateChanges=[];for(const e of this.stores.values())e.stateChangeCount=0;this.notifyListeners()}registerStore(e,t,s){if(this.stores.has(e))return;const n={name:e,api:t,stateChangeCount:0,isPersisted:s?.isPersisted??!1,persistName:s?.persistName,color:getStoreColor(e)};this.stores.set(e,n),this.notifyStoreListeners()}unregisterStore(e){this.stores.delete(e),this.notifyStoreListeners()}getStores(){return Array.from(this.stores.values())}getStore(e){return this.stores.get(e)}getStoreColor(e){return this.stores.get(e)?.color??getStoreColor(e)}filterStateChanges(e){let t=[...this.stateChanges];if(e.searchText){const s=e.searchText.toLowerCase();t=t.filter(e=>e.storeName.toLowerCase().includes(s)||e.partialPreview.toLowerCase().includes(s)||e.changedKeys.some(e=>e.toLowerCase().includes(s)))}return e.storeNames&&e.storeNames.length>0&&(t=t.filter(t=>e.storeNames.includes(t.storeName))),e.onlyWithChanges&&(t=t.filter(e=>e.hasStateChange)),t}getStats(){const e=this.stateChanges.length,t=this.stateChanges.filter(e=>e.hasStateChange).length,s=this.stores.size,n=this.stateChanges.filter(e=>void 0!==e.duration).map(e=>e.duration),r=n.length>0?n.reduce((e,t)=>e+t,0)/n.length:0;return{totalChanges:e,changesWithStateChange:t,changesWithoutStateChange:e-t,storeCount:s,averageDuration:Math.round(100*r)/100}}getUniqueStoreNames(){return Array.from(this.stores.keys()).sort()}setEnabled(e){this.isEnabled=e}getEnabled(){return this.isEnabled}setMaxChanges(e){this.maxChanges=e,this.stateChanges.length>e&&(this.stateChanges=this.stateChanges.slice(0,e),this.notifyListeners())}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}subscribeToStores(e){return this.storeListeners.add(e),()=>{this.storeListeners.delete(e)}}notifyListeners(){const e=this.getStateChanges();this.listeners.forEach(t=>t(e))}notifyStoreListeners(){const e=this.getStores();this.storeListeners.forEach(t=>t(e))}}export const zustandStateStore=new ZustandStateStore;
1
+ "use strict";
2
+
3
+ /**
4
+ * Zustand state store - captures and stores Zustand state changes
5
+ *
6
+ * Mirrors the architecture of reduxActionStore.ts from @buoy-gg/redux
7
+ */
8
+
9
+ // ============================================
10
+ // Store Color Palette
11
+ // ============================================
12
+
13
+ const STORE_COLORS = {
14
+ counter: "#10B981",
15
+ // emerald
16
+ auth: "#8B5CF6",
17
+ // purple
18
+ user: "#3B82F6",
19
+ // blue
20
+ cart: "#EC4899",
21
+ // pink
22
+ app: "#6366F1",
23
+ // indigo
24
+ ui: "#F59E0B",
25
+ // amber
26
+ settings: "#14B8A6",
27
+ // teal
28
+ theme: "#06B6D4",
29
+ // cyan
30
+ navigation: "#F97316",
31
+ // orange
32
+ form: "#EF4444" // red
33
+ };
34
+
35
+ /**
36
+ * Get consistent color for a store based on its name
37
+ */
38
+ function getStoreColor(storeName) {
39
+ const lowerName = storeName.toLowerCase();
40
+
41
+ // Check for exact match
42
+ if (STORE_COLORS[lowerName]) {
43
+ return STORE_COLORS[lowerName];
44
+ }
45
+
46
+ // Check for partial matches
47
+ for (const [key, color] of Object.entries(STORE_COLORS)) {
48
+ if (lowerName.includes(key)) {
49
+ return color;
50
+ }
51
+ }
52
+
53
+ // Generate consistent hex color from name hash
54
+ const hash = storeName.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
55
+ const hue = hash * 137 % 360;
56
+ // Convert HSL to hex so "color + alpha_hex" pattern works everywhere
57
+ const s = 0.7;
58
+ const l = 0.6;
59
+ const c = (1 - Math.abs(2 * l - 1)) * s;
60
+ const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
61
+ const m = l - c / 2;
62
+ let r = 0,
63
+ g = 0,
64
+ b = 0;
65
+ if (hue < 60) {
66
+ r = c;
67
+ g = x;
68
+ } else if (hue < 120) {
69
+ r = x;
70
+ g = c;
71
+ } else if (hue < 180) {
72
+ g = c;
73
+ b = x;
74
+ } else if (hue < 240) {
75
+ g = x;
76
+ b = c;
77
+ } else if (hue < 300) {
78
+ r = x;
79
+ b = c;
80
+ } else {
81
+ r = c;
82
+ b = x;
83
+ }
84
+ const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
85
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
86
+ }
87
+
88
+ // ============================================
89
+ // Helper Functions
90
+ // ============================================
91
+
92
+ /**
93
+ * Format payload preview for display
94
+ */
95
+ function formatPartialPreview(partial, maxLength = 40) {
96
+ if (partial === undefined) return "";
97
+ if (partial === null) return "null";
98
+ try {
99
+ if (typeof partial === "function") {
100
+ return "(updater fn)";
101
+ }
102
+ if (typeof partial === "string") {
103
+ return partial.length > maxLength ? `"${partial.slice(0, maxLength - 3)}..."` : `"${partial}"`;
104
+ }
105
+ if (typeof partial === "number" || typeof partial === "boolean") {
106
+ return String(partial);
107
+ }
108
+ if (Array.isArray(partial)) {
109
+ if (partial.length === 0) return "[]";
110
+ const preview = JSON.stringify(partial);
111
+ return preview.length > maxLength ? `[${partial.length} items]` : preview;
112
+ }
113
+ if (typeof partial === "object") {
114
+ const keys = Object.keys(partial);
115
+ if (keys.length === 0) return "{}";
116
+ const preview = JSON.stringify(partial);
117
+ if (preview.length <= maxLength) return preview;
118
+ return `{ ${keys.length} keys }`;
119
+ }
120
+ return String(partial).slice(0, maxLength);
121
+ } catch {
122
+ return "[complex]";
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Calculate state diff summary
128
+ */
129
+ function getStateDiffSummary(prevState, nextState) {
130
+ if (prevState === nextState) {
131
+ return {
132
+ summary: "no change",
133
+ changedKeys: [],
134
+ changedCount: 0
135
+ };
136
+ }
137
+ if (typeof prevState !== "object" || typeof nextState !== "object" || prevState === null || nextState === null) {
138
+ return {
139
+ summary: "changed",
140
+ changedKeys: [],
141
+ changedCount: 1
142
+ };
143
+ }
144
+ const prevKeys = Object.keys(prevState);
145
+ const nextKeys = Object.keys(nextState);
146
+ const added = nextKeys.filter(k => !prevKeys.includes(k));
147
+ const removed = prevKeys.filter(k => !nextKeys.includes(k));
148
+ const changed = [];
149
+ for (const key of prevKeys) {
150
+ if (nextKeys.includes(key) && prevState[key] !== nextState[key]) {
151
+ changed.push(key);
152
+ }
153
+ }
154
+ const allChangedKeys = [...added, ...removed, ...changed];
155
+ const parts = [];
156
+ if (added.length > 0) parts.push(`+${added.length}`);
157
+ if (removed.length > 0) parts.push(`-${removed.length}`);
158
+ if (changed.length > 0) parts.push(`~${changed.length}`);
159
+ const totalChanged = allChangedKeys.length;
160
+ if (parts.length === 0) {
161
+ return {
162
+ summary: "nested change",
163
+ changedKeys: [],
164
+ changedCount: 1
165
+ };
166
+ }
167
+ return {
168
+ summary: `${parts.join(" ")} ${totalChanged === 1 ? "key" : "keys"}`,
169
+ changedKeys: allChangedKeys,
170
+ changedCount: totalChanged
171
+ };
172
+ }
173
+
174
+ // ============================================
175
+ // Zustand State Store
176
+ // ============================================
177
+
178
+ class ZustandStateStore {
179
+ stateChanges = [];
180
+ stores = new Map();
181
+ listeners = new Set();
182
+ storeListeners = new Set();
183
+ clearListeners = new Set();
184
+ maxChanges = 200;
185
+ idCounter = 0;
186
+ isEnabled = true;
187
+ captureSuppressed = false;
188
+ remoteSetStateHandler = null;
189
+
190
+ // ---- State Change Tracking ----
191
+
192
+ addStateChange(params) {
193
+ if (!this.isEnabled || this.captureSuppressed) return;
194
+ const {
195
+ storeName,
196
+ partial,
197
+ replace,
198
+ prevState,
199
+ nextState,
200
+ duration,
201
+ category,
202
+ isPersisted = false
203
+ } = params;
204
+ const hasStateChange = prevState !== nextState;
205
+ const {
206
+ summary,
207
+ changedKeys,
208
+ changedCount
209
+ } = getStateDiffSummary(prevState, nextState);
210
+ const partialPreview = formatPartialPreview(partial);
211
+ const isSlowUpdate = (duration ?? 0) > 16;
212
+ const resolvedCategory = category ?? (replace ? "replace" : "setState");
213
+ const stateChange = {
214
+ id: `${Date.now()}-${++this.idCounter}`,
215
+ storeName,
216
+ timestamp: Date.now(),
217
+ prevState,
218
+ nextState,
219
+ partial,
220
+ duration,
221
+ hasStateChange,
222
+ replace,
223
+ category: resolvedCategory,
224
+ changedKeys,
225
+ changedKeysCount: changedCount,
226
+ diffSummary: summary,
227
+ partialPreview,
228
+ isSlowUpdate,
229
+ isPersisted
230
+ };
231
+ this.stateChanges = [stateChange, ...this.stateChanges].slice(0, this.maxChanges);
232
+
233
+ // Update store change count
234
+ const storeInfo = this.stores.get(storeName);
235
+ if (storeInfo) {
236
+ storeInfo.stateChangeCount++;
237
+ }
238
+ this.notifyListeners();
239
+ }
240
+ getStateChanges() {
241
+ return [...this.stateChanges];
242
+ }
243
+ getStateChangeById(id) {
244
+ return this.stateChanges.find(c => c.id === id);
245
+ }
246
+ clearStateChanges() {
247
+ this.stateChanges = [];
248
+ // Reset counts
249
+ for (const store of this.stores.values()) {
250
+ store.stateChangeCount = 0;
251
+ }
252
+ this.notifyListeners();
253
+ this.clearListeners.forEach(listener => {
254
+ try {
255
+ listener();
256
+ } catch {
257
+ // Ignore listener errors
258
+ }
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Listen for clearStateChanges() calls. Used in remote mirror mode to
264
+ * forward a clear performed in the dashboard UI to the synced device.
265
+ */
266
+ onClear(listener) {
267
+ this.clearListeners.add(listener);
268
+ return () => {
269
+ this.clearListeners.delete(listener);
270
+ };
271
+ }
272
+
273
+ // ---- Remote Mirror Mode ----
274
+
275
+ /**
276
+ * Permanently suppress local capture. Use when this store acts as a mirror
277
+ * of a remote device's state changes (e.g. the desktop dashboard): data
278
+ * arrives via replaceFromSnapshot() and local middleware must never record.
279
+ */
280
+ disableCapture() {
281
+ this.captureSuppressed = true;
282
+ }
283
+
284
+ /** Whether the store is in remote mirror mode (capture suppressed). */
285
+ isCaptureSuppressed() {
286
+ return this.captureSuppressed;
287
+ }
288
+
289
+ /**
290
+ * Serializable snapshot of the tracked stores (registry metadata plus each
291
+ * store's current state). Used by the sync adapter on the device side —
292
+ * the live `api` handles can't go over the wire.
293
+ */
294
+ getStoreSnapshots() {
295
+ return Array.from(this.stores.values()).map(store => {
296
+ let currentState;
297
+ try {
298
+ currentState = store.api.getState();
299
+ } catch {
300
+ currentState = undefined;
301
+ }
302
+ return {
303
+ name: store.name,
304
+ stateChangeCount: store.stateChangeCount,
305
+ isPersisted: store.isPersisted,
306
+ persistName: store.persistName,
307
+ color: store.color,
308
+ currentState
309
+ };
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Handler invoked when the UI calls setState on a mirrored store (e.g.
315
+ * time-travel from the dashboard). Forward it to the synced device.
316
+ */
317
+ setRemoteSetStateHandler(handler) {
318
+ this.remoteSetStateHandler = handler;
319
+ }
320
+
321
+ /**
322
+ * Replace the entire mirror contents from a synced device snapshot. The
323
+ * rebuilt registry entries get stub `api` handles: getState() serves the
324
+ * snapshotted state, setState() forwards to the remote handler. Respects
325
+ * the enabled flag so the UI's pause button freezes the mirror.
326
+ */
327
+ replaceFromSnapshot(changes, storeSnapshots) {
328
+ if (!this.isEnabled) return;
329
+ this.stateChanges = changes.slice(0, this.maxChanges);
330
+ this.stores = new Map(storeSnapshots.map(snapshot => [snapshot.name, {
331
+ name: snapshot.name,
332
+ api: {
333
+ getState: () => snapshot.currentState,
334
+ setState: (partial, replace) => {
335
+ this.remoteSetStateHandler?.(snapshot.name, partial, replace);
336
+ },
337
+ subscribe: () => () => {}
338
+ },
339
+ stateChangeCount: snapshot.stateChangeCount,
340
+ isPersisted: snapshot.isPersisted,
341
+ persistName: snapshot.persistName,
342
+ color: snapshot.color
343
+ }]));
344
+ this.notifyListeners();
345
+ this.notifyStoreListeners();
346
+ }
347
+
348
+ // ---- Store Registry ----
349
+
350
+ registerStore(name, api, options) {
351
+ if (this.stores.has(name)) return;
352
+ const storeInfo = {
353
+ name,
354
+ api,
355
+ stateChangeCount: 0,
356
+ isPersisted: options?.isPersisted ?? false,
357
+ persistName: options?.persistName,
358
+ color: getStoreColor(name)
359
+ };
360
+ this.stores.set(name, storeInfo);
361
+ this.notifyStoreListeners();
362
+ }
363
+ unregisterStore(name) {
364
+ this.stores.delete(name);
365
+ this.notifyStoreListeners();
366
+ }
367
+ getStores() {
368
+ return Array.from(this.stores.values());
369
+ }
370
+ getStore(name) {
371
+ return this.stores.get(name);
372
+ }
373
+ getStoreColor(name) {
374
+ return this.stores.get(name)?.color ?? getStoreColor(name);
375
+ }
376
+
377
+ // ---- Filtering ----
378
+
379
+ filterStateChanges(filter) {
380
+ let filtered = [...this.stateChanges];
381
+ if (filter.searchText) {
382
+ const search = filter.searchText.toLowerCase();
383
+ filtered = filtered.filter(c => c.storeName.toLowerCase().includes(search) || c.partialPreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
384
+ }
385
+ if (filter.storeNames && filter.storeNames.length > 0) {
386
+ filtered = filtered.filter(c => filter.storeNames.includes(c.storeName));
387
+ }
388
+ if (filter.onlyWithChanges) {
389
+ filtered = filtered.filter(c => c.hasStateChange);
390
+ }
391
+ return filtered;
392
+ }
393
+
394
+ // ---- Stats ----
395
+
396
+ getStats() {
397
+ const total = this.stateChanges.length;
398
+ const withChanges = this.stateChanges.filter(c => c.hasStateChange).length;
399
+ const storeCount = this.stores.size;
400
+ const durations = this.stateChanges.filter(c => c.duration !== undefined).map(c => c.duration);
401
+ const avgDuration = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
402
+ return {
403
+ totalChanges: total,
404
+ changesWithStateChange: withChanges,
405
+ changesWithoutStateChange: total - withChanges,
406
+ storeCount,
407
+ averageDuration: Math.round(avgDuration * 100) / 100
408
+ };
409
+ }
410
+ getUniqueStoreNames() {
411
+ return Array.from(this.stores.keys()).sort();
412
+ }
413
+
414
+ // ---- Enable / Disable ----
415
+
416
+ setEnabled(enabled) {
417
+ this.isEnabled = enabled;
418
+ }
419
+ getEnabled() {
420
+ return this.isEnabled;
421
+ }
422
+ setMaxChanges(max) {
423
+ this.maxChanges = max;
424
+ if (this.stateChanges.length > max) {
425
+ this.stateChanges = this.stateChanges.slice(0, max);
426
+ this.notifyListeners();
427
+ }
428
+ }
429
+
430
+ // ---- Subscriptions ----
431
+
432
+ subscribe(listener) {
433
+ this.listeners.add(listener);
434
+ return () => {
435
+ this.listeners.delete(listener);
436
+ };
437
+ }
438
+ subscribeToStores(listener) {
439
+ this.storeListeners.add(listener);
440
+ return () => {
441
+ this.storeListeners.delete(listener);
442
+ };
443
+ }
444
+ notifyListeners() {
445
+ const changes = this.getStateChanges();
446
+ this.listeners.forEach(listener => listener(changes));
447
+ }
448
+ notifyStoreListeners() {
449
+ const stores = this.getStores();
450
+ this.storeListeners.forEach(listener => listener(stores));
451
+ }
452
+ }
453
+ export const zustandStateStore = new ZustandStateStore();
@@ -44,7 +44,8 @@ export { ZustandModal } from "./zustand/components/ZustandModal";
44
44
  export { ZustandStateChangeItem } from "./zustand/components/ZustandStateChangeItem";
45
45
  export { ZustandStateDetailContent, ZustandStateDetailFooter, } from "./zustand/components/ZustandStateDetailContent";
46
46
  export { ZustandIcon, ZUSTAND_ICON_COLOR } from "./zustand/components/ZustandIcon";
47
- export type { ZustandModalProps, ZustandStateChange, ZustandStoreInfo, ZustandFilter, StateChangeCategory, BuoyZustandMiddlewareOptions, } from "./zustand/types";
47
+ export { zustandSyncAdapter } from "./zustand/sync/zustandSyncAdapter";
48
+ export type { ZustandModalProps, ZustandStateChange, ZustandStoreInfo, ZustandStoreSnapshot, ZustandFilter, StateChangeCategory, BuoyZustandMiddlewareOptions, } from "./zustand/types";
48
49
  /** @internal */
49
50
  export { zustandStateStore } from "./zustand/utils/zustandStateStore";
50
51
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKhE,OAAO,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAKpE,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAKrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAK5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,YAAY,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AAK3F,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAKnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAKvE,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AAKzB,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAGjE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;qBAKX;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;CAKlC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAC1C,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;;;;;qBAMoB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;EAWpC"}