@buoy-gg/shared-ui 7.0.4 → 7.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/colors/index.js +49 -0
- package/lib/commonjs/dataViewer/DataTreeActionBar.js +389 -0
- package/lib/commonjs/dataViewer/DataTreeActionDock.js +222 -0
- package/lib/commonjs/dataViewer/DataViewer.js +18 -3
- package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +227 -28
- package/lib/commonjs/dataViewer/index.js +88 -0
- package/lib/commonjs/dataViewer/jsonOps.js +320 -0
- package/lib/commonjs/dataViewer/lineDiff.js +285 -83
- package/lib/commonjs/dataViewer/tree/TreeDiffViewer.js +20 -5
- package/lib/commonjs/dataViewer/treeActions.js +167 -0
- package/lib/commonjs/env/EnvironmentSelector.js +2 -2
- package/lib/commonjs/env/EnvironmentSelectorInline.js +2 -2
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/icons/index.js +79 -3
- package/lib/commonjs/icons/lucide-icons-original-full.js +5 -1
- package/lib/commonjs/icons/lucide-icons.js +59 -2
- package/lib/commonjs/index.js +35 -0
- package/lib/commonjs/settings/components/BubbleSettingsSection.js +21 -19
- package/lib/commonjs/storage/devToolsStorageKeys.js +6 -3
- package/lib/commonjs/ui/components/Badge.js +2 -0
- package/lib/commonjs/ui/components/CompactRow.js +7 -0
- package/lib/commonjs/ui/components/CopyButton.js +27 -4
- package/lib/commonjs/ui/components/DevToolsCard.js +11 -0
- package/lib/commonjs/ui/components/DynamicFilterView.js +8 -1
- package/lib/commonjs/ui/components/EmptyState.js +12 -5
- package/lib/commonjs/ui/components/ErrorBoundary.js +3 -2
- package/lib/commonjs/ui/components/EventHistoryViewer/EventHistoryViewer.js +63 -11
- package/lib/commonjs/ui/components/ExpandableSection.js +4 -3
- package/lib/commonjs/ui/components/ExpandableSectionHeader.js +2 -1
- package/lib/commonjs/ui/components/FilterViewPattern.js +8 -7
- package/lib/commonjs/ui/components/ListItem.js +7 -5
- package/lib/commonjs/ui/components/PowerToggleButton.js +4 -1
- package/lib/commonjs/ui/components/TabSelector.js +3 -0
- package/lib/commonjs/ui/console/CyberpunkConsoleSection.js +1 -1
- package/lib/commonjs/ui/console/GalaxyButton.js +1 -1
- package/lib/commonjs/ui/gameUI/components/GameUICompactStats.js +2 -2
- package/lib/commonjs/ui/gameUI/constants/gameUIColors.js +28 -6
- package/lib/commonjs/ui/gameUI/constants/macOSDesignSystemColors.js +22 -4
- package/lib/commonjs/utils/index.js +25 -0
- package/lib/commonjs/utils/reloadApp.js +144 -0
- package/lib/module/colors/index.js +21 -0
- package/lib/module/dataViewer/DataTreeActionBar.js +384 -0
- package/lib/module/dataViewer/DataTreeActionDock.js +218 -0
- package/lib/module/dataViewer/DataViewer.js +18 -3
- package/lib/module/dataViewer/VirtualizedDataExplorer.js +228 -30
- package/lib/module/dataViewer/index.js +16 -1
- package/lib/module/dataViewer/jsonOps.js +311 -0
- package/lib/module/dataViewer/lineDiff.js +284 -82
- package/lib/module/dataViewer/tree/TreeDiffViewer.js +20 -5
- package/lib/module/dataViewer/treeActions.js +161 -0
- package/lib/module/env/EnvironmentSelector.js +2 -2
- package/lib/module/env/EnvironmentSelectorInline.js +2 -2
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/icons/index.js +10 -1
- package/lib/module/icons/lucide-icons-original-full.js +5 -1
- package/lib/module/icons/lucide-icons.js +56 -0
- package/lib/module/index.js +4 -0
- package/lib/module/settings/components/BubbleSettingsSection.js +21 -19
- package/lib/module/storage/devToolsStorageKeys.js +6 -3
- package/lib/module/ui/components/Badge.js +2 -0
- package/lib/module/ui/components/CompactRow.js +7 -0
- package/lib/module/ui/components/CopyButton.js +29 -6
- package/lib/module/ui/components/DevToolsCard.js +11 -0
- package/lib/module/ui/components/DynamicFilterView.js +8 -1
- package/lib/module/ui/components/EmptyState.js +12 -5
- package/lib/module/ui/components/ErrorBoundary.js +3 -2
- package/lib/module/ui/components/EventHistoryViewer/EventHistoryViewer.js +63 -11
- package/lib/module/ui/components/ExpandableSection.js +4 -3
- package/lib/module/ui/components/ExpandableSectionHeader.js +2 -1
- package/lib/module/ui/components/FilterViewPattern.js +9 -7
- package/lib/module/ui/components/ListItem.js +7 -5
- package/lib/module/ui/components/PowerToggleButton.js +4 -1
- package/lib/module/ui/components/TabSelector.js +3 -0
- package/lib/module/ui/console/CyberpunkConsoleSection.js +1 -1
- package/lib/module/ui/console/GalaxyButton.js +1 -1
- package/lib/module/ui/gameUI/components/GameUICompactStats.js +2 -2
- package/lib/module/ui/gameUI/constants/gameUIColors.js +27 -6
- package/lib/module/ui/gameUI/constants/macOSDesignSystemColors.js +19 -2
- package/lib/module/utils/index.js +1 -0
- package/lib/module/utils/reloadApp.js +137 -0
- package/lib/typescript/commonjs/colors/index.d.ts +22 -0
- package/lib/typescript/commonjs/colors/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts +53 -0
- package/lib/typescript/commonjs/dataViewer/DataTreeActionBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts +33 -0
- package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts +14 -1
- package/lib/typescript/commonjs/dataViewer/DataViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts +30 -1
- package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/__tests__/jsonOps.test.d.ts +19 -0
- package/lib/typescript/commonjs/dataViewer/__tests__/jsonOps.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/index.d.ts +5 -1
- package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/jsonOps.d.ts +124 -0
- package/lib/typescript/commonjs/dataViewer/jsonOps.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/lineDiff.d.ts +27 -0
- package/lib/typescript/commonjs/dataViewer/lineDiff.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts +8 -1
- package/lib/typescript/commonjs/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/treeActions.d.ts +83 -0
- package/lib/typescript/commonjs/dataViewer/treeActions.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/icons/index.d.ts +6 -1
- package/lib/typescript/commonjs/icons/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/lucide-icons-original-full.d.ts.map +1 -1
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts +9 -0
- package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings/components/BubbleSettingsSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +5 -1
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/CopyButton.d.ts +7 -1
- package/lib/typescript/commonjs/ui/components/CopyButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EmptyState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ErrorBoundary.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/EventHistoryViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts +23 -7
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ExpandableSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ExpandableSectionHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FilterViewPattern.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ListItem.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts +36 -30
- package/lib/typescript/commonjs/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/gameUI/constants/macOSDesignSystemColors.d.ts +5 -0
- package/lib/typescript/commonjs/ui/gameUI/constants/macOSDesignSystemColors.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/index.d.ts +1 -0
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/reloadApp.d.ts +68 -0
- package/lib/typescript/commonjs/utils/reloadApp.d.ts.map +1 -0
- package/lib/typescript/module/colors/index.d.ts +22 -0
- package/lib/typescript/module/colors/index.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts +53 -0
- package/lib/typescript/module/dataViewer/DataTreeActionBar.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts +33 -0
- package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/DataViewer.d.ts +14 -1
- package/lib/typescript/module/dataViewer/DataViewer.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts +30 -1
- package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/__tests__/jsonOps.test.d.ts +19 -0
- package/lib/typescript/module/dataViewer/__tests__/jsonOps.test.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/index.d.ts +5 -1
- package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/jsonOps.d.ts +124 -0
- package/lib/typescript/module/dataViewer/jsonOps.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/lineDiff.d.ts +27 -0
- package/lib/typescript/module/dataViewer/lineDiff.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts +8 -1
- package/lib/typescript/module/dataViewer/tree/TreeDiffViewer.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/treeActions.d.ts +83 -0
- package/lib/typescript/module/dataViewer/treeActions.d.ts.map +1 -0
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/icons/index.d.ts +6 -1
- package/lib/typescript/module/icons/index.d.ts.map +1 -1
- package/lib/typescript/module/icons/lucide-icons-original-full.d.ts.map +1 -1
- package/lib/typescript/module/icons/lucide-icons.d.ts +9 -0
- package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/settings/components/BubbleSettingsSection.d.ts.map +1 -1
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +5 -1
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/CopyButton.d.ts +7 -1
- package/lib/typescript/module/ui/components/CopyButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EmptyState.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ErrorBoundary.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EventHistoryViewer/EventHistoryViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts +23 -7
- package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ExpandableSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ExpandableSectionHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FilterViewPattern.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ListItem.d.ts.map +1 -1
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts +36 -30
- package/lib/typescript/module/ui/gameUI/constants/gameUIColors.d.ts.map +1 -1
- package/lib/typescript/module/ui/gameUI/constants/macOSDesignSystemColors.d.ts +5 -0
- package/lib/typescript/module/ui/gameUI/constants/macOSDesignSystemColors.d.ts.map +1 -1
- package/lib/typescript/module/utils/index.d.ts +1 -0
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/reloadApp.d.ts +68 -0
- package/lib/typescript/module/utils/reloadApp.d.ts.map +1 -0
- package/package.json +15 -4
- package/lib/commonjs/icons/ImageOverlayIcon.js +0 -142
- package/lib/module/icons/ImageOverlayIcon.js +0 -137
- package/lib/typescript/commonjs/icons/ImageOverlayIcon.d.ts +0 -14
- package/lib/typescript/commonjs/icons/ImageOverlayIcon.d.ts.map +0 -1
- package/lib/typescript/module/icons/ImageOverlayIcon.d.ts +0 -14
- 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 &&
|
|
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
|
-
|
|
521
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
718
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
868
|
-
|
|
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:
|
|
877
|
-
isSelected:
|
|
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";
|