@buoy-gg/jotai 7.0.30 → 7.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/commonjs/index.js +7 -0
  2. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +30 -119
  3. package/lib/commonjs/jotai/components/JotaiAtomChangeItem.js +19 -7
  4. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +41 -5
  5. package/lib/commonjs/jotai/components/JotaiAtomValueDetailContent.js +444 -0
  6. package/lib/commonjs/jotai/components/JotaiEventFilterView.js +15 -0
  7. package/lib/commonjs/jotai/components/JotaiModal.js +159 -23
  8. package/lib/commonjs/jotai/utils/jotaiStateStore.js +9 -4
  9. package/lib/commonjs/jotai/utils/testIDs.js +90 -0
  10. package/lib/commonjs/jotai/utils/watchAtoms.js +8 -2
  11. package/lib/commonjs/jotai/utils/writeSemantics.js +47 -0
  12. package/lib/module/index.js +1 -0
  13. package/lib/module/jotai/components/JotaiAtomBrowser.js +33 -122
  14. package/lib/module/jotai/components/JotaiAtomChangeItem.js +19 -7
  15. package/lib/module/jotai/components/JotaiAtomDetailContent.js +41 -5
  16. package/lib/module/jotai/components/JotaiAtomValueDetailContent.js +442 -0
  17. package/lib/module/jotai/components/JotaiEventFilterView.js +15 -0
  18. package/lib/module/jotai/components/JotaiModal.js +160 -24
  19. package/lib/module/jotai/utils/jotaiStateStore.js +9 -4
  20. package/lib/module/jotai/utils/testIDs.js +82 -0
  21. package/lib/module/jotai/utils/watchAtoms.js +8 -2
  22. package/lib/module/jotai/utils/writeSemantics.js +43 -0
  23. package/lib/typescript/index.d.ts +1 -0
  24. package/lib/typescript/index.d.ts.map +1 -1
  25. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +6 -5
  26. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  27. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts +5 -0
  28. package/lib/typescript/jotai/components/JotaiAtomChangeItem.d.ts.map +1 -1
  29. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  30. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts +40 -0
  31. package/lib/typescript/jotai/components/JotaiAtomValueDetailContent.d.ts.map +1 -0
  32. package/lib/typescript/jotai/components/JotaiEventFilterView.d.ts.map +1 -1
  33. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  34. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +1 -0
  35. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  36. package/lib/typescript/jotai/types/index.d.ts +12 -0
  37. package/lib/typescript/jotai/types/index.d.ts.map +1 -1
  38. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +1 -1
  39. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  40. package/lib/typescript/jotai/utils/testIDs.d.ts +62 -0
  41. package/lib/typescript/jotai/utils/testIDs.d.ts.map +1 -0
  42. package/lib/typescript/jotai/utils/watchAtoms.d.ts +8 -2
  43. package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -1
  44. package/lib/typescript/jotai/utils/writeSemantics.d.ts +25 -0
  45. package/lib/typescript/jotai/utils/writeSemantics.d.ts.map +1 -0
  46. package/package.json +3 -3
@@ -10,27 +10,42 @@
10
10
 
11
11
  import { useState, useMemo, useRef, useCallback, useEffect } from "react";
12
12
  import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
13
- import { JsModal, ModalHeader, TabSelector, macOSColors, night, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit } from "@buoy-gg/shared-ui";
13
+ import { JsModal, ModalHeader, TabSelector, macOSColors, night, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider, useLimit, windowDragProps } from "@buoy-gg/shared-ui";
14
14
  import { useIsPro } from "@buoy-gg/license";
15
15
  import { useJotaiAtomChanges } from "../hooks/useJotaiAtomChanges";
16
16
  import { JotaiAtomChangeItem } from "./JotaiAtomChangeItem";
17
17
  import { JotaiAtomDetailContent, JotaiAtomDetailFooter } from "./JotaiAtomDetailContent";
18
18
  import { JotaiAtomBrowser } from "./JotaiAtomBrowser";
19
+ import { JotaiAtomValueDetailContent } from "./JotaiAtomValueDetailContent";
19
20
  import { JotaiEventFilterView } from "./JotaiEventFilterView";
