@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.
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +30 -119
- package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +19 -7
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +41 -5
- package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +444 -0
- package/lib/commonjs/jotai/components/JotaiEventFilterView.js +15 -0
- package/lib/commonjs/jotai/components/JotaiModal.js +159 -23
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +9 -4
- package/lib/commonjs/jotai/utils/testIDs.js +90 -0
- package/lib/commonjs/jotai/utils/watchAtoms.js +8 -2
- package/lib/commonjs/jotai/utils/writeSemantics.js +47 -0
- package/lib/module/index.js +1 -0
- package/lib/module/jotai/components/JotaiAtomBrowser.js +33 -122
- package/lib/module/jotai/components/JotaiAtomChangeItem.js +19 -7
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +41 -5
- package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +442 -0
- package/lib/module/jotai/components/JotaiEventFilterView.js +15 -0
- package/lib/module/jotai/components/JotaiModal.js +160 -24
- package/lib/module/jotai/utils/jotaiStateStore.js +9 -4
- package/lib/module/jotai/utils/testIDs.js +82 -0
- package/lib/module/jotai/utils/watchAtoms.js +8 -2
- package/lib/module/jotai/utils/writeSemantics.js +43 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +6 -5
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts +5 -0
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts +40 -0
- package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +1 -0
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +1 -0
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/jotai/types/index.d.ts +12 -0
- package/lib/typescript/jotai/types/index.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +1 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/testIDs.d.ts +62 -0
- package/lib/typescript/jotai/utils/testIDs.d.ts.map +1 -0
- package/lib/typescript/jotai/utils/watchAtoms.d.ts +8 -2
- package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -1
- package/lib/typescript/jotai/utils/writeSemantics.d.ts +25 -0
- package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +1 -0
- package/package.json +3 -3
|
@@ -13,7 +13,9 @@ var _useJotaiAtomChanges = require("../hooks/useJotaiAtomChanges");
|
|
|
13
13
|
var _JotaiAtomChangeItem = require("./JotaiAtomChangeItem");
|
|
14
14
|
var _JotaiAtomDetailContent = require("./JotaiAtomDetailContent");
|
|
15
15
|
var _JotaiAtomBrowser = require("./JotaiAtomBrowser");
|
|
16
|
+
var _JotaiAtomValueDetailContent = require("./JotaiAtomValueDetailContent");
|
|
16
17
|
var _JotaiEventFilterView = require("./JotaiEventFilterView");
|
|
18
|
+
var _testIDs = require("../utils/testIDs");
|
|
17
19
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
20
|
/**
|
|
19
21
|
* Main Jotai DevTools modal
|
|
@@ -23,19 +25,32 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
23
25
|
* - Events tab: Live atom change monitoring with diffs
|
|
24
26
|
*/
|
|
25
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);
|
|
26
38
|
function EventsEmptyState({
|
|
27
39
|
isEnabled
|
|
28
40
|
}) {
|
|
29
41
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
30
42
|
style: styles.emptyState,
|
|
43
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-empty`,
|
|
31
44
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
|
|
32
45
|
size: 32,
|
|
33
46
|
color: _sharedUi.night.color.textTertiary
|
|
34
47
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
35
48
|
style: styles.emptyTitle,
|
|
49
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-empty-title`,
|
|
36
50
|
children: "No atom changes"
|
|
37
51
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
38
52
|
style: styles.emptyText,
|
|
53
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-empty-text`,
|
|
39
54
|
children: isEnabled ? "Atom changes will appear here as values update." : "Enable capture to start recording atom changes"
|
|
40
55
|
})]
|
|
41
56
|
});
|
|
@@ -58,6 +73,9 @@ function JotaiModal({
|
|
|
58
73
|
const [showProModal, setShowProModal] = (0, _react.useState)(false);
|
|
59
74
|
const [activeTab, setActiveTab] = (0, _react.useState)(initialTab ?? "atoms");
|
|
60
75
|
const [selectedHistoryAtom, setSelectedHistoryAtom] = (0, _react.useState)(null);
|
|
76
|
+
// The atom whose DETAIL SCREEN (live editor) is open — tapping a card in
|
|
77
|
+
// the atoms tab navigates here, mirroring ZustandModal's selectedStoreName.
|
|
78
|
+
const [selectedAtomLabel, setSelectedAtomLabel] = (0, _react.useState)(null);
|
|
61
79
|
const [showFilters, setShowFilters] = (0, _react.useState)(false);
|
|
62
80
|
const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
|
|
63
81
|
const {
|
|
@@ -142,6 +160,16 @@ function JotaiModal({
|
|
|
142
160
|
}));
|
|
143
161
|
}
|
|
144
162
|
};
|
|
163
|
+
|
|
164
|
+
/** Drop the query AND put the header back. The X, and only the X. */
|
|
165
|
+
const closeSearch = (0, _react.useCallback)(() => {
|
|
166
|
+
setSearchText("");
|
|
167
|
+
setFilter(prev => ({
|
|
168
|
+
...prev,
|
|
169
|
+
searchText: ""
|
|
170
|
+
}));
|
|
171
|
+
setIsSearchActive(false);
|
|
172
|
+
}, [setFilter]);
|
|
145
173
|
const handleChangePress = (0, _react.useCallback)(change => {
|
|
146
174
|
setSelectedChangeId(change.id);
|
|
147
175
|
}, []);
|
|
@@ -163,6 +191,16 @@ function JotaiModal({
|
|
|
163
191
|
setSelectedHistoryAtom(null);
|
|
164
192
|
setSelectedChangeId(null);
|
|
165
193
|
}, []);
|
|
194
|
+
const handleOpenAtom = (0, _react.useCallback)(label => {
|
|
195
|
+
setSelectedAtomLabel(label);
|
|
196
|
+
// The desktop mirror hydrates an atom's full value on demand — this is
|
|
197
|
+
// the moment it becomes needed (was fired on card-expand before the
|
|
198
|
+
// detail screen replaced expansion).
|
|
199
|
+
onAtomExpand?.(label);
|
|
200
|
+
}, [onAtomExpand]);
|
|
201
|
+
const handleBackFromAtom = (0, _react.useCallback)(() => {
|
|
202
|
+
setSelectedAtomLabel(null);
|
|
203
|
+
}, []);
|
|
166
204
|
const getAtomsSnapshot = (0, _react.useCallback)(() => {
|
|
167
205
|
return displayedAtoms.reduce((acc, atom) => {
|
|
168
206
|
try {
|
|
@@ -221,12 +259,19 @@ function JotaiModal({
|
|
|
221
259
|
setIsSearchActive(false);
|
|
222
260
|
}, [searchText, setFilter]);
|
|
223
261
|
const keyExtractor = (0, _react.useCallback)(item => item.id, []);
|
|
262
|
+
|
|
263
|
+
// Built for the whole list rather than per row: the ordinal a row carries is
|
|
264
|
+
// a fact about the list around it. See ../utils/testIDs.ts for why an atom
|
|
265
|
+
// label alone cannot address a change row — and why the ordinal is counted
|
|
266
|
+
// per atom when one write can move several.
|
|
267
|
+
const rowTestIDs = (0, _react.useMemo)(() => (0, _testIDs.buildChangeRowTestIDs)(activeChanges), [activeChanges]);
|
|
224
268
|
const renderItem = (0, _react.useCallback)(({
|
|
225
269
|
item
|
|
226
270
|
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomChangeItem.JotaiAtomChangeItem, {
|
|
227
271
|
change: item,
|
|
228
|
-
onPress: handleChangePress
|
|
229
|
-
|
|
272
|
+
onPress: handleChangePress,
|
|
273
|
+
testID: rowTestIDs.get(item.id)
|
|
274
|
+
}), [handleChangePress, rowTestIDs]);
|
|
230
275
|
|
|
231
276
|
// ---- Header ----
|
|
232
277
|
|
|
@@ -238,7 +283,8 @@ function JotaiModal({
|
|
|
238
283
|
onBack: () => setShowFilters(false)
|
|
239
284
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
240
285
|
title: "Filters",
|
|
241
|
-
centered: true
|
|
286
|
+
centered: true,
|
|
287
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-filters-title`
|
|
242
288
|
})]
|
|
243
289
|
});
|
|
244
290
|
}
|
|
@@ -248,7 +294,8 @@ function JotaiModal({
|
|
|
248
294
|
onBack: handleBack
|
|
249
295
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
250
296
|
title: `${selectedChange.atomLabel}/${selectedChange.category}`,
|
|
251
|
-
centered: true
|
|
297
|
+
centered: true,
|
|
298
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-detail-title`
|
|
252
299
|
})]
|
|
253
300
|
});
|
|
254
301
|
}
|
|
@@ -258,16 +305,33 @@ function JotaiModal({
|
|
|
258
305
|
onBack: handleBackFromHistory
|
|
259
306
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
260
307
|
title: `${selectedHistoryAtom} History`,
|
|
261
|
-
centered: true
|
|
308
|
+
centered: true,
|
|
309
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-history-title`
|
|
262
310
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
|
|
263
311
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
264
312
|
value: getHistorySnapshot,
|
|
265
313
|
disabled: activeChanges.length === 0,
|
|
266
|
-
buttonStyle: styles.headerActionButton
|
|
314
|
+
buttonStyle: styles.headerActionButton,
|
|
315
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
267
316
|
})
|
|
268
317
|
})]
|
|
269
318
|
});
|
|
270
319
|
}
|
|
320
|
+
|
|
321
|
+
// Atom detail header — the live editor screen. AFTER history: history is
|
|
322
|
+
// reached FROM the detail screen and stacks over it, so when both are set
|
|
323
|
+
// history owns the header, and its back lands you back on the detail.
|
|
324
|
+
if (selectedAtomLabel) {
|
|
325
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
326
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
327
|
+
onBack: handleBackFromAtom
|
|
328
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
329
|
+
title: selectedAtomLabel,
|
|
330
|
+
centered: true,
|
|
331
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-atom-detail-title`
|
|
332
|
+
})]
|
|
333
|
+
});
|
|
334
|
+
}
|
|
271
335
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
272
336
|
children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
273
337
|
onBack: onBack
|
|
@@ -275,27 +339,42 @@ function JotaiModal({
|
|
|
275
339
|
title: "",
|
|
276
340
|
children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
277
341
|
style: styles.headerSearchContainer,
|
|
342
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-search`,
|
|
278
343
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
279
344
|
size: 14,
|
|
280
345
|
color: _sharedUi.night.color.textSecondary
|
|
281
346
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
|
|
282
347
|
ref: searchInputRef,
|
|
348
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-search-field`,
|
|
283
349
|
style: styles.headerSearchInput,
|
|
284
350
|
placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
|
|
285
351
|
placeholderTextColor: _sharedUi.night.color.placeholder,
|
|
286
352
|
value: searchText,
|
|
287
353
|
onChangeText: handleSearch,
|
|
288
|
-
onSubmitEditing: () =>
|
|
289
|
-
|
|
354
|
+
onSubmitEditing: () => searchInputRef.current?.blur()
|
|
355
|
+
// Stay open while the query is still narrowing the list.
|
|
356
|
+
//
|
|
357
|
+
// Collapsing on blur hid the reason rows were missing: the
|
|
358
|
+
// header went back to its TABS — whose counts are of the
|
|
359
|
+
// WHOLE capture — while the list stayed filtered, so the tool
|
|
360
|
+
// showed "Atoms (8)" above a single row and offered nothing to
|
|
361
|
+
// explain the gap. The only way back was to reopen a field the
|
|
362
|
+
// user had no reason to think was still armed. Same shape, and
|
|
363
|
+
// the same fix, as the Redux, Events and Zustand tools.
|
|
364
|
+
,
|
|
365
|
+
onBlur: () => {
|
|
366
|
+
if (searchText.length === 0) setIsSearchActive(false);
|
|
367
|
+
},
|
|
368
|
+
accessibilityLabel: "Search Jotai atoms and events",
|
|
290
369
|
autoCapitalize: "none",
|
|
291
370
|
autoCorrect: false,
|
|
292
371
|
returnKeyType: "search"
|
|
293
|
-
}),
|
|
294
|
-
onPress:
|
|
295
|
-
handleSearch("");
|
|
296
|
-
setIsSearchActive(false);
|
|
297
|
-
},
|
|
372
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
373
|
+
onPress: closeSearch,
|
|
298
374
|
style: styles.headerSearchClear,
|
|
375
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-search-clear`,
|
|
376
|
+
accessibilityRole: "button",
|
|
377
|
+
accessibilityLabel: "Close search",
|
|
299
378
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.X, {
|
|
300
379
|
size: 14,
|
|
301
380
|
color: _sharedUi.night.color.textSecondary
|
|
@@ -310,12 +389,16 @@ function JotaiModal({
|
|
|
310
389
|
label: `Events${displayedChanges.length > 0 ? ` (${displayedChanges.length})` : ""}`
|
|
311
390
|
}],
|
|
312
391
|
activeTab: activeTab,
|
|
313
|
-
onTabChange: handleTabChange
|
|
392
|
+
onTabChange: handleTabChange,
|
|
393
|
+
testIDPrefix: `${_testIDs.JOTAI_TEST_ID}-tab`
|
|
314
394
|
})
|
|
315
395
|
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
|
|
316
396
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
317
397
|
onPress: () => setIsSearchActive(true),
|
|
318
398
|
style: styles.headerActionButton,
|
|
399
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-search-open`,
|
|
400
|
+
accessibilityRole: "button",
|
|
401
|
+
accessibilityLabel: "Search",
|
|
319
402
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
320
403
|
size: 14,
|
|
321
404
|
color: _sharedUi.night.color.textSecondary
|
|
@@ -323,6 +406,9 @@ function JotaiModal({
|
|
|
323
406
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
324
407
|
onPress: () => setShowFilters(true),
|
|
325
408
|
style: [styles.headerActionButton, hasActiveFilters && styles.headerActionButtonActive],
|
|
409
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-filters-open`,
|
|
410
|
+
accessibilityRole: "button",
|
|
411
|
+
accessibilityLabel: "Filters",
|
|
326
412
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
|
|
327
413
|
size: 14,
|
|
328
414
|
color: hasActiveFilters ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
|
|
@@ -330,20 +416,24 @@ function JotaiModal({
|
|
|
330
416
|
}), activeTab === "atoms" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
331
417
|
value: getAtomsSnapshot,
|
|
332
418
|
disabled: displayedAtoms.length === 0,
|
|
333
|
-
buttonStyle: styles.headerActionButton
|
|
419
|
+
buttonStyle: styles.headerActionButton,
|
|
420
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
334
421
|
}), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
335
422
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
336
423
|
value: getEventsSnapshot,
|
|
337
424
|
disabled: displayedChanges.length === 0,
|
|
338
|
-
buttonStyle: styles.headerActionButton
|
|
425
|
+
buttonStyle: styles.headerActionButton,
|
|
426
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
339
427
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
340
428
|
isEnabled: isEnabled,
|
|
341
429
|
onToggle: toggleCapture,
|
|
342
|
-
accessibilityLabel: "Toggle Jotai atom capture"
|
|
430
|
+
accessibilityLabel: "Toggle Jotai atom capture",
|
|
431
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-power`
|
|
343
432
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarClearButton, {
|
|
344
433
|
onPress: clearChanges,
|
|
345
434
|
disabled: displayedChanges.length === 0,
|
|
346
|
-
buttonStyle: styles.headerActionButton
|
|
435
|
+
buttonStyle: styles.headerActionButton,
|
|
436
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-clear`
|
|
347
437
|
})]
|
|
348
438
|
})]
|
|
349
439
|
})]
|
|
@@ -373,18 +463,20 @@ function JotaiModal({
|
|
|
373
463
|
}
|
|
374
464
|
if (selectedHistoryAtom) {
|
|
375
465
|
return activeChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
466
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-history-list`,
|
|
376
467
|
data: activeChanges,
|
|
377
468
|
renderItem: renderItem,
|
|
378
469
|
keyExtractor: keyExtractor,
|
|
379
470
|
contentContainerStyle: styles.listContent,
|
|
380
471
|
showsVerticalScrollIndicator: true,
|
|
381
|
-
removeClippedSubviews:
|
|
472
|
+
removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
|
|
382
473
|
initialNumToRender: 12,
|
|
383
474
|
maxToRenderPerBatch: 8,
|
|
384
475
|
windowSize: 5,
|
|
385
476
|
updateCellsBatchingPeriod: 50
|
|
386
477
|
}) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
387
478
|
style: styles.emptyState,
|
|
479
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-history-empty`,
|
|
388
480
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
|
|
389
481
|
size: 32,
|
|
390
482
|
color: _sharedUi.night.color.textTertiary
|
|
@@ -397,12 +489,39 @@ function JotaiModal({
|
|
|
397
489
|
})]
|
|
398
490
|
});
|
|
399
491
|
}
|
|
492
|
+
|
|
493
|
+
// Atom detail — live editor + details + read-only tree. After history in
|
|
494
|
+
// this chain for the same reason as the header: history stacks OVER the
|
|
495
|
+
// detail screen it was opened from.
|
|
496
|
+
if (selectedAtomLabel) {
|
|
497
|
+
const selectedAtom = atoms.find(a => a.label === selectedAtomLabel) ?? null;
|
|
498
|
+
return selectedAtom ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomValueDetailContent.JotaiAtomValueDetailContent, {
|
|
499
|
+
atom: selectedAtom,
|
|
500
|
+
onViewHistory: handleViewHistory
|
|
501
|
+
}) :
|
|
502
|
+
/*#__PURE__*/
|
|
503
|
+
// The atom fell out of the registry mid-look (a remote mirror
|
|
504
|
+
// rebuilding, a dev reload). Say so rather than rendering nothing.
|
|
505
|
+
(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
506
|
+
style: styles.emptyState,
|
|
507
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-atom-detail-gone`,
|
|
508
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Box, {
|
|
509
|
+
size: 32,
|
|
510
|
+
color: _sharedUi.night.color.textTertiary
|
|
511
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
512
|
+
style: styles.emptyTitle,
|
|
513
|
+
children: "Atom no longer registered"
|
|
514
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
515
|
+
style: styles.emptyText,
|
|
516
|
+
children: [selectedAtomLabel, " is not in the registry any more."]
|
|
517
|
+
})]
|
|
518
|
+
});
|
|
519
|
+
}
|
|
400
520
|
if (activeTab === "atoms") {
|
|
401
521
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomBrowser.JotaiAtomBrowser, {
|
|
402
522
|
atoms: displayedAtoms,
|
|
403
523
|
searchQuery: searchText,
|
|
404
|
-
|
|
405
|
-
onAtomExpand: onAtomExpand
|
|
524
|
+
onOpenAtom: handleOpenAtom
|
|
406
525
|
});
|
|
407
526
|
}
|
|
408
527
|
|
|
@@ -410,6 +529,7 @@ function JotaiModal({
|
|
|
410
529
|
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
411
530
|
children: [!isEnabled && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
412
531
|
style: styles.disabledBanner,
|
|
532
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-banner-disabled`,
|
|
413
533
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
|
|
414
534
|
size: 14,
|
|
415
535
|
color: _sharedUi.night.color.warning
|
|
@@ -421,8 +541,11 @@ function JotaiModal({
|
|
|
421
541
|
style: styles.lockedBanner,
|
|
422
542
|
onPress: handleUpgradePress,
|
|
423
543
|
activeOpacity: 0.8,
|
|
544
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-locked`,
|
|
545
|
+
accessibilityRole: "button",
|
|
424
546
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
425
547
|
style: styles.lockedText,
|
|
548
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-locked-text`,
|
|
426
549
|
children: [lockedChangeCount, " older", " ", lockedChangeCount === 1 ? "change" : "changes", " locked"]
|
|
427
550
|
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
428
551
|
style: styles.upgradeBadge,
|
|
@@ -433,12 +556,13 @@ function JotaiModal({
|
|
|
433
556
|
})]
|
|
434
557
|
}), visibleChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
435
558
|
ref: flatListRef,
|
|
559
|
+
testID: `${_testIDs.JOTAI_TEST_ID}-list`,
|
|
436
560
|
data: visibleChanges,
|
|
437
561
|
renderItem: renderItem,
|
|
438
562
|
keyExtractor: keyExtractor,
|
|
439
563
|
contentContainerStyle: styles.listContent,
|
|
440
564
|
showsVerticalScrollIndicator: true,
|
|
441
|
-
removeClippedSubviews:
|
|
565
|
+
removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
|
|
442
566
|
initialNumToRender: 12,
|
|
443
567
|
maxToRenderPerBatch: 8,
|
|
444
568
|
windowSize: 5,
|
|
@@ -460,7 +584,18 @@ function JotaiModal({
|
|
|
460
584
|
|
|
461
585
|
// The change lists own their scrolling so FlatList virtualization works
|
|
462
586
|
// (see ZustandModal — same pattern, same tool-bench evidence).
|
|
463
|
-
|
|
587
|
+
//
|
|
588
|
+
// The ATOM BROWSER owns its scrolling too, which is not just symmetry. Its
|
|
589
|
+
// root is a `flex: 1` ScrollView, and a `flex: 1` box inside JsModal's
|
|
590
|
+
// wrapper (`contentContainerStyle: { flexGrow: 1 }`) resolves against the
|
|
591
|
+
// CONTENT rather than the viewport — so nested, the browser's scroller grows
|
|
592
|
+
// to the full height of its rows, never scrolls, and the wrapper scrolls
|
|
593
|
+
// instead. A person cannot tell the difference; automation can, because the
|
|
594
|
+
// scroller Detox is handed reports a frame taller than the sheet and every
|
|
595
|
+
// scroll start point it computes lands in the clipped part ("view is not
|
|
596
|
+
// scrollable at the given start point"). Owning the scroll gives the browser
|
|
597
|
+
// a bounded height and one scroller with a handle on it.
|
|
598
|
+
const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryAtom != null || activeTab === "events" || activeTab === "atoms");
|
|
464
599
|
const body = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
465
600
|
style: styles.container,
|
|
466
601
|
children: renderContent()
|
|
@@ -479,6 +614,7 @@ function JotaiModal({
|
|
|
479
614
|
style: styles.inlineRoot,
|
|
480
615
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
481
616
|
style: styles.inlineHeader,
|
|
617
|
+
..._sharedUi.windowDragProps,
|
|
482
618
|
children: renderHeaderContent()
|
|
483
619
|
}), body, footerNode]
|
|
484
620
|
}), upgradeModal]
|
|
@@ -277,7 +277,8 @@ class JotaiStateStore {
|
|
|
277
277
|
label: atom.label,
|
|
278
278
|
changeCount: atom.changeCount,
|
|
279
279
|
color: atom.color,
|
|
280
|
-
currentValue
|
|
280
|
+
currentValue,
|
|
281
|
+
writeKind: atom.writeKind
|
|
281
282
|
};
|
|
282
283
|
});
|
|
283
284
|
}
|
|
@@ -332,7 +333,10 @@ class JotaiStateStore {
|
|
|
332
333
|
label: snapshot.label,
|
|
333
334
|
changeCount: snapshot.changeCount,
|
|
334
335
|
color: snapshot.color,
|
|
335
|
-
getValue: () => currentValue
|
|
336
|
+
getValue: () => currentValue,
|
|
337
|
+
// No setValue: the mirror has no wire back to the device from
|
|
338
|
+
// this entry (yet) — the detail screen renders read-only there.
|
|
339
|
+
writeKind: snapshot.writeKind
|
|
336
340
|
}];
|
|
337
341
|
}));
|
|
338
342
|
this.notifyListeners();
|
|
@@ -369,14 +373,15 @@ class JotaiStateStore {
|
|
|
369
373
|
|
|
370
374
|
// ---- Atom Registry ----
|
|
371
375
|
|
|
372
|
-
registerAtom(label, getValue, setValue) {
|
|
376
|
+
registerAtom(label, getValue, setValue, writeKind) {
|
|
373
377
|
if (this.atoms.has(label)) return;
|
|
374
378
|
const atomInfo = {
|
|
375
379
|
label,
|
|
376
380
|
changeCount: 0,
|
|
377
381
|
color: getAtomColor(label),
|
|
378
382
|
getValue,
|
|
379
|
-
setValue
|
|
383
|
+
setValue,
|
|
384
|
+
writeKind
|
|
380
385
|
};
|
|
381
386
|
this.atoms.set(label, atomInfo);
|
|
382
387
|
this.notifyAtomListeners();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.JOTAI_TEST_ID = void 0;
|
|
7
|
+
exports.atomRowTestID = atomRowTestID;
|
|
8
|
+
exports.buildChangeRowTestIDs = buildChangeRowTestIDs;
|
|
9
|
+
exports.changeRowTestID = changeRowTestID;
|
|
10
|
+
exports.slugForTestID = slugForTestID;
|
|
11
|
+
/**
|
|
12
|
+
* Jotai tool test handles.
|
|
13
|
+
*
|
|
14
|
+
* Every tappable and every value the e2e suite reads back is addressed
|
|
15
|
+
* `buoy-jotai-<thing>`. Two rules need explaining, and the second is the one
|
|
16
|
+
* this tool does not share with any other.
|
|
17
|
+
*
|
|
18
|
+
* **A change row's handle carries an ORDINAL, and it counts per ATOM.** Same
|
|
19
|
+
* reason as the Zustand tool: an atom a screen writes to fifty times produces
|
|
20
|
+
* fifty separate, equally real rows, and addressing them by label alone would
|
|
21
|
+
* give fifty views one handle — Detox fails a *matcher* that hits more than
|
|
22
|
+
* one element, so the suite would break on `toExist()` before any assertion,
|
|
23
|
+
* with an error that says nothing about duplicate changes. The handle is
|
|
24
|
+
* `buoy-jotai-row-<slug(atomLabel)>-<n>`, n counting back from the newest row
|
|
25
|
+
* for that atom (1 = newest). Deliberately relative: an atom change has no
|
|
26
|
+
* identity a host could know in advance, and what a test CAN know is "the
|
|
27
|
+
* write I just fired is the newest change to that atom". Read and tap in one
|
|
28
|
+
* beat — a row's ordinal moves the instant another change lands.
|
|
29
|
+
*
|
|
30
|
+
* **The ordinal is per ATOM, and one write can move several atoms.** This is
|
|
31
|
+
* where jotai differs from every store-shaped tool. Atoms form a dependency
|
|
32
|
+
* graph, and this tool watches NODES: a single `set` on a primitive atom
|
|
33
|
+
* notifies every mounted derived atom whose value actually moved, so one
|
|
34
|
+
* press can add a row under three different labels — and add nothing at all
|
|
35
|
+
* under a fourth whose recomputed value happened to be unchanged. Counting
|
|
36
|
+
* ordinals across the whole list would make a row's handle depend on how many
|
|
37
|
+
* *other* atoms the write happened to touch, which is exactly the thing a
|
|
38
|
+
* test of a graph must not assume.
|
|
39
|
+
*
|
|
40
|
+
* **An atom row's handle does NOT carry an ordinal.** An atom is registered
|
|
41
|
+
* once under a label that is unique by construction
|
|
42
|
+
* (`jotaiStateStore.registerAtom` returns early on a label it already holds,
|
|
43
|
+
* and `watchAtoms` marks the atom with a per-label symbol), so
|
|
44
|
+
* `buoy-jotai-atom-<slug(label)>` is as stable as the registration itself —
|
|
45
|
+
* and unlike a change, it is a thing the host knows the name of before the
|
|
46
|
+
* app has booted.
|
|
47
|
+
*
|
|
48
|
+
* The suite mirrors these rules in CommonJS (`example-dev-build/utils/
|
|
49
|
+
* jotaiBench.js`) and asserts the two agree — Detox runs in Node on the host
|
|
50
|
+
* and cannot import TypeScript.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** Prefix every handle in this tool shares. */
|
|
54
|
+
const JOTAI_TEST_ID = exports.JOTAI_TEST_ID = "buoy-jotai";
|
|
55
|
+
|
|
56
|
+
/** `jbenchCount` → `jbenchcount`. Shared with every Buoy tool. */
|
|
57
|
+
function slugForTestID(value) {
|
|
58
|
+
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle for one change row: `buoy-jotai-row-jbenchcount-1`.
|
|
63
|
+
*
|
|
64
|
+
* `ordinal` is 1-based and counts back from the newest row for this atom.
|
|
65
|
+
*/
|
|
66
|
+
function changeRowTestID(atomLabel, ordinal) {
|
|
67
|
+
return `${JOTAI_TEST_ID}-row-${slugForTestID(atomLabel)}-${ordinal}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Handle for one atom row in the browser: `buoy-jotai-atom-jbenchcount`. */
|
|
71
|
+
function atomRowTestID(atomLabel) {
|
|
72
|
+
return `${JOTAI_TEST_ID}-atom-${slugForTestID(atomLabel)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Row handle per change id for one rendered list, newest-first.
|
|
77
|
+
*
|
|
78
|
+
* Built once for the whole list rather than per row, because an ordinal is a
|
|
79
|
+
* fact about the list and a row cannot compute its own.
|
|
80
|
+
*/
|
|
81
|
+
function buildChangeRowTestIDs(changes) {
|
|
82
|
+
const seen = new Map();
|
|
83
|
+
const handles = new Map();
|
|
84
|
+
for (const change of changes) {
|
|
85
|
+
const ordinal = (seen.get(change.atomLabel) ?? 0) + 1;
|
|
86
|
+
seen.set(change.atomLabel, ordinal);
|
|
87
|
+
handles.set(change.id, changeRowTestID(change.atomLabel, ordinal));
|
|
88
|
+
}
|
|
89
|
+
return handles;
|
|
90
|
+
}
|
|
@@ -7,6 +7,7 @@ exports.isAtomWatched = isAtomWatched;
|
|
|
7
7
|
exports.watchAtoms = watchAtoms;
|
|
8
8
|
exports.watchDefaultStoreAtoms = watchDefaultStoreAtoms;
|
|
9
9
|
var _jotaiStateStore = require("./jotaiStateStore");
|
|
10
|
+
var _writeSemantics = require("./writeSemantics");
|
|
10
11
|
/**
|
|
11
12
|
* Buoy Jotai DevTools — Atom instrumentation
|
|
12
13
|
*
|
|
@@ -65,7 +66,8 @@ function watchAtoms(store, atoms, options) {
|
|
|
65
66
|
// function — primitive atoms get a default one, read-only derived atoms
|
|
66
67
|
// have none). Deriveds used to get a setter that threw inside jotai at
|
|
67
68
|
// set time; now they're correctly reported as read-only up front.
|
|
68
|
-
const
|
|
69
|
+
const isWritable = typeof atom.write === "function";
|
|
70
|
+
const setter = store.set && isWritable ? value => {
|
|
69
71
|
store.set(atom, value);
|
|
70
72
|
} : undefined;
|
|
71
73
|
_jotaiStateStore.jotaiStateStore.registerAtom(label, () => {
|
|
@@ -74,7 +76,11 @@ function watchAtoms(store, atoms, options) {
|
|
|
74
76
|
} catch {
|
|
75
77
|
return undefined;
|
|
76
78
|
}
|
|
77
|
-
}, setter
|
|
79
|
+
}, setter,
|
|
80
|
+
// "value" = jotai's shared defaultWrite (a primitive atom: store.set
|
|
81
|
+
// stores the value). "action" = a custom write receiving the value as
|
|
82
|
+
// its argument — still writable, but the editor says so.
|
|
83
|
+
isWritable ? (0, _writeSemantics.classifyAtomWrite)(atom) : undefined);
|
|
78
84
|
|
|
79
85
|
// Record initial value
|
|
80
86
|
if (enabled) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.classifyAtomWrite = classifyAtomWrite;
|
|
7
|
+
var _vanilla = require("jotai/vanilla");
|
|
8
|
+
/**
|
|
9
|
+
* What does `store.set(atom, x)` DO with x?
|
|
10
|
+
*
|
|
11
|
+
* jotai's `atom(initialValue)` gives every primitive atom the SAME shared
|
|
12
|
+
* `defaultWrite` function (see jotai/src/vanilla/atom.ts): it stores x as the
|
|
13
|
+
* atom's new value (or calls it with the previous value when x is a
|
|
14
|
+
* function). Any other `write` is app code — `atomWithReducer` receives
|
|
15
|
+
* ACTIONS, `atomWithStorage` persists as a side effect, a custom write can do
|
|
16
|
+
* anything — so a raw value handed to `store.set` may not mean "set to this".
|
|
17
|
+
*
|
|
18
|
+
* The tool's editor needs that distinction to be honest: a "value" atom edits
|
|
19
|
+
* like plain data; an "action" atom is still writable (the MCP `setAtom` and
|
|
20
|
+
* Time Machine restore already write to them) but the editor says what the
|
|
21
|
+
* write actually does.
|
|
22
|
+
*
|
|
23
|
+
* Detection: a probe primitive atom exposes this jotai instance's
|
|
24
|
+
* `defaultWrite`; identity match is exact. The `toString` fallback covers an
|
|
25
|
+
* app that somehow bundles a second jotai copy (its atoms carry a different —
|
|
26
|
+
* but textually identical — defaultWrite instance). An unrecognized write
|
|
27
|
+
* classifies as "action", which only makes the editor more cautious.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const probe = (0, _vanilla.atom)(null);
|
|
31
|
+
const DEFAULT_WRITE = probe.write;
|
|
32
|
+
const DEFAULT_WRITE_SOURCE = typeof DEFAULT_WRITE === "function" ? String(DEFAULT_WRITE) : null;
|
|
33
|
+
|
|
34
|
+
/** Classify a writable atom's write. Call only when `atom.write` exists. */
|
|
35
|
+
function classifyAtomWrite(candidate) {
|
|
36
|
+
const atomLike = candidate;
|
|
37
|
+
const write = atomLike?.write;
|
|
38
|
+
if (typeof write !== "function") return "action";
|
|
39
|
+
if (write === DEFAULT_WRITE) return "value";
|
|
40
|
+
// Dual-copy fallback: a primitive atom from another jotai instance holds
|
|
41
|
+
// its own value (`init` present) and its write's source is the library's
|
|
42
|
+
// defaultWrite verbatim.
|
|
43
|
+
if (atomLike !== null && "init" in atomLike && DEFAULT_WRITE_SOURCE !== null && String(write) === DEFAULT_WRITE_SOURCE) {
|
|
44
|
+
return "value";
|
|
45
|
+
}
|
|
46
|
+
return "action";
|
|
47
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -66,6 +66,7 @@ export { useJotaiAtomChanges } from "./jotai/hooks/useJotaiAtomChanges";
|
|
|
66
66
|
export { JotaiModal, JotaiToolView } from "./jotai/components/JotaiModal";
|
|
67
67
|
export { JotaiAtomChangeItem } from "./jotai/components/JotaiAtomChangeItem";
|
|
68
68
|
export { JotaiAtomDetailContent, JotaiAtomDetailFooter } from "./jotai/components/JotaiAtomDetailContent";
|
|
69
|
+
export { JotaiAtomValueDetailContent } from "./jotai/components/JotaiAtomValueDetailContent";
|
|
69
70
|
export { JotaiIcon, JOTAI_ICON_COLOR } from "./jotai/components/JotaiIcon";
|
|
70
71
|
|
|
71
72
|
// =============================================================================
|