@buoy-gg/zustand 7.0.33 → 7.0.35

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 (45) hide show
  1. package/lib/commonjs/zustand/components/ZustandActionButton.js +1 -1
  2. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +13 -107
  3. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +24 -288
  4. package/lib/commonjs/zustand/components/ZustandModal.js +20 -85
  5. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +8 -61
  6. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +5 -5
  7. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +8 -52
  8. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +2 -2
  9. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +4 -4
  10. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +3 -3
  11. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +33 -53
  12. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +24 -78
  13. package/lib/commonjs/zustand/utils/snapshotProvider.js +17 -60
  14. package/lib/commonjs/zustand/utils/zustandStateStore.js +37 -223
  15. package/lib/module/zustand/components/ZustandActionButton.js +2 -2
  16. package/lib/module/zustand/components/ZustandDetailViewToggle.js +15 -109
  17. package/lib/module/zustand/components/ZustandEventFilterView.js +28 -292
  18. package/lib/module/zustand/components/ZustandModal.js +22 -87
  19. package/lib/module/zustand/components/ZustandStateChangeItem.js +10 -63
  20. package/lib/module/zustand/components/ZustandStateDetailContent.js +6 -6
  21. package/lib/module/zustand/components/ZustandStateInfoView.js +7 -51
  22. package/lib/module/zustand/components/ZustandStoreBrowser.js +3 -3
  23. package/lib/module/zustand/components/ZustandStoreDetailContent.js +5 -5
  24. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +4 -4
  25. package/lib/module/zustand/hooks/useZustandStateChanges.js +34 -54
  26. package/lib/module/zustand/sync/zustandSyncAdapter.js +25 -79
  27. package/lib/module/zustand/utils/snapshotProvider.js +18 -61
  28. package/lib/module/zustand/utils/zustandStateStore.js +37 -223
  29. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts +2 -1
  30. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -1
  31. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts +6 -0
  32. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -1
  33. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -1
  34. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -1
  35. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -1
  36. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -1
  37. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -1
  38. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -1
  39. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts +2 -1
  40. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -1
  41. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts +103 -117
  42. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -1
  43. package/lib/typescript/zustand/utils/zustandStateStore.d.ts +6 -26
  44. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -1
  45. package/package.json +3 -3
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  var _sharedUi = require("@buoy-gg/shared-ui");
4
- var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
5
4
  var _zustandStateStore = require("./zustandStateStore");
6
5
  var _editableState = require("./editableState");
