@buoy-gg/storage 7.0.4 → 7.0.5

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 (148) hide show
  1. package/lib/commonjs/index.js +77 -1
  2. package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
  3. package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
  4. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +263 -115
  5. package/lib/commonjs/storage/components/GameUIStorageStats.js +7 -7
  6. package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -1
  7. package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -1
  8. package/lib/commonjs/storage/components/SelectionActionBar.js +2 -36
  9. package/lib/commonjs/storage/components/StorageActions.js +6 -6
  10. package/lib/commonjs/storage/components/StorageBrowserMode.js +16 -2
  11. package/lib/commonjs/storage/components/StorageEventDetailContent.js +106 -149
  12. package/lib/commonjs/storage/components/StorageEventListener.js +13 -13
  13. package/lib/commonjs/storage/components/StorageFilterCards.js +13 -135
  14. package/lib/commonjs/storage/components/StorageKeyCard.js +1 -1
  15. package/lib/commonjs/storage/components/StorageKeyEditorScreen.js +392 -0
  16. package/lib/commonjs/storage/components/StorageKeyRow.js +53 -15
  17. package/lib/commonjs/storage/components/StorageKeySection.js +10 -4
  18. package/lib/commonjs/storage/components/StorageKeyStats.js +10 -10
  19. package/lib/commonjs/storage/components/StorageModalWithTabs.js +272 -129
  20. package/lib/commonjs/storage/components/StorageScalarKeyEditor.js +46 -0
  21. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +141 -0
  22. package/lib/commonjs/storage/components/StorageValueEditModal.js +361 -0
  23. package/lib/commonjs/storage/hooks/useKeyEditor.js +104 -0
  24. package/lib/commonjs/storage/hooks/useMeasuredWidth.js +74 -0
  25. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +70 -15
  26. package/lib/commonjs/storage/hooks/useTreeDraft.js +107 -0
  27. package/lib/commonjs/storage/index.js +29 -0
  28. package/lib/commonjs/storage/stores/storageEventStore.js +4 -2
  29. package/lib/commonjs/storage/sync/storageSyncAdapter.js +125 -3
  30. package/lib/commonjs/storage/utils/AsyncStorageListener.js +76 -12
  31. package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +10 -9
  32. package/lib/commonjs/storage/utils/MMKVListener.js +40 -4
  33. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +52 -1
  34. package/lib/commonjs/storage/utils/deleteStorageKeys.js +53 -0
  35. package/lib/commonjs/storage/utils/index.js +13 -0
  36. package/lib/commonjs/storage/utils/lineDiff.js +21 -354
  37. package/lib/commonjs/storage/utils/setStorageValue.js +218 -0
  38. package/lib/commonjs/storage/utils/storageBrowserRefresh.js +49 -0
  39. package/lib/commonjs/storage/utils/storageOps.js +53 -0
  40. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +60 -38
  41. package/lib/commonjs/storage/utils/valueType.js +24 -0
  42. package/lib/module/index.js +18 -1
  43. package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +10 -3
  44. package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +26 -8
  45. package/lib/module/storage/components/GameUIStorageBrowser.js +266 -118
  46. package/lib/module/storage/components/GameUIStorageStats.js +7 -7
  47. package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -1
  48. package/lib/module/storage/components/MMKVInstanceSelector.js +1 -1
  49. package/lib/module/storage/components/SelectionActionBar.js +2 -36
  50. package/lib/module/storage/components/StorageActions.js +7 -7
  51. package/lib/module/storage/components/StorageBrowserMode.js +16 -2
  52. package/lib/module/storage/components/StorageEventDetailContent.js +110 -151
  53. package/lib/module/storage/components/StorageEventListener.js +14 -14
  54. package/lib/module/storage/components/StorageFilterCards.js +13 -135
  55. package/lib/module/storage/components/StorageKeyCard.js +1 -1
  56. package/lib/module/storage/components/StorageKeyEditorScreen.js +388 -0
  57. package/lib/module/storage/components/StorageKeyRow.js +55 -17
  58. package/lib/module/storage/components/StorageKeySection.js +11 -5
  59. package/lib/module/storage/components/StorageKeyStats.js +11 -10
  60. package/lib/module/storage/components/StorageModalWithTabs.js +272 -130
  61. package/lib/module/storage/components/StorageScalarKeyEditor.js +42 -0
  62. package/lib/module/storage/components/StorageTypeFilterBar.js +136 -0
  63. package/lib/module/storage/components/StorageValueEditModal.js +357 -0
  64. package/lib/module/storage/hooks/useKeyEditor.js +100 -0
  65. package/lib/module/storage/hooks/useMeasuredWidth.js +71 -0
  66. package/lib/module/storage/hooks/useSecureStoreKeys.js +71 -16
  67. package/lib/module/storage/hooks/useTreeDraft.js +103 -0
  68. package/lib/module/storage/index.js +3 -0
  69. package/lib/module/storage/stores/storageEventStore.js +4 -2
  70. package/lib/module/storage/sync/storageSyncAdapter.js +125 -3
  71. package/lib/module/storage/utils/AsyncStorageListener.js +76 -12
  72. package/lib/module/storage/utils/MMKVInstanceRegistry.js +8 -7
  73. package/lib/module/storage/utils/MMKVListener.js +40 -4
  74. package/lib/module/storage/utils/SecureStoreRegistry.js +52 -1
  75. package/lib/module/storage/utils/deleteStorageKeys.js +49 -0
  76. package/lib/module/storage/utils/index.js +3 -0
  77. package/lib/module/storage/utils/lineDiff.js +7 -354
  78. package/lib/module/storage/utils/setStorageValue.js +209 -0
  79. package/lib/module/storage/utils/storageBrowserRefresh.js +44 -0
  80. package/lib/module/storage/utils/storageOps.js +46 -0
  81. package/lib/module/storage/utils/storageTimeTravelUtils.js +61 -40
  82. package/lib/module/storage/utils/valueType.js +23 -0
  83. package/lib/typescript/index.d.ts +12 -1
  84. package/lib/typescript/index.d.ts.map +1 -1
  85. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts +4 -1
  86. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +1 -1
  87. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts +7 -1
  88. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +1 -1
  89. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts +27 -1
  90. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  91. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  92. package/lib/typescript/storage/components/StorageBrowserMode.d.ts +17 -1
  93. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +1 -1
  94. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts +4 -1
  95. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +1 -1
  96. package/lib/typescript/storage/components/StorageFilterCards.d.ts +9 -3
  97. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
  98. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts +42 -0
  99. package/lib/typescript/storage/components/StorageKeyEditorScreen.d.ts.map +1 -0
  100. package/lib/typescript/storage/components/StorageKeyRow.d.ts +17 -0
  101. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  102. package/lib/typescript/storage/components/StorageKeySection.d.ts +7 -1
  103. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +1 -1
  104. package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +1 -1
  105. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts +28 -1
  106. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +1 -1
  107. package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts +24 -0
  108. package/lib/typescript/storage/components/StorageScalarKeyEditor.d.ts.map +1 -0
  109. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts +29 -0
  110. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +1 -0
  111. package/lib/typescript/storage/components/StorageValueEditModal.d.ts +36 -0
  112. package/lib/typescript/storage/components/StorageValueEditModal.d.ts.map +1 -0
  113. package/lib/typescript/storage/hooks/useKeyEditor.d.ts +51 -0
  114. package/lib/typescript/storage/hooks/useKeyEditor.d.ts.map +1 -0
  115. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts +37 -0
  116. package/lib/typescript/storage/hooks/useMeasuredWidth.d.ts.map +1 -0
  117. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +8 -1
  118. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -1
  119. package/lib/typescript/storage/hooks/useTreeDraft.d.ts +67 -0
  120. package/lib/typescript/storage/hooks/useTreeDraft.d.ts.map +1 -0
  121. package/lib/typescript/storage/index.d.ts +2 -0
  122. package/lib/typescript/storage/index.d.ts.map +1 -1
  123. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +41 -0
  124. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  125. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts +15 -0
  126. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +1 -1
  127. package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +1 -1
  128. package/lib/typescript/storage/utils/MMKVListener.d.ts +7 -0
  129. package/lib/typescript/storage/utils/MMKVListener.d.ts.map +1 -1
  130. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +20 -0
  131. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -1
  132. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts +27 -0
  133. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +1 -0
  134. package/lib/typescript/storage/utils/index.d.ts +1 -0
  135. package/lib/typescript/storage/utils/index.d.ts.map +1 -1
  136. package/lib/typescript/storage/utils/lineDiff.d.ts +7 -31
  137. package/lib/typescript/storage/utils/lineDiff.d.ts.map +1 -1
  138. package/lib/typescript/storage/utils/setStorageValue.d.ts +64 -0
  139. package/lib/typescript/storage/utils/setStorageValue.d.ts.map +1 -0
  140. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts +28 -0
  141. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +1 -0
  142. package/lib/typescript/storage/utils/storageOps.d.ts +25 -0
  143. package/lib/typescript/storage/utils/storageOps.d.ts.map +1 -0
  144. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts +15 -5
  145. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +1 -1
  146. package/lib/typescript/storage/utils/valueType.d.ts +13 -0
  147. package/lib/typescript/storage/utils/valueType.d.ts.map +1 -1
  148. package/package.json +5 -5
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
4
+ import { StorageTypeFilterBar } from "./StorageTypeFilterBar";
4
5
  import { macOSColors } from "@buoy-gg/shared-ui";
