@buoy-gg/storage 7.0.30 → 7.0.31

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 (41) hide show
  1. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +92 -21
  2. package/lib/commonjs/storage/components/StorageBrowserMode.js +8 -2
  3. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +572 -0
  4. package/lib/commonjs/storage/components/StorageKeyRow.js +21 -2
  5. package/lib/commonjs/storage/components/StorageKeySection.js +44 -21
  6. package/lib/commonjs/storage/components/StorageModalWithTabs.js +24 -2
  7. package/lib/commonjs/storage/components/StorageSaveNotice.js +171 -0
  8. package/lib/commonjs/storage/components/StorageValueEditModal.js +11 -364
  9. package/lib/commonjs/storage/hooks/useKeyEditor.js +12 -85
  10. package/lib/commonjs/storage/hooks/useTreeDraft.js +7 -100
  11. package/lib/module/storage/components/GameUIStorageBrowser.js +94 -23
  12. package/lib/module/storage/components/StorageBrowserMode.js +8 -2
  13. package/lib/module/storage/components/StorageKeyDetailScreen.js +570 -0
  14. package/lib/module/storage/components/StorageKeyRow.js +20 -3
  15. package/lib/module/storage/components/StorageKeySection.js +43 -21
  16. package/lib/module/storage/components/StorageModalWithTabs.js +24 -2
  17. package/lib/module/storage/components/StorageSaveNotice.js +168 -0
  18. package/lib/module/storage/components/StorageValueEditModal.js +13 -366
  19. package/lib/module/storage/hooks/useKeyEditor.js +14 -87
  20. package/lib/module/storage/hooks/useTreeDraft.js +9 -102
  21. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +13 -2
  22. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  23. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +7 -2
  24. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  25. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts +46 -0
  26. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +1 -0
  27. package/lib/typescript/storage/components/StorageKeyRow.d.ts +9 -0
  28. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  29. package/lib/typescript/storage/components/StorageKeySection.d.ts +19 -1
  30. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  31. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  32. package/lib/typescript/storage/components/StorageSaveNotice.d.ts +6 -0
  33. package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +1 -0
  34. package/lib/typescript/storage/components/StorageValueEditModal.d.ts +9 -31
  35. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -1
  36. package/lib/typescript/storage/hooks/useKeyEditor.d.ts +7 -31
  37. package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -1
  38. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +7 -66
  39. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -1
  40. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +1 -1
  41. package/package.json +5 -5
