@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
@@ -7,9 +7,8 @@ import { gameUIColors } from "../ui/gameUI/index.js";
7
7
  import { CopyButton } from "../ui/components/CopyButton.js";
8
8
  import { ChevronRight } from "../icons/index.js";
9
9
  import { IndentGuidesOverlay } from "./IndentGuidesOverlay.js";
10
-
11
- // Stable constants to prevent re-renders [[memory:4875251]]
12
10
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ // Stable constants to prevent re-renders [[memory:4875251]]
13
12
  const HIT_SLOP_10 = {
14
13
  top: 10,
15
14
  bottom: 10,
@@ -22,6 +21,8 @@ const CHUNK_SIZE = 50; // Process data in chunks to avoid blocking UI
22
21
  const MAX_DEPTH_LIMIT = 15; // Prevent excessive nesting
23
22
  const MAX_ITEMS_PER_LEVEL = 500; // Limit items to prevent memory issues
24
23
  const LONG_KEY_THRESHOLD = 30; // Keys longer than this use vertical layout
24
+ /** Two presses inside this window count as a double-tap (RN has no dblclick). */
25
+ const DOUBLE_TAP_MS = 300;
25
26
 
26
27
  // Pre-computed indent styles (VS Code-style width)
27
28
  const INDENT_WIDTH = 16;
@@ -220,8 +221,48 @@ const STABLE_STYLES = StyleSheet.create({
220
221
  }
221
222
  });
222
223
 
224
+ /**
225
+ * A tree row as the HOST sees it.
226
+ *
227
+ * `item.path` is addressed for the TREE, which is seeded with a synthetic
228
+ * "root" node wrapping the caller's data — so the top row is `["root"]` and its
229
+ * first child `["root", "0"]`. Ops address the DATA, where those are `[]` and
230
+ * `["0"]`. Dropping the first segment is what reconciles the two, and doing it
231
+ * in ONE place is what keeps selection, inline editing and ops all agreeing.
232
+ */
233
+ function selectionOf(item) {
234
+ return {
235
+ path: item.path.slice(1),
236
+ value: item.value,
237
+ label: item.key
238
+ };
239
+ }
240
+
223
241
  // Type definitions for flattened data structure
224
242
 
243
+ /**
244
+ * Node count, capped at `limit`.
245
+ *
246
+ * Deliberately short-circuits: this runs on every render to decide which
247
+ * flattening path to take, so it must cost O(limit), never O(document).
248
+ */
249
+ function countNodesUpTo(value, limit, seen = 0) {
250
+ if (seen >= limit) return seen;
251
+ let n = seen + 1;
252
+ if (Array.isArray(value)) {
253
+ for (const child of value) {
254
+ if (n >= limit) return n;
255
+ n = countNodesUpTo(child, limit, n);
256
+ }
257
+ } else if (value !== null && typeof value === "object") {
258
+ for (const child of Object.values(value)) {
259
+ if (n >= limit) return n;
260
+ n = countNodesUpTo(child, limit, n);
261
+ }
262
+ }
263
+ return n;
264
+ }
265
+
225
266
  // Enhanced type detection optimized for performance
