@buoy-gg/shared-ui 7.0.4 → 7.0.6

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 (191) hide show
  1. package/lib/commonjs/colors/index.js +49 -0
  2. package/lib/commonjs/dataViewer/DataTreeActionBar.js +389 -0
  3. package/lib/commonjs/dataViewer/DataTreeActionDock.js +222 -0
  4. package/lib/commonjs/dataViewer/DataViewer.js +18 -3
  5. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +227 -28
  6. package/lib/commonjs/dataViewer/index.js +88 -0
  7. package/lib/commonjs/dataViewer/jsonOps.js +320 -0
  8. package/lib/commonjs/dataViewer/lineDiff.js +285 -83
  9. package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +20 -5
  10. package/lib/commonjs/dataViewer/treeActions.js +167 -0
  11. package/lib/commonjs/env/EnvironmentSelector.js +2 -2
  12. package/lib/commonjs/env/EnvironmentSelectorInline.js +2 -2
  13. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  14. package/lib/commonjs/icons/index.js +79 -3
  15. package/lib/commonjs/icons/lucide-icons-original-full.js +5 -1
  16. package/lib/commonjs/icons/lucide-icons.js +59 -2
  17. package/lib/commonjs/index.js +35 -0
  18. package/lib/commonjs/settings/components/BubbleSettingsSection.js +21 -19
  19. package/lib/commonjs/storage/devToolsStorageKeys.js +6 -3
  20. package/lib/commonjs/ui/components/Badge.js +2 -0
  21. package/lib/commonjs/ui/components/CompactRow.js +7 -0
  22. package/lib/commonjs/ui/components/CopyButton.js +27 -4
  23. package/lib/commonjs/ui/components/DevToolsCard.js +11 -0
  24. package/lib/commonjs/ui/components/DynamicFilterView.js +8 -1
  25. package/lib/commonjs/ui/components/EmptyState.js +12 -5
  26. package/lib/commonjs/ui/components/ErrorBoundary.js +3 -2
  27. package/lib/commonjs/ui/components/EventHistoryViewer/EventHistoryViewer.js +63 -11
  28. package/lib/commonjs/ui/components/ExpandableSection.js +4 -3
  29. package/lib/commonjs/ui/components/ExpandableSectionHeader.js +2 -1
  30. package/lib/commonjs/ui/components/FilterViewPattern.js +8 -7
  31. package/lib/commonjs/ui/components/ListItem.js +7 -5
  32. package/lib/commonjs/ui/components/PowerToggleButton.js +4 -1
  33. package/lib/commonjs/ui/components/TabSelector.js +3 -0
  34. package/lib/commonjs/ui/console/CyberpunkConsoleSection.js +1 -1
  35. package/lib/commonjs/ui/console/GalaxyButton.js +1 -1
  36. package/lib/commonjs/ui/gameUI/components/GameUICompactStats.js +2 -2
  37. package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +28 -6
  38. package/lib/commonjs/ui/gameUI/constants/macOSDesignSystemColors.js +22 -4
  39. package/lib/commonjs/utils/index.js +25 -0
  40. package/lib/commonjs/utils/reloadApp.js +144 -0
  41. package/lib/module/colors/index.js +21 -0
  42. package/lib/module/dataViewer/DataTreeActionBar.js +384 -0
  43. package/lib/module/dataViewer/DataTreeActionDock.js +218 -0
  44. package/lib/module/dataViewer/DataViewer.js +18 -3
  45. package/lib/module/dataViewer/VirtualizedDataExplorer.js +228 -30
  46. package/lib/module/dataViewer/index.js +16 -1
  47. package/lib/module/dataViewer/jsonOps.js +311 -0
  48. package/lib/module/dataViewer/lineDiff.js +284 -82
  49. package/lib/module/dataViewer/tree/TreeDiffViewer.js +20 -5
  50. package/lib/module/dataViewer/treeActions.js +161 -0
  51. package/lib/module/env/EnvironmentSelector.js +2 -2
  52. package/lib/module/env/EnvironmentSelectorInline.js +2 -2
  53. package/lib/module/hooks/safe-area-impl.js +1 -1
  54. package/lib/module/icons/index.js +10 -1
  55. package/lib/module/icons/lucide-icons-original-full.js +5 -1
  56. package/lib/module/icons/lucide-icons.js +56 -0
  57. package/lib/module/index.js +4 -0
  58. package/lib/module/settings/components/BubbleSettingsSection.js +21 -19
  59. package/lib/module/storage/devToolsStorageKeys.js +6 -3
  60. package/lib/module/ui/components/Badge.js +2 -0
  61. package/lib/module/ui/components/CompactRow.js +7 -0
  62. package/lib/module/ui/components/CopyButton.js +29 -6
  63. package/lib/module/ui/components/DevToolsCard.js +11 -0
  64. package/lib/module/ui/components/DynamicFilterView.js +8 -1
  65. package/lib/module/ui/components/EmptyState.js +12 -5
  66. package/lib/module/ui/components/ErrorBoundary.js +3 -2
  67. package/lib/module/ui/components/EventHistoryViewer/EventHistoryViewer.js +63 -11
  68. package/lib/module/ui/components/ExpandableSection.js +4 -3
  69. package/lib/module/ui/components/ExpandableSectionHeader.js +2 -1
  70. package/lib/module/ui/components/FilterViewPattern.js +9 -7
  71. package/lib/module/ui/components/ListItem.js +7 -5
  72. package/lib/module/ui/components/PowerToggleButton.js +4 -1
  73. package/lib/module/ui/components/TabSelector.js +3 -0
  74. package/lib/module/ui/console/CyberpunkConsoleSection.js +1 -1
  75. package/lib/module/ui/console/GalaxyButton.js +1 -1
  76. package/lib/module/ui/gameUI/components/GameUICompactStats.js +2 -2
  77. package/lib/module/ui/gameUI/constants/gameUIColors.js +27 -6
  78. package/lib/module/ui/gameUI/constants/macOSDesignSystemColors.js +19 -2
  79. package/lib/module/utils/index.js +1 -0
  80. package/lib/module/utils/reloadApp.js +137 -0
  81. package/lib/typescript/commonjs/colors/index.d.ts +22 -0
  82. package/lib/typescript/commonjs/colors/index.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts +53 -0
  84. package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts +33 -0
  86. package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts +14 -1
  88. package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts +30 -1
  90. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/dataViewer/__tests__/jsonOps.test.d.ts +19 -0
  92. package/lib/typescript/commonjs/dataViewer/__tests__/jsonOps.test.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/dataViewer/index.d.ts +5 -1
  94. package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/dataViewer/jsonOps.d.ts +124 -0
  96. package/lib/typescript/commonjs/dataViewer/jsonOps.d.ts.map +1 -0
  97. package/lib/typescript/commonjs/dataViewer/lineDiff.d.ts +27 -0
  98. package/lib/typescript/commonjs/dataViewer/lineDiff.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts +8 -1
  100. package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts +83 -0
  102. package/lib/typescript/commonjs/dataViewer/treeActions.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  104. package/lib/typescript/commonjs/icons/index.d.ts +6 -1
  105. package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/icons/lucide-icons-original-full.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/icons/lucide-icons.d.ts +9 -0
  108. package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/index.d.ts +2 -1
  110. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/settings/components/BubbleSettingsSection.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +5 -1
  113. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts +7 -1
  115. package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/components/EmptyState.d.ts.map +1 -1
  117. package/lib/typescript/commonjs/ui/components/ErrorBoundary.d.ts.map +1 -1
  118. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/EventHistoryViewer.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts +23 -7
  120. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/components/ExpandableSection.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/components/ExpandableSectionHeader.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/ui/components/FilterViewPattern.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/ui/components/ListItem.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +36 -30
  126. package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  127. package/lib/typescript/commonjs/ui/gameUI/constants/macOSDesignSystemColors.d.ts +5 -0
  128. package/lib/typescript/commonjs/ui/gameUI/constants/macOSDesignSystemColors.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/utils/index.d.ts +1 -0
  130. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/utils/reloadApp.d.ts +68 -0
  132. package/lib/typescript/commonjs/utils/reloadApp.d.ts.map +1 -0
  133. package/lib/typescript/module/colors/index.d.ts +22 -0
  134. package/lib/typescript/module/colors/index.d.ts.map +1 -0
  135. package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts +53 -0
  136. package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts.map +1 -0
  137. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts +33 -0
  138. package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -0
  139. package/lib/typescript/module/dataViewer/DataViewer.d.ts +14 -1
  140. package/lib/typescript/module/dataViewer/DataViewer.d.ts.map +1 -1
  141. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts +30 -1
  142. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  143. package/lib/typescript/module/dataViewer/__tests__/jsonOps.test.d.ts +19 -0
  144. package/lib/typescript/module/dataViewer/__tests__/jsonOps.test.d.ts.map +1 -0
  145. package/lib/typescript/module/dataViewer/index.d.ts +5 -1
  146. package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
  147. package/lib/typescript/module/dataViewer/jsonOps.d.ts +124 -0
  148. package/lib/typescript/module/dataViewer/jsonOps.d.ts.map +1 -0
  149. package/lib/typescript/module/dataViewer/lineDiff.d.ts +27 -0
  150. package/lib/typescript/module/dataViewer/lineDiff.d.ts.map +1 -1
  151. package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts +8 -1
  152. package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
  153. package/lib/typescript/module/dataViewer/treeActions.d.ts +83 -0
  154. package/lib/typescript/module/dataViewer/treeActions.d.ts.map +1 -0
  155. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  156. package/lib/typescript/module/icons/index.d.ts +6 -1
  157. package/lib/typescript/module/icons/index.d.ts.map +1 -1
  158. package/lib/typescript/module/icons/lucide-icons-original-full.d.ts.map +1 -1
  159. package/lib/typescript/module/icons/lucide-icons.d.ts +9 -0
  160. package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
  161. package/lib/typescript/module/index.d.ts +2 -1
  162. package/lib/typescript/module/index.d.ts.map +1 -1
  163. package/lib/typescript/module/settings/components/BubbleSettingsSection.d.ts.map +1 -1
  164. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +5 -1
  165. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  166. package/lib/typescript/module/ui/components/CopyButton.d.ts +7 -1
  167. package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
  168. package/lib/typescript/module/ui/components/EmptyState.d.ts.map +1 -1
  169. package/lib/typescript/module/ui/components/ErrorBoundary.d.ts.map +1 -1
  170. package/lib/typescript/module/ui/components/EventHistoryViewer/EventHistoryViewer.d.ts.map +1 -1
  171. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts +23 -7
  172. package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
  173. package/lib/typescript/module/ui/components/ExpandableSection.d.ts.map +1 -1
  174. package/lib/typescript/module/ui/components/ExpandableSectionHeader.d.ts.map +1 -1
  175. package/lib/typescript/module/ui/components/FilterViewPattern.d.ts.map +1 -1
  176. package/lib/typescript/module/ui/components/ListItem.d.ts.map +1 -1
  177. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +36 -30
  178. package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
  179. package/lib/typescript/module/ui/gameUI/constants/macOSDesignSystemColors.d.ts +5 -0
  180. package/lib/typescript/module/ui/gameUI/constants/macOSDesignSystemColors.d.ts.map +1 -1
  181. package/lib/typescript/module/utils/index.d.ts +1 -0
  182. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  183. package/lib/typescript/module/utils/reloadApp.d.ts +68 -0
  184. package/lib/typescript/module/utils/reloadApp.d.ts.map +1 -0
  185. package/package.json +15 -4
  186. package/lib/commonjs/icons/ImageOverlayIcon.js +0 -142
  187. package/lib/module/icons/ImageOverlayIcon.js +0 -137
  188. package/lib/typescript/commonjs/icons/ImageOverlayIcon.d.ts +0 -14
  189. package/lib/typescript/commonjs/icons/ImageOverlayIcon.d.ts.map +0 -1
  190. package/lib/typescript/module/icons/ImageOverlayIcon.d.ts +0 -14
  191. package/lib/typescript/module/icons/ImageOverlayIcon.d.ts.map +0 -1
