@buoy-gg/jotai 7.0.33 → 7.0.35

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 (38) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +2 -2
  2. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
  3. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +18 -97
  4. package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +5 -5
  5. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
  6. package/lib/commonjs/jotai/components/JotaiModal.js +20 -85
  7. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
  8. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
  9. package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
  10. package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
  11. package/lib/commonjs/preset.js +14 -22
  12. package/lib/module/jotai/components/JotaiAtomBrowser.js +3 -3
  13. package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
  14. package/lib/module/jotai/components/JotaiAtomDetailContent.js +19 -98
  15. package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +6 -6
  16. package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
  17. package/lib/module/jotai/components/JotaiModal.js +22 -87
  18. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
  19. package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
  20. package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
  21. package/lib/module/jotai/utils/snapshotProvider.js +8 -26
  22. package/lib/module/preset.js +13 -20
  23. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  24. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  25. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  26. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +1 -1
  27. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
  28. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  29. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  30. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
  31. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
  32. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
  33. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  34. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
  35. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  36. package/lib/typescript/preset.d.ts +2 -8
  37. package/lib/typescript/preset.d.ts.map +1 -1
  38. package/package.json +3 -3
@@ -25,17 +25,6 @@ var _jsxRuntime = require("react/jsx-runtime");
25
25
  * - Events tab: Live atom change monitoring with diffs
26
26
  */
27
27
 
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.
36
- */
37
- const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
38
-
39
28
  /**
40
29
  * The change log's "nothing here". Everything but the nouns is the shared
41
30
  * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
@@ -358,49 +347,24 @@ function JotaiModal({
358
347
  onBack: onBack
359
348
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
360
349
  title: "",
361
- children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
362
- style: styles.headerSearchContainer,
363
- testID: `${_testIDs.JOTAI_TEST_ID}-search`,
364
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
365
- size: 14,
366
- color: _sharedUi.night.color.textSecondary
367
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
368
- ref: searchInputRef,
369
- testID: `${_testIDs.JOTAI_TEST_ID}-search-field`,
370
- style: styles.headerSearchInput,
371
- placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
372
- placeholderTextColor: _sharedUi.night.color.placeholder,
373
- value: searchText,
374
- onChangeText: handleSearch,
375
- onSubmitEditing: () => searchInputRef.current?.blur()
376
- // Stay open while the query is still narrowing the list.
377
- //
378
- // Collapsing on blur hid the reason rows were missing: the
379
- // header went back to its TABS — whose counts are of the
380
- // WHOLE capture — while the list stayed filtered, so the tool
381
- // showed "Atoms (8)" above a single row and offered nothing to
382
- // explain the gap. The only way back was to reopen a field the
383
- // user had no reason to think was still armed. Same shape, and
384
- // the same fix, as the Redux, Events and Zustand tools.
385
- ,
386
- onBlur: () => {
387
- if (searchText.length === 0) setIsSearchActive(false);
388
- },
389
- accessibilityLabel: "Search Jotai atoms and events",
390
- autoCapitalize: "none",
391
- autoCorrect: false,
392
- returnKeyType: "search"
393
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
394
- onPress: closeSearch,
395
- style: styles.headerSearchClear,
396
- testID: `${_testIDs.JOTAI_TEST_ID}-search-clear`,
397
- accessibilityRole: "button",
398
- accessibilityLabel: "Close search",
399
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
400
- size: 14,
401
- color: _sharedUi.night.color.textSecondary
402
- })
403
- })]
350
+ children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
351
+ value: searchText,
352
+ onChangeText: handleSearch,
353
+ onCancel: closeSearch,
354
+ placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
355
+ accessibilityLabel: "Search Jotai atoms and events",
356
+ iconSize: 14,
357
+ testID: `${_testIDs.JOTAI_TEST_ID}-search-field`,
358
+ containerTestID: `${_testIDs.JOTAI_TEST_ID}-search`,
359
+ clearTestID: `${_testIDs.JOTAI_TEST_ID}-search-clear`
360
+ // Stay open while the query is still narrowing the list: the
361
+ // header would otherwise go back to its TABS — whose counts are
362
+ // of the WHOLE capture — above a filtered list, with nothing to
363
+ // explain the gap.
364
+ ,
365
+ onBlur: () => {
366
+ if (searchText.length === 0) setIsSearchActive(false);
367
+ }
404
368
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
405
369
  tabs: [{
406
370
  key: "atoms",
@@ -490,11 +454,7 @@ function JotaiModal({
490
454
  keyExtractor: keyExtractor,
491
455
  contentContainerStyle: styles.listContent,
492
456
  showsVerticalScrollIndicator: true,
493
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
494
- initialNumToRender: 12,
495
- maxToRenderPerBatch: 8,
496
- windowSize: 5,
497
- updateCellsBatchingPeriod: 50
457
+ ..._sharedUi.EVENT_LIST_PRESET
498
458
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
499
459
  testID: `${_testIDs.JOTAI_TEST_ID}-history-empty`,
500
460
  ...(0, _sharedUi.resolveListEmptyState)({
@@ -583,11 +543,7 @@ function JotaiModal({
583
543
  keyExtractor: keyExtractor,
584
544
  contentContainerStyle: styles.listContent,
585
545
  showsVerticalScrollIndicator: true,
586
- removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
587
- initialNumToRender: 12,
588
- maxToRenderPerBatch: 8,
589
- windowSize: 5,
590
- updateCellsBatchingPeriod: 50
546
+ ..._sharedUi.EVENT_LIST_PRESET
591
547
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EventsEmptyState, {
592
548
  isEnabled: isEnabled,
593
549
  searchText: searchText,
@@ -701,27 +657,6 @@ const styles = _reactNative.StyleSheet.create({
701
657
  // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
702
658
  backgroundColor: "transparent"
703
659
  },
704
- headerSearchContainer: {
705
- flexDirection: "row",
706
- alignItems: "center",
707
- backgroundColor: _sharedUi.night.color.surfaceElevated,
708
- borderRadius: _sharedUi.night.radius.row,
709
- borderWidth: 1,
710
- borderColor: _sharedUi.night.color.border,
711
- paddingHorizontal: 12,
712
- paddingVertical: 5
713
- },
714
- headerSearchInput: {
715
- flex: 1,
716
- color: _sharedUi.night.color.text,
717
- fontSize: 13,
718
- marginLeft: 6,
719
- paddingVertical: 2
720
- },
721
- headerSearchClear: {
722
- marginLeft: 6,
723
- padding: 4
724
- },
725
660
  headerActionButton: {
726
661
  width: 32,
727
662
  height: 32,
@@ -5,78 +5,62 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.useJotaiAtomChanges = useJotaiAtomChanges;
7
7
  var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  var _jotaiStateStore = require("../utils/jotaiStateStore");
9
10
  /**
10
11
  * Hook for consuming Jotai atom changes from the store
11
12
  *
12
- * Mirrors useZustandStateChanges.ts from @buoy-gg/zustand
13
+ * The subscribe / filter / pause / clear plumbing is shared `useChangeLog`;
14
+ * this file owns the jotai-specific filter, stats and atom registry.
13
15
  */
