@buoy-gg/jotai 7.0.30 → 7.0.32

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 (46) hide show
  1. package/lib/commonjs/index.js +7 -0
  2. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +30 -119
  3. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +19 -7
  4. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +41 -5
  5. package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +444 -0
  6. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +15 -0
  7. package/lib/commonjs/jotai/components/JotaiModal.js +159 -23
  8. package/lib/commonjs/jotai/utils/jotaiStateStore.js +9 -4
  9. package/lib/commonjs/jotai/utils/testIDs.js +90 -0
  10. package/lib/commonjs/jotai/utils/watchAtoms.js +8 -2
  11. package/lib/commonjs/jotai/utils/writeSemantics.js +47 -0
  12. package/lib/module/index.js +1 -0
  13. package/lib/module/jotai/components/JotaiAtomBrowser.js +33 -122
  14. package/lib/module/jotai/components/JotaiAtomChangeItem.js +19 -7
  15. package/lib/module/jotai/components/JotaiAtomDetailContent.js +41 -5
  16. package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +442 -0
  17. package/lib/module/jotai/components/JotaiEventFilterView.js +15 -0
  18. package/lib/module/jotai/components/JotaiModal.js +160 -24
  19. package/lib/module/jotai/utils/jotaiStateStore.js +9 -4
  20. package/lib/module/jotai/utils/testIDs.js +82 -0
  21. package/lib/module/jotai/utils/watchAtoms.js +8 -2
  22. package/lib/module/jotai/utils/writeSemantics.js +43 -0
  23. package/lib/typescript/index.d.ts +1 -0
  24. package/lib/typescript/index.d.ts.map +1 -1
  25. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +6 -5
  26. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  27. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts +5 -0
  28. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  29. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  30. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts +40 -0
  31. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +1 -0
  32. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  33. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  34. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +1 -0
  35. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  36. package/lib/typescript/jotai/types/index.d.ts +12 -0
  37. package/lib/typescript/jotai/types/index.d.ts.map +1 -1
  38. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +1 -1
  39. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  40. package/lib/typescript/jotai/utils/testIDs.d.ts +62 -0
  41. package/lib/typescript/jotai/utils/testIDs.d.ts.map +1 -0
  42. package/lib/typescript/jotai/utils/watchAtoms.d.ts +8 -2
  43. package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -1
  44. package/lib/typescript/jotai/utils/writeSemantics.d.ts +25 -0
  45. package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +1 -0
  46. package/package.json +3 -3
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.JotaiAtomValueDetailContent = JotaiAtomValueDetailContent;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
+ var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
11
+ var _jotaiStateStore = require("../utils/jotaiStateStore");
12
+ var _testIDs = require("../utils/testIDs");
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ /**
15
+ * One atom, on a screen of its own — the React Query detail-screen shape
16
+ * pointed at a live jotai atom. The jotai counterpart of zustand's
17
+ * `ZustandStoreDetailContent`; tapping an atom card opens this instead of
18
+ * expanding in place.
19
+ *
20
+ * 1. VALUE EDITOR — the shared LiveExplorer. No draft, no Save: an edit
21
+ * writes into the atom as you make it and the app reacts immediately.
22
+ * Typing commits on a short debounce (~300ms, flushed on blur) so a word
23
+ * is one recorded change, not one per keystroke — atom writes land in
24
+ * the change log and count against the free tier's cap.
25
+ * 2. ATOM DETAILS — what the expanded card used to say: type, write
26
+ * semantics, change count (with the road to history).
27
+ * 3. VALUE EXPLORER — the read-only tree of the same value.
28
+ *
29
+ * THE WRITE RULE — whole-value replace, jotai's own semantics. An atom is one
30
+ * value; there is no merge. However deep the edit, the writer rebuilds the
31
+ * whole value (`updateNestedDataByPath`) and hands it to `store.set(atom, …)`
32
+ * — so nested deletes are fine and only the ROOT itself can't be removed.
33
+ *
34
+ * Write-kind honesty (classified from jotai's own `defaultWrite`, see
35
+ * writeSemantics.ts):
36
+ * - "value" — primitive atom: `store.set` stores the edited value. Clean.
37
+ * - "action" — custom write (`atomWithStorage`, `atomWithReducer`, …): the
38
+ * edited value is passed to that write as its argument. For most
39
+ * (`atomWithStorage`) that IS a set; for a reducer it's an action. Still
40
+ * editable — the MCP's `setAtom` and Time Machine restore already write
41
+ * these — but the editor says what the write really does.
42
+ * - read-only derived — no setter exists; the editor section explains and
43
+ * the info/explorer sections still render.
44
+ */
45
+
46
+ /** How long typed edits coalesce before the single write fires. */const TYPING_COMMIT_MS = 300;
47
+
48
+ /** `buoy-jotai-atom-detail-…` — this screen's own handle root. */
49
+ const DETAIL_ID = `${_testIDs.JOTAI_TEST_ID}-atom-detail`;
50
+ function detailExplorerTestID(dataPath) {
51
+ const slug = part => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
52
+ return dataPath.length === 0 ? `${DETAIL_ID}-explorer-root` : `${DETAIL_ID}-explorer-${dataPath.map(slug).filter(Boolean).join("-")}`;
53
+ }
54
+
55
+ /**
56
+ * Why this atom's value can't be live-edited, or null when it can.
57
+ *
58
+ * The path utils handle scalars, arrays, plain objects, Map and Set. What
59
+ * they can't faithfully rebuild is a class instance (a leaf edit would spread
60
+ * it into a plain object, dropping its prototype) — and a mirror's
61
+ * on-device marker or a function value aren't editable data at all.
62
+ */
63
+ function editBlockedReasonFor(atom, value) {
64
+ if (!atom.setValue) {
65
+ return "Derived atom — its value recomputes from other atoms and has no setter. Edit the atoms it reads from instead.";
66
+ }
67
+ if (typeof value === "function") {
68
+ return "This atom holds a function — nothing here to edit as data.";
69
+ }
70
+ if (value && typeof value === "object" && value.__buoyValueOnDevice === true) {
71
+ return "The value stayed on the device (too large for the wire) — open the tool on-device to edit it.";
72
+ }
73
+ if (value && typeof value === "object") {
74
+ const proto = Object.getPrototypeOf(value);
75
+ const isPlain = proto === Object.prototype || proto === null || Array.isArray(value) || value instanceof Map || value instanceof Set;
76
+ if (!isPlain) {
77
+ const name = value.constructor?.name ?? "class";
78
+ return `This atom holds a ${name} instance — rebuilding it from edited fields would drop its prototype. Edit via app code.`;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ function valueTypeLabel(value) {
84
+ if (value === null) return "null";
85
+ if (value === undefined) return "undefined";
86
+ if (Array.isArray(value)) return `array · ${value.length}`;
87
+ return typeof value;
88
+ }
89
+ function JotaiAtomValueDetailContent({
90
+ atom,
91
+ onViewHistory
92
+ }) {
93
+ const rowID = (0, _testIDs.atomRowTestID)(atom.label);
94
+
95
+ // Re-render on every recorded change — our own edits included — so the
96
+ // editor, the read-only explorer, and the change count all track the live
97
+ // atom. Changes and registry updates arrive on separate listener sets, so
98
+ // subscribe to both. On a desktop mirror the snapshot refresh drives the
99
+ // same listeners.
100
+ const [version, setVersion] = (0, _react.useState)(0);
101
+ (0, _react.useEffect)(() => {
102
+ const bump = () => setVersion(v => v + 1);
103
+ const unsubChanges = _jotaiStateStore.jotaiStateStore.subscribe(bump);
104
+ const unsubAtoms = _jotaiStateStore.jotaiStateStore.subscribeToAtoms(bump);
105
+ return () => {
106
+ unsubChanges();
107
+ unsubAtoms();
108
+ };
109
+ }, []);
110
+
111
+ // Recomputed per change on purpose — this screen edits the LIVE atom, so
112
+ // every view of it derives from the atom's current answer, never a seed.
113
+ const value = (0, _react.useMemo)(() => {
114
+ try {
115
+ return atom.getValue();
116
+ } catch {
117
+ return undefined;
118
+ }
119
+ },
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ [atom, version]);
122
+ const blockedReason = editBlockedReasonFor(atom, value);
123
+ const [rawOpen, setRawOpen] = (0, _react.useState)(false);
124
+ const writer = (0, _react.useMemo)(() => {
125
+ if (blockedReason !== null) return undefined;
126
+ return {
127
+ update: (path, next) => {
128
+ const current = atom.getValue();
129
+ atom.setValue?.(path.length === 0 ? next : (0, _dataViewer.updateNestedDataByPath)(current, path, next));
130
+ },
131
+ remove: path => {
132
+ // Depth 0 is vetoed by canRemove; an atom is one value — you can
133
+ // empty it, not remove it.
134
+ if (path.length === 0) return;
135
+ const current = atom.getValue();
136
+ atom.setValue?.((0, _dataViewer.deleteNestedDataByPath)(current, path));
137
+ },
138
+ canRemove: path => path.length > 0
139
+ };
140
+ }, [atom, blockedReason]);
141
+ const displayValue = (0, _react.useMemo)(() => {
142
+ if (value && typeof value === "object") {
143
+ const filtered = {};
144
+ for (const [key, v] of Object.entries(value)) {
145
+ if (typeof v !== "function") filtered[key] = v;
146
+ }
147
+ return (0, _sharedUi.parseValue)(filtered);
148
+ }
149
+ return (0, _sharedUi.parseValue)(value);
150
+ // eslint-disable-next-line react-hooks/exhaustive-deps
151
+ }, [value, version]);
152
+
153
+ /** Raw-hatch save: whole-value edit, one write. */
154
+ const applyRaw = (0, _react.useCallback)(next => {
155
+ atom.setValue?.(next);
156
+ setRawOpen(false);
157
+ }, [atom]);
158
+ const writePill = atom.setValue === undefined ? {
159
+ color: _sharedUi.night.color.textTertiary,
160
+ label: "READ-ONLY (DERIVED)"
161
+ } : atom.writeKind === "action" ? {
162
+ color: _sharedUi.buoyColors.warning,
163
+ label: "CUSTOM WRITE"
164
+ } : {
165
+ color: _sharedUi.buoyColors.success,
166
+ label: "PRIMITIVE"
167
+ };
168
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
169
+ style: styles.root,
170
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, {
171
+ testID: `${DETAIL_ID}-scroll`,
172
+ style: styles.scroll,
173
+ contentContainerStyle: styles.scrollContent,
174
+ keyboardShouldPersistTaps: "handled",
175
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
176
+ style: styles.section,
177
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
178
+ style: styles.editorContainer,
179
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
180
+ style: styles.sectionHeaderRow,
181
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
182
+ testID: `${DETAIL_ID}-editor-header`,
183
+ style: styles.sectionHeaderText,
184
+ children: "Value Editor"
185
+ }), blockedReason === null ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
186
+ onPress: () => setRawOpen(true),
187
+ testID: `${DETAIL_ID}-raw`,
188
+ accessibilityRole: "button",
189
+ accessibilityLabel: "Edit raw JSON",
190
+ style: styles.rawButton,
191
+ hitSlop: {
192
+ top: 6,
193
+ bottom: 6,
194
+ left: 6,
195
+ right: 6
196
+ },
197
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FileJson, {
198
+ size: 13,
199
+ color: _sharedUi.night.color.accent
200
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
201
+ style: styles.rawButtonText,
202
+ children: "Raw"
203
+ })]
204
+ }) : null]
205
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
206
+ style: styles.sectionBody,
207
+ children: blockedReason === null ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
208
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.LiveExplorer, {
209
+ editable: true,
210
+ label: atom.label,
211
+ value: value,
212
+ defaultExpanded: [atom.label],
213
+ writer: writer,
214
+ dataVersion: version,
215
+ testIDForPath: detailExplorerTestID,
216
+ debounceMs: TYPING_COMMIT_MS
217
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
218
+ style: styles.editorHint,
219
+ children: atom.writeKind === "action" ? "This atom has a custom write — the edited value is handed to it as its argument (a set for atomWithStorage, an ACTION for a reducer atom)." : "Edits write into the live atom as you make them — the whole value is replaced, jotai's own semantics."
220
+ })]
221
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
222
+ style: styles.blockedText,
223
+ testID: `${DETAIL_ID}-edit-blocked`,
224
+ children: blockedReason
225
+ })
226
+ })]
227
+ })
228
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
229
+ style: styles.section,
230
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
231
+ style: styles.infoContainer,
232
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
233
+ style: styles.sectionHeaderTextPlain,
234
+ children: "Atom Details"
235
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
236
+ style: styles.infoBody,
237
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ExpandedInfoRow, {
238
+ label: "Type",
239
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PillBadge, {
240
+ color: atom.color,
241
+ testID: `${rowID}-type`,
242
+ children: "JOTAI"
243
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
244
+ style: styles.valueTypeText,
245
+ testID: `${rowID}-valuetype`,
246
+ children: valueTypeLabel(value)
247
+ })]
248
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ExpandedInfoRow, {
249
+ label: "Write",
250
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PillBadge, {
251
+ color: writePill.color,
252
+ testID: `${DETAIL_ID}-write`,
253
+ children: writePill.label
254
+ })
255
+ }), atom.changeCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ExpandedInfoRow, {
256
+ label: "Changes",
257
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PillBadge, {
258
+ color: _sharedUi.buoyColors.warning,
259
+ testID: `${rowID}-changes`,
260
+ children: String(atom.changeCount)
261
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
262
+ onPress: () => onViewHistory(atom.label),
263
+ style: styles.inlineLinkButton,
264
+ hitSlop: {
265
+ top: 6,
266
+ bottom: 6,
267
+ left: 6,
268
+ right: 6
269
+ },
270
+ testID: `${rowID}-history`,
271
+ accessibilityRole: "button",
272
+ accessibilityLabel: `View ${atom.label} history`,
273
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
274
+ style: [styles.inlineLinkText, {
275
+ color: atom.color
276
+ }],
277
+ children: "view history \u2192"
278
+ })
279
+ })]
280
+ })]
281
+ })]
282
+ })
283
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
284
+ style: styles.section,
285
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
286
+ style: styles.infoContainer,
287
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
288
+ style: styles.sectionHeaderTextPlain,
289
+ children: "Value Explorer"
290
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
291
+ style: styles.viewerBody,
292
+ testID: `${rowID}-value`,
293
+ children: displayValue && typeof displayValue === "object" ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
294
+ title: "",
295
+ data: displayValue,
296
+ showTypeFilter: true,
297
+ rawMode: true,
298
+ initialExpanded: false
299
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
300
+ style: styles.primitiveText,
301
+ testID: `${rowID}-primitive`,
302
+ children: String(value ?? "undefined")
303
+ })
304
+ })]
305
+ })
306
+ })]
307
+ }), rawOpen && blockedReason === null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.ValueEditModal, {
308
+ path: [],
309
+ value: value,
310
+ onSave: applyRaw,
311
+ onCancel: () => setRawOpen(false),
312
+ saveLabel: "Apply",
313
+ testIDPrefix: `${_testIDs.JOTAI_TEST_ID}-value`
314
+ }) : null]
315
+ });
316
+ }
317
+ const styles = _reactNative.StyleSheet.create({
318
+ root: {
319
+ flex: 1,
320
+ minHeight: 0
321
+ },
322
+ scroll: {
323
+ flex: 1
324
+ },
325
+ scrollContent: {
326
+ paddingHorizontal: 8,
327
+ paddingTop: 8,
328
+ paddingBottom: 24
329
+ },
330
+ section: {
331
+ marginBottom: 16
332
+ },
333
+ // The editable section wears the accent, like RQ's Data Editor — the one
334
+ // box on this screen that changes things should read differently from the
335
+ // two that don't.
336
+ editorContainer: {
337
+ minWidth: 200,
338
+ backgroundColor: _sharedUi.night.color.surface,
339
+ borderRadius: 6,
340
+ borderWidth: 1,
341
+ borderColor: _sharedUi.night.color.accent + "4D",
342
+ overflow: "hidden",
343
+ shadowColor: _sharedUi.night.color.accent,
344
+ shadowOffset: {
345
+ width: 0,
346
+ height: 0
347
+ },
348
+ shadowOpacity: 0.1,
349
+ shadowRadius: 6
350
+ },
351
+ sectionHeaderRow: {
352
+ flexDirection: "row",
353
+ alignItems: "center",
354
+ justifyContent: "space-between",
355
+ backgroundColor: _sharedUi.night.color.accent + "1A",
356
+ paddingHorizontal: 12,
357
+ paddingVertical: 8,
358
+ borderBottomWidth: 1,
359
+ borderBottomColor: _sharedUi.night.color.accent + "33"
360
+ },
361
+ sectionHeaderText: {
362
+ fontWeight: "600",
363
+ fontSize: 12,
364
+ color: _sharedUi.night.color.accent,
365
+ letterSpacing: 0.5,
366
+ textTransform: "uppercase",
367
+ fontFamily: "monospace"
368
+ },
369
+ rawButton: {
370
+ flexDirection: "row",
371
+ alignItems: "center",
372
+ gap: 4
373
+ },
374
+ rawButtonText: {
375
+ color: _sharedUi.night.color.accent,
376
+ fontSize: 11,
377
+ fontWeight: "700"
378
+ },
379
+ sectionBody: {
380
+ padding: 8
381
+ },
382
+ editorHint: {
383
+ color: _sharedUi.night.color.textTertiary,
384
+ fontSize: 10,
385
+ paddingTop: 8,
386
+ paddingHorizontal: 4,
387
+ lineHeight: 14
388
+ },
389
+ blockedText: {
390
+ color: _sharedUi.night.color.textSecondary,
391
+ fontSize: 12,
392
+ padding: 12,
393
+ lineHeight: 18
394
+ },
395
+ infoContainer: {
396
+ minWidth: 200,
397
+ backgroundColor: _sharedUi.night.color.surface,
398
+ borderRadius: 6,
399
+ borderWidth: 1,
400
+ borderColor: _sharedUi.night.color.border,
401
+ overflow: "hidden"
402
+ },
403
+ sectionHeaderTextPlain: {
404
+ backgroundColor: _sharedUi.night.color.surfaceElevated,
405
+ paddingHorizontal: 12,
406
+ paddingVertical: 8,
407
+ fontWeight: "600",
408
+ fontSize: 12,
409
+ color: _sharedUi.night.color.textSecondary,
410
+ borderBottomWidth: 1,
411
+ borderBottomColor: _sharedUi.night.color.border,
412
+ letterSpacing: 0.5,
413
+ textTransform: "uppercase",
414
+ fontFamily: "monospace"
415
+ },
416
+ infoBody: {
417
+ padding: 12,
418
+ gap: 10
419
+ },
420
+ viewerBody: {
421
+ backgroundColor: _sharedUi.night.color.bg,
422
+ minHeight: 60
423
+ },
424
+ primitiveText: {
425
+ color: _sharedUi.night.color.text,
426
+ fontSize: 12,
427
+ fontFamily: "monospace",
428
+ padding: 14
429
+ },
430
+ valueTypeText: {
431
+ color: _sharedUi.night.color.textTertiary,
432
+ fontSize: 10,
433
+ fontFamily: "monospace",
434
+ marginLeft: 6
435
+ },
436
+ inlineLinkButton: {
437
+ marginLeft: 4
438
+ },
439
+ inlineLinkText: {
440
+ fontSize: 10,
441
+ fontWeight: "600",
442
+ fontFamily: "monospace"
443
+ }
444
+ });
@@ -8,6 +8,7 @@ var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _jotaiStateStore = require("../utils/jotaiStateStore");
11
+ var _testIDs = require("../utils/testIDs");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  /**
13
14
  * JotaiEventFilterView
@@ -39,6 +40,7 @@ function JotaiEventFilterView({
39
40
  contentContainerStyle: styles.scrollContent,
40
41
  showsVerticalScrollIndicator: false,
41
42
  nestedScrollEnabled: true,
43
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-scroll`,
42
44
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
43
45
  style: styles.section,
44
46
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.SectionHeader, {
@@ -51,11 +53,13 @@ function JotaiEventFilterView({
51
53
  })]
52
54
  }), atoms.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
53
55
  style: styles.emptyText,
56
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-atoms-empty`,
54
57
  children: "No atoms registered yet."
55
58
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
56
59
  style: styles.atomList,
57
60
  contentContainerStyle: styles.atomListContent,
58
61
  nestedScrollEnabled: true,
62
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-atom-list`,
59
63
  children: atoms.map(atom => {
60
64
  const atomColor = _jotaiStateStore.jotaiStateStore.getAtomColor(atom.label);
61
65
  const isFiltered = Array.from(ignoredPatterns).some(p => atom.label.toLowerCase().includes(p.toLowerCase()));
@@ -63,6 +67,12 @@ function JotaiEventFilterView({
63
67
  style: [styles.atomRow, isFiltered && styles.atomRowFiltered],
64
68
  onPress: () => onTogglePattern(atom.label),
65
69
  activeOpacity: 0.7,
70
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-atom-${(0, _testIDs.slugForTestID)(atom.label)}`,
71
+ accessibilityRole: "button",
72
+ accessibilityLabel: isFiltered ? `Show ${atom.label}` : `Hide ${atom.label}`,
73
+ accessibilityState: {
74
+ selected: isFiltered
75
+ },
66
76
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
67
77
  style: [styles.atomDot, {
68
78
  backgroundColor: atomColor
@@ -87,6 +97,8 @@ function JotaiEventFilterView({
87
97
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
88
98
  style: styles.addInputWrapper,
89
99
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
100
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-add-input`,
101
+ accessibilityLabel: "Add an ignore pattern",
90
102
  value: newPattern,
91
103
  onChangeText: setNewPattern,
92
104
  onSubmitEditing: handleAddPattern,
@@ -116,6 +128,9 @@ function JotaiEventFilterView({
116
128
  style: styles.patternRow,
117
129
  onPress: () => onTogglePattern(pattern),
118
130
  activeOpacity: 0.8,
131
+ testID: `${_testIDs.JOTAI_TEST_ID}-filters-chip-${(0, _testIDs.slugForTestID)(pattern)}`,
132
+ accessibilityRole: "button",
133
+ accessibilityLabel: `Remove filter ${pattern}`,
119
134
  children: [atomColor ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
120
135
  style: [styles.atomDot, {
121
136
  backgroundColor: atomColor