@buoy-gg/jotai 7.0.32 → 7.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +20 -41
  2. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +7 -52
  3. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +8 -87
  4. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +21 -299
  5. package/lib/commonjs/jotai/components/JotaiModal.js +82 -124
  6. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +33 -49
  7. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +19 -66
  8. package/lib/commonjs/jotai/utils/jotaiStateStore.js +23 -188
  9. package/lib/commonjs/jotai/utils/snapshotProvider.js +7 -25
  10. package/lib/commonjs/preset.js +14 -22
  11. package/lib/module/jotai/components/JotaiAtomBrowser.js +21 -42
  12. package/lib/module/jotai/components/JotaiAtomChangeItem.js +9 -54
  13. package/lib/module/jotai/components/JotaiAtomDetailContent.js +9 -88
  14. package/lib/module/jotai/components/JotaiEventFilterView.js +24 -302
  15. package/lib/module/jotai/components/JotaiModal.js +84 -126
  16. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +34 -50
  17. package/lib/module/jotai/sync/jotaiSyncAdapter.js +20 -67
  18. package/lib/module/jotai/utils/jotaiStateStore.js +23 -188
  19. package/lib/module/jotai/utils/snapshotProvider.js +8 -26
  20. package/lib/module/preset.js +13 -20
  21. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  22. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  23. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  24. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts +2 -2
  25. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  26. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  27. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts +2 -1
  28. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +1 -1
  29. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -68
  30. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  31. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +2 -25
  32. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  33. package/lib/typescript/preset.d.ts +2 -8
  34. package/lib/typescript/preset.d.ts.map +1 -1
  35. package/package.json +3 -3
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.jotaiStateStore = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
7
8
  /**
8
9
  * Jotai state store — captures and stores Jotai atom changes
9
10
  *
@@ -40,44 +41,7 @@ const ATOM_COLORS = {
40
41
  filter: "#84CC16" // lime
41
42
  };
42
43
  function getAtomColor(label) {
43
- const lower = label.toLowerCase();
44
- if (ATOM_COLORS[lower]) return ATOM_COLORS[lower];
45
- for (const [key, color] of Object.entries(ATOM_COLORS)) {
46
- if (lower.includes(key)) return color;
47
- }
48
-
49
- // Generate consistent hex from name hash
50
- const hash = label.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
51
- const hue = hash * 137 % 360;
52
- const s = 0.7;
53
- const l = 0.6;
54
- const c = (1 - Math.abs(2 * l - 1)) * s;
55
- const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
56
- const m = l - c / 2;
57
- let r = 0,
58
- g = 0,
59
- b = 0;
60
- if (hue < 60) {
61
- r = c;
62
- g = x;
63
- } else if (hue < 120) {
64
- r = x;
65
- g = c;
66
- } else if (hue < 180) {
67
- g = c;
68
- b = x;
69
- } else if (hue < 240) {
70
- g = x;
71
- b = c;
72
- } else if (hue < 300) {
73
- r = x;
74
- b = c;
75
- } else {
76
- r = c;
77
- b = x;
78
- }
79
- const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
80
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
44
+ return (0, _sharedUi.entityColor)(label, ATOM_COLORS);
81
45
  }
82
46
 
83
47
  // ============================================
@@ -85,97 +49,25 @@ function getAtomColor(label) {
85
49
  // ============================================
86
50
 
87
51
  function formatValuePreview(value, maxLength = 40) {
88
- if (value === undefined) return "undefined";
89
- if (value === null) return "null";
90
- try {
91
- if (typeof value === "function") return "(fn)";
92
- if (typeof value === "string") {
93
- return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
94
- }
95
- if (typeof value === "number" || typeof value === "boolean") {
96
- return String(value);
97
- }
98
- if (Array.isArray(value)) {
99
- if (value.length === 0) return "[]";
100
- const preview = JSON.stringify(value);
101
- return preview.length > maxLength ? `[${value.length} items]` : preview;
102
- }
103
- if (typeof value === "object") {
104
- const keys = Object.keys(value);
105
- if (keys.length === 0) return "{}";
106
- const preview = JSON.stringify(value);
107
- if (preview.length <= maxLength) return preview;
108
- return `{ ${keys.length} keys }`;
109
- }
110
- return String(value).slice(0, maxLength);
111
- } catch {
112
- return "[complex]";
113
- }
114
- }
115
- function getValueDiffSummary(prevValue, nextValue) {
116
- if (prevValue === nextValue) {
117
- return {
118
- summary: "no change",
119
- changedKeys: [],
120
- changedCount: 0
121
- };
122
- }
123
- if (typeof prevValue !== "object" || typeof nextValue !== "object" || prevValue === null || nextValue === null) {
124
- return {
125
- summary: "changed",
126
- changedKeys: [],
127
- changedCount: 0
128
- };
129
- }
130
- const prevKeys = Object.keys(prevValue);
131
- const nextKeys = Object.keys(nextValue);
132
- const added = nextKeys.filter(k => !prevKeys.includes(k));
133
- const removed = prevKeys.filter(k => !nextKeys.includes(k));
134
- const changed = [];
135
- for (const key of prevKeys) {
136
- if (nextKeys.includes(key) && prevValue[key] !== nextValue[key]) {
137
- changed.push(key);
138
- }
139
- }
140
- const allChangedKeys = [...added, ...removed, ...changed];
141
- const parts = [];
142
- if (added.length > 0) parts.push(`+${added.length}`);
143
- if (removed.length > 0) parts.push(`-${removed.length}`);
144
- if (changed.length > 0) parts.push(`~${changed.length}`);
145
- const total = allChangedKeys.length;
146
- if (parts.length === 0) {
147
- return {
148
- summary: "nested change",
149
- changedKeys: [],
150
- changedCount: 0
151
- };
152
- }
153
- return {
154
- summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
155
- changedKeys: allChangedKeys,
156
- changedCount: total
157
- };
52
+ return (0, _sharedUi.formatValuePreview)(value, {
53
+ maxLength,
54
+ functionLabel: "(fn)"
55
+ });
158
56
  }
57
+ const getValueDiffSummary = _sharedUi.shallowDiffSummary;
159
58
 
160
59
  // ============================================
161
60
  // Jotai State Store
162
61
  // ============================================
163
62
 
164
- class JotaiStateStore {
165
- atomChanges = [];
63
+ class JotaiStateStore extends _sharedUi.ChangeLog {
166
64
  atoms = new Map();
167
- listeners = new Set();
168
65
  atomListeners = new Set();
169
- clearListeners = new Set();
170
- maxChanges = 200;
171
- idCounter = 0;
172
- isEnabled = true;
173
- captureSuppressed = false;
174
66
 
175
67
  // ---- Change Tracking ----
176
68
 
177
69
  addAtomChange(params) {
178
- if (!this.isEnabled || this.captureSuppressed) return;
70
+ if (!this.canRecord()) return;
179
71
  const {
180
72
  atomLabel,
181
73
  prevValue,
@@ -190,7 +82,7 @@ class JotaiStateStore {
190
82
  } = getValueDiffSummary(prevValue, nextValue);
191
83
  const valuePreview = formatValuePreview(nextValue);
192
84
  const change = {
193
- id: `${Date.now()}-${++this.idCounter}`,
85
+ id: this.nextId(),
194
86
  atomLabel,
195
87
  timestamp: Date.now(),
196
88
  prevValue,
@@ -203,7 +95,7 @@ class JotaiStateStore {
203
95
  valuePreview,
204
96
  isSlowUpdate: false
205
97
  };
206
- this.atomChanges = [change, ...this.atomChanges].slice(0, this.maxChanges);
98
+ this.push(change);
207
99
  const atomInfo = this.atoms.get(atomLabel);
208
100
  if (atomInfo) {
209
101
  atomInfo.changeCount++;
@@ -212,54 +104,21 @@ class JotaiStateStore {
212
104
  this.notifyAtomListeners();
213
105
  }
214
106
  getAtomChanges() {
215
- return [...this.atomChanges];
107
+ return this.getAll();
216
108
  }
217
109
  getAtomChangeById(id) {
218
- return this.atomChanges.find(c => c.id === id);
110
+ return this.getById(id);
219
111
  }
220
112
  clearAtomChanges() {
221
- this.atomChanges = [];
222
113
  for (const atom of this.atoms.values()) {
223
114
  atom.changeCount = 0;
224
115
  }
225
- this.notifyListeners();
116
+ this.clear();
226
117
  this.notifyAtomListeners();
227
- this.clearListeners.forEach(listener => {
228
- try {
229
- listener();
230
- } catch {
231
- // Ignore listener errors
232
- }
233
- });
234
- }
235
-
236
- /**
237
- * Listen for clearAtomChanges() calls. Used in remote mirror mode to
238
- * forward a clear performed in the dashboard UI to the synced device.
239
- */
240
- onClear(listener) {
241
- this.clearListeners.add(listener);
242
- return () => {
243
- this.clearListeners.delete(listener);
244
- };
245
118
  }