7
6
  /**
@@ -93,29 +92,13 @@ async function capture() {
93
92
  }
94
93
  async function restore(data, options) {
95
94
  const snapshot = data;
96
- const excludedSet = new Set(options?.excludeKeys ?? []);
97
- const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
98
- const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
99
- let excludedHits = 0;
100
- const result = {
101
- ok: true,
102
- applied: 0,
103
- skipped: [],
104
- warnings: []
105
- };
95
+ const scope = (0, _sharedUi.createRestoreScope)(options);
96
+ const result = (0, _sharedUi.emptyRestoreResult)();
106
97
  if (!snapshot?.stores) {
107
- return {
108
- ok: false,
109
- applied: 0,
110
- skipped: [],
111
- warnings: ["empty zustand snapshot"]
112
- };
98
+ return (0, _sharedUi.failedRestore)("empty zustand snapshot");
113
99
  }
114
100
  for (const [name, envelope] of Object.entries(snapshot.stores)) {
115
- if (!touch(name)) {
116
- excludedHits += 1;
117
- continue;
118
- }
101
+ if (!scope.allows(name)) continue;
119
102
  const info = _zustandStateStore.zustandStateStore.getStore(name);
120
103
  if (!info) {
121
104
  result.skipped.push({
@@ -142,12 +125,10 @@ async function restore(data, options) {
142
125
  result.ok = false;
143
126
  }
144
127
  }
145
- if (excludedHits > 0) {
146
- result.warnings.push(`${excludedHits} store(s) excluded by you`);
147
- }
128
+ const excludedWarning = scope.excludedWarning("store(s)");
129
+ if (excludedWarning) result.warnings.push(excludedWarning);
148
130
  return result;
149
131
  }
150
- const ITEM_RENDER_BYTE_LIMIT = 256 * 1024;
151
132
 
152
133
  /**
153
134
  * Per-store restore preview. Both sides are function-stripped through the
@@ -176,8 +157,7 @@ async function previewItems(target, base) {
176
157
  const items = [];
177
158
  const notes = [];
178
159
  let preservedFns = 0;
179
- const names = new Set([...Object.keys(baseStores), ...Object.keys(targetStores)]);
180
- for (const name of names) {
160
+ for (const name of (0, _sharedUi.unionKeys)(baseStores, targetStores)) {
181
161
  const inTarget = name in targetStores;
182
162
  const inBase = name in baseStores;
183
163
  if (!inTarget) {
@@ -200,39 +180,16 @@ async function previewItems(target, base) {
200
180
  });
201
181
  continue;
202
182
  }
203
- if (!inBase) {
204
- items.push({
205
- key: name,
206
- label: name,
207
- verdict: "added",
208
- newValue: targetStores[name].state
209
- });
210
- continue;
211
- }
212
- const oldState = baseStores[name].state;
213
- const newState = targetStores[name].state;
214
- if ((0, _dataViewer.boundedDeepEqual)(oldState, newState)) {
215
- items.push({
216
- key: name,
217
- label: name,
218
- verdict: "unchanged"
219
- });
220
- } else if ((0, _dataViewer.jsonLikelyLargerThan)(oldState, ITEM_RENDER_BYTE_LIMIT) || (0, _dataViewer.jsonLikelyLargerThan)(newState, ITEM_RENDER_BYTE_LIMIT)) {
221
- items.push({
222
- key: name,
223
- label: name,
224
- verdict: "too-large",
225
- reason: "store too large to render a diff — restore still applies it"
226
- });
227
- } else {
228
- items.push({
229
- key: name,
230
- label: name,
231
- verdict: "changed",
232
- oldValue: oldState,
233
- newValue: newState
234
- });
235
- }
183
+ items.push((0, _sharedUi.classifyPreviewItem)({
184
+ key: name,
185
+ label: name,
186
+ oldValue: baseStores[name]?.state,
187
+ newValue: targetStores[name].state,
188
+ inOld: inBase,
189
+ inNew: true
190
+ }, {
191
+ tooLargeReason: "store too large to render a diff — restore still applies it"
192
+ }));
236
193
  }
237
194
  if (preservedFns > 0) {
238
195
  notes.push(`${preservedFns} store action function${preservedFns === 1 ? "" : "s"} preserved on restore (not diffed)`);
@@ -4,10 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.zustandStateStore = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
7
8
  /**
8
9
  * Zustand state store - captures and stores Zustand state changes
9
10
  *
10
- * Mirrors the architecture of reduxActionStore.ts from @buoy-gg/redux
11
+ * The generic event-log half (ring buffer, listeners, clear-forwarding for
12
+ * the desktop mirror, pause/suppress flags, hydrate-by-id) is the shared
13
+ * `ChangeLog`; this file owns the zustand-specific half — the store registry,
14
+ * snapshots, mirror rebuild, filters and stats.
11
15
  */
12
16
 
13
17
  // ============================================
@@ -40,168 +44,33 @@ const STORE_COLORS = {
40
44
  * Get consistent color for a store based on its name
41
45
  */
42
46
  function getStoreColor(storeName) {
43
- const lowerName = storeName.toLowerCase();
44
-
45
- // Check for exact match
46
- if (STORE_COLORS[lowerName]) {
47
- return STORE_COLORS[lowerName];
48
- }
49
-
50
- // Check for partial matches
51
- for (const [key, color] of Object.entries(STORE_COLORS)) {
52
- if (lowerName.includes(key)) {
53
- return color;
54
- }
55
- }
56
-
57
- // Generate consistent hex color from name hash
58
- const hash = storeName.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
59
- const hue = hash * 137 % 360;
60
- // Convert HSL to hex so "color + alpha_hex" pattern works everywhere
61
- const s = 0.7;
62
- const l = 0.6;
63
- const c = (1 - Math.abs(2 * l - 1)) * s;
64
- const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
65
- const m = l - c / 2;
66
- let r = 0,
67
- g = 0,
68
- b = 0;
69
- if (hue < 60) {
70
- r = c;
71
- g = x;
72
- } else if (hue < 120) {
73
- r = x;
74
- g = c;
75
- } else if (hue < 180) {
76
- g = c;
77
- b = x;
78
- } else if (hue < 240) {
79
- g = x;
80
- b = c;
81
- } else if (hue < 300) {
82
- r = x;
83
- b = c;
84
- } else {
85
- r = c;
86
- b = x;
87
- }
88
- const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, "0");
89
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
47
+ return (0, _sharedUi.entityColor)(storeName, STORE_COLORS);
90
48
  }
