@buoy-gg/shared-ui 7.0.33 → 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 (189) hide show
  1. package/lib/commonjs/JsModal.js +57 -22
  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/index.js +8 -1
  10. package/lib/commonjs/stores/ChangeLog.js +197 -0
  11. package/lib/commonjs/stores/createToolStore.js +175 -0
  12. package/lib/commonjs/stores/index.js +15 -1
  13. package/lib/commonjs/sync/toolSyncAdapter.js +63 -0
  14. package/lib/commonjs/sync/wireForm.js +172 -0
  15. package/lib/commonjs/tools/defineTool.js +79 -0
  16. package/lib/commonjs/ui/components/ChangeRow.js +110 -0
  17. package/lib/commonjs/ui/components/ChipStrip.js +94 -0
  18. package/lib/commonjs/ui/components/DetailViewToggle.js +130 -0
  19. package/lib/commonjs/ui/components/DevToolsCard.js +3 -1
  20. package/lib/commonjs/ui/components/EntityFilterView.js +354 -0
  21. package/lib/commonjs/ui/components/ExportPreview.js +174 -0
  22. package/lib/commonjs/ui/components/InfoRow.js +90 -0
  23. package/lib/commonjs/ui/components/ModalHeader.js +40 -7
  24. package/lib/commonjs/ui/components/Pill.js +114 -0
  25. package/lib/commonjs/ui/components/ToggleRow.js +116 -0
  26. package/lib/commonjs/ui/components/ToolFooter.js +204 -0
  27. package/lib/commonjs/ui/components/index.js +100 -1
  28. package/lib/commonjs/ui/index.js +24 -2
  29. package/lib/commonjs/ui/listPresets.js +43 -0
  30. package/lib/commonjs/ui/night/primitives.js +3 -1
  31. package/lib/commonjs/utils/snapshotProviderKit.js +167 -0
  32. package/lib/commonjs/utils/stateTools.js +160 -0
  33. package/lib/module/JsModal.js +57 -22
  34. package/lib/module/hooks/index.js +3 -1
  35. package/lib/module/hooks/safe-area-impl.js +1 -1
  36. package/lib/module/hooks/useChangeLog.js +50 -0
  37. package/lib/module/hooks/useEventStoreEvents.js +61 -0
  38. package/lib/module/hooks/usePersistedState.js +44 -10
  39. package/lib/module/index.js +11 -1
  40. package/lib/module/storage/definePersistedSetting.js +63 -0
  41. package/lib/module/storage/index.js +2 -1
  42. package/lib/module/stores/ChangeLog.js +192 -0
  43. package/lib/module/stores/createToolStore.js +171 -0
  44. package/lib/module/stores/index.js +3 -1
  45. package/lib/module/sync/toolSyncAdapter.js +58 -0
  46. package/lib/module/sync/wireForm.js +159 -0
  47. package/lib/module/tools/defineTool.js +74 -0
  48. package/lib/module/ui/components/ChangeRow.js +105 -0
  49. package/lib/module/ui/components/ChipStrip.js +91 -0
  50. package/lib/module/ui/components/DetailViewToggle.js +125 -0
  51. package/lib/module/ui/components/DevToolsCard.js +3 -1
  52. package/lib/module/ui/components/EntityFilterView.js +350 -0
  53. package/lib/module/ui/components/ExportPreview.js +169 -0
  54. package/lib/module/ui/components/InfoRow.js +86 -0
  55. package/lib/module/ui/components/ModalHeader.js +40 -7
  56. package/lib/module/ui/components/Pill.js +110 -0
  57. package/lib/module/ui/components/ToggleRow.js +112 -0
  58. package/lib/module/ui/components/ToolFooter.js +196 -0
  59. package/lib/module/ui/components/index.js +10 -1
  60. package/lib/module/ui/index.js +4 -1
  61. package/lib/module/ui/listPresets.js +39 -0
  62. package/lib/module/ui/night/primitives.js +3 -1
  63. package/lib/module/utils/snapshotProviderKit.js +156 -0
  64. package/lib/module/utils/stateTools.js +154 -0
  65. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/hooks/index.d.ts +3 -1
  67. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  69. package/lib/typescript/commonjs/hooks/useChangeLog.d.ts +38 -0
  70. package/lib/typescript/commonjs/hooks/useChangeLog.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts +25 -0
  72. package/lib/typescript/commonjs/hooks/useEventStoreEvents.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/hooks/usePersistedState.d.ts +34 -5
  74. package/lib/typescript/commonjs/hooks/usePersistedState.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/index.d.ts +6 -1
  76. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts +39 -0
  78. package/lib/typescript/commonjs/storage/definePersistedSetting.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/storage/index.d.ts +1 -0
  80. package/lib/typescript/commonjs/storage/index.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/stores/ChangeLog.d.ts +100 -0
  82. package/lib/typescript/commonjs/stores/ChangeLog.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/stores/createToolStore.d.ts +92 -0
  84. package/lib/typescript/commonjs/stores/createToolStore.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/stores/index.d.ts +2 -0
  86. package/lib/typescript/commonjs/stores/index.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts +66 -0
  88. package/lib/typescript/commonjs/sync/toolSyncAdapter.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/sync/wireForm.d.ts +100 -0
  90. package/lib/typescript/commonjs/sync/wireForm.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/tools/defineTool.d.ts +100 -0
  92. package/lib/typescript/commonjs/tools/defineTool.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts +52 -0
  94. package/lib/typescript/commonjs/ui/components/ChangeRow.d.ts.map +1 -0
  95. package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts +41 -0
  96. package/lib/typescript/commonjs/ui/components/ChipStrip.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts +36 -0
  98. package/lib/typescript/commonjs/ui/components/DetailViewToggle.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts +8 -1
  100. package/lib/typescript/commonjs/ui/components/DevToolsCard.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts +51 -0
  102. package/lib/typescript/commonjs/ui/components/EntityFilterView.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts +48 -0
  104. package/lib/typescript/commonjs/ui/components/ExportPreview.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts +33 -0
  106. package/lib/typescript/commonjs/ui/components/InfoRow.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts +31 -1
  108. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/ui/components/Pill.d.ts +38 -0
  110. package/lib/typescript/commonjs/ui/components/Pill.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts +33 -0
  112. package/lib/typescript/commonjs/ui/components/ToggleRow.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts +69 -0
  114. package/lib/typescript/commonjs/ui/components/ToolFooter.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/ui/components/index.d.ts +9 -0
  116. package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  118. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/listPresets.d.ts +28 -0
  120. package/lib/typescript/commonjs/ui/listPresets.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/ui/night/primitives.d.ts +3 -1
  122. package/lib/typescript/commonjs/ui/night/primitives.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts +81 -0
  124. package/lib/typescript/commonjs/utils/snapshotProviderKit.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/utils/stateTools.d.ts +44 -0
  126. package/lib/typescript/commonjs/utils/stateTools.d.ts.map +1 -0
  127. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  128. package/lib/typescript/module/hooks/index.d.ts +3 -1
  129. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  130. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  131. package/lib/typescript/module/hooks/useChangeLog.d.ts +38 -0
  132. package/lib/typescript/module/hooks/useChangeLog.d.ts.map +1 -0
  133. package/lib/typescript/module/hooks/useEventStoreEvents.d.ts +25 -0
  134. package/lib/typescript/module/hooks/useEventStoreEvents.d.ts.map +1 -0
  135. package/lib/typescript/module/hooks/usePersistedState.d.ts +34 -5
  136. package/lib/typescript/module/hooks/usePersistedState.d.ts.map +1 -1
  137. package/lib/typescript/module/index.d.ts +6 -1
  138. package/lib/typescript/module/index.d.ts.map +1 -1
  139. package/lib/typescript/module/storage/definePersistedSetting.d.ts +39 -0
  140. package/lib/typescript/module/storage/definePersistedSetting.d.ts.map +1 -0
  141. package/lib/typescript/module/storage/index.d.ts +1 -0
  142. package/lib/typescript/module/storage/index.d.ts.map +1 -1
  143. package/lib/typescript/module/stores/ChangeLog.d.ts +100 -0
  144. package/lib/typescript/module/stores/ChangeLog.d.ts.map +1 -0
  145. package/lib/typescript/module/stores/createToolStore.d.ts +92 -0
  146. package/lib/typescript/module/stores/createToolStore.d.ts.map +1 -0
  147. package/lib/typescript/module/stores/index.d.ts +2 -0
  148. package/lib/typescript/module/stores/index.d.ts.map +1 -1
  149. package/lib/typescript/module/sync/toolSyncAdapter.d.ts +66 -0
  150. package/lib/typescript/module/sync/toolSyncAdapter.d.ts.map +1 -0
  151. package/lib/typescript/module/sync/wireForm.d.ts +100 -0
  152. package/lib/typescript/module/sync/wireForm.d.ts.map +1 -0
  153. package/lib/typescript/module/tools/defineTool.d.ts +100 -0
  154. package/lib/typescript/module/tools/defineTool.d.ts.map +1 -0
  155. package/lib/typescript/module/ui/components/ChangeRow.d.ts +52 -0
  156. package/lib/typescript/module/ui/components/ChangeRow.d.ts.map +1 -0
  157. package/lib/typescript/module/ui/components/ChipStrip.d.ts +41 -0
  158. package/lib/typescript/module/ui/components/ChipStrip.d.ts.map +1 -0
  159. package/lib/typescript/module/ui/components/DetailViewToggle.d.ts +36 -0
  160. package/lib/typescript/module/ui/components/DetailViewToggle.d.ts.map +1 -0
  161. package/lib/typescript/module/ui/components/DevToolsCard.d.ts +8 -1
  162. package/lib/typescript/module/ui/components/DevToolsCard.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/components/EntityFilterView.d.ts +51 -0
  164. package/lib/typescript/module/ui/components/EntityFilterView.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/components/ExportPreview.d.ts +48 -0
  166. package/lib/typescript/module/ui/components/ExportPreview.d.ts.map +1 -0
  167. package/lib/typescript/module/ui/components/InfoRow.d.ts +33 -0
  168. package/lib/typescript/module/ui/components/InfoRow.d.ts.map +1 -0
  169. package/lib/typescript/module/ui/components/ModalHeader.d.ts +31 -1
  170. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/components/Pill.d.ts +38 -0
  172. package/lib/typescript/module/ui/components/Pill.d.ts.map +1 -0
  173. package/lib/typescript/module/ui/components/ToggleRow.d.ts +33 -0
  174. package/lib/typescript/module/ui/components/ToggleRow.d.ts.map +1 -0
  175. package/lib/typescript/module/ui/components/ToolFooter.d.ts +69 -0
  176. package/lib/typescript/module/ui/components/ToolFooter.d.ts.map +1 -0
  177. package/lib/typescript/module/ui/components/index.d.ts +9 -0
  178. package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
  179. package/lib/typescript/module/ui/index.d.ts +1 -0
  180. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  181. package/lib/typescript/module/ui/listPresets.d.ts +28 -0
  182. package/lib/typescript/module/ui/listPresets.d.ts.map +1 -0
  183. package/lib/typescript/module/ui/night/primitives.d.ts +3 -1
  184. package/lib/typescript/module/ui/night/primitives.d.ts.map +1 -1
  185. package/lib/typescript/module/utils/snapshotProviderKit.d.ts +81 -0
  186. package/lib/typescript/module/utils/snapshotProviderKit.d.ts.map +1 -0
  187. package/lib/typescript/module/utils/stateTools.d.ts +44 -0
  188. package/lib/typescript/module/utils/stateTools.d.ts.map +1 -0
  189. package/package.json +4 -4
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PREVIEW_ITEM_RENDER_BYTE_LIMIT = void 0;
7
+ exports.classifyPreviewItem = classifyPreviewItem;
8
+ exports.createRestoreScope = createRestoreScope;
9
+ exports.emptyRestoreResult = emptyRestoreResult;
10
+ exports.failedRestore = failedRestore;
11
+ exports.skippedRestore = skippedRestore;
12
+ exports.unionKeys = unionKeys;
13
+ exports.wontApply = wontApply;
14
+ var _objectDiff = require("../dataViewer/objectDiff.js");
15
+ /**
16
+ * snapshotProviderKit — the parts of a Time Machine `StateSnapshotProvider`
17
+ * that are the same for every source.
18
+ *
19
+ * storage, redux, zustand, jotai and react-query each register a provider
20
+ * (see stateSnapshotRegistry.ts). What differs is how they read and write
21
+ * their source. What was copied five times is:
22
+ *
23
+ * - the restore SCOPE gate: `excludeKeys` (touch all but these) and
24
+ * `includeKeys` (touch only these), the "N item(s) excluded by you"
25
+ * warning, and the empty `StateSnapshotRestoreResult`;
26
+ * - the preview VERDICT ladder: removed → added → unchanged → too-large →
27
+ * changed, with the 256 KB per-item render cap;
28
+ * - the "won't apply" override: a difference restore cannot apply must say
29
+ * so instead of rendering as a green change.
30
+ *
31
+ * The parts that stay per-tool are the ones that matter: redux verifies the
32
+ * jump by reference identity, zustand grafts action functions back, jotai
33
+ * leaves derived atoms to recompute, storage gates MMKV buffers and
34
+ * biometric keys, react-query diffs a comparable projection.
35
+ */
36
+
37
+ /** Beyond this a preview item is "too-large": restore still applies it, the diff isn't drawn. */
38
+ const PREVIEW_ITEM_RENDER_BYTE_LIMIT = exports.PREVIEW_ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
39
+ /**
40
+ * Both option lists use preview item keys, and both mean "leave everything
41
+ * else untouched in BOTH directions". `includeKeys` wins when present;
42
+ * `excludeKeys` still applies on top.
43
+ */
44
+ function createRestoreScope(options) {
45
+ const excludedSet = new Set(options?.excludeKeys ?? []);
46
+ const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
47
+ let excludedHits = 0;
48
+ const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
49
+ return {
50
+ touch,
51
+ allows(key) {
52
+ if (touch(key)) return true;
53
+ excludedHits += 1;
54
+ return false;
55
+ },
56
+ countExcluded(n) {
57
+ excludedHits += n;
58
+ },
59
+ get excludedHits() {
60
+ return excludedHits;
61
+ },
62
+ excludedWarning(noun) {
63
+ return excludedHits > 0 ? `${excludedHits} ${noun} excluded by you` : null;
64
+ }
65
+ };
66
+ }
67
+
68
+ /** A fresh, successful, empty result to accumulate into. */
69
+ function emptyRestoreResult() {
70
+ return {
71
+ ok: true,
72
+ applied: 0,
73
+ skipped: [],
74
+ warnings: []
75
+ };
76
+ }
77
+
78
+ /** A failed result with one warning (an empty or unusable snapshot). */
79
+ function failedRestore(warning) {
80
+ return {
81
+ ok: false,
82
+ applied: 0,
83
+ skipped: [],
84
+ warnings: [warning]
85
+ };
86
+ }
87
+
88
+ /** A failed result with one skipped item and its reason. */
89
+ function skippedRestore(id, reason) {
90
+ return {
91
+ ok: false,
92
+ applied: 0,
93
+ skipped: [{
94
+ id,
95
+ reason
96
+ }],
97
+ warnings: []
98
+ };
99
+ }
100
+ /**
101
+ * The verdict ladder: removed → added → unchanged → too-large → changed.
102
+ * Callers layer source-specific verdicts (`wont-apply`, `recomputes`) on top
103
+ * with `wontApply()`.
104
+ */
105
+ function classifyPreviewItem(input, options = {}) {
106
+ const {
107
+ key,
108
+ label,
109
+ oldValue,
110
+ newValue,
111
+ inOld,
112
+ inNew
113
+ } = input;
114
+ if (!inNew) return {
115
+ key,
116
+ label,
117
+ verdict: "removed",
118
+ oldValue
119
+ };
120
+ if (!inOld) return {
121
+ key,
122
+ label,
123
+ verdict: "added",
124
+ newValue
125
+ };
126
+ const equal = options.equal ?? _objectDiff.boundedDeepEqual;
127
+ if (equal(oldValue, newValue)) return {
128
+ key,
129
+ label,
130
+ verdict: "unchanged"
131
+ };
132
+ const tooLarge = options.tooLarge ?? (value => (0, _objectDiff.jsonLikelyLargerThan)(value, PREVIEW_ITEM_RENDER_BYTE_LIMIT));
133
+ if (tooLarge(oldValue) || tooLarge(newValue)) {
134
+ return {
135
+ key,
136
+ label,
137
+ verdict: "too-large",
138
+ reason: options.tooLargeReason ?? "value too large to render a diff — restore still applies it"
139
+ };
140
+ }
141
+ return {
142
+ key,
143
+ label,
144
+ verdict: "changed",
145
+ oldValue,
146
+ newValue
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Mark an item as something restore cannot apply. An unchanged item is left
152
+ * alone — there is nothing to apply, so nothing is being refused.
153
+ */
154
+ function wontApply(item, reason) {
155
+ if (item.verdict === "unchanged") return item;
156
+ return {
157
+ ...item,
158
+ verdict: "wont-apply",
159
+ reason
160
+ };
161
+ }
162
+
163
+ /** Every key present in either record, once. */
164
+ function unionKeys(a, b) {
165
+ const keysOf = r => r === undefined ? [] : r instanceof Map ? Array.from(r.keys()) : Object.keys(r);
166
+ return new Set([...keysOf(a), ...keysOf(b)]);
167
+ }
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.entityColor = entityColor;
7
+ exports.formatValuePreview = formatValuePreview;
8
+ exports.shallowDiffSummary = shallowDiffSummary;
9
+ /**
10
+ * stateTools — helpers the state tools (redux / zustand / jotai) each carried
11
+ * a private copy of. Pure functions; no React, no RN.
12
+ */
13
+
14
+ // ============================================
15
+ // Entity color
16
+ // ============================================
17
+
18
+ /**
19
+ * Consistent color for a named entity (a zustand store, a jotai atom, a redux
20
+ * slice). Exact match on `known` wins, then a substring match, then a stable
21
+ * HSL hash of the name rendered as `#rrggbb` so the "color + alpha hex"
22
+ * pattern works everywhere.
23
+ */
24
+ function entityColor(name, known = {}) {
25
+ const lower = name.toLowerCase();
26
+ if (known[lower]) return known[lower];
27
+ for (const [key, color] of Object.entries(known)) {
28
+ if (lower.includes(key)) return color;
29
+ }
30
+ const hash = name.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
31
+ const hue = hash * 137 % 360;
32
+ const s = 0.7;
33
+ const l = 0.6;
34
+ const c = (1 - Math.abs(2 * l - 1)) * s;
35
+ const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
36
+ const m = l - c / 2;
37
+ let r = 0;
38
+ let g = 0;
39
+ let b = 0;
40
+ if (hue < 60) {
41
+ r = c;
42
+ g = x;
43
+ } else if (hue < 120) {
44
+ r = x;
45
+ g = c;
46
+ } else if (hue < 180) {
47
+ g = c;
48
+ b = x;
49
+ } else if (hue < 240) {
50
+ g = x;
51
+ b = c;
52
+ } else if (hue < 300) {
53
+ r = x;
54
+ b = c;
55
+ } else {
56
+ r = c;
57
+ b = x;
58
+ }
59
+ const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
60
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
61
+ }
62
+
63
+ // ============================================
64
+ // Value preview
65
+ // ============================================
66
+
67
+ /**
68
+ * One-line preview of an arbitrary value for a list row: strings quoted and
69
+ * truncated, arrays and objects stringified when short, otherwise collapsed
70
+ * to `[N items]` / `{ N keys }`. Never throws.
71
+ */
72
+ function formatValuePreview(value, options = {}) {
73
+ const maxLength = options.maxLength ?? 40;
74
+ if (value === undefined) return options.undefinedLabel ?? "undefined";
75
+ if (value === null) return "null";
76
+ try {
77
+ if (typeof value === "function") return options.functionLabel ?? "(fn)";
78
+ if (typeof value === "string") {
79
+ return value.length > maxLength ? `"${value.slice(0, maxLength - 3)}..."` : `"${value}"`;
80
+ }
81
+ if (typeof value === "number" || typeof value === "boolean") {
82
+ return String(value);
83
+ }
84
+ if (Array.isArray(value)) {
85
+ if (value.length === 0) return "[]";
86
+ const preview = JSON.stringify(value);
87
+ return preview.length > maxLength ? `[${value.length} items]` : preview;
88
+ }
89
+ if (typeof value === "object") {
90
+ const keys = Object.keys(value);
91
+ if (keys.length === 0) return "{}";
92
+ const preview = JSON.stringify(value);
93
+ if (preview.length <= maxLength) return preview;
94
+ return `{ ${keys.length} keys }`;
95
+ }
96
+ return String(value).slice(0, maxLength);
97
+ } catch {
98
+ return "[complex]";
99
+ }
100
+ }
101
+
102
+ // ============================================
103
+ // Shallow diff summary
104
+ // ============================================
105
+
106
+ /**
107
+ * Top-level, reference-equality diff between two states. Enough for a list
108
+ * row ("+1 ~2 keys"); the detail view runs the real tree diff.
109
+ *
110
+ * A state object that was REPLACED without any top-level value moving —
111
+ * exactly what zustand's persist middleware does when it rehydrates — is
112
+ * reported as `"nested change"` with no keys, never as one changed key with
113
+ * none named.
114
+ */
115
+ function shallowDiffSummary(prev, next) {
116
+ if (prev === next) {
117
+ return {
118
+ summary: "no change",
119
+ changedKeys: [],
120
+ changedCount: 0
121
+ };
122
+ }
123
+ if (typeof prev !== "object" || typeof next !== "object" || prev === null || next === null) {
124
+ return {
125
+ summary: "changed",
126
+ changedKeys: [],
127
+ changedCount: 0
128
+ };
129
+ }
130
+ const prevRecord = prev;
131
+ const nextRecord = next;
132
+ const prevKeys = Object.keys(prevRecord);
133
+ const nextKeys = Object.keys(nextRecord);
134
+ const added = nextKeys.filter(k => !prevKeys.includes(k));
135
+ const removed = prevKeys.filter(k => !nextKeys.includes(k));
136
+ const changed = [];
137
+ for (const key of prevKeys) {
138
+ if (nextKeys.includes(key) && prevRecord[key] !== nextRecord[key]) {
139
+ changed.push(key);
140
+ }
141
+ }
142
+ const changedKeys = [...added, ...removed, ...changed];
143
+ const parts = [];
144
+ if (added.length > 0) parts.push(`+${added.length}`);
145
+ if (removed.length > 0) parts.push(`-${removed.length}`);
146
+ if (changed.length > 0) parts.push(`~${changed.length}`);
147
+ if (parts.length === 0) {
148
+ return {
149
+ summary: "nested change",
150
+ changedKeys: [],
151
+ changedCount: 0
152
+ };
153
+ }
154
+ const total = changedKeys.length;
155
+ return {
156
+ summary: `${parts.join(" ")} ${total === 1 ? "key" : "keys"}`,
157
+ changedKeys,
158
+ changedCount: total
159
+ };
160
+ }
@@ -602,7 +602,6 @@ const JsModalComponent = ({
602
602
  // ============================================================================
603
603
  // REFS for values we need to track
604
604
  // ============================================================================
605
- const isExternallyControlled = !!externalAnimatedHeight;
606
605
  const effectiveMaxHeight = maxHeight || SCREEN.height - insets.top;
607
606
 
608
607
  // Mode toggle handler
@@ -724,29 +723,57 @@ const JsModalComponent = ({
724
723
  // ============================================================================
725
724
  // EFFECT: Visibility Animations - All using native driver!
726
725
  // ============================================================================
726
+ const snapVisibility = shown => {
727
+ if (shown) {
728
+ if (mode === "bottomSheet") {
729
+ bottomSheetTranslateY.setValue(0);
730
+ } else {
731
+ floatingScale.setValue(1);
732
+ floatingPosition.setValue({
733
+ x: currentDimensionsRef.current.left,
734
+ y: currentDimensionsRef.current.top
735
+ });
736
+ }
737
+ visibilityProgress.setValue(1);
738
+ } else {
739
+ if (mode === "bottomSheet") {
740
+ bottomSheetTranslateY.setValue(SCREEN.height);
741
+ }
742
+ visibilityProgress.setValue(0);
743
+ }
744
+ };
745
+
746
+ /**
747
+ * Snap DURING RENDER when `visible` flips, not only in the effect below.
748
+ *
749
+ * These values drive the sheet's opacity/translate through the native
750
+ * driver. The React commit for a `visible` change carries whatever the
751
+ * JS-side values are at render time — the OLD, hidden ones when the snap
752
+ * only happens in the effect — and on Fabric that commit's mount can land
753
+ * on the main thread AFTER the effect's native `setValue`, overwriting it.
754
+ * The Animated node then says "shown" while the UIView sits at opacity 0,
755
+ * until some unrelated re-render commits the JS-side values again. That
756
+ * is how "restore tool A: nothing; restore tool B: A appears; hide B: now
757
+ * B appears" happened — every restore was one commit behind.
758
+ *
759
+ * Setting the values before the render's props are computed makes the
760
+ * commit and the native node agree, whichever lands last. `setValue` is
761
+ * idempotent, so a re-run render is harmless.
762
+ */
763
+ const snappedVisibleRef = useRef(null);
764
+ if (INSTANT_MODAL_ENTRANCE && snappedVisibleRef.current !== visible) {
765
+ snappedVisibleRef.current = visible;
766
+ snapVisibility(visible);
767
+ }
727
768
  useEffect(() => {
728
769
  // Visibility effect
729
770
  let openAnimation = null;
730
771
  let closeAnimation = null;
731
772
  if (INSTANT_MODAL_ENTRANCE) {
732
- // Snap to the final state — no animation in either direction.
733
- if (visible) {
734
- if (mode === "bottomSheet") {
735
- bottomSheetTranslateY.setValue(0);
736
- } else {
737
- floatingScale.setValue(1);
738
- floatingPosition.setValue({
739
- x: currentDimensionsRef.current.left,
740
- y: currentDimensionsRef.current.top
741
- });
742
- }
743
- visibilityProgress.setValue(1);
744
- } else {
745
- if (mode === "bottomSheet") {
746
- bottomSheetTranslateY.setValue(SCREEN.height);
747
- }
748
- visibilityProgress.setValue(0);
749
- }
773
+ // Snap to the final state — no animation in either direction. The
774
+ // render-phase snap above already did this for a `visible` flip; this
775
+ // re-applies it for a `mode` change while shown.
776
+ snapVisibility(visible);
750
777
  return;
751
778
  }
752
779
  if (visible) {
@@ -911,8 +938,15 @@ const JsModalComponent = ({
911
938
  // eslint-disable-next-line react-hooks/exhaustive-deps
912
939
  }, [benchMotion, visible]);
913
940
  const bottomSheetPanResponder = useMemo(() => PanResponder.create({
914
- onStartShouldSetPanResponder: () => !isExternallyControlled && mode === "bottomSheet",
915
- onMoveShouldSetPanResponder: (evt, gestureState) => !isExternallyControlled && mode === "bottomSheet" && Math.abs(gestureState.dy) > 3,
941
+ // An external `animatedHeight` used to disable this responder
942
+ // outright ("externally controlled"), which left Ask Buoy's sheet —
943
+ // the one host that passes one, for its peek animation — with a
944
+ // header that could not be dragged in bottom-sheet mode. The move
945
+ // and release handlers below already write every height into the
946
+ // external value, so the host and the finger share one number; the
947
+ // grab just has to stop whatever the host is animating first.
948
+ onStartShouldSetPanResponder: () => mode === "bottomSheet",
949
+ onMoveShouldSetPanResponder: (evt, gestureState) => mode === "bottomSheet" && Math.abs(gestureState.dy) > 3,
916
950
  onPanResponderTerminationRequest: () => false,
917
951
  onPanResponderGrant: evt => {
918
952
  setIsResizing(true);
@@ -924,6 +958,7 @@ const JsModalComponent = ({
924
958
  animatedBottomPosition.stopAnimation(val => {
925
959
  currentHeightRef.current = val;
926
960
  });
961
+ externalAnimatedHeight?.stopAnimation();
927
962
  bottomSheetTranslateY.stopAnimation();
928
963
  },
929
964
  onPanResponderMove: evt => {
@@ -970,7 +1005,7 @@ const JsModalComponent = ({
970
1005
  setIsResizing(false);
971
1006
  // snap back to the last stable height if you want; otherwise no-op
972
1007
  }
973
- }), [mode, isExternallyControlled, minHeight, effectiveMaxHeight, animatedBottomPosition, externalAnimatedHeight, bottomSheetTranslateY, visibilityProgress, onClose]);
1008
+ }), [mode, minHeight, effectiveMaxHeight, animatedBottomPosition, externalAnimatedHeight, bottomSheetTranslateY, visibilityProgress, onClose]);
974
1009
 
975
1010
  // ============================================================================
976
1011
  // CREATE RESIZE HANDLER: For 4-corner resize in floating mode (fixed geometry)
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  export { useFilterManager } from "./useFilterManager.js";
4
- export { usePersistedState } from "./usePersistedState.js";
4
+ export { usePersistedState, resetPersistedStateCache } from "./usePersistedState.js";
5
+ export { useChangeLog, useSubscribedValue } from "./useChangeLog.js";
6
+ export { useEventStoreEvents } from "./useEventStoreEvents.js";
5
7
  export { useSafeAreaInsets } from "./useSafeAreaInsets.js";
6
8
  export { getSafeAreaInsets } from "./useSafeAreaInsets.js";
7
9
  export { useDeviceInfo, getDeviceInfo, logDeviceInfo } from "./useDeviceInfo.js";
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-09-02T23:25:39.783Z
6
+ * Generated at: 2026-09-04T01:21:28.486Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *
@@ -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
  }