@buoy-gg/shared-ui 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 (129) hide show
  1. package/lib/commonjs/JsModal.js +18 -2
  2. package/lib/commonjs/JsModal.web.js +7 -4
  3. package/lib/commonjs/dataViewer/DataTreeActionDock.js +4 -2
  4. package/lib/commonjs/dataViewer/LiveExplorer.js +930 -0
  5. package/lib/commonjs/dataViewer/ValueEditModal.js +385 -0
  6. package/lib/commonjs/dataViewer/index.js +47 -0
  7. package/lib/commonjs/dataViewer/nestedPath.js +115 -0
  8. package/lib/commonjs/dataViewer/treeActions.js +3 -2
  9. package/lib/commonjs/dataViewer/useTreeDraft.js +125 -0
  10. package/lib/commonjs/dataViewer/useTreeEditor.js +105 -0
  11. package/lib/commonjs/env/EnvironmentIndicator.js +7 -1
  12. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  13. package/lib/commonjs/index.js +24 -0
  14. package/lib/commonjs/license/tierLimits.js +14 -0
  15. package/lib/commonjs/storage/devToolsStorageKeys.js +36 -0
  16. package/lib/commonjs/sync/toolActionRegistry.js +166 -0
  17. package/lib/commonjs/sync/toolUiBridge.js +74 -0
  18. package/lib/commonjs/ui/components/CompactRow.js +4 -0
  19. package/lib/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
  20. package/lib/commonjs/ui/components/ExpandedInfoRow.js +3 -1
  21. package/lib/commonjs/ui/components/ModalHeader.js +7 -4
  22. package/lib/commonjs/ui/components/SearchBar.js +5 -109
  23. package/lib/commonjs/ui/components/ToolButton.js +5 -2
  24. package/lib/module/JsModal.js +18 -2
  25. package/lib/module/JsModal.web.js +7 -4
  26. package/lib/module/dataViewer/DataTreeActionDock.js +4 -2
  27. package/lib/module/dataViewer/LiveExplorer.js +925 -0
  28. package/lib/module/dataViewer/ValueEditModal.js +381 -0
  29. package/lib/module/dataViewer/index.js +10 -1
  30. package/lib/module/dataViewer/nestedPath.js +109 -0
  31. package/lib/module/dataViewer/treeActions.js +3 -2
  32. package/lib/module/dataViewer/useTreeDraft.js +121 -0
  33. package/lib/module/dataViewer/useTreeEditor.js +101 -0
  34. package/lib/module/env/EnvironmentIndicator.js +7 -1
  35. package/lib/module/hooks/safe-area-impl.js +1 -1
  36. package/lib/module/index.js +2 -0
  37. package/lib/module/license/tierLimits.js +14 -0
  38. package/lib/module/storage/devToolsStorageKeys.js +36 -0
  39. package/lib/module/sync/toolActionRegistry.js +154 -0
  40. package/lib/module/sync/toolUiBridge.js +68 -0
  41. package/lib/module/ui/components/CompactRow.js +4 -0
  42. package/lib/module/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
  43. package/lib/module/ui/components/ExpandedInfoRow.js +3 -1
  44. package/lib/module/ui/components/ModalHeader.js +7 -4
  45. package/lib/module/ui/components/SearchBar.js +8 -112
  46. package/lib/module/ui/components/ToolButton.js +5 -2
  47. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts +6 -1
  50. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts +73 -0
  52. package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts +40 -0
  54. package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/dataViewer/index.d.ts +5 -0
  56. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts +20 -0
  58. package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts +10 -0
  60. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts +81 -0
  62. package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts +52 -0
  64. package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/env/EnvironmentIndicator.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  67. package/lib/typescript/commonjs/index.d.ts +2 -0
  68. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/license/tierLimits.d.ts +10 -0
  70. package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +36 -0
  72. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts +85 -0
  74. package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +39 -0
  76. package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +5 -1
  78. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts +6 -0
  81. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts +7 -1
  83. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts +1 -3
  85. package/lib/typescript/commonjs/ui/components/SearchBar.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/components/ToolButton.d.ts +5 -1
  87. package/lib/typescript/commonjs/ui/components/ToolButton.d.ts.map +1 -1
  88. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  89. package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
  90. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts +6 -1
  91. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
  92. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts +73 -0
  93. package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -0
  94. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts +40 -0
  95. package/lib/typescript/module/dataViewer/ValueEditModal.d.ts.map +1 -0
  96. package/lib/typescript/module/dataViewer/index.d.ts +5 -0
  97. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  98. package/lib/typescript/module/dataViewer/nestedPath.d.ts +20 -0
  99. package/lib/typescript/module/dataViewer/nestedPath.d.ts.map +1 -0
  100. package/lib/typescript/module/dataViewer/treeActions.d.ts +10 -0
  101. package/lib/typescript/module/dataViewer/treeActions.d.ts.map +1 -1
  102. package/lib/typescript/module/dataViewer/useTreeDraft.d.ts +81 -0
  103. package/lib/typescript/module/dataViewer/useTreeDraft.d.ts.map +1 -0
  104. package/lib/typescript/module/dataViewer/useTreeEditor.d.ts +52 -0
  105. package/lib/typescript/module/dataViewer/useTreeEditor.d.ts.map +1 -0
  106. package/lib/typescript/module/env/EnvironmentIndicator.d.ts.map +1 -1
  107. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  108. package/lib/typescript/module/index.d.ts +2 -0
  109. package/lib/typescript/module/index.d.ts.map +1 -1
  110. package/lib/typescript/module/license/tierLimits.d.ts +10 -0
  111. package/lib/typescript/module/license/tierLimits.d.ts.map +1 -1
  112. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +36 -0
  113. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  114. package/lib/typescript/module/sync/toolActionRegistry.d.ts +85 -0
  115. package/lib/typescript/module/sync/toolActionRegistry.d.ts.map +1 -0
  116. package/lib/typescript/module/sync/toolUiBridge.d.ts +39 -0
  117. package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -0
  118. package/lib/typescript/module/ui/components/CompactRow.d.ts +5 -1
  119. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts +6 -0
  122. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts +7 -1
  124. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/components/SearchBar.d.ts +1 -3
  126. package/lib/typescript/module/ui/components/SearchBar.d.ts.map +1 -1
  127. package/lib/typescript/module/ui/components/ToolButton.d.ts +5 -1
  128. package/lib/typescript/module/ui/components/ToolButton.d.ts.map +1 -1
  129. package/package.json +4 -4
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Editing one value: a panel at the top of the screen, with Cancel and Save.
5
+ *
6
+ * This started as a pushed full screen and that was more ceremony than the task
7
+ * deserves — changing one string is the most ordinary thing in these tools, and
8
+ * it shouldn't cost a navigation. A modal is what people expect, and the reason
9
+ * it's pinned to the TOP rather than centred is the keyboard: a centred panel
10
+ * gets shoved up or covered the moment the field focuses, and a bottom sheet is
11
+ * simply underneath it. At the top it stays put and stays readable.
12
+ *
13
+ * The field is multiline for anything textual. Stored values are routinely
14
+ * tokens, URLs and serialized blobs, and a single line shows you roughly forty
15
+ * characters of them, scrolling sideways — which is how you edit the wrong part
16
+ * of a string without noticing.
17
+ *
18
+ * Type is preserved by construction: the ORIGINAL value's type picks the editor
19
+ * AND decides how the draft is read back, so editing a number can only produce
20
+ * a number. Raw JSON — the escape hatch for containers — re-checks the parsed
21
+ * result and refuses a different shape.
22
+ *
23
+ * Nothing here writes. `onSave` hands the value to whoever owns it.
24
+ *
25
+ * Hoisted from the Storage tool (which keeps its testIDs via `testIDPrefix`)
26
+ * so the Zustand store editor types values through the same rules.
27
+ */
28
+
29
+ import { useState } from "react";
30
+ import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
31
+ import { absoluteFill } from "../utils/absoluteFill.js";
32
+ import { macOSColors } from "../ui/gameUI/index.js";
33
+ import { night } from "../ui/night/nightTheme.js";
34
+ import { breadcrumb, typeLabel } from "./treeActions.js";
35
+
36
+ /**
37
+ * How this value gets edited. Driven by the value's own type, which is also
38
+ * what makes the result type-safe without asking the user to declare anything.
39
+ */
40
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
41
+ function modeFor(value) {
42
+ if (typeof value === "boolean") return "boolean";
43
+ if (typeof value === "number") return "number";
44
+ if (value !== null && typeof value === "object") return "raw";
45
+ return "text";
46
+ }
47
+ function initialText(value, mode) {
48
+ if (mode === "raw") return JSON.stringify(value, null, 2);
49
+ if (value === null || value === undefined) return "";
50
+ return String(value);
51
+ }
52
+ function hintFor(mode, type) {
53
+ switch (mode) {
54
+ case "number":
55
+ return "Numeric only — anything else is refused, so this stays a number.";
56
+ case "raw":
57
+ return `Raw JSON. Must stay ${type === "array" ? "an array" : "an object"} — a different shape is refused.`;
58
+ default:
59
+ return "Saves as a string. The type never changes here.";
60
+ }
61
+ }
62
+ export function ValueEditModal({
63
+ path,
64
+ value,
65
+ onSave,
66
+ onCancel,
67
+ saveLabel = "Save",
68
+ busy = false,
69
+ testIDPrefix = "buoy-data-value"
70
+ }) {
71
+ const mode = modeFor(value);
72
+ const type = typeLabel(value);
73
+ const [text, setText] = useState(() => initialText(value, mode));
74
+
75
+ /**
76
+ * Advisory only — {@link commit} re-derives it, so a disabled button and a
77
+ * refused save can never disagree about what's valid.
78
+ */
79
+ const error = validate(text, mode, type);
80
+ const blocked = busy || error != null;
81
+ const commit = () => {
82
+ if (busy || validate(text, mode, type) != null) return;
83
+ onSave(parse(text, mode, value));
84
+ };
85
+ return /*#__PURE__*/_jsxs(View, {
86
+ style: styles.overlay,
87
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
88
+ style: styles.scrim,
89
+ activeOpacity: 1,
90
+ onPress: busy ? undefined : onCancel,
91
+ testID: `${testIDPrefix}-scrim`,
92
+ accessibilityRole: "button",
93
+ accessibilityLabel: "Dismiss editor"
94
+ }), /*#__PURE__*/_jsx(View, {
95
+ style: styles.panel,
96
+ pointerEvents: "box-none",
97
+ children: /*#__PURE__*/_jsxs(View, {
98
+ style: styles.card,
99
+ children: [/*#__PURE__*/_jsxs(View, {
100
+ style: styles.headerRow,
101
+ children: [/*#__PURE__*/_jsx(Text, {
102
+ style: styles.title,
103
+ children: "Edit value"
104
+ }), /*#__PURE__*/_jsx(View, {
105
+ style: styles.typeBadge,
106
+ children: /*#__PURE__*/_jsx(Text, {
107
+ style: styles.typeBadgeText,
108
+ children: type
109
+ })
110
+ })]
111
+ }), /*#__PURE__*/_jsx(Text, {
112
+ style: styles.path,
113
+ numberOfLines: 1,
114
+ children: breadcrumb(path)
115
+ }), mode === "boolean" ? /*#__PURE__*/_jsx(BooleanChoice, {
116
+ current: value === true,
117
+ onPick: onSave,
118
+ testIDPrefix: testIDPrefix
119
+ }) : /*#__PURE__*/_jsx(ScrollView, {
120
+ style: styles.fieldScroll,
121
+ keyboardShouldPersistTaps: "handled",
122
+ children: /*#__PURE__*/_jsx(TextInput, {
123
+ testID: `${testIDPrefix}-input`,
124
+ accessibilityLabel: "Value",
125
+ value: text,
126
+ onChangeText: setText,
127
+ autoFocus: true,
128
+ autoCapitalize: "none",
129
+ autoCorrect: false,
130
+ spellCheck: false,
131
+ editable: !busy
132
+ // Multiline for anything textual: stored values are long more
133
+ // often than they're short, and a single line hides that.
134
+ ,
135
+ multiline: mode !== "number"
136
+ // ASCII for raw JSON, because iOS Smart Punctuation is ON by
137
+ // default and rewrites the quotes you type as curly ones —
138
+ // which makes the text unparseable, disables Save, and leaves
139
+ // an "invalid JSON" message whose cause is invisible at a
140
+ // glance. An ASCII keyboard doesn't do the substitution.
141
+ ,
142
+ keyboardType: mode === "number" ? "numeric" : mode === "raw" ? "ascii-capable" : "default",
143
+ onSubmitEditing: mode === "number" ? commit : undefined,
144
+ placeholder: mode === "raw" ? "{ }" : "value",
145
+ placeholderTextColor: night.color.placeholder,
146
+ style: [styles.input, mode === "raw" && styles.inputRaw, mode !== "number" && styles.inputMultiline, error != null && styles.inputInvalid]
147
+ })
148
+ }), /*#__PURE__*/_jsx(Text, {
149
+ style: error != null ? styles.error : styles.hint,
150
+ children: error ?? hintFor(mode, type)
151
+ }), /*#__PURE__*/_jsxs(View, {
152
+ style: styles.actions,
153
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
154
+ onPress: onCancel,
155
+ disabled: busy,
156
+ testID: `${testIDPrefix}-cancel`,
157
+ accessibilityRole: "button",
158
+ accessibilityLabel: "Cancel edit",
159
+ style: styles.cancelButton,
160
+ children: /*#__PURE__*/_jsx(Text, {
161
+ style: styles.cancelText,
162
+ children: "Cancel"
163
+ })
164
+ }), mode === "boolean" ? null : /*#__PURE__*/_jsx(TouchableOpacity, {
165
+ onPress: commit,
166
+ disabled: blocked,
167
+ testID: `${testIDPrefix}-save`,
168
+ accessibilityRole: "button",
169
+ accessibilityLabel: saveLabel,
170
+ style: [styles.saveButton, blocked && styles.saveDisabled],
171
+ children: /*#__PURE__*/_jsx(Text, {
172
+ style: styles.saveText,
173
+ children: busy ? "Saving…" : saveLabel
174
+ })
175
+ })]
176
+ })]
177
+ })
178
+ })]
179
+ });
180
+ }
181
+ function BooleanChoice({
182
+ current,
183
+ onPick,
184
+ testIDPrefix
185
+ }) {
186
+ return /*#__PURE__*/_jsx(View, {
187
+ style: styles.boolRow,
188
+ children: [true, false].map(option => /*#__PURE__*/_jsx(TouchableOpacity, {
189
+ onPress: () => onPick(option),
190
+ testID: `${testIDPrefix}-${option}`,
191
+ accessibilityRole: "button",
192
+ accessibilityLabel: `Set ${option}`,
193
+ style: [styles.boolButton, current === option && styles.boolActive],
194
+ children: /*#__PURE__*/_jsx(Text, {
195
+ style: [styles.boolText, current === option && styles.boolTextActive],
196
+ children: String(option)
197
+ })
198
+ }, String(option)))
199
+ });
200
+ }
201
+
202
+ /** Null when the draft is applicable, else why it isn't. */
203
+ function validate(text, mode, type) {
204
+ if (mode === "number") {
205
+ if (text.trim() === "") return "Enter a number.";
206
+ if (Number.isNaN(Number(text))) return `"${text}" isn't a number.`;
207
+ return null;
208
+ }
209
+ if (mode === "raw") {
210
+ let parsed;
211
+ try {
212
+ parsed = JSON.parse(text);
213
+ } catch (e) {
214
+ return e instanceof Error ? e.message : "Invalid JSON.";
215
+ }
216
+ if (typeLabel(parsed) !== type) {
217
+ return `Must stay ${type} — that parses as ${typeLabel(parsed)}.`;
218
+ }
219
+ return null;
220
+ }
221
+ return null;
222
+ }
223
+ function parse(text, mode, original) {
224
+ if (mode === "number") return Number(text);
225
+ if (mode === "raw") return JSON.parse(text);
226
+ // An emptied field on a null keeps it null rather than silently minting an
227
+ // empty string, which would change the type this promised to preserve.
228
+ if (original === null && text === "") return null;
229
+ return text;
230
+ }
231
+ const styles = StyleSheet.create({
232
+ overlay: {
233
+ ...absoluteFill,
234
+ zIndex: 20
235
+ },
236
+ scrim: {
237
+ ...absoluteFill,
238
+ backgroundColor: "rgba(0,0,0,0.55)"
239
+ },
240
+ /** Pinned to the top so the keyboard can't reach it. */
241
+ panel: {
242
+ padding: 12
243
+ },
244
+ card: {
245
+ backgroundColor: night.color.surface,
246
+ borderRadius: 14,
247
+ borderWidth: 1,
248
+ borderColor: night.color.border,
249
+ padding: 14,
250
+ gap: 8
251
+ },
252
+ headerRow: {
253
+ flexDirection: "row",
254
+ alignItems: "center",
255
+ gap: 8
256
+ },
257
+ title: {
258
+ flex: 1,
259
+ color: night.color.text,
260
+ fontSize: 14,
261
+ fontWeight: "700"
262
+ },
263
+ typeBadge: {
264
+ paddingHorizontal: 8,
265
+ paddingVertical: 2,
266
+ borderRadius: 6,
267
+ backgroundColor: night.color.surfaceElevated
268
+ },
269
+ typeBadgeText: {
270
+ color: macOSColors.semantic.warning,
271
+ fontSize: 10,
272
+ fontWeight: "700",
273
+ fontFamily: "monospace"
274
+ },
275
+ path: {
276
+ color: night.color.textTertiary,
277
+ fontSize: 11,
278
+ fontFamily: "monospace"
279
+ },
280
+ /** Caps the panel's height so a huge blob can't push the buttons off-screen. */
281
+ fieldScroll: {
282
+ maxHeight: 240
283
+ },
284
+ input: {
285
+ color: night.color.text,
286
+ backgroundColor: night.color.surfaceElevated,
287
+ borderWidth: 1,
288
+ borderColor: night.color.border,
289
+ borderRadius: 10,
290
+ paddingHorizontal: 12,
291
+ paddingVertical: 10,
292
+ fontSize: 14,
293
+ fontFamily: "monospace"
294
+ },
295
+ inputMultiline: {
296
+ minHeight: 110,
297
+ lineHeight: 20,
298
+ textAlignVertical: "top"
299
+ },
300
+ inputRaw: {
301
+ minHeight: 180,
302
+ fontSize: 12,
303
+ lineHeight: 18
304
+ },
305
+ inputInvalid: {
306
+ borderColor: night.color.danger
307
+ },
308
+ hint: {
309
+ color: night.color.textTertiary,
310
+ fontSize: 11,
311
+ lineHeight: 16
312
+ },
313
+ error: {
314
+ color: night.color.danger,
315
+ fontSize: 11,
316
+ lineHeight: 16,
317
+ fontFamily: "monospace"
318
+ },
319
+ actions: {
320
+ flexDirection: "row",
321
+ gap: 8,
322
+ paddingTop: 2
323
+ },
324
+ cancelButton: {
325
+ flex: 1,
326
+ minHeight: 44,
327
+ alignItems: "center",
328
+ justifyContent: "center",
329
+ borderRadius: 10,
330
+ borderWidth: 1,
331
+ borderColor: night.color.border
332
+ },
333
+ cancelText: {
334
+ color: night.color.textSecondary,
335
+ fontSize: 14,
336
+ fontWeight: "600"
337
+ },
338
+ saveButton: {
339
+ flex: 1.4,
340
+ minHeight: 44,
341
+ alignItems: "center",
342
+ justifyContent: "center",
343
+ borderRadius: 10,
344
+ backgroundColor: night.color.accent
345
+ },
346
+ saveDisabled: {
347
+ opacity: 0.35
348
+ },
349
+ saveText: {
350
+ color: night.color.onAccent,
351
+ fontSize: 15,
352
+ fontWeight: "700"
353
+ },
354
+ boolRow: {
355
+ flexDirection: "row",
356
+ gap: 10
357
+ },
358
+ boolButton: {
359
+ flex: 1,
360
+ minHeight: 64,
361
+ alignItems: "center",
362
+ justifyContent: "center",
363
+ borderRadius: 12,
364
+ borderWidth: 1,
365
+ borderColor: night.color.border,
366
+ backgroundColor: night.color.surfaceElevated
367
+ },
368
+ boolActive: {
369
+ borderColor: macOSColors.semantic.warning,
370
+ backgroundColor: macOSColors.semantic.warning + "18"
371
+ },
372
+ boolText: {
373
+ color: night.color.textTertiary,
374
+ fontSize: 16,
375
+ fontWeight: "700",
376
+ fontFamily: "monospace"
377
+ },
378
+ boolTextActive: {
379
+ color: macOSColors.semantic.warning
380
+ }
381
+ });
@@ -45,4 +45,13 @@ export { diffThemes, gitClassicTheme, devToolsDefaultTheme } from "./diffThemes.
45
45
  export { objectDiff, boundedDeepEqual, toDiffable, jsonLikelyLargerThan } from "./objectDiff.js";
