@buoy-gg/shared-ui 7.0.32 → 7.0.34

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 (237) hide show
  1. package/lib/commonjs/JsModal.js +70 -28
  2. package/lib/commonjs/hooks/index.js +26 -0
  3. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  4. package/lib/commonjs/hooks/useChangeLog.js +55 -0
  5. package/lib/commonjs/hooks/useEventStoreEvents.js +65 -0
  6. package/lib/commonjs/hooks/usePersistedState.js +45 -10
  7. package/lib/commonjs/index.js +143 -0
  8. package/lib/commonjs/storage/definePersistedSetting.js +67 -0
  9. package/lib/commonjs/storage/devToolsStorageKeys.js +12 -0
  10. package/lib/commonjs/storage/index.js +8 -1
  11. package/lib/commonjs/stores/ChangeLog.js +197 -0
  12. package/lib/commonjs/stores/createToolStore.js +175 -0
  13. package/lib/commonjs/stores/index.js +15 -1
  14. package/lib/commonjs/sync/toolSyncAdapter.js +63 -0
  15. package/lib/commonjs/sync/wireForm.js +172 -0
  16. package/lib/commonjs/tools/defineTool.js +79 -0
  17. package/lib/commonjs/ui/components/ChangeRow.js +110 -0
  18. package/lib/commonjs/ui/components/ChipStrip.js +94 -0
  19. package/lib/commonjs/ui/components/DetailViewToggle.js +130 -0
  20. package/lib/commonjs/ui/components/DevToolsCard.js +3 -1
  21. package/lib/commonjs/ui/components/EmptyState.js +10 -2
  22. package/lib/commonjs/ui/components/EntityFilterView.js +354 -0
  23. package/lib/commonjs/ui/components/ExportPreview.js +174 -0
  24. package/lib/commonjs/ui/components/InfoRow.js +90 -0
  25. package/lib/commonjs/ui/components/ModalHeader.js +40 -7
  26. package/lib/commonjs/ui/components/Pill.js +114 -0
  27. package/lib/commonjs/ui/components/ToggleRow.js +116 -0
  28. package/lib/commonjs/ui/components/ToolEmptyState.js +154 -0
  29. package/lib/commonjs/ui/components/ToolFooter.js +204 -0
  30. package/lib/commonjs/ui/components/WindowControls.js +8 -5
  31. package/lib/commonjs/ui/components/index.js +113 -1
  32. package/lib/commonjs/ui/index.js +36 -2
  33. package/lib/commonjs/ui/listPresets.js +43 -0
  34. package/lib/commonjs/ui/night/primitives.js +3 -1
  35. package/lib/commonjs/ui/otto/OttoSprite.js +510 -0
  36. package/lib/commonjs/ui/otto/index.js +38 -0
  37. package/lib/commonjs/ui/otto/ottoEngine.js +2734 -0
  38. package/lib/commonjs/ui/otto/ottoSheet.js +191 -0
  39. package/lib/commonjs/utils/snapshotProviderKit.js +167 -0
  40. package/lib/commonjs/utils/stateTools.js +160 -0
  41. package/lib/module/JsModal.js +70 -28
  42. package/lib/module/hooks/index.js +3 -1
  43. package/lib/module/hooks/safe-area-impl.js +1 -1
  44. package/lib/module/hooks/useChangeLog.js +50 -0
  45. package/lib/module/hooks/useEventStoreEvents.js +61 -0
  46. package/lib/module/hooks/usePersistedState.js +44 -10
  47. package/lib/module/index.js +11 -1
  48. package/lib/module/storage/definePersistedSetting.js +63 -0
  49. package/lib/module/storage/devToolsStorageKeys.js +12 -0
  50. package/lib/module/storage/index.js +2 -1
  51. package/lib/module/stores/ChangeLog.js +192 -0
  52. package/lib/module/stores/createToolStore.js +171 -0
  53. package/lib/module/stores/index.js +3 -1
  54. package/lib/module/sync/toolSyncAdapter.js +58 -0
  55. package/lib/module/sync/wireForm.js +159 -0
  56. package/lib/module/tools/defineTool.js +74 -0
  57. package/lib/module/ui/components/ChangeRow.js +105 -0
  58. package/lib/module/ui/components/ChipStrip.js +91 -0
  59. package/lib/module/ui/components/DetailViewToggle.js +125 -0
  60. package/lib/module/ui/components/DevToolsCard.js +3 -1
  61. package/lib/module/ui/components/EmptyState.js +10 -2
  62. package/lib/module/ui/components/EntityFilterView.js +350 -0
  63. package/lib/module/ui/components/ExportPreview.js +169 -0
  64. package/lib/module/ui/components/InfoRow.js +86 -0
  65. package/lib/module/ui/components/ModalHeader.js +40 -7
  66. package/lib/module/ui/components/Pill.js +110 -0
  67. package/lib/module/ui/components/ToggleRow.js +112 -0
  68. package/lib/module/ui/components/ToolEmptyState.js +149 -0
  69. package/lib/module/ui/components/ToolFooter.js +196 -0
  70. package/lib/module/ui/components/WindowControls.js +8 -5
  71. package/lib/module/ui/components/index.js +13 -1
  72. package/lib/module/ui/index.js +7 -1
  73. package/lib/module/ui/listPresets.js +39 -0
  74. package/lib/module/ui/night/primitives.js +3 -1
  75. package/lib/module/ui/otto/OttoSprite.js +496 -0
  76. package/lib/module/ui/otto/index.js +20 -0
  77. package/lib/module/ui/otto/ottoEngine.js +2726 -0
  78. package/lib/module/ui/otto/ottoSheet.js +180 -0
  79. package/lib/module/utils/snapshotProviderKit.js +156 -0
  80. package/lib/module/utils/stateTools.js +154 -0
  81. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/hooks/index.d.ts +3 -1
  83. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  85. package/lib/typescript/commonjs/hooks/useChangeLog.d.ts +38 -0
  86. package/lib/typescript/commonjs/hooks/useChangeLog.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts +25 -0
  88. package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/hooks/usePersistedState.d.ts +34 -5
  90. package/lib/typescript/commonjs/hooks/usePersistedState.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/index.d.ts +6 -1
  92. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts +39 -0
  94. package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +12 -0
  96. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/storage/index.d.ts +1 -0
  98. package/lib/typescript/commonjs/storage/index.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/stores/ChangeLog.d.ts +100 -0
  100. package/lib/typescript/commonjs/stores/ChangeLog.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/stores/createToolStore.d.ts +92 -0
  102. package/lib/typescript/commonjs/stores/createToolStore.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/stores/index.d.ts +2 -0
  104. package/lib/typescript/commonjs/stores/index.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts +66 -0
  106. package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/sync/wireForm.d.ts +100 -0
  108. package/lib/typescript/commonjs/sync/wireForm.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/tools/defineTool.d.ts +100 -0
  110. package/lib/typescript/commonjs/tools/defineTool.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts +52 -0
  112. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts +41 -0
  114. package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts +36 -0
  116. package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts +8 -1
  118. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/components/EmptyState.d.ts +14 -1
  120. package/lib/typescript/commonjs/ui/components/EmptyState.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts +51 -0
  122. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts +48 -0
  124. package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts +33 -0
  126. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +31 -1
  128. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/ui/components/Pill.d.ts +38 -0
  130. package/lib/typescript/commonjs/ui/components/Pill.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts +33 -0
  132. package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/ui/components/ToolEmptyState.d.ts +113 -0
  134. package/lib/typescript/commonjs/ui/components/ToolEmptyState.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts +69 -0
  136. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts +7 -1
  138. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/ui/components/index.d.ts +11 -0
  140. package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/ui/index.d.ts +2 -0
  142. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/ui/listPresets.d.ts +28 -0
  144. package/lib/typescript/commonjs/ui/listPresets.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/ui/night/primitives.d.ts +3 -1
  146. package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/ui/otto/OttoSprite.d.ts +103 -0
  148. package/lib/typescript/commonjs/ui/otto/OttoSprite.d.ts.map +1 -0
  149. package/lib/typescript/commonjs/ui/otto/index.d.ts +19 -0
  150. package/lib/typescript/commonjs/ui/otto/index.d.ts.map +1 -0
  151. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts +2205 -0
  152. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts.map +1 -0
  153. package/lib/typescript/commonjs/ui/otto/ottoSheet.d.ts +73 -0
  154. package/lib/typescript/commonjs/ui/otto/ottoSheet.d.ts.map +1 -0
  155. package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts +81 -0
  156. package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts.map +1 -0
  157. package/lib/typescript/commonjs/utils/stateTools.d.ts +44 -0
  158. package/lib/typescript/commonjs/utils/stateTools.d.ts.map +1 -0
  159. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  160. package/lib/typescript/module/hooks/index.d.ts +3 -1
  161. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  162. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  163. package/lib/typescript/module/hooks/useChangeLog.d.ts +38 -0
  164. package/lib/typescript/module/hooks/useChangeLog.d.ts.map +1 -0
  165. package/lib/typescript/module/hooks/useEventStoreEvents.d.ts +25 -0
  166. package/lib/typescript/module/hooks/useEventStoreEvents.d.ts.map +1 -0
  167. package/lib/typescript/module/hooks/usePersistedState.d.ts +34 -5
  168. package/lib/typescript/module/hooks/usePersistedState.d.ts.map +1 -1
  169. package/lib/typescript/module/index.d.ts +6 -1
  170. package/lib/typescript/module/index.d.ts.map +1 -1
  171. package/lib/typescript/module/storage/definePersistedSetting.d.ts +39 -0
  172. package/lib/typescript/module/storage/definePersistedSetting.d.ts.map +1 -0
  173. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +12 -0
  174. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  175. package/lib/typescript/module/storage/index.d.ts +1 -0
  176. package/lib/typescript/module/storage/index.d.ts.map +1 -1
  177. package/lib/typescript/module/stores/ChangeLog.d.ts +100 -0
  178. package/lib/typescript/module/stores/ChangeLog.d.ts.map +1 -0
  179. package/lib/typescript/module/stores/createToolStore.d.ts +92 -0
  180. package/lib/typescript/module/stores/createToolStore.d.ts.map +1 -0
  181. package/lib/typescript/module/stores/index.d.ts +2 -0
  182. package/lib/typescript/module/stores/index.d.ts.map +1 -1
  183. package/lib/typescript/module/sync/toolSyncAdapter.d.ts +66 -0
  184. package/lib/typescript/module/sync/toolSyncAdapter.d.ts.map +1 -0
  185. package/lib/typescript/module/sync/wireForm.d.ts +100 -0
  186. package/lib/typescript/module/sync/wireForm.d.ts.map +1 -0
  187. package/lib/typescript/module/tools/defineTool.d.ts +100 -0
  188. package/lib/typescript/module/tools/defineTool.d.ts.map +1 -0
  189. package/lib/typescript/module/ui/components/ChangeRow.d.ts +52 -0
  190. package/lib/typescript/module/ui/components/ChangeRow.d.ts.map +1 -0
  191. package/lib/typescript/module/ui/components/ChipStrip.d.ts +41 -0
  192. package/lib/typescript/module/ui/components/ChipStrip.d.ts.map +1 -0
  193. package/lib/typescript/module/ui/components/DetailViewToggle.d.ts +36 -0
  194. package/lib/typescript/module/ui/components/DetailViewToggle.d.ts.map +1 -0
  195. package/lib/typescript/module/ui/components/DevToolsCard.d.ts +8 -1
  196. package/lib/typescript/module/ui/components/DevToolsCard.d.ts.map +1 -1
  197. package/lib/typescript/module/ui/components/EmptyState.d.ts +14 -1
  198. package/lib/typescript/module/ui/components/EmptyState.d.ts.map +1 -1
  199. package/lib/typescript/module/ui/components/EntityFilterView.d.ts +51 -0
  200. package/lib/typescript/module/ui/components/EntityFilterView.d.ts.map +1 -0
  201. package/lib/typescript/module/ui/components/ExportPreview.d.ts +48 -0
  202. package/lib/typescript/module/ui/components/ExportPreview.d.ts.map +1 -0
  203. package/lib/typescript/module/ui/components/InfoRow.d.ts +33 -0
  204. package/lib/typescript/module/ui/components/InfoRow.d.ts.map +1 -0
  205. package/lib/typescript/module/ui/components/ModalHeader.d.ts +31 -1
  206. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  207. package/lib/typescript/module/ui/components/Pill.d.ts +38 -0
  208. package/lib/typescript/module/ui/components/Pill.d.ts.map +1 -0
  209. package/lib/typescript/module/ui/components/ToggleRow.d.ts +33 -0
  210. package/lib/typescript/module/ui/components/ToggleRow.d.ts.map +1 -0
  211. package/lib/typescript/module/ui/components/ToolEmptyState.d.ts +113 -0
  212. package/lib/typescript/module/ui/components/ToolEmptyState.d.ts.map +1 -0
  213. package/lib/typescript/module/ui/components/ToolFooter.d.ts +69 -0
  214. package/lib/typescript/module/ui/components/ToolFooter.d.ts.map +1 -0
  215. package/lib/typescript/module/ui/components/WindowControls.d.ts +7 -1
  216. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  217. package/lib/typescript/module/ui/components/index.d.ts +11 -0
  218. package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
  219. package/lib/typescript/module/ui/index.d.ts +2 -0
  220. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  221. package/lib/typescript/module/ui/listPresets.d.ts +28 -0
  222. package/lib/typescript/module/ui/listPresets.d.ts.map +1 -0
  223. package/lib/typescript/module/ui/night/primitives.d.ts +3 -1
  224. package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -1
  225. package/lib/typescript/module/ui/otto/OttoSprite.d.ts +103 -0
  226. package/lib/typescript/module/ui/otto/OttoSprite.d.ts.map +1 -0
  227. package/lib/typescript/module/ui/otto/index.d.ts +19 -0
  228. package/lib/typescript/module/ui/otto/index.d.ts.map +1 -0
  229. package/lib/typescript/module/ui/otto/ottoEngine.d.ts +2205 -0
  230. package/lib/typescript/module/ui/otto/ottoEngine.d.ts.map +1 -0
  231. package/lib/typescript/module/ui/otto/ottoSheet.d.ts +73 -0
  232. package/lib/typescript/module/ui/otto/ottoSheet.d.ts.map +1 -0
  233. package/lib/typescript/module/utils/snapshotProviderKit.d.ts +81 -0
  234. package/lib/typescript/module/utils/snapshotProviderKit.d.ts.map +1 -0
  235. package/lib/typescript/module/utils/stateTools.d.ts +44 -0
  236. package/lib/typescript/module/utils/stateTools.d.ts.map +1 -0
  237. package/package.json +4 -4
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * React bindings for `ChangeLog` — what useReduxActions, useZustandStateChanges
5
+ * and useJotaiAtomChanges each re-derived: seed state from the log, subscribe
6
+ * for the lifetime of the component, hold a filter, memoize the filtered
7
+ * list, and expose clear / pause / lookup.
8
+ *
9
+ * Tool-specific stats and registries (stores, atoms) compose on top — see
10
+ * `useSubscribedValue` for the registry half.
11
+ */
12
+
13
+ import { useCallback, useEffect, useMemo, useState } from "react";
14
+ export function useChangeLog(log, options = {}) {
15
+ const [items, setItems] = useState(() => log.getAll());
16
+ const [filter, setFilter] = useState(() => options.initialFilter ?? {});
17
+ const [isEnabled, setIsEnabled] = useState(() => log.getEnabled());
18
+ useEffect(() => log.subscribe(setItems), [log]);
19
+ const filterItems = options.filterItems;
20
+ const filteredItems = useMemo(() => filterItems ? filterItems(items, filter) : items, [items, filter, filterItems]);
21
+ const clear = useCallback(() => {
22
+ log.clear();
23
+ }, [log]);
24
+ const toggleCapture = useCallback(() => {
25
+ const next = !isEnabled;
26
+ log.setEnabled(next);
27
+ setIsEnabled(next);
28
+ }, [log, isEnabled]);
29
+ const getById = useCallback(id => log.getById(id), [log]);
30
+ return {
31
+ items,
32
+ filteredItems,
33
+ filter,
34
+ setFilter,
35
+ isEnabled,
36
+ toggleCapture,
37
+ clear,
38
+ getById
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Hold a value that a store pushes through a `(value) => void` listener —
44
+ * a registry of stores, atoms, sources. Seeded synchronously from `read`.
45
+ */
46
+ export function useSubscribedValue(read, subscribe) {
47
+ const [value, setValue] = useState(read);
48
+ useEffect(() => subscribe(setValue), [subscribe]);
49
+ return value;
50
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * React bindings for a `BaseEventStore` — what useRouteEvents and
5
+ * useStorageEvents each hand-rolled: subscribe while enabled (which
6
+ * auto-starts the store's capture), coalesce bursts onto one animation
7
+ * frame so a storm of writes is one commit instead of one per emission,
8
+ * select/limit the list, and report whether capture is live.
9
+ */
10
+
11
+ import { useCallback, useEffect, useState } from "react";
12
+ export function useEventStoreEvents(store, options = {}) {
13
+ const {
14
+ enabled = true,
15
+ maxEvents = 500,
16
+ select
17
+ } = options;
18
+ const [events, setEvents] = useState([]);
19
+ // Tracked locally so it updates reactively with the subscription.
20
+ const [isActive, setIsActive] = useState(enabled);
21
+ useEffect(() => {
22
+ if (!enabled) {
23
+ setIsActive(false);
24
+ return;
25
+ }
26
+
27
+ // Frame-coalesced: bursts land as ONE commit per frame instead of one
28
+ // per emission — the dominant live-churn cost measured on the network
29
+ // tool (tool-bench 2026-07-06).
30
+ let rafId = null;
31
+ let pending = null;
32
+ const unsubscribe = store.subscribeToEvents(all => {
33
+ pending = all;
34
+ if (rafId != null) return;
35
+ rafId = requestAnimationFrame(() => {
36
+ rafId = null;
37
+ if (!pending) return;
38
+ let next = pending;
39
+ pending = null;
40
+ if (select) next = select(next);
41
+ if (next.length > maxEvents) next = next.slice(0, maxEvents);
42
+ setEvents(next);
43
+ });
44
+ });
45
+ setIsActive(true);
46
+ return () => {
47
+ unsubscribe();
48
+ if (rafId != null) cancelAnimationFrame(rafId);
49
+ // Not setting isActive=false here: a dependency change re-subscribes
50
+ // immediately, and that is not "stopped".
51
+ };
52
+ }, [store, enabled, maxEvents, select]);
53
+ const clearEvents = useCallback(() => {
54
+ store.clearEvents();
55
+ }, [store]);
56
+ return {
57
+ events,
58
+ clearEvents,
59
+ isActive
60
+ };
61
+ }
@@ -10,25 +10,43 @@ import { persistentStorage } from "../utils/persistentStorage.js";
10
10
  */
11
11
  const hydratedCache = new Map();
12
12
 
13
+ /** The two calls a backend needs. `persistentStorage` and AsyncStorage both fit. */
14
+
15
+ function defaultParse(raw) {
16
+ try {
17
+ return JSON.parse(raw);
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
13
23
  /**
14
24
  * `useState` persisted through `persistentStorage` (FileSystem → AsyncStorage
15
25
  * → memory). Replaces the load-then-`hasLoaded`-guard `useEffect` pattern
16
26
  * every tool used to hand-roll.
17
27
  *
18
- * Values are JSON-serialized; the default is returned until hydration
19
- * completes (subsequent mounts of the same key hydrate synchronously from an
20
- * in-memory cache). The third tuple element reports hydration so callers can
21
- * defer writes that must not clobber a not-yet-loaded value.
28
+ * Values are JSON-serialized by default; the default is returned until
29
+ * hydration completes (subsequent mounts of the same key hydrate synchronously
30
+ * from an in-memory cache). The third tuple element reports hydration so
31
+ * callers can defer writes that must not clobber a not-yet-loaded value.
22
32
  *
23
33
  * ```tsx
24
34
  * const [tab, setTab, tabHydrated] = usePersistedState("@react_buoy_custom_flags_tab", "flags");
35
+ *
36
+ * // A Set, stored as an array:
37
+ * const [pins, setPins] = usePersistedState(key, new Set<string>(), {
38
+ * parse: (raw) => new Set(JSON.parse(raw) as string[]),
39
+ * serialize: (set) => JSON.stringify([...set]),
40
+ * });
25
41
  * ```
26
42
  */
27
- export function usePersistedState(key, defaultValue) {
43
+ export function usePersistedState(key, defaultValue, options = {}) {
28
44
  const [value, setValue] = useState(() => hydratedCache.has(key) ? hydratedCache.get(key) : defaultValue);
29
45
  const [hydrated, setHydrated] = useState(() => hydratedCache.has(key));
30
46
  const keyRef = useRef(key);
31
47
  keyRef.current = key;
48
+ const optionsRef = useRef(options);
49
+ optionsRef.current = options;
32
50
  useEffect(() => {
33
51
  if (hydratedCache.has(key)) {
34
52
  setValue(hydratedCache.get(key));
@@ -36,17 +54,22 @@ export function usePersistedState(key, defaultValue) {
36
54
  return;
37
55
  }
38
56
  let cancelled = false;
39
- persistentStorage.getItem(key).then(raw => {
57
+ const storage = optionsRef.current.storage ?? persistentStorage;
58
+ const parse = optionsRef.current.parse ?? defaultParse;
59
+ storage.getItem(key).then(raw => {
40
60
  if (cancelled) return;
41
61
  if (raw != null) {
62
+ let parsed;
42
63
  try {
43
- const parsed = JSON.parse(raw);
44
- hydratedCache.set(key, parsed);
45
- setValue(parsed);
64
+ parsed = parse(raw);
46
65
  } catch {
47
66
  // Corrupt payload — fall back to the default and let the next
48
67
  // write repair it.
49
68
  }
69
+ if (parsed !== undefined) {
70
+ hydratedCache.set(key, parsed);
71
+ setValue(parsed);
72
+ }
50
73
  }
51
74
  setHydrated(true);
52
75
  }).catch(() => {
@@ -60,9 +83,20 @@ export function usePersistedState(key, defaultValue) {
60
83
  setValue(prev => {
61
84
  const resolved = typeof next === "function" ? next(prev) : next;
62
85
  hydratedCache.set(keyRef.current, resolved);
63
- persistentStorage.setItem(keyRef.current, JSON.stringify(resolved)).catch(() => {});
86
+ const storage = optionsRef.current.storage ?? persistentStorage;
87
+ const serialize = optionsRef.current.serialize ?? JSON.stringify;
88
+ try {
89
+ storage.setItem(keyRef.current, serialize(resolved)).catch(() => {});
90
+ } catch {
91
+ // Unserializable value — keep it in memory, skip the write.
92
+ }
64
93
  return resolved;
65
94
  });
66
95
  }, []);
67
96
  return [value, set, hydrated];
97
+ }
98
+
99
+ /** Test seam: forget every hydrated value. */
100
+ export function resetPersistedStateCache() {
101
+ hydratedCache.clear();
68
102
  }
@@ -6,6 +6,10 @@ export * from "./ui/index.js";
6
6
  // Store base classes
7
7
  export * from "./stores/index.js";
8
8
  export * from "./sync/wireBudget.js";
9
+ export * from "./sync/wireForm.js";
10
+ export * from "./sync/toolSyncAdapter.js";
11
+ // Typed tool presets + derived createXTool factories (structurally InstalledApp)
12
+ export * from "./tools/defineTool.js";
9
13
  export * from "./sync/toolActionRegistry.js";
10
14
  export * from "./sync/toolUiBridge.js";
11
15
 
@@ -44,6 +48,12 @@ Subscribable,
44
48
  subscriberCountNotifier, subscribeToSubscriberCountChanges, notifySubscriberCountChange,
45
49
  // Safe expo-router wrappers (falls back to no-ops on RN CLI)
46
50
  useSafeRouter, useSafePathname, useSafeSegments, useSafeGlobalSearchParams, getSafeRouter, getSafeCurrentPathname, getSafeCurrentRoute, isExpoRouterAvailable } from "./utils/index.js";
51
+ // Time Machine snapshot-provider kit (restore scope, verdict ladder, result shapes)
52
+ export { PREVIEW_ITEM_RENDER_BYTE_LIMIT, createRestoreScope, emptyRestoreResult, failedRestore, skippedRestore, classifyPreviewItem, wontApply, unionKeys } from "./utils/snapshotProviderKit.js";
53
+
54
+ // State-tool helpers (entity colors, value previews, shallow diff summaries)
55
+ export { entityColor, formatValuePreview, shallowDiffSummary } from "./utils/stateTools.js";
56
+
47
57
  // Also export formatting utils
48
58
  export * from "./utils/formatting/index.js";
49
59
  export * from "./utils/time/index.js";
@@ -59,7 +69,7 @@ export * from "./clipboard/index.js";
59
69
  export * from "./logger/index.js";
60
70
  export * from "./settings/index.js";
61
71
  export * from "./storage/index.js";
62
- export { useSafeAreaInsets, useFilterManager, usePersistedState, useDeviceInfo, getDeviceInfo, logDeviceInfo } from "./hooks/index.js";
72
+ export { useSafeAreaInsets, useFilterManager, usePersistedState, resetPersistedStateCache, useChangeLog, useSubscribedValue, useEventStoreEvents, useDeviceInfo, getDeviceInfo, logDeviceInfo } from "./hooks/index.js";
63
73
 
64
74
  // JsModal
65
75
  export { JsModal } from "./JsModal";
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * definePersistedSetting — the `load / save / clear` triple every tool wrote
5
+ * for its module-level settings (copy settings, HUD prefs, JS TOP prefs, …).
6
+ *
7
+ * Contract, the same one all of them followed:
8
+ * - `load()` never throws and never returns a partial object: the stored
9
+ * value is merged OVER the defaults so fields added in a later release
10
+ * get their default instead of `undefined`, then `sanitize` gets the last
11
+ * word (drop values the UI no longer offers, coerce types).
12
+ * - `save()` and `clear()` are best-effort; persistence is optional.
13
+ * - `loadOrNull()` distinguishes "nothing saved yet" from the defaults, for
14
+ * callers that treat first launch differently.
15
+ */
16
+
17
+ import { persistentStorage } from "../utils/persistentStorage.js";
18
+ function isPlainObject(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ export function definePersistedSetting(key, defaults, options = {}) {
22
+ const storage = options.storage ?? persistentStorage;
23
+ const parse = options.parse ?? (raw => JSON.parse(raw));
24
+ const serialize = options.serialize ?? (value => JSON.stringify(value));
25
+ function resolve(stored) {
26
+ const merged = isPlainObject(defaults) && isPlainObject(stored) ? {
27
+ ...defaults,
28
+ ...stored
29
+ } : stored;
30
+ return options.sanitize ? options.sanitize(merged, stored) : merged;
31
+ }
32
+ async function loadOrNull() {
33
+ try {
34
+ const raw = await storage.getItem(key);
35
+ if (raw == null || raw === "") return null;
36
+ return resolve(parse(raw));
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ return {
42
+ key,
43
+ defaults,
44
+ loadOrNull,
45
+ async load() {
46
+ return (await loadOrNull()) ?? defaults;
47
+ },
48
+ async save(value) {
49
+ try {
50
+ await storage.setItem(key, serialize(value));
51
+ } catch {
52
+ // Persistence is best-effort.
53
+ }
54
+ },
55
+ async clear() {
56
+ try {
57
+ await storage.removeItem(key);
58
+ } catch {
59
+ // Persistence is best-effort.
60
+ }
61
+ }
62
+ };
63
+ }
@@ -250,6 +250,18 @@ export const devToolsStorageKeys = {
250
250
  pausedAt: () => `${devToolsStorageKeys.events.root()}_paused_at`,
251
251
  /** Copy/export settings */
252
252
  copySettings: () => `${devToolsStorageKeys.events.root()}_copy_settings`
253
+ },
254
+ /**
255
+ * Desktop/MCP sync identity
256
+ */
257
+ sync: {
258
+ root: () => `${devToolsStorageKeys.base}_sync`,
259
+ /**
260
+ * The per-install id minted the first time this app dials the broker.
261
+ * It is what makes two devices on the same build register as two
262
+ * devices — see core's deviceIdentity.ts.
263
+ */
264
+ deviceId: () => `${devToolsStorageKeys.sync.root()}_device_id`
253
265
  }
254
266
  };
255
267
 
@@ -1,3 +1,4 @@
1
1
  "use strict";
2
2
 
3
- export { devToolsStorageKeys, isDevToolsStorageKey } from "./devToolsStorageKeys.js";
3
+ export { devToolsStorageKeys, isDevToolsStorageKey } from "./devToolsStorageKeys.js";
4
+ export { definePersistedSetting } from "./definePersistedSetting.js";
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ChangeLog — the class-shaped event log the state tools share.
5
+ *
6
+ * redux (`ReduxActionStore`), zustand (`ZustandStateStore`), jotai
7
+ * (`JotaiStateStore`) and react-query (`ReactQueryEventStore`) each carried
8
+ * the same ~120 lines: a newest-first ring buffer, a `Set` of array listeners
9
+ * (`(items) => void`), a `Set` of clear listeners for the desktop's mirror
10
+ * mode, an `enabled` flag (the UI's pause button), a `captureSuppressed` flag
11
+ * (the dashboard must never record its own traffic), a monotonic id counter,
12
+ * `getById`, and hydrate-by-id for lazily fetched detail payloads.
13
+ *
14
+ * This is that core. Tool stores `extend` it and keep their public method
15
+ * names (`addStateChange`, `getActions`, `clearAtomChanges`, …) as thin
16
+ * wrappers, so no call site — including Buoy Desktop — has to change.
17
+ *
18
+ * Semantics preserved from the originals, deliberately:
19
+ * - `getAll()` returns a COPY (`[...items]`) — consumers seed React state
20
+ * from it and rely on a fresh identity per notification.
21
+ * - `clear()` always notifies, even when already empty, then fires the clear
22
+ * listeners (the dashboard forwards the clear to the device either way).
23
+ * - `replaceAll()` respects `enabled`: the pause button freezes a mirror.
24
+ * - `disableCapture()` is permanent for the store's lifetime.
25
+ */
26
+
27
+ export class ChangeLog {
28
+ /** Newest first. Subclasses may read; mutate only through the helpers. */
29
+ items = [];
30
+ itemListeners = new Set();
31
+ clearListeners = new Set();
32
+ idCounter = 0;
33
+ enabled = true;
34
+ captureSuppressed = false;
35
+ constructor(options = {}) {
36
+ this.max = options.max ?? 200;
37
+ }
38
+
39
+ // ---- Recording ----
40
+
41
+ /** True when a new item should be recorded (not paused, not a mirror). */
42
+ canRecord() {
43
+ return this.enabled && !this.captureSuppressed;
44
+ }
45
+
46
+ /** `${Date.now()}-${n}` — unique per store instance, sorts by time. */
47
+ nextId() {
48
+ return `${Date.now()}-${++this.idCounter}`;
49
+ }
50
+
51
+ /**
52
+ * Prepend one item and trim to `max`. Does NOT notify — subclasses often
53
+ * touch companion state (per-entity counters, raw-state eviction) before
54
+ * they notify. Call `notifyListeners()` when done, or use `record()`.
55
+ */
56
+ push(item) {
57
+ this.items = [item, ...this.items].slice(0, this.max);
58
+ }
59
+
60
+ /** `push()` + notify. The common case. */
61
+ record(item) {
62
+ this.push(item);
63
+ this.notifyListeners();
64
+ }
65
+
66
+ /** Replace the buffer wholesale (already merged/hydrated by the caller) and notify. */
67
+ setItems(next) {
68
+ this.items = next.slice(0, this.max);
69
+ this.notifyListeners();
70
+ }
71
+
72
+ // ---- Reading ----
73
+
74
+ /** All items, newest first. Fresh array each call. */
75
+ getAll() {
76
+ return [...this.items];
77
+ }
78
+ getById(id) {
79
+ return this.items.find(item => item.id === id);
80
+ }
81
+ get size() {
82
+ return this.items.length;
83
+ }
84
+
85
+ // ---- Clearing ----
86
+
87
+ /**
88
+ * Empty the buffer, notify, then fire clear listeners. Always notifies —
89
+ * a clear is a user action and the list must repaint even if nothing was
90
+ * in it. Subclasses that keep companion counters reset them first.
91
+ */
92
+ clear() {
93
+ this.items = [];
94
+ this.notifyListeners();
95
+ this.clearListeners.forEach(listener => {
96
+ try {
97
+ listener();
98
+ } catch {
99
+ // Ignore listener errors
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Listen for `clear()` calls. Used in remote mirror mode to forward a
106
+ * clear performed in the dashboard UI to the synced device.
107
+ */
108
+ onClear(listener) {
109
+ this.clearListeners.add(listener);
110
+ return () => {
111
+ this.clearListeners.delete(listener);
112
+ };
113
+ }
114
+
115
+ // ---- Remote mirror mode ----
116
+
117
+ /**
118
+ * Permanently suppress local capture. Use when this store acts as a mirror
119
+ * of a remote device (the desktop dashboard): data arrives via
120
+ * `replaceAll()` / a subclass snapshot method and local instrumentation
121
+ * must never record.
122
+ */
123
+ disableCapture() {
124
+ this.captureSuppressed = true;
125
+ }
126
+
127
+ /** Whether the store is in remote mirror mode (capture suppressed). */
128
+ isCaptureSuppressed() {
129
+ return this.captureSuppressed;
130
+ }
131
+
132
+ /**
133
+ * Replace the whole list from a remote snapshot and notify. Respects the
134
+ * enabled flag so the UI's pause button freezes the mirror. Returns false
135
+ * when skipped. Subclasses with hydration-preserving merges compute the
136
+ * merged list themselves and call `setItems()`.
137
+ */
138
+ replaceAll(next) {
139
+ if (!this.enabled) return false;
140
+ this.setItems(next);
141
+ return true;
142
+ }
143
+
144
+ /**
145
+ * Patch one item in place (by id) and notify. Used to hydrate a mirrored
146
+ * item with detail payloads fetched on demand from the device.
147
+ */
148
+ hydrateById(id, patch) {
149
+ const index = this.items.findIndex(item => item.id === id);
150
+ if (index === -1) return;
151
+ this.items = [...this.items.slice(0, index), {
152
+ ...this.items[index],
153
+ ...patch
154
+ }, ...this.items.slice(index + 1)];
155
+ this.notifyListeners();
156
+ }
157
+
158
+ // ---- Enable / disable ----
159
+
160
+ setEnabled(enabled) {
161
+ this.enabled = enabled;
162
+ }
163
+ getEnabled() {
164
+ return this.enabled;
165
+ }
166
+ setMax(max) {
167
+ this.max = max;
168
+ if (this.items.length > max) {
169
+ this.items = this.items.slice(0, max);
170
+ this.notifyListeners();
171
+ }
172
+ }
173
+ getMax() {
174
+ return this.max;
175
+ }
176
+
177
+ // ---- Subscriptions ----
178
+
179
+ subscribe(listener) {
180
+ this.itemListeners.add(listener);
181
+ return () => {
182
+ this.itemListeners.delete(listener);
183
+ };
184
+ }
185
+ notifyListeners() {
186
+ const items = this.getAll();
187
+ this.itemListeners.forEach(listener => listener(items));
188
+ }
189
+ get listenerCount() {
190
+ return this.itemListeners.size;
191
+ }
192
+ }
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * createToolStore — the one bounded, subscribable buffer every tool used to
5
+ * hand-roll.
6
+ *
7
+ * Before this existed, console, sentry, images, assets, focus-inspector,
8
+ * tv-remote, redux, zustand, jotai and react-query each carried their own copy
9
+ * of the same ~100 lines: a `Set` of listeners, a `Set` of clear listeners
10
+ * (so the desktop dashboard can forward a clear to the device in mirror
11
+ * mode), a ring buffer with a max, an optional frame-coalesced emit, and the
12
+ * `__buoyFlushToolSync` escape hatch that pushes a tool's snapshot to the
13
+ * dashboard NOW (a crash's last words must not wait on the sync throttle).
14
+ *
15
+ * This is a plain function, not a class, on purpose: every consumer exposes a
16
+ * module-level functional API (`subscribe`, `getSnapshot`, `clearX`) and
17
+ * feeds `useSyncExternalStore`, so the store is a value the module wraps
18
+ * rather than something it extends. `BaseEventStore` stays for the
19
+ * class-based, auto-start/stop capture stores (network, storage, routes).
20
+ *
21
+ * Contract:
22
+ * - `getSnapshot()` returns the SAME array reference until the next
23
+ * mutation, so `useSyncExternalStore` never re-renders for nothing.
24
+ * - `order: "append"` keeps the newest item LAST (console-style scrollback);
25
+ * `order: "prepend"` keeps it FIRST (event-log style). Trimming drops from
26
+ * the opposite end. Consumers never have to `.reverse()`.
27
+ * - `clear()` fires `onClear` listeners even when the buffer is already
28
+ * empty — the dashboard's forwarded clear must reach the device regardless.
29
+ * - `set()` (mirror mode) never fires clear listeners; it is a replacement.
30
+ */
31
+
32
+ /** Listener signature: change-only, read the snapshot yourself. */
33
+
34
+ function scheduleFrame(cb) {
35
+ if (typeof requestAnimationFrame === "function") {
36
+ requestAnimationFrame(() => cb());
37
+ } else {
38
+ setTimeout(cb, 0);
39
+ }
40
+ }
41
+ export function createToolStore(options) {
42
+ const id = options.id;
43
+ const order = options.order ?? "append";
44
+ const coalesce = options.coalesce ?? "sync";
45
+ const flushMinIntervalMs = options.flushMinIntervalMs ?? 150;
46
+ let max = options.max;
47
+ let items = [];
48
+ let paused = false;
49
+ let nextId = options.firstId ?? 1;
50
+ let lastFlushAt = 0;
51
+ let emitScheduled = false;
52
+ const listeners = new Set();
53
+ const clearListeners = new Set();
54
+ function fire() {
55
+ for (const listener of listeners) {
56
+ try {
57
+ listener();
58
+ } catch {
59
+ // A broken subscriber must not take the store (or a crash path) down.
60
+ }
61
+ }
62
+ }
63
+ function emit() {
64
+ if (coalesce === "sync") {
65
+ fire();
66
+ return;
67
+ }
68
+ if (emitScheduled) return;
69
+ emitScheduled = true;
70
+ scheduleFrame(() => {
71
+ emitScheduled = false;
72
+ fire();
73
+ });
74
+ }
75
+ function trim(list) {
76
+ if (list.length <= max) return list;
77
+ return order === "append" ? list.slice(list.length - max) : list.slice(0, max);
78
+ }
79
+ const store = {
80
+ id,
81
+ subscribe(listener) {
82
+ listeners.add(listener);
83
+ return () => {
84
+ listeners.delete(listener);
85
+ };
86
+ },
87
+ getSnapshot() {
88
+ return items;
89
+ },
90
+ size() {
91
+ return items.length;
92
+ },
93
+ add(item) {
94
+ if (paused) return;
95
+ if (order === "append") {
96
+ items = items.length >= max ? [...items.slice(items.length - max + 1), item] : [...items, item];
97
+ } else {
98
+ items = items.length >= max ? [item, ...items.slice(0, max - 1)] : [item, ...items];
99
+ }
100
+ emit();
101
+ },
102
+ set(next) {
103
+ items = trim(next);
104
+ emit();
105
+ },
106
+ update(fn) {
107
+ items = trim(fn(items));
108
+ emit();
109
+ },
110
+ clear() {
111
+ for (const listener of clearListeners) {
112
+ try {
113
+ listener();
114
+ } catch {
115
+ // ignore
116
+ }
117
+ }
118
+ if (items.length === 0) return;
119
+ items = [];
120
+ emit();
121
+ },
122
+ onClear(listener) {
123
+ clearListeners.add(listener);
124
+ return () => {
125
+ clearListeners.delete(listener);
126
+ };
127
+ },
128
+ notify() {
129
+ emit();
130
+ },
131
+ flushSync(force = false) {
132
+ const now = Date.now();
133
+ if (!force && now - lastFlushAt < flushMinIntervalMs) return;
134
+ lastFlushAt = now;
135
+ try {
136
+ const set = globalThis.__buoyFlushToolSync;
137
+ if (!set) return;
138
+ for (const flush of set) flush(id);
139
+ } catch {
140
+ // Best-effort: a crash path must never crash.
141
+ }
142
+ },
143
+ setPaused(next) {
144
+ paused = next;
145
+ },
146
+ isPaused() {
147
+ return paused;
148
+ },
149
+ setMax(next) {
150
+ max = next;
151
+ const trimmed = trim(items);
152
+ if (trimmed !== items) {
153
+ items = trimmed;
154
+ emit();
155
+ }
156
+ },
157
+ getMax() {
158
+ return max;
159
+ },
160
+ allocId() {
161
+ return nextId++;
162
+ },
163
+ bumpIdAbove(idFloor) {
164
+ if (idFloor + 1 > nextId) nextId = idFloor + 1;
165
+ },
166
+ listenerCount() {
167
+ return listeners.size;
168
+ }
169
+ };
170
+ return store;
171
+ }