246
119
 
247
120
  // ---- Remote Mirror Mode ----
248
121
 
249
- /**
250
- * Permanently suppress local capture. Use when this store acts as a mirror
251
- * of a remote device's atom changes (e.g. the desktop dashboard): data
252
- * arrives via replaceFromSnapshot() and local watchers must never record.
253
- */
254
- disableCapture() {
255
- this.captureSuppressed = true;
256
- }
257
-
258
- /** Whether the store is in remote mirror mode (capture suppressed). */
259
- isCaptureSuppressed() {
260
- return this.captureSuppressed;
261
- }
262
-
263
122
  /**
264
123
  * Serializable snapshot of the tracked atoms (registry metadata plus each
265
124
  * atom's current value). Used by the sync adapter on the device side —
@@ -290,18 +149,18 @@ class JotaiStateStore {
290
149
  * freezes the mirror.
291
150
  */
292
151
  replaceFromSnapshot(changes, atomSnapshots) {
293
- if (!this.isEnabled) return;
152
+ if (!this.getEnabled()) return;
294
153
 
295
154
  // Preserve values a remote mirror already hydrated — wire-form snapshots
296
155
  // always carry markers, and a later snapshot must not blank the detail pane.
297
156
  const hydrated = new Map();
298
- for (const existing of this.atomChanges) {
157
+ for (const existing of this.items) {
299
158
  const prev = existing.prevValue;
300
159
  if (!prev || prev.__buoyValueOnDevice !== true) {
301
160
  hydrated.set(existing.id, existing);
302
161
  }
303
162
  }
304
- this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
163
+ const merged = changes.slice(0, this.max).map(change => {
305
164
  const kept = hydrated.get(change.id);
306
165
  const incoming = change.prevValue;
307
166
  return kept && incoming?.__buoyValueOnDevice === true ? {
@@ -339,7 +198,7 @@ class JotaiStateStore {
339
198
  writeKind: snapshot.writeKind
340
199
  }];
341
200
  }));
342
- this.notifyListeners();
201
+ this.setItems(merged);
343
202
  this.notifyAtomListeners();
344
203
  }