5
6
  import { isMMKVAvailable } from "../utils/mmkvAvailability";
6
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -18,7 +19,8 @@ export function StorageFilterCards({
18
19
  activeFilter = "all",
19
20
  onFilterChange,
20
21
  activeStorageType = "all",
21
- onStorageTypeChange
22
+ onStorageTypeChange,
23
+ showStatus = true
22
24
  }) {
23
25
  // Use tabStats if provided (for filtered counts), otherwise fall back to stats
24
26
  const displayStats = tabStats || {
@@ -32,7 +34,7 @@ export function StorageFilterCards({
32
34
  };
33
35
  return /*#__PURE__*/_jsxs(View, {
34
36
  style: styles.container,
35
- children: [/*#__PURE__*/_jsxs(View, {
37
+ children: [showStatus ? /*#__PURE__*/_jsxs(View, {
36
38
  style: styles.filtersRow,
37
39
  children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
38
40
  style: [styles.filterChip, activeFilter === "all" && [styles.filterChipActive, {
@@ -86,101 +88,15 @@ export function StorageFilterCards({
86
88
  children: "Issues"
87
89
  })]
88
90
  })]
89
- }), onStorageTypeChange && /*#__PURE__*/_jsxs(View, {
90
- style: styles.typesRow,
91
- children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
92
- style: [styles.typePill, {
93
- borderColor: macOSColors.border.default
94
- }, activeStorageType === "all" && [styles.typePillActive, {
95
- backgroundColor: macOSColors.background.hover,
96
- borderColor: macOSColors.border.hover
97
- }]],
98
- onPress: () => onStorageTypeChange?.("all"),
99
- activeOpacity: 0.8,
100
- children: [/*#__PURE__*/_jsx(Text, {
101
- style: [styles.typePillLabel, activeStorageType === "all" && {
102
- color: macOSColors.text.primary,
103
- fontWeight: "600"
104
- }],
105
- children: "All Types"
106
- }), /*#__PURE__*/_jsx(Text, {
107
- style: [styles.typePillValue, activeStorageType === "all" && {
108
- color: macOSColors.text.primary
109
- }, activeStorageType !== "all" && {
110
- color: macOSColors.text.muted
111
- }],
112
- children: displayStats.totalCount
113
- })]
114
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
115
- style: [styles.typePill, {
116
- borderColor: macOSColors.border.default
117
- }, activeStorageType === "async" && [styles.typePillActive, {
118
- backgroundColor: macOSColors.semantic.warning + "15",
119
- borderColor: macOSColors.semantic.warning + "40"
120
- }]],
121
- onPress: () => onStorageTypeChange?.("async"),
122
- activeOpacity: 0.8,
123
- children: [/*#__PURE__*/_jsx(Text, {
124
- style: [styles.typePillLabel, activeStorageType === "async" && {
125
- color: macOSColors.semantic.warning,
126
- fontWeight: "600"
127
- }],
128
- children: "Async"
129
- }), /*#__PURE__*/_jsx(Text, {
130
- style: [styles.typePillValue, activeStorageType === "async" ? {
131
- color: macOSColors.semantic.warning
132
- } : {
133
- color: macOSColors.text.muted
134
- }],
135
- children: displayStats.asyncCount
136
- })]
137
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
138
- style: [styles.typePill, {
139
- borderColor: macOSColors.border.default
140
- }, activeStorageType === "mmkv" && [styles.typePillActive, {
141
- backgroundColor: macOSColors.semantic.info + "15",
142
- borderColor: macOSColors.semantic.info + "40"
143
- }]],
144
- onPress: () => onStorageTypeChange?.("mmkv"),
145
- activeOpacity: 0.8,
146
- children: [/*#__PURE__*/_jsx(Text, {
147
- style: [styles.typePillLabel, activeStorageType === "mmkv" && {
148
- color: macOSColors.semantic.info,
149
- fontWeight: "600"
150
- }],
151
- children: "MMKV"
152
- }), /*#__PURE__*/_jsx(Text, {
153
- style: [styles.typePillValue, activeStorageType === "mmkv" ? {
154
- color: macOSColors.semantic.info
155
- } : {
156
- color: macOSColors.text.muted
157
- }],
158
- children: displayStats.mmkvCount
159
- })]
160
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
161
- style: [styles.typePill, {
162
- borderColor: macOSColors.border.default
163
- }, activeStorageType === "secure" && [styles.typePillActive, {
164
- backgroundColor: macOSColors.semantic.success + "15",
165
- borderColor: macOSColors.semantic.success + "40"
166
- }]],
167
- onPress: () => onStorageTypeChange?.("secure"),
168
- activeOpacity: 0.8,
169
- children: [/*#__PURE__*/_jsx(Text, {
170
- style: [styles.typePillLabel, activeStorageType === "secure" && {
171
- color: macOSColors.semantic.success,
172
- fontWeight: "600"
173
- }],
174
- children: "Secure"
175
- }), /*#__PURE__*/_jsx(Text, {
176
- style: [styles.typePillValue, activeStorageType === "secure" ? {
177
- color: macOSColors.semantic.success
178
- } : {
179
- color: macOSColors.text.muted
180
- }],
181
- children: displayStats.secureCount
182
- })]
183
- })]
91
+ }) : null, onStorageTypeChange && /*#__PURE__*/_jsx(StorageTypeFilterBar, {
92
+ active: activeStorageType,
93
+ onChange: onStorageTypeChange,
94
+ counts: {
95
+ all: displayStats.totalCount,
96
+ async: displayStats.asyncCount,
97
+ mmkv: displayStats.mmkvCount,
98
+ secure: displayStats.secureCount
99
+ }
184
100
  })]
185
101
  });
186
102
  }
@@ -298,43 +214,5 @@ const styles = StyleSheet.create({
298
214
  fontWeight: "600",
299
215
  fontFamily: "monospace",
300
216
  lineHeight: 20
301
- },
302
- // Storage type pills
303
- typesRow: {
304
- flexDirection: "row",
305
- gap: 8,
306
- marginTop: 2
307
- },
308
- typePill: {
309
- flex: 1,
310
- backgroundColor: macOSColors.background.input + "80",
311
- borderRadius: 8,
312
- borderWidth: 1,
313
- borderColor: macOSColors.border.default + "30",
314
- paddingVertical: 6,
315
- paddingHorizontal: 8,
316
- alignItems: "center",
317
- justifyContent: "center",
318
- minHeight: 32
319
- },
320
- typePillActive: {
321
- borderWidth: 1,
322
- transform: [{
323
- scale: 1.005
324
- }]
325
- },
326
- typePillLabel: {
327
- fontSize: 9,
328
- color: macOSColors.text.secondary,
329
- textTransform: "uppercase",
330
- letterSpacing: 0.4,
331
- fontWeight: "500",
332
- marginBottom: 1
333
- },
334
- typePillValue: {
335
- fontSize: 12,
336
- fontWeight: "600",
337
- fontFamily: "monospace",
338
- lineHeight: 14
339
217
  }
340
218
  });