46
46
 
47
47
  // The production split renderer (folding + virtualization), promoted from storage
48
- export { ThemedSplitView } from "./ThemedSplitView.js";
48
+ export { ThemedSplitView } from "./ThemedSplitView.js";
49
+
50
+ // The buffered tree editor (draft + rules + value modal), promoted from storage
51
+ export { useTreeDraft } from "./useTreeDraft.js";
52
+ export { useTreeEditor } from "./useTreeEditor.js";
53
+ export { ValueEditModal } from "./ValueEditModal.js";
54
+
55
+ // The LIVE data editor (no draft, no Save), hoisted from react-query
56
+ export { LiveExplorer, makeRootWriter } from "./LiveExplorer.js";
57
+ export { updateNestedDataByPath, deleteNestedDataByPath } from "./nestedPath.js";
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Immutable path writes over arbitrary JSON-ish data — the write kernel the
5
+ * live explorer edits through.
6
+ *
7
+ * Hoisted verbatim from @buoy-gg/react-query (utils/updateNestedDataByPath +
8
+ * utils/deleteNestedDataByPath), where the RQ Data Editor has run on them in
9
+ * production; react-query re-exports from here. Handles objects, arrays, and
10
+ * Map/Set (a Set edit round-trips through an array).
11
+ */
12
+
13
+ /**
14
+ * Returns a copy of `oldData` with the node at `updatePath` replaced by
15
+ * `value`. An empty path replaces the whole document.
16
+ */
17
+ export const updateNestedDataByPath = (oldData, updatePath, value) => {
18
+ if (updatePath.length === 0) {
19
+ return value;
20
+ }
21
+ if (oldData instanceof Map) {
22
+ const newData = new Map(oldData);
23
+ if (updatePath.length === 1) {
24
+ newData.set(updatePath[0], value);
25
+ return newData;
26
+ }
27
+ const [head, ...tail] = updatePath;
28
+ const currentValue = newData.get(head);
29
+ newData.set(head, updateNestedDataByPath(currentValue ?? null, tail, value));
30
+ return newData;
31
+ }
32
+ if (oldData instanceof Set) {
33
+ const setAsArray = updateNestedDataByPath(Array.from(oldData), updatePath, value);
34
+ return new Set(Array.isArray(setAsArray) ? setAsArray : []);
35
+ }
36
+ if (Array.isArray(oldData)) {
37
+ const newData = [...oldData];
38
+ if (updatePath.length === 1) {
39
+ // @ts-expect-error NAS
40
+ newData[updatePath[0]] = value;
41
+ return newData;
42
+ }
43
+ const [head, ...tail] = updatePath;
44
+ // @ts-expect-error NAS
45
+ newData[head] = updateNestedDataByPath(newData[head], tail, value);
46
+ return newData;
47
+ }
48
+ if (oldData instanceof Object) {
49
+ const newData = {
50
+ ...oldData
51
+ };
52
+ if (updatePath.length === 1) {
53
+ // @ts-expect-error NAS
54
+ newData[updatePath[0]] = value;
55
+ return newData;
56
+ }
57
+ const [head, ...tail] = updatePath;
58
+ // @ts-expect-error NAS
59
+ newData[head] = updateNestedDataByPath(newData[head], tail, value);
60
+ return newData;
61
+ }
62
+ return oldData;
63
+ };
64
+
65
+ /**
66
+ * Returns a copy of `oldData` without the node at `deletePath`.
67
+ */
68
+ export const deleteNestedDataByPath = (oldData, deletePath) => {
69
+ if (oldData instanceof Map) {
70
+ const newData = new Map(oldData);
71
+ if (deletePath.length === 1) {
72
+ newData.delete(deletePath[0]);
73
+ return newData;
74
+ }
75
+ const [head, ...tail] = deletePath;
76
+ newData.set(head, deleteNestedDataByPath(newData.get(head), tail));
77
+ return newData;
78
+ }
79
+ if (oldData instanceof Set) {
80
+ const setAsArray = deleteNestedDataByPath(Array.from(oldData), deletePath);
81
+ return new Set(setAsArray);
82
+ }
83
+ if (Array.isArray(oldData)) {
84
+ const newData = [...oldData];
85
+ if (deletePath.length === 1) {
86
+ return newData.filter((_, idx) => idx.toString() !== deletePath[0]);
87
+ }
88
+ const [head, ...tail] = deletePath;
89
+
90
+ // @ts-expect-error NAS
91
+ newData[head] = deleteNestedDataByPath(newData[head], tail);
92
+ return newData;
93
+ }
94
+ if (oldData instanceof Object) {
95
+ const newData = {
96
+ ...oldData
97
+ };
98
+ if (deletePath.length === 1) {
99
+ // @ts-expect-error NAS
100
+ delete newData[deletePath[0]];
101
+ return newData;
102
+ }
103
+ const [head, ...tail] = deletePath;
104
+ // @ts-expect-error NAS
105
+ newData[head] = deleteNestedDataByPath(newData[head], tail);
106
+ return newData;
107
+ }
108
+ return oldData;
109
+ };
@@ -69,6 +69,7 @@ export function treeActionsFor(root, selection, options = {}) {
69
69
  const value = selection?.value;
70
70
  const has = selection != null;
71
71
  const isRoot = path.length === 0;
72
+ const removeLocked = options.rootEntriesLocked === true && path.length === 1;
72
73
  const parent = has && !isRoot ? getAtPath(root, path.slice(0, -1)) : undefined;
73
74
  const parentIsArray = Array.isArray(parent);
74
75
  const index = parentIsArray ? Number(path[path.length - 1]) : -1;
@@ -152,8 +153,8 @@ export function treeActionsFor(root, selection, options = {}) {
152
153
  id: "remove",
153
154
  label: "Remove",
154
155
  danger: true,
155
- enabled: has && !isRoot,
156
- op: has && !isRoot ? {
156
+ enabled: has && !isRoot && !removeLocked,
157
+ op: has && !isRoot && !removeLocked ? {
157
158
  kind: "remove",
158
159
  path
159
160
  } : null
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * A local draft of a tree value, edited in place and written ONCE.
5
+ *
6
+ * Applying each tree op straight to the owning document was the obvious first
7
+ * cut and it behaves badly for two compounding reasons:
8
+ *
9
+ * 1. **Every op is a round trip.** The write lands, the owner emits an event,
10
+ * a snapshot comes back, and the browser re-reads everything — so a six-tap
11
+ * reorder is six writes and six full refreshes. The tree rebuilds under the
12
+ * user mid-gesture.
13
+ * 2. **The highlight and the value fall out of step.** Selection is local and
14
+ * moves instantly; the value it points at only catches up when the round
15
+ * trip completes, so for a beat the highlight sits on the old contents.
16
+ *
17
+ * Optimistic updates — the React Query pattern of rendering the expected result
18
+ * and reconciling later — fix (2) but not (1): they still write per change.
19
+ * Buffering fixes both, and it's what every editor that owns a document does
20
+ * (a form's dirty state, an IDE's unsaved buffer, JSON Editor's in-memory doc
21
+ * with an explicit save).
22
+ *
23
+ * The trade is that the buffer can go stale if the owner writes the same
24
+ * document while you're editing. That's the same last-write-wins a text editor
25
+ * has, and `isDirty` at least makes it visible that you're holding unsaved
26
+ * changes.
27
+ *
28
+ * Grew up as the Storage tool's key editor; hoisted here so the Zustand store
29
+ * editor is the same editor rather than a drifting copy. The save contract
30
+ * takes the VALUE — a storage host stringifies it, a zustand host diffs it
31
+ * into a merge partial.
32
+ */
33
+
34
+ import { useCallback, useState } from "react";
35
+ import { applyJsonOp, selectionPathAfterOp } from "./jsonOps.js";
36
+ /**
37
+ * @param liveValue the value as its owner currently holds it (already parsed)
38
+ * @param save persists the draft VALUE; rejecting keeps edit mode open
39
+ */
40
+ export function useTreeDraft(liveValue, save) {
41
+ const [draft, setDraft] = useState(undefined);
42
+ const [isEditing, setIsEditing] = useState(false);
43
+ const [isSaving, setIsSaving] = useState(false);
44
+ const [error, setError] = useState(null);
45
+ const [selectPath, setSelectPath] = useState();
46
+ /**
47
+ * The value as it was when editing began. Compared against for dirtiness
48
+ * rather than against `liveValue`, which can move underneath us if the owner
49
+ * writes the same document — that would otherwise flip a clean draft to
50
+ * dirty.
51
+ */
52
+ const [baseline, setBaseline] = useState("");
53
+ const value = isEditing && draft !== undefined ? draft : liveValue;
54
+ const isDirty = isEditing && JSON.stringify(draft) !== baseline;
55
+ const begin = useCallback(() => {
56
+ setDraft(liveValue);
57
+ setBaseline(JSON.stringify(liveValue));
58
+ setSelectPath(undefined);
59
+ setError(null);
60
+ setIsEditing(true);
61
+ }, [liveValue]);
62
+ const apply = useCallback(op => {
63
+ // Purely local — this is the whole point. No write, so no event, no
64
+ // snapshot, no refresh, and the tree re-renders from state synchronously.
65
+ setDraft(prev => applyJsonOp(prev, op));
66
+ const next = selectionPathAfterOp(op);
67
+ if (next !== undefined) setSelectPath(next);
68
+ }, []);
69
+ const selectAt = useCallback(path => {
70
+ setSelectPath(path);
71
+ }, []);
72
+
73
+ /** Leave edit mode entirely. Only correct on the way out (after a write). */
74
+ const reset = useCallback(() => {
75
+ setIsEditing(false);
76
+ setDraft(undefined);
77
+ setSelectPath(undefined);
78
+ setError(null);
79
+ }, []);
80
+
81
+ /** Re-seed the draft from what's stored, staying in edit mode. */
82
+ const discard = useCallback(() => {
83
+ setDraft(liveValue);
84
+ setBaseline(JSON.stringify(liveValue));
85
+ setSelectPath(undefined);
86
+ setError(null);
87
+ setIsEditing(true);
88
+ }, [liveValue]);
89
+ const commit = useCallback(async () => {
90
+ if (!isDirty || !save) {
91
+ reset();
92
+ return true;
93
+ }
94
+ setIsSaving(true);
95
+ setError(null);
96
+ try {
97
+ await save(draft);
98
+ reset();
99
+ return true;
100
+ } catch (e) {
101
+ // Stay in edit mode so the work isn't lost to a failed write.
102
+ setError(e instanceof Error ? e.message : String(e));
103
+ return false;
104
+ } finally {
105
+ setIsSaving(false);
106
+ }
107
+ }, [draft, isDirty, reset, save]);
108
+ return {
109
+ isEditing,
110
+ value,
111
+ isDirty,
112
+ isSaving,
113
+ error,
114
+ selectPath,
115
+ begin,
116
+ apply,
117
+ selectAt,
118
+ commit,
119
+ discard
120
+ };
121
+ }