@buoy-gg/jotai 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 (48) hide show
  1. package/lib/commonjs/index.js +98 -1
  2. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +300 -1
  3. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +113 -1
  4. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +754 -1
  5. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +305 -1
  6. package/lib/commonjs/jotai/components/JotaiIcon.js +35 -1
  7. package/lib/commonjs/jotai/components/JotaiModal.js +567 -1
  8. package/lib/commonjs/jotai/components/index.js +59 -1
  9. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +83 -1
  10. package/lib/commonjs/jotai/index.js +85 -1
  11. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +38 -0
  12. package/lib/commonjs/jotai/utils/jotaiStateStore.js +399 -1
  13. package/lib/commonjs/jotai/utils/watchAtoms.js +149 -1
  14. package/lib/commonjs/preset.js +98 -1
  15. package/lib/module/index.js +79 -1
  16. package/lib/module/jotai/components/JotaiAtomBrowser.js +296 -1
  17. package/lib/module/jotai/components/JotaiAtomChangeItem.js +109 -1
  18. package/lib/module/jotai/components/JotaiAtomDetailContent.js +748 -1
  19. package/lib/module/jotai/components/JotaiEventFilterView.js +301 -1
  20. package/lib/module/jotai/components/JotaiIcon.js +31 -1
  21. package/lib/module/jotai/components/JotaiModal.js +563 -1
  22. package/lib/module/jotai/components/index.js +8 -1
  23. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +79 -1
  24. package/lib/module/jotai/index.js +10 -1
  25. package/lib/module/jotai/sync/jotaiSyncAdapter.js +35 -0
  26. package/lib/module/jotai/utils/jotaiStateStore.js +395 -1
  27. package/lib/module/jotai/utils/watchAtoms.js +144 -1
  28. package/lib/module/preset.js +94 -1
  29. package/lib/typescript/index.d.ts +2 -1
  30. package/lib/typescript/index.d.ts.map +1 -0
  31. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -0
  32. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -0
  33. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -0
  34. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -0
  35. package/lib/typescript/jotai/components/JotaiIcon.d.ts.map +1 -0
  36. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -0
  37. package/lib/typescript/jotai/components/index.d.ts.map +1 -0
  38. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -0
  39. package/lib/typescript/jotai/index.d.ts.map +1 -0
  40. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +23 -0
  41. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -0
  42. package/lib/typescript/jotai/types/index.d.ts +11 -0
  43. package/lib/typescript/jotai/types/index.d.ts.map +1 -0
  44. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +29 -1
  45. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -0
  46. package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -0
  47. package/lib/typescript/preset.d.ts.map +1 -0
  48. package/package.json +3 -3
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ import { jotaiStateStore } from "../utils/jotaiStateStore";
4
+
5
+ /**
6
+ * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
7
+ * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
8
+ * this package doesn't need a dependency on it).
9
+ *
10
+ * Atom changes are captured by watchAtoms(), independent of whether a
11
+ * dashboard is watching — subscribing here only streams what the store
12
+ * already records. The snapshot carries both the change timeline and the
13
+ * atom registry (with each atom's current value) so the dashboard can render
14
+ * the atom browser.
15
+ */
16
+ export const jotaiSyncAdapter = {
17
+ version: 1,
18
+ getSnapshot: () => ({
19
+ changes: jotaiStateStore.getAtomChanges(),
20
+ atoms: jotaiStateStore.getAtomSnapshots()
21
+ }),
22
+ subscribe: onChange => {
23
+ const unsubscribeChanges = jotaiStateStore.subscribe(onChange);
24
+ const unsubscribeAtoms = jotaiStateStore.subscribeToAtoms(onChange);
25
+ return () => {
26
+ unsubscribeChanges();
27
+ unsubscribeAtoms();
28
+ };
29
+ },
30
+ actions: {
31
+ clearEvents: () => {
32
+ jotaiStateStore.clearAtomChanges();
33
+ }
34
+ }
35
+ };
@@ -1 +1,395 @@
1
- "use strict";const ATOM_COLORS={count:"#10B981",auth:"#8B5CF6",user:"#3B82F6",cart:"#EC4899",app:"#6366F1",ui:"#F59E0B",settings:"#14B8A6",theme:"#06B6D4",nav:"#F97316",form:"#EF4444",modal:"#A855F7",filter:"#84CC16"};function getAtomColor(t){const e=t.toLowerCase();if(ATOM_COLORS[e])return ATOM_COLORS[e];for(const[t,s]of Object.entries(ATOM_COLORS))if(e.includes(t))return s;const s=137*t.split("").reduce((t,e)=>t+e.charCodeAt(0),0)%360,n=.7*(1-Math.abs(1.2-1)),a=n*(1-Math.abs(s/60%2-1)),o=.6-n/2;let r=0,i=0,h=0;s<60?(r=n,i=a):s<120?(r=a,i=n):s<180?(i=n,h=a):s<240?(i=a,h=n):s<300?(r=a,h=n):(r=n,h=a);const l=t=>Math.round(255*(t+o)).toString(16).padStart(2,"0");return`#${l(r)}${l(i)}${l(h)}`}function formatValuePreview(t,e=40){if(void 0===t)return"undefined";if(null===t)return"null";try{if("function"==typeof t)return"(fn)";if("string"==typeof t)return t.length>e?`"${t.slice(0,e-3)}..."`:`"${t}"`;if("number"==typeof t||"boolean"==typeof t)return String(t);if(Array.isArray(t)){if(0===t.length)return"[]";const s=JSON.stringify(t);return s.length>e?`[${t.length} items]`:s}if("object"==typeof t){const s=Object.keys(t);if(0===s.length)return"{}";const n=JSON.stringify(t);return n.length<=e?n:`{ ${s.length} keys }`}return String(t).slice(0,e)}catch{return"[complex]"}}function getValueDiffSummary(t,e){if(t===e)return{summary:"no change",changedKeys:[],changedCount:0};if("object"!=typeof t||"object"!=typeof e||null===t||null===e)return{summary:"changed",changedKeys:[],changedCount:0};const s=Object.keys(t),n=Object.keys(e),a=n.filter(t=>!s.includes(t)),o=s.filter(t=>!n.includes(t)),r=[];for(const a of s)n.includes(a)&&t[a]!==e[a]&&r.push(a);const i=[...a,...o,...r],h=[];a.length>0&&h.push(`+${a.length}`),o.length>0&&h.push(`-${o.length}`),r.length>0&&h.push(`~${r.length}`);const l=i.length;return 0===h.length?{summary:"nested change",changedKeys:[],changedCount:0}:{summary:`${h.join(" ")} ${1===l?"key":"keys"}`,changedKeys:i,changedCount:l}}class JotaiStateStore{atomChanges=[];atoms=new Map;listeners=new Set;atomListeners=new Set;maxChanges=200;idCounter=0;isEnabled=!0;addAtomChange(t){if(!this.isEnabled)return;const{atomLabel:e,prevValue:s,nextValue:n,category:a="write"}=t,o=s!==n,{summary:r,changedKeys:i,changedCount:h}=getValueDiffSummary(s,n),l=formatValuePreview(n),u={id:`${Date.now()}-${++this.idCounter}`,atomLabel:e,timestamp:Date.now(),prevValue:s,nextValue:n,hasValueChange:o,category:a,changedKeys:i,changedKeysCount:h,diffSummary:r,valuePreview:l,isSlowUpdate:!1};this.atomChanges=[u,...this.atomChanges].slice(0,this.maxChanges);const g=this.atoms.get(e);g&&g.changeCount++,this.notifyListeners(),this.notifyAtomListeners()}getAtomChanges(){return[...this.atomChanges]}getAtomChangeById(t){return this.atomChanges.find(e=>e.id===t)}clearAtomChanges(){this.atomChanges=[];for(const t of this.atoms.values())t.changeCount=0;this.notifyListeners(),this.notifyAtomListeners()}registerAtom(t,e){if(this.atoms.has(t))return;const s={label:t,changeCount:0,color:getAtomColor(t),getValue:e};this.atoms.set(t,s),this.notifyAtomListeners()}unregisterAtom(t){this.atoms.delete(t),this.notifyAtomListeners()}getAtoms(){return Array.from(this.atoms.values())}getAtom(t){return this.atoms.get(t)}getAtomColor(t){return this.atoms.get(t)?.color??getAtomColor(t)}filterAtomChanges(t){let e=[...this.atomChanges];if(t.searchText){const s=t.searchText.toLowerCase();e=e.filter(t=>t.atomLabel.toLowerCase().includes(s)||t.valuePreview.toLowerCase().includes(s)||t.changedKeys.some(t=>t.toLowerCase().includes(s)))}return t.atomLabels&&t.atomLabels.length>0&&(e=e.filter(e=>t.atomLabels.includes(e.atomLabel))),t.onlyWithChanges&&(e=e.filter(t=>t.hasValueChange)),e}getStats(){const t=this.atomChanges.length,e=this.atomChanges.filter(t=>t.hasValueChange).length;return{totalChanges:t,changesWithValueChange:e,changesWithoutValueChange:t-e,atomCount:this.atoms.size}}getUniqueAtomLabels(){return Array.from(this.atoms.keys()).sort()}setEnabled(t){this.isEnabled=t}getEnabled(){return this.isEnabled}setMaxChanges(t){this.maxChanges=t,this.atomChanges.length>t&&(this.atomChanges=this.atomChanges.slice(0,t),this.notifyListeners())}subscribe(t){return this.listeners.add(t),()=>this.listeners.delete(t)}subscribeToAtoms(t){return this.atomListeners.add(t),()=>this.atomListeners.delete(t)}notifyListeners(){const t=this.getAtomChanges();this.listeners.forEach(e=>e(t))}notifyAtomListeners(){const t=this.getAtoms();this.atomListeners.forEach(e=>e(t))}}export const jotaiStateStore=new JotaiStateStore;
1
+ "use strict";
2
+
3
+ /**
4
+ * Jotai state store — captures and stores Jotai atom changes
5
+ *
6
+ * Mirrors the architecture of zustandStateStore.ts from @buoy-gg/zustand
7
+ */
8
+
9
+ // ============================================
10
+ // Atom Color Palette
11
+ // ============================================
12
+
13
+ const ATOM_COLORS = {
14
+ count: "#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
+ nav: "#F97316",
31
+ // orange
32
+ form: "#EF4444",
33
+ // red
34
+ modal: "#A855F7",
35
+ // violet
36
+ filter: "#84CC16" // lime
37
+ };
38
+ function getAtomColor(label) {
39
+ const lower = label.toLowerCase();
40
+ if (ATOM_COLORS[lower]) return ATOM_COLORS[lower];
41
+ for (const [key, color] of Object.entries(ATOM_COLORS)) {
42
+ if (lower.includes(key)) return color;
43
+ }
44
+
45
+ // Generate consistent hex from name hash
46
+ const hash = label.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
47
+ const hue = hash * 137 % 360;
48
+ const s = 0.7;
49
+ const l = 0.6;
50
+ const c = (1 - Math.abs(2 * l - 1)) * s;
51
+ const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
52
+ const m = l - c / 2;
53
+ let r = 0,
54
+ g = 0,
55
+ b = 0;
56
+ if (hue < 60) {
57
+ r = c;
58
+ g = x;
59
+ } else if (hue < 120) {
60
+ r = x;
61
+ g = c;
62
+ } else if (hue < 180) {
63
+ g = c;
64
+ b = x;
65
+ } else if (hue < 240) {
66
+ g = x;
67
+ b = c;
68
+ } else if (hue < 300) {
69
+ r = x;
70
+ b = c;
71
+ } else {
72
+ r = c;
73
+ b = x;
74
+ }
75
+ const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
76
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
77
+ }
78
+
79
+ // ============================================
80
+ // Helper Functions
81
+ // ============================================
82
+
83
+ function formatValuePreview(value, maxLength = 40) {
84
+ if (value === undefined) return "undefined";
85
+ if (value === null) return "null";
86
+ try {
87
+ if (typeof value === "function") return "(fn)";
88
+ if (typeof value === "string") {
89
+ return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
90
+ }
91
+ if (typeof value === "number" || typeof value === "boolean") {
92
+ return String(value);
93
+ }
94
+ if (Array.isArray(value)) {
95
+ if (value.length === 0) return "[]";
96
+ const preview = JSON.stringify(value);
97
+ return preview.length > maxLength ? `[${value.length} items]` : preview;
98
+ }
99
+ if (typeof value === "object") {
100
+ const keys = Object.keys(value);
101
+ if (keys.length === 0) return "{}";
102
+ const preview = JSON.stringify(value);
103
+ if (preview.length <= maxLength) return preview;
104
+ return `{ ${keys.length} keys }`;
105
+ }
106
+ return String(value).slice(0, maxLength);
107
+ } catch {
108
+ return "[complex]";
109
+ }
110
+ }
111
+ function getValueDiffSummary(prevValue, nextValue) {
112
+ if (prevValue === nextValue) {
113
+ return {
114
+ summary: "no change",
115
+ changedKeys: [],
116
+ changedCount: 0
117
+ };
118
+ }
119
+ if (typeof prevValue !== "object" || typeof nextValue !== "object" || prevValue === null || nextValue === null) {
120
+ return {
121
+ summary: "changed",
122
+ changedKeys: [],
123
+ changedCount: 0
124
+ };
125
+ }
126
+ const prevKeys = Object.keys(prevValue);
127
+ const nextKeys = Object.keys(nextValue);
128
+ const added = nextKeys.filter(k => !prevKeys.includes(k));
129
+ const removed = prevKeys.filter(k => !nextKeys.includes(k));
130
+ const changed = [];
131
+ for (const key of prevKeys) {
132
+ if (nextKeys.includes(key) && prevValue[key] !== nextValue[key]) {
133
+ changed.push(key);
134
+ }
135
+ }
136
+ const allChangedKeys = [...added, ...removed, ...changed];
137
+ const parts = [];
138
+ if (added.length > 0) parts.push(`+${added.length}`);
139
+ if (removed.length > 0) parts.push(`-${removed.length}`);
140
+ if (changed.length > 0) parts.push(`~${changed.length}`);
141
+ const total = allChangedKeys.length;
142
+ if (parts.length === 0) {
143
+ return {
144
+ summary: "nested change",
145
+ changedKeys: [],
146
+ changedCount: 0
147
+ };
148
+ }
149
+ return {
150
+ summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
151
+ changedKeys: allChangedKeys,
152
+ changedCount: total
153
+ };
154
+ }
155
+
156
+ // ============================================
157
+ // Jotai State Store
158
+ // ============================================
159
+
160
+ class JotaiStateStore {
161
+ atomChanges = [];
162
+ atoms = new Map();
163
+ listeners = new Set();
164
+ atomListeners = new Set();
165
+ clearListeners = new Set();
166
+ maxChanges = 200;
167
+ idCounter = 0;
168
+ isEnabled = true;
169
+ captureSuppressed = false;
170
+
171
+ // ---- Change Tracking ----
172
+
173
+ addAtomChange(params) {
174
+ if (!this.isEnabled || this.captureSuppressed) return;
175
+ const {
176
+ atomLabel,
177
+ prevValue,
178
+ nextValue,
179
+ category = "write"
180
+ } = params;
181
+ const hasValueChange = prevValue !== nextValue;
182
+ const {
183
+ summary,
184
+ changedKeys,
185
+ changedCount
186
+ } = getValueDiffSummary(prevValue, nextValue);
187
+ const valuePreview = formatValuePreview(nextValue);
188
+ const change = {
189
+ id: `${Date.now()}-${++this.idCounter}`,
190
+ atomLabel,
191
+ timestamp: Date.now(),
192
+ prevValue,
193
+ nextValue,
194
+ hasValueChange,
195
+ category,
196
+ changedKeys,
197
+ changedKeysCount: changedCount,
198
+ diffSummary: summary,
199
+ valuePreview,
200
+ isSlowUpdate: false
201
+ };
202
+ this.atomChanges = [change, ...this.atomChanges].slice(0, this.maxChanges);
203
+ const atomInfo = this.atoms.get(atomLabel);
204
+ if (atomInfo) {
205
+ atomInfo.changeCount++;
206
+ }
207
+ this.notifyListeners();
208
+ this.notifyAtomListeners();
209
+ }
210
+ getAtomChanges() {
211
+ return [...this.atomChanges];
212
+ }
213
+ getAtomChangeById(id) {
214
+ return this.atomChanges.find(c => c.id === id);
215
+ }
216
+ clearAtomChanges() {
217
+ this.atomChanges = [];
218
+ for (const atom of this.atoms.values()) {
219
+ atom.changeCount = 0;
220
+ }
221
+ this.notifyListeners();
222
+ this.notifyAtomListeners();
223
+ this.clearListeners.forEach(listener => {
224
+ try {
225
+ listener();
226
+ } catch {
227
+ // Ignore listener errors
228
+ }
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Listen for clearAtomChanges() calls. Used in remote mirror mode to
234
+ * forward a clear performed in the dashboard UI to the synced device.
235
+ */
236
+ onClear(listener) {
237
+ this.clearListeners.add(listener);
238
+ return () => {
239
+ this.clearListeners.delete(listener);
240
+ };
241
+ }
242
+
243
+ // ---- Remote Mirror Mode ----
244
+
245
+ /**
246
+ * Permanently suppress local capture. Use when this store acts as a mirror
247
+ * of a remote device's atom changes (e.g. the desktop dashboard): data
248
+ * arrives via replaceFromSnapshot() and local watchers must never record.
249
+ */
250
+ disableCapture() {
251
+ this.captureSuppressed = true;
252
+ }
253
+
254
+ /** Whether the store is in remote mirror mode (capture suppressed). */
255
+ isCaptureSuppressed() {
256
+ return this.captureSuppressed;
257
+ }
258
+
259
+ /**
260
+ * Serializable snapshot of the tracked atoms (registry metadata plus each
261
+ * atom's current value). Used by the sync adapter on the device side —
262
+ * the live `getValue` handles can't go over the wire.
263
+ */
264
+ getAtomSnapshots() {
265
+ return Array.from(this.atoms.values()).map(atom => {
266
+ let currentValue;
267
+ try {
268
+ currentValue = atom.getValue();
269
+ } catch {
270
+ currentValue = undefined;
271
+ }
272
+ return {
273
+ label: atom.label,
274
+ changeCount: atom.changeCount,
275
+ color: atom.color,
276
+ currentValue
277
+ };
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Replace the entire mirror contents from a synced device snapshot. The
283
+ * rebuilt registry entries get stub `getValue` handles serving the
284
+ * snapshotted value. Respects the enabled flag so the UI's pause button
285
+ * freezes the mirror.
286
+ */
287
+ replaceFromSnapshot(changes, atomSnapshots) {
288
+ if (!this.isEnabled) return;
289
+ this.atomChanges = changes.slice(0, this.maxChanges);
290
+ this.atoms = new Map(atomSnapshots.map(snapshot => [snapshot.label, {
291
+ label: snapshot.label,
292
+ changeCount: snapshot.changeCount,
293
+ color: snapshot.color,
294
+ getValue: () => snapshot.currentValue
295
+ }]));
296
+ this.notifyListeners();
297
+ this.notifyAtomListeners();
298
+ }
299
+
300
+ // ---- Atom Registry ----
301
+
302
+ registerAtom(label, getValue) {
303
+ if (this.atoms.has(label)) return;
304
+ const atomInfo = {
305
+ label,
306
+ changeCount: 0,
307
+ color: getAtomColor(label),
308
+ getValue
309
+ };
310
+ this.atoms.set(label, atomInfo);
311
+ this.notifyAtomListeners();
312
+ }
313
+ unregisterAtom(label) {
314
+ this.atoms.delete(label);
315
+ this.notifyAtomListeners();
316
+ }
317
+ getAtoms() {
318
+ return Array.from(this.atoms.values());
319
+ }
320
+ getAtom(label) {
321
+ return this.atoms.get(label);
322
+ }
323
+ getAtomColor(label) {
324
+ return this.atoms.get(label)?.color ?? getAtomColor(label);
325
+ }
326
+
327
+ // ---- Filtering ----
328
+
329
+ filterAtomChanges(filter) {
330
+ let filtered = [...this.atomChanges];
331
+ if (filter.searchText) {
332
+ const search = filter.searchText.toLowerCase();
333
+ filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
334
+ }
335
+ if (filter.atomLabels && filter.atomLabels.length > 0) {
336
+ filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
337
+ }
338
+ if (filter.onlyWithChanges) {
339
+ filtered = filtered.filter(c => c.hasValueChange);
340
+ }
341
+ return filtered;
342
+ }
343
+
344
+ // ---- Stats ----
345
+
346
+ getStats() {
347
+ const total = this.atomChanges.length;
348
+ const withChanges = this.atomChanges.filter(c => c.hasValueChange).length;
349
+ return {
350
+ totalChanges: total,
351
+ changesWithValueChange: withChanges,
352
+ changesWithoutValueChange: total - withChanges,
353
+ atomCount: this.atoms.size
354
+ };
355
+ }
356
+ getUniqueAtomLabels() {
357
+ return Array.from(this.atoms.keys()).sort();
358
+ }
359
+
360
+ // ---- Enable / Disable ----
361
+
362
+ setEnabled(enabled) {
363
+ this.isEnabled = enabled;
364
+ }
365
+ getEnabled() {
366
+ return this.isEnabled;
367
+ }
368
+ setMaxChanges(max) {
369
+ this.maxChanges = max;
370
+ if (this.atomChanges.length > max) {
371
+ this.atomChanges = this.atomChanges.slice(0, max);
372
+ this.notifyListeners();
373
+ }
374
+ }
375
+
376
+ // ---- Subscriptions ----
377
+
378
+ subscribe(listener) {
379
+ this.listeners.add(listener);
380
+ return () => this.listeners.delete(listener);
381
+ }
382
+ subscribeToAtoms(listener) {
383
+ this.atomListeners.add(listener);
384
+ return () => this.atomListeners.delete(listener);
385
+ }
386
+ notifyListeners() {
387
+ const changes = this.getAtomChanges();
388
+ this.listeners.forEach(listener => listener(changes));
389
+ }
390
+ notifyAtomListeners() {
391
+ const atoms = this.getAtoms();
392
+ this.atomListeners.forEach(listener => listener(atoms));
393
+ }
394
+ }
395
+ export const jotaiStateStore = new JotaiStateStore();
@@ -1 +1,144 @@
1
- "use strict";import{jotaiStateStore}from"./jotaiStateStore";export function watchAtoms(t,e,o){const a=!1!==o?.enabled,r=[];for(const[o,i]of Object.entries(e)){const e=i,n=Symbol.for(`@@buoy-jotai/watched/${o}`);if(e[n])continue;let c;e[n]=!0;try{c=t.get(i)}catch{c=void 0}jotaiStateStore.registerAtom(o,()=>{try{return t.get(i)}catch{return}}),a&&jotaiStateStore.addAtomChange({atomLabel:o,prevValue:void 0,nextValue:c,category:"initial"});const u=t.sub(i,()=>{try{const e=t.get(i);jotaiStateStore.addAtomChange({atomLabel:o,prevValue:c,nextValue:e,category:"write"}),c=e}catch{}});r.push(()=>{u(),delete e[n],jotaiStateStore.unregisterAtom(o)})}return()=>r.forEach(t=>t())}export function watchDefaultStoreAtoms(t,e){let o;try{const{getDefaultStore:t}=require("jotai/vanilla");o=t()}catch{try{const{getDefaultStore:t}=require("jotai");o=t()}catch{return console.warn("[@buoy-gg/jotai] Could not find getDefaultStore from jotai. Pass the store explicitly via watchAtoms(store, atoms) instead."),()=>{}}}return watchAtoms(o,t,e)}export function isAtomWatched(t){return void 0!==jotaiStateStore.getAtom(t)}
1
+ "use strict";
2
+
3
+ /**
4
+ * Buoy Jotai DevTools — Atom instrumentation
5
+ *
6
+ * watchAtoms() — RECOMMENDED. Pass a Jotai store and a named atom map.
7
+ * Uses store.sub() to observe each atom externally. Never touches atom internals.
8
+ *
9
+ * watchDefaultStoreAtoms() — Convenience wrapper that uses getDefaultStore().
10
+ */
11
+
12
+ import { jotaiStateStore } from "./jotaiStateStore";
13
+
14
+ /** Any Jotai atom — alias for Atom<unknown> for readability */
15
+
16
+ /** Minimal store shape — what Jotai's createStore() / getDefaultStore() returns */
17
+
18
+ /**
19
+ * Watch a set of Jotai atoms for value changes.
20
+ *
21
+ * Pass a Jotai store and an object mapping display names to atoms.
22
+ * Uses store.sub() to observe each atom — never modifies atom internals.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * import { getDefaultStore } from 'jotai';
27
+ * import { watchAtoms } from '@buoy-gg/jotai';
28
+ * import { countAtom } from './atoms/count';
29
+ * import { authAtom } from './atoms/auth';
30
+ *
31
+ * watchAtoms(getDefaultStore(), {
32
+ * countAtom,
33
+ * authAtom,
34
+ * });
35
+ * ```
36
+ *
37
+ * @param store - Jotai store (from createStore() or getDefaultStore())
38
+ * @param atoms - Object mapping display labels to Jotai atoms
39
+ * @param options - Optional configuration
40
+ * @returns Cleanup function that removes all subscriptions
41
+ */
42
+ export function watchAtoms(store, atoms, options) {
43
+ const enabled = options?.enabled !== false;
44
+ const cleanups = [];
45
+ for (const [label, atom] of Object.entries(atoms)) {
46
+ // Skip if already watched under this label
47
+ const atomAny = atom;
48
+ const watchedKey = Symbol.for(`@@buoy-jotai/watched/${label}`);
49
+ if (atomAny[watchedKey]) continue;
50
+ atomAny[watchedKey] = true;
51
+ let prevValue;
52
+ try {
53
+ prevValue = store.get(atom);
54
+ } catch {
55
+ prevValue = undefined;
56
+ }
57
+
58
+ // Register the atom for the browser view
59
+ jotaiStateStore.registerAtom(label, () => {
60
+ try {
61
+ return store.get(atom);
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ });
66
+
67
+ // Record initial value
68
+ if (enabled) {
69
+ jotaiStateStore.addAtomChange({
70
+ atomLabel: label,
71
+ prevValue: undefined,
72
+ nextValue: prevValue,
73
+ category: "initial"
74
+ });
75
+ }
76
+
77
+ // Subscribe to future changes
78
+ const unsubscribe = store.sub(atom, () => {
79
+ try {
80
+ const nextValue = store.get(atom);
81
+ jotaiStateStore.addAtomChange({
82
+ atomLabel: label,
83
+ prevValue,
84
+ nextValue,
85
+ category: "write"
86
+ });
87
+ prevValue = nextValue;
88
+ } catch {
89
+ // Silently catch — our bug must never propagate into the store
90
+ }
91
+ });
92
+ cleanups.push(() => {
93
+ unsubscribe();
94
+ delete atomAny[watchedKey];
95
+ jotaiStateStore.unregisterAtom(label);
96
+ });
97
+ }
98
+ return () => cleanups.forEach(fn => fn());
99
+ }
100
+
101
+ /**
102
+ * Watch atoms using Jotai's default store (no Provider setup required).
103
+ *
104
+ * Convenience wrapper around watchAtoms() — auto-imports getDefaultStore.
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * import { watchDefaultStoreAtoms } from '@buoy-gg/jotai';
109
+ * import { countAtom, authAtom } from './atoms';
110
+ *
111
+ * watchDefaultStoreAtoms({ countAtom, authAtom });
112
+ * ```
113
+ */
114
+ export function watchDefaultStoreAtoms(atoms, options) {
115
+ // Dynamic import to avoid bundling jotai internals when not needed
116
+ let store;
117
+ try {
118
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
119
+ const {
120
+ getDefaultStore
121
+ } = require("jotai/vanilla");
122
+ store = getDefaultStore();
123
+ } catch {
124
+ // Try the main jotai entry (React Native)
125
+ try {
126
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
127
+ const {
128
+ getDefaultStore
129
+ } = require("jotai");
130
+ store = getDefaultStore();
131
+ } catch {
132
+ console.warn("[@buoy-gg/jotai] Could not find getDefaultStore from jotai. " + "Pass the store explicitly via watchAtoms(store, atoms) instead.");
133
+ return () => {};
134
+ }
135
+ }
136
+ return watchAtoms(store, atoms, options);
137
+ }
138
+
139
+ /**
140
+ * Check if an atom label is currently being watched
141
+ */
142
+ export function isAtomWatched(label) {
143
+ return jotaiStateStore.getAtom(label) !== undefined;
144
+ }
@@ -1 +1,94 @@
1
- "use strict";import{JotaiModal}from"./jotai/components/JotaiModal";import{JotaiIcon}from"./jotai/components/JotaiIcon";import{jsx as _jsx}from"react/jsx-runtime";export const jotaiToolPreset={id:"jotai",name:"JOTAI",description:"Jotai atom & state inspector",slot:"both",icon:({size:o})=>_jsx(JotaiIcon,{size:o}),component:JotaiModal,props:{enableSharedModalDimensions:!1}};export function createJotaiTool(o){return{id:o?.id||"jotai",name:o?.name||"JOTAI",description:o?.description||"Jotai atom & state inspector",slot:"both",icon:({size:i})=>_jsx(JotaiIcon,{size:i,color:o?.iconColor}),component:JotaiModal,props:{enableSharedModalDimensions:void 0!==o?.enableSharedModalDimensions&&o.enableSharedModalDimensions}}}
1
+ "use strict";
2
+
3
+ /**
4
+ * Pre-configured Jotai DevTools preset for FloatingDevTools
5
+ *
6
+ * ZERO-CONFIG: This preset is auto-discovered by FloatingDevTools!
7
+ * Just install @buoy-gg/jotai and call watchAtoms() with your atoms.
8
+ *
9
+ * @example Automatic (recommended)
10
+ * ```tsx
11
+ * import { getDefaultStore } from 'jotai';
12
+ * import { watchAtoms } from '@buoy-gg/jotai';
13
+ * import { countAtom, authAtom } from './atoms';
14
+ *
15
+ * watchAtoms(getDefaultStore(), { countAtom, authAtom });
16
+ *
17
+ * // The Jotai tool appears automatically in FloatingDevTools!
18
+ * <FloatingDevTools />
19
+ * ```
20
+ *
21
+ * @example Manual (only for custom configuration)
22
+ * ```tsx
23
+ * import { createJotaiTool } from '@buoy-gg/jotai';
24
+ *
25
+ * const customJotaiTool = createJotaiTool({
26
+ * name: "ATOMS",
27
+ * iconColor: "#6C47FF",
28
+ * });
29
+ *
30
+ * <FloatingDevTools apps={[customJotaiTool]} />
31
+ * ```
32
+ */
33
+
34
+ import { JotaiModal } from "./jotai/components/JotaiModal";
35
+ import { JotaiIcon } from "./jotai/components/JotaiIcon";
36
+
37
+ /**
38
+ * Pre-configured Jotai DevTools preset for FloatingDevTools.
39
+ * Includes:
40
+ * - Live atom change monitoring
41
+ * - Atom value inspection (JSON viewer)
42
+ * - Value diff visualization (tree + split)
43
+ * - Filter by atom label
44
+ * - Changed keys tracking for object atoms
45
+ */
46
+ import { jsx as _jsx } from "react/jsx-runtime";
47
+ export const jotaiToolPreset = {
48
+ id: "jotai",
49
+ name: "JOTAI",
50
+ description: "Jotai atom & state inspector",
51
+ slot: "both",
52
+ icon: ({
53
+ size
54
+ }) => /*#__PURE__*/_jsx(JotaiIcon, {
55
+ size: size
56
+ }),
57
+ component: JotaiModal,
58
+ props: {
59
+ enableSharedModalDimensions: false
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Create a custom Jotai DevTools configuration.
65
+ * Use this if you want to override default settings.
66
+ *
67
+ * @example
68
+ * ```tsx
69
+ * import { createJotaiTool } from '@buoy-gg/jotai';
70
+ *
71
+ * const myJotaiTool = createJotaiTool({
72
+ * name: "ATOMS",
73
+ * iconColor: "#6C47FF",
74
+ * });
75
+ * ```
76
+ */
77
+ export function createJotaiTool(options) {
78
+ return {
79
+ id: options?.id || "jotai",
80
+ name: options?.name || "JOTAI",
81
+ description: options?.description || "Jotai atom & state inspector",
82
+ slot: "both",
83
+ icon: ({
84
+ size
85
+ }) => /*#__PURE__*/_jsx(JotaiIcon, {
86
+ size: size,
87
+ color: options?.iconColor
88
+ }),
89
+ component: JotaiModal,
90
+ props: {
91
+ enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
92
+ }
93
+ };
94
+ }
@@ -40,7 +40,8 @@ export { JotaiModal } from "./jotai/components/JotaiModal";
40
40
  export { JotaiAtomChangeItem } from "./jotai/components/JotaiAtomChangeItem";
41
41
  export { JotaiAtomDetailContent, JotaiAtomDetailFooter, } from "./jotai/components/JotaiAtomDetailContent";
42
42
  export { JotaiIcon, JOTAI_ICON_COLOR } from "./jotai/components/JotaiIcon";
43
- export type { JotaiModalProps, JotaiAtomChange, JotaiAtomInfo, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
43
+ export { jotaiSyncAdapter } from "./jotai/sync/jotaiSyncAdapter";
44
+ export type { JotaiModalProps, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
44
45
  /** @internal */
45
46
  export { jotaiStateStore } from "./jotai/utils/jotaiStateStore";
46
47
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAKnF,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAyGD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,aAAa,GACd,EAAE,qBAAqB,+BAiFvB"}