@@ -0,0 +1,570 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * One storage key, on a screen of its own — the React Query detail-screen
5
+ * shape (and the Zustand tool's twin), pointed at a stored value.
6
+ *
7
+ * Tapping a key card opens this instead of expanding in place. Top to bottom:
8
+ *
9
+ * 1. VALUE EDITOR — the shared LiveExplorer. No draft, no Save: an edit
10
+ * re-serializes the whole value and writes it as you make it. Typing
11
+ * commits on a short debounce (~300ms, flushed on blur) so a word is one
12
+ * storage write, not one per keystroke — storage writes hit disk and
13
+ * land in the events timeline, which is the one way this tool differs
14
+ * from RQ's unlogged cache writes. A scalar key is just the root leaf's
15
+ * input; a boolean key is a one-tap toggle.
16
+ * 2. KEY DETAILS — what the expanded card used to say: status, backend,
17
+ * MMKV instance, updates (with the road to history), schema diagnostics.
18
+ * 3. RAW VALUE — the serialized string exactly as the backend holds it.
19
+ * 4. The card's housekeeping actions: pin, hide, delete.
20
+ *
21
+ * THE ROUND-TRIP WRINKLE zustand doesn't have: a storage write is async —
22
+ * write, device event, snapshot, refreshed prop. Between an edit and its
23
+ * echo, `storageKey.value` is STALE, so basing the next edit on the prop
24
+ * would silently revert the previous one (edit qty, then name, and qty snaps
25
+ * back). `lastWrittenRef` holds the most recent written document and is the
26
+ * base for every edit until the prop catches up to it — last-write-wins
27
+ * against the app, exactly once, and never against the editor itself.
28
+ */
29
+
30
+ import { useCallback, useMemo, useRef, useState } from "react";
31
+ import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
32
+ import { EyeOff, FileJson, HardDrive, night, macOSColors, Pin, PillBadge, Trash2, TypeBadge, useSafeAreaInsets } from "@buoy-gg/shared-ui";
33
+ import { DataViewer, LiveExplorer, ValueEditModal, updateNestedDataByPath, deleteNestedDataByPath } from "@buoy-gg/shared-ui/dataViewer";
34
+ import { getStorageTypeLabel } from "../utils/storageQueryUtils";
35
+ import { getValueTypeLabel, parseStorageValue } from "../utils/valueType";
36
+ import { getStatusConfig, getStorageTypeColor } from "./StorageKeyRow";
37
+
38
+ /** How long typed edits coalesce before the single write fires. */
39
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
40
+ const TYPING_COMMIT_MS = 300;
41
+ const DETAIL_ID = "buoy-storage-detail";
42
+ function detailExplorerTestID(dataPath) {
43
+ const slug = part => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
44
+ return dataPath.length === 0 ? `${DETAIL_ID}-explorer-root` : `${DETAIL_ID}-explorer-${dataPath.map(slug).filter(Boolean).join("-")}`;
45
+ }
46
+
47
+ /** Same MMKV instance-color rule the card uses. */
48
+ const INSTANCE_COLORS = [macOSColors.semantic.info, macOSColors.semantic.success, macOSColors.semantic.warning, macOSColors.semantic.debug, "#FF6B9D", "#00D9FF"];
49
+ function getInstanceColor(instanceId) {
50
+ const hash = instanceId.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
51
+ return INSTANCE_COLORS[hash % INSTANCE_COLORS.length];
52
+ }
53
+ export function StorageKeyDetailScreen({
54
+ storageKey,
55
+ editBlockedReason,
56
+ eventCount,
57
+ onSave,
58
+ onViewHistory,
59
+ onDeleteKey,
60
+ onHideKey,
61
+ isPinned = false,
62
+ onTogglePin,
63
+ applySafeAreaInset
64
+ }) {
65
+ const insets = useSafeAreaInsets();
66
+ const bottomInset = applySafeAreaInset ? insets.bottom : 0;
67
+ const canEdit = editBlockedReason == null;
68
+ const config = getStatusConfig(storageKey.status);
69
+ const storageTypeLabel = getStorageTypeLabel(storageKey.storageType);
70
+ const [rawOpen, setRawOpen] = useState(false);
71
+ // Force the explorer to re-sync focused inputs after a Raw-hatch apply.
72
+ const [rawVersion, setRawVersion] = useState(0);
73
+
74
+ /**
75
+ * The most recent document THIS screen wrote, until the refreshed prop
76
+ * catches up to it — see the module docstring's round-trip wrinkle. Held
77
+ * as its serialized form so "caught up" is one string compare against the
78
+ * same serialization the write used.
79
+ */
80
+ const lastWrittenRef = useRef(null);
81
+ const propParsed = useMemo(() => parseStorageValue(storageKey.value), [storageKey.value]);
82
+ const propRaw = typeof storageKey.value === "string" ? storageKey.value : JSON.stringify(storageKey.value);
83
+ if (lastWrittenRef.current && lastWrittenRef.current.raw === propRaw) {
84
+ // The device echoed our write back — the prop is current again.
85
+ lastWrittenRef.current = null;
86
+ }
87
+ const baseValue = lastWrittenRef.current ? lastWrittenRef.current.value : propParsed;
88
+ const write = useCallback(next => {
89
+ const raw = JSON.stringify(next);
90
+ lastWrittenRef.current = {
91
+ raw,
92
+ value: next
93
+ };
94
+ onSave(raw).catch(e => {
95
+ // The write was refused (the Pro gate, a dead backend) — the base
96
+ // must fall back to what's actually stored, or the tree keeps
97
+ // showing an edit that never landed.
98
+ lastWrittenRef.current = null;
99
+ Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
100
+ });
101
+ }, [onSave]);
102
+ const baseRef = useRef(baseValue);
103
+ baseRef.current = baseValue;
104
+ const writer = useMemo(() => {
105
+ if (!canEdit) return undefined;
106
+ return {
107
+ update: (path, value) => {
108
+ const next = path.length === 0 ? value : updateNestedDataByPath(baseRef.current, path, value);
109
+ write(next);
110
+ },
111
+ remove: path => {
112
+ if (path.length === 0) return;
113
+ write(deleteNestedDataByPath(baseRef.current, path));
114
+ }
115
+ // Storage replaces the whole key on every write, so any node may go.
116
+ };
117
+ }, [canEdit, write]);
118
+ const showHousekeeping = onTogglePin || onHideKey || canEdit && onDeleteKey;
119
+ return /*#__PURE__*/_jsxs(View, {
120
+ style: styles.screen,
121
+ children: [/*#__PURE__*/_jsx(View, {
122
+ testID: `${DETAIL_ID}-key`,
123
+ accessibilityLabel: storageKey.key
124
+ }), /*#__PURE__*/_jsxs(ScrollView, {
125
+ testID: `${DETAIL_ID}-scroll`,
126
+ style: styles.scroll,
127
+ contentContainerStyle: [styles.scrollContent, {
128
+ paddingBottom: 24 + bottomInset
129
+ }],
130
+ keyboardShouldPersistTaps: "handled",
131
+ children: [/*#__PURE__*/_jsx(View, {
132
+ style: styles.section,
133
+ children: /*#__PURE__*/_jsxs(View, {
134
+ style: styles.editorContainer,
135
+ children: [/*#__PURE__*/_jsxs(View, {
136
+ style: styles.sectionHeaderRow,
137
+ children: [/*#__PURE__*/_jsx(Text, {
138
+ testID: `${DETAIL_ID}-editor-header`,
139
+ style: styles.sectionHeaderAccentText,
140
+ children: "Value Editor"
141
+ }), canEdit ? /*#__PURE__*/_jsxs(TouchableOpacity, {
142
+ onPress: () => setRawOpen(true),
143
+ testID: `${DETAIL_ID}-raw`,
144
+ accessibilityRole: "button",
145
+ accessibilityLabel: "Edit raw JSON",
146
+ style: styles.rawButton,
147
+ hitSlop: {
148
+ top: 6,
149
+ bottom: 6,
150
+ left: 6,
151
+ right: 6
152
+ },
153
+ children: [/*#__PURE__*/_jsx(FileJson, {
154
+ size: 13,
155
+ color: night.color.accent
156
+ }), /*#__PURE__*/_jsx(Text, {
157
+ style: styles.rawButtonText,
158
+ children: "Raw"
159
+ })]
160
+ }) : null]
161
+ }), /*#__PURE__*/_jsxs(View, {
162
+ style: styles.sectionBody,
163
+ children: [/*#__PURE__*/_jsx(LiveExplorer, {
164
+ editable: canEdit,
165
+ label: "Value",
166
+ value: baseValue,
167
+ defaultExpanded: ["Value"],
168
+ writer: writer,
169
+ dataVersion: rawVersion,
170
+ testIDForPath: detailExplorerTestID,
171
+ debounceMs: TYPING_COMMIT_MS
172
+ }), canEdit ? /*#__PURE__*/_jsx(Text, {
173
+ style: styles.editorHint,
174
+ children: "Edits write to the device as you make them \u2014 undo lives in the key's history."
175
+ }) : /*#__PURE__*/_jsx(Text, {
176
+ style: styles.blockedText,
177
+ testID: `${DETAIL_ID}-edit-blocked`,
178
+ children: editBlockedReason
179
+ })]
180
+ })]
181
+ })
182
+ }), /*#__PURE__*/_jsx(View, {
183
+ style: styles.section,
184
+ children: /*#__PURE__*/_jsxs(View, {
185
+ style: styles.infoContainer,
186
+ children: [/*#__PURE__*/_jsx(Text, {
187
+ style: styles.sectionHeaderPlain,
188
+ children: "Key Details"
189
+ }), /*#__PURE__*/_jsxs(View, {
190
+ style: styles.infoBody,
191
+ children: [/*#__PURE__*/_jsxs(View, {
192
+ style: styles.infoRow,
193
+ children: [/*#__PURE__*/_jsx(Text, {
194
+ style: styles.infoLabel,
195
+ children: "Status:"
196
+ }), /*#__PURE__*/_jsx(PillBadge, {
197
+ color: config.color,
198
+ testID: `${DETAIL_ID}-status`,
199
+ children: config.label
200
+ }), /*#__PURE__*/_jsx(Text, {
201
+ style: styles.infoDim,
202
+ children: config.sublabel
203
+ })]
204
+ }), /*#__PURE__*/_jsxs(View, {
205
+ style: styles.infoRow,
206
+ children: [/*#__PURE__*/_jsx(Text, {
207
+ style: styles.infoLabel,
208
+ children: "Storage:"
209
+ }), /*#__PURE__*/_jsx(PillBadge, {
210
+ color: getStorageTypeColor(storageKey.storageType),
211
+ children: storageTypeLabel
212
+ }), /*#__PURE__*/_jsx(Text, {
213
+ style: styles.infoDim,
214
+ children: getValueTypeLabel(storageKey.value)
215
+ })]
216
+ }), storageKey.storageType === "mmkv" && storageKey.instanceId && /*#__PURE__*/_jsxs(View, {
217
+ style: styles.infoRow,
218
+ children: [/*#__PURE__*/_jsx(Text, {
219
+ style: styles.infoLabel,
220
+ children: "Instance:"
221
+ }), /*#__PURE__*/_jsx(PillBadge, {
222
+ color: getInstanceColor(storageKey.instanceId),
223
+ icon: /*#__PURE__*/_jsx(HardDrive, {
224
+ size: 9,
225
+ color: getInstanceColor(storageKey.instanceId)
226
+ }),
227
+ children: storageKey.instanceId
228
+ })]
229
+ }), eventCount != null && eventCount > 0 && /*#__PURE__*/_jsxs(View, {
230
+ style: styles.infoRow,
231
+ children: [/*#__PURE__*/_jsx(Text, {
232
+ style: styles.infoLabel,
233
+ children: "Updates:"
234
+ }), /*#__PURE__*/_jsx(PillBadge, {
235
+ color: macOSColors.semantic.warning,
236
+ children: String(eventCount)
237
+ }), eventCount > 1 && onViewHistory && /*#__PURE__*/_jsx(TouchableOpacity, {
238
+ onPress: onViewHistory,
239
+ testID: "buoy-storage-key-history",
240
+ accessibilityLabel: `View history for ${storageKey.key}`,
241
+ accessibilityRole: "button",
242
+ style: styles.inlineLinkButton,
243
+ hitSlop: {
244
+ top: 6,
245
+ bottom: 6,
246
+ left: 6,
247
+ right: 6
248
+ },
249
+ children: /*#__PURE__*/_jsx(Text, {
250
+ style: styles.inlineLinkText,
251
+ children: "view history \u2192"
252
+ })
253
+ })]
254
+ }), storageKey.status === "required_wrong_type" && storageKey.expectedType && /*#__PURE__*/_jsxs(View, {
255
+ style: styles.infoRow,
256
+ children: [/*#__PURE__*/_jsx(Text, {
257
+ style: styles.infoLabel,
258
+ children: "Expected:"
259
+ }), /*#__PURE__*/_jsx(TypeBadge, {
260
+ type: storageKey.expectedType
261
+ }), /*#__PURE__*/_jsxs(Text, {
262
+ style: styles.infoDim,
263
+ children: ["got ", getValueTypeLabel(storageKey.value)]
264
+ })]
265
+ }), storageKey.status === "required_wrong_value" && storageKey.expectedValue && /*#__PURE__*/_jsxs(View, {
266
+ style: styles.infoRow,
267
+ children: [/*#__PURE__*/_jsx(Text, {
268
+ style: styles.infoLabel,
269
+ children: "Expected:"
270
+ }), /*#__PURE__*/_jsx(Text, {
271
+ style: styles.infoExpected,
272
+ children: String(storageKey.expectedValue)
273
+ })]
274
+ }), storageKey.description ? /*#__PURE__*/_jsxs(View, {
275
+ style: styles.infoRow,
276
+ children: [/*#__PURE__*/_jsx(Text, {
277
+ style: styles.infoLabel,
278
+ children: "Info:"
279
+ }), /*#__PURE__*/_jsx(Text, {
280
+ style: styles.infoDescription,
281
+ children: storageKey.description
282
+ })]
283
+ }) : null]
284
+ })]
285
+ })
286
+ }), /*#__PURE__*/_jsx(View, {
287
+ style: styles.section,
288
+ children: /*#__PURE__*/_jsxs(View, {
289
+ style: styles.infoContainer,
290
+ children: [/*#__PURE__*/_jsx(Text, {
291
+ style: styles.sectionHeaderPlain,
292
+ children: "Value Explorer"
293
+ }), /*#__PURE__*/_jsx(View, {
294
+ style: styles.viewerBody,
295
+ testID: `${DETAIL_ID}-viewer`,
296
+ children: baseValue !== null && typeof baseValue === "object" ? /*#__PURE__*/_jsx(DataViewer, {
297
+ title: "",
298
+ data: baseValue,
299
+ showTypeFilter: true,
300
+ rawMode: true,
301
+ initialExpanded: false
302
+ }) : /*#__PURE__*/_jsx(Text, {
303
+ style: styles.rawValueText,
304
+ selectable: true,
305
+ children: propRaw ?? "undefined"
306
+ })
307
+ })]
308
+ })
309
+ }), showHousekeeping ? /*#__PURE__*/_jsxs(View, {
310
+ style: styles.actionRow,
311
+ children: [onTogglePin ? /*#__PURE__*/_jsxs(TouchableOpacity, {
312
+ style: [styles.actionChip, isPinned && styles.actionChipActive],
313
+ testID: "buoy-storage-key-pin",
314
+ accessibilityLabel: isPinned ? `Unpin ${storageKey.key}` : `Pin ${storageKey.key} to top`,
315
+ accessibilityRole: "button",
316
+ onPress: () => onTogglePin(storageKey.key),
317
+ hitSlop: {
318
+ top: 6,
319
+ bottom: 6,
320
+ left: 6,
321
+ right: 6
322
+ },
323
+ children: [/*#__PURE__*/_jsx(Pin, {
324
+ size: 12,
325
+ active: isPinned,
326
+ color: night.color.accent
327
+ }), /*#__PURE__*/_jsx(Text, {
328
+ style: [styles.actionChipText, {
329
+ color: night.color.accent
330
+ }],
331
+ children: isPinned ? "Unpin" : "Pin to top"
332
+ })]
333
+ }) : null, onHideKey ? /*#__PURE__*/_jsxs(TouchableOpacity, {
334
+ style: styles.actionChip,
335
+ testID: "buoy-storage-key-hide",
336
+ accessibilityLabel: `Hide ${storageKey.key} from list`,
337
+ accessibilityRole: "button",
338
+ onPress: () => onHideKey(storageKey),
339
+ hitSlop: {
340
+ top: 6,
341
+ bottom: 6,
342
+ left: 6,
343
+ right: 6
344
+ },
345
+ children: [/*#__PURE__*/_jsx(EyeOff, {
346
+ size: 12,
347
+ color: night.color.warning
348
+ }), /*#__PURE__*/_jsx(Text, {
349
+ style: [styles.actionChipText, {
350
+ color: night.color.warning
351
+ }],
352
+ children: "Hide from list"
353
+ })]
354
+ }) : null, canEdit && onDeleteKey ? /*#__PURE__*/_jsxs(TouchableOpacity, {
355
+ style: [styles.actionChip, styles.dangerChip],
356
+ testID: "buoy-storage-key-delete",
357
+ accessibilityLabel: `Delete key ${storageKey.key}`,
358
+ accessibilityRole: "button",
359
+ onPress: () => onDeleteKey(storageKey),
360
+ hitSlop: {
361
+ top: 6,
362
+ bottom: 6,
363
+ left: 6,
364
+ right: 6
365
+ },
366
+ children: [/*#__PURE__*/_jsx(Trash2, {
367
+ size: 12,
368
+ color: night.color.danger
369
+ }), /*#__PURE__*/_jsx(Text, {
370
+ style: [styles.actionChipText, {
371
+ color: night.color.danger
372
+ }],
373
+ children: "Delete key"
374
+ })]
375
+ }) : null]
376
+ }) : null]
377
+ }), rawOpen && canEdit ? /*#__PURE__*/_jsx(ValueEditModal, {
378
+ path: [],
379
+ value: baseValue,
380
+ onSave: next => {
381
+ write(next);
382
+ setRawVersion(v => v + 1);
383
+ setRawOpen(false);
384
+ },
385
+ onCancel: () => setRawOpen(false),
386
+ saveLabel: "Apply",
387
+ testIDPrefix: "buoy-storage-value"
388
+ }) : null]
389
+ });
390
+ }
391
+ const styles = StyleSheet.create({
392
+ screen: {
393
+ flex: 1,
394
+ minHeight: 0
395
+ // Transparent — this is the modal's body, and the modal paints its own
396
+ // background (the same contract as the Zustand detail screen).
397
+ },
398
+ scroll: {
399
+ flex: 1
400
+ },
401
+ scrollContent: {
402
+ paddingHorizontal: 8,
403
+ paddingTop: 8
404
+ },
405
+ section: {
406
+ marginBottom: 16
407
+ },
408
+ editorContainer: {
409
+ minWidth: 200,
410
+ backgroundColor: night.color.surface,
411
+ borderRadius: 6,
412
+ borderWidth: 1,
413
+ borderColor: night.color.accent + "4D",
414
+ overflow: "hidden",
415
+ shadowColor: night.color.accent,
416
+ shadowOffset: {
417
+ width: 0,
418
+ height: 0
419
+ },
420
+ shadowOpacity: 0.1,
421
+ shadowRadius: 6
422
+ },
423
+ sectionHeaderRow: {
424
+ flexDirection: "row",
425
+ alignItems: "center",
426
+ justifyContent: "space-between",
427
+ backgroundColor: night.color.accent + "1A",
428
+ paddingHorizontal: 12,
429
+ paddingVertical: 8,
430
+ borderBottomWidth: 1,
431
+ borderBottomColor: night.color.accent + "33"
432
+ },
433
+ sectionHeaderAccentText: {
434
+ fontWeight: "600",
435
+ fontSize: 12,
436
+ color: night.color.accent,
437
+ letterSpacing: 0.5,
438
+ textTransform: "uppercase",
439
+ fontFamily: "monospace"
440
+ },
441
+ rawButton: {
442
+ flexDirection: "row",
443
+ alignItems: "center",
444
+ gap: 4
445
+ },
446
+ rawButtonText: {
447
+ color: night.color.accent,
448
+ fontSize: 11,
449
+ fontWeight: "700"
450
+ },
451
+ sectionBody: {
452
+ padding: 8
453
+ },
454
+ editorHint: {
455
+ color: night.color.textTertiary,
456
+ fontSize: 10,
457
+ paddingTop: 8,
458
+ paddingHorizontal: 4,
459
+ lineHeight: 14
460
+ },
461
+ blockedText: {
462
+ color: night.color.textSecondary,
463
+ fontSize: 12,
464
+ padding: 12,
465
+ lineHeight: 18
466
+ },
467
+ infoContainer: {
468
+ minWidth: 200,
469
+ backgroundColor: night.color.surface,
470
+ borderRadius: 6,
471
+ borderWidth: 1,
472
+ borderColor: night.color.border,
473
+ overflow: "hidden"
474
+ },
475
+ sectionHeaderPlain: {
476
+ backgroundColor: night.color.surfaceElevated,
477
+ paddingHorizontal: 12,
478
+ paddingVertical: 8,
479
+ fontWeight: "600",
480
+ fontSize: 12,
481
+ color: night.color.textSecondary,
482
+ borderBottomWidth: 1,
483
+ borderBottomColor: night.color.border,
484
+ letterSpacing: 0.5,
485
+ textTransform: "uppercase",
486
+ fontFamily: "monospace"
487
+ },
488
+ infoBody: {
489
+ padding: 12,
490
+ gap: 10
491
+ },
492
+ infoRow: {
493
+ flexDirection: "row",
494
+ alignItems: "center",
495
+ gap: 8
496
+ },
497
+ infoLabel: {
498
+ fontSize: 10,
499
+ color: night.color.textTertiary,
500
+ fontWeight: "600",
501
+ minWidth: 64,
502
+ fontFamily: "monospace"
503
+ },
504
+ infoDim: {
505
+ color: night.color.textTertiary,
506
+ fontSize: 10,
507
+ fontFamily: "monospace"
508
+ },
509
+ infoExpected: {
510
+ fontSize: 11,
511
+ color: macOSColors.semantic.warning,
512
+ fontFamily: "monospace",
513
+ flex: 1
514
+ },
515
+ infoDescription: {
516
+ fontSize: 11,
517
+ color: night.color.textSecondary,
518
+ flex: 1
519
+ },
520
+ rawValueText: {
521
+ color: night.color.textSecondary,
522
+ fontSize: 11,
523
+ fontFamily: "monospace",
524
+ lineHeight: 16,
525
+ padding: 12
526
+ },
527
+ viewerBody: {
528
+ backgroundColor: night.color.bg,
529
+ minHeight: 48
530
+ },
531
+ inlineLinkButton: {
532
+ marginLeft: 4
533
+ },
534
+ inlineLinkText: {
535
+ fontSize: 10,
536
+ fontWeight: "600",
537
+ fontFamily: "monospace",
538
+ color: night.color.accent
539
+ },
540
+ actionRow: {
541
+ flexDirection: "row",
542
+ alignItems: "center",
543
+ flexWrap: "wrap",
544
+ gap: 8,
545
+ paddingHorizontal: 4
546
+ },
547
+ actionChip: {
548
+ flexDirection: "row",
549
+ alignItems: "center",
550
+ gap: 6,
551
+ paddingVertical: 6,
552
+ paddingHorizontal: 10,
553
+ borderRadius: 6,
554
+ backgroundColor: night.color.surface,
555
+ borderWidth: 1,
556
+ borderColor: night.color.border
557
+ },
558
+ dangerChip: {
559
+ borderColor: night.color.danger + "55"
560
+ },
561
+ actionChipActive: {
562
+ backgroundColor: night.color.accentSoft,
563
+ borderColor: night.color.accentBorderStrong
564
+ },
565
+ actionChipText: {
566
+ fontSize: 11,
567
+ fontWeight: "600",
568
+ fontFamily: "monospace"
569
+ }
570
+ });
@@ -30,7 +30,9 @@ function getInstanceColor(instanceId) {
30
30
  const hash = instanceId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
31
31
  return INSTANCE_COLORS[hash % INSTANCE_COLORS.length];
32
32
  }
33
- const getStatusConfig = status => {
33
+ /** Exported for the key DETAIL screen, so card and detail can't disagree
34
+ * about what a status is called or what color it wears. */
35
+ export const getStatusConfig = status => {
34
36
  switch (status) {
35
37
  case "required_present":
36
38
  return {
@@ -62,6 +64,14 @@ const getStatusConfig = status => {
62
64
  color: macOSColors.semantic.debug,
63
65
  sublabel: "Optional"
64
66
  };
67
+ // Runtime data can carry a status the union doesn't know (a stale
68
+ // snapshot, a newer adapter) — a devtool must render it, not redbox.
69
+ default:
70
+ return {
71
+ label: "Set",
72
+ color: macOSColors.text.secondary,
73
+ sublabel: String(status)
74
+ };
65
75
  }
66
76
  };
67
77
  const formatValue = value => {
@@ -374,7 +384,7 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
374
384
  testID: `buoy-storage-row-${storageKey.storageType}-${storageKey.key}`,
375
385
  accessibilityLabel: `${storageKey.storageType} ${storageKey.key}`
376
386
  }), eventCount != null && eventCount > 1 && /*#__PURE__*/_jsx(View, {
377
- style: [styles.absCountBadge, {
387
+ style: [styles.absCountBadge, isExpanded && styles.absCountBadgeExpanded, {
378
388
  backgroundColor: macOSColors.semantic.warning + "22",
379
389
  borderColor: macOSColors.semantic.warning + "55"
380
390
  }],
@@ -389,7 +399,9 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
389
399
  })]
390
400
  });
391
401
  });
392
- const getStorageTypeColor = storageType => {
402
+
403
+ /** Exported for the key DETAIL screen — same reason as getStatusConfig. */
404
+ export const getStorageTypeColor = storageType => {
393
405
  switch (storageType) {
394
406
  case "mmkv":
395
407
  return macOSColors.semantic.info;
@@ -475,6 +487,11 @@ const styles = StyleSheet.create({
475
487
  borderWidth: 1,
476
488
  zIndex: 1
477
489
  },
490
+ // The expanded CompactRow grows its wrapper margin from 3 to 12, so the
491
+ // badge has to drop with it or it floats above the card border.
492
+ absCountBadgeExpanded: {
493
+ top: 13
494
+ },
478
495
  absCountText: {
479
496
  fontSize: 9,
480
497
  fontWeight: "700",