@buoy-gg/jotai 7.0.40 → 7.0.42
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/README.md +11 -76
- package/lib/commonjs/index.js +1 -112
- package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +1 -199
- package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +1 -85
- package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +1 -715
- package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +1 -444
- package/lib/commonjs/jotai/components/JotaiEventFilterView.js +1 -43
- package/lib/commonjs/jotai/components/JotaiIcon.js +1 -18
- package/lib/commonjs/jotai/components/JotaiModal.js +1 -743
- package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +1 -69
- package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +1 -306
- package/lib/commonjs/jotai/types/index.js +1 -1
- package/lib/commonjs/jotai/utils/jotaiStateStore.js +1 -321
- package/lib/commonjs/jotai/utils/snapshotProvider.js +1 -215
- package/lib/commonjs/jotai/utils/testIDs.js +1 -90
- package/lib/commonjs/jotai/utils/watchAtoms.js +1 -162
- package/lib/commonjs/jotai/utils/writeSemantics.js +1 -47
- package/lib/commonjs/preset.js +1 -90
- package/lib/module/index.js +1 -85
- package/lib/module/jotai/components/JotaiAtomBrowser.js +1 -195
- package/lib/module/jotai/components/JotaiAtomChangeItem.js +1 -81
- package/lib/module/jotai/components/JotaiAtomDetailContent.js +1 -710
- package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +1 -442
- package/lib/module/jotai/components/JotaiEventFilterView.js +1 -39
- package/lib/module/jotai/components/JotaiIcon.js +1 -11
- package/lib/module/jotai/components/JotaiModal.js +1 -738
- package/lib/module/jotai/hooks/useJotaiAtomChanges.js +1 -65
- package/lib/module/jotai/sync/jotaiSyncAdapter.js +1 -303
- package/lib/module/jotai/types/index.js +1 -1
- package/lib/module/jotai/utils/jotaiStateStore.js +1 -317
- package/lib/module/jotai/utils/snapshotProvider.js +1 -214
- package/lib/module/jotai/utils/testIDs.js +1 -82
- package/lib/module/jotai/utils/watchAtoms.js +1 -157
- package/lib/module/jotai/utils/writeSemantics.js +1 -43
- package/lib/module/preset.js +1 -87
- package/lib/web/index.mjs +2 -0
- package/package.json +35 -12
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiIcon.d.ts.map +0 -1
- package/lib/typescript/jotai/components/JotaiModal.d.ts.map +0 -1
- package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +0 -1
- package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/jotai/types/index.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/snapshotProvider.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/testIDs.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +0 -1
- package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
|
@@ -1,743 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.JotaiModal = JotaiModal;
|
|
7
|
-
exports.JotaiToolView = JotaiToolView;
|
|
8
|
-
var _react = require("react");
|
|
9
|
-
var _reactNative = require("react-native");
|
|
10
|
-
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
11
|
-
var _license = require("@buoy-gg/license");
|
|
12
|
-
var _useJotaiAtomChanges = require("../hooks/useJotaiAtomChanges");
|
|
13
|
-
var _JotaiAtomChangeItem = require("./JotaiAtomChangeItem");
|
|
14
|
-
var _JotaiAtomDetailContent = require("./JotaiAtomDetailContent");
|
|
15
|
-
var _JotaiAtomBrowser = require("./JotaiAtomBrowser");
|
|
16
|
-
var _JotaiAtomValueDetailContent = require("./JotaiAtomValueDetailContent");
|
|
17
|
-
var _JotaiEventFilterView = require("./JotaiEventFilterView");
|
|
18
|
-
var _testIDs = require("../utils/testIDs");
|
|
19
|
-
var _jsxRuntime = require("react/jsx-runtime");
|
|
20
|
-
/**
|
|
21
|
-
* Main Jotai DevTools modal
|
|
22
|
-
*
|
|
23
|
-
* Two tabs mirroring the Zustand DevTools pattern:
|
|
24
|
-
* - Atoms tab: Browse all registered atoms and their current value
|
|
25
|
-
* - Events tab: Live atom change monitoring with diffs
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The change log's "nothing here". Everything but the nouns is the shared
|
|
30
|
-
* precedence — see `ToolEmptyState` in shared-ui for why it lives there.
|
|
31
|
-
*/
|
|
32
|
-
function EventsEmptyState({
|
|
33
|
-
isEnabled,
|
|
34
|
-
searchText,
|
|
35
|
-
totalChanges
|
|
36
|
-
}) {
|
|
37
|
-
const content = (0, _sharedUi.resolveListEmptyState)({
|
|
38
|
-
subject: "changes",
|
|
39
|
-
search: searchText,
|
|
40
|
-
total: totalChanges,
|
|
41
|
-
capturing: isEnabled,
|
|
42
|
-
waiting: {
|
|
43
|
-
otto: "waiting",
|
|
44
|
-
title: "No atom changes",
|
|
45
|
-
message: "Atom changes will appear here as values update."
|
|
46
|
-
},
|
|
47
|
-
paused: {
|
|
48
|
-
title: "No atom changes",
|
|
49
|
-
message: "Enable capture to start recording atom changes"
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
|
|
53
|
-
...content,
|
|
54
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-empty`
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
function JotaiModal({
|
|
58
|
-
visible,
|
|
59
|
-
onClose,
|
|
60
|
-
onBack,
|
|
61
|
-
onMinimize,
|
|
62
|
-
enableSharedModalDimensions = false,
|
|
63
|
-
inline = false,
|
|
64
|
-
initialTab,
|
|
65
|
-
initialSelectedChangeId,
|
|
66
|
-
initialDetailView,
|
|
67
|
-
onChangeSelect,
|
|
68
|
-
onAtomExpand
|
|
69
|
-
}) {
|
|
70
|
-
const isPro = (0, _license.useIsPro)();
|
|
71
|
-
const changeLimit = (0, _sharedUi.useLimit)("jotai.changes");
|
|
72
|
-
const [showProModal, setShowProModal] = (0, _react.useState)(false);
|
|
73
|
-
const [activeTab, setActiveTab] = (0, _react.useState)(initialTab ?? "atoms");
|
|
74
|
-
const [selectedHistoryAtom, setSelectedHistoryAtom] = (0, _react.useState)(null);
|
|
75
|
-
// The atom whose DETAIL SCREEN (live editor) is open — tapping a card in
|
|
76
|
-
// the atoms tab navigates here, mirroring ZustandModal's selectedStoreName.
|
|
77
|
-
const [selectedAtomLabel, setSelectedAtomLabel] = (0, _react.useState)(null);
|
|
78
|
-
const [showFilters, setShowFilters] = (0, _react.useState)(false);
|
|
79
|
-
const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
|
|
80
|
-
const {
|
|
81
|
-
atomChanges,
|
|
82
|
-
filteredChanges,
|
|
83
|
-
stats,
|
|
84
|
-
filter,
|
|
85
|
-
setFilter,
|
|
86
|
-
atoms,
|
|
87
|
-
clearChanges,
|
|
88
|
-
isEnabled,
|
|
89
|
-
toggleCapture
|
|
90
|
-
} = (0, _useJotaiAtomChanges.useJotaiAtomChanges)();
|
|
91
|
-
|
|
92
|
-
// Apply ignored patterns on top of filteredChanges
|
|
93
|
-
const displayedChanges = (0, _react.useMemo)(() => {
|
|
94
|
-
if (ignoredPatterns.size === 0) return filteredChanges;
|
|
95
|
-
return filteredChanges.filter(c => !Array.from(ignoredPatterns).some(p => c.atomLabel.toLowerCase().includes(p.toLowerCase())));
|
|
96
|
-
}, [filteredChanges, ignoredPatterns]);
|
|
97
|
-
|
|
98
|
-
// Apply ignored patterns to atoms list
|
|
99
|
-
const displayedAtoms = (0, _react.useMemo)(() => {
|
|
100
|
-
if (ignoredPatterns.size === 0) return atoms;
|
|
101
|
-
return atoms.filter(a => !Array.from(ignoredPatterns).some(p => a.label.toLowerCase().includes(p.toLowerCase())));
|
|
102
|
-
}, [atoms, ignoredPatterns]);
|
|
103
|
-
|
|
104
|
-
// Pro is Infinity here, so slice() returns everything and the locked count
|
|
105
|
-
// floors at 0 — no tier branching needed at the call site.
|
|
106
|
-
/**
|
|
107
|
-
* How many changes this atom has captured, before the search — the
|
|
108
|
-
* denominator the scoped empty state needs so "clear the search" is only
|
|
109
|
-
* offered when clearing it would actually show something.
|
|
110
|
-
*/
|
|
111
|
-
const scopedTotalForAtom = (0, _react.useMemo)(() => {
|
|
112
|
-
if (!selectedHistoryAtom) return 0;
|
|
113
|
-
return atomChanges.filter(c => c.atomLabel === selectedHistoryAtom).length;
|
|
114
|
-
}, [atomChanges, selectedHistoryAtom]);
|
|
115
|
-
const visibleChanges = (0, _react.useMemo)(() => displayedChanges.slice(0, changeLimit), [displayedChanges, changeLimit]);
|
|
116
|
-
const lockedChangeCount = (0, _react.useMemo)(() => Math.max(0, displayedChanges.length - changeLimit), [displayedChanges.length, changeLimit]);
|
|
117
|
-
const hasLockedChanges = lockedChangeCount > 0;
|
|
118
|
-
|
|
119
|
-
// When viewing a single atom's history, scope the list
|
|
120
|
-
const activeChanges = (0, _react.useMemo)(() => {
|
|
121
|
-
if (selectedHistoryAtom) {
|
|
122
|
-
return visibleChanges.filter(c => c.atomLabel === selectedHistoryAtom);
|
|
123
|
-
}
|
|
124
|
-
return visibleChanges;
|
|
125
|
-
}, [visibleChanges, selectedHistoryAtom]);
|
|
126
|
-
const [selectedChangeId, setSelectedChangeId] = (0, _react.useState)(null);
|
|
127
|
-
const selectedChangeIndex = (0, _react.useMemo)(() => {
|
|
128
|
-
if (selectedChangeId === null) return null;
|
|
129
|
-
const idx = activeChanges.findIndex(c => c.id === selectedChangeId);
|
|
130
|
-
return idx >= 0 ? idx : null;
|
|
131
|
-
}, [selectedChangeId, activeChanges]);
|
|
132
|
-
const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
|
|
133
|
-
|
|
134
|
-
// Notify the host on selection (keyed by change id so re-renders with the
|
|
135
|
-
// same selection don't re-fire; remote mirrors fetch atom values here).
|
|
136
|
-
const lastNotifiedSelectionRef = (0, _react.useRef)(null);
|
|
137
|
-
(0, _react.useEffect)(() => {
|
|
138
|
-
if (!onChangeSelect) return;
|
|
139
|
-
const id = selectedChange?.id ?? null;
|
|
140
|
-
if (id === null || id === lastNotifiedSelectionRef.current) return;
|
|
141
|
-
lastNotifiedSelectionRef.current = id;
|
|
142
|
-
onChangeSelect(selectedChange);
|
|
143
|
-
}, [selectedChange, onChangeSelect]);
|
|
144
|
-
|
|
145
|
-
// Deep link into a change's detail (see initialSelectedChangeId). Applied
|
|
146
|
-
// at most once per id, so the list re-rendering — or the user pressing Back
|
|
147
|
-
// — never re-opens it.
|
|
148
|
-
const appliedInitialSelectionRef = (0, _react.useRef)(null);
|
|
149
|
-
(0, _react.useEffect)(() => {
|
|
150
|
-
if (!initialSelectedChangeId) return;
|
|
151
|
-
if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
|
|
152
|
-
const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
|
|
153
|
-
if (!exists) return;
|
|
154
|
-
appliedInitialSelectionRef.current = initialSelectedChangeId;
|
|
155
|
-
setSelectedChangeId(initialSelectedChangeId);
|
|
156
|
-
}, [initialSelectedChangeId, activeChanges]);
|
|
157
|
-
|
|
158
|
-
// Search state
|
|
159
|
-
const [searchText, setSearchText] = (0, _react.useState)("");
|
|
160
|
-
const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
|
|
161
|
-
const searchInputRef = (0, _react.useRef)(null);
|
|
162
|
-
const flatListRef = (0, _react.useRef)(null);
|
|
163
|
-
const handleModeChange = (0, _react.useCallback)(_mode => {}, []);
|
|
164
|
-
const handleSearch = text => {
|
|
165
|
-
setSearchText(text);
|
|
166
|
-
if (activeTab === "events") {
|
|
167
|
-
setFilter(prev => ({
|
|
168
|
-
...prev,
|
|
169
|
-
searchText: text
|
|
170
|
-
}));
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/** Drop the query AND put the header back. The X, and only the X. */
|
|
175
|
-
const closeSearch = (0, _react.useCallback)(() => {
|
|
176
|
-
setSearchText("");
|
|
177
|
-
setFilter(prev => ({
|
|
178
|
-
...prev,
|
|
179
|
-
searchText: ""
|
|
180
|
-
}));
|
|
181
|
-
setIsSearchActive(false);
|
|
182
|
-
}, [setFilter]);
|
|
183
|
-
const handleChangePress = (0, _react.useCallback)(change => {
|
|
184
|
-
setSelectedChangeId(change.id);
|
|
185
|
-
}, []);
|
|
186
|
-
const handleUpgradePress = (0, _react.useCallback)(() => {
|
|
187
|
-
setShowProModal(true);
|
|
188
|
-
}, []);
|
|
189
|
-
const handleBack = (0, _react.useCallback)(() => {
|
|
190
|
-
setSelectedChangeId(null);
|
|
191
|
-
}, []);
|
|
192
|
-
const handleIndexChange = (0, _react.useCallback)(newIndex => {
|
|
193
|
-
const change = activeChanges[newIndex];
|
|
194
|
-
if (change) setSelectedChangeId(change.id);
|
|
195
|
-
}, [activeChanges]);
|
|
196
|
-
const handleViewHistory = (0, _react.useCallback)(atomLabel => {
|
|
197
|
-
setSelectedHistoryAtom(atomLabel);
|
|
198
|
-
setSelectedChangeId(null);
|
|
199
|
-
}, []);
|
|
200
|
-
const handleBackFromHistory = (0, _react.useCallback)(() => {
|
|
201
|
-
setSelectedHistoryAtom(null);
|
|
202
|
-
setSelectedChangeId(null);
|
|
203
|
-
}, []);
|
|
204
|
-
const handleOpenAtom = (0, _react.useCallback)(label => {
|
|
205
|
-
setSelectedAtomLabel(label);
|
|
206
|
-
// The desktop mirror hydrates an atom's full value on demand — this is
|
|
207
|
-
// the moment it becomes needed (was fired on card-expand before the
|
|
208
|
-
// detail screen replaced expansion).
|
|
209
|
-
onAtomExpand?.(label);
|
|
210
|
-
}, [onAtomExpand]);
|
|
211
|
-
const handleBackFromAtom = (0, _react.useCallback)(() => {
|
|
212
|
-
setSelectedAtomLabel(null);
|
|
213
|
-
}, []);
|
|
214
|
-
const getAtomsSnapshot = (0, _react.useCallback)(() => {
|
|
215
|
-
return displayedAtoms.reduce((acc, atom) => {
|
|
216
|
-
try {
|
|
217
|
-
const value = atom.getValue();
|
|
218
|
-
acc[atom.label] = (0, _sharedUi.truncatePayload)(value);
|
|
219
|
-
} catch {
|
|
220
|
-
acc[atom.label] = null;
|
|
221
|
-
}
|
|
222
|
-
return acc;
|
|
223
|
-
}, {});
|
|
224
|
-
}, [displayedAtoms]);
|
|
225
|
-
const getEventsSnapshot = (0, _react.useCallback)(() => {
|
|
226
|
-
return displayedChanges.map(change => ({
|
|
227
|
-
id: change.id,
|
|
228
|
-
atomLabel: change.atomLabel,
|
|
229
|
-
timestamp: change.timestamp,
|
|
230
|
-
prevValue: (0, _sharedUi.truncatePayload)(change.prevValue),
|
|
231
|
-
nextValue: (0, _sharedUi.truncatePayload)(change.nextValue),
|
|
232
|
-
hasValueChange: change.hasValueChange,
|
|
233
|
-
changedKeys: change.changedKeys
|
|
234
|
-
}));
|
|
235
|
-
}, [displayedChanges]);
|
|
236
|
-
const getHistorySnapshot = (0, _react.useCallback)(() => {
|
|
237
|
-
return activeChanges.map(change => ({
|
|
238
|
-
id: change.id,
|
|
239
|
-
atomLabel: change.atomLabel,
|
|
240
|
-
timestamp: change.timestamp,
|
|
241
|
-
prevValue: (0, _sharedUi.truncatePayload)(change.prevValue),
|
|
242
|
-
nextValue: (0, _sharedUi.truncatePayload)(change.nextValue),
|
|
243
|
-
hasValueChange: change.hasValueChange,
|
|
244
|
-
changedKeys: change.changedKeys
|
|
245
|
-
}));
|
|
246
|
-
}, [activeChanges]);
|
|
247
|
-
const handleTogglePattern = (0, _react.useCallback)(pattern => {
|
|
248
|
-
setIgnoredPatterns(prev => {
|
|
249
|
-
const next = new Set(prev);
|
|
250
|
-
if (next.has(pattern)) next.delete(pattern);else next.add(pattern);
|
|
251
|
-
return next;
|
|
252
|
-
});
|
|
253
|
-
}, []);
|
|
254
|
-
const handleAddPattern = (0, _react.useCallback)(pattern => {
|
|
255
|
-
setIgnoredPatterns(prev => new Set([...prev, pattern]));
|
|
256
|
-
}, []);
|
|
257
|
-
const handleTabChange = (0, _react.useCallback)(tab => {
|
|
258
|
-
setActiveTab(tab);
|
|
259
|
-
setSelectedHistoryAtom(null);
|
|
260
|
-
setSelectedChangeId(null);
|
|
261
|
-
setShowFilters(false);
|
|
262
|
-
if (searchText) {
|
|
263
|
-
setSearchText("");
|
|
264
|
-
setFilter(prev => ({
|
|
265
|
-
...prev,
|
|
266
|
-
searchText: ""
|
|
267
|
-
}));
|
|
268
|
-
}
|
|
269
|
-
setIsSearchActive(false);
|
|
270
|
-
}, [searchText, setFilter]);
|
|
271
|
-
const keyExtractor = (0, _react.useCallback)(item => item.id, []);
|
|
272
|
-
|
|
273
|
-
// Built for the whole list rather than per row: the ordinal a row carries is
|
|
274
|
-
// a fact about the list around it. See ../utils/testIDs.ts for why an atom
|
|
275
|
-
// label alone cannot address a change row — and why the ordinal is counted
|
|
276
|
-
// per atom when one write can move several.
|
|
277
|
-
const rowTestIDs = (0, _react.useMemo)(() => (0, _testIDs.buildChangeRowTestIDs)(activeChanges), [activeChanges]);
|
|
278
|
-
const renderItem = (0, _react.useCallback)(({
|
|
279
|
-
item
|
|
280
|
-
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomChangeItem.JotaiAtomChangeItem, {
|
|
281
|
-
change: item,
|
|
282
|
-
onPress: handleChangePress,
|
|
283
|
-
testID: rowTestIDs.get(item.id)
|
|
284
|
-
}), [handleChangePress, rowTestIDs]);
|
|
285
|
-
|
|
286
|
-
// ---- Header ----
|
|
287
|
-
|
|
288
|
-
const hasActiveFilters = ignoredPatterns.size > 0;
|
|
289
|
-
const renderHeaderContent = () => {
|
|
290
|
-
if (showFilters) {
|
|
291
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
292
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
293
|
-
onBack: () => setShowFilters(false)
|
|
294
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
295
|
-
title: "Filters",
|
|
296
|
-
centered: true,
|
|
297
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-filters-title`
|
|
298
|
-
})]
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
if (selectedChange) {
|
|
302
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
303
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
304
|
-
onBack: handleBack
|
|
305
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
306
|
-
title: `${selectedChange.atomLabel}/${selectedChange.category}`,
|
|
307
|
-
centered: true,
|
|
308
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-detail-title`
|
|
309
|
-
})]
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
if (selectedHistoryAtom) {
|
|
313
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
314
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
315
|
-
onBack: handleBackFromHistory
|
|
316
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
317
|
-
title: `${selectedHistoryAtom} History`,
|
|
318
|
-
centered: true,
|
|
319
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-history-title`
|
|
320
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
|
|
321
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
322
|
-
value: getHistorySnapshot,
|
|
323
|
-
disabled: activeChanges.length === 0,
|
|
324
|
-
buttonStyle: styles.headerActionButton,
|
|
325
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
326
|
-
})
|
|
327
|
-
})]
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Atom detail header — the live editor screen. AFTER history: history is
|
|
332
|
-
// reached FROM the detail screen and stacks over it, so when both are set
|
|
333
|
-
// history owns the header, and its back lands you back on the detail.
|
|
334
|
-
if (selectedAtomLabel) {
|
|
335
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
336
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
337
|
-
onBack: handleBackFromAtom
|
|
338
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
339
|
-
title: selectedAtomLabel,
|
|
340
|
-
centered: true,
|
|
341
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-atom-detail-title`
|
|
342
|
-
})]
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
|
|
346
|
-
children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
|
|
347
|
-
onBack: onBack
|
|
348
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
|
|
349
|
-
title: "",
|
|
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
|
-
}
|
|
368
|
-
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
|
|
369
|
-
tabs: [{
|
|
370
|
-
key: "atoms",
|
|
371
|
-
label: `Atoms${displayedAtoms.length > 0 ? ` (${displayedAtoms.length})` : ""}`
|
|
372
|
-
}, {
|
|
373
|
-
key: "events",
|
|
374
|
-
label: `Events${displayedChanges.length > 0 ? ` (${displayedChanges.length})` : ""}`
|
|
375
|
-
}],
|
|
376
|
-
activeTab: activeTab,
|
|
377
|
-
onTabChange: handleTabChange,
|
|
378
|
-
testIDPrefix: `${_testIDs.JOTAI_TEST_ID}-tab`
|
|
379
|
-
})
|
|
380
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
|
|
381
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
382
|
-
onPress: () => setIsSearchActive(true),
|
|
383
|
-
style: styles.headerActionButton,
|
|
384
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-search-open`,
|
|
385
|
-
accessibilityRole: "button",
|
|
386
|
-
accessibilityLabel: "Search",
|
|
387
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
|
|
388
|
-
size: 14,
|
|
389
|
-
color: _sharedUi.night.color.textSecondary
|
|
390
|
-
})
|
|
391
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
392
|
-
onPress: () => setShowFilters(true),
|
|
393
|
-
style: [styles.headerActionButton, hasActiveFilters && styles.headerActionButtonActive],
|
|
394
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-filters-open`,
|
|
395
|
-
accessibilityRole: "button",
|
|
396
|
-
accessibilityLabel: "Filters",
|
|
397
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
|
|
398
|
-
size: 14,
|
|
399
|
-
color: hasActiveFilters ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
|
|
400
|
-
})
|
|
401
|
-
}), activeTab === "atoms" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
402
|
-
value: getAtomsSnapshot,
|
|
403
|
-
disabled: displayedAtoms.length === 0,
|
|
404
|
-
buttonStyle: styles.headerActionButton,
|
|
405
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
406
|
-
}), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
407
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
|
|
408
|
-
value: getEventsSnapshot,
|
|
409
|
-
disabled: displayedChanges.length === 0,
|
|
410
|
-
buttonStyle: styles.headerActionButton,
|
|
411
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-copy`
|
|
412
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
|
|
413
|
-
isEnabled: isEnabled,
|
|
414
|
-
onToggle: toggleCapture,
|
|
415
|
-
accessibilityLabel: "Toggle Jotai atom capture",
|
|
416
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-power`
|
|
417
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarClearButton, {
|
|
418
|
-
onPress: clearChanges,
|
|
419
|
-
disabled: displayedChanges.length === 0,
|
|
420
|
-
buttonStyle: styles.headerActionButton,
|
|
421
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-clear`
|
|
422
|
-
})]
|
|
423
|
-
})]
|
|
424
|
-
})]
|
|
425
|
-
});
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
// ---- Content ----
|
|
429
|
-
|
|
430
|
-
const renderContent = () => {
|
|
431
|
-
if (showFilters) {
|
|
432
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiEventFilterView.JotaiEventFilterView, {
|
|
433
|
-
ignoredPatterns: ignoredPatterns,
|
|
434
|
-
onTogglePattern: handleTogglePattern,
|
|
435
|
-
onAddPattern: handleAddPattern,
|
|
436
|
-
atoms: atoms
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
if (selectedChange && selectedChangeIndex !== null) {
|
|
440
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomDetailContent.JotaiAtomDetailContent, {
|
|
441
|
-
change: selectedChange,
|
|
442
|
-
changes: activeChanges,
|
|
443
|
-
selectedIndex: selectedChangeIndex,
|
|
444
|
-
onIndexChange: handleIndexChange,
|
|
445
|
-
disableInternalFooter: true,
|
|
446
|
-
initialView: initialDetailView
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
if (selectedHistoryAtom) {
|
|
450
|
-
return activeChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
451
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-history-list`,
|
|
452
|
-
data: activeChanges,
|
|
453
|
-
renderItem: renderItem,
|
|
454
|
-
keyExtractor: keyExtractor,
|
|
455
|
-
contentContainerStyle: styles.listContent,
|
|
456
|
-
showsVerticalScrollIndicator: true,
|
|
457
|
-
..._sharedUi.EVENT_LIST_PRESET
|
|
458
|
-
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
|
|
459
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-history-empty`,
|
|
460
|
-
...(0, _sharedUi.resolveListEmptyState)({
|
|
461
|
-
subject: "changes",
|
|
462
|
-
search: searchText,
|
|
463
|
-
// Scoped to one atom, so the count is this atom's, not the log's.
|
|
464
|
-
total: searchText.trim() ? scopedTotalForAtom : 0,
|
|
465
|
-
capturing: isEnabled,
|
|
466
|
-
waiting: {
|
|
467
|
-
otto: "waiting",
|
|
468
|
-
title: "No history yet",
|
|
469
|
-
message: `Changes for ${selectedHistoryAtom} will appear here.`
|
|
470
|
-
},
|
|
471
|
-
paused: {
|
|
472
|
-
title: "No history yet",
|
|
473
|
-
message: `Capture is off, so nothing is being recorded for ${selectedHistoryAtom}.`
|
|
474
|
-
}
|
|
475
|
-
})
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Atom detail — live editor + details + read-only tree. After history in
|
|
480
|
-
// this chain for the same reason as the header: history stacks OVER the
|
|
481
|
-
// detail screen it was opened from.
|
|
482
|
-
if (selectedAtomLabel) {
|
|
483
|
-
const selectedAtom = atoms.find(a => a.label === selectedAtomLabel) ?? null;
|
|
484
|
-
return selectedAtom ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomValueDetailContent.JotaiAtomValueDetailContent, {
|
|
485
|
-
atom: selectedAtom,
|
|
486
|
-
onViewHistory: handleViewHistory
|
|
487
|
-
}) :
|
|
488
|
-
/*#__PURE__*/
|
|
489
|
-
// The atom fell out of the registry mid-look (a remote mirror
|
|
490
|
-
// rebuilding, a dev reload). Say so rather than rendering nothing.
|
|
491
|
-
(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
|
|
492
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-atom-detail-gone`
|
|
493
|
-
// ghost = "stale or detached: an unmounted component, an old
|
|
494
|
-
// snapshot" — exactly what the atom you are looking at has become.
|
|
495
|
-
,
|
|
496
|
-
otto: "ghost",
|
|
497
|
-
title: "Atom no longer registered",
|
|
498
|
-
message: `${selectedAtomLabel} is not in the registry any more.`
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
if (activeTab === "atoms") {
|
|
502
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomBrowser.JotaiAtomBrowser, {
|
|
503
|
-
atoms: displayedAtoms,
|
|
504
|
-
searchQuery: searchText,
|
|
505
|
-
onOpenAtom: handleOpenAtom
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Events tab
|
|
510
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
511
|
-
children: [!isEnabled && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
512
|
-
style: styles.disabledBanner,
|
|
513
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-banner-disabled`,
|
|
514
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
|
|
515
|
-
size: 14,
|
|
516
|
-
color: _sharedUi.night.color.warning
|
|
517
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
518
|
-
style: styles.disabledText,
|
|
519
|
-
children: "Atom capture is disabled"
|
|
520
|
-
})]
|
|
521
|
-
}), hasLockedChanges && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
|
|
522
|
-
style: styles.lockedBanner,
|
|
523
|
-
onPress: handleUpgradePress,
|
|
524
|
-
activeOpacity: 0.8,
|
|
525
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-locked`,
|
|
526
|
-
accessibilityRole: "button",
|
|
527
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
|
|
528
|
-
style: styles.lockedText,
|
|
529
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-locked-text`,
|
|
530
|
-
children: [lockedChangeCount, " older", " ", lockedChangeCount === 1 ? "change" : "changes", " locked"]
|
|
531
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
532
|
-
style: styles.upgradeBadge,
|
|
533
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
534
|
-
style: styles.upgradeBadgeText,
|
|
535
|
-
children: "UPGRADE"
|
|
536
|
-
})
|
|
537
|
-
})]
|
|
538
|
-
}), visibleChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
539
|
-
ref: flatListRef,
|
|
540
|
-
testID: `${_testIDs.JOTAI_TEST_ID}-list`,
|
|
541
|
-
data: visibleChanges,
|
|
542
|
-
renderItem: renderItem,
|
|
543
|
-
keyExtractor: keyExtractor,
|
|
544
|
-
contentContainerStyle: styles.listContent,
|
|
545
|
-
showsVerticalScrollIndicator: true,
|
|
546
|
-
..._sharedUi.EVENT_LIST_PRESET
|
|
547
|
-
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EventsEmptyState, {
|
|
548
|
-
isEnabled: isEnabled,
|
|
549
|
-
searchText: searchText,
|
|
550
|
-
totalChanges: stats.totalChanges
|
|
551
|
-
})]
|
|
552
|
-
});
|
|
553
|
-
};
|
|
554
|
-
const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : "buoy-jotai-modal";
|
|
555
|
-
if (!visible) return null;
|
|
556
|
-
const footerNode = selectedChange && selectedChangeIndex !== null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomDetailContent.JotaiAtomDetailFooter, {
|
|
557
|
-
change: selectedChange,
|
|
558
|
-
changes: activeChanges,
|
|
559
|
-
selectedIndex: selectedChangeIndex,
|
|
560
|
-
onIndexChange: handleIndexChange
|
|
561
|
-
}) : null;
|
|
562
|
-
const footerHeight = selectedChange && activeChanges.length > 1 ? 68 : 0;
|
|
563
|
-
|
|
564
|
-
// The change lists own their scrolling so FlatList virtualization works
|
|
565
|
-
// (see ZustandModal — same pattern, same tool-bench evidence).
|
|
566
|
-
//
|
|
567
|
-
// The ATOM BROWSER owns its scrolling too, which is not just symmetry. Its
|
|
568
|
-
// root is a `flex: 1` ScrollView, and a `flex: 1` box inside JsModal's
|
|
569
|
-
// wrapper (`contentContainerStyle: { flexGrow: 1 }`) resolves against the
|
|
570
|
-
// CONTENT rather than the viewport — so nested, the browser's scroller grows
|
|
571
|
-
// to the full height of its rows, never scrolls, and the wrapper scrolls
|
|
572
|
-
// instead. A person cannot tell the difference; automation can, because the
|
|
573
|
-
// scroller Detox is handed reports a frame taller than the sheet and every
|
|
574
|
-
// scroll start point it computes lands in the clipped part ("view is not
|
|
575
|
-
// scrollable at the given start point"). Owning the scroll gives the browser
|
|
576
|
-
// a bounded height and one scroller with a handle on it.
|
|
577
|
-
const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryAtom != null || activeTab === "events" || activeTab === "atoms");
|
|
578
|
-
const body = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
579
|
-
style: styles.container,
|
|
580
|
-
children: renderContent()
|
|
581
|
-
});
|
|
582
|
-
const upgradeModal = /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
|
|
583
|
-
visible: showProModal,
|
|
584
|
-
onClose: () => setShowProModal(false),
|
|
585
|
-
featureName: "Full Atom History"
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// The ONLY difference between the phone and the desktop dashboard is which
|
|
589
|
-
// shell the header, body and footer go in. Everything above this is shared.
|
|
590
|
-
if (inline) {
|
|
591
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
|
|
592
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
593
|
-
style: styles.inlineRoot,
|
|
594
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
595
|
-
style: styles.inlineHeader,
|
|
596
|
-
..._sharedUi.windowDragProps,
|
|
597
|
-
children: renderHeaderContent()
|
|
598
|
-
}), body, footerNode]
|
|
599
|
-
}), upgradeModal]
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
|
|
603
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
|
|
604
|
-
visible: visible,
|
|
605
|
-
onClose: onClose,
|
|
606
|
-
onMinimize: onMinimize,
|
|
607
|
-
persistenceKey: persistenceKey,
|
|
608
|
-
header: {
|
|
609
|
-
showToggleButton: true,
|
|
610
|
-
customContent: renderHeaderContent()
|
|
611
|
-
},
|
|
612
|
-
onModeChange: handleModeChange,
|
|
613
|
-
enablePersistence: true,
|
|
614
|
-
initialMode: "bottomSheet",
|
|
615
|
-
enableGlitchEffects: true,
|
|
616
|
-
disableScrollWrapper: listOwnsScroll,
|
|
617
|
-
styles: {},
|
|
618
|
-
footer: footerNode,
|
|
619
|
-
footerHeight: footerHeight,
|
|
620
|
-
children: body
|
|
621
|
-
}), upgradeModal]
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* The same Jotai tool as `JotaiModal`, rendered INLINE for hosts that
|
|
627
|
-
* place tools in a panel rather than a modal. Exists so there is exactly one
|
|
628
|
-
* implementation — the desktop dashboard used to ship its own copy of this
|
|
629
|
-
* component's header, list and detail router.
|
|
630
|
-
*/
|
|
631
|
-
function JotaiToolView(props = {}) {
|
|
632
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsx)(JotaiModal, {
|
|
633
|
-
...props,
|
|
634
|
-
inline: true,
|
|
635
|
-
visible: true,
|
|
636
|
-
onClose: () => {}
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
const styles = _reactNative.StyleSheet.create({
|
|
640
|
-
inlineRoot: {
|
|
641
|
-
flex: 1,
|
|
642
|
-
minHeight: 0,
|
|
643
|
-
// Transparent so the desktop workbench's hoisted sky (one field spanning
|
|
644
|
-
// sidebar + panel) shows through — the shell paints its base beneath it.
|
|
645
|
-
backgroundColor: "transparent"
|
|
646
|
-
},
|
|
647
|
-
inlineHeader: {
|
|
648
|
-
paddingHorizontal: 12,
|
|
649
|
-
paddingVertical: 8,
|
|
650
|
-
borderBottomWidth: 1,
|
|
651
|
-
borderBottomColor: _sharedUi.macOSColors.border.default,
|
|
652
|
-
// Clear, like Bench — the host's hoisted sky shows through the header.
|
|
653
|
-
backgroundColor: "transparent"
|
|
654
|
-
},
|
|
655
|
-
container: {
|
|
656
|
-
flex: 1,
|
|
657
|
-
// Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
|
|
658
|
-
backgroundColor: "transparent"
|
|
659
|
-
},
|
|
660
|
-
headerActionButton: {
|
|
661
|
-
width: 32,
|
|
662
|
-
height: 32,
|
|
663
|
-
borderRadius: 8,
|
|
664
|
-
backgroundColor: _sharedUi.night.color.buttonSurface,
|
|
665
|
-
borderWidth: 1,
|
|
666
|
-
borderColor: _sharedUi.night.color.border,
|
|
667
|
-
alignItems: "center",
|
|
668
|
-
justifyContent: "center"
|
|
669
|
-
},
|
|
670
|
-
headerActionButtonActive: {
|
|
671
|
-
backgroundColor: _sharedUi.night.color.accentSoft,
|
|
672
|
-
borderColor: _sharedUi.night.color.accentBorderStrong
|
|
673
|
-
},
|
|
674
|
-
disabledBanner: {
|
|
675
|
-
flexDirection: "row",
|
|
676
|
-
alignItems: "center",
|
|
677
|
-
gap: 8,
|
|
678
|
-
padding: 10,
|
|
679
|
-
marginHorizontal: 12,
|
|
680
|
-
marginTop: 8,
|
|
681
|
-
backgroundColor: _sharedUi.night.color.warningSoft,
|
|
682
|
-
borderRadius: 8,
|
|
683
|
-
borderWidth: 1,
|
|
684
|
-
borderColor: _sharedUi.night.color.warning + "20"
|
|
685
|
-
},
|
|
686
|
-
disabledText: {
|
|
687
|
-
color: _sharedUi.night.color.warning,
|
|
688
|
-
fontSize: 11,
|
|
689
|
-
flex: 1
|
|
690
|
-
},
|
|
691
|
-
listContent: {
|
|
692
|
-
paddingTop: 8,
|
|
693
|
-
paddingBottom: 20
|
|
694
|
-
},
|
|
695
|
-
emptyState: {
|
|
696
|
-
alignItems: "center",
|
|
697
|
-
paddingVertical: 40
|
|
698
|
-
},
|
|
699
|
-
emptyTitle: {
|
|
700
|
-
color: _sharedUi.night.color.text,
|
|
701
|
-
fontSize: 14,
|
|
702
|
-
fontWeight: "600",
|
|
703
|
-
marginTop: 12,
|
|
704
|
-
marginBottom: 6
|
|
705
|
-
},
|
|
706
|
-
emptyText: {
|
|
707
|
-
color: _sharedUi.night.color.textTertiary,
|
|
708
|
-
fontSize: 12,
|
|
709
|
-
textAlign: "center",
|
|
710
|
-
lineHeight: 18
|
|
711
|
-
},
|
|
712
|
-
lockedBanner: {
|
|
713
|
-
flexDirection: "row",
|
|
714
|
-
alignItems: "center",
|
|
715
|
-
gap: 8,
|
|
716
|
-
padding: 10,
|
|
717
|
-
marginHorizontal: 12,
|
|
718
|
-
marginTop: 8,
|
|
719
|
-
backgroundColor: _sharedUi.night.color.accentSoft,
|
|
720
|
-
borderRadius: 8,
|
|
721
|
-
borderWidth: 1,
|
|
722
|
-
borderColor: _sharedUi.night.color.accentBorder
|
|
723
|
-
},
|
|
724
|
-
lockedText: {
|
|
725
|
-
color: _sharedUi.night.color.accent,
|
|
726
|
-
fontSize: 11,
|
|
727
|
-
fontWeight: "500",
|
|
728
|
-
flex: 1
|
|
729
|
-
},
|
|
730
|
-
upgradeBadge: {
|
|
731
|
-
backgroundColor: _sharedUi.night.color.accent,
|
|
732
|
-
paddingHorizontal: 8,
|
|
733
|
-
paddingVertical: 3,
|
|
734
|
-
borderRadius: 4
|
|
735
|
-
},
|
|
736
|
-
upgradeBadgeText: {
|
|
737
|
-
// Ink on a filled accent surface — the night accent takes dark ink.
|
|
738
|
-
color: _sharedUi.night.color.onAccent,
|
|
739
|
-
fontSize: 9,
|
|
740
|
-
fontWeight: "700",
|
|
741
|
-
letterSpacing: 0.5
|
|
742
|
-
}
|
|
743
|
-
});
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.JotaiModal=JotaiModal;exports.JotaiToolView=JotaiToolView;var _react=require("react");var _reactNative=require("react-native");var _sharedUi=require("@buoy-gg/shared-ui");var _license=require("@buoy-gg/license");var _useJotaiAtomChanges=require("../hooks/useJotaiAtomChanges");var _JotaiAtomChangeItem=require("./JotaiAtomChangeItem");var _JotaiAtomDetailContent=require("./JotaiAtomDetailContent");var _JotaiAtomBrowser=require("./JotaiAtomBrowser");var _JotaiAtomValueDetailContent=require("./JotaiAtomValueDetailContent");var _JotaiEventFilterView=require("./JotaiEventFilterView");var _testIDs=require("../utils/testIDs");var _jsxRuntime=require("react/jsx-runtime");function EventsEmptyState({isEnabled:u,searchText:S,totalChanges:y}){const A=(0,_sharedUi.resolveListEmptyState)({subject:"changes",search:S,total:y,capturing:u,waiting:{otto:"waiting",title:"No atom changes",message:"Atom changes will appear here as values update."},paused:{title:"No atom changes",message:"Enable capture to start recording atom changes"}});return(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{...A,testID:`${_testIDs.JOTAI_TEST_ID}-empty`})}function JotaiModal({visible:u,onClose:S,onBack:y,onMinimize:A,enableSharedModalDimensions:Q=false,inline:X=false,initialTab:Y,initialSelectedChangeId:h,initialDetailView:Z,onChangeSelect:j,onAtomExpand:B}){const ve=(0,_license.useIsPro)();const I=(0,_sharedUi.useLimit)("jotai.changes");const[ee,M]=(0,_react.useState)(false);const[s,te]=(0,_react.useState)(Y??"atoms");const[a,_]=(0,_react.useState)(null);const[T,H]=(0,_react.useState)(null);const[D,v]=(0,_react.useState)(false);const[l,V]=(0,_react.useState)(new Set);const{atomChanges:P,filteredChanges:E,stats:oe,filter:Ee,setFilter:b,atoms:x,clearChanges:ae,isEnabled:C,toggleCapture:ne}=(0,_useJotaiAtomChanges.useJotaiAtomChanges)();const r=(0,_react.useMemo)(()=>{if(l.size===0)return E;return E.filter(e=>!Array.from(l).some(t=>e.atomLabel.toLowerCase().includes(t.toLowerCase())))},[E,l]);const g=(0,_react.useMemo)(()=>{if(l.size===0)return x;return x.filter(e=>!Array.from(l).some(t=>e.label.toLowerCase().includes(t.toLowerCase())))},[x,l]);const re=(0,_react.useMemo)(()=>{if(!a)return 0;return P.filter(e=>e.atomLabel===a).length},[P,a]);const f=(0,_react.useMemo)(()=>r.slice(0,I),[r,I]);const w=(0,_react.useMemo)(()=>Math.max(0,r.length-I),[r.length,I]);const se=w>0;const o=(0,_react.useMemo)(()=>{if(a){return f.filter(e=>e.atomLabel===a)}return f},[f,a]);const[k,c]=(0,_react.useState)(null);const d=(0,_react.useMemo)(()=>{if(k===null)return null;const e=o.findIndex(t=>t.id===k);return e>=0?e:null},[k,o]);const n=d!==null?o[d]:null;const $=(0,_react.useRef)(null);(0,_react.useEffect)(()=>{if(!j)return;const e=n?.id??null;if(e===null||e===$.current)return;$.current=e;j(n)},[n,j]);const O=(0,_react.useRef)(null);(0,_react.useEffect)(()=>{if(!h)return;if(O.current===h)return;const e=o.some(t=>t.id===h);if(!e)return;O.current=h;c(h)},[h,o]);const[i,J]=(0,_react.useState)("");const[le,p]=(0,_react.useState)(false);const we=(0,_react.useRef)(null);const ie=(0,_react.useRef)(null);const ce=(0,_react.useCallback)(e=>{},[]);const de=e=>{J(e);if(s==="events"){b(t=>({...t,searchText:e}))}};const ue=(0,_react.useCallback)(()=>{J("");b(e=>({...e,searchText:""}));p(false)},[b]);const L=(0,_react.useCallback)(e=>{c(e.id)},[]);const he=(0,_react.useCallback)(()=>{M(true)},[]);const ge=(0,_react.useCallback)(()=>{c(null)},[]);const R=(0,_react.useCallback)(e=>{const t=o[e];if(t)c(t.id)},[o]);const me=(0,_react.useCallback)(e=>{_(e);c(null)},[]);const Te=(0,_react.useCallback)(()=>{_(null);c(null)},[]);const be=(0,_react.useCallback)(e=>{H(e);B?.(e)},[B]);const xe=(0,_react.useCallback)(()=>{H(null)},[]);const fe=(0,_react.useCallback)(()=>{return g.reduce((e,t)=>{try{const m=t.getValue();e[t.label]=(0,_sharedUi.truncatePayload)(m)}catch{e[t.label]=null}return e},{})},[g]);const ye=(0,_react.useCallback)(()=>{return r.map(e=>({id:e.id,atomLabel:e.atomLabel,timestamp:e.timestamp,prevValue:(0,_sharedUi.truncatePayload)(e.prevValue),nextValue:(0,_sharedUi.truncatePayload)(e.nextValue),hasValueChange:e.hasValueChange,changedKeys:e.changedKeys}))},[r]);const Ie=(0,_react.useCallback)(()=>{return o.map(e=>({id:e.id,atomLabel:e.atomLabel,timestamp:e.timestamp,prevValue:(0,_sharedUi.truncatePayload)(e.prevValue),nextValue:(0,_sharedUi.truncatePayload)(e.nextValue),hasValueChange:e.hasValueChange,changedKeys:e.changedKeys}))},[o]);const Ce=(0,_react.useCallback)(e=>{V(t=>{const m=new Set(t);if(m.has(e))m.delete(e);else m.add(e);return m})},[]);const pe=(0,_react.useCallback)(e=>{V(t=>new Set([...t,e]))},[]);const Se=(0,_react.useCallback)(e=>{te(e);_(null);c(null);v(false);if(i){J("");b(t=>({...t,searchText:""}))}p(false)},[i,b]);const F=(0,_react.useCallback)(e=>e.id,[]);const z=(0,_react.useMemo)(()=>(0,_testIDs.buildChangeRowTestIDs)(o),[o]);const N=(0,_react.useCallback)(({item:e})=>(0,_jsxRuntime.jsx)(_JotaiAtomChangeItem.JotaiAtomChangeItem,{change:e,onPress:L,testID:z.get(e.id)}),[L,z]);const q=l.size>0;const W=()=>{if(D){return(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:()=>v(false)}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"Filters",centered:true,testID:`${_testIDs.JOTAI_TEST_ID}-filters-title`})]})}if(n){return(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:ge}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:`${n.atomLabel}/${n.category}`,centered:true,testID:`${_testIDs.JOTAI_TEST_ID}-detail-title`})]})}if(a){return(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:Te}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:`${a} History`,centered:true,testID:`${_testIDs.JOTAI_TEST_ID}-history-title`}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions,{children:(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:Ie,disabled:o.length===0,buttonStyle:styles.headerActionButton,testID:`${_testIDs.JOTAI_TEST_ID}-copy`})})]})}if(T){return(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:xe}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:T,centered:true,testID:`${_testIDs.JOTAI_TEST_ID}-atom-detail-title`})]})}return(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[y&&(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:y}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"",children:le?(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Search,{value:i,onChangeText:de,onCancel:ue,placeholder:s==="atoms"?"Search atoms...":"Search events...",accessibilityLabel:"Search Jotai atoms and events",iconSize:14,testID:`${_testIDs.JOTAI_TEST_ID}-search-field`,containerTestID:`${_testIDs.JOTAI_TEST_ID}-search`,clearTestID:`${_testIDs.JOTAI_TEST_ID}-search-clear`,onBlur:()=>{if(i.length===0)p(false)}}):(0,_jsxRuntime.jsx)(_sharedUi.TabSelector,{tabs:[{key:"atoms",label:`Atoms${g.length>0?` (${g.length})`:""}`},{key:"events",label:`Events${r.length>0?` (${r.length})`:""}`}],activeTab:s,onTabChange:Se,testIDPrefix:`${_testIDs.JOTAI_TEST_ID}-tab`})}),(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions,{children:[(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:()=>p(true),style:styles.headerActionButton,testID:`${_testIDs.JOTAI_TEST_ID}-search-open`,accessibilityRole:"button",accessibilityLabel:"Search",children:(0,_jsxRuntime.jsx)(_sharedUi.Search,{size:14,color:_sharedUi.night.color.textSecondary})}),(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:()=>v(true),style:[styles.headerActionButton,q&&styles.headerActionButtonActive],testID:`${_testIDs.JOTAI_TEST_ID}-filters-open`,accessibilityRole:"button",accessibilityLabel:"Filters",children:(0,_jsxRuntime.jsx)(_sharedUi.Filter,{size:14,color:q?_sharedUi.night.color.accent:_sharedUi.night.color.textSecondary})}),s==="atoms"&&(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:fe,disabled:g.length===0,buttonStyle:styles.headerActionButton,testID:`${_testIDs.JOTAI_TEST_ID}-copy`}),s==="events"&&(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:ye,disabled:r.length===0,buttonStyle:styles.headerActionButton,testID:`${_testIDs.JOTAI_TEST_ID}-copy`}),(0,_jsxRuntime.jsx)(_sharedUi.PowerToggleButton,{isEnabled:C,onToggle:ne,accessibilityLabel:"Toggle Jotai atom capture",testID:`${_testIDs.JOTAI_TEST_ID}-power`}),(0,_jsxRuntime.jsx)(_sharedUi.ToolbarClearButton,{onPress:ae,disabled:r.length===0,buttonStyle:styles.headerActionButton,testID:`${_testIDs.JOTAI_TEST_ID}-clear`})]})]})]})};const Ae=()=>{if(D){return(0,_jsxRuntime.jsx)(_JotaiEventFilterView.JotaiEventFilterView,{ignoredPatterns:l,onTogglePattern:Ce,onAddPattern:pe,atoms:x})}if(n&&d!==null){return(0,_jsxRuntime.jsx)(_JotaiAtomDetailContent.JotaiAtomDetailContent,{change:n,changes:o,selectedIndex:d,onIndexChange:R,disableInternalFooter:true,initialView:Z})}if(a){return o.length>0?(0,_jsxRuntime.jsx)(_reactNative.FlatList,{testID:`${_testIDs.JOTAI_TEST_ID}-history-list`,data:o,renderItem:N,keyExtractor:F,contentContainerStyle:styles.listContent,showsVerticalScrollIndicator:true,..._sharedUi.EVENT_LIST_PRESET}):(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{testID:`${_testIDs.JOTAI_TEST_ID}-history-empty`,...(0,_sharedUi.resolveListEmptyState)({subject:"changes",search:i,total:i.trim()?re:0,capturing:C,waiting:{otto:"waiting",title:"No history yet",message:`Changes for ${a} will appear here.`},paused:{title:"No history yet",message:`Capture is off, so nothing is being recorded for ${a}.`}})})}if(T){const e=x.find(t=>t.label===T)??null;return e?(0,_jsxRuntime.jsx)(_JotaiAtomValueDetailContent.JotaiAtomValueDetailContent,{atom:e,onViewHistory:me}):(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{testID:`${_testIDs.JOTAI_TEST_ID}-atom-detail-gone`,otto:"ghost",title:"Atom no longer registered",message:`${T} is not in the registry any more.`})}if(s==="atoms"){return(0,_jsxRuntime.jsx)(_JotaiAtomBrowser.JotaiAtomBrowser,{atoms:g,searchQuery:i,onOpenAtom:be})}return(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[!C&&(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.disabledBanner,testID:`${_testIDs.JOTAI_TEST_ID}-banner-disabled`,children:[(0,_jsxRuntime.jsx)(_sharedUi.Power,{size:14,color:_sharedUi.night.color.warning}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.disabledText,children:"Atom capture is disabled"})]}),se&&(0,_jsxRuntime.jsxs)(_reactNative.TouchableOpacity,{style:styles.lockedBanner,onPress:he,activeOpacity:.8,testID:`${_testIDs.JOTAI_TEST_ID}-locked`,accessibilityRole:"button",children:[(0,_jsxRuntime.jsxs)(_reactNative.Text,{style:styles.lockedText,testID:`${_testIDs.JOTAI_TEST_ID}-locked-text`,children:[w," older"," ",w===1?"change":"changes"," locked"]}),(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.upgradeBadge,children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.upgradeBadgeText,children:"UPGRADE"})})]}),f.length>0?(0,_jsxRuntime.jsx)(_reactNative.FlatList,{ref:ie,testID:`${_testIDs.JOTAI_TEST_ID}-list`,data:f,renderItem:N,keyExtractor:F,contentContainerStyle:styles.listContent,showsVerticalScrollIndicator:true,..._sharedUi.EVENT_LIST_PRESET}):(0,_jsxRuntime.jsx)(EventsEmptyState,{isEnabled:C,searchText:i,totalChanges:oe.totalChanges})]})};const je=Q?_sharedUi.devToolsStorageKeys.modal.root():"buoy-jotai-modal";if(!u)return null;const K=n&&d!==null?(0,_jsxRuntime.jsx)(_JotaiAtomDetailContent.JotaiAtomDetailFooter,{change:n,changes:o,selectedIndex:d,onIndexChange:R}):null;const _e=n&&o.length>1?68:0;const De=!D&&!(n&&d!==null)&&(a!=null||s==="events"||s==="atoms");const U=(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.container,children:Ae()});const G=(0,_jsxRuntime.jsx)(_sharedUi.ProUpgradeModal,{visible:ee,onClose:()=>M(false),featureName:"Full Atom History"});if(X){return(0,_jsxRuntime.jsxs)(_sharedUi.TickProvider,{children:[(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.inlineRoot,children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.inlineHeader,..._sharedUi.windowDragProps,children:W()}),U,K]}),G]})}return(0,_jsxRuntime.jsxs)(_sharedUi.TickProvider,{children:[(0,_jsxRuntime.jsx)(_sharedUi.JsModal,{visible:u,onClose:S,onMinimize:A,persistenceKey:je,header:{showToggleButton:true,customContent:W()},onModeChange:ce,enablePersistence:true,initialMode:"bottomSheet",enableGlitchEffects:true,disableScrollWrapper:De,styles:{},footer:K,footerHeight:_e,children:U}),G]})}function JotaiToolView(u={}){return(0,_jsxRuntime.jsx)(JotaiModal,{...u,inline:true,visible:true,onClose:()=>{}})}const styles=_reactNative.StyleSheet.create({inlineRoot:{flex:1,minHeight:0,backgroundColor:"transparent"},inlineHeader:{paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:_sharedUi.macOSColors.border.default,backgroundColor:"transparent"},container:{flex:1,backgroundColor:"transparent"},headerActionButton:{width:32,height:32,borderRadius:8,backgroundColor:_sharedUi.night.color.buttonSurface,borderWidth:1,borderColor:_sharedUi.night.color.border,alignItems:"center",justifyContent:"center"},headerActionButtonActive:{backgroundColor:_sharedUi.night.color.accentSoft,borderColor:_sharedUi.night.color.accentBorderStrong},disabledBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:_sharedUi.night.color.warningSoft,borderRadius:8,borderWidth:1,borderColor:_sharedUi.night.color.warning+"20"},disabledText:{color:_sharedUi.night.color.warning,fontSize:11,flex:1},listContent:{paddingTop:8,paddingBottom:20},emptyState:{alignItems:"center",paddingVertical:40},emptyTitle:{color:_sharedUi.night.color.text,fontSize:14,fontWeight:"600",marginTop:12,marginBottom:6},emptyText:{color:_sharedUi.night.color.textTertiary,fontSize:12,textAlign:"center",lineHeight:18},lockedBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:_sharedUi.night.color.accentSoft,borderRadius:8,borderWidth:1,borderColor:_sharedUi.night.color.accentBorder},lockedText:{color:_sharedUi.night.color.accent,fontSize:11,fontWeight:"500",flex:1},upgradeBadge:{backgroundColor:_sharedUi.night.color.accent,paddingHorizontal:8,paddingVertical:3,borderRadius:4},upgradeBadgeText:{color:_sharedUi.night.color.onAccent,fontSize:9,fontWeight:"700",letterSpacing:.5}});
|