14
16
 
17
+ function filterChanges(changes, filter) {
18
+ let filtered = changes;
19
+ if (filter.searchText) {
20
+ const search = filter.searchText.toLowerCase();
21
+ filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
22
+ }
23
+ if (filter.atomLabels && filter.atomLabels.length > 0) {
24
+ filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
25
+ }
26
+ if (filter.onlyWithChanges) {
27
+ filtered = filtered.filter(c => c.hasValueChange);
28
+ }
29
+ return filtered;
30
+ }
31
+ const readAtoms = () => _jotaiStateStore.jotaiStateStore.getAtoms();
32
+ const subscribeToAtoms = listener => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(listener);
15
33
  function useJotaiAtomChanges() {
16
- const [atomChanges, setAtomChanges] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtomChanges());
17
- const [atoms, setAtoms] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getAtoms());
18
- const [filter, setFilter] = (0, _react.useState)({});
19
- const [isEnabled, setIsEnabled] = (0, _react.useState)(() => _jotaiStateStore.jotaiStateStore.getEnabled());
20
- (0, _react.useEffect)(() => {
21
- const unsubChanges = _jotaiStateStore.jotaiStateStore.subscribe(newChanges => {
22
- setAtomChanges(newChanges);
23
- });
24
- const unsubAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(newAtoms => {
25
- setAtoms(newAtoms);
26
- });
27
- return () => {
28
- unsubChanges();
29
- unsubAtoms();
30
- };
31
- }, []);
32
- const filteredChanges = (0, _react.useMemo)(() => {
33
- let filtered = atomChanges;
34
- if (filter.searchText) {
35
- const search = filter.searchText.toLowerCase();
36
- filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
37
- }
38
- if (filter.atomLabels && filter.atomLabels.length > 0) {
39
- filtered = filtered.filter(c => filter.atomLabels.includes(c.atomLabel));
40
- }
41
- if (filter.onlyWithChanges) {
42
- filtered = filtered.filter(c => c.hasValueChange);
43
- }
44
- return filtered;
45
- }, [atomChanges, filter]);
34
+ const log = (0, _sharedUi.useChangeLog)(_jotaiStateStore.jotaiStateStore, {
35
+ filterItems: filterChanges
36
+ });
37
+ const atoms = (0, _sharedUi.useSubscribedValue)(readAtoms, subscribeToAtoms);
46
38
  const stats = (0, _react.useMemo)(() => {
47
- const total = atomChanges.length;
48
- const withChanges = atomChanges.filter(c => c.hasValueChange).length;
39
+ const total = log.items.length;
40
+ const withChanges = log.items.filter(c => c.hasValueChange).length;
49
41
  return {
50
42
  totalChanges: total,
51
43
  changesWithValueChange: withChanges,
52
44
  changesWithoutValueChange: total - withChanges,
53
45
  atomCount: atoms.length
54
46
  };
55
- }, [atomChanges, atoms]);
47
+ }, [log.items, atoms]);
56
48
  const atomLabels = (0, _react.useMemo)(() => {
57
49
  return _jotaiStateStore.jotaiStateStore.getUniqueAtomLabels();
58
50
  }, [atoms]);