91
49
 
92
- // ============================================
93
- // Helper Functions
94
- // ============================================
95
-
96
50
  /**
97
51
  * Format payload preview for display
98
52
  */
99
53
  function formatPartialPreview(partial, maxLength = 40) {
100
- if (partial === undefined) return "";
101
- if (partial === null) return "null";
102
- try {
103
- if (typeof partial === "function") {
104
- return "(updater fn)";
105
- }
106
- if (typeof partial === "string") {
107
- return partial.length > maxLength ? `"${partial.slice(0, maxLength - 3)}..."` : `"${partial}"`;
108
- }
109
- if (typeof partial === "number" || typeof partial === "boolean") {
110
- return String(partial);
111
- }
112
- if (Array.isArray(partial)) {
113
- if (partial.length === 0) return "[]";
114
- const preview = JSON.stringify(partial);
115
- return preview.length > maxLength ? `[${partial.length} items]` : preview;
116
- }
117
- if (typeof partial === "object") {
118
- const keys = Object.keys(partial);
119
- if (keys.length === 0) return "{}";
120
- const preview = JSON.stringify(partial);
121
- if (preview.length <= maxLength) return preview;
122
- return `{ ${keys.length} keys }`;
123
- }
124
- return String(partial).slice(0, maxLength);
125
- } catch {
126
- return "[complex]";
127
- }
128
- }
129
-
130
- /**
131
- * Calculate state diff summary.
132
- *
133
- * `changedCount` counts the keys in `changedKeys` and nothing else. The two
134
- * used to disagree: a state object that was REPLACED without any top-level
135
- * value moving — which is exactly what zustand's persist middleware does when
136
- * it rehydrates — reported one changed key and named none, so the row said
137
- * "1 key changed" and the detail page it opened drew no CHANGED KEYS card at
138
- * all. `summary` is where a change with no nameable keys is described.
139
- */
140
- function getStateDiffSummary(prevState, nextState) {
141
- if (prevState === nextState) {
142
- return {
143
- summary: "no change",
144
- changedKeys: [],
145
- changedCount: 0
146
- };
147
- }
148
- if (typeof prevState !== "object" || typeof nextState !== "object" || prevState === null || nextState === null) {
149
- return {
150
- summary: "changed",
151
- changedKeys: [],
152
- changedCount: 0
153
- };
154
- }
155
- const prevKeys = Object.keys(prevState);
156
- const nextKeys = Object.keys(nextState);
157
- const added = nextKeys.filter(k => !prevKeys.includes(k));
158
- const removed = prevKeys.filter(k => !nextKeys.includes(k));
159
- const changed = [];
160
- for (const key of prevKeys) {
161
- if (nextKeys.includes(key) && prevState[key] !== nextState[key]) {
162
- changed.push(key);
163
- }
164
- }
165
- const allChangedKeys = [...added, ...removed, ...changed];
166
- const parts = [];
167
- if (added.length > 0) parts.push(`+${added.length}`);
168
- if (removed.length > 0) parts.push(`-${removed.length}`);
169
- if (changed.length > 0) parts.push(`~${changed.length}`);
170
- const totalChanged = allChangedKeys.length;
171
- if (parts.length === 0) {
172
- return {
173
- summary: "nested change",
174
- changedKeys: [],
175
- changedCount: 0
176
- };
177
- }
178
- return {
179
- summary: `${parts.join(" ")} ${totalChanged === 1 ? "key" : "keys"}`,
180
- changedKeys: allChangedKeys,
181
- changedCount: totalChanged
182
- };
54
+ return (0, _sharedUi.formatValuePreview)(partial, {
55
+ maxLength,
56
+ undefinedLabel: "",
57
+ functionLabel: "(updater fn)"
58
+ });
183
59
  }