21
+ import { JOTAI_TEST_ID, buildChangeRowTestIDs } from "../utils/testIDs";
20
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
+ /**
24
+ * Cell clipping, off in e2e bundles only.
25
+ *
26
+ * `removeClippedSubviews` unmounts the views of rows scrolled out of the
27
+ * window, which is what makes a long change list cheap — and which makes
28
+ * Detox report a perfectly rendered row as un-hittable. The env var is
29
+ * inlined at bundle time, so a bundler that does NOT inline it leaves the
30
+ * production behaviour exactly as it was.
31
+ */
32
+ const CLIP_OFFSCREEN_CELLS = !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E);
21
33
  function EventsEmptyState({
22
34
  isEnabled
23
35
  }) {
24
36
  return /*#__PURE__*/_jsxs(View, {
25
37
  style: styles.emptyState,
38
+ testID: `${JOTAI_TEST_ID}-empty`,
26
39
  children: [/*#__PURE__*/_jsx(Box, {
27
40
  size: 32,
28
41
  color: night.color.textTertiary
29
42
  }), /*#__PURE__*/_jsx(Text, {
30
43
  style: styles.emptyTitle,
44
+ testID: `${JOTAI_TEST_ID}-empty-title`,
31
45
  children: "No atom changes"
32
46
  }), /*#__PURE__*/_jsx(Text, {
33
47
  style: styles.emptyText,
48
+ testID: `${JOTAI_TEST_ID}-empty-text`,
34
49
  children: isEnabled ? "Atom changes will appear here as values update." : "Enable capture to start recording atom changes"
35
50
  })]
36
51
  });
@@ -53,6 +68,9 @@ export function JotaiModal({
53
68
  const [showProModal, setShowProModal] = useState(false);
54
69
  const [activeTab, setActiveTab] = useState(initialTab ?? "atoms");
55
70
  const [selectedHistoryAtom, setSelectedHistoryAtom] = useState(null);
71
+ // The atom whose DETAIL SCREEN (live editor) is open — tapping a card in
72
+ // the atoms tab navigates here, mirroring ZustandModal's selectedStoreName.
73
+ const [selectedAtomLabel, setSelectedAtomLabel] = useState(null);
56
74
  const [showFilters, setShowFilters] = useState(false);
57
75
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
58
76
  const {
@@ -137,6 +155,16 @@ export function JotaiModal({
137
155
  }));
138
156
  }
139
157
  };
158
+
159
+ /** Drop the query AND put the header back. The X, and only the X. */
160
+ const closeSearch = useCallback(() => {
161
+ setSearchText("");
162
+ setFilter(prev => ({
163
+ ...prev,
164
+ searchText: ""
165
+ }));
166
+ setIsSearchActive(false);
167
+ }, [setFilter]);
140
168
  const handleChangePress = useCallback(change => {
141
169
  setSelectedChangeId(change.id);
142
170
  }, []);
@@ -158,6 +186,16 @@ export function JotaiModal({
158
186
  setSelectedHistoryAtom(null);
159
187
  setSelectedChangeId(null);
160
188
  }, []);
189
+ const handleOpenAtom = useCallback(label => {
190
+ setSelectedAtomLabel(label);
191
+ // The desktop mirror hydrates an atom's full value on demand — this is
192
+ // the moment it becomes needed (was fired on card-expand before the
193
+ // detail screen replaced expansion).
194
+ onAtomExpand?.(label);
195
+ }, [onAtomExpand]);
196
+ const handleBackFromAtom = useCallback(() => {
197
+ setSelectedAtomLabel(null);
198
+ }, []);
161
199
  const getAtomsSnapshot = useCallback(() => {
162
200
  return displayedAtoms.reduce((acc, atom) => {
163
201
  try {
@@ -216,12 +254,19 @@ export function JotaiModal({
216
254
  setIsSearchActive(false);
217
255
  }, [searchText, setFilter]);
218
256
  const keyExtractor = useCallback(item => item.id, []);
257
+
258
+ // Built for the whole list rather than per row: the ordinal a row carries is
259
+ // a fact about the list around it. See ../utils/testIDs.ts for why an atom
260
+ // label alone cannot address a change row — and why the ordinal is counted
261
+ // per atom when one write can move several.
262
+ const rowTestIDs = useMemo(() => buildChangeRowTestIDs(activeChanges), [activeChanges]);
219
263
  const renderItem = useCallback(({
220
264
  item
221
265
  }) => /*#__PURE__*/_jsx(JotaiAtomChangeItem, {
222
266
  change: item,
223
- onPress: handleChangePress
224
- }), [handleChangePress]);
267
+ onPress: handleChangePress,
268
+ testID: rowTestIDs.get(item.id)
269
+ }), [handleChangePress, rowTestIDs]);
225
270
 
226
271
  // ---- Header ----
227
272
 
@@ -233,7 +278,8 @@ export function JotaiModal({
233
278
  onBack: () => setShowFilters(false)
234
279
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
235
280
  title: "Filters",
236
- centered: true
281
+ centered: true,
282
+ testID: `${JOTAI_TEST_ID}-filters-title`
237
283
  })]
238
284
  });