@@ -344,7 +344,7 @@ const styles = StyleSheet.create({
344
344
  gap: 4
345
345
  },
346
346
  storageKeyText: {
347
- color: "#FFFFFF",
347
+ color: macOSColors.text.primary,
348
348
  fontWeight: "500",
349
349
  fontSize: 12,
350
350
  flexWrap: "wrap",
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Editing a key, on a screen of its own.
5
+ *
6
+ * Editing used to happen inside the expanded card in the key list: a tree a few
7
+ * rows tall, an action bar wrapped onto three lines, and the rest of the list
8
+ * still scrolling underneath. Every control was competing for the same ~200px.
9
+ *
10
+ * So the card now offers ONE edit button and the actual work happens here, with
11
+ * the whole viewport: the tree gets the height, the six structural actions get
12
+ * thumb-sized tiles docked at the bottom where they're always in the same
13
+ * place, and manual JSON editing — the escape hatch, not the main path — moves
14
+ * to a single button in the top-right corner.
15
+ *
16
+ * Typing a value happens in a modal pinned to the top of the screen, over the
17
+ * tree rather than instead of it — see {@link StorageValueEditModal}.
18
+ */
19
+
20
+ import { useMemo } from "react";
21
+ import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
22
+ import { macOSColors, FileJson, PillBadge, useSafeAreaInsets, X } from "@buoy-gg/shared-ui";
23
+ import { DataViewer, DataTreeActionDock, getAtPath } from "@buoy-gg/shared-ui/dataViewer";
24
+ import { useKeyEditor } from "../hooks/useKeyEditor";
25
+ import { StorageValueEditModal } from "./StorageValueEditModal";
26
+ import { StorageScalarKeyEditor } from "./StorageScalarKeyEditor";
27
+ import { getStorageTypeLabel } from "../utils/storageQueryUtils";
28
+ import { parseStorageValue } from "../utils/valueType";
29
+
30
+ /** Props once the value has been un-stringified — see `parseStorageValue`. */
31
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
32
+ /**
33
+ * A container and a scalar are different editors, not one editor with a flag.
34
+ *
35
+ * A tree of one row is not a tree, so a scalar key skips the whole structural
36
+ * editor and gets just the value modal. Splitting here keeps each editor's
37
+ * state to what it actually needs — see {@link StorageScalarKeyEditor}.
38
+ */
39
+ export function StorageKeyEditorScreen(props) {
40
+ // Memo justified: this is a JSON.parse of the whole stored value, and the
41
+ // browser underneath re-renders on every storage event the app emits.
42
+ const raw = props.storageKey.value;
43
+ const value = useMemo(() => parseStorageValue(raw), [raw]);
44
+ return value !== null && typeof value === "object" ? /*#__PURE__*/_jsx(StorageTreeEditor, {
45
+ ...props,
46
+ value: value
47
+ }) : /*#__PURE__*/_jsx(StorageScalarKeyEditor, {
48
+ storageKey: props.storageKey,
49
+ value: value,
50
+ onSave: props.onSave,
51
+ onClose: props.onClose
52
+ });
53
+ }
54
+
55
+ /* ------------------------------------------------------------------ *
56
+ * Containers — tree + structural actions, buffered into a draft.
57
+ * ------------------------------------------------------------------ */
58
+
59
+ function StorageTreeEditor({
60
+ storageKey,
61
+ value,
62
+ onSave,
63
+ onClose,
64
+ applySafeAreaInset
65
+ }) {
66
+ const insets = useSafeAreaInsets();
67
+ const bottomInset = applySafeAreaInset ? insets.bottom : 0;
68
+ const editor = useKeyEditor({
69
+ value,
70
+ save: onSave,
71
+ onClose,
72
+ confirmDiscard: choices => Alert.alert("Unsaved changes", `"${storageKey.key}" has edits that haven't been written to the device.`, [{
73
+ text: "Keep editing",
74
+ style: "cancel"
75
+ }, {
76
+ text: "Discard",
77
+ style: "destructive",
78
+ onPress: choices.discard
79
+ }, {
80
+ text: "Save",
81
+ onPress: choices.save
82
+ }])
83
+ });
84
+ const {
85
+ draft,
86
+ selection,
87
+ valuePath
88
+ } = editor;
89
+ return /*#__PURE__*/_jsxs(View, {
90
+ style: styles.screen,
91
+ children: [/*#__PURE__*/_jsx(EditorHeader, {
92
+ storageKey: storageKey,
93
+ state: draft.isSaving ? "Saving…" : draft.isDirty ? "Draft · unsaved" : "In sync with device",
94
+ dirty: draft.isDirty,
95
+ onClose: editor.requestClose
96
+ // Manual editing of the WHOLE value — everything else on this screen is
97
+ // structural, so raw JSON gets a corner rather than a tile.
98
+ ,
99
+ onRaw: editor.openRawEditor
100
+ }), /*#__PURE__*/_jsxs(View, {
101
+ style: styles.metaRow,
102
+ children: [/*#__PURE__*/_jsx(PillBadge, {
103
+ color: storageTypeColor(storageKey.storageType),
104
+ children: getStorageTypeLabel(storageKey.storageType)
105
+ }), /*#__PURE__*/_jsx(Text, {
106
+ style: styles.metaText,
107
+ children: summarize(draft.value)
108
+ })]
109
+ }), /*#__PURE__*/_jsxs(ScrollView, {
110
+ style: styles.treeScroll,
111
+ contentContainerStyle: styles.treeContent,
112
+ keyboardShouldPersistTaps: "handled",
113
+ children: [/*#__PURE__*/_jsx(View, {
114
+ style: styles.treeCard,
115
+ children: /*#__PURE__*/_jsx(DataViewer, {
116
+ data: draft.value,
117
+ title: "",
118
+ rawMode: true,
119
+ initialExpanded: true
120
+ // Type filtering is a READING aid: the filtered view is a
121
+ // projection whose row paths don't address the real document, so
122
+ // the explorer stops reporting selection while one is active. An
123
+ // editor whose actions silently go dead isn't worth the filter.
124
+ ,
125
+ showTypeFilter: false,
126
+ onSelectionChange: editor.setSelection,
127
+ selectPath: draft.selectPath,
128
+ onRequestInlineEdit: sel => editor.openValueEditor(sel.path)
129
+ })
130
+ }), /*#__PURE__*/_jsx(Text, {
131
+ style: styles.treeHint,
132
+ children: "Tap a row to select it \xB7 double-tap to edit its value"
133
+ })]
134
+ }), draft.error ? /*#__PURE__*/_jsx(Text, {
135
+ style: styles.error,
136
+ numberOfLines: 3,
137
+ children: draft.error
138
+ }) : null, /*#__PURE__*/_jsxs(View, {
139
+ style: [styles.footer, {
140
+ paddingBottom: bottomInset
141
+ }],
142
+ children: [/*#__PURE__*/_jsx(DataTreeActionDock, {
143
+ root: draft.value,
144
+ selection: selection,
145
+ onEdit: draft.apply,
146
+ onSelectPath: draft.selectAt
147
+ // Booleans never reach this — the dock applies their flip directly.
148
+ ,
149
+ onEditValue: () => editor.openValueEditor(selection?.path ?? [])
150
+ }), draft.isDirty ? /*#__PURE__*/_jsxs(View, {
151
+ style: styles.commitRow,
152
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
153
+ onPress: draft.discard,
154
+ disabled: draft.isSaving,
155
+ accessibilityLabel: "Discard changes",
156
+ style: styles.discardButton,
157
+ children: /*#__PURE__*/_jsx(Text, {
158
+ style: styles.discardText,
159
+ children: "Discard"
160
+ })
161
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
162
+ onPress: editor.saveAndClose,
163
+ disabled: draft.isSaving,
164
+ accessibilityLabel: "Save changes",
165
+ style: [styles.saveButton, draft.isSaving && styles.saveDisabled],
166
+ children: /*#__PURE__*/_jsx(Text, {
167
+ style: styles.saveText,
168
+ children: draft.isSaving ? "Saving…" : "Save changes"
169
+ })
170
+ })]
171
+ }) : null]
172
+ }), valuePath ? /*#__PURE__*/_jsx(StorageValueEditModal, {
173
+ path: valuePath,
174
+ value: getAtPath(draft.value, valuePath),
175
+ onSave: editor.applyValue,
176
+ onCancel: editor.closeValueEditor
177
+ // It only reaches the draft here — the single write happens on Save.
178
+ ,
179
+ saveLabel: "Apply"
180
+ }) : null]
181
+ });
182
+ }
183
+
184
+ /* ------------------------------------------------------------------ *
185
+ * Shared pieces
186
+ * ------------------------------------------------------------------ */
187
+
188
+ function EditorHeader({
189
+ storageKey,
190
+ state,
191
+ dirty,
192
+ onClose,
193
+ onRaw
194
+ }) {
195
+ return /*#__PURE__*/_jsxs(View, {
196
+ style: styles.header,
197
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
198
+ onPress: onClose,
199
+ accessibilityLabel: "Close editor",
200
+ style: styles.headerButton,
201
+ hitSlop: {
202
+ top: 8,
203
+ bottom: 8,
204
+ left: 8,
205
+ right: 8
206
+ },
207
+ children: /*#__PURE__*/_jsx(X, {
208
+ size: 16,
209
+ color: macOSColors.text.secondary
210
+ })
211
+ }), /*#__PURE__*/_jsxs(View, {
212
+ style: styles.headerTitle,
213
+ children: [/*#__PURE__*/_jsx(Text, {
214
+ style: styles.headerKey,
215
+ numberOfLines: 1,
216
+ children: storageKey.key
217
+ }), /*#__PURE__*/_jsx(Text, {
218
+ style: [styles.headerState, dirty && styles.headerDirty],
219
+ children: state
220
+ })]
221
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
222
+ onPress: onRaw,
223
+ accessibilityLabel: "Edit raw JSON",
224
+ style: styles.headerButton,
225
+ hitSlop: {
226
+ top: 8,
227
+ bottom: 8,
228
+ left: 8,
229
+ right: 8
230
+ },
231
+ children: [/*#__PURE__*/_jsx(FileJson, {
232
+ size: 16,
233
+ color: macOSColors.semantic.info
234
+ }), /*#__PURE__*/_jsx(Text, {
235
+ style: styles.headerRawText,
236
+ children: "Raw"
237
+ })]
238
+ })]
239
+ });
240
+ }
241
+
242
+ /** `array · 3 items` — what the header says the draft currently is. */
243
+ function summarize(value) {
244
+ if (Array.isArray(value)) {
245
+ return `array · ${value.length} item${value.length === 1 ? "" : "s"}`;
246
+ }
247
+ if (value !== null && typeof value === "object") {
248
+ const n = Object.keys(value).length;
249
+ return `object · ${n} key${n === 1 ? "" : "s"}`;
250
+ }
251
+ return typeof value;
252
+ }
253
+ function storageTypeColor(storageType) {
254
+ switch (storageType) {
255
+ case "mmkv":
256
+ return macOSColors.semantic.info;
257
+ case "async":
258
+ return macOSColors.semantic.warning;
259
+ case "secure":
260
+ return macOSColors.semantic.success;
261
+ default:
262
+ return macOSColors.text.secondary;
263
+ }
264
+ }
265
+ const styles = StyleSheet.create({
266
+ screen: {
267
+ flex: 1,
268
+ backgroundColor: macOSColors.background.base
269
+ },
270
+ header: {
271
+ flexDirection: "row",
272
+ alignItems: "center",
273
+ gap: 8,
274
+ paddingHorizontal: 10,
275
+ paddingVertical: 8,
276
+ borderBottomWidth: 1,
277
+ borderBottomColor: macOSColors.border.default
278
+ },
279
+ headerButton: {
280
+ flexDirection: "row",
281
+ alignItems: "center",
282
+ gap: 4,
283
+ paddingVertical: 6,
284
+ paddingHorizontal: 4
285
+ },
286
+ headerRawText: {
287
+ color: macOSColors.semantic.info,
288
+ fontSize: 12,
289
+ fontWeight: "700"
290
+ },
291
+ headerTitle: {
292
+ flex: 1,
293
+ alignItems: "center",
294
+ gap: 2
295
+ },
296
+ headerKey: {
297
+ color: macOSColors.text.primary,
298
+ fontSize: 13,
299
+ fontWeight: "600",
300
+ fontFamily: "monospace"
301
+ },
302
+ headerState: {
303
+ color: macOSColors.text.muted,
304
+ fontSize: 10
305
+ },
306
+ headerDirty: {
307
+ color: macOSColors.semantic.warning
308
+ },
309
+ metaRow: {
310
+ flexDirection: "row",
311
+ alignItems: "center",
312
+ gap: 8,
313
+ paddingHorizontal: 12,
314
+ paddingVertical: 8
315
+ },
316
+ metaText: {
317
+ color: macOSColors.text.muted,
318
+ fontSize: 11,
319
+ fontFamily: "monospace"
320
+ },
321
+ treeScroll: {
322
+ flex: 1
323
+ },
324
+ treeContent: {
325
+ paddingHorizontal: 12,
326
+ paddingBottom: 16
327
+ },
328
+ treeCard: {
329
+ backgroundColor: macOSColors.background.card,
330
+ borderRadius: 10,
331
+ borderWidth: 1,
332
+ borderColor: macOSColors.border.default,
333
+ padding: 6
334
+ },
335
+ treeHint: {
336
+ color: macOSColors.text.muted,
337
+ fontSize: 10,
338
+ paddingTop: 8,
339
+ paddingHorizontal: 4
340
+ },
341
+ error: {
342
+ color: macOSColors.semantic.error,
343
+ fontSize: 11,
344
+ fontFamily: "monospace",
345
+ paddingHorizontal: 12,
346
+ paddingBottom: 6
347
+ },
348
+ footer: {
349
+ backgroundColor: macOSColors.background.card
350
+ },
351
+ commitRow: {
352
+ flexDirection: "row",
353
+ gap: 8,
354
+ paddingHorizontal: 10,
355
+ paddingBottom: 12,
356
+ paddingTop: 2
357
+ },
358
+ discardButton: {
359
+ flex: 1,
360
+ minHeight: 46,
361
+ alignItems: "center",
362
+ justifyContent: "center",
363
+ borderRadius: 12,
364
+ borderWidth: 1,
365
+ borderColor: macOSColors.border.default
366
+ },
367
+ discardText: {
368
+ color: macOSColors.text.secondary,
369
+ fontSize: 14,
370
+ fontWeight: "600"
371
+ },
372
+ saveButton: {
373
+ flex: 1.6,
374
+ minHeight: 46,
375
+ alignItems: "center",
376
+ justifyContent: "center",
377
+ borderRadius: 12,
378
+ backgroundColor: macOSColors.semantic.success
379
+ },
380
+ saveDisabled: {
381
+ opacity: 0.5
382
+ },
383
+ saveText: {
384
+ color: macOSColors.background.base,
385
+ fontSize: 15,
386
+ fontWeight: "700"
387
+ }
388
+ });
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { memo } from "react";
4
4
  import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
