@buoy-gg/jotai 7.0.41 → 7.0.43

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 (54) hide show
  1. package/README.md +11 -80
  2. package/lib/commonjs/index.js +1 -112
  3. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +1 -199
  4. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +1 -95
  5. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +1 -715
  6. package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +1 -444
  7. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +1 -43
  8. package/lib/commonjs/jotai/components/JotaiIcon.js +1 -18
  9. package/lib/commonjs/jotai/components/JotaiModal.js +1 -743
  10. package/lib/commonjs/jotai/hooks/useJotaiAtomChanges.js +1 -69
  11. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +1 -306
  12. package/lib/commonjs/jotai/types/index.js +1 -1
  13. package/lib/commonjs/jotai/utils/jotaiStateStore.js +1 -321
  14. package/lib/commonjs/jotai/utils/snapshotProvider.js +1 -215
  15. package/lib/commonjs/jotai/utils/testIDs.js +1 -90
  16. package/lib/commonjs/jotai/utils/watchAtoms.js +1 -143
  17. package/lib/commonjs/jotai/utils/writeSemantics.js +1 -47
  18. package/lib/commonjs/preset.js +1 -90
  19. package/lib/module/index.js +1 -85
  20. package/lib/module/jotai/components/JotaiAtomBrowser.js +1 -195
  21. package/lib/module/jotai/components/JotaiAtomChangeItem.js +1 -91
  22. package/lib/module/jotai/components/JotaiAtomDetailContent.js +1 -710
  23. package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +1 -442
  24. package/lib/module/jotai/components/JotaiEventFilterView.js +1 -39
  25. package/lib/module/jotai/components/JotaiIcon.js +1 -11
  26. package/lib/module/jotai/components/JotaiModal.js +1 -738
  27. package/lib/module/jotai/hooks/useJotaiAtomChanges.js +1 -65
  28. package/lib/module/jotai/sync/jotaiSyncAdapter.js +1 -303
  29. package/lib/module/jotai/types/index.js +1 -1
  30. package/lib/module/jotai/utils/jotaiStateStore.js +1 -317
  31. package/lib/module/jotai/utils/snapshotProvider.js +1 -214
  32. package/lib/module/jotai/utils/testIDs.js +1 -82
  33. package/lib/module/jotai/utils/watchAtoms.js +1 -138
  34. package/lib/module/jotai/utils/writeSemantics.js +1 -43
  35. package/lib/module/preset.js +1 -87
  36. package/lib/web/index.mjs +2 -3
  37. package/package.json +4 -7
  38. package/lib/typescript/index.d.ts.map +0 -1
  39. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +0 -1
  40. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +0 -1
  41. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +0 -1
  42. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +0 -1
  43. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +0 -1
  44. package/lib/typescript/jotai/components/JotaiIcon.d.ts.map +0 -1
  45. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +0 -1
  46. package/lib/typescript/jotai/hooks/useJotaiAtomChanges.d.ts.map +0 -1
  47. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +0 -1
  48. package/lib/typescript/jotai/types/index.d.ts.map +0 -1
  49. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +0 -1
  50. package/lib/typescript/jotai/utils/snapshotProvider.d.ts.map +0 -1
  51. package/lib/typescript/jotai/utils/testIDs.d.ts.map +0 -1
  52. package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +0 -1
  53. package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +0 -1
  54. 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}});