@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.
- 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
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.applyJsonOp = applyJsonOp;
|
|
7
|
+
exports.getAtPath = getAtPath;
|
|
8
|
+
exports.isEditableContainer = isEditableContainer;
|
|
9
|
+
exports.isRebuildableObject = isRebuildableObject;
|
|
10
|
+
exports.selectionPathAfterOp = selectionPathAfterOp;
|
|
11
|
+
exports.suggestedNewValue = suggestedNewValue;
|
|
12
|
+
/**
|
|
13
|
+
* Structural edits to a JSON value, addressed by path.
|
|
14
|
+
*
|
|
15
|
+
* Why an op vocabulary instead of "here's the new value": editing a container
|
|
16
|
+
* as text is fine for a three-item array and miserable for a fifty-item one.
|
|
17
|
+
* Every real JSON editor — React DevTools, TanStack Query's explorer,
|
|
18
|
+
* josdejong's JSON Editor — instead exposes per-node actions, and they all
|
|
19
|
+
* converge on the same primitives because JSON Patch (RFC 6902) already
|
|
20
|
+
* standardized them: add / remove / replace / move / copy, addressed by a
|
|
21
|
+
* pointer into the document.
|
|
22
|
+
*
|
|
23
|
+
* This is that set, named for humans and specialized for a UI:
|
|
24
|
+
* - array-shaped ops carry the intent ("append", "insert at 3") rather than
|
|
25
|
+
* making the caller compute the resulting array
|
|
26
|
+
* - `renameKey` preserves key ORDER, which a naive delete-then-add loses and
|
|
27
|
+
* which readers notice immediately
|
|
28
|
+
*
|
|
29
|
+
* Ops are pure and immutable: `applyJsonOp` returns a new root and never
|
|
30
|
+
* mutates the input, so callers can diff, undo, or discard freely.
|
|
31
|
+
*
|
|
32
|
+
* A `path` is the same `string[]` the data explorer already flattens rows with
|
|
33
|
+
* (a JSON Pointer in disguise). `[]` addresses the root. Array indices are
|
|
34
|
+
* their decimal string, matching how the explorer emits them.
|
|
35
|
+
*
|
|
36
|
+
* Scope: plain arrays and plain objects. Anything else at a path (Map, Set,
|
|
37
|
+
* class instances) is returned untouched — those can't round-trip through JSON
|
|
38
|
+
* anyway, and silently rebuilding one as a plain object would lose more than
|
|
39
|
+
* it fixed.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/** A structural edit. `path` addresses the node the verb reads naturally against. */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Objects this module is willing to REBUILD with a spread.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately stricter than `isPlainObject` in `types/types`: that one is a
|
|
48
|
+
* denylist (not a Date, Map, Set, …) and so admits class instances, which is
|
|
49
|
+
* right for rendering but wrong here — `{ ...instance }` silently drops the
|
|
50
|
+
* prototype and hands back something that no longer behaves like the original.
|
|
51
|
+
* A prototype check is the only way to know a spread round-trips.
|
|
52
|
+
*/
|
|
53
|
+
function isRebuildableObject(value) {
|
|
54
|
+
if (value === null || typeof value !== "object") return false;
|
|
55
|
+
if (Array.isArray(value)) return false;
|
|
56
|
+
const proto = Object.getPrototypeOf(value);
|
|
57
|
+
return proto === Object.prototype || proto === null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where the selection should sit after `op` is applied.
|
|
62
|
+
*
|
|
63
|
+
* An op reshapes the document, so the path the user had selected may now name a
|
|
64
|
+
* DIFFERENT node — move item 3 to slot 2 and path `3` is whatever got displaced,
|
|
65
|
+
* not the thing you just moved. Following the edit is what makes the highlight
|
|
66
|
+
* mean "the node I acted on" rather than "the slot I acted from".
|
|
67
|
+
*
|
|
68
|
+
* Returns `undefined` to leave the selection alone, `null` to clear it.
|
|
69
|
+
*/
|
|
70
|
+
function selectionPathAfterOp(op) {
|
|
71
|
+
switch (op.kind) {
|
|
72
|
+
// Follow the node to its new slot.
|
|
73
|
+
case "move":
|
|
74
|
+
return [...op.path.slice(0, -1), String(op.toIndex)];
|
|
75
|
+
// The copy lands directly after the original, and is the thing you meant.
|
|
76
|
+
case "duplicate":
|
|
77
|
+
{
|
|
78
|
+
const last = op.path[op.path.length - 1];
|
|
79
|
+
const index = Number(last);
|
|
80
|
+
return Number.isInteger(index) ? [...op.path.slice(0, -1), String(index + 1)] : undefined;
|
|
81
|
+
}
|
|
82
|
+
// The selected node no longer exists.
|
|
83
|
+
case "remove":
|
|
84
|
+
return null;
|
|
85
|
+
default:
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when a path addresses a container this module can edit. */
|
|
91
|
+
function isEditableContainer(value) {
|
|
92
|
+
return Array.isArray(value) || isRebuildableObject(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read the value at `path`, or undefined if the path doesn't resolve. */
|
|
96
|
+
function getAtPath(root, path) {
|
|
97
|
+
let node = root;
|
|
98
|
+
for (const segment of path) {
|
|
99
|
+
if (Array.isArray(node)) {
|
|
100
|
+
node = node[Number(segment)];
|
|
101
|
+
} else if (isRebuildableObject(node)) {
|
|
102
|
+
node = node[segment];
|
|
103
|
+
} else {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return node;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Rebuild `root` with the container at `path` replaced by `updater`'s result.
|
|
112
|
+
*
|
|
113
|
+
* Everything else is expressed through this: an op on an entry is an op on its
|
|
114
|
+
* PARENT container, which is also why removal and rename are possible at all —
|
|
115
|
+
* they change the shape of the parent, not the value of the child.
|
|
116
|
+
*/
|
|
117
|
+
function updateContainer(root, path, updater) {
|
|
118
|
+
if (path.length === 0) return updater(root);
|
|
119
|
+
const [head, ...rest] = path;
|
|
120
|
+
if (Array.isArray(root)) {
|
|
121
|
+
const index = Number(head);
|
|
122
|
+
if (!Number.isInteger(index) || index < 0 || index >= root.length) {
|
|
123
|
+
return root;
|
|
124
|
+
}
|
|
125
|
+
const next = [...root];
|
|
126
|
+
next[index] = updateContainer(next[index], rest, updater);
|
|
127
|
+
return next;
|
|
128
|
+
}
|
|
129
|
+
if (isRebuildableObject(root)) {
|
|
130
|
+
if (!(head in root)) return root;
|
|
131
|
+
return {
|
|
132
|
+
...root,
|
|
133
|
+
[head]: updateContainer(root[head], rest, updater)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Path runs past something we don't edit — leave the document alone.
|
|
138
|
+
return root;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Split a path into its parent path and final segment. */
|
|
142
|
+
function splitPath(path) {
|
|
143
|
+
if (path.length === 0) return null;
|
|
144
|
+
return {
|
|
145
|
+
parent: path.slice(0, -1),
|
|
146
|
+
last: path[path.length - 1]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Apply `op` to `root`, returning a new value.
|
|
152
|
+
*
|
|
153
|
+
* Never throws and never mutates: an op that doesn't apply (bad path, wrong
|
|
154
|
+
* container type, index out of range) returns `root` unchanged, so a stale
|
|
155
|
+
* click from a tree that re-rendered underneath the user is a no-op rather
|
|
156
|
+
* than a corrupted document.
|
|
157
|
+
*/
|
|
158
|
+
function applyJsonOp(root, op) {
|
|
159
|
+
switch (op.kind) {
|
|
160
|
+
case "replace":
|
|
161
|
+
return updateContainer(root, op.path, () => op.value);
|
|
162
|
+
case "clear":
|
|
163
|
+
return updateContainer(root, op.path, container => {
|
|
164
|
+
if (Array.isArray(container)) return [];
|
|
165
|
+
if (isRebuildableObject(container)) return {};
|
|
166
|
+
return container;
|
|
167
|
+
});
|
|
168
|
+
case "append":
|
|
169
|
+
return updateContainer(root, op.path, container => Array.isArray(container) ? [...container, op.value] : container);
|
|
170
|
+
case "insert":
|
|
171
|
+
return updateContainer(root, op.path, container => {
|
|
172
|
+
if (!Array.isArray(container)) return container;
|
|
173
|
+
const at = clamp(op.index, 0, container.length);
|
|
174
|
+
return [...container.slice(0, at), op.value, ...container.slice(at)];
|
|
175
|
+
});
|
|
176
|
+
case "addKey":
|
|
177
|
+
return updateContainer(root, op.path, container => {
|
|
178
|
+
if (!isRebuildableObject(container)) return container;
|
|
179
|
+
// Refuse to clobber: "add" that silently overwrote an existing key
|
|
180
|
+
// would lose data the user never saw.
|
|
181
|
+
if (op.key in container) return container;
|
|
182
|
+
return {
|
|
183
|
+
...container,
|
|
184
|
+
[op.key]: op.value
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
case "remove":
|
|
188
|
+
{
|
|
189
|
+
const split = splitPath(op.path);
|
|
190
|
+
if (!split) return root; // Removing the root is meaningless.
|
|
191
|
+
return updateContainer(root, split.parent, container => removeEntry(container, split.last));
|
|
192
|
+
}
|
|
193
|
+
case "duplicate":
|
|
194
|
+
{
|
|
195
|
+
const split = splitPath(op.path);
|
|
196
|
+
if (!split) return root;
|
|
197
|
+
return updateContainer(root, split.parent, container => duplicateEntry(container, split.last));
|
|
198
|
+
}
|
|
199
|
+
case "move":
|
|
200
|
+
{
|
|
201
|
+
const split = splitPath(op.path);
|
|
202
|
+
if (!split) return root;
|
|
203
|
+
return updateContainer(root, split.parent, container => {
|
|
204
|
+
if (!Array.isArray(container)) return container;
|
|
205
|
+
const from = Number(split.last);
|
|
206
|
+
if (!Number.isInteger(from) || from < 0 || from >= container.length) {
|
|
207
|
+
return container;
|
|
208
|
+
}
|
|
209
|
+
const to = clamp(op.toIndex, 0, container.length - 1);
|
|
210
|
+
if (to === from) return container;
|
|
211
|
+
const next = [...container];
|
|
212
|
+
const [item] = next.splice(from, 1);
|
|
213
|
+
next.splice(to, 0, item);
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
case "renameKey":
|
|
218
|
+
{
|
|
219
|
+
const split = splitPath(op.path);
|
|
220
|
+
if (!split) return root;
|
|
221
|
+
return updateContainer(root, split.parent, container => renameEntry(container, split.last, op.key));
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
return root;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function removeEntry(container, segment) {
|
|
228
|
+
if (Array.isArray(container)) {
|
|
229
|
+
const index = Number(segment);
|
|
230
|
+
if (!Number.isInteger(index) || index < 0 || index >= container.length) {
|
|
231
|
+
return container;
|
|
232
|
+
}
|
|
233
|
+
return container.filter((_, i) => i !== index);
|
|
234
|
+
}
|
|
235
|
+
if (isRebuildableObject(container)) {
|
|
236
|
+
if (!(segment in container)) return container;
|
|
237
|
+
const next = {};
|
|
238
|
+
for (const key of Object.keys(container)) {
|
|
239
|
+
if (key !== segment) next[key] = container[key];
|
|
240
|
+
}
|
|
241
|
+
return next;
|
|
242
|
+
}
|
|
243
|
+
return container;
|
|
244
|
+
}
|
|
245
|
+
function duplicateEntry(container, segment) {
|
|
246
|
+
if (Array.isArray(container)) {
|
|
247
|
+
const index = Number(segment);
|
|
248
|
+
if (!Number.isInteger(index) || index < 0 || index >= container.length) {
|
|
249
|
+
return container;
|
|
250
|
+
}
|
|
251
|
+
return [...container.slice(0, index + 1), clone(container[index]), ...container.slice(index + 1)];
|
|
252
|
+
}
|
|
253
|
+
if (isRebuildableObject(container)) {
|
|
254
|
+
if (!(segment in container)) return container;
|
|
255
|
+
// Objects have no "next to" — mint a non-colliding key and keep position.
|
|
256
|
+
const copyKey = uniqueKey(container, `${segment} copy`);
|
|
257
|
+
const next = {};
|
|
258
|
+
for (const key of Object.keys(container)) {
|
|
259
|
+
next[key] = container[key];
|
|
260
|
+
if (key === segment) next[copyKey] = clone(container[key]);
|
|
261
|
+
}
|
|
262
|
+
return next;
|
|
263
|
+
}
|
|
264
|
+
return container;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Rename in place. Rebuilt key-by-key rather than delete-then-assign so the
|
|
269
|
+
* entry keeps its position — an object whose keys reshuffle on every rename
|
|
270
|
+
* reads as though the edit did something it didn't.
|
|
271
|
+
*/
|
|
272
|
+
function renameEntry(container, segment, nextKey) {
|
|
273
|
+
if (!isRebuildableObject(container)) return container;
|
|
274
|
+
if (!(segment in container)) return container;
|
|
275
|
+
if (nextKey === segment) return container;
|
|
276
|
+
if (nextKey in container) return container; // Would clobber a sibling.
|
|
277
|
+
|
|
278
|
+
const next = {};
|
|
279
|
+
for (const key of Object.keys(container)) {
|
|
280
|
+
if (key === segment) next[nextKey] = container[key];else next[key] = container[key];
|
|
281
|
+
}
|
|
282
|
+
return next;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** `base`, `base 2`, `base 3`… until it doesn't collide. */
|
|
286
|
+
function uniqueKey(container, base) {
|
|
287
|
+
if (!(base in container)) return base;
|
|
288
|
+
let n = 2;
|
|
289
|
+
while (`${base} ${n}` in container) n++;
|
|
290
|
+
return `${base} ${n}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Structural copy, so a duplicated container isn't shared with its original. */
|
|
294
|
+
function clone(value) {
|
|
295
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
296
|
+
if (isRebuildableObject(value)) {
|
|
297
|
+
const next = {};
|
|
298
|
+
for (const key of Object.keys(value)) next[key] = clone(value[key]);
|
|
299
|
+
return next;
|
|
300
|
+
}
|
|
301
|
+
return value;
|
|
302
|
+
}
|
|
303
|
+
function clamp(value, min, max) {
|
|
304
|
+
if (!Number.isFinite(value)) return min;
|
|
305
|
+
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A sensible starting value when adding an entry, matching the container's
|
|
310
|
+
* existing items so "append" to an array of numbers doesn't hand back a string.
|
|
311
|
+
*/
|
|
312
|
+
function suggestedNewValue(container) {
|
|
313
|
+
const samples = Array.isArray(container) ? container : isRebuildableObject(container) ? Object.values(container) : [];
|
|
314
|
+
const last = samples[samples.length - 1];
|
|
315
|
+
if (typeof last === "number") return 0;
|
|
316
|
+
if (typeof last === "boolean") return false;
|
|
317
|
+
if (Array.isArray(last)) return [];
|
|
318
|
+
if (isRebuildableObject(last)) return {};
|
|
319
|
+
return "";
|
|
320
|
+
}
|