239
285
  }
@@ -243,7 +289,8 @@ export function JotaiModal({
243
289
  onBack: handleBack
244
290
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
245
291
  title: `${selectedChange.atomLabel}/${selectedChange.category}`,
246
- centered: true
292
+ centered: true,
293
+ testID: `${JOTAI_TEST_ID}-detail-title`
247
294
  })]
248
295
  });
249
296
  }
@@ -253,16 +300,33 @@ export function JotaiModal({
253
300
  onBack: handleBackFromHistory
254
301
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
255
302
  title: `${selectedHistoryAtom} History`,
256
- centered: true
303
+ centered: true,
304
+ testID: `${JOTAI_TEST_ID}-history-title`
257
305
  }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
258
306
  children: /*#__PURE__*/_jsx(ToolbarCopyButton, {
259
307
  value: getHistorySnapshot,
260
308
  disabled: activeChanges.length === 0,
261
- buttonStyle: styles.headerActionButton
309
+ buttonStyle: styles.headerActionButton,
310
+ testID: `${JOTAI_TEST_ID}-copy`
262
311
  })
263
312
  })]
264
313
  });
265
314
  }
315
+
316
+ // Atom detail header — the live editor screen. AFTER history: history is
317
+ // reached FROM the detail screen and stacks over it, so when both are set
318
+ // history owns the header, and its back lands you back on the detail.
319
+ if (selectedAtomLabel) {
320
+ return /*#__PURE__*/_jsxs(ModalHeader, {
321
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
322
+ onBack: handleBackFromAtom
323
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
324
+ title: selectedAtomLabel,
325
+ centered: true,
326
+ testID: `${JOTAI_TEST_ID}-atom-detail-title`
327
+ })]
328
+ });
329
+ }
266
330
  return /*#__PURE__*/_jsxs(ModalHeader, {
267
331
  children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
268
332
  onBack: onBack
@@ -270,27 +334,42 @@ export function JotaiModal({
270
334
  title: "",
271
335
  children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
272
336
  style: styles.headerSearchContainer,
337
+ testID: `${JOTAI_TEST_ID}-search`,
273
338
  children: [/*#__PURE__*/_jsx(Search, {
274
339
  size: 14,
275
340
  color: night.color.textSecondary
276
341
  }), /*#__PURE__*/_jsx(TextInput, {
277
342
  ref: searchInputRef,
343
+ testID: `${JOTAI_TEST_ID}-search-field`,
278
344
  style: styles.headerSearchInput,
279
345
  placeholder: activeTab === "atoms" ? "Search atoms..." : "Search events...",
280
346
  placeholderTextColor: night.color.placeholder,
281
347
  value: searchText,
282
348
  onChangeText: handleSearch,
283
- onSubmitEditing: () => setIsSearchActive(false),
284
- onBlur: () => setIsSearchActive(false),
349
+ onSubmitEditing: () => searchInputRef.current?.blur()
350
+ // Stay open while the query is still narrowing the list.
351
+ //
352
+ // Collapsing on blur hid the reason rows were missing: the
353
+ // header went back to its TABS — whose counts are of the
354
+ // WHOLE capture — while the list stayed filtered, so the tool
355
+ // showed "Atoms (8)" above a single row and offered nothing to
356
+ // explain the gap. The only way back was to reopen a field the
357
+ // user had no reason to think was still armed. Same shape, and
358
+ // the same fix, as the Redux, Events and Zustand tools.
359
+ ,
360
+ onBlur: () => {
361
+ if (searchText.length === 0) setIsSearchActive(false);
362
+ },
363
+ accessibilityLabel: "Search Jotai atoms and events",
285
364
  autoCapitalize: "none",
286
365
  autoCorrect: false,
287
366
  returnKeyType: "search"
288
- }), searchText.length > 0 && /*#__PURE__*/_jsx(TouchableOpacity, {
289
- onPress: () => {
290
- handleSearch("");
291
- setIsSearchActive(false);
292
- },
367
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
368
+ onPress: closeSearch,
293
369
  style: styles.headerSearchClear,
370
+ testID: `${JOTAI_TEST_ID}-search-clear`,
371
+ accessibilityRole: "button",
372
+ accessibilityLabel: "Close search",
294
373
  children: /*#__PURE__*/_jsx(X, {
295
374
  size: 14,
296
375
  color: night.color.textSecondary
@@ -305,12 +384,16 @@ export function JotaiModal({
305
384
  label: `Events${displayedChanges.length > 0 ? ` (${displayedChanges.length})` : ""}`
306
385
  }],
307
386
  activeTab: activeTab,
308
- onTabChange: handleTabChange
387
+ onTabChange: handleTabChange,
388
+ testIDPrefix: `${JOTAI_TEST_ID}-tab`
309
389
  })
310
390
  }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
311
391
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
312
392
  onPress: () => setIsSearchActive(true),
313
393
  style: styles.headerActionButton,
394
+ testID: `${JOTAI_TEST_ID}-search-open`,
395
+ accessibilityRole: "button",
396
+ accessibilityLabel: "Search",
314
397
  children: /*#__PURE__*/_jsx(Search, {
315
398
  size: 14,
316
399
  color: night.color.textSecondary
@@ -318,6 +401,9 @@ export function JotaiModal({
318
401
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
319
402
  onPress: () => setShowFilters(true),
320
403
  style: [styles.headerActionButton, hasActiveFilters && styles.headerActionButtonActive],
404
+ testID: `${JOTAI_TEST_ID}-filters-open`,
405
+ accessibilityRole: "button",
406
+ accessibilityLabel: "Filters",
321
407
  children: /*#__PURE__*/_jsx(Filter, {
322
408
  size: 14,
323
409
  color: hasActiveFilters ? night.color.accent : night.color.textSecondary
@@ -325,20 +411,24 @@ export function JotaiModal({
325
411
  }), activeTab === "atoms" && /*#__PURE__*/_jsx(ToolbarCopyButton, {
326
412
  value: getAtomsSnapshot,
327
413
  disabled: displayedAtoms.length === 0,
328
- buttonStyle: styles.headerActionButton
414
+ buttonStyle: styles.headerActionButton,
415
+ testID: `${JOTAI_TEST_ID}-copy`
329
416
  }), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
330
417
  children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
331
418
  value: getEventsSnapshot,
332
419
  disabled: displayedChanges.length === 0,
333
- buttonStyle: styles.headerActionButton
420
+ buttonStyle: styles.headerActionButton,
421
+ testID: `${JOTAI_TEST_ID}-copy`
334
422
  }), /*#__PURE__*/_jsx(PowerToggleButton, {
335
423
  isEnabled: isEnabled,
336
424
  onToggle: toggleCapture,
337
- accessibilityLabel: "Toggle Jotai atom capture"
425
+ accessibilityLabel: "Toggle Jotai atom capture",
426
+ testID: `${JOTAI_TEST_ID}-power`
338
427
  }), /*#__PURE__*/_jsx(ToolbarClearButton, {
339
428
  onPress: clearChanges,
340
429
  disabled: displayedChanges.length === 0,
341
- buttonStyle: styles.headerActionButton
430
+ buttonStyle: styles.headerActionButton,
431
+ testID: `${JOTAI_TEST_ID}-clear`
342
432
  })]
343
433
  })]
344
434
  })]
@@ -368,18 +458,20 @@ export function JotaiModal({
368
458
  }
369
459
  if (selectedHistoryAtom) {
370
460
  return activeChanges.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
461
+ testID: `${JOTAI_TEST_ID}-history-list`,
371
462
  data: activeChanges,
372
463
  renderItem: renderItem,
373
464
  keyExtractor: keyExtractor,
374
465
  contentContainerStyle: styles.listContent,
375
466
  showsVerticalScrollIndicator: true,
376
- removeClippedSubviews: true,
467
+ removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
377
468
  initialNumToRender: 12,
378
469
  maxToRenderPerBatch: 8,
379
470
  windowSize: 5,
380
471
  updateCellsBatchingPeriod: 50
381
472
  }) : /*#__PURE__*/_jsxs(View, {
382
473
  style: styles.emptyState,
474
+ testID: `${JOTAI_TEST_ID}-history-empty`,
383
475
  children: [/*#__PURE__*/_jsx(Box, {
384
476
  size: 32,
385
477
  color: night.color.textTertiary
@@ -392,12 +484,39 @@ export function JotaiModal({
392
484
  })]
393
485
  });
394
486
  }
487
+
488
+ // Atom detail — live editor + details + read-only tree. After history in
489
+ // this chain for the same reason as the header: history stacks OVER the
490
+ // detail screen it was opened from.
491
+ if (selectedAtomLabel) {
492
+ const selectedAtom = atoms.find(a => a.label === selectedAtomLabel) ?? null;
493
+ return selectedAtom ? /*#__PURE__*/_jsx(JotaiAtomValueDetailContent, {
494
+ atom: selectedAtom,
495
+ onViewHistory: handleViewHistory
496
+ }) :
497
+ /*#__PURE__*/
498
+ // The atom fell out of the registry mid-look (a remote mirror
499
+ // rebuilding, a dev reload). Say so rather than rendering nothing.
500
+ _jsxs(View, {
501
+ style: styles.emptyState,
502
+ testID: `${JOTAI_TEST_ID}-atom-detail-gone`,
503
+ children: [/*#__PURE__*/_jsx(Box, {
504
+ size: 32,
505
+ color: night.color.textTertiary
506
+ }), /*#__PURE__*/_jsx(Text, {
507
+ style: styles.emptyTitle,
508
+ children: "Atom no longer registered"
509
+ }), /*#__PURE__*/_jsxs(Text, {
510
+ style: styles.emptyText,
511
+ children: [selectedAtomLabel, " is not in the registry any more."]
512
+ })]
513
+ });
514
+ }
395
515
  if (activeTab === "atoms") {
396
516
  return /*#__PURE__*/_jsx(JotaiAtomBrowser, {
397
517
  atoms: displayedAtoms,
398
518
  searchQuery: searchText,
399
- onViewHistory: handleViewHistory,
400
- onAtomExpand: onAtomExpand
519
+ onOpenAtom: handleOpenAtom
401
520
  });
402
521
  }
403
522
 
@@ -405,6 +524,7 @@ export function JotaiModal({
405
524
  return /*#__PURE__*/_jsxs(_Fragment, {
406
525
  children: [!isEnabled && /*#__PURE__*/_jsxs(View, {
407
526
  style: styles.disabledBanner,
527
+ testID: `${JOTAI_TEST_ID}-banner-disabled`,
408
528
  children: [/*#__PURE__*/_jsx(Power, {
409
529
  size: 14,
410
530
  color: night.color.warning
@@ -416,8 +536,11 @@ export function JotaiModal({
416
536
  style: styles.lockedBanner,
417
537
  onPress: handleUpgradePress,
418
538
  activeOpacity: 0.8,
539
+ testID: `${JOTAI_TEST_ID}-locked`,
540
+ accessibilityRole: "button",
419
541
  children: [/*#__PURE__*/_jsxs(Text, {
420
542
  style: styles.lockedText,
543
+ testID: `${JOTAI_TEST_ID}-locked-text`,
421
544
  children: [lockedChangeCount, " older", " ", lockedChangeCount === 1 ? "change" : "changes", " locked"]
422
545
  }), /*#__PURE__*/_jsx(View, {
423
546
  style: styles.upgradeBadge,
@@ -428,12 +551,13 @@ export function JotaiModal({
428
551
  })]
429
552
  }), visibleChanges.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
430
553
  ref: flatListRef,
554
+ testID: `${JOTAI_TEST_ID}-list`,
431
555
  data: visibleChanges,
432
556
  renderItem: renderItem,
433
557
  keyExtractor: keyExtractor,
434
558
  contentContainerStyle: styles.listContent,
435
559
  showsVerticalScrollIndicator: true,
436
- removeClippedSubviews: true,
560
+ removeClippedSubviews: CLIP_OFFSCREEN_CELLS,
437
561
  initialNumToRender: 12,
438
562
  maxToRenderPerBatch: 8,
439
563
  windowSize: 5,
@@ -455,7 +579,18 @@ export function JotaiModal({
455
579
 
456
580
  // The change lists own their scrolling so FlatList virtualization works
457
581
  // (see ZustandModal — same pattern, same tool-bench evidence).
458
- const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryAtom != null || activeTab === "events");
582
+ //
583
+ // The ATOM BROWSER owns its scrolling too, which is not just symmetry. Its
584
+ // root is a `flex: 1` ScrollView, and a `flex: 1` box inside JsModal's
585
+ // wrapper (`contentContainerStyle: { flexGrow: 1 }`) resolves against the
586
+ // CONTENT rather than the viewport — so nested, the browser's scroller grows
587
+ // to the full height of its rows, never scrolls, and the wrapper scrolls
588
+ // instead. A person cannot tell the difference; automation can, because the
589
+ // scroller Detox is handed reports a frame taller than the sheet and every
590
+ // scroll start point it computes lands in the clipped part ("view is not
591
+ // scrollable at the given start point"). Owning the scroll gives the browser
592
+ // a bounded height and one scroller with a handle on it.
593
+ const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryAtom != null || activeTab === "events" || activeTab === "atoms");
459
594
  const body = /*#__PURE__*/_jsx(View, {
460
595
  style: styles.container,
461
596
  children: renderContent()
@@ -474,6 +609,7 @@ export function JotaiModal({
474
609
  style: styles.inlineRoot,
475
610
  children: [/*#__PURE__*/_jsx(View, {
476
611
  style: styles.inlineHeader,
612
+ ...windowDragProps,
477
613
  children: renderHeaderContent()
478
614
  }), body, footerNode]
479
615
  }), upgradeModal]
@@ -273,7 +273,8 @@ class JotaiStateStore {
273
273
  label: atom.label,
274
274
  changeCount: atom.changeCount,
275
275
  color: atom.color,
276
- currentValue
276
+ currentValue,
277
+ writeKind: atom.writeKind
277
278
  };
278
279
  });
279
280
  }
@@ -328,7 +329,10 @@ class JotaiStateStore {
328
329
  label: snapshot.label,
329
330
  changeCount: snapshot.changeCount,
330
331
  color: snapshot.color,
331
- getValue: () => currentValue
332
+ getValue: () => currentValue,
333
+ // No setValue: the mirror has no wire back to the device from
334
+ // this entry (yet) — the detail screen renders read-only there.
335
+ writeKind: snapshot.writeKind
332
336
  }];
333
337
  }));
334
338
  this.notifyListeners();
@@ -365,14 +369,15 @@ class JotaiStateStore {
365
369
 
366
370
  // ---- Atom Registry ----
367
371
 
368
- registerAtom(label, getValue, setValue) {
372
+ registerAtom(label, getValue, setValue, writeKind) {
369
373
  if (this.atoms.has(label)) return;
370
374
  const atomInfo = {
371
375
  label,
372
376
  changeCount: 0,
373
377
  color: getAtomColor(label),
374
378
  getValue,
375
- setValue
379
+ setValue,
380
+ writeKind
376
381
  };
377
382
  this.atoms.set(label, atomInfo);
378
383
  this.notifyAtomListeners();
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Jotai tool test handles.
5
+ *
6
+ * Every tappable and every value the e2e suite reads back is addressed
7
+ * `buoy-jotai-<thing>`. Two rules need explaining, and the second is the one
8
+ * this tool does not share with any other.
9
+ *
10
+ * **A change row's handle carries an ORDINAL, and it counts per ATOM.** Same
11
+ * reason as the Zustand tool: an atom a screen writes to fifty times produces
12
+ * fifty separate, equally real rows, and addressing them by label alone would
13
+ * give fifty views one handle — Detox fails a *matcher* that hits more than
14
+ * one element, so the suite would break on `toExist()` before any assertion,
15
+ * with an error that says nothing about duplicate changes. The handle is
16
+ * `buoy-jotai-row-<slug(atomLabel)>-<n>`, n counting back from the newest row
17
+ * for that atom (1 = newest). Deliberately relative: an atom change has no
18
+ * identity a host could know in advance, and what a test CAN know is "the
19
+ * write I just fired is the newest change to that atom". Read and tap in one
20
+ * beat — a row's ordinal moves the instant another change lands.
21
+ *
22
+ * **The ordinal is per ATOM, and one write can move several atoms.** This is
23
+ * where jotai differs from every store-shaped tool. Atoms form a dependency
24
+ * graph, and this tool watches NODES: a single `set` on a primitive atom
25
+ * notifies every mounted derived atom whose value actually moved, so one
26
+ * press can add a row under three different labels — and add nothing at all
27
+ * under a fourth whose recomputed value happened to be unchanged. Counting
28
+ * ordinals across the whole list would make a row's handle depend on how many
29
+ * *other* atoms the write happened to touch, which is exactly the thing a
30
+ * test of a graph must not assume.
31
+ *
32
+ * **An atom row's handle does NOT carry an ordinal.** An atom is registered
33
+ * once under a label that is unique by construction
34
+ * (`jotaiStateStore.registerAtom` returns early on a label it already holds,
35
+ * and `watchAtoms` marks the atom with a per-label symbol), so
36
+ * `buoy-jotai-atom-<slug(label)>` is as stable as the registration itself —
37
+ * and unlike a change, it is a thing the host knows the name of before the
38
+ * app has booted.
39
+ *
40
+ * The suite mirrors these rules in CommonJS (`example-dev-build/utils/
41
+ * jotaiBench.js`) and asserts the two agree — Detox runs in Node on the host
42
+ * and cannot import TypeScript.
43
+ */
44
+
45
+ /** Prefix every handle in this tool shares. */
46
+ export const JOTAI_TEST_ID = "buoy-jotai";
47
+
48
+ /** `jbenchCount` → `jbenchcount`. Shared with every Buoy tool. */
49
+ export function slugForTestID(value) {
50
+ return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
51
+ }
52
+
53
+ /**
54
+ * Handle for one change row: `buoy-jotai-row-jbenchcount-1`.
55
+ *
56
+ * `ordinal` is 1-based and counts back from the newest row for this atom.
57
+ */
58
+ export function changeRowTestID(atomLabel, ordinal) {
59
+ return `${JOTAI_TEST_ID}-row-${slugForTestID(atomLabel)}-${ordinal}`;
60
+ }
61
+
62
+ /** Handle for one atom row in the browser: `buoy-jotai-atom-jbenchcount`. */
63
+ export function atomRowTestID(atomLabel) {
64
+ return `${JOTAI_TEST_ID}-atom-${slugForTestID(atomLabel)}`;
65
+ }
66
+
67
+ /**
68
+ * Row handle per change id for one rendered list, newest-first.
69
+ *
70
+ * Built once for the whole list rather than per row, because an ordinal is a
71
+ * fact about the list and a row cannot compute its own.
72
+ */
73
+ export function buildChangeRowTestIDs(changes) {
74
+ const seen = new Map();
75
+ const handles = new Map();
76
+ for (const change of changes) {
77
+ const ordinal = (seen.get(change.atomLabel) ?? 0) + 1;
78
+ seen.set(change.atomLabel, ordinal);
79
+ handles.set(change.id, changeRowTestID(change.atomLabel, ordinal));
80
+ }
81
+ return handles;
82
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { jotaiStateStore } from "./jotaiStateStore";
13
+ import { classifyAtomWrite } from "./writeSemantics";
13
14
 
14
15
  /** Any Jotai atom — alias for Atom<unknown> for readability */
15
16
 
@@ -60,7 +61,8 @@ export function watchAtoms(store, atoms, options) {
60
61
  // function — primitive atoms get a default one, read-only derived atoms
61
62
  // have none). Deriveds used to get a setter that threw inside jotai at
62
63
  // set time; now they're correctly reported as read-only up front.
63
- const setter = store.set && typeof atom.write === "function" ? value => {
64
+ const isWritable = typeof atom.write === "function";
65
+ const setter = store.set && isWritable ? value => {
64
66
  store.set(atom, value);
65
67
  } : undefined;
66
68
  jotaiStateStore.registerAtom(label, () => {
@@ -69,7 +71,11 @@ export function watchAtoms(store, atoms, options) {
69
71
  } catch {
70
72
  return undefined;
71
73
  }
72
- }, setter);
74
+ }, setter,
75
+ // "value" = jotai's shared defaultWrite (a primitive atom: store.set
76
+ // stores the value). "action" = a custom write receiving the value as
77
+ // its argument — still writable, but the editor says so.
78
+ isWritable ? classifyAtomWrite(atom) : undefined);
73
79
 
74
80
  // Record initial value
75
81
  if (enabled) {
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * What does `store.set(atom, x)` DO with x?
5
+ *
6
+ * jotai's `atom(initialValue)` gives every primitive atom the SAME shared
7
+ * `defaultWrite` function (see jotai/src/vanilla/atom.ts): it stores x as the
8
+ * atom's new value (or calls it with the previous value when x is a
9
+ * function). Any other `write` is app code — `atomWithReducer` receives
10
+ * ACTIONS, `atomWithStorage` persists as a side effect, a custom write can do
11
+ * anything — so a raw value handed to `store.set` may not mean "set to this".
12
+ *
13
+ * The tool's editor needs that distinction to be honest: a "value" atom edits
14
+ * like plain data; an "action" atom is still writable (the MCP `setAtom` and
15
+ * Time Machine restore already write to them) but the editor says what the
16
+ * write actually does.
17
+ *
18
+ * Detection: a probe primitive atom exposes this jotai instance's
19
+ * `defaultWrite`; identity match is exact. The `toString` fallback covers an
20
+ * app that somehow bundles a second jotai copy (its atoms carry a different —
21
+ * but textually identical — defaultWrite instance). An unrecognized write
22
+ * classifies as "action", which only makes the editor more cautious.
23
+ */
24
+
25
+ import { atom as probeAtom } from "jotai/vanilla";
26
+ const probe = probeAtom(null);
27
+ const DEFAULT_WRITE = probe.write;
28
+ const DEFAULT_WRITE_SOURCE = typeof DEFAULT_WRITE === "function" ? String(DEFAULT_WRITE) : null;
29
+
30
+ /** Classify a writable atom's write. Call only when `atom.write` exists. */
31
+ export function classifyAtomWrite(candidate) {
32
+ const atomLike = candidate;
33
+ const write = atomLike?.write;
34
+ if (typeof write !== "function") return "action";
35
+ if (write === DEFAULT_WRITE) return "value";
36
+ // Dual-copy fallback: a primitive atom from another jotai instance holds
37
+ // its own value (`init` present) and its write's source is the library's
38
+ // defaultWrite verbatim.
39
+ if (atomLike !== null && "init" in atomLike && DEFAULT_WRITE_SOURCE !== null && String(write) === DEFAULT_WRITE_SOURCE) {
40
+ return "value";
41
+ }
42
+ return "action";
43
+ }
@@ -40,6 +40,7 @@ export type { UseJotaiAtomChangesResult } from "./jotai/hooks/useJotaiAtomChange
40
40
  export { JotaiModal, JotaiToolView } from "./jotai/components/JotaiModal";
41
41
  export { JotaiAtomChangeItem } from "./jotai/components/JotaiAtomChangeItem";
42
42
  export { JotaiAtomDetailContent, JotaiAtomDetailFooter, } from "./jotai/components/JotaiAtomDetailContent";
43
+ export { JotaiAtomValueDetailContent } from "./jotai/components/JotaiAtomValueDetailContent";
43
44
  export { JotaiIcon, JOTAI_ICON_COLOR } from "./jotai/components/JotaiIcon";
44
45
  export { jotaiSyncAdapter } from "./jotai/sync/jotaiSyncAdapter";
45
46
  export type { JotaiModalProps, JotaiDetailViewMode, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,gCAAgC,CAAC;AAKxC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAOnF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,gCAAgC,CAAC;AAKxC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAOnF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,2BAA2B,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
@@ -2,16 +2,17 @@
2
2
  * JotaiAtomBrowser
3
3
  *
4
4
  * Atoms tab — shows all registered Jotai atoms and their current value.
5
- * Mirrors ZustandStoreBrowser.tsx from @buoy-gg/zustand.
5
+ * Mirrors ZustandStoreBrowser.tsx from @buoy-gg/zustand: tapping a card
6
+ * NAVIGATES to the atom's detail screen (live editor + details + read-only
7
+ * tree) rather than expanding in place — the modal owns that selection.
6
8
  */
7
9
  import type { JotaiAtomInfo } from "../types";
8
10
  interface JotaiAtomBrowserProps {
9
11
  atoms: JotaiAtomInfo[];
10
12
  searchQuery: string;
11
- onViewHistory: (atomLabel: string) => void;
12
- /** Fired when an atom row is expanded (not when collapsed). */
13
- onAtomExpand?: (label: string) => void;
13
+ /** Open an atom's detail screen (live editor). */
14
+ onOpenAtom: (label: string) => void;
14
15
  }
15
- export declare function JotaiAtomBrowser({ atoms, searchQuery, onViewHistory, onAtomExpand, }: JotaiAtomBrowserProps): import("react").JSX.Element;
16
+ export declare function JotaiAtomBrowser({ atoms, searchQuery, onOpenAtom, }: JotaiAtomBrowserProps): import("react").JSX.Element;
16
17
  export {};
17
18
  //# sourceMappingURL=JotaiAtomBrowser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAgHD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,aAAa,EACb,YAAY,GACb,EAAE,qBAAqB,+BAwFvB"}
1
+ {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAyDD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,UAAU,GACX,EAAE,qBAAqB,+BAuFvB"}
@@ -7,6 +7,11 @@ import type { JotaiAtomChange } from "../types";
7
7
  interface JotaiAtomChangeItemProps {
8
8
  change: JotaiAtomChange;
9
9
  onPress: (change: JotaiAtomChange) => void;
10
+ /**
11
+ * Row handle, built for the whole list by `buildChangeRowTestIDs` — a row
12
+ * carries an ordinal it cannot compute on its own. See ../utils/testIDs.ts.
13
+ */
14
+ testID?: string;
10
15
  }
11
16
  export declare const JotaiAtomChangeItem: import("react").NamedExoticComponent<JotaiAtomChangeItemProps>;
12
17
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomChangeItem.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomChangeItem.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,UAAU,CAAC;AAGpE,UAAU,wBAAwB;IAChC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AAsDD,eAAO,MAAM,mBAAmB,gEAiC9B,CAAC"}
1
+ {"version":3,"file":"JotaiAtomChangeItem.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomChangeItem.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,UAAU,CAAC;AAGpE,UAAU,wBAAwB;IAChC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsDD,eAAO,MAAM,mBAAmB,gEAoD9B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAiQlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBA2J7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}
1
+ {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAmSlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBAgL7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}