5
- import { macOSColors, CompactRow, TypeBadge, HardDrive, Square, CheckSquare, EyeOff, Pin, ExpandedInfoRow, PillBadge } from "@buoy-gg/shared-ui";
5
+ import { macOSColors, CompactRow, TypeBadge, HardDrive, Square, CheckSquare, Edit3, EyeOff, Pin, ExpandedInfoRow, PillBadge } from "@buoy-gg/shared-ui";
6
6
  import { getStorageTypeLabel } from "../utils/storageQueryUtils";
7
- import { getValueTypeLabel, getValueTypeWithPreview } from "../utils/valueType";
7
+ import { getValueTypeLabel, getValueTypeWithPreview, parseStorageValue } from "../utils/valueType";
8
8
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
9
9
 
10
10
  // MMKV Instance color palette - consistent colors per instance
@@ -86,8 +86,12 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
86
86
  onViewHistory,
87
87
  onHideKey,
88
88
  isPinned = false,
89
- onTogglePin
89
+ onTogglePin,
90
+ onEditKey,
91
+ onToggleKey,
92
+ editBlockedReason
90
93
  }) {
94
+ const canEdit = onEditKey != null && editBlockedReason == null;
91
95
  const config = getStatusConfig(storageKey.status);
92
96
  const hasValue = storageKey.value !== undefined && storageKey.value !== null;
93
97
 
@@ -101,18 +105,12 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
101
105
  // Show storage type as secondary text
102
106
  const storageTypeLabel = getStorageTypeLabel(storageKey.storageType);
103
107
 
104
- // Check if value is JSON object/array for DataViewer
105
- const isJsonData = storageKey.value && (typeof storageKey.value === "object" || typeof storageKey.value === "string" && (storageKey.value.startsWith("{") || storageKey.value.startsWith("[")));
108
+ // Shared with the editor screen, so the card and the thing it opens can't
109
+ // disagree about whether a key holds a container.
110
+ const parsedValue = parseStorageValue(storageKey.value);
106
111
 
107
- // Parse JSON string if needed
108
- let parsedValue = storageKey.value;
109
- if (typeof storageKey.value === "string" && isJsonData) {
110
- try {
111
- parsedValue = JSON.parse(storageKey.value);
112
- } catch {
113
- // Keep original if parse fails
114
- }
115
- }
112
+ /** The tree only renders for containers. */
113
+ const showsTree = parsedValue !== null && typeof parsedValue === "object";
116
114
 
117
115
  // Create expanded content for value and storage details
118
116
  const expandedContent = /*#__PURE__*/_jsxs(View, {
@@ -152,7 +150,7 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
152
150
  children: "view history \u2192"
153
151
  })
154
152
  })]