226
267
  const getValueType = value => {
227
268
  if (value === null) return "null";
@@ -373,6 +414,16 @@ const useDataFlattening = (data, maxDepth = 10, autoExpandFirstLevel = false) =>
373
414
  // Store circular cache outside of re-renders to prevent reset
374
415
  const circularCacheRef = useRef(new WeakSet());
375
416
  const processingRef = useRef(false);
417
+ /**
418
+ * Monotonic token identifying the newest flatten run.
419
+ *
420
+ * WHY: a cancelled run used to `return` without clearing `isProcessing`, so
421
+ * whenever renders arrived faster than the 10ms debounce + flatten, every run
422
+ * was superseded before it finished and the spinner never came down — the
423
+ * viewer sat on "Processing data…" forever. Now each run checks it is still
424
+ * the current one, and the run that ends up last ALWAYS resolves the flag.
425
+ */
426
+ const runIdRef = useRef(0);
376
427
  const dataVersionRef = useRef(0);
377
428
  const lastActionRef = useRef(undefined);
378
429
 
@@ -502,14 +553,16 @@ const useDataFlattening = (data, maxDepth = 10, autoExpandFirstLevel = false) =>
502
553
  if (processingRef.current) {
503
554
  return;
504
555
  }
505
- let isCancelled = false;
506
556
  let timeoutId;
557
+ const runId = ++runIdRef.current;
558
+ /** True while this run is still the newest one scheduled. */
559
+ const isCurrent = () => runIdRef.current === runId;
507
560
  processingRef.current = true;
508
561
  setIsProcessing(true);
509
562
  const processData = async () => {
510
563
  // Failsafe timeout to prevent stuck processing
511
564
  timeoutId = setTimeout(() => {
512
- if (processingRef.current && !isCancelled) {
565
+ if (processingRef.current && isCurrent()) {
513
566
  setIsProcessing(false);
514
567
  processingRef.current = false;
515
568
  }
@@ -517,8 +570,10 @@ const useDataFlattening = (data, maxDepth = 10, autoExpandFirstLevel = false) =>
517
570
  // Small delay to debounce rapid changes
518
571
  // Small delay to batch rapid changes
519
572
  await new Promise(resolve => setTimeout(() => resolve(), 10));
520
- if (isCancelled) {
521
- processingRef.current = false;
573
+
574
+ // Superseded by a newer run — that run now owns `isProcessing`, so leave
575
+ // the flag alone and let it resolve.
576
+ if (!isCurrent()) {
522
577
  return;
523
578
  }
524
579
  try {
@@ -536,31 +591,26 @@ const useDataFlattening = (data, maxDepth = 10, autoExpandFirstLevel = false) =>
536
591
  });
537
592
  });
538
593
  flatDataMapRef.current = newMap;
539
- if (!isCancelled) {
594
+ if (timeoutId) clearTimeout(timeoutId);
595
+ if (isCurrent()) {
540
596
  setFlatData(newFlatData);
541
597
  setIsProcessing(false);
542
598
  processingRef.current = false;
543
- if (timeoutId) clearTimeout(timeoutId);
544
- } else {
545
- if (timeoutId) clearTimeout(timeoutId);
546
599
  }
547
600
  } catch (error) {
548
601
  console.error(error);
549
602
  // Reset to empty data on error
550
- if (!isCancelled) {
603
+ if (timeoutId) clearTimeout(timeoutId);
604
+ if (isCurrent()) {
551
605
  setFlatData([]);
552
606
  flatDataMapRef.current = new Map();
553
607
  setIsProcessing(false);
554
608
  processingRef.current = false;
555
- if (timeoutId) clearTimeout(timeoutId);
556
- } else {
557
- if (timeoutId) clearTimeout(timeoutId);
558
609
  }
559
610
  }
560
611
  };
561
612
  processData();
562
613
  return () => {
563
- isCancelled = true;
564
614
  processingRef.current = false;
565
615
  if (timeoutId) clearTimeout(timeoutId);
566
616
  };
@@ -712,10 +762,50 @@ const useDataFlattening = (data, maxDepth = 10, autoExpandFirstLevel = false) =>
712
762
  return newSet;
713
763
  });
714
764
  }, [updateFlatDataIncremental]);
765
+
766
+ /**
767
+ * Small documents flatten SYNCHRONOUSLY, during render.
768
+ *
769
+ * The chunked effect above exists so a 500-node payload can't block a frame,
770
+ * and it pays for that with an async, multi-commit rebuild every time `data`
771
+ * changes identity. Editing is immutable, so that's every single op — which
772
+ * made a three-item array rebuild itself in stages on every tap, visibly, for
773
+ * machinery it never needed. Below the chunk threshold the entire flatten is
774
+ * cheaper than one of those commits, so derive it inline and let React
775
+ * reconcile the rows normally. The chunked path is untouched above that size.
776
+ */
777
+ const isSmallDocument = countNodesUpTo(data, CHUNK_SIZE, 0) < CHUNK_SIZE;
778
+ const syncFlatData = useMemo(() => {
779
+ if (!isSmallDocument) return null;
780
+ return flattenDataStable(data, expandedItems, new WeakSet(), "root", 0, undefined, [], 0, 1, []);
781
+ }, [isSmallDocument, data, expandedItems, flattenDataStable]);
782
+
783
+ /**
784
+ * Expand a set of ancestor ids so a node deeper in the tree becomes visible.
785
+ *
786
+ * Needed because a host can relocate the selection to a node the user never
787
+ * expanded their way to — append into a collapsed array and the new entry is
788
+ * selected but off-tree, which reads as "nothing happened". Unlike
789
+ * `toggleExpanded` this doesn't patch `flatData` incrementally; the full
790
+ * flatten effect already re-runs on `expandedItems`, and a reveal is rare
791
+ * enough that the extra pass isn't worth a second incremental code path.
792
+ */
793
+ const revealPath = useCallback(ancestorIds => {
794
+ if (ancestorIds.length === 0) return;
795
+ setExpandedItems(prev => {
796
+ if (ancestorIds.every(id => prev.has(id))) return prev;
797
+ const next = new Set(prev);
798
+ for (const id of ancestorIds) next.add(id);
799
+ return next;
800
+ });
801
+ }, []);
715
802
  return {
716
- flatData,
717
- isProcessing,
718
- toggleExpanded
803
+ flatData: syncFlatData ?? flatData,
804
+ // A synchronous list is never "processing" — the spinner would flash on
805
+ // every keystroke otherwise.
806
+ isProcessing: syncFlatData ? false : isProcessing,
807
+ toggleExpanded,
808
+ revealPath
719
809
  };