184
60
 
185
61
  // ============================================
186
62
  // Zustand State Store
187
63
  // ============================================
188
64
 
189
- class ZustandStateStore {
190
- stateChanges = [];
65
+ class ZustandStateStore extends _sharedUi.ChangeLog {
191
66
  stores = new Map();
192
- listeners = new Set();
193
67
  storeListeners = new Set();
194
- clearListeners = new Set();
195
- maxChanges = 200;
196
- idCounter = 0;
197
- isEnabled = true;
198
- captureSuppressed = false;
199
68
  remoteSetStateHandler = null;
200
69
 
201
70
  // ---- State Change Tracking ----
202
71
 
203
72
  addStateChange(params) {
204
- if (!this.isEnabled || this.captureSuppressed) return;
73
+ if (!this.canRecord()) return;
205
74
  const {
206
75
  storeName,
207
76
  partial,
@@ -213,16 +82,20 @@ class ZustandStateStore {
213
82
  isPersisted = false
214
83
  } = params;
215
84
  const hasStateChange = prevState !== nextState;
85
+ // `changedCount` counts the keys in `changedKeys` and nothing else, so the
86
+ // row's "N keys changed" and the detail page's CHANGED KEYS card agree —
87
+ // including the persist-rehydrate case where the object is replaced
88
+ // without any top-level value moving.
216
89
  const {
217
90
  summary,
218
91
  changedKeys,
219
92
  changedCount
220
- } = getStateDiffSummary(prevState, nextState);
93
+ } = (0, _sharedUi.shallowDiffSummary)(prevState, nextState);
221
94
  const partialPreview = formatPartialPreview(partial);
222
95
  const isSlowUpdate = (duration ?? 0) > 16;
223
96
  const resolvedCategory = category ?? (replace ? "replace" : "setState");
224
97
  const stateChange = {
225
- id: `${Date.now()}-${++this.idCounter}`,
98
+ id: this.nextId(),
226
99
  storeName,
227
100
  timestamp: Date.now(),
228
101
  prevState,
@@ -239,7 +112,7 @@ class ZustandStateStore {
239
112
  isSlowUpdate,
240
113
  isPersisted
241
114
  };
242
- this.stateChanges = [stateChange, ...this.stateChanges].slice(0, this.maxChanges);
115
+ this.push(stateChange);
243
116
 
244
117
  // Update store change count
245
118
  const storeInfo = this.stores.get(storeName);
@@ -249,36 +122,17 @@ class ZustandStateStore {
249
122
  this.notifyListeners();
250
123
  }
251
124
  getStateChanges() {
252
- return [...this.stateChanges];
125
+ return this.getAll();
253
126
  }
254
127
  getStateChangeById(id) {
255
- return this.stateChanges.find(c => c.id === id);
128
+ return this.getById(id);
256
129
  }
257
130
  clearStateChanges() {
258
- this.stateChanges = [];
259
131
  // Reset counts
260
132
  for (const store of this.stores.values()) {
261
133
  store.stateChangeCount = 0;
262
134
  }
263
- this.notifyListeners();
264
- this.clearListeners.forEach(listener => {
265
- try {
266
- listener();
267
- } catch {
268
- // Ignore listener errors
269
- }
270
- });
271
- }
272
-
273
- /**
274
- * Listen for clearStateChanges() calls. Used in remote mirror mode to
275
- * forward a clear performed in the dashboard UI to the synced device.
276
- */
277
- onClear(listener) {
278
- this.clearListeners.add(listener);
279
- return () => {
280
- this.clearListeners.delete(listener);
281
- };
135
+ this.clear();
282
136
  }
283
137
 
284
138
  // ---- Remote Mirror Mode ----
@@ -291,33 +145,19 @@ class ZustandStateStore {
291
145
  * flag so the UI's pause button freezes the reseed.
292
146
  */
293
147
  replaceStateChanges(changes) {
294
- if (!this.isEnabled) return;
295
- this.stateChanges = changes.slice(0, this.maxChanges);
148
+ if (!this.getEnabled()) return;
296
149
  for (const store of this.stores.values()) {
297
150
  store.stateChangeCount = 0;
298
151
  }
299
- for (const change of this.stateChanges) {
152
+ const kept = changes.slice(0, this.max);
153
+ for (const change of kept) {
300
154
  const store = this.stores.get(change.storeName);
301
155
  if (store) store.stateChangeCount++;
302
156
  }
303
- this.notifyListeners();
157
+ this.setItems(kept);
304
158
  this.notifyStoreListeners();
305
159
  }
306
160
 
307
- /**
308
- * Permanently suppress local capture. Use when this store acts as a mirror
309
- * of a remote device's state changes (e.g. the desktop dashboard): data
310
- * arrives via replaceFromSnapshot() and local middleware must never record.
311
- */
312
- disableCapture() {
313
- this.captureSuppressed = true;
314
- }
315
-
316
- /** Whether the store is in remote mirror mode (capture suppressed). */
317
- isCaptureSuppressed() {
318
- return this.captureSuppressed;
319
- }
320
-
321
161
  /**
322
162
  * Serializable snapshot of the tracked stores (registry metadata plus each
323
163
  * store's current state). Used by the sync adapter on the device side —
@@ -362,18 +202,18 @@ class ZustandStateStore {
362
202
  * the enabled flag so the UI's pause button freezes the mirror.
363
203
  */
364
204
  replaceFromSnapshot(changes, storeSnapshots) {
365
- if (!this.isEnabled) return;
205
+ if (!this.getEnabled()) return;
366
206
 
367
207
  // Preserve trees a remote mirror already hydrated — wire-form snapshots
368
208
  // always carry markers, and a later snapshot must not blank the detail pane.
369
209
  const hydrated = new Map();
370
- for (const existing of this.stateChanges) {
210
+ for (const existing of this.items) {
371
211
  const prev = existing.prevState;
372
212
  if (!prev || prev.__buoyStateOnDevice !== true) {
373
213
  hydrated.set(existing.id, existing);
374
214
  }
375
215
  }
376
- this.stateChanges = changes.slice(0, this.maxChanges).map(change => {
216
+ const merged = changes.slice(0, this.max).map(change => {
377
217
  const kept = hydrated.get(change.id);
378
218
  const incoming = change.prevState;
379
219
  return kept && incoming?.__buoyStateOnDevice === true ? {
@@ -417,7 +257,7 @@ class ZustandStateStore {
417
257
  color: snapshot.color
418
258
  }];
419
259
  }));
420
- this.notifyListeners();
260
+ this.setItems(merged);
421
261
  this.notifyStoreListeners();
422
262
  }
423
263
 
@@ -426,13 +266,7 @@ class ZustandStateStore {
426
266
  * device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
427
267
  */
428
268
  hydrateChange(id, patch) {
429
- const index = this.stateChanges.findIndex(c => c.id === id);
430
- if (index === -1) return;
431
- this.stateChanges = [...this.stateChanges.slice(0, index), {
432
- ...this.stateChanges[index],
433
- ...patch
434
- }, ...this.stateChanges.slice(index + 1)];
435
- this.notifyListeners();
269
+ this.hydrateById(id, patch);
436
270
  }
437
271
 
438
272
  /**
@@ -485,7 +319,7 @@ class ZustandStateStore {
485
319
  // ---- Filtering ----
486
320
 
487
321
  filterStateChanges(filter) {
488
- let filtered = [...this.stateChanges];
322
+ let filtered = this.getAll();
489
323
  if (filter.searchText) {
490
324
  const search = filter.searchText.toLowerCase();
491
325
  filtered = filtered.filter(c => c.storeName.toLowerCase().includes(search) || c.partialPreview.toLowerCase().includes(search) || c.changedKeys.some(k => k.toLowerCase().includes(search)));
@@ -502,10 +336,10 @@ class ZustandStateStore {
502
336
  // ---- Stats ----
503
337
 
504
338
  getStats() {
505
- const total = this.stateChanges.length;
506
- const withChanges = this.stateChanges.filter(c => c.hasStateChange).length;
339
+ const total = this.items.length;
340
+ const withChanges = this.items.filter(c => c.hasStateChange).length;
507
341
  const storeCount = this.stores.size;
508
- const durations = this.stateChanges.filter(c => c.duration !== undefined).map(c => c.duration);
342
+ const durations = this.items.filter(c => c.duration !== undefined).map(c => c.duration);
509
343
  const avgDuration = durations.length > 0 ? durations.reduce((sum, d) => sum + d, 0) / durations.length : 0;
510
344
  return {
511
345
  totalChanges: total,
@@ -521,38 +355,18 @@ class ZustandStateStore {
521
355
 
522
356
  // ---- Enable / Disable ----
523
357
 
524
- setEnabled(enabled) {
525
- this.isEnabled = enabled;
526
- }
527
- getEnabled() {
528
- return this.isEnabled;
529
- }
530
358
  setMaxChanges(max) {
531
- this.maxChanges = max;
532
- if (this.stateChanges.length > max) {
533
- this.stateChanges = this.stateChanges.slice(0, max);
534
- this.notifyListeners();
535
- }
359
+ this.setMax(max);
536
360
  }
537
361
 
538
362
  // ---- Subscriptions ----
539
363
 
540
- subscribe(listener) {
541
- this.listeners.add(listener);
542
- return () => {
543
- this.listeners.delete(listener);
544
- };
545
- }
546
364
  subscribeToStores(listener) {
547
365
  this.storeListeners.add(listener);
548
366
  return () => {
549
367
  this.storeListeners.delete(listener);
550
368
  };
551
369
  }
552
- notifyListeners() {
553
- const changes = this.getStateChanges();
554
- this.listeners.forEach(listener => listener(changes));
555
- }
556
370
  notifyStoreListeners() {
557
371
  const stores = this.getStores();
558
372
  this.storeListeners.forEach(listener => listener(stores));
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { TouchableOpacity, Text, View, StyleSheet } from "react-native";
11
- import { night } from "@buoy-gg/shared-ui";
11
+ import { night, monoFont } from "@buoy-gg/shared-ui";
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  const buttonConfigs = {
14
14
  copy: {
@@ -108,7 +108,7 @@ const styles = StyleSheet.create({
108
108
  fontWeight: "600",
109
109
  letterSpacing: 0.5,
110
110
  textTransform: "uppercase",
111
- fontFamily: "monospace",
111
+ fontFamily: monoFont,
112
112
  height: 12
113
113
  }
114
114
  });
@@ -4,134 +4,40 @@
4
4
  * ZustandDetailViewToggle
5
5
  *
6
6
  * Three-card toggle for switching between Change, State, and Diff views.
7
- * Mirrors ReduxDetailViewToggle.tsx
7
+ * The cards themselves are the shared `DetailViewToggle`; this file owns
8
+ * the zustand view list and testID prefix.
8
9
  */
9
10
 
10
11
  import React, { memo } from "react";
11
- import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
12
- import { night, FileText, Database, GitBranch } from "@buoy-gg/shared-ui";
13
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
- const VIEW_CONFIGS = [{
12
+ import { DetailViewToggle, FileText, Database, GitBranch } from "@buoy-gg/shared-ui";
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ const VIEWS = [{
15
15
  key: "change",
16
16
  label: "CHANGE",
17
17
  description: "View change details",
18
- icon: FileText,
19
- activeColor: night.color.accent
18
+ icon: FileText
20
19
  }, {
21
20
  key: "state",
22
21
  label: "STATE",
23
22
  description: "View current state",
24
- icon: Database,
25
- activeColor: night.color.accent
23
+ icon: Database
26
24
  }, {
27
25
  key: "diff",
28
26
  label: "DIFF",
29
27
  description: "Compare changes",
30
- icon: GitBranch,
31
- activeColor: night.color.accent
28
+ icon: GitBranch
32
29
  }];
30
+ const DIFF_DISABLED = ["diff"];
33
31
  export const ZustandDetailViewToggle = /*#__PURE__*/memo(function ZustandDetailViewToggle({
34
32
  activeView,
35
33
  onViewChange,
36
34
  diffDisabled = false
37
35
  }) {
38
- return /*#__PURE__*/_jsx(View, {
39
- style: styles.container,
40
- children: VIEW_CONFIGS.map(config => {
41
- const isActive = activeView === config.key;
42
- const isDisabled = config.key === "diff" && diffDisabled;
43
- const IconComponent = config.icon;
44
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
45
- style: [styles.card, isActive && [styles.cardActive, {
46
- borderColor: config.activeColor
47
- }], isDisabled && styles.cardDisabled],
48
- onPress: () => !isDisabled && onViewChange(config.key),
49
- activeOpacity: isDisabled ? 1 : 0.8,
50
- disabled: isDisabled,
51
- testID: `buoy-zustand-view-${config.key}`,
52
- accessibilityRole: "button",
53
- accessibilityLabel: config.label,
54
- accessibilityState: {
55
- disabled: isDisabled,
56
- selected: isActive
57
- },
58
- children: [/*#__PURE__*/_jsxs(View, {
59
- style: styles.cardContent,
60
- children: [/*#__PURE__*/_jsx(IconComponent, {
61
- size: 14,
62
- color: isActive ? config.activeColor : isDisabled ? night.color.textTertiary : night.color.textSecondary
63
- }), /*#__PURE__*/_jsx(Text, {
64
- style: [styles.cardLabel, isActive && [styles.cardLabelActive, {
65
- color: config.activeColor
66
- }], isDisabled && styles.cardLabelDisabled],
67
- children: config.label
68
- })]
69
- }), /*#__PURE__*/_jsx(Text, {
70
- style: [styles.cardDescription, isActive && styles.cardDescriptionActive, isDisabled && styles.cardDescriptionDisabled],
71
- children: config.description
72
- })]
73
- }, config.key);
74
- })
36
+ return /*#__PURE__*/_jsx(DetailViewToggle, {
37
+ views: VIEWS,
38
+ activeView: activeView,
39
+ onViewChange: onViewChange,
40
+ disabledKeys: diffDisabled ? DIFF_DISABLED : undefined,
41
+ testIDPrefix: "buoy-zustand"
75
42
  });
76
- });
77
- const styles = StyleSheet.create({
78
- container: {
79
- flexDirection: "row",
80
- gap: 8,
81
- padding: 14,
82
- // Transparent so the night sheet's star field shows through.
83
- backgroundColor: "transparent"
84
- },
85
- card: {
86
- flex: 1,
87
- backgroundColor: night.color.surface,
88
- borderRadius: 12,
89
- borderWidth: 1,
90
- borderColor: night.color.border,
91
- padding: 10,
92
- gap: 4
93
- },
94
- cardActive: {
95
- borderWidth: 1.5,
96
- backgroundColor: night.color.accentSoft,
97
- shadowColor: night.color.accent,
98
- shadowOffset: {
99
- width: 0,
100
- height: 2
101
- },
102
- shadowOpacity: 0.1,
103
- shadowRadius: 8,
104
- elevation: 3
105
- },
106
- cardDisabled: {
107
- opacity: 0.5
108
- },
109
- cardContent: {
110
- flexDirection: "row",
111
- alignItems: "center",
112
- gap: 6
113
- },
114
- cardLabel: {
115
- fontSize: 10,
116
- fontWeight: "700",
117
- letterSpacing: 0.5,
118
- color: night.color.textSecondary
119
- },
120
- cardLabelActive: {
121
- color: night.color.text
122
- },
123
- cardLabelDisabled: {
124
- color: night.color.textTertiary
125
- },
126
- cardDescription: {
127
- fontSize: 9,
128
- color: night.color.textTertiary,
129
- lineHeight: 12
130
- },
131
- cardDescriptionActive: {
132
- color: night.color.textSecondary
133
- },
134
- cardDescriptionDisabled: {
135
- color: night.color.textTertiary
136
- }
137
43
  });