@@ -14,6 +14,24 @@ export let DiffType = /*#__PURE__*/function (DiffType) {
14
14
  DiffType["MODIFIED"] = "modified";
15
15
  return DiffType;
16
16
  }({});
17
+
18
+ /**
19
+ * Marks one side of a diff as "this value did not exist".
20
+ *
21
+ * Deliberately distinct from `null`, `[]`, `{}` and `""` — those are real
22
+ * stored values and diff normally. An absent side contributes zero lines, so a
23
+ * key that was just created reads as purely added rather than as an edit of the
24
+ * literal text `null`.
25
+ *
26
+ * `Symbol.for` so the identity holds even if a bundler ends up with two copies
27
+ * of this module.
28
+ */
29
+ export const ABSENT_VALUE = Symbol.for("buoy.diff.absent");
30
+
31
+ /** True when a diff side represents a value that did not exist. */
32
+ export function isAbsent(value) {
33
+ return value === ABSENT_VALUE;
34
+ }
17
35
  /**
18
36
  * Compute diff based on method - for word-level diff within lines
19
37
  */
@@ -153,6 +171,10 @@ function computeDiffByMethod(oldStr, newStr, method) {
153
171
  * Convert object to formatted JSON lines
154
172
  */
155
173
  function objectToLines(obj) {
174
+ // A value that never existed has no text at all — not the word "null".
175
+ if (isAbsent(obj)) {
176
+ return [];
177
+ }
156
178
  if (obj === null || obj === undefined) {
157
179
  return [String(obj)];
158
180
  }
@@ -236,63 +258,265 @@ function filterDiffWithContext(diffs, contextLines) {
236
258
  }
237
259
 
238
260
  /**
239
- * Compare lines based on method
261
+ * Normalize a line into the key used for matching.
262
+ *
263
+ * Display always uses the raw JSON; only matching sees this. `trimmedLines`
264
+ * drops indentation, and `ignoreTrailingComma` drops the trailing comma so
265
+ * pushing one item onto an array doesn't report the previous last item as
266
+ * modified.
240
267
  */
241
- function compareLinesWithMethod(line1, line2, method) {
242
- switch (method) {
243
- case "trimmedLines":
244
- return line1.trim() === line2.trim();
245
- case "chars":
246
- case "words":
247
- case "lines":
248
- default:
249
- return line1 === line2;
268
+ function normalizeCompareKey(line, compareMethod, ignoreTrailingComma) {
269
+ const base = compareMethod === "trimmedLines" ? line.trim() : line;
270
+ return ignoreTrailingComma ? base.replace(/,\s*$/, "") : base;
271
+ }
272
+
273
+ /**
274
+ * The original heuristic: lines that start alike are an edit of one line rather
275
+ * than an unrelated removal plus addition. Only ever applied inside a hunk the
276
+ * line matcher already proved unmatched, so it can no longer pair lines that
277
+ * merely shifted position.
278
+ */
279
+ function looksLikeModification(oldLine, newLine) {
280
+ const oldTrimmed = oldLine.trim();
281
+ const newTrimmed = newLine.trim();
282
+ return !!oldTrimmed && !!newTrimmed && (oldTrimmed.startsWith(newTrimmed.substring(0, 3)) || newTrimmed.startsWith(oldTrimmed.substring(0, 3)));
283
+ }
284
+ /**
285
+ * Myers is O(ND) in the number of edits, so near-identical values (the common
286
+ * case — one array push) are close to free and only wildly different values get
287
+ * expensive. Past these bounds we fall back to the pairwise walk.
288
+ */
289
+ const MYERS_MAX_TOTAL_LINES = 8000;
290
+ const MYERS_MAX_EDIT_DISTANCE = 1024;
291
+
292
+ /**
293
+ * Myers' O(ND) diff ("An O(ND) Difference Algorithm and Its Variations", 1986)
294
+ * over line keys — the same algorithm git and `diff` use.
295
+ *
296
+ * Returns null when the input is too large or too dissimilar to be worth it,
297
+ * which tells the caller to use the fallback walk.
298
+ */
299
+ function myersLineOps(a, b) {
300
+ const n = a.length;
301
+ const m = b.length;
302
+ if (n + m > MYERS_MAX_TOTAL_LINES) return null;
303
+ if (n === 0 && m === 0) return [];
304
+ const max = n + m;
305
+ // +1 of padding on each side so the k-1 / k+1 probes at the frontier stay in
306
+ // bounds without clamping (clamping would corrupt the index arithmetic).
307
+ const offset = max + 1;
308
+ const v = new Int32Array(2 * max + 3);
309
+ const maxD = Math.min(max, MYERS_MAX_EDIT_DISTANCE);
310
+
311
+ // Frontier snapshot per depth. Only k in [-(d+1), d+1] is ever read back, so
312
+ // each snapshot is O(d) and the whole trace is O(D^2) — not O(D * max).
313
+ const trace = [];
314
+ for (let d = 0; d <= maxD; d++) {
315
+ trace.push(v.slice(offset - (d + 1), offset + d + 2));
316
+ for (let k = -d; k <= d; k += 2) {
317
+ let x;
318
+ if (k === -d || k !== d && v[k - 1 + offset] < v[k + 1 + offset]) {
319
+ x = v[k + 1 + offset];
320
+ } else {
321
+ x = v[k - 1 + offset] + 1;
322
+ }
323
+ let y = x - k;
324
+
325
+ // Follow the diagonal as far as the lines keep matching.
326
+ while (x < n && y < m && a[x] === b[y]) {
327
+ x++;
328
+ y++;
329
+ }
330
+ v[k + offset] = x;
331
+ if (x >= n && y >= m) {
332
+ return backtrackOps(trace, n, m);
333
+ }
334
+ }
250
335
  }
336
+ return null;
251
337
  }
252
338
 
253
339
  /**
254
- * Compute line-by-line diff between two objects
340
+ * Walk the recorded frontiers backwards to recover the edit script.
255
341
  */
256
- export function computeLineDiff(oldValue, newValue, options = {}) {
257
- const {
258
- compareMethod = "words",
259
- disableWordDiff = false,
260
- showDiffOnly = false,
261
- contextLines = 3
262
- } = options;
263
- const oldLines = objectToLines(oldValue);
264
- const newLines = objectToLines(newValue);
342
+ function backtrackOps(trace, n, m) {
343
+ const ops = [];
344
+ let x = n;
345
+ let y = m;
346
+ for (let d = trace.length - 1; d >= 0; d--) {
347
+ const snapshot = trace[d];
348
+ // Snapshot d covers k in [-(d+1), d+1], so k=0 sits at index d+1.
349
+ const base = d + 1;
350
+ const k = x - y;
351
+ let prevK;
352
+ if (k === -d || k !== d && snapshot[k - 1 + base] < snapshot[k + 1 + base]) {
353
+ prevK = k + 1;
354
+ } else {
355
+ prevK = k - 1;
356
+ }
357
+ const prevX = snapshot[prevK + base];
358
+ const prevY = prevX - prevK;
359
+ while (x > prevX && y > prevY) {
360
+ ops.push({
361
+ kind: "eq",
362
+ i: x - 1,
363
+ j: y - 1
364
+ });
365
+ x--;
366
+ y--;
367
+ }
368
+ if (d > 0) {
369
+ if (x === prevX) {
370
+ ops.push({
371
+ kind: "ins",
372
+ j: y - 1
373
+ });
374
+ } else {
375
+ ops.push({
376
+ kind: "del",
377
+ i: x - 1
378
+ });
379
+ }
380
+ }
381
+ x = prevX;
382
+ y = prevY;
383
+ }
384
+ ops.reverse();
385
+ return ops;
386
+ }
387
+
388
+ /**
389
+ * Emit one hunk of unmatched lines. Removals and additions are paired off in
390
+ * order and become MODIFIED rows when they look like an edit of the same line;
391
+ * everything left over stays a one-sided removal or addition.
392
+ */
393
+ function emitHunk(result, dels, inss, oldLines, newLines, compareMethod, disableWordDiff) {
394
+ const pairs = Math.min(dels.length, inss.length);
395
+ for (let k = 0; k < pairs; k++) {
396
+ const i = dels[k];
397
+ const j = inss[k];
398
+ if (!looksLikeModification(oldLines[i], newLines[j])) {
399
+ result.push({
400
+ leftLineNumber: i + 1,
401
+ type: DiffType.REMOVED,
402
+ leftContent: oldLines[i],
403
+ leftRaw: oldLines[i]
404
+ });
405
+ result.push({
406
+ rightLineNumber: j + 1,
407
+ type: DiffType.ADDED,
408
+ rightContent: newLines[j],
409
+ rightRaw: newLines[j]
410
+ });
411
+ continue;
412
+ }
413
+ if (!disableWordDiff && compareMethod !== "lines") {
414
+ const wordDiff = computeDiffByMethod(oldLines[i], newLines[j], compareMethod);
415
+ result.push({
416
+ leftLineNumber: i + 1,
417
+ rightLineNumber: j + 1,
418
+ type: DiffType.MODIFIED,
419
+ leftContent: wordDiff.left,
420
+ rightContent: wordDiff.right,
421
+ leftRaw: oldLines[i],
422
+ rightRaw: newLines[j]
423
+ });
424
+ } else {
425
+ result.push({
426
+ leftLineNumber: i + 1,
427
+ rightLineNumber: j + 1,
428
+ type: DiffType.MODIFIED,
429
+ leftContent: oldLines[i],
430
+ rightContent: newLines[j],
431
+ leftRaw: oldLines[i],
432
+ rightRaw: newLines[j]
433
+ });
434
+ }
435
+ }
436
+ for (let k = pairs; k < dels.length; k++) {
437
+ const i = dels[k];
438
+ result.push({
439
+ leftLineNumber: i + 1,
440
+ type: DiffType.REMOVED,
441
+ leftContent: oldLines[i],
442
+ leftRaw: oldLines[i]
443
+ });
444
+ }
445
+ for (let k = pairs; k < inss.length; k++) {
446
+ const j = inss[k];
447
+ result.push({
448
+ rightLineNumber: j + 1,
449
+ type: DiffType.ADDED,
450
+ rightContent: newLines[j],
451
+ rightRaw: newLines[j]
452
+ });
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Turn the edit script into rows, grouping each run of unmatched lines into a
458
+ * hunk so modification pairing stays local to a real change.
459
+ */
460
+ function buildRowsFromOps(ops, oldLines, newLines, compareMethod, disableWordDiff) {
265
461
  const result = [];
266
- let leftLineNum = 1;
267
- let rightLineNum = 1;
462
+ let idx = 0;
463
+ while (idx < ops.length) {
464
+ const op = ops[idx];
465
+ if (op.kind === "eq") {
466
+ result.push({
467
+ leftLineNumber: op.i + 1,
468
+ rightLineNumber: op.j + 1,
469
+ type: DiffType.DEFAULT,
470
+ leftContent: oldLines[op.i],
471
+ rightContent: newLines[op.j],
472
+ leftRaw: oldLines[op.i],
473
+ rightRaw: newLines[op.j]
474
+ });
475
+ idx++;
476
+ continue;
477
+ }
478
+ const dels = [];
479
+ const inss = [];
480
+ while (idx < ops.length && ops[idx].kind !== "eq") {
481
+ const hunkOp = ops[idx];
482
+ if (hunkOp.kind === "del") dels.push(hunkOp.i);else if (hunkOp.kind === "ins") inss.push(hunkOp.j);
483
+ idx++;
484
+ }
485
+ emitHunk(result, dels, inss, oldLines, newLines, compareMethod, disableWordDiff);
486
+ }
487
+ return result;
488
+ }
268
489
 
269
- // Simple line diff algorithm (can be improved with LCS)
270
- let i = 0,
271
- j = 0;
490
+ /**
491
+ * Fallback pairwise walk, used only when the input is past the Myers bounds.
492
+ * Advances both sides in lockstep, so a shifted line reads as changed — the
493
+ * reason it isn't the primary path.
494
+ */
495
+ function pairwiseLineWalk(oldLines, newLines, oldKeys, newKeys, compareMethod, disableWordDiff) {
496
+ const result = [];
497
+ let i = 0;
498
+ let j = 0;
272
499
  while (i < oldLines.length || j < newLines.length) {
273
500
  if (i >= oldLines.length) {
274
- // Rest are additions
275
501
  result.push({
276
- rightLineNumber: rightLineNum++,
502
+ rightLineNumber: j + 1,
277
503
  type: DiffType.ADDED,
278
504
  rightContent: newLines[j],
279
505
  rightRaw: newLines[j]
280
506
  });
281
507
  j++;
282
508
  } else if (j >= newLines.length) {
283
- // Rest are removals
284
509
  result.push({
285
- leftLineNumber: leftLineNum++,
510
+ leftLineNumber: i + 1,
286
511
  type: DiffType.REMOVED,
287
512
  leftContent: oldLines[i],
288
513
  leftRaw: oldLines[i]
289
514
  });
290
515
  i++;
291
- } else if (compareLinesWithMethod(oldLines[i], newLines[j], compareMethod === "trimmedLines" ? "trimmedLines" : "lines")) {
292
- // Lines match (possibly after trimming if trimmedLines)
516
+ } else if (oldKeys[i] === newKeys[j]) {
293
517
  result.push({
294
- leftLineNumber: leftLineNum++,
295
- rightLineNumber: rightLineNum++,
518
+ leftLineNumber: i + 1,
519
+ rightLineNumber: j + 1,
296
520
  type: DiffType.DEFAULT,
297
521
  leftContent: oldLines[i],
298
522
  rightContent: newLines[j],
@@ -302,57 +526,35 @@ export function computeLineDiff(oldValue, newValue, options = {}) {
302
526
  i++;
303
527
  j++;
304
528
  } else {
305
- // Lines differ - check if it's a modification or separate add/remove
306
- const oldTrimmed = oldLines[i].trim();
307
- const newTrimmed = newLines[j].trim();
308
-
309
- // Simple heuristic: if lines start similarly, treat as modification
310
- if (oldTrimmed && newTrimmed && (oldTrimmed.startsWith(newTrimmed.substring(0, 3)) || newTrimmed.startsWith(oldTrimmed.substring(0, 3)))) {
311
- // Treat as modification - compute word diff if enabled
312
- if (!disableWordDiff && compareMethod !== "lines") {
313
- const wordDiff = computeDiffByMethod(oldLines[i], newLines[j], compareMethod);
314
- result.push({
315
- leftLineNumber: leftLineNum++,
316
- rightLineNumber: rightLineNum++,
317
- type: DiffType.MODIFIED,
318
- leftContent: wordDiff.left,
319
- rightContent: wordDiff.right,
320
- leftRaw: oldLines[i],
321
- rightRaw: newLines[j]
322
- });
323
- } else {
324
- // No word diff - just mark lines as different
325
- result.push({
326
- leftLineNumber: leftLineNum++,
327
- rightLineNumber: rightLineNum++,
328
- type: DiffType.MODIFIED,
329
- leftContent: oldLines[i],
330
- rightContent: newLines[j],
331
- leftRaw: oldLines[i],
332
- rightRaw: newLines[j]
333
- });
334
- }
335
- i++;
336
- j++;
337
- } else {
338
- // Treat as separate remove and add
339
- result.push({
340
- leftLineNumber: leftLineNum++,
341
- type: DiffType.REMOVED,
342
- leftContent: oldLines[i],
343
- leftRaw: oldLines[i]
344
- });
345
- result.push({
346
- rightLineNumber: rightLineNum++,
347
- type: DiffType.ADDED,
348
- rightContent: newLines[j],
349
- rightRaw: newLines[j]
350
- });
351
- i++;
352
- j++;
353
- }
529
+ emitHunk(result, [i], [j], oldLines, newLines, compareMethod, disableWordDiff);
530
+ i++;
531
+ j++;
354
532
  }
355
533
  }
534
+ return result;
535
+ }
536
+
537
+ /**
538
+ * Compute line-by-line diff between two objects
539
+ *
540
+ * Lines are matched with Myers' O(ND) algorithm over normalized keys, so an
541
+ * insert or append shows up as the one line that actually arrived instead of
542
+ * every line after it shifting into a change.
543
+ */
544
+ export function computeLineDiff(oldValue, newValue, options = {}) {
545
+ const {
546
+ compareMethod = "words",
547
+ disableWordDiff = false,
548
+ showDiffOnly = false,
549
+ contextLines = 3,
550
+ ignoreTrailingComma = true
551
+ } = options;
552
+ const oldLines = objectToLines(oldValue);
553
+ const newLines = objectToLines(newValue);
554
+ const oldKeys = oldLines.map(line => normalizeCompareKey(line, compareMethod, ignoreTrailingComma));
555
+ const newKeys = newLines.map(line => normalizeCompareKey(line, compareMethod, ignoreTrailingComma));
556
+ const ops = myersLineOps(oldKeys, newKeys);
557
+ const result = ops ? buildRowsFromOps(ops, oldLines, newLines, compareMethod, disableWordDiff) : pairwiseLineWalk(oldLines, newLines, oldKeys, newKeys, compareMethod, disableWordDiff);
356
558
 
357
559
  // Apply showDiffOnly filter if enabled
358
560
  if (showDiffOnly) {
@@ -18,6 +18,7 @@
18
18
  import React, { useMemo, useState, useEffect, useRef } from "react";
19
19
  import { View, Text, ScrollView, StyleSheet, TouchableOpacity } from "react-native";
20
20
  import { gameUIColors } from "@buoy-gg/shared-ui";
21
+ import { isAbsent } from "../lineDiff.js";
21
22
 
22
23
  // ============================================
23
24
  // TYPES & INTERFACES
@@ -105,6 +106,11 @@ function isEqual(a, b) {
105
106
  function computeDiff(oldValue, newValue, path = []) {
106
107
  const result = [];
107
108
 
109
+ // A side marked absent never existed, so it contributes nothing and the other
110
+ // side reads as purely added (or removed). Only the root can be absent.
111
+ if (isAbsent(oldValue)) oldValue = undefined;
112
+ if (isAbsent(newValue)) newValue = undefined;
113
+
108
114
  // Handle primitives and null/undefined
109
115
  if (!isObject(oldValue) && !isObject(newValue) && !Array.isArray(oldValue) && !Array.isArray(newValue)) {
110
116
  if (!isEqual(oldValue, newValue)) {
@@ -317,7 +323,8 @@ export default function TreeDiffViewer({
317
323
  newValue,
318
324
  theme: themeName = "dark",
319
325
  expandAll = false,
320
- showUnchanged = true
326
+ showUnchanged = true,
327
+ scrollEnabled = true
321
328
  }) {
322
329
  const theme = themeName === "dark" ? darkTheme : lightTheme;
323
330
 
@@ -632,8 +639,12 @@ export default function TreeDiffViewer({
632
639
 
633
640
  // Show header only if there are changes
634
641
  const hasChanges = stats.added > 0 || stats.removed > 0 || stats.changed > 0;
642
+
643
+ // Inline mode must not claim `flex: 1`, or it would stretch to the pane and
644
+ // clip the rows an ancestor ScrollView is supposed to reveal.
645
+ const RowsContainer = scrollEnabled ? ScrollView : View;
635
646
  return /*#__PURE__*/_jsxs(View, {
636
- style: [styles.container, {
647
+ style: [scrollEnabled ? styles.container : styles.containerInline, {
637
648
  backgroundColor: theme.background
638
649
  }],
639
650
  children: [hasChanges && /*#__PURE__*/_jsx(View, {
@@ -698,8 +709,8 @@ export default function TreeDiffViewer({
698
709
  })]
699
710
  })]
700
711
  })
701
- }), /*#__PURE__*/_jsx(ScrollView, {
702
- style: styles.scrollView,
712
+ }), /*#__PURE__*/_jsx(RowsContainer, {
713
+ style: scrollEnabled ? styles.scrollView : undefined,
703
714
  showsVerticalScrollIndicator: false,
704
715
  children: diffTree.length === 0 ? /*#__PURE__*/_jsxs(View, {
705
716
  style: styles.emptyState,
@@ -739,6 +750,10 @@ const styles = StyleSheet.create({
739
750
  borderRadius: 8,
740
751
  overflow: "hidden"
741
752
  },
753
+ containerInline: {
754
+ borderRadius: 8,
755
+ overflow: "hidden"
756
+ },
742
757
  header: {
743
758
  paddingHorizontal: 12,
744
759
  paddingVertical: 8,
@@ -790,7 +805,7 @@ const styles = StyleSheet.create({
790
805
  justifyContent: "center",
791
806
  flexDirection: "row",
792
807
  borderBottomWidth: StyleSheet.hairlineWidth,
793
- borderBottomColor: "rgba(255, 255, 255, 0.02)"
808
+ borderBottomColor: "rgba(128, 128, 128, 0.04)"
794
809
  },
795
810
  lineNumber: {
796
811
  width: 32,
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * What you can do to the node a data explorer has selected — as data, not UI.
5
+ *
6
+ * Two surfaces render this list and they look nothing alike: the dashboard's
7
+ * footer bar is a row of small chips beside a cursor, the phone's full-screen
8
+ * editor is a grid of thumb-sized tiles. Deriving both from one function is the
9
+ * point — "can this item move up" is a property of the DOCUMENT, and having two
10
+ * components each answer it from their own reading of `root` is how the two
11
+ * surfaces quietly start disagreeing about what's legal.
12
+ *
13
+ * So this module answers it once and hands back a list. The renderers decide
14
+ * what a disabled action looks like and which icon it wears; they never decide
15
+ * whether it applies.
16
+ */
17
+
18
+ import { getAtPath, isEditableContainer, isRebuildableObject, suggestedNewValue } from "./jsonOps.js";
19
+
20
+ /** The node a data explorer currently has selected. */
21
+
22
+ /** The node's type, as a badge shows it. */
23
+ export function typeLabel(value) {
24
+ if (value === null) return "null";
25
+ if (Array.isArray(value)) return "array";
26
+ if (isRebuildableObject(value)) return "object";
27
+ return typeof value;
28
+ }
29
+
30
+ /** `a › b › 2` — reads as a location, which a bare index like `2` does not. */
31
+ export function breadcrumb(path) {
32
+ return path.length === 0 ? "root" : path.join(" › ");
33
+ }
34
+
35
+ /**
36
+ * Index the list by id, for renderers that lay the six out in their own order.
37
+ *
38
+ * The bar and the dock genuinely disagree here — the bar puts the two move
39
+ * arrows together in the middle because they're icon-only and read as a pair,
40
+ * the dock follows the mockup's reading order. Neither ordering belongs in the
41
+ * capability list, so both look up what they need.
42
+ */
43
+ export function treeActionMap(actions) {
44
+ const map = {};
45
+ for (const action of actions) map[action.id] = action;
46
+ return map;
47
+ }
48
+
49
+ /** A key that doesn't collide, for "add" on an object. */
50
+ function newKeyFor(container) {
51
+ if (!("newKey" in container)) return "newKey";
52
+ let n = 2;
53
+ while (`newKey ${n}` in container) n++;
54
+ return `newKey ${n}`;
55
+ }
56
+
57
+ /**
58
+ * The six things a selection can do, ALWAYS in this order and always all six.
59
+ *
60
+ * Returning the inapplicable ones as `enabled: false` rather than omitting them
61
+ * is deliberate and is why this returns a fixed-length list: a renderer that
62
+ * drops entries reflows every time the selection moves, so stepping from item 1
63
+ * to item 0 would slide the remaining controls sideways under a finger already
64
+ * on its way down. Stable position also means the same action is always in the
65
+ * same place, which is most of what makes a grid of six learnable.
66
+ */
67
+ export function treeActionsFor(root, selection, options = {}) {
68
+ const path = selection?.path ?? [];
69
+ const value = selection?.value;
70
+ const has = selection != null;
71
+ const isRoot = path.length === 0;
72
+ const parent = has && !isRoot ? getAtPath(root, path.slice(0, -1)) : undefined;
73
+ const parentIsArray = Array.isArray(parent);
74
+ const index = parentIsArray ? Number(path[path.length - 1]) : -1;
75
+ const isArray = Array.isArray(value);
76
+ const isObject = isRebuildableObject(value);
77
+ const isContainer = isEditableContainer(value);
78
+ const canEditValue = has && (options.containersEditable === true || !isContainer);
79
+
80
+ // One "+" slot whose meaning follows the container type, rather than two
81
+ // mutually-exclusive buttons of which one is always dead.
82
+ const addedKey = isObject ? newKeyFor(value) : "";
83
+ const add = isObject ? {
84
+ id: "add",
85
+ label: "Add key",
86
+ enabled: true,
87
+ op: {
88
+ kind: "addKey",
89
+ path,
90
+ key: addedKey,
91
+ value: suggestedNewValue(value)
92
+ },
93
+ selectAfter: [...path, addedKey]
94
+ } : {
95
+ id: "add",
96
+ label: "Append",
97
+ enabled: isArray,
98
+ op: isArray ? {
99
+ kind: "append",
100
+ path,
101
+ value: suggestedNewValue(value)
102
+ } : null,
103
+ selectAfter: isArray ? [...path, String(value.length)] : undefined
104
+ };
105
+ return [
106
+ // A boolean has exactly two states, so there is nothing to ask and nothing
107
+ // to type — an editor for it would be a screen whose only job is to offer
108
+ // the value you didn't pick. It flips in place instead.
109
+ typeof value === "boolean" ? {
110
+ id: "editValue",
111
+ label: "Toggle",
112
+ enabled: true,
113
+ op: {
114
+ kind: "replace",
115
+ path,
116
+ value: !value
117
+ }
118
+ } : {
119
+ id: "editValue",
120
+ label: "Edit value",
121
+ enabled: canEditValue,
122
+ op: null
123
+ }, add, {
124
+ // `duplicate` handles object keys too (it mints a "<key> copy" in place),
125
+ // so this applies to any entry that has a parent.
126
+ id: "duplicate",
127
+ label: "Duplicate",
128
+ enabled: has && !isRoot,
129
+ op: has && !isRoot ? {
130
+ kind: "duplicate",
131
+ path
132
+ } : null
133
+ }, {
134
+ id: "moveUp",
135
+ label: "Move up",
136
+ enabled: has && parentIsArray && index > 0,
137
+ op: has && parentIsArray && index > 0 ? {
138
+ kind: "move",
139
+ path,
140
+ toIndex: index - 1
141
+ } : null
142
+ }, {
143
+ id: "moveDown",
144
+ label: "Move down",
145
+ enabled: has && parentIsArray && index > -1 && index < parent.length - 1,
146
+ op: has && parentIsArray && index > -1 && index < parent.length - 1 ? {
147
+ kind: "move",
148
+ path,
149
+ toIndex: index + 1
150
+ } : null
151
+ }, {
152
+ id: "remove",
153
+ label: "Remove",
154
+ danger: true,
155
+ enabled: has && !isRoot,
156
+ op: has && !isRoot ? {
157
+ kind: "remove",
158
+ path
159
+ } : null
160
+ }];
161
+ }
@@ -176,10 +176,10 @@ const styles = StyleSheet.create({
176
176
  width: "100%"
177
177
  },
178
178
  optionContainerSelected: {
179
- backgroundColor: "rgba(255, 255, 255, 0.05)"
179
+ backgroundColor: "rgba(128, 128, 128, 0.1)"
180
180
  },
181
181
  optionContainerPressed: {
182
- backgroundColor: "rgba(255, 255, 255, 0.1)"
182
+ backgroundColor: "rgba(128, 128, 128, 0.2)"
183
183
  },
184
184
  optionLeft: {
185
185
  flexDirection: "row",
@@ -270,10 +270,10 @@ const styles = StyleSheet.create({
270
270
  height: OPTION_HEIGHT
271
271
  },
272
272
  optionContainerSelected: {
273
- backgroundColor: "rgba(255, 255, 255, 0.05)"
273
+ backgroundColor: "rgba(128, 128, 128, 0.1)"
274
274
  },
275
275
  optionContainerPressed: {
276
- backgroundColor: "rgba(255, 255, 255, 0.1)"
276
+ backgroundColor: "rgba(128, 128, 128, 0.2)"
277
277
  },
278
278
  optionLeft: {
279
279
  flexDirection: "row",
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Auto-generated safe area implementation
5
5
  * Detected: none
6
- * Generated at: 2026-07-23T18:32:21.569Z
6
+ * Generated at: 2026-07-29T13:27:30.301Z
7
7
  *
8
8
  * DO NOT EDIT - This file is generated by scripts/detect-safe-area.js
9
9
  *