720
810
  };
721
811
 
@@ -725,10 +815,28 @@ const VirtualizedItemComponent = ({
725
815
  onToggleExpanded,
726
816
  index,
727
817
  onSelect,
728
- isSelected
818
+ isSelected,
819
+ selectable,
820
+ onRequestInlineEdit,
821
+ isInlineEditing,
822
+ inlineEditor
729
823
  }) => {
730
824
  const [isPressed, setIsPressed] = useState(false);
731
825
  const [showFullKey, setShowFullKey] = useState(false);
826
+ const canEdit = selectable === true;
827
+ const lastPressRef = useRef(0);
828
+
829
+ /**
830
+ * `item.path` is addressed for the TREE, which is seeded with a synthetic
831
+ * "root" node wrapping the caller's data — so the top row is `["root"]` and
832
+ * its first child `["root", "0"]`. Ops address the DATA, where those are `[]`
833
+ * and `["0"]`. Dropping the first segment is what reconciles the two.
834
+ *
835
+ * Worth stating plainly because getting it wrong fails silently: `applyJsonOp`
836
+ * treats an unresolvable path as a no-op, so every button would simply have
837
+ * done nothing rather than erroring.
838
+ */
839
+ const dataPath = item.path.slice(1);
732
840
 
733
841
  // Use pre-computed styles to avoid inline calculations [[memory:4875251]]
734
842
  const indentStyle = INDENT_STYLES[Math.min(item.depth, MAX_DEPTH_LIMIT)] || INDENT_STYLES[0];
@@ -738,11 +846,40 @@ const VirtualizedItemComponent = ({
738
846
  const isLongKey = false;
739
847
 
740
848
  // Use inline handler since component is already memoized [[memory:4875251]]
849
+
850
+ /**
851
+ * Row body. In editable mode this SELECTS only — pressing a container to see
852
+ * its actions would otherwise collapse the very subtree you're editing, and
853
+ * the expander already exists to do the toggling (the same split every tree
854
+ * view makes). Read-only mode keeps the original toggle-on-row-press.
855
+ */
741
856
  const handlePress = () => {
857
+ if (canEdit) {
858
+ // Double-tap opens the inline editor, matching the mockup's
859
+ // double-click. Detected by hand because RN has no dblclick: two presses
860
+ // inside DOUBLE_TAP_MS on an already-selected scalar row.
861
+ const now = Date.now();
862
+ const isDouble = now - lastPressRef.current < DOUBLE_TAP_MS;
863
+ lastPressRef.current = now;
864
+ if (isDouble && !item.isExpandable && onRequestInlineEdit) {
865
+ onRequestInlineEdit(item);
866
+ return;
867
+ }
868
+ onSelect(item);
869
+ return;
870
+ }
871
+ if (item.isExpandable) {
872
+ onToggleExpanded(item.id);
873
+ }
874
+ onSelect(item);
875
+ };
876
+
877
+ /** The chevron always toggles, in both modes. */
878
+ const handleExpanderPress = () => {
742
879
  if (item.isExpandable) {
743
880
  onToggleExpanded(item.id);
744
881
  }
745
- onSelect(index);
882
+ onSelect(item);
746
883
  };
747
884
  const handleKeyPress = () => {};
748
885
 
@@ -759,11 +896,16 @@ const VirtualizedItemComponent = ({
759
896
  onPress: handlePress,
760
897
  onPressIn: () => setIsPressed(true),
761
898
  onPressOut: () => setIsPressed(false),
762
- activeOpacity: item.isExpandable ? 0.7 : 1,
763
- disabled: !item.isExpandable,
899
+ activeOpacity: item.isExpandable || canEdit ? 0.7 : 1
900
+ // A leaf row has nothing to expand, so it was left unpressable. In
901
+ // editable mode the press IS the selection that reveals this row's
902
+ // actions, so scalars — the ones you most want to edit — have to be
903
+ // pressable too.
904
+ ,
905
+ disabled: !item.isExpandable && !canEdit,
764
906
  children: [item.isExpandable ? /*#__PURE__*/_jsx(Expander, {
765
907
  expanded: item.isExpanded,
766
- onPress: handlePress
908
+ onPress: handleExpanderPress
767
909
  }) : /*#__PURE__*/_jsx(View, {
768
910
  style: STABLE_STYLES.expanderContainer
769
911
  }), /*#__PURE__*/_jsxs(View, {
@@ -772,7 +914,7 @@ const VirtualizedItemComponent = ({
772
914
  style: STABLE_STYLES.labelText,
773
915
  numberOfLines: 1,
774
916
  children: [item.key, ":"]
775
- }), item.isExpandable ? /*#__PURE__*/_jsxs(Text, {
917
+ }), isInlineEditing && inlineEditor ? inlineEditor : item.isExpandable ? /*#__PURE__*/_jsxs(Text, {
776
918
  style: [STABLE_STYLES.valueText, {
777
919
  color: gameUIColors.secondary
778
920
  }],
@@ -809,13 +951,19 @@ export const VirtualizedDataExplorer = ({
809
951
  rawMode = false,
810
952
  initialExpanded = false,
811
953
  // disableCopy is accepted for API compatibility but CopyButton handles Pro gating internally
812
- disableCopy: _disableCopy = false
954
+ disableCopy: _disableCopy = false,
955
+ onSelectionChange,
956
+ onRequestInlineEdit,
957
+ inlineEditPath,
958
+ renderInlineEditor,
959
+ selectPath
813
960
  }) => {
814
961
  const [isExpanded, setIsExpanded] = useState(rawMode); // Auto-expand in raw mode
815
962
  const {
816
963
  flatData,
817
964
  isProcessing,
818
- toggleExpanded
965
+ toggleExpanded,
966
+ revealPath
819
967
  } = useDataFlattening(data, maxDepth, initialExpanded);
820
968
 
821
969
  // Track visible range for overlay rendering
@@ -864,8 +1012,54 @@ export const VirtualizedDataExplorer = ({
864
1012
  };
865
1013
 
866
1014
  // Stable renderItem using module-scope function [[memory:4875251]]
867
- const [selectedIndex, setSelectedIndex] = useState(null);
868
- const activeDepth = selectedIndex != null ? flatData[selectedIndex]?.depth : undefined;
1015
+ /**
1016
+ * Selection is tracked by the row's stable id (its path), NOT its index.
1017
+ *
1018
+ * An edit reshapes the list — removing item 1 slides everything up — so an
1019
+ * index-based selection would silently start pointing at a different node
1020
+ * and the action bar would operate on the wrong one. An id survives that, and
1021
+ * resolves to nothing when the node it named is gone.
1022
+ */
1023
+ const [selectedId, setSelectedId] = useState(null);
1024
+ const selectedItem = useMemo(() => selectedId == null ? undefined : flatData.find(it => it.id === selectedId), [flatData, selectedId]);
1025
+ const activeDepth = selectedItem?.depth;
1026
+
1027
+ // Report the selection outward, re-emitting after each data change so the
1028
+ // action bar is looking at the CURRENT value of the node it has selected.
1029
+ useEffect(() => {
1030
+ if (!onSelectionChange) return;
1031
+ onSelectionChange(selectedItem ? selectionOf(selectedItem) : null);
1032
+ }, [selectedItem, onSelectionChange]);
1033
+
1034
+ /** Tapping the selected row again clears it, so the bar can be dismissed. */
1035
+ const handleSelect = useCallback(item => {
1036
+ setSelectedId(prev => prev === item.id ? null : item.id);
1037
+ }, []);
1038
+
1039
+ // Re-sync when the host relocates the selection. Keyed on the joined path so
1040
+ // an unchanged array identity (a fresh render) doesn't clobber a user's tap.
1041
+ const selectPathKey = selectPath === undefined ? undefined : selectPath === null ? "\u0000null" : selectPath.join(".");
1042
+ const lastSelectPathKey = useRef(selectPathKey);
1043
+ useEffect(() => {
1044
+ if (selectPathKey === lastSelectPathKey.current) return;
1045
+ lastSelectPathKey.current = selectPathKey;
1046
+ if (selectPathKey === undefined) return;
1047
+ if (selectPath == null) {
1048
+ setSelectedId(null);
1049
+ return;
1050
+ }
1051
+ // Reveal it first — a selection the user can't see reads as a no-op.
1052
+ const ancestors = ["root"];
1053
+ for (const segment of selectPath.slice(0, -1)) {
1054
+ ancestors.push(`${ancestors[ancestors.length - 1]}.${segment}`);
1055
+ }
1056
+ revealPath(ancestors);
1057
+ setSelectedId(["root", ...selectPath].join("."));
1058
+ }, [selectPathKey, selectPath, revealPath]);
1059
+ const handleRequestInlineEdit = useCallback(item => {
1060
+ setSelectedId(item.id);
1061
+ onRequestInlineEdit?.(selectionOf(item));
1062
+ }, [onRequestInlineEdit]);
869
1063
  const renderItem = ({
870
1064
  item,
871
1065
  index
@@ -873,8 +1067,12 @@ export const VirtualizedDataExplorer = ({
873
1067
  item: item,
874
1068
  index: index,
875
1069
  onToggleExpanded: toggleExpanded,
876
- onSelect: setSelectedIndex,
877
- isSelected: selectedIndex === index
1070
+ onSelect: handleSelect,
1071
+ isSelected: selectedItem?.id === item.id,
1072
+ selectable: onSelectionChange != null,
1073
+ onRequestInlineEdit: handleRequestInlineEdit,
1074
+ isInlineEditing: inlineEditPath != null && inlineEditPath.join(".") === item.path.slice(1).join("."),
1075
+ inlineEditor: renderInlineEditor ? renderInlineEditor(selectionOf(item)) : null
878
1076
  });
879
1077
 
880
1078
  // Uniform row height for crisp guide geometry
@@ -9,9 +9,24 @@ export { IndentGuides } from "./IndentGuides.js";
9
9
  export { IndentGuidesOverlay } from "./IndentGuidesOverlay.js";
10
10
  export { default as TreeDiffViewer } from "./tree/TreeDiffViewer.js";
11
11
 
12
+ // Structural edits by path — the vocabulary behind the explorer's per-node
13
+ // actions (append / insert / remove / move / rename / …), modeled on JSON Patch.
14
+ export { applyJsonOp, getAtPath, isEditableContainer, suggestedNewValue, selectionPathAfterOp } from "./jsonOps.js";
15
+
16
+ // What the selected node can do, as data. One source so the dashboard's bar
17
+ // and the phone's dock can't drift about what's legal.
18
+ export { treeActionsFor, treeActionMap, breadcrumb, typeLabel } from "./treeActions.js";
19
+
20
+ // Edit actions for the explorer's selected node. Rendered by the HOST, beside
21
+ // its own actions — a 24px tree row can't hold a touch-sized button.
22
+ export { DataTreeActionBar, DataTreeInlineEditor } from "./DataTreeActionBar.js";
23
+
24
+ // The same six actions as a thumb-sized grid, for the phone's full-screen editor.
25
+ export { DataTreeActionDock } from "./DataTreeActionDock.js";
26
+
12
27
  // Diff viewer components
13
28
  export { SplitDiffViewer } from "./SplitDiffViewer.js";
14
29
  export { DiffSummary } from "./DiffSummary.js";
15
30
  // Diff utilities and themes
16
- export { computeLineDiff, DiffType } from "./lineDiff.js";
31
+ export { computeLineDiff, DiffType, ABSENT_VALUE, isAbsent } from "./lineDiff.js";
17
32
  export { diffThemes, gitClassicTheme, devToolsDefaultTheme } from "./diffThemes.js";