@buoy-gg/shared-ui 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 (143) hide show
  1. package/lib/commonjs/JsModal.js +18 -2
  2. package/lib/commonjs/JsModal.web.js +7 -4
  3. package/lib/commonjs/dataViewer/DataTreeActionDock.js +4 -2
  4. package/lib/commonjs/dataViewer/LiveExplorer.js +930 -0
  5. package/lib/commonjs/dataViewer/ValueEditModal.js +385 -0
  6. package/lib/commonjs/dataViewer/index.js +47 -0
  7. package/lib/commonjs/dataViewer/nestedPath.js +115 -0
  8. package/lib/commonjs/dataViewer/treeActions.js +3 -2
  9. package/lib/commonjs/dataViewer/useTreeDraft.js +125 -0
  10. package/lib/commonjs/dataViewer/useTreeEditor.js +105 -0
  11. package/lib/commonjs/env/EnvironmentIndicator.js +7 -1
  12. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  13. package/lib/commonjs/index.js +24 -0
  14. package/lib/commonjs/license/tierLimits.js +14 -0
  15. package/lib/commonjs/storage/devToolsStorageKeys.js +36 -0
  16. package/lib/commonjs/sync/toolActionRegistry.js +166 -0
  17. package/lib/commonjs/sync/toolUiBridge.js +74 -0
  18. package/lib/commonjs/ui/components/CompactRow.js +4 -0
  19. package/lib/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
  20. package/lib/commonjs/ui/components/ExpandedInfoRow.js +3 -1
  21. package/lib/commonjs/ui/components/ModalHeader.js +12 -4
  22. package/lib/commonjs/ui/components/SearchBar.js +5 -109
  23. package/lib/commonjs/ui/components/ToolButton.js +5 -2
  24. package/lib/commonjs/ui/components/index.js +13 -0
  25. package/lib/commonjs/ui/components/windowDrag.js +45 -0
  26. package/lib/module/JsModal.js +18 -2
  27. package/lib/module/JsModal.web.js +7 -4
  28. package/lib/module/dataViewer/DataTreeActionDock.js +4 -2
  29. package/lib/module/dataViewer/LiveExplorer.js +925 -0
  30. package/lib/module/dataViewer/ValueEditModal.js +381 -0
  31. package/lib/module/dataViewer/index.js +10 -1
  32. package/lib/module/dataViewer/nestedPath.js +109 -0
  33. package/lib/module/dataViewer/treeActions.js +3 -2
  34. package/lib/module/dataViewer/useTreeDraft.js +121 -0
  35. package/lib/module/dataViewer/useTreeEditor.js +101 -0
  36. package/lib/module/env/EnvironmentIndicator.js +7 -1
  37. package/lib/module/hooks/safe-area-impl.js +1 -1
  38. package/lib/module/index.js +2 -0
  39. package/lib/module/license/tierLimits.js +14 -0
  40. package/lib/module/storage/devToolsStorageKeys.js +36 -0
  41. package/lib/module/sync/toolActionRegistry.js +154 -0
  42. package/lib/module/sync/toolUiBridge.js +68 -0
  43. package/lib/module/ui/components/CompactRow.js +4 -0
  44. package/lib/module/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
  45. package/lib/module/ui/components/ExpandedInfoRow.js +3 -1
  46. package/lib/module/ui/components/ModalHeader.js +12 -4
  47. package/lib/module/ui/components/SearchBar.js +8 -112
  48. package/lib/module/ui/components/ToolButton.js +5 -2
  49. package/lib/module/ui/components/index.js +1 -0
  50. package/lib/module/ui/components/windowDrag.js +41 -0
  51. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts +6 -1
  54. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts +73 -0
  56. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts +40 -0
  58. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/dataViewer/index.d.ts +5 -0
  60. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts +20 -0
  62. package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts +10 -0
  64. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts +81 -0
  66. package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts +52 -0
  68. package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/env/EnvironmentIndicator.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  71. package/lib/typescript/commonjs/index.d.ts +2 -0
  72. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/license/tierLimits.d.ts +10 -0
  74. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +36 -0
  76. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts +85 -0
  78. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +39 -0
  80. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +5 -1
  82. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts +6 -0
  85. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts +7 -1
  87. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts +1 -3
  90. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/components/ToolButton.d.ts +5 -1
  92. package/lib/typescript/commonjs/ui/components/ToolButton.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/ui/components/index.d.ts +1 -0
  94. package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/components/windowDrag.d.ts +39 -0
  96. package/lib/typescript/commonjs/ui/components/windowDrag.d.ts.map +1 -0
  97. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  98. package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
  99. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts +6 -1
  100. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  101. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts +73 -0
  102. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -0
  103. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts +40 -0
  104. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts.map +1 -0
  105. package/lib/typescript/module/dataViewer/index.d.ts +5 -0
  106. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  107. package/lib/typescript/module/dataViewer/nestedPath.d.ts +20 -0
  108. package/lib/typescript/module/dataViewer/nestedPath.d.ts.map +1 -0
  109. package/lib/typescript/module/dataViewer/treeActions.d.ts +10 -0
  110. package/lib/typescript/module/dataViewer/treeActions.d.ts.map +1 -1
  111. package/lib/typescript/module/dataViewer/useTreeDraft.d.ts +81 -0
  112. package/lib/typescript/module/dataViewer/useTreeDraft.d.ts.map +1 -0
  113. package/lib/typescript/module/dataViewer/useTreeEditor.d.ts +52 -0
  114. package/lib/typescript/module/dataViewer/useTreeEditor.d.ts.map +1 -0
  115. package/lib/typescript/module/env/EnvironmentIndicator.d.ts.map +1 -1
  116. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  117. package/lib/typescript/module/index.d.ts +2 -0
  118. package/lib/typescript/module/index.d.ts.map +1 -1
  119. package/lib/typescript/module/license/tierLimits.d.ts +10 -0
  120. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -1
  121. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +36 -0
  122. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  123. package/lib/typescript/module/sync/toolActionRegistry.d.ts +85 -0
  124. package/lib/typescript/module/sync/toolActionRegistry.d.ts.map +1 -0
  125. package/lib/typescript/module/sync/toolUiBridge.d.ts +39 -0
  126. package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -0
  127. package/lib/typescript/module/ui/components/CompactRow.d.ts +5 -1
  128. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  130. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts +6 -0
  131. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts +7 -1
  133. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  134. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  135. package/lib/typescript/module/ui/components/SearchBar.d.ts +1 -3
  136. package/lib/typescript/module/ui/components/SearchBar.d.ts.map +1 -1
  137. package/lib/typescript/module/ui/components/ToolButton.d.ts +5 -1
  138. package/lib/typescript/module/ui/components/ToolButton.d.ts.map +1 -1
  139. package/lib/typescript/module/ui/components/index.d.ts +1 -0
  140. package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
  141. package/lib/typescript/module/ui/components/windowDrag.d.ts +39 -0
  142. package/lib/typescript/module/ui/components/windowDrag.d.ts.map +1 -0
  143. package/package.json +4 -4
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useTreeDraft = useTreeDraft;
7
+ var _react = require("react");
8
+ var _jsonOps = require("./jsonOps.js");
9
+ /**
10
+ * A local draft of a tree value, edited in place and written ONCE.
11
+ *
12
+ * Applying each tree op straight to the owning document was the obvious first
13
+ * cut and it behaves badly for two compounding reasons:
14
+ *
15
+ * 1. **Every op is a round trip.** The write lands, the owner emits an event,
16
+ * a snapshot comes back, and the browser re-reads everything — so a six-tap
17
+ * reorder is six writes and six full refreshes. The tree rebuilds under the
18
+ * user mid-gesture.
19
+ * 2. **The highlight and the value fall out of step.** Selection is local and
20
+ * moves instantly; the value it points at only catches up when the round
21
+ * trip completes, so for a beat the highlight sits on the old contents.
22
+ *
23
+ * Optimistic updates — the React Query pattern of rendering the expected result
24
+ * and reconciling later — fix (2) but not (1): they still write per change.
25
+ * Buffering fixes both, and it's what every editor that owns a document does
26
+ * (a form's dirty state, an IDE's unsaved buffer, JSON Editor's in-memory doc
27
+ * with an explicit save).
28
+ *
29
+ * The trade is that the buffer can go stale if the owner writes the same
30
+ * document while you're editing. That's the same last-write-wins a text editor
31
+ * has, and `isDirty` at least makes it visible that you're holding unsaved
32
+ * changes.
33
+ *
34
+ * Grew up as the Storage tool's key editor; hoisted here so the Zustand store
35
+ * editor is the same editor rather than a drifting copy. The save contract
36
+ * takes the VALUE — a storage host stringifies it, a zustand host diffs it
37
+ * into a merge partial.
38
+ */
39
+
40
+ /**
41
+ * @param liveValue the value as its owner currently holds it (already parsed)
42
+ * @param save persists the draft VALUE; rejecting keeps edit mode open
43
+ */
44
+ function useTreeDraft(liveValue, save) {
45
+ const [draft, setDraft] = (0, _react.useState)(undefined);
46
+ const [isEditing, setIsEditing] = (0, _react.useState)(false);
47
+ const [isSaving, setIsSaving] = (0, _react.useState)(false);
48
+ const [error, setError] = (0, _react.useState)(null);
49
+ const [selectPath, setSelectPath] = (0, _react.useState)();
50
+ /**
51
+ * The value as it was when editing began. Compared against for dirtiness
52
+ * rather than against `liveValue`, which can move underneath us if the owner
53
+ * writes the same document — that would otherwise flip a clean draft to
54
+ * dirty.
55
+ */
56
+ const [baseline, setBaseline] = (0, _react.useState)("");
57
+ const value = isEditing && draft !== undefined ? draft : liveValue;
58
+ const isDirty = isEditing && JSON.stringify(draft) !== baseline;
59
+ const begin = (0, _react.useCallback)(() => {
60
+ setDraft(liveValue);
61
+ setBaseline(JSON.stringify(liveValue));
62
+ setSelectPath(undefined);
63
+ setError(null);
64
+ setIsEditing(true);
65
+ }, [liveValue]);
66
+ const apply = (0, _react.useCallback)(op => {
67
+ // Purely local — this is the whole point. No write, so no event, no
68
+ // snapshot, no refresh, and the tree re-renders from state synchronously.
69
+ setDraft(prev => (0, _jsonOps.applyJsonOp)(prev, op));
70
+ const next = (0, _jsonOps.selectionPathAfterOp)(op);
71
+ if (next !== undefined) setSelectPath(next);
72
+ }, []);
73
+ const selectAt = (0, _react.useCallback)(path => {
74
+ setSelectPath(path);
75
+ }, []);
76
+
77
+ /** Leave edit mode entirely. Only correct on the way out (after a write). */
78
+ const reset = (0, _react.useCallback)(() => {
79
+ setIsEditing(false);
80
+ setDraft(undefined);
81
+ setSelectPath(undefined);
82
+ setError(null);
83
+ }, []);
84
+
85
+ /** Re-seed the draft from what's stored, staying in edit mode. */
86
+ const discard = (0, _react.useCallback)(() => {
87
+ setDraft(liveValue);
88
+ setBaseline(JSON.stringify(liveValue));
89
+ setSelectPath(undefined);
90
+ setError(null);
91
+ setIsEditing(true);
92
+ }, [liveValue]);
93
+ const commit = (0, _react.useCallback)(async () => {
94
+ if (!isDirty || !save) {
95
+ reset();
96
+ return true;
97
+ }
98
+ setIsSaving(true);
99
+ setError(null);
100
+ try {
101
+ await save(draft);
102
+ reset();
103
+ return true;
104
+ } catch (e) {
105
+ // Stay in edit mode so the work isn't lost to a failed write.
106
+ setError(e instanceof Error ? e.message : String(e));
107
+ return false;
108
+ } finally {
109
+ setIsSaving(false);
110
+ }
111
+ }, [draft, isDirty, reset, save]);
112
+ return {
113
+ isEditing,
114
+ value,
115
+ isDirty,
116
+ isSaving,
117
+ error,
118
+ selectPath,
119
+ begin,
120
+ apply,
121
+ selectAt,
122
+ commit,
123
+ discard
124
+ };
125
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useTreeEditor = useTreeEditor;
7
+ var _react = require("react");
8
+ var _jsonOps = require("./jsonOps.js");
9
+ var _useTreeDraft = require("./useTreeDraft.js");
10
+ /**
11
+ * Everything editing a tree document involves, minus how it looks.
12
+ *
13
+ * The phone and the dashboard run the same editor: one buffered draft, one
14
+ * write on save, a selection the structural actions operate on, a modal for
15
+ * typing a value, and a guard so closing can't silently drop unsaved work.
16
+ * What differs is presentation — thumb-sized tiles under a full screen versus
17
+ * chips docked in a panel — which is a rendering concern and nothing more.
18
+ *
19
+ * Keeping the rules here means the surfaces (and the tools — storage keys and
20
+ * zustand stores run this same hook) can't drift on the things that would
21
+ * actually hurt: whether a boolean opens an editor, whether a failed write
22
+ * closes the editor anyway, what "dirty" means.
23
+ */
24
+
25
+ function useTreeEditor({
26
+ value,
27
+ save,
28
+ onClose,
29
+ confirmDiscard
30
+ }) {
31
+ const draft = (0, _useTreeDraft.useTreeDraft)(value, save);
32
+ const [selection, setSelection] = (0, _react.useState)(null);
33
+ const [valuePath, setValuePath] = (0, _react.useState)(null);
34
+ const {
35
+ begin
36
+ } = draft;
37
+ (0, _react.useEffect)(() => {
38
+ begin();
39
+ // Seed the draft ONCE, when the editor opens. `begin` closes over the live
40
+ // value, whose identity changes every time the list refreshes underneath
41
+ // us (a parsed JSON string is a new object each pass), so re-running would
42
+ // reset the draft mid-edit and discard the user's work.
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, []);
45
+
46
+ /**
47
+ * "Change this node". For a boolean that's a flip rather than a modal — the
48
+ * same rule `treeActionsFor` applies to the action controls, so the gesture
49
+ * and the button can't mean different things.
50
+ */
51
+ const openValueEditor = (0, _react.useCallback)(path => {
52
+ const current = (0, _jsonOps.getAtPath)(draft.value, path);
53
+ if (typeof current === "boolean") {
54
+ draft.apply({
55
+ kind: "replace",
56
+ path,
57
+ value: !current
58
+ });
59
+ return;
60
+ }
61
+ setValuePath(path);
62
+ }, [draft]);
63
+ const applyValue = (0, _react.useCallback)(next => {
64
+ draft.apply({
65
+ kind: "replace",
66
+ path: valuePath ?? [],
67
+ value: next
68
+ });
69
+ setValuePath(null);
70
+ }, [draft, valuePath]);
71
+
72
+ // Only leave once the write actually landed — `commit` resolves false and
73
+ // keeps the draft on screen when the owner refuses it, and closing anyway
74
+ // would throw the work away while showing an error nobody ever sees.
75
+ const saveAndClose = (0, _react.useCallback)(() => {
76
+ void draft.commit().then(saved => saved && onClose());
77
+ }, [draft, onClose]);
78
+ const requestClose = (0, _react.useCallback)(() => {
79
+ if (!draft.isDirty) {
80
+ onClose();
81
+ return;
82
+ }
83
+ // The draft is the only copy of this work, so closing over it silently is
84
+ // the one way this editor can lose something the user can't get back.
85
+ confirmDiscard({
86
+ save: saveAndClose,
87
+ discard: () => {
88
+ draft.discard();
89
+ onClose();
90
+ }
91
+ });
92
+ }, [confirmDiscard, draft, onClose, saveAndClose]);
93
+ return {
94
+ draft,
95
+ selection,
96
+ setSelection,
97
+ valuePath,
98
+ openValueEditor,
99
+ closeValueEditor: () => setValuePath(null),
100
+ applyValue,
101
+ openRawEditor: () => setValuePath([]),
102
+ requestClose,
103
+ saveAndClose
104
+ };
105
+ }
@@ -93,7 +93,13 @@ function EnvironmentIndicator({
93
93
  style: {
94
94
  fontSize: 11,
95
95
  fontWeight: "600",
96
- fontFamily: "Poppins-SemiBold",
96
+ // No fontFamily on purpose: the platform UI font (SF Pro / Roboto)
97
+ // is what this badge is meant to look like, and it is what the DOM
98
+ // twin in EnvironmentIndicator.web.tsx has always rendered. This
99
+ // used to name "Poppins-SemiBold" — a family no app, package or
100
+ // example in the repo has ever loaded, so it resolved nowhere:
101
+ // silently the system font on device, and the browser's default
102
+ // SERIF under react-native-web (the docs demo).
97
103
  color: _gameUIColors.gameUIColors.primaryLight,
98
104
  letterSpacing: 0.5
99
105
  },
@@ -7,7 +7,7 @@ exports.useNativeSafeAreaInsets = exports.safeAreaType = exports.hasSafeAreaPack
7
7
  /**
8
8
  * Auto-generated safe area implementation
9
9
  * Detected: none
10
- * Generated at: 2026-08-24T23:44:23.535Z
10
+ * Generated at: 2026-09-01T17:57:34.045Z
11
11
  *
12
12
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
13
13
  *
@@ -720,6 +720,30 @@ Object.keys(_wireBudget).forEach(function (key) {
720
720
  }
721
721
  });
722
722
  });
723
+ var _toolActionRegistry = require("./sync/toolActionRegistry.js");
724
+ Object.keys(_toolActionRegistry).forEach(function (key) {
725
+ if (key === "default" || key === "__esModule") return;
726
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
727
+ if (key in exports && exports[key] === _toolActionRegistry[key]) return;
728
+ Object.defineProperty(exports, key, {
729
+ enumerable: true,
730
+ get: function () {
731
+ return _toolActionRegistry[key];
732
+ }
733
+ });
734
+ });
735
+ var _toolUiBridge = require("./sync/toolUiBridge.js");
736
+ Object.keys(_toolUiBridge).forEach(function (key) {
737
+ if (key === "default" || key === "__esModule") return;
738
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
739
+ if (key in exports && exports[key] === _toolUiBridge[key]) return;
740
+ Object.defineProperty(exports, key, {
741
+ enumerable: true,
742
+ get: function () {
743
+ return _toolUiBridge[key];
744
+ }
745
+ });
746
+ });
723
747
  var _index3 = require("./utils/index.js");
724
748
  var _index4 = require("./utils/formatting/index.js");
725
749
  Object.keys(_index4).forEach(function (key) {
@@ -107,6 +107,20 @@ const LIMITS = {
107
107
  anonymous: 1,
108
108
  free: 3,
109
109
  pro: Infinity
110
+ },
111
+ // ── scenarios ─────────────────────────────────────────────────────────────
112
+ // The SCENARIO is the unit, not the rules it creates: a free user's
113
+ // three-endpoint scenario either runs whole or refuses up front, and is never
114
+ // silently trimmed to one rule mid-run. Enforcement is atomic in pre-flight.
115
+ "scenarios.saved": {
116
+ anonymous: 1,
117
+ free: 3,
118
+ pro: Infinity
119
+ },
120
+ "scenarios.active": {
121
+ anonymous: 1,
122
+ free: 1,
123
+ pro: 3
110
124
  }
111
125
  };
112
126
  const TIER_LIMITS = exports.TIER_LIMITS = LIMITS;
@@ -135,6 +135,18 @@ const devToolsStorageKeys = exports.devToolsStorageKeys = {
135
135
  root: () => `${devToolsStorageKeys.base}_time_machine`,
136
136
  diffViewerMode: () => `${devToolsStorageKeys.timeMachine.root()}_diff_viewer_mode` // 'split' | 'tree'
137
137
  },
138
+ /**
139
+ * Scenarios storage keys. The library (saved + draft scenarios) and the
140
+ * active-scenario record live in separate keys deliberately: the active
141
+ * record is written on every run and deactivation, while the library changes
142
+ * rarely, and a single blob would rewrite the whole library each run.
143
+ */
144
+ scenarios: {
145
+ root: () => `${devToolsStorageKeys.base}_scenarios`,
146
+ modal: () => `${devToolsStorageKeys.scenarios.root()}_modal`,
147
+ library: () => `${devToolsStorageKeys.scenarios.root()}_library`,
148
+ active: () => `${devToolsStorageKeys.scenarios.root()}_active`
149
+ },
138
150
  /**
139
151
  * React Query-related storage keys
140
152
  */
@@ -209,16 +221,40 @@ const devToolsStorageKeys = exports.devToolsStorageKeys = {
209
221
  settings: () => `${devToolsStorageKeys.benchmark.root()}_settings`,
210
222
  selectedReports: () => `${devToolsStorageKeys.benchmark.root()}_selected_reports`
211
223
  },
224
+ /**
225
+ * Debug Borders overlay keys
226
+ */
227
+ debugBorders: {
228
+ root: () => `${devToolsStorageKeys.base}_debug_borders`,
229
+ /** JsModal `persistenceKey` for the overlay's info window. */
230
+ infoModal: () => `${devToolsStorageKeys.debugBorders.root()}_info_modal`
231
+ },
212
232
  /**
213
233
  * Unified events-related storage keys
214
234
  */
215
235
  events: {
216
236
  root: () => `${devToolsStorageKeys.base}_events`,
237
+ /**
238
+ * NOT what the Events tool uses. Its preset passes the literal
239
+ * `"buoy-events-modal"` to JsModal, and three things outside this repo's
240
+ * runtime now pin that string: the website's live tour, its QA script, and
241
+ * the e2e runner's reset. `isDevToolsStorageKey` treats the `buoy-` prefix
242
+ * as internal, so nothing leaks — this is a naming inconsistency, not a
243
+ * bug, and moving the key would cost a coordinated change across four
244
+ * surfaces to buy tidiness. Kept so the shape of this file stays uniform.
245
+ */
217
246
  modal: () => `${devToolsStorageKeys.events.root()}_modal`,
218
247
  /** Selected badge/source filters */
219
248
  enabledSources: () => `${devToolsStorageKeys.events.root()}_enabled_sources`,
220
249
  /** Whether event capturing is active */
221
250
  isCapturing: () => `${devToolsStorageKeys.events.root()}_is_capturing`,
251
+ /**
252
+ * When capture was last switched off (ms epoch). Persisted with the
253
+ * capturing flag so a remount/relaunch while paused keeps hiding events
254
+ * captured into the shared store after the ORIGINAL pause moment (a
255
+ * watching dashboard keeps sources alive through the device's pause).
256
+ */
257
+ pausedAt: () => `${devToolsStorageKeys.events.root()}_paused_at`,
222
258
  /** Copy/export settings */
223
259
  copySettings: () => `${devToolsStorageKeys.events.root()}_copy_settings`
224
260
  }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.dispatchToolAction = dispatchToolAction;
7
+ exports.getToolAdapters = getToolAdapters;
8
+ exports.isToolRegistryReady = isToolRegistryReady;
9
+ exports.listSnapshotToolIds = listSnapshotToolIds;
10
+ exports.listToolActions = listToolActions;
11
+ exports.listToolIds = listToolIds;
12
+ exports.readToolSnapshot = readToolSnapshot;
13
+ exports.registerToolAdapters = registerToolAdapters;
14
+ exports.waitForToolRegistry = waitForToolRegistry;
15
+ /**
16
+ * toolActionRegistry — the on-device twin of the broker's action dispatch.
17
+ *
18
+ * Buoy already has exactly one place where `{toolId, action, params}` becomes a
19
+ * real call into a tool: `handleAction` in external-sync, fed by the adapter
20
+ * map that `autoExternalSync` assembles. That map was private to the React
21
+ * tree, so anything running ON the device (the Scenarios runner) had no way to
22
+ * reach it and would have had to duplicate the map — two dispatch paths that
23
+ * drift the first time an adapter changes.
24
+ *
25
+ * This module publishes the SAME map through a global seam, so a scenario step,
26
+ * a desktop click and an MCP call are one code path with two entry points.
27
+ *
28
+ * WHY A GLOBAL, NOT AN IMPORT: the map is built inside a component (the
29
+ * react-query adapter is a hook, the env adapter is a factory closed over a
30
+ * prop, and custom tools arrive as props), so it cannot be a module constant.
31
+ * Meanwhile the consumer must not import `@buoy-gg/core` — that package imports
32
+ * the tools, and a tool importing it back is the require cycle that puts a
33
+ * LogBox on every reload. Same reasoning, and the same shape, as
34
+ * `crashFlush.ts` in external-sync.
35
+ *
36
+ * WE STORE A GETTER, NOT A SNAPSHOT, for the reason `attachDeviceSync` does:
37
+ * the adapter map is re-created on renders that change its inputs, and a stale
38
+ * captured copy would keep dispatching into adapters the tree has replaced.
39
+ *
40
+ * SAFETY: this registry is populated only while `AutoExternalSync` is mounted,
41
+ * which is itself gated (dev, or release + an explicit opt-in + a real Pro
42
+ * license). It is deliberately NOT a substitute for a caller's own production
43
+ * gate — anything that bends app state through it must check `__DEV__` itself,
44
+ * because the actions reachable here (network overrides, storage writes,
45
+ * impersonation) have no business running in a shipped app.
46
+ */
47
+
48
+ const REGISTRY_KEY = "__buoyToolActionRegistry";
49
+
50
+ /** The duck-typed adapter shape, matching external-sync's `ToolSyncAdapter`. */
51
+
52
+ /**
53
+ * A SET of getters, not one slot — React can briefly mount two providers, or
54
+ * re-run an effect's setup before the previous cleanup. A last-writer-wins slot
55
+ * would let the second registration silently blank the first.
56
+ */
57
+
58
+ function sources() {
59
+ const g = globalThis;
60
+ if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = new Set();
61
+ return g[REGISTRY_KEY];
62
+ }
63
+
64
+ /**
65
+ * Publish the live adapter map. Called by `autoExternalSync` with a getter that
66
+ * reads its current `useMemo` value. Returns the unregister function.
67
+ */
68
+ function registerToolAdapters(getTools) {
69
+ const set = sources();
70
+ set.add(getTools);
71
+ return () => {
72
+ set.delete(getTools);
73
+ };
74
+ }
75
+
76
+ /** The merged adapter map across every live source. */
77
+ function getToolAdapters() {
78
+ const merged = {};
79
+ for (const get of sources()) {
80
+ try {
81
+ Object.assign(merged, get());
82
+ } catch {
83
+ // A source that throws must not take the whole registry down.
84
+ }
85
+ }
86
+ return merged;
87
+ }
88
+
89
+ /** Tool ids currently dispatchable. */
90
+ function listToolIds() {
91
+ return Object.keys(getToolAdapters());
92
+ }
93
+
94
+ /** `toolId -> action names`, for pre-flight validation and capability display. */
95
+ function listToolActions() {
96
+ const out = {};
97
+ for (const [id, adapter] of Object.entries(getToolAdapters())) {
98
+ out[id] = Object.keys(adapter.actions ?? {});
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Tool ids whose adapter publishes a `getSnapshot`.
105
+ *
106
+ * Buoy's read surface is split in two: some tools answer through `actions`
107
+ * (`listQueries`, `async.getAllKeys`), and some answer only through the
108
+ * snapshot the sync layer polls — console entries, network events, the route
109
+ * sitemap, the env table. Anything driving tools ON the device sees only the
110
+ * first half unless it asks for the second, which is why the console and
111
+ * network panels look empty to an in-app agent that has an action for
112
+ * everything except reading them.
113
+ */
114
+ function listSnapshotToolIds() {
115
+ return Object.entries(getToolAdapters()).filter(([, adapter]) => typeof adapter.getSnapshot === "function").map(([id]) => id);
116
+ }
117
+
118
+ /**
119
+ * Read one tool's snapshot — the same value `attachDeviceSync` puts on the wire
120
+ * for MCP's `get_snapshot`, without a socket.
121
+ *
122
+ * Async to match {@link dispatchToolAction}: every caller already awaits, and
123
+ * an adapter is free to make this a promise later without breaking them.
124
+ */
125
+ async function readToolSnapshot(toolId) {
126
+ const tools = getToolAdapters();
127
+ const adapter = tools[toolId];
128
+ if (!adapter) throw new Error(`Unknown tool "${toolId}"`);
129
+ if (typeof adapter.getSnapshot !== "function") {
130
+ throw new Error(`Tool "${toolId}" has no snapshot to read`);
131
+ }
132
+ return await adapter.getSnapshot();
133
+ }
134
+ function isToolRegistryReady() {
135
+ return sources().size > 0;
136
+ }
137
+
138
+ /**
139
+ * Dispatch one action, shaping failures exactly as `handleAction` does: an
140
+ * unknown tool and an unknown action are distinguishable, and adapter errors
141
+ * surface as plain messages rather than objects nobody can read on a phone.
142
+ */
143
+ async function dispatchToolAction(toolId, action, params) {
144
+ const tools = getToolAdapters();
145
+ const tool = tools[toolId];
146
+ const fn = tool?.actions?.[action];
147
+ if (!fn) {
148
+ throw new Error(tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`);
149
+ }
150
+ return await fn(params);
151
+ }
152
+
153
+ /**
154
+ * Wait for the registry to be populated, for the narrow window between first
155
+ * frame and `AutoExternalSync` mounting. Resolves false on timeout so the
156
+ * caller can say "Buoy tools are still starting up" instead of "unknown tool".
157
+ */
158
+ async function waitForToolRegistry(timeoutMs = 2000) {
159
+ if (isToolRegistryReady()) return true;
160
+ const deadline = Date.now() + timeoutMs;
161
+ while (Date.now() < deadline) {
162
+ await new Promise(r => setTimeout(r, 50));
163
+ if (isToolRegistryReady()) return true;
164
+ }
165
+ return false;
166
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.canOpenBuoyTools = canOpenBuoyTools;
7
+ exports.openBuoyTool = openBuoyTool;
8
+ exports.registerToolOpener = registerToolOpener;
9
+ /**
10
+ * toolUiBridge — let a tool ask the host to bring another tool back on screen.
11
+ *
12
+ * WHY THIS EXISTS. Buoy's tool overlays (the SIMULATED banner, the recording
13
+ * banner, the perf HUD) render as siblings of `AppHostProvider`, not inside it,
14
+ * so none of them can reach the host that knows how to open and restore tools.
15
+ * That is usually fine — an overlay's job is to be seen, not to navigate. It
16
+ * stops being fine the moment an overlay is the ONLY thing on screen and the
17
+ * next step lives inside a tool: the macro recorder minimizes Scenarios so a
18
+ * tester can drive the app, and when they stop, the review screen they need is
19
+ * behind a tool icon they have to know to hunt for.
20
+ *
21
+ * The alternative was moving every overlay inside the provider, which would
22
+ * re-order the stacking of six unrelated tools to fix one banner's button. This
23
+ * is the same global-seam shape `toolActionRegistry` already uses for the same
24
+ * reason: the capability lives inside a component tree, the caller must not
25
+ * import `@buoy-gg/core` (that package imports the tools, and a tool importing
26
+ * it back is a require cycle), so it is published through a global.
27
+ *
28
+ * A SET of openers, not one slot, for the reason the action registry keeps one:
29
+ * React can briefly mount two providers, or re-run an effect's setup before the
30
+ * previous cleanup, and a last-writer-wins slot lets the second registration
31
+ * silently blank the first.
32
+ */
33
+
34
+ const BRIDGE_KEY = "__buoyToolUiBridge";
35
+
36
+ /** Returns true when the tool was actually brought back on screen. */
37
+
38
+ function openers() {
39
+ const g = globalThis;
40
+ if (!g[BRIDGE_KEY]) g[BRIDGE_KEY] = new Set();
41
+ return g[BRIDGE_KEY];
42
+ }
43
+
44
+ /** Published by the host, inside the provider that owns open tools. */
45
+ function registerToolOpener(opener) {
46
+ const set = openers();
47
+ set.add(opener);
48
+ return () => {
49
+ set.delete(opener);
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Ask the host to show a tool.
55
+ *
56
+ * Returns false when nothing could do it — no host mounted, or the tool isn't
57
+ * open for it to restore. Callers must treat that as a real answer and say so,
58
+ * rather than rendering a button that silently does nothing.
59
+ */
60
+ function openBuoyTool(toolId) {
61
+ for (const opener of openers()) {
62
+ try {
63
+ if (opener(toolId)) return true;
64
+ } catch {
65
+ // A host that throws must not stop another from answering.
66
+ }
67
+ }
68
+ return false;
69
+ }
70
+
71
+ /** True when some host is listening — for deciding whether to offer the button. */
72
+ function canOpenBuoyTools() {
73
+ return openers().size > 0;
74
+ }
@@ -29,6 +29,8 @@ function CompactRow({
29
29
  testID,
30
30
  statusTestID,
31
31
  statusSublabelTestID,
32
+ primaryTestID,
33
+ secondaryTestID,
32
34
  accessibilityLabel
33
35
  }) {
34
36
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
@@ -82,12 +84,14 @@ function CompactRow({
82
84
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
83
85
  style: styles.queryHash,
84
86
  numberOfLines: isExpanded ? undefined : 2,
87
+ testID: primaryTestID,
85
88
  children: primaryText
86
89
  }), !isExpanded && (secondaryText || secondaryAccessory) ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
87
90
  style: styles.secondaryRow,
88
91
  children: [secondaryText ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
89
92
  style: styles.secondaryText,
90
93
  numberOfLines: 1,
94
+ testID: secondaryTestID,
91
95
  children: secondaryText
92
96
  }) : null, secondaryAccessory]
93
97
  }) : null]
@@ -24,7 +24,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
24
24
  const DiffModeTabs = exports.DiffModeTabs = /*#__PURE__*/(0, _react.memo)(function DiffModeTabs({
25
25
  tabs,
26
26
  activeTab,
27
- onTabChange
27
+ onTabChange,
28
+ testIDPrefix
28
29
  }) {
29
30
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
30
31
  style: styles.container,
@@ -36,6 +37,13 @@ const DiffModeTabs = exports.DiffModeTabs = /*#__PURE__*/(0, _react.memo)(functi
36
37
  onPress: () => !isDisabled && onTabChange(tab.key),
37
38
  activeOpacity: isDisabled ? 1 : 0.7,
38
39
  disabled: isDisabled,
40
+ testID: testIDPrefix ? `${testIDPrefix}-${tab.key}` : undefined,
41
+ accessibilityRole: "button",
42
+ accessibilityLabel: tab.label,
43
+ accessibilityState: {
44
+ selected: isActive,
45
+ disabled: isDisabled
46
+ },
39
47
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
40
48
  style: [styles.tabText, isActive && styles.tabTextActive, isDisabled && styles.tabTextDisabled],
41
49
  children: tab.label
@@ -45,7 +45,8 @@ function PillBadge({
45
45
  color,
46
46
  children,
47
47
  icon,
48
- size = "md"
48
+ size = "md",
49
+ testID
49
50
  }) {
50
51
  const isSm = size === "sm";
51
52
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -60,6 +61,7 @@ function PillBadge({
60
61
  style: [styles.pillText, isSm && styles.pillTextSm, {
61
62
  color
62
63
  }],
64
+ testID: testID,
63
65
  children: children
64
66
  })]
65
67
  });