59
- const clearChanges = (0, _react.useCallback)(() => {
60
- _jotaiStateStore.jotaiStateStore.clearAtomChanges();
61
- }, []);
62
- const toggleCapture = (0, _react.useCallback)(() => {
63
- const newEnabled = !isEnabled;
64
- _jotaiStateStore.jotaiStateStore.setEnabled(newEnabled);
65
- setIsEnabled(newEnabled);
66
- }, [isEnabled]);
67
51
  const getChangeById = (0, _react.useCallback)(id => {
68
52
  return _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
69
53
  }, []);
70
54
  return {
71
- atomChanges,
72
- filteredChanges,
73
- filter,
74
- setFilter,
55
+ atomChanges: log.items,
56
+ filteredChanges: log.filteredItems,
57
+ filter: log.filter,
58
+ setFilter: log.setFilter,
75
59
  stats,
76
60
  atoms,
77
- clearChanges,
78
- isEnabled,
79
- toggleCapture,
61
+ clearChanges: log.clear,
62
+ isEnabled: log.isEnabled,
63
+ toggleCapture: log.toggleCapture,
80
64
  atomLabels,
81
65
  getChangeById
82
66
  };
@@ -11,36 +11,21 @@ var _sharedUi = require("@buoy-gg/shared-ui");
11
11
  * Same hazard as redux/zustand: up to 200 changes × 2 values on every
12
12
  * snapshot. Dashboards fetch values via `getChangeDetail`.
13
13
  */
14
- const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = Object.freeze({
15
- __buoyValueOnDevice: true,
16
- note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
17
- });
18
- const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
19
- const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
14
+ const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = (0, _sharedUi.onDeviceMarker)("Value", "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue.");
20
15
  /**
21
16
  * Per-change wire cache — the store prepends and never mutates a recorded
22
17
  * change, so identity is a correct, self-invalidating key.
23
18
  */
24
- const changeCache = new WeakMap();
25
- function toWireChange(change) {
26
- const cached = changeCache.get(change);
27
- if (cached) return cached;
28
- const wire = {
29
- ...change,
30
- prevValue: VALUE_ON_DEVICE,
31
- nextValue: VALUE_ON_DEVICE
32
- };
33
- changeCache.set(change, wire);
34
- return wire;
35
- }
36
- function toWireValue(value) {
37
- return (0, _sharedUi.isOverWireBudget)(value, MAX_WIRE_PAYLOAD_BYTES) ? VALUE_ON_DEVICE : value;
38
- }
19
+ const toWireChange = (0, _sharedUi.memoByIdentity)(change => ({
20
+ ...change,
21
+ prevValue: VALUE_ON_DEVICE,
22
+ nextValue: VALUE_ON_DEVICE
23
+ }));
24
+ const toWireValue = value => (0, _sharedUi.toWire)(value, VALUE_ON_DEVICE);
39
25
 