345
204
 
@@ -348,13 +207,7 @@ class JotaiStateStore {
348
207
  * device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
349
208
  */
350
209
  hydrateChange(id, patch) {
351
- const index = this.atomChanges.findIndex(c => c.id === id);
352
- if (index === -1) return;
353
- this.atomChanges = [...this.atomChanges.slice(0, index), {
354
- ...this.atomChanges[index],
355
- ...patch
356
- }, ...this.atomChanges.slice(index + 1)];
357
- this.notifyListeners();
210
+ this.hydrateById(id, patch);
358
211
  }
359
212
 
360
213
  /**
@@ -418,7 +271,7 @@ class JotaiStateStore {
418
271
  // ---- Filtering ----
419
272
 
420
273
  filterAtomChanges(filter) {
421
- let filtered = [...this.atomChanges];
274
+ let filtered = this.getAll();
422
275
  if (filter.searchText) {
423
276
  const search = filter.searchText.toLowerCase();
424
277
  filtered = filtered.filter(c => c.atomLabel.toLowerCase().includes(search) || c.valuePreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
@@ -435,8 +288,8 @@ class JotaiStateStore {
435
288
  // ---- Stats ----
436
289
 
437
290
  getStats() {
438
- const total = this.atomChanges.length;
439
- const withChanges = this.atomChanges.filter(c => c.hasValueChange).length;
291
+ const total = this.items.length;
292
+ const withChanges = this.items.filter(c => c.hasValueChange).length;
440
293
  return {
441
294
  totalChanges: total,
442
295
  changesWithValueChange: withChanges,
@@ -450,34 +303,16 @@ class JotaiStateStore {
450
303
 
451
304
  // ---- Enable / Disable ----
452
305
 
453
- setEnabled(enabled) {
454
- this.isEnabled = enabled;
455
- }
456
- getEnabled() {
457
- return this.isEnabled;
458
- }
459
306
  setMaxChanges(max) {
460
- this.maxChanges = max;
461
- if (this.atomChanges.length > max) {
462
- this.atomChanges = this.atomChanges.slice(0, max);
463
- this.notifyListeners();
464
- }
307
+ this.setMax(max);
465
308
  }
466
309
 
467
310
  // ---- Subscriptions ----
468
311
 
469
- subscribe(listener) {
470
- this.listeners.add(listener);
471
- return () => this.listeners.delete(listener);
472
- }
473
312
  subscribeToAtoms(listener) {
474
313
  this.atomListeners.add(listener);
475
314
  return () => this.atomListeners.delete(listener);
476
315
  }
477
- notifyListeners() {
478
- const changes = this.getAtomChanges();
479
- this.listeners.forEach(listener => listener(changes));
480
- }
481
316
  notifyAtomListeners() {
482
317
  const atoms = this.getAtoms();
483
318
  this.atomListeners.forEach(listener => listener(atoms));
@@ -65,30 +65,14 @@ async function capture() {
65
65
  }
66
66
  async function restore(data, options) {
67
67
  const snapshot = data;
68
- const excludedSet = new Set(options?.excludeKeys ?? []);
69
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
70
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
71
- let excludedHits = 0;
72
- const result = {
73
- ok: true,
74
- applied: 0,
75
- skipped: [],
76
- warnings: []
77
- };
68
+ const scope = (0, _sharedUi.createRestoreScope)(options);
69
+ const result = (0, _sharedUi.emptyRestoreResult)();
78
70
  if (!snapshot?.atoms) {
79
- return {
80
- ok: false,
81
- applied: 0,
82
- skipped: [],
83
- warnings: ["empty jotai snapshot"]
84
- };
71
+ return (0, _sharedUi.failedRestore)("empty jotai snapshot");
85
72
  }
86
73
  let derived = 0;
87
74
  for (const [label, entry] of Object.entries(snapshot.atoms)) {
88
- if (!touch(label)) {
89
- excludedHits += 1;
90
- continue;
91
- }
75
+ if (!scope.allows(label)) continue;
92
76
  if (!entry.writable) {
93
77
  derived += 1;
94
78
  continue;
@@ -122,9 +106,8 @@ async function restore(data, options) {
122
106
  if (derived > 0) {
123
107
  result.warnings.push(`${derived} derived atom(s) left to recompute`);
124
108
  }
125
- if (excludedHits > 0) {
126
- result.warnings.push(`${excludedHits} atom(s) excluded by you`);
127
- }
109
+ const excludedWarning = scope.excludedWarning("atom(s)");
110
+ if (excludedWarning) result.warnings.push(excludedWarning);
128
111
  return result;
129
112
  }
130
113
 
@@ -149,8 +132,7 @@ async function previewItems(target, base) {
149
132
  }
150
133
  const items = [];
151
134
  const notes = [];
152
- const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
153
- for (const label of labels) {
135
+ for (const label of (0, _sharedUi.unionKeys)(baseAtoms, targetAtoms)) {
154
136
  const inTarget = label in targetAtoms;
155
137
  if (!inTarget) {
156
138
  if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
@@ -3,10 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createJotaiTool = createJotaiTool;
7
- exports.jotaiToolPreset = void 0;
6
+ exports.jotaiToolPreset = exports.createJotaiTool = void 0;
8
7
  var _JotaiModal = require("./jotai/components/JotaiModal");
9
8
  var _JotaiIcon = require("./jotai/components/JotaiIcon");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  /**
12
12
  * Pre-configured Jotai DevTools preset for FloatingDevTools
@@ -48,7 +48,7 @@ var _jsxRuntime = require("react/jsx-runtime");
48
48
  * - Filter by atom label
49
49
  * - Changed keys tracking for object atoms
50
50
  */
51
- const jotaiToolPreset = exports.jotaiToolPreset = {
51
+ const jotaiToolPreset = exports.jotaiToolPreset = (0, _sharedUi.defineToolPreset)({
52
52
  id: "jotai",
53
53
  name: "JOTAI",
54
54
  description: "Jotai atom & state inspector",
@@ -62,7 +62,7 @@ const jotaiToolPreset = exports.jotaiToolPreset = {
62
62
  props: {
63
63
  enableSharedModalDimensions: false
64
64
  }
65
- };
65
+ });
66
66
 
67
67
  /**
68
68
  * Create a custom Jotai DevTools configuration.
@@ -78,21 +78,13 @@ const jotaiToolPreset = exports.jotaiToolPreset = {
78
78
  * });
79
79
  * ```
80
80
  */
81
- function createJotaiTool(options) {
82
- return {
83
- id: options?.id || "jotai",
84
- name: options?.name || "JOTAI",
85
- description: options?.description || "Jotai atom & state inspector",
86
- slot: "both",
87
- icon: ({
88
- size
89
- }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiIcon.JotaiIcon, {
90
- size: size,
91
- color: options?.iconColor
92
- }),
93
- component: _JotaiModal.JotaiModal,
94
- props: {
95
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
96
- }
97
- };
98
- }
81
+ const createJotaiTool = exports.createJotaiTool = (0, _sharedUi.createToolFactory)(jotaiToolPreset, options => ({
82
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
83
+ }), options => ({
84
+ icon: ({
85
+ size
86
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiIcon.JotaiIcon, {
87
+ size: size,
88
+ color: options.iconColor
89
+ })
90
+ }));
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { useMemo } from "react";
13
13
  import { View, Text, StyleSheet, ScrollView } from "react-native";
14
- import { CompactRow, night, Box } from "@buoy-gg/shared-ui";
14
+ import { CompactRow, night, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
15
15
  import { jotaiStateStore } from "../utils/jotaiStateStore";
16
16
  import { JOTAI_TEST_ID, atomRowTestID } from "../utils/testIDs";
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -49,20 +49,14 @@ function getWriteKindLabel(atom) {
49
49
  return atom.setValue ? "writable" : "derived";
50
50
  }
51
51
  function EmptyBrowserState() {
52
- return /*#__PURE__*/_jsxs(View, {
53
- style: styles.emptyState,
54
- testID: `${JOTAI_TEST_ID}-atoms-empty`,
55
- children: [/*#__PURE__*/_jsx(Box, {
56
- size: 32,
57
- color: night.color.textTertiary
58
- }), /*#__PURE__*/_jsx(Text, {
59
- style: styles.emptyTitle,
60
- testID: `${JOTAI_TEST_ID}-atoms-empty-title`,
61
- children: "No atoms registered"
62
- }), /*#__PURE__*/_jsx(Text, {
63
- style: styles.emptyText,
64
- children: "Use watchAtoms(store, { atomName }) to register your Jotai atoms.\nThey will appear here with their current value."
65
- })]
52
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
53
+ testID: `${JOTAI_TEST_ID}-atoms-empty`
54
+ // scanning = "scanning for targets… and finding none". The registry is
55
+ // discovered, not configured, so an empty one is a scan result.
56
+ ,
57
+ otto: "scanning",
58
+ title: "No atoms registered",
59
+ message: "Use watchAtoms(store, { atomName }) to register your Jotai atoms.\nThey will appear here with their current value."
66
60
  });
67
61
  }
68
62
  export function JotaiAtomBrowser({
@@ -79,17 +73,19 @@ export function JotaiAtomBrowser({
79
73
  return /*#__PURE__*/_jsx(EmptyBrowserState, {});
80
74
  }
81
75
  if (filteredAtoms.length === 0 && searchQuery) {
82
- return /*#__PURE__*/_jsxs(View, {
83
- style: styles.emptyState,
76
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
84
77
  testID: `${JOTAI_TEST_ID}-atoms-no-match`,
85
- children: [/*#__PURE__*/_jsx(Text, {
86
- style: styles.emptyTitle,
87
- testID: `${JOTAI_TEST_ID}-atoms-no-match-title`,
88
- children: "No matching atoms"
89
- }), /*#__PURE__*/_jsxs(Text, {
90
- style: styles.emptyText,
91
- children: ["No atoms match \"", searchQuery, "\""]
92
- })]
78
+ ...resolveListEmptyState({
79
+ subject: "atoms",
80
+ verb: "registered",
81
+ search: searchQuery,
82
+ total: atoms.length,
83
+ waiting: {
84
+ otto: "scanning",
85
+ title: "No atoms registered",
86
+ message: "Use watchAtoms() to register your Jotai atoms."
87
+ }
88
+ })
93
89
  });
94
90
  }
95
91
  return /*#__PURE__*/_jsxs(ScrollView, {
@@ -195,22 +191,5 @@ const styles = StyleSheet.create({
195
191
  fontWeight: "700",
196
192
  color: night.color.accent,
197
193
  fontFamily: "monospace"
198
- },
199
- emptyState: {
200
- alignItems: "center",
201
- paddingVertical: 40
202
- },
203
- emptyTitle: {
204
- color: night.color.text,
205
- fontSize: 14,
206
- fontWeight: "600",
207
- marginTop: 12,
208
- marginBottom: 6
209
- },
210
- emptyText: {
211
- color: night.color.textTertiary,
212
- fontSize: 12,
213
- textAlign: "center",
214
- lineHeight: 18
215
194
  }
216
195
  });
@@ -7,10 +7,9 @@
7
7
  */
8
8
 
9
9
  import { memo } from "react";
10
- import { View, Text, StyleSheet } from "react-native";
11
- import { CompactRow, buoyColors, night, Zap, RelativeTime } from "@buoy-gg/shared-ui";
10
+ import { ChangeRow, night } from "@buoy-gg/shared-ui";
12
11
  import { jotaiStateStore } from "../utils/jotaiStateStore";
13
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ import { jsx as _jsx } from "react/jsx-runtime";
14
13
  function getStatusLabel(category, atomLabel) {
15
14
  if (category === "initial") return "Initial";
16
15
  return atomLabel.charAt(0).toUpperCase() + atomLabel.slice(1);
@@ -68,59 +67,15 @@ export const JotaiAtomChangeItem = /*#__PURE__*/memo(function JotaiAtomChangeIte
68
67
  const sublabel = getSublabel(change);
69
68
  const primaryText = getPrimaryText(change);
70
69
  const badgeText = getBadgeText(change);
71
-
72
- // Always a custom badge, even when there is no icon to hang off it. The
73
- // alternative — letting CompactRow draw `badgeText` itself — leaves the one
74
- // row whose badge matters most (a write that moved the atom's identity but
75
- // none of its contents) as the only row in the list with no handle on its
76
- // badge. Same call, and the same reason, as ZustandStateChangeItem.
77
- const customBadge = /*#__PURE__*/_jsxs(View, {
78
- style: styles.badgeContainer,
79
- children: [/*#__PURE__*/_jsx(Text, {
80
- style: [styles.badgeText, {
81
- color: statusColor
82
- }],
83
- testID: testID ? `${testID}-badge` : undefined,
84
- children: badgeText
85
- }), change.hasValueChange ? /*#__PURE__*/_jsx(View, {
86
- style: styles.changeBadge,
87
- testID: testID ? `${testID}-change` : undefined,
88
- children: /*#__PURE__*/_jsx(Zap, {
89
- size: 12,
90
- color: buoyColors.warning
91
- })
92
- }) : null]
93
- });
94
- return /*#__PURE__*/_jsx(CompactRow, {
95
- statusDotColor: statusColor,
70
+ return /*#__PURE__*/_jsx(ChangeRow, {
71
+ statusColor: statusColor,
96
72
  statusLabel: statusLabel,
97
- statusSublabel: sublabel,
73
+ sublabel: sublabel,
98
74
  primaryText: primaryText,
99
- bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
100
- timestamp: change.timestamp
101
- }),
102
- customBadge: customBadge,
103
- badgeColor: statusColor,
104
- showChevron: true,
75
+ timestamp: change.timestamp,
76
+ badgeText: badgeText,
77
+ changed: change.hasValueChange,
105
78
  onPress: () => onPress(change),
106
- testID: testID,
107
- statusTestID: testID ? `${testID}-status` : undefined,
108
- statusSublabelTestID: testID ? `${testID}-sublabel` : undefined,
109
- primaryTestID: testID ? `${testID}-keys` : undefined
79
+ testID: testID
110
80
  });
111
- });
112
- const styles = StyleSheet.create({
113
- badgeContainer: {
114
- flexDirection: "row",
115
- alignItems: "center",
116
- gap: 4
117
- },
118
- badgeText: {
119
- fontSize: 11,
120
- fontWeight: "600",
121
- fontFamily: "monospace"
122
- },
123
- changeBadge: {
124
- paddingHorizontal: 4
125
- }
126
81
  });