155
- }), isJsonData && typeof parsedValue === "object" ? /*#__PURE__*/_jsx(View, {
153
+ }), showsTree ? /*#__PURE__*/_jsx(View, {
156
154
  style: styles.dataViewerContainer,
157
155
  children: /*#__PURE__*/_jsx(DataViewer, {
158
156
  data: parsedValue,
@@ -207,9 +205,34 @@ export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
207
205
  style: styles.expandedDescription,
208
206
  children: storageKey.description
209
207
  })]
210
- }) : null, onTogglePin || onHideKey ? /*#__PURE__*/_jsxs(View, {
208
+ }) : null, editBlockedReason ? /*#__PURE__*/_jsxs(View, {
209
+ style: styles.expandedRow,
210
+ children: [/*#__PURE__*/_jsx(Text, {
211
+ style: styles.expandedLabel,
212
+ children: "Edit:"
213
+ }), /*#__PURE__*/_jsx(Text, {
214
+ style: styles.expandedDescription,
215
+ children: editBlockedReason
216
+ })]
217
+ }) : null, onTogglePin || onHideKey || canEdit ? /*#__PURE__*/_jsxs(View, {
211
218
  style: styles.actionRow,
212
- children: [onTogglePin ? /*#__PURE__*/_jsxs(TouchableOpacity, {
219
+ children: [canEdit ? /*#__PURE__*/_jsxs(TouchableOpacity, {
220
+ style: styles.editButton,
221
+ onPress: () => isBoolean && onToggleKey ? onToggleKey(storageKey) : onEditKey?.(storageKey),
222
+ hitSlop: {
223
+ top: 6,
224
+ bottom: 6,
225
+ left: 6,
226
+ right: 6
227
+ },
228
+ children: [/*#__PURE__*/_jsx(Edit3, {
229
+ size: 13,
230
+ color: macOSColors.background.base
231
+ }), /*#__PURE__*/_jsx(Text, {
232
+ style: styles.editButtonText,
233
+ children: isBoolean && onToggleKey ? "Toggle" : "Edit"
234
+ })]
235
+ }) : null, onTogglePin ? /*#__PURE__*/_jsxs(TouchableOpacity, {
213
236
  style: [styles.actionChip, isPinned && styles.actionChipActive],
214
237
  onPress: () => onTogglePin(storageKey.key),
215
238
  hitSlop: {
@@ -431,6 +454,21 @@ const styles = StyleSheet.create({
431
454
  backgroundColor: macOSColors.semantic.info + "15",
432
455
  borderColor: macOSColors.semantic.info + "44"
433
456
  },
457
+ /** Filled, and first — the one action in this row that changes anything. */
458
+ editButton: {
459
+ flexDirection: "row",
460
+ alignItems: "center",
461
+ gap: 6,
462
+ paddingVertical: 8,
463
+ paddingHorizontal: 14,
464
+ borderRadius: 8,
465
+ backgroundColor: macOSColors.semantic.success
466
+ },
467
+ editButtonText: {
468
+ color: macOSColors.background.base,
469
+ fontSize: 12,
470
+ fontWeight: "700"
471
+ },
434
472
  actionChipText: {
435
473
  fontSize: 11,
436
474
  fontWeight: "600",