40
26
  /**
41
27
  * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
42
- * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
43
- * this package doesn't need a dependency on it).
28
+ * `useExternalSync`.
44
29
  *
45
30
  * Atom changes are captured by watchAtoms(), independent of whether a
46
31
  * dashboard is watching — subscribing here only streams what the store
@@ -52,7 +37,7 @@ function toWireValue(value) {
52
37
  * Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
53
38
  * `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
54
39
  */
55
- const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
40
+ const jotaiSyncAdapter = exports.jotaiSyncAdapter = (0, _sharedUi.createSyncAdapter)({
56
41
  version: 2,
57
42
  getSnapshot: () => ({
58
43
  changes: _jotaiStateStore.jotaiStateStore.getAtomChanges().map(toWireChange),
@@ -61,14 +46,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
61
46
  currentValue: toWireValue(atom.currentValue)
62
47
  }))
63
48
  }),
64
- subscribe: onChange => {
65
- const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
66
- const unsubscribeAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange);
67
- return () => {
68
- unsubscribeChanges();
69
- unsubscribeAtoms();
70
- };
71
- },
49
+ subscribe: [onChange => _jotaiStateStore.jotaiStateStore.subscribe(onChange), onChange => _jotaiStateStore.jotaiStateStore.subscribeToAtoms(onChange)],
72
50
  actions: {
73
51
  clearEvents: () => {
74
52
  _jotaiStateStore.jotaiStateStore.clearAtomChanges();
@@ -78,31 +56,10 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
78
56
  * The per-snapshot stream stays value-free; this is the explicit,
79
57
  * size-guarded channel for the detail pane.
80
58
  */
81
- getChangeDetail: params => {
82
- const {
83
- id
84
- } = params ?? {};
85
- if (!id) return {
86
- found: false,
87
- reason: "missing id"
88
- };
89
- const change = _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
90
- if (!change) return {
91
- found: false,
92
- reason: "unknown id"
93
- };
94
- const cappedNote = {
95
- __buoyTruncated: true,
96
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
97
- };
98
- const cap = value => (0, _sharedUi.isOverWireBudget)(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
99
- return {
100
- found: true,
101
- id,
102
- prevValue: cap(change.prevValue),
103
- nextValue: cap(change.nextValue)
104
- };
105
- },
59
+ getChangeDetail: (0, _sharedUi.detailByIdAction)(id => _jotaiStateStore.jotaiStateStore.getAtomChangeById(id), (change, cap) => ({
60
+ prevValue: cap(change.prevValue),
61
+ nextValue: cap(change.nextValue)
62
+ })),
106
63
  /**
107
64
  * On-demand current value for one atom. Wire-form snapshots replace
108
65
  * oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
@@ -110,7 +67,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
110
67
  getAtomValue: params => {
111
68
  const {
112
69
  label
113
- } = params ?? {};
70
+ } = (0, _sharedUi.readParams)(params);
114
71
  if (!label) return {
115
72
  found: false,
116
73
  reason: "missing label"
@@ -126,10 +83,6 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
126
83
  } catch {
127
84
  currentValue = undefined;
128
85
  }
129
- const cappedNote = {
130
- __buoyTruncated: true,
131
- note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
132
- };
133
86
  try {
134
87
  JSON.stringify(currentValue);
135
88
  } catch {
@@ -142,7 +95,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
142
95
  return {
143
96
  found: true,
144
97
  label,
145
- currentValue: (0, _sharedUi.isOverWireBudget)(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
98
+ currentValue: (0, _sharedUi.capDetail)(currentValue, _sharedUi.WIRE_DETAIL_LIMIT_BYTES)
146
99
  };
147
100
  },
148
101
  /**
@@ -155,7 +108,7 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
155
108
  const {
156
109
  includeValues = false,
157
110
  limit
158
- } = params ?? {};
111
+ } = (0, _sharedUi.readParams)(params);
159
112
  const all = _jotaiStateStore.jotaiStateStore.getAtoms();
160
113
  const capped = typeof limit === "number" && limit > 0 ? all.slice(0, limit) : all;
161
114
  const atoms = capped.map(a => {
@@ -188,9 +141,9 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
188
141
  const {
189
142
  label,
190
143
  value
191
- } = params ?? {};
144
+ } = (0, _sharedUi.readParams)(params);
192
145
  if (!label) throw new Error("setAtom requires a `label`");
193
146
  _jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
194
147
  }
195
148
  }
196
- };
149
+ });