@visual-json/react 0.1.1 → 0.3.0

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/dist/index.js CHANGED
@@ -25,7 +25,6 @@ __export(index_exports, {
25
25
  DiffView: () => DiffView,
26
26
  FormView: () => FormView,
27
27
  JsonEditor: () => JsonEditor,
28
- PropertyEditor: () => PropertyEditor,
29
28
  SearchBar: () => SearchBar,
30
29
  StudioContext: () => StudioContext,
31
30
  TreeView: () => TreeView,
@@ -40,7 +39,175 @@ var import_react10 = require("react");
40
39
 
41
40
  // src/visual-json.tsx
42
41
  var import_react2 = require("react");
42
+ var import_core4 = require("@visual-json/core");
43
+
44
+ // ../../@internal/ui/dist/index.mjs
43
45
  var import_core = require("@visual-json/core");
46
+ var import_core2 = require("@visual-json/core");
47
+ function getVisibleNodes(root, isExpanded) {
48
+ const result = [];
49
+ function walk(node) {
50
+ result.push(node);
51
+ if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
52
+ for (const child of node.children) {
53
+ walk(child);
54
+ }
55
+ }
56
+ }
57
+ walk(root);
58
+ return result;
59
+ }
60
+ var LABEL_FIELDS = ["name", "type", "title", "id", "label", "key"];
61
+ function getDisplayKey(node, state) {
62
+ if (node.parentId === null) return "/";
63
+ const parent = state.nodesById.get(node.parentId);
64
+ if (parent?.type !== "array" || node.type !== "object") return node.key;
65
+ for (const field of LABEL_FIELDS) {
66
+ const child = node.children.find((c) => c.key === field);
67
+ if (child?.value != null && child.value !== "") {
68
+ return String(child.value);
69
+ }
70
+ }
71
+ return node.key;
72
+ }
73
+ function collectAllIds(node) {
74
+ const ids = [node.id];
75
+ for (const child of node.children) {
76
+ ids.push(...collectAllIds(child));
77
+ }
78
+ return ids;
79
+ }
80
+ var DEFAULT_CSS_VARS = {
81
+ "--vj-bg": "#1e1e1e",
82
+ "--vj-bg-panel": "#252526",
83
+ "--vj-bg-hover": "#2a2d2e",
84
+ "--vj-bg-selected": "#2a5a1e",
85
+ "--vj-bg-selected-muted": "#2a2d2e",
86
+ "--vj-bg-match": "#3a3520",
87
+ "--vj-bg-match-active": "#51502b",
88
+ "--vj-border": "#333333",
89
+ "--vj-border-subtle": "#2a2a2a",
90
+ "--vj-text": "#cccccc",
91
+ "--vj-text-muted": "#888888",
92
+ "--vj-text-dim": "#666666",
93
+ "--vj-text-dimmer": "#555555",
94
+ "--vj-string": "#ce9178",
95
+ "--vj-number": "#b5cea8",
96
+ "--vj-boolean": "#569cd6",
97
+ "--vj-accent": "#007acc",
98
+ "--vj-accent-muted": "#094771",
99
+ "--vj-input-bg": "#3c3c3c",
100
+ "--vj-input-border": "#555555",
101
+ "--vj-error": "#f48771",
102
+ "--vj-font": "monospace",
103
+ "--vj-input-font-size": "13px"
104
+ };
105
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
106
+ function sortByTreeOrder(root, ids) {
107
+ const result = [];
108
+ function walk(node) {
109
+ if (ids.has(node.id)) result.push(node.id);
110
+ for (const child of node.children) walk(child);
111
+ }
112
+ walk(root);
113
+ return result;
114
+ }
115
+ function computeDrop(tree, drag) {
116
+ const { draggedNodeIds, dropTargetNodeId, dropPosition } = drag;
117
+ if (draggedNodeIds.size === 0 || !dropTargetNodeId || !dropPosition)
118
+ return null;
119
+ const targetNode = tree.nodesById.get(dropTargetNodeId);
120
+ if (!targetNode || !targetNode.parentId) return null;
121
+ for (const id of draggedNodeIds) {
122
+ if ((0, import_core.isDescendant)(tree, dropTargetNodeId, id)) return null;
123
+ }
124
+ const targetParentId = targetNode.parentId;
125
+ const targetParent = tree.nodesById.get(targetParentId);
126
+ if (!targetParent) return null;
127
+ const parentChildren = targetParent.children;
128
+ const orderedDragIds = parentChildren.filter((c) => draggedNodeIds.has(c.id)).map((c) => c.id);
129
+ const allSameParent = orderedDragIds.length === draggedNodeIds.size && [...draggedNodeIds].every((id) => {
130
+ const n = tree.nodesById.get(id);
131
+ return n?.parentId === targetParentId;
132
+ });
133
+ if (allSameParent) {
134
+ return (0, import_core.reorderChildrenMulti)(
135
+ tree,
136
+ targetParentId,
137
+ orderedDragIds,
138
+ dropTargetNodeId,
139
+ dropPosition
140
+ );
141
+ }
142
+ const orderedIds = sortByTreeOrder(tree.root, draggedNodeIds);
143
+ const draggedNodes = orderedIds.map((id) => tree.nodesById.get(id)).filter((n) => !!n && n.parentId !== null).map((n) => structuredClone(n));
144
+ let newTree = tree;
145
+ for (const id of [...orderedIds].reverse()) {
146
+ if (newTree.nodesById.has(id)) {
147
+ newTree = (0, import_core.removeNode)(newTree, id);
148
+ }
149
+ }
150
+ const updatedTarget = newTree.nodesById.get(dropTargetNodeId);
151
+ if (!updatedTarget || !updatedTarget.parentId) return null;
152
+ const updatedParent = newTree.nodesById.get(updatedTarget.parentId);
153
+ if (!updatedParent) return null;
154
+ let insertIdx = updatedParent.children.findIndex(
155
+ (c) => c.id === dropTargetNodeId
156
+ );
157
+ if (dropPosition === "after") insertIdx++;
158
+ for (let i = 0; i < draggedNodes.length; i++) {
159
+ newTree = (0, import_core.insertNode)(
160
+ newTree,
161
+ updatedParent.id,
162
+ draggedNodes[i],
163
+ insertIdx + i
164
+ );
165
+ }
166
+ return newTree;
167
+ }
168
+ function setMultiDragImage(dataTransfer, count, rootEl) {
169
+ const ghost = document.createElement("div");
170
+ ghost.textContent = `${count} selected`;
171
+ const root = rootEl ?? document.querySelector("[data-form-container], [role='tree']");
172
+ const cs = root ? getComputedStyle(root) : null;
173
+ const bg = cs?.getPropertyValue("--vj-bg-selected").trim() || DEFAULT_CSS_VARS["--vj-bg-selected"];
174
+ const fg = cs?.getPropertyValue("--vj-text-selected").trim() || cs?.getPropertyValue("--vj-text").trim() || DEFAULT_CSS_VARS["--vj-text"];
175
+ const font = cs?.getPropertyValue("--vj-font").trim() || DEFAULT_CSS_VARS["--vj-font"];
176
+ ghost.style.cssText = [
177
+ "position:fixed",
178
+ "top:-1000px",
179
+ "left:-1000px",
180
+ "padding:4px 12px",
181
+ `background:${bg}`,
182
+ `color:${fg}`,
183
+ `font-family:${font}`,
184
+ "font-size:13px",
185
+ "border-radius:4px",
186
+ "white-space:nowrap",
187
+ "pointer-events:none"
188
+ ].join(";");
189
+ document.body.appendChild(ghost);
190
+ dataTransfer.setDragImage(ghost, 0, 14);
191
+ requestAnimationFrame(() => ghost.remove());
192
+ }
193
+ var DIFF_COLORS = {
194
+ added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
195
+ removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
196
+ changed: { bg: "#3a3a1e", marker: "~", label: "#dcdcaa" }
197
+ };
198
+ function formatValue(value) {
199
+ if (value === void 0) return "";
200
+ if (value === null) return "null";
201
+ if (typeof value === "string") return JSON.stringify(value);
202
+ if (typeof value === "object") {
203
+ const json = JSON.stringify(value, null, 2);
204
+ if (json.length > 80) {
205
+ return JSON.stringify(value).slice(0, 77) + "...";
206
+ }
207
+ return json;
208
+ }
209
+ return String(value);
210
+ }
44
211
 
45
212
  // src/context.ts
46
213
  var import_react = require("react");
@@ -53,27 +220,122 @@ function useStudio() {
53
220
  return ctx;
54
221
  }
55
222
 
56
- // src/visual-json.tsx
57
- var import_jsx_runtime = require("react/jsx-runtime");
58
- function collectAllIds(node) {
59
- const ids = [node.id];
60
- for (const child of node.children) {
61
- ids.push(...collectAllIds(child));
223
+ // src/selection-utils.ts
224
+ var import_core3 = require("@visual-json/core");
225
+ function computeSelectAllIds(tree, focusedNodeId, currentlySelected) {
226
+ if (!focusedNodeId) return null;
227
+ let node = tree.nodesById.get(focusedNodeId);
228
+ if (!node) return null;
229
+ while (node) {
230
+ const parent = node.parentId ? tree.nodesById.get(node.parentId) : void 0;
231
+ const siblings = parent ? parent.children : [tree.root];
232
+ const siblingIds = new Set(siblings.map((s) => s.id));
233
+ const allSelected = siblings.every((s) => currentlySelected.has(s.id));
234
+ if (!allSelected) {
235
+ return siblingIds;
236
+ }
237
+ if (!parent || !parent.parentId) return siblingIds;
238
+ node = parent;
239
+ }
240
+ return null;
241
+ }
242
+ function computeRangeIds(visibleNodes, anchorId, targetId) {
243
+ const anchorIdx = visibleNodes.findIndex((n) => n.id === anchorId);
244
+ const targetIdx = visibleNodes.findIndex((n) => n.id === targetId);
245
+ if (anchorIdx === -1 || targetIdx === -1) return null;
246
+ const start = Math.min(anchorIdx, targetIdx);
247
+ const end = Math.max(anchorIdx, targetIdx);
248
+ const ids = /* @__PURE__ */ new Set();
249
+ for (let i = start; i <= end; i++) {
250
+ ids.add(visibleNodes[i].id);
62
251
  }
63
252
  return ids;
64
253
  }
254
+ function deleteSelectedNodes(tree, selectedIds, visibleNodes) {
255
+ const idsToDelete = [...selectedIds].filter((id) => {
256
+ const node = tree.nodesById.get(id);
257
+ if (!node || node.parentId === null) return false;
258
+ let cur = tree.nodesById.get(node.parentId);
259
+ while (cur) {
260
+ if (selectedIds.has(cur.id)) return false;
261
+ cur = cur.parentId ? tree.nodesById.get(cur.parentId) : void 0;
262
+ }
263
+ return true;
264
+ });
265
+ if (idsToDelete.length === 0) return { newTree: tree, nextFocusId: null };
266
+ const firstDeletedIdx = visibleNodes.findIndex((n) => selectedIds.has(n.id));
267
+ let newTree = tree;
268
+ for (const id of idsToDelete) {
269
+ if (newTree.nodesById.has(id)) {
270
+ newTree = (0, import_core3.removeNode)(newTree, id);
271
+ }
272
+ }
273
+ let nextFocusId = null;
274
+ for (let i = firstDeletedIdx; i < visibleNodes.length; i++) {
275
+ const id = visibleNodes[i].id;
276
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
277
+ nextFocusId = id;
278
+ break;
279
+ }
280
+ }
281
+ if (!nextFocusId) {
282
+ for (let i = firstDeletedIdx - 1; i >= 0; i--) {
283
+ const id = visibleNodes[i].id;
284
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
285
+ nextFocusId = id;
286
+ break;
287
+ }
288
+ }
289
+ }
290
+ return { newTree, nextFocusId };
291
+ }
292
+
293
+ // src/visual-json.tsx
294
+ var import_jsx_runtime = require("react/jsx-runtime");
65
295
  function VisualJson({
66
296
  value,
67
297
  onChange,
68
298
  schema,
69
299
  children
70
300
  }) {
71
- const [tree, setTreeState] = (0, import_react2.useState)(() => (0, import_core.fromJson)(value));
72
- const [selectedNodeId, setSelectedNodeId] = (0, import_react2.useState)(null);
301
+ const [tree, setTreeState] = (0, import_react2.useState)(() => (0, import_core4.fromJson)(value));
302
+ const [focusedNodeId, setFocusedNodeId] = (0, import_react2.useState)(null);
303
+ const [selectedNodeIds, setSelectedNodeIdsState] = (0, import_react2.useState)(
304
+ () => /* @__PURE__ */ new Set()
305
+ );
306
+ const selectedNodeIdsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
307
+ const setSelectedNodeIds = (0, import_react2.useCallback)((ids) => {
308
+ selectedNodeIdsRef.current = ids;
309
+ setSelectedNodeIdsState(ids);
310
+ }, []);
311
+ const anchorNodeIdRef = (0, import_react2.useRef)(null);
312
+ const [anchorNodeId, setAnchorNodeIdState] = (0, import_react2.useState)(null);
313
+ const [drillDownNodeId, setDrillDownNodeId] = (0, import_react2.useState)(null);
73
314
  const [expandedNodeIds, setExpandedNodeIds] = (0, import_react2.useState)(
74
315
  () => /* @__PURE__ */ new Set([tree.root.id])
75
316
  );
76
- const historyRef = (0, import_react2.useRef)(new import_core.History());
317
+ const setAnchorNodeId = (0, import_react2.useCallback)((id) => {
318
+ anchorNodeIdRef.current = id;
319
+ setAnchorNodeIdState(id);
320
+ }, []);
321
+ const focusSelectAndDrillDown = (0, import_react2.useCallback)(
322
+ (nodeId) => {
323
+ setFocusedNodeId(nodeId);
324
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
325
+ setAnchorNodeId(nodeId);
326
+ setDrillDownNodeId(nodeId);
327
+ },
328
+ [setSelectedNodeIds, setAnchorNodeId]
329
+ );
330
+ const visibleNodes = (0, import_react2.useMemo)(
331
+ () => getVisibleNodes(tree.root, (id) => expandedNodeIds.has(id)),
332
+ [tree.root, expandedNodeIds]
333
+ );
334
+ const visibleNodesOverrideRef = (0, import_react2.useRef)(null);
335
+ const setVisibleNodesOverride = (0, import_react2.useCallback)((nodes) => {
336
+ visibleNodesOverrideRef.current = nodes;
337
+ }, []);
338
+ const historyRef = (0, import_react2.useRef)(new import_core4.History());
77
339
  const isInternalChange = (0, import_react2.useRef)(false);
78
340
  const hasMounted = (0, import_react2.useRef)(false);
79
341
  const [canUndo, setCanUndo] = (0, import_react2.useState)(false);
@@ -98,11 +360,11 @@ function VisualJson({
98
360
  isInternalChange.current = false;
99
361
  return;
100
362
  }
101
- const newTree = (0, import_core.fromJson)(value);
363
+ const newTree = (0, import_core4.fromJson)(value);
102
364
  setTreeState(newTree);
103
365
  setExpandedNodeIds(/* @__PURE__ */ new Set([newTree.root.id]));
104
- setSelectedNodeId(null);
105
- historyRef.current = new import_core.History();
366
+ focusSelectAndDrillDown(null);
367
+ historyRef.current = new import_core4.History();
106
368
  historyRef.current.push(newTree);
107
369
  setCanUndo(false);
108
370
  setCanRedo(false);
@@ -118,7 +380,7 @@ function VisualJson({
118
380
  setCanUndo(historyRef.current.canUndo);
119
381
  setCanRedo(historyRef.current.canRedo);
120
382
  isInternalChange.current = true;
121
- onChange?.((0, import_core.toJson)(newTree.root));
383
+ onChange?.((0, import_core4.toJson)(newTree.root));
122
384
  },
123
385
  [onChange]
124
386
  );
@@ -129,7 +391,7 @@ function VisualJson({
129
391
  setCanUndo(historyRef.current.canUndo);
130
392
  setCanRedo(historyRef.current.canRedo);
131
393
  isInternalChange.current = true;
132
- onChange?.((0, import_core.toJson)(prev.root));
394
+ onChange?.((0, import_core4.toJson)(prev.root));
133
395
  }
134
396
  }, [onChange]);
135
397
  const redo = (0, import_react2.useCallback)(() => {
@@ -139,7 +401,7 @@ function VisualJson({
139
401
  setCanUndo(historyRef.current.canUndo);
140
402
  setCanRedo(historyRef.current.canRedo);
141
403
  isInternalChange.current = true;
142
- onChange?.((0, import_core.toJson)(next.root));
404
+ onChange?.((0, import_core4.toJson)(next.root));
143
405
  }
144
406
  }, [onChange]);
145
407
  (0, import_react2.useEffect)(() => {
@@ -159,8 +421,66 @@ function VisualJson({
159
421
  document.addEventListener("keydown", handleKeyDown);
160
422
  return () => document.removeEventListener("keydown", handleKeyDown);
161
423
  }, [undo, redo]);
162
- const selectNode = (0, import_react2.useCallback)((nodeId) => {
163
- setSelectedNodeId(nodeId);
424
+ const selectNode = (0, import_react2.useCallback)(
425
+ (nodeId) => {
426
+ setFocusedNodeId(nodeId);
427
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
428
+ setAnchorNodeId(nodeId);
429
+ },
430
+ [setSelectedNodeIds, setAnchorNodeId]
431
+ );
432
+ const selectAndDrillDown = focusSelectAndDrillDown;
433
+ const toggleNodeSelection = (0, import_react2.useCallback)(
434
+ (nodeId) => {
435
+ const next = new Set(selectedNodeIdsRef.current);
436
+ if (next.has(nodeId)) {
437
+ next.delete(nodeId);
438
+ } else {
439
+ next.add(nodeId);
440
+ }
441
+ setSelectedNodeIds(next);
442
+ if (next.size === 0) {
443
+ setFocusedNodeId(null);
444
+ setAnchorNodeId(null);
445
+ } else {
446
+ setFocusedNodeId(nodeId);
447
+ setAnchorNodeId(nodeId);
448
+ }
449
+ },
450
+ [setSelectedNodeIds, setAnchorNodeId]
451
+ );
452
+ const selectNodeRange = (0, import_react2.useCallback)(
453
+ (toNodeId) => {
454
+ const nodes = visibleNodesOverrideRef.current ?? visibleNodes;
455
+ const anchor = anchorNodeIdRef.current;
456
+ if (!anchor) {
457
+ setFocusedNodeId(toNodeId);
458
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
459
+ setAnchorNodeId(toNodeId);
460
+ return;
461
+ }
462
+ const rangeIds = computeRangeIds(nodes, anchor, toNodeId);
463
+ if (!rangeIds) {
464
+ setFocusedNodeId(toNodeId);
465
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
466
+ setAnchorNodeId(toNodeId);
467
+ return;
468
+ }
469
+ setSelectedNodeIds(rangeIds);
470
+ setFocusedNodeId(toNodeId);
471
+ },
472
+ [visibleNodes, setSelectedNodeIds, setAnchorNodeId]
473
+ );
474
+ const setSelection = (0, import_react2.useCallback)(
475
+ (focusedId, newSelectedIds, newAnchorId) => {
476
+ setFocusedNodeId(focusedId);
477
+ setSelectedNodeIds(newSelectedIds);
478
+ setAnchorNodeId(newAnchorId);
479
+ },
480
+ [setSelectedNodeIds, setAnchorNodeId]
481
+ );
482
+ const drillDown = (0, import_react2.useCallback)((nodeId) => {
483
+ setDrillDownNodeId(nodeId);
164
484
  }, []);
165
485
  const toggleExpand = (0, import_react2.useCallback)((nodeId) => {
166
486
  setExpandedNodeIds((prev) => {
@@ -200,13 +520,14 @@ function VisualJson({
200
520
  setSearchMatchNodeIds(/* @__PURE__ */ new Set());
201
521
  return;
202
522
  }
203
- const matches = (0, import_core.searchNodes)(tree, query);
523
+ const matches = (0, import_core4.searchNodes)(tree, query);
204
524
  setSearchMatches(matches);
205
525
  setSearchMatchIndex(0);
206
526
  const matchIds = new Set(matches.map((m) => m.nodeId));
207
527
  setSearchMatchNodeIds(matchIds);
208
528
  if (matches.length > 0) {
209
- const ancestors = (0, import_core.getAncestorIds)(
529
+ const firstId = matches[0].nodeId;
530
+ const ancestors = (0, import_core4.getAncestorIds)(
210
531
  tree,
211
532
  matches.map((m) => m.nodeId)
212
533
  );
@@ -215,7 +536,7 @@ function VisualJson({
215
536
  for (const id of ancestors) next.add(id);
216
537
  return next;
217
538
  });
218
- setSelectedNodeId(matches[0].nodeId);
539
+ focusSelectAndDrillDown(firstId);
219
540
  }
220
541
  },
221
542
  [tree]
@@ -224,17 +545,17 @@ function VisualJson({
224
545
  if (searchMatches.length === 0) return;
225
546
  const nextIdx = (searchMatchIndex + 1) % searchMatches.length;
226
547
  setSearchMatchIndex(nextIdx);
227
- setSelectedNodeId(searchMatches[nextIdx].nodeId);
228
- }, [searchMatches, searchMatchIndex]);
548
+ focusSelectAndDrillDown(searchMatches[nextIdx].nodeId);
549
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
229
550
  const prevSearchMatch = (0, import_react2.useCallback)(() => {
230
551
  if (searchMatches.length === 0) return;
231
552
  const prevIdx = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length;
232
553
  setSearchMatchIndex(prevIdx);
233
- setSelectedNodeId(searchMatches[prevIdx].nodeId);
234
- }, [searchMatches, searchMatchIndex]);
554
+ focusSelectAndDrillDown(searchMatches[prevIdx].nodeId);
555
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
235
556
  (0, import_react2.useEffect)(() => {
236
557
  if (!searchQuery.trim()) return;
237
- const matches = (0, import_core.searchNodes)(tree, searchQuery);
558
+ const matches = (0, import_core4.searchNodes)(tree, searchQuery);
238
559
  setSearchMatches(matches);
239
560
  setSearchMatchIndex(
240
561
  (prev) => Math.min(prev, Math.max(matches.length - 1, 0))
@@ -244,7 +565,10 @@ function VisualJson({
244
565
  const state = (0, import_react2.useMemo)(
245
566
  () => ({
246
567
  tree,
247
- selectedNodeId,
568
+ focusedNodeId,
569
+ selectedNodeIds,
570
+ anchorNodeId,
571
+ drillDownNodeId,
248
572
  expandedNodeIds,
249
573
  schema: schema ?? null,
250
574
  searchQuery,
@@ -254,7 +578,10 @@ function VisualJson({
254
578
  }),
255
579
  [
256
580
  tree,
257
- selectedNodeId,
581
+ focusedNodeId,
582
+ selectedNodeIds,
583
+ anchorNodeId,
584
+ drillDownNodeId,
258
585
  expandedNodeIds,
259
586
  schema,
260
587
  searchQuery,
@@ -267,6 +594,12 @@ function VisualJson({
267
594
  () => ({
268
595
  setTree,
269
596
  selectNode,
597
+ selectAndDrillDown,
598
+ toggleNodeSelection,
599
+ selectNodeRange,
600
+ setSelection,
601
+ setVisibleNodesOverride,
602
+ drillDown,
270
603
  toggleExpand,
271
604
  expandNode,
272
605
  collapseNode,
@@ -283,6 +616,12 @@ function VisualJson({
283
616
  [
284
617
  setTree,
285
618
  selectNode,
619
+ selectAndDrillDown,
620
+ toggleNodeSelection,
621
+ selectNodeRange,
622
+ setSelection,
623
+ setVisibleNodesOverride,
624
+ drillDown,
286
625
  toggleExpand,
287
626
  expandNode,
288
627
  collapseNode,
@@ -303,7 +642,7 @@ function VisualJson({
303
642
 
304
643
  // src/tree-view.tsx
305
644
  var import_react5 = require("react");
306
- var import_core3 = require("@visual-json/core");
645
+ var import_core6 = require("@visual-json/core");
307
646
 
308
647
  // src/context-menu.tsx
309
648
  var import_react3 = require("react");
@@ -402,111 +741,81 @@ function ContextMenu({ x, y, items, onClose }) {
402
741
  );
403
742
  }
404
743
 
405
- // src/display-key.ts
406
- var LABEL_FIELDS = ["name", "type", "title", "id", "label", "key"];
407
- function getDisplayKey(node, state) {
408
- if (node.parentId === null) return "/";
409
- const parent = state.nodesById.get(node.parentId);
410
- if (parent?.type !== "array" || node.type !== "object") return node.key;
411
- for (const field of LABEL_FIELDS) {
412
- const child = node.children.find((c) => c.key === field);
413
- if (child?.value != null && child.value !== "") {
414
- return String(child.value);
415
- }
416
- }
417
- return node.key;
418
- }
419
-
420
- // src/get-visible-nodes.ts
421
- function getVisibleNodes(root, isExpanded) {
422
- const result = [];
423
- function walk(node) {
424
- result.push(node);
425
- if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
426
- for (const child of node.children) {
427
- walk(child);
428
- }
429
- }
430
- }
431
- walk(root);
432
- return result;
433
- }
434
-
435
744
  // src/use-drag-drop.ts
436
745
  var import_react4 = require("react");
437
- var import_core2 = require("@visual-json/core");
746
+ var import_core5 = require("@visual-json/core");
747
+ var EMPTY_SET2 = Object.freeze(/* @__PURE__ */ new Set());
438
748
  var INITIAL_DRAG_STATE = {
439
- draggedNodeId: null,
749
+ draggedNodeIds: EMPTY_SET2,
440
750
  dropTargetNodeId: null,
441
751
  dropPosition: null
442
752
  };
443
- function useDragDrop() {
753
+ function setMultiDragImage2(e, count) {
754
+ setMultiDragImage(e.dataTransfer, count);
755
+ }
756
+ function useDragDrop(visibleNodes, selectedNodeIds) {
444
757
  const { state, actions } = useStudio();
445
758
  const [dragState, setDragState] = (0, import_react4.useState)(INITIAL_DRAG_STATE);
446
759
  const dragStateRef = (0, import_react4.useRef)(dragState);
447
760
  dragStateRef.current = dragState;
448
- const handleDragStart = (0, import_react4.useCallback)((nodeId) => {
449
- setDragState({
450
- draggedNodeId: nodeId,
451
- dropTargetNodeId: null,
452
- dropPosition: null
453
- });
454
- }, []);
455
- const handleDragOver = (0, import_react4.useCallback)(
761
+ const visibleNodeIndexMap = (0, import_react4.useMemo)(() => {
762
+ const map = /* @__PURE__ */ new Map();
763
+ visibleNodes.forEach((n, i) => map.set(n.id, i));
764
+ return map;
765
+ }, [visibleNodes]);
766
+ const handleDragStart = (0, import_react4.useCallback)(
767
+ (nodeId) => {
768
+ let ids;
769
+ if (selectedNodeIds.size > 0 && selectedNodeIds.has(nodeId)) {
770
+ ids = selectedNodeIds;
771
+ } else {
772
+ ids = /* @__PURE__ */ new Set([nodeId]);
773
+ }
774
+ setDragState({
775
+ draggedNodeIds: ids,
776
+ dropTargetNodeId: null,
777
+ dropPosition: null
778
+ });
779
+ },
780
+ [selectedNodeIds]
781
+ );
782
+ const rawDragOver = (0, import_react4.useCallback)(
456
783
  (nodeId, position) => {
784
+ const draggedIds = dragStateRef.current.draggedNodeIds;
785
+ for (const draggedId of draggedIds) {
786
+ if (nodeId === draggedId || (0, import_core5.isDescendant)(state.tree, nodeId, draggedId)) {
787
+ return;
788
+ }
789
+ }
457
790
  setDragState((prev) => ({
458
791
  ...prev,
459
792
  dropTargetNodeId: nodeId,
460
793
  dropPosition: position
461
794
  }));
462
795
  },
463
- []
796
+ [state.tree]
797
+ );
798
+ const handleDragOver = (0, import_react4.useCallback)(
799
+ (nodeId, position) => {
800
+ if (position === "before") {
801
+ const idx = visibleNodeIndexMap.get(nodeId);
802
+ if (idx !== void 0 && idx > 0) {
803
+ rawDragOver(visibleNodes[idx - 1].id, "after");
804
+ return;
805
+ }
806
+ }
807
+ rawDragOver(nodeId, position);
808
+ },
809
+ [visibleNodes, visibleNodeIndexMap, rawDragOver]
464
810
  );
465
811
  const handleDragEnd = (0, import_react4.useCallback)(() => {
466
812
  setDragState(INITIAL_DRAG_STATE);
467
813
  }, []);
468
814
  const handleDrop = (0, import_react4.useCallback)(() => {
469
- const { draggedNodeId, dropTargetNodeId, dropPosition } = dragStateRef.current;
470
- if (!draggedNodeId || !dropTargetNodeId || !dropPosition) return;
471
- const draggedNode = state.tree.nodesById.get(draggedNodeId);
472
- const targetNode = state.tree.nodesById.get(dropTargetNodeId);
473
- if (!draggedNode || !targetNode) return;
474
- if (draggedNode.parentId && draggedNode.parentId === targetNode.parentId) {
475
- const parent = state.tree.nodesById.get(draggedNode.parentId);
476
- if (parent) {
477
- const fromIndex = parent.children.findIndex(
478
- (c) => c.id === draggedNodeId
479
- );
480
- let toIndex = parent.children.findIndex(
481
- (c) => c.id === dropTargetNodeId
482
- );
483
- if (dropPosition === "after") toIndex++;
484
- if (fromIndex < toIndex) toIndex--;
485
- if (fromIndex !== toIndex && fromIndex >= 0 && toIndex >= 0) {
486
- const newTree = (0, import_core2.reorderChildren)(
487
- state.tree,
488
- parent.id,
489
- fromIndex,
490
- toIndex
491
- );
492
- actions.setTree(newTree);
493
- }
494
- }
495
- } else if (targetNode.parentId) {
496
- const newParent = state.tree.nodesById.get(targetNode.parentId);
497
- if (newParent) {
498
- let toIndex = newParent.children.findIndex(
499
- (c) => c.id === dropTargetNodeId
500
- );
501
- if (dropPosition === "after") toIndex++;
502
- const newTree = (0, import_core2.moveNode)(
503
- state.tree,
504
- draggedNodeId,
505
- newParent.id,
506
- toIndex
507
- );
508
- actions.setTree(newTree);
509
- }
815
+ const currentDragState = dragStateRef.current;
816
+ const newTree = computeDrop(state.tree, currentDragState);
817
+ if (newTree) {
818
+ actions.setTree(newTree);
510
819
  }
511
820
  setDragState(INITIAL_DRAG_STATE);
512
821
  }, [state.tree, actions]);
@@ -528,6 +837,7 @@ function TreeNodeRow({
528
837
  showValues,
529
838
  showCounts,
530
839
  isFocused,
840
+ onSelectRange,
531
841
  onDragStart,
532
842
  onDragOver,
533
843
  onDragEnd,
@@ -535,19 +845,15 @@ function TreeNodeRow({
535
845
  onContextMenu
536
846
  }) {
537
847
  const { state, actions } = useStudio();
538
- const isSelected = state.selectedNodeId === node.id;
848
+ const isSelected = state.selectedNodeIds.has(node.id);
539
849
  const isExpanded = state.expandedNodeIds.has(node.id);
540
850
  const isContainer = node.type === "object" || node.type === "array";
541
851
  const [hovered, setHovered] = (0, import_react5.useState)(false);
542
852
  const isRoot = node.parentId === null;
543
853
  const isSearchMatch = state.searchMatchNodeIds.has(node.id);
544
854
  const isActiveMatch = state.searchMatches.length > 0 && state.searchMatches[state.searchMatchIndex]?.nodeId === node.id;
545
- const schema = state.schema;
546
- const nodeSchema = schema ? (0, import_core3.getPropertySchema)(schema, node.path) : void 0;
547
- const validation = nodeSchema ? (0, import_core3.validateNode)(node, nodeSchema) : null;
548
- const hasError = validation ? !validation.valid : false;
549
855
  const isDragTarget = dragState.dropTargetNodeId === node.id;
550
- const isDraggedNode = dragState.draggedNodeId === node.id;
856
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
551
857
  function displayValue() {
552
858
  if (isContainer) {
553
859
  return node.type === "array" ? `[${node.children.length}]` : `{${node.children.length}}`;
@@ -563,12 +869,12 @@ function TreeNodeRow({
563
869
  const position = e.clientY < midY ? "before" : "after";
564
870
  onDragOver(node.id, position);
565
871
  }
566
- let borderTop = "none";
567
- let borderBottom = "none";
872
+ let borderTopColor = "transparent";
873
+ let borderBottomColor = "transparent";
568
874
  if (isDragTarget && dragState.dropPosition === "before") {
569
- borderTop = "2px solid var(--vj-accent, #007acc)";
875
+ borderTopColor = "var(--vj-accent, #007acc)";
570
876
  } else if (isDragTarget && dragState.dropPosition === "after") {
571
- borderBottom = "2px solid var(--vj-accent, #007acc)";
877
+ borderBottomColor = "var(--vj-accent, #007acc)";
572
878
  }
573
879
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
574
880
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -577,13 +883,24 @@ function TreeNodeRow({
577
883
  role: "treeitem",
578
884
  "aria-selected": isSelected,
579
885
  "aria-expanded": isContainer ? isExpanded : void 0,
580
- onClick: () => actions.selectNode(node.id),
886
+ onClick: (e) => {
887
+ if (e.shiftKey) {
888
+ onSelectRange(node.id);
889
+ } else if (e.metaKey || e.ctrlKey) {
890
+ actions.toggleNodeSelection(node.id);
891
+ } else {
892
+ actions.selectAndDrillDown(node.id);
893
+ }
894
+ },
581
895
  onMouseEnter: () => setHovered(true),
582
896
  onMouseLeave: () => setHovered(false),
583
897
  onContextMenu: (e) => onContextMenu(e, node),
584
898
  draggable: !isRoot,
585
899
  onDragStart: (e) => {
586
900
  e.dataTransfer.effectAllowed = "move";
901
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
902
+ setMultiDragImage2(e, state.selectedNodeIds.size);
903
+ }
587
904
  onDragStart(node.id);
588
905
  },
589
906
  onDragOver: handleDragOverEvent,
@@ -597,15 +914,16 @@ function TreeNodeRow({
597
914
  display: "flex",
598
915
  alignItems: "center",
599
916
  gap: 6,
600
- padding: "3px 8px",
917
+ padding: "1px 8px",
601
918
  paddingLeft: 8 + depth * 16,
602
919
  cursor: "pointer",
603
920
  backgroundColor: isSelected ? isFocused ? "var(--vj-bg-selected, #2a5a1e)" : "var(--vj-bg-selected-muted, var(--vj-bg-hover, #2a2d2e))" : isActiveMatch ? "var(--vj-bg-match-active, #51502b)" : isSearchMatch ? "var(--vj-bg-match, #3a3520)" : hovered ? "var(--vj-bg-hover, #2a2d2e)" : "transparent",
604
921
  minHeight: 28,
605
922
  userSelect: "none",
606
923
  opacity: isDraggedNode ? 0.4 : 1,
607
- borderTop,
608
- borderBottom,
924
+ borderTop: `2px solid ${borderTopColor}`,
925
+ borderBottom: `2px solid ${borderBottomColor}`,
926
+ boxSizing: "border-box",
609
927
  color: isSelected && isFocused ? "var(--vj-text-selected, var(--vj-text, #cccccc))" : "var(--vj-text, #cccccc)"
610
928
  },
611
929
  children: [
@@ -688,6 +1006,7 @@ function TreeNodeRow({
688
1006
  showValues,
689
1007
  showCounts,
690
1008
  isFocused,
1009
+ onSelectRange,
691
1010
  onDragStart,
692
1011
  onDragOver,
693
1012
  onDragEnd,
@@ -705,25 +1024,34 @@ function TreeView({
705
1024
  }) {
706
1025
  const { state, actions } = useStudio();
707
1026
  const containerRef = (0, import_react5.useRef)(null);
1027
+ const visibleNodes = (0, import_react5.useMemo)(
1028
+ () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
1029
+ [state.tree.root, state.expandedNodeIds]
1030
+ );
708
1031
  const {
709
1032
  dragState,
710
1033
  handleDragStart,
711
1034
  handleDragOver,
712
1035
  handleDragEnd,
713
1036
  handleDrop
714
- } = useDragDrop();
715
- const [contextMenu, setContextMenu] = (0, import_react5.useState)(null);
716
- const visibleNodes = (0, import_react5.useMemo)(
717
- () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
718
- [state.tree.root, state.expandedNodeIds]
1037
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
1038
+ const handleSelectRange = (0, import_react5.useCallback)(
1039
+ (nodeId) => {
1040
+ actions.setVisibleNodesOverride(visibleNodes);
1041
+ actions.selectNodeRange(nodeId);
1042
+ },
1043
+ [visibleNodes, actions]
719
1044
  );
1045
+ const [contextMenu, setContextMenu] = (0, import_react5.useState)(null);
720
1046
  const handleContextMenu = (0, import_react5.useCallback)(
721
1047
  (e, node) => {
722
1048
  e.preventDefault();
723
- actions.selectNode(node.id);
1049
+ if (!state.selectedNodeIds.has(node.id)) {
1050
+ actions.selectAndDrillDown(node.id);
1051
+ }
724
1052
  setContextMenu({ x: e.clientX, y: e.clientY, node });
725
1053
  },
726
- [actions]
1054
+ [actions, state.selectedNodeIds]
727
1055
  );
728
1056
  const buildContextMenuItems = (0, import_react5.useCallback)(
729
1057
  (node) => {
@@ -767,7 +1095,7 @@ function TreeView({
767
1095
  items.push({
768
1096
  label: "Copy value as JSON",
769
1097
  action: () => {
770
- const val = (0, import_core3.toJson)(node);
1098
+ const val = (0, import_core6.toJson)(node);
771
1099
  const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
772
1100
  navigator.clipboard.writeText(text).catch(() => {
773
1101
  });
@@ -778,7 +1106,7 @@ function TreeView({
778
1106
  items.push({
779
1107
  label: "Duplicate",
780
1108
  action: () => {
781
- const newTree = (0, import_core3.duplicateNode)(state.tree, node.id);
1109
+ const newTree = (0, import_core6.duplicateNode)(state.tree, node.id);
782
1110
  actions.setTree(newTree);
783
1111
  }
784
1112
  });
@@ -792,7 +1120,7 @@ function TreeView({
792
1120
  ].filter((t) => t !== node.type).map((t) => ({
793
1121
  label: `Change to ${t}`,
794
1122
  action: () => {
795
- const newTree = (0, import_core3.changeType)(state.tree, node.id, t);
1123
+ const newTree = (0, import_core6.changeType)(state.tree, node.id, t);
796
1124
  actions.setTree(newTree);
797
1125
  }
798
1126
  }));
@@ -802,7 +1130,7 @@ function TreeView({
802
1130
  items.push({
803
1131
  label: "Delete",
804
1132
  action: () => {
805
- const newTree = (0, import_core3.removeNode)(state.tree, node.id);
1133
+ const newTree = (0, import_core6.removeNode)(state.tree, node.id);
806
1134
  actions.setTree(newTree);
807
1135
  }
808
1136
  });
@@ -814,19 +1142,31 @@ function TreeView({
814
1142
  const handleKeyDown = (0, import_react5.useCallback)(
815
1143
  (e) => {
816
1144
  const currentIndex = visibleNodes.findIndex(
817
- (n) => n.id === state.selectedNodeId
1145
+ (n) => n.id === state.focusedNodeId
818
1146
  );
819
1147
  switch (e.key) {
820
1148
  case "ArrowDown": {
821
1149
  e.preventDefault();
822
1150
  const next = visibleNodes[currentIndex + 1];
823
- if (next) actions.selectNode(next.id);
1151
+ if (next) {
1152
+ if (e.shiftKey) {
1153
+ handleSelectRange(next.id);
1154
+ } else {
1155
+ actions.selectNode(next.id);
1156
+ }
1157
+ }
824
1158
  break;
825
1159
  }
826
1160
  case "ArrowUp": {
827
1161
  e.preventDefault();
828
1162
  const prev = visibleNodes[currentIndex - 1];
829
- if (prev) actions.selectNode(prev.id);
1163
+ if (prev) {
1164
+ if (e.shiftKey) {
1165
+ handleSelectRange(prev.id);
1166
+ } else {
1167
+ actions.selectNode(prev.id);
1168
+ }
1169
+ }
830
1170
  break;
831
1171
  }
832
1172
  case "ArrowRight": {
@@ -853,17 +1193,47 @@ function TreeView({
853
1193
  }
854
1194
  break;
855
1195
  }
1196
+ case "a": {
1197
+ if (e.metaKey || e.ctrlKey) {
1198
+ e.preventDefault();
1199
+ const ids = computeSelectAllIds(
1200
+ state.tree,
1201
+ state.focusedNodeId,
1202
+ state.selectedNodeIds
1203
+ );
1204
+ if (ids) {
1205
+ actions.setSelection(
1206
+ state.focusedNodeId,
1207
+ ids,
1208
+ state.focusedNodeId
1209
+ );
1210
+ }
1211
+ }
1212
+ break;
1213
+ }
1214
+ case "Escape": {
1215
+ e.preventDefault();
1216
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
1217
+ actions.selectNode(state.focusedNodeId);
1218
+ } else {
1219
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
1220
+ }
1221
+ break;
1222
+ }
856
1223
  case "Delete":
857
1224
  case "Backspace": {
858
1225
  e.preventDefault();
859
- const toDelete = currentIndex >= 0 ? visibleNodes[currentIndex] : null;
860
- if (toDelete && toDelete.parentId) {
861
- const nextSelect = visibleNodes[currentIndex + 1] ?? visibleNodes[currentIndex - 1];
862
- const newTree = (0, import_core3.removeNode)(state.tree, toDelete.id);
863
- actions.setTree(newTree);
864
- if (nextSelect && nextSelect.id !== toDelete.id) {
865
- actions.selectNode(nextSelect.id);
866
- }
1226
+ const { newTree, nextFocusId } = deleteSelectedNodes(
1227
+ state.tree,
1228
+ state.selectedNodeIds,
1229
+ visibleNodes
1230
+ );
1231
+ if (newTree === state.tree) break;
1232
+ actions.setTree(newTree);
1233
+ if (nextFocusId) {
1234
+ actions.selectNode(nextFocusId);
1235
+ } else {
1236
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
867
1237
  }
868
1238
  break;
869
1239
  }
@@ -871,23 +1241,25 @@ function TreeView({
871
1241
  },
872
1242
  [
873
1243
  visibleNodes,
874
- state.selectedNodeId,
1244
+ state.focusedNodeId,
1245
+ state.selectedNodeIds,
875
1246
  state.expandedNodeIds,
876
1247
  state.tree,
877
- actions
1248
+ actions,
1249
+ handleSelectRange
878
1250
  ]
879
1251
  );
880
1252
  const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
881
1253
  (0, import_react5.useEffect)(() => {
882
- if (state.selectedNodeId && containerRef.current) {
1254
+ if (state.focusedNodeId && containerRef.current) {
883
1255
  const el = containerRef.current.querySelector(
884
- `[data-node-id="${state.selectedNodeId}"]`
1256
+ `[data-node-id="${state.focusedNodeId}"]`
885
1257
  );
886
1258
  if (el) {
887
1259
  el.scrollIntoView({ block: "nearest" });
888
1260
  }
889
1261
  }
890
- }, [state.selectedNodeId]);
1262
+ }, [state.focusedNodeId]);
891
1263
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
892
1264
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
893
1265
  "div",
@@ -917,6 +1289,7 @@ function TreeView({
917
1289
  showValues,
918
1290
  showCounts,
919
1291
  isFocused,
1292
+ onSelectRange: handleSelectRange,
920
1293
  onDragStart: handleDragStart,
921
1294
  onDragOver: handleDragOver,
922
1295
  onDragEnd: handleDragEnd,
@@ -940,7 +1313,7 @@ function TreeView({
940
1313
 
941
1314
  // src/form-view.tsx
942
1315
  var import_react8 = require("react");
943
- var import_core4 = require("@visual-json/core");
1316
+ var import_core7 = require("@visual-json/core");
944
1317
 
945
1318
  // src/breadcrumbs.tsx
946
1319
  var import_react6 = require("react");
@@ -949,8 +1322,8 @@ var MAX_SUGGESTIONS = 20;
949
1322
  var DROPDOWN_MAX_HEIGHT = 200;
950
1323
  function Breadcrumbs({ className }) {
951
1324
  const { state, actions } = useStudio();
952
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
953
- const currentPath = selectedNode?.path ?? "/";
1325
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
1326
+ const currentPath = drillDownNode?.path ?? "/";
954
1327
  const [inputValue, setInputValue] = (0, import_react6.useState)(currentPath);
955
1328
  const [open, setOpen] = (0, import_react6.useState)(false);
956
1329
  const [highlightIndex, setHighlightIndex] = (0, import_react6.useState)(0);
@@ -980,7 +1353,7 @@ function Breadcrumbs({ className }) {
980
1353
  (path) => {
981
1354
  for (const [id, node] of state.tree.nodesById) {
982
1355
  if (node.path === path) {
983
- actions.selectNode(id);
1356
+ actions.selectAndDrillDown(id);
984
1357
  break;
985
1358
  }
986
1359
  }
@@ -1068,7 +1441,7 @@ function Breadcrumbs({ className }) {
1068
1441
  style: {
1069
1442
  width: "100%",
1070
1443
  boxSizing: "border-box",
1071
- padding: "2px 0",
1444
+ padding: "3px 0",
1072
1445
  fontSize: "var(--vj-input-font-size, 13px)",
1073
1446
  fontFamily: "var(--vj-font, monospace)",
1074
1447
  color: "var(--vj-text-muted, #999999)",
@@ -1295,9 +1668,9 @@ function EnumInput({
1295
1668
  var import_jsx_runtime6 = require("react/jsx-runtime");
1296
1669
  function getResolvedSchema(schema, rootSchema, path) {
1297
1670
  if (!schema) return void 0;
1298
- const raw = (0, import_core4.getPropertySchema)(schema, path, rootSchema);
1671
+ const raw = (0, import_core7.getPropertySchema)(schema, path, rootSchema);
1299
1672
  if (!raw) return void 0;
1300
- return (0, import_core4.resolveRef)(raw, rootSchema ?? schema);
1673
+ return (0, import_core7.resolveRef)(raw, rootSchema ?? schema);
1301
1674
  }
1302
1675
  function getValueColor(node) {
1303
1676
  if (node.type === "boolean" || node.type === "null")
@@ -1318,7 +1691,6 @@ function FormField({
1318
1691
  depth,
1319
1692
  showDescriptions,
1320
1693
  showCounts,
1321
- formSelectedNodeId,
1322
1694
  editingNodeId,
1323
1695
  collapsedIds,
1324
1696
  maxKeyLength,
@@ -1336,26 +1708,26 @@ function FormField({
1336
1708
  const { state, actions } = useStudio();
1337
1709
  const isContainer = node.type === "object" || node.type === "array";
1338
1710
  const collapsed = collapsedIds.has(node.id);
1339
- const isSelected = formSelectedNodeId === node.id;
1711
+ const isSelected = state.selectedNodeIds.has(node.id);
1340
1712
  const isEditing = editingNodeId === node.id;
1341
1713
  const propSchema = getResolvedSchema(schema, rootSchema, node.path);
1342
1714
  const isRequired = checkRequired(node, schema, rootSchema);
1343
1715
  const [hovered, setHovered] = (0, import_react8.useState)(false);
1344
1716
  const isRoot = node.parentId === null;
1345
1717
  const isDragTarget = dragState.dropTargetNodeId === node.id;
1346
- const isDraggedNode = dragState.draggedNodeId === node.id;
1718
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
1347
1719
  function handleDragOverEvent(e) {
1348
1720
  e.preventDefault();
1349
1721
  const rect = e.currentTarget.getBoundingClientRect();
1350
1722
  const midY = rect.top + rect.height / 2;
1351
1723
  onDragOver(node.id, e.clientY < midY ? "before" : "after");
1352
1724
  }
1353
- let borderTop = "none";
1354
- let borderBottom = "none";
1725
+ let borderTopColor = "transparent";
1726
+ let borderBottomColor = "transparent";
1355
1727
  if (isDragTarget && dragState.dropPosition === "before") {
1356
- borderTop = "2px solid var(--vj-accent, #007acc)";
1728
+ borderTopColor = "var(--vj-accent, #007acc)";
1357
1729
  } else if (isDragTarget && dragState.dropPosition === "after") {
1358
- borderBottom = "2px solid var(--vj-accent, #007acc)";
1730
+ borderBottomColor = "var(--vj-accent, #007acc)";
1359
1731
  }
1360
1732
  const valueRef = (0, import_react8.useRef)(null);
1361
1733
  const keyRef = (0, import_react8.useRef)(null);
@@ -1385,29 +1757,28 @@ function FormField({
1385
1757
  } else {
1386
1758
  parsed = newValue;
1387
1759
  }
1388
- const newTree = (0, import_core4.setValue)(state.tree, node.id, parsed);
1760
+ const newTree = (0, import_core7.setValue)(state.tree, node.id, parsed);
1389
1761
  actions.setTree(newTree);
1390
1762
  },
1391
1763
  [state.tree, node.id, node.type, actions, propSchema]
1392
1764
  );
1393
1765
  const handleKeyChange = (0, import_react8.useCallback)(
1394
1766
  (newKey) => {
1395
- const newTree = (0, import_core4.setKey)(state.tree, node.id, newKey);
1767
+ const newTree = (0, import_core7.setKey)(state.tree, node.id, newKey);
1396
1768
  actions.setTree(newTree);
1397
1769
  },
1398
1770
  [state.tree, node.id, actions]
1399
1771
  );
1400
1772
  const handleRemove = (0, import_react8.useCallback)(() => {
1401
- const newTree = (0, import_core4.removeNode)(state.tree, node.id);
1773
+ const newTree = (0, import_core7.removeNode)(state.tree, node.id);
1402
1774
  actions.setTree(newTree);
1403
1775
  }, [state.tree, node.id, actions]);
1404
1776
  const handleAddChild = (0, import_react8.useCallback)(() => {
1405
1777
  const key = node.type === "array" ? String(node.children.length) : `newKey${node.children.length}`;
1406
- const newTree = (0, import_core4.addProperty)(state.tree, node.id, key, "");
1778
+ const newTree = (0, import_core7.addProperty)(state.tree, node.id, key, "");
1407
1779
  actions.setTree(newTree);
1408
1780
  }, [state.tree, node.id, node.type, node.children.length, actions]);
1409
1781
  const description = propSchema?.description;
1410
- const defaultVal = propSchema?.default;
1411
1782
  const isDeprecated = propSchema?.deprecated;
1412
1783
  const fieldTitle = propSchema?.title;
1413
1784
  const parentIsObject = node.parentId && state.tree.nodesById.get(node.parentId)?.type === "object";
@@ -1422,6 +1793,9 @@ function FormField({
1422
1793
  draggable: !isRoot,
1423
1794
  onDragStart: (e) => {
1424
1795
  e.dataTransfer.effectAllowed = "move";
1796
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1797
+ setMultiDragImage2(e, state.selectedNodeIds.size);
1798
+ }
1425
1799
  onDragStart(node.id);
1426
1800
  },
1427
1801
  onDragOver: handleDragOverEvent,
@@ -1434,20 +1808,21 @@ function FormField({
1434
1808
  display: "flex",
1435
1809
  alignItems: "center",
1436
1810
  gap: 6,
1437
- padding: "3px 8px",
1811
+ padding: "1px 8px",
1438
1812
  paddingLeft: 8 + depth * 16,
1439
1813
  cursor: "pointer",
1440
1814
  backgroundColor: rowBg,
1441
1815
  color: rowColor,
1442
1816
  height: 28,
1817
+ boxSizing: "border-box",
1443
1818
  userSelect: "none",
1444
1819
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1445
- borderTop,
1446
- borderBottom
1820
+ borderTop: `2px solid ${borderTopColor}`,
1821
+ borderBottom: `2px solid ${borderBottomColor}`
1447
1822
  },
1448
1823
  onClick: (e) => {
1449
1824
  e.stopPropagation();
1450
- onSelect(node.id);
1825
+ onSelect(node.id, e);
1451
1826
  },
1452
1827
  onDoubleClick: () => onToggleCollapse(node.id),
1453
1828
  onMouseEnter: () => setHovered(true),
@@ -1503,7 +1878,7 @@ function FormField({
1503
1878
  {
1504
1879
  style: {
1505
1880
  color: !isRoot && !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1506
- fontSize: 13,
1881
+ fontSize: "var(--vj-input-font-size, 13px)",
1507
1882
  fontFamily: "var(--vj-font, monospace)",
1508
1883
  fontWeight: 500,
1509
1884
  flexShrink: 0,
@@ -1604,7 +1979,6 @@ function FormField({
1604
1979
  depth: depth + 1,
1605
1980
  showDescriptions,
1606
1981
  showCounts,
1607
- formSelectedNodeId,
1608
1982
  editingNodeId,
1609
1983
  collapsedIds,
1610
1984
  maxKeyLength,
@@ -1632,6 +2006,9 @@ function FormField({
1632
2006
  draggable: !isRoot,
1633
2007
  onDragStart: (e) => {
1634
2008
  e.dataTransfer.effectAllowed = "move";
2009
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
2010
+ setMultiDragImage2(e, state.selectedNodeIds.size);
2011
+ }
1635
2012
  onDragStart(node.id);
1636
2013
  },
1637
2014
  onDragOver: handleDragOverEvent,
@@ -1644,20 +2021,21 @@ function FormField({
1644
2021
  display: "flex",
1645
2022
  alignItems: "center",
1646
2023
  gap: 6,
1647
- padding: "3px 8px",
2024
+ padding: "1px 8px",
1648
2025
  paddingLeft: 8 + depth * 16,
1649
2026
  cursor: "pointer",
1650
2027
  backgroundColor: rowBg,
1651
2028
  color: rowColor,
1652
2029
  height: 28,
2030
+ boxSizing: "border-box",
1653
2031
  userSelect: "none",
1654
2032
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1655
- borderTop,
1656
- borderBottom
2033
+ borderTop: `2px solid ${borderTopColor}`,
2034
+ borderBottom: `2px solid ${borderBottomColor}`
1657
2035
  },
1658
2036
  onClick: (e) => {
1659
2037
  e.stopPropagation();
1660
- onSelect(node.id);
2038
+ onSelect(node.id, e);
1661
2039
  },
1662
2040
  onDoubleClick: () => onStartEditing(node.id),
1663
2041
  onMouseEnter: () => setHovered(true),
@@ -1694,7 +2072,7 @@ function FormField({
1694
2072
  {
1695
2073
  style: {
1696
2074
  color: !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1697
- fontSize: 13,
2075
+ fontSize: "var(--vj-input-font-size, 13px)",
1698
2076
  fontFamily: "var(--vj-font, monospace)",
1699
2077
  flexShrink: 0,
1700
2078
  display: "inline-block",
@@ -1738,7 +2116,7 @@ function FormField({
1738
2116
  {
1739
2117
  style: {
1740
2118
  color: valueColor,
1741
- fontSize: 13,
2119
+ fontSize: "var(--vj-input-font-size, 13px)",
1742
2120
  fontFamily: "var(--vj-font, monospace)",
1743
2121
  overflow: "hidden",
1744
2122
  textOverflow: "ellipsis",
@@ -1816,7 +2194,7 @@ function renderEditInput(node, propSchema, displayValue, handleValueChange, inpu
1816
2194
  style: {
1817
2195
  color: "var(--vj-boolean, #569cd6)",
1818
2196
  fontFamily: "var(--vj-font, monospace)",
1819
- fontSize: 13,
2197
+ fontSize: "var(--vj-input-font-size, 13px)",
1820
2198
  fontStyle: "italic",
1821
2199
  flex: 1
1822
2200
  },
@@ -1849,11 +2227,8 @@ function FormView({
1849
2227
  }) {
1850
2228
  const { state, actions } = useStudio();
1851
2229
  const rootSchema = state.schema ?? void 0;
1852
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
1853
- const displayNode = selectedNode ?? state.tree.root;
1854
- const [formSelectedNodeId, setFormSelectedNodeId] = (0, import_react8.useState)(
1855
- null
1856
- );
2230
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
2231
+ const displayNode = drillDownNode ?? state.tree.root;
1857
2232
  const [editingNodeId, setEditingNodeId] = (0, import_react8.useState)(null);
1858
2233
  const preEditTreeRef = (0, import_react8.useRef)(null);
1859
2234
  const [collapsedIds, setCollapsedIds] = (0, import_react8.useState)(
@@ -1861,15 +2236,7 @@ function FormView({
1861
2236
  );
1862
2237
  const containerRef = (0, import_react8.useRef)(null);
1863
2238
  const [isFocused, setIsFocused] = (0, import_react8.useState)(false);
1864
- const {
1865
- dragState,
1866
- handleDragStart,
1867
- handleDragOver,
1868
- handleDragEnd,
1869
- handleDrop
1870
- } = useDragDrop();
1871
2239
  (0, import_react8.useEffect)(() => {
1872
- setFormSelectedNodeId(null);
1873
2240
  setEditingNodeId(null);
1874
2241
  setCollapsedIds(/* @__PURE__ */ new Set());
1875
2242
  }, [displayNode.id]);
@@ -1877,6 +2244,17 @@ function FormView({
1877
2244
  () => getVisibleNodes(displayNode, (id) => !collapsedIds.has(id)),
1878
2245
  [displayNode, collapsedIds]
1879
2246
  );
2247
+ const {
2248
+ dragState,
2249
+ handleDragStart,
2250
+ handleDragOver,
2251
+ handleDragEnd,
2252
+ handleDrop
2253
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
2254
+ (0, import_react8.useEffect)(() => {
2255
+ actions.setVisibleNodesOverride(visibleNodes);
2256
+ return () => actions.setVisibleNodesOverride(null);
2257
+ }, [visibleNodes, actions]);
1880
2258
  const { maxKeyLength, maxDepth } = (0, import_react8.useMemo)(() => {
1881
2259
  let maxKey = 1;
1882
2260
  let maxD = 0;
@@ -1890,10 +2268,20 @@ function FormView({
1890
2268
  }
1891
2269
  return { maxKeyLength: maxKey, maxDepth: maxD };
1892
2270
  }, [visibleNodes, displayNode.path, state.tree]);
1893
- const handleSelect = (0, import_react8.useCallback)((nodeId) => {
1894
- setFormSelectedNodeId(nodeId);
1895
- setEditingNodeId(null);
1896
- }, []);
2271
+ const handleSelect = (0, import_react8.useCallback)(
2272
+ (nodeId, e) => {
2273
+ setEditingNodeId(null);
2274
+ if (e.shiftKey) {
2275
+ actions.setVisibleNodesOverride(visibleNodes);
2276
+ actions.selectNodeRange(nodeId);
2277
+ } else if (e.metaKey || e.ctrlKey) {
2278
+ actions.toggleNodeSelection(nodeId);
2279
+ } else {
2280
+ actions.selectNode(nodeId);
2281
+ }
2282
+ },
2283
+ [actions, visibleNodes]
2284
+ );
1897
2285
  const handleToggleCollapse = (0, import_react8.useCallback)((nodeId) => {
1898
2286
  setCollapsedIds((prev) => {
1899
2287
  const next = new Set(prev);
@@ -1941,15 +2329,23 @@ function FormView({
1941
2329
  }
1942
2330
  return;
1943
2331
  }
1944
- const currentIndex = visibleNodes.findIndex(
1945
- (n) => n.id === formSelectedNodeId
2332
+ let currentIndex = visibleNodes.findIndex(
2333
+ (n) => n.id === state.focusedNodeId
1946
2334
  );
2335
+ if (currentIndex === -1 && visibleNodes.length > 0) {
2336
+ currentIndex = 0;
2337
+ }
1947
2338
  switch (e.key) {
1948
2339
  case "ArrowDown": {
1949
2340
  e.preventDefault();
1950
2341
  const next = visibleNodes[currentIndex + 1];
1951
2342
  if (next) {
1952
- setFormSelectedNodeId(next.id);
2343
+ if (e.shiftKey) {
2344
+ actions.setVisibleNodesOverride(visibleNodes);
2345
+ actions.selectNodeRange(next.id);
2346
+ } else {
2347
+ actions.selectNode(next.id);
2348
+ }
1953
2349
  scrollToNode(next.id);
1954
2350
  }
1955
2351
  break;
@@ -1958,7 +2354,12 @@ function FormView({
1958
2354
  e.preventDefault();
1959
2355
  const prev = visibleNodes[currentIndex - 1];
1960
2356
  if (prev) {
1961
- setFormSelectedNodeId(prev.id);
2357
+ if (e.shiftKey) {
2358
+ actions.setVisibleNodesOverride(visibleNodes);
2359
+ actions.selectNodeRange(prev.id);
2360
+ } else {
2361
+ actions.selectNode(prev.id);
2362
+ }
1962
2363
  scrollToNode(prev.id);
1963
2364
  }
1964
2365
  break;
@@ -1974,7 +2375,7 @@ function FormView({
1974
2375
  return next;
1975
2376
  });
1976
2377
  } else if (node.children.length > 0) {
1977
- setFormSelectedNodeId(node.children[0].id);
2378
+ actions.selectNode(node.children[0].id);
1978
2379
  scrollToNode(node.children[0].id);
1979
2380
  }
1980
2381
  }
@@ -1996,7 +2397,7 @@ function FormView({
1996
2397
  (n) => n.id === current.parentId
1997
2398
  );
1998
2399
  if (parentInVisible) {
1999
- setFormSelectedNodeId(parentInVisible.id);
2400
+ actions.selectNode(parentInVisible.id);
2000
2401
  scrollToNode(parentInVisible.id);
2001
2402
  }
2002
2403
  }
@@ -2004,30 +2405,54 @@ function FormView({
2004
2405
  }
2005
2406
  case "Enter": {
2006
2407
  e.preventDefault();
2007
- if (formSelectedNodeId) {
2408
+ if (state.focusedNodeId) {
2008
2409
  preEditTreeRef.current = state.tree;
2009
- setEditingNodeId(formSelectedNodeId);
2410
+ actions.selectNode(state.focusedNodeId);
2411
+ setEditingNodeId(state.focusedNodeId);
2412
+ }
2413
+ break;
2414
+ }
2415
+ case "a": {
2416
+ if (e.metaKey || e.ctrlKey) {
2417
+ e.preventDefault();
2418
+ const ids = computeSelectAllIds(
2419
+ state.tree,
2420
+ state.focusedNodeId,
2421
+ state.selectedNodeIds
2422
+ );
2423
+ if (ids) {
2424
+ actions.setSelection(
2425
+ state.focusedNodeId,
2426
+ ids,
2427
+ state.focusedNodeId
2428
+ );
2429
+ }
2010
2430
  }
2011
2431
  break;
2012
2432
  }
2013
2433
  case "Escape": {
2014
2434
  e.preventDefault();
2015
- setEditingNodeId(null);
2435
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
2436
+ actions.selectNode(state.focusedNodeId);
2437
+ } else {
2438
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2439
+ }
2016
2440
  break;
2017
2441
  }
2018
2442
  case "Delete":
2019
2443
  case "Backspace": {
2020
2444
  e.preventDefault();
2021
- const toDelete = currentIndex >= 0 ? visibleNodes[currentIndex] : null;
2022
- if (toDelete && toDelete.parentId) {
2023
- const nextSelect = visibleNodes[currentIndex + 1] ?? visibleNodes[currentIndex - 1];
2024
- const newTree = (0, import_core4.removeNode)(state.tree, toDelete.id);
2025
- actions.setTree(newTree);
2026
- if (nextSelect && nextSelect.id !== toDelete.id) {
2027
- setFormSelectedNodeId(nextSelect.id);
2028
- } else {
2029
- setFormSelectedNodeId(null);
2030
- }
2445
+ const { newTree, nextFocusId } = deleteSelectedNodes(
2446
+ state.tree,
2447
+ state.selectedNodeIds,
2448
+ visibleNodes
2449
+ );
2450
+ if (newTree === state.tree) break;
2451
+ actions.setTree(newTree);
2452
+ if (nextFocusId) {
2453
+ actions.selectNode(nextFocusId);
2454
+ } else {
2455
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2031
2456
  }
2032
2457
  break;
2033
2458
  }
@@ -2035,7 +2460,8 @@ function FormView({
2035
2460
  },
2036
2461
  [
2037
2462
  visibleNodes,
2038
- formSelectedNodeId,
2463
+ state.focusedNodeId,
2464
+ state.selectedNodeIds,
2039
2465
  editingNodeId,
2040
2466
  collapsedIds,
2041
2467
  scrollToNode,
@@ -2060,9 +2486,11 @@ function FormView({
2060
2486
  "div",
2061
2487
  {
2062
2488
  style: {
2489
+ display: "flex",
2490
+ alignItems: "center",
2063
2491
  padding: "4px 8px",
2064
2492
  borderBottom: "1px solid var(--vj-border, #333333)",
2065
- backgroundColor: "var(--vj-bg-panel, #252526)",
2493
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2066
2494
  flexShrink: 0
2067
2495
  },
2068
2496
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Breadcrumbs, {})
@@ -2095,7 +2523,6 @@ function FormView({
2095
2523
  depth: 0,
2096
2524
  showDescriptions,
2097
2525
  showCounts,
2098
- formSelectedNodeId,
2099
2526
  editingNodeId,
2100
2527
  collapsedIds,
2101
2528
  maxKeyLength,
@@ -2165,7 +2592,7 @@ function SearchBar({ className }) {
2165
2592
  alignItems: "center",
2166
2593
  gap: 6,
2167
2594
  padding: "4px 8px",
2168
- backgroundColor: "var(--vj-bg-panel, #252526)",
2595
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2169
2596
  borderBottom: "1px solid var(--vj-border, #333333)"
2170
2597
  },
2171
2598
  children: [
@@ -2372,31 +2799,6 @@ function SearchBar({ className }) {
2372
2799
 
2373
2800
  // src/json-editor.tsx
2374
2801
  var import_jsx_runtime8 = require("react/jsx-runtime");
2375
- var DEFAULT_CSS_VARS = {
2376
- "--vj-bg": "#1e1e1e",
2377
- "--vj-bg-panel": "#252526",
2378
- "--vj-bg-hover": "#2a2d2e",
2379
- "--vj-bg-selected": "#2a5a1e",
2380
- "--vj-bg-selected-muted": "#2a2d2e",
2381
- "--vj-bg-match": "#3a3520",
2382
- "--vj-bg-match-active": "#51502b",
2383
- "--vj-border": "#333333",
2384
- "--vj-border-subtle": "#2a2a2a",
2385
- "--vj-text": "#cccccc",
2386
- "--vj-text-muted": "#888888",
2387
- "--vj-text-dim": "#666666",
2388
- "--vj-text-dimmer": "#555555",
2389
- "--vj-string": "#ce9178",
2390
- "--vj-number": "#b5cea8",
2391
- "--vj-boolean": "#569cd6",
2392
- "--vj-accent": "#007acc",
2393
- "--vj-accent-muted": "#094771",
2394
- "--vj-input-bg": "#3c3c3c",
2395
- "--vj-input-border": "#555555",
2396
- "--vj-error": "#f48771",
2397
- "--vj-font": "monospace",
2398
- "--vj-input-font-size": "13px"
2399
- };
2400
2802
  function JsonEditor({
2401
2803
  value,
2402
2804
  defaultValue,
@@ -2723,451 +3125,13 @@ function EditorLayout({
2723
3125
  ] });
2724
3126
  }
2725
3127
 
2726
- // src/property-editor.tsx
3128
+ // src/diff-view.tsx
2727
3129
  var import_react11 = require("react");
2728
- var import_core5 = require("@visual-json/core");
3130
+ var import_core8 = require("@visual-json/core");
2729
3131
  var import_jsx_runtime9 = require("react/jsx-runtime");
2730
- var ALL_TYPES = [
2731
- "string",
2732
- "number",
2733
- "boolean",
2734
- "null",
2735
- "object",
2736
- "array"
2737
- ];
2738
- function PropertyRow({ node, schemaProperty }) {
2739
- const { state, actions } = useStudio();
2740
- const isContainer = node.type === "object" || node.type === "array";
2741
- const [hoveredRow, setHoveredRow] = (0, import_react11.useState)(false);
2742
- const enumRef = (0, import_react11.useRef)(null);
2743
- function handleValueChange(newValue) {
2744
- let parsed;
2745
- if (newValue === "null") parsed = null;
2746
- else if (newValue === "true") parsed = true;
2747
- else if (newValue === "false") parsed = false;
2748
- else if ((node.type === "number" || schemaProperty?.type === "number" || schemaProperty?.type === "integer") && !isNaN(Number(newValue)) && newValue.trim() !== "")
2749
- parsed = Number(newValue);
2750
- else parsed = newValue;
2751
- const newTree = (0, import_core5.setValue)(state.tree, node.id, parsed);
2752
- actions.setTree(newTree);
2753
- }
2754
- function handleKeyChange(newKey) {
2755
- const newTree = (0, import_core5.setKey)(state.tree, node.id, newKey);
2756
- actions.setTree(newTree);
2757
- }
2758
- function handleRemove() {
2759
- const newTree = (0, import_core5.removeNode)(state.tree, node.id);
2760
- actions.setTree(newTree);
2761
- }
2762
- function handleAddChild() {
2763
- const key = node.type === "array" ? String(node.children.length) : `key${node.children.length}`;
2764
- const newTree = (0, import_core5.addProperty)(state.tree, node.id, key, "");
2765
- actions.setTree(newTree);
2766
- }
2767
- function displayValue() {
2768
- if (isContainer) {
2769
- return node.type === "array" ? `[${node.children.length} items]` : `{${node.children.length} keys}`;
2770
- }
2771
- if (node.value === null) return "";
2772
- if (node.value === void 0) return "";
2773
- if (typeof node.value === "string" && node.value === "") return "";
2774
- return String(node.value);
2775
- }
2776
- const hasEnumValues = schemaProperty?.enum && schemaProperty.enum.length > 0;
2777
- const description = schemaProperty?.description;
2778
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2779
- "div",
2780
- {
2781
- onMouseEnter: () => setHoveredRow(true),
2782
- onMouseLeave: () => setHoveredRow(false),
2783
- children: [
2784
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2785
- "div",
2786
- {
2787
- style: {
2788
- display: "flex",
2789
- alignItems: "center",
2790
- gap: 8,
2791
- padding: "4px 12px",
2792
- borderBottom: "1px solid var(--vj-border-subtle, #2a2a2a)",
2793
- minHeight: 32,
2794
- backgroundColor: hoveredRow ? "var(--vj-bg-hover, #2a2d2e)" : "transparent"
2795
- },
2796
- children: [
2797
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2798
- "input",
2799
- {
2800
- value: node.key,
2801
- onChange: (e) => handleKeyChange(e.target.value),
2802
- style: {
2803
- background: "none",
2804
- border: "1px solid transparent",
2805
- borderRadius: 3,
2806
- color: "var(--vj-text, #cccccc)",
2807
- fontFamily: "var(--vj-font, monospace)",
2808
- fontSize: "var(--vj-input-font-size, 13px)",
2809
- padding: "2px 6px",
2810
- width: 140,
2811
- flexShrink: 0
2812
- }
2813
- }
2814
- ),
2815
- !isContainer ? hasEnumValues ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2816
- EnumInput,
2817
- {
2818
- enumValues: schemaProperty.enum,
2819
- value: displayValue(),
2820
- onValueChange: handleValueChange,
2821
- inputRef: enumRef,
2822
- inputStyle: {
2823
- background: "none",
2824
- border: "1px solid transparent",
2825
- borderRadius: 3,
2826
- color: node.type === "string" ? "var(--vj-string, #ce9178)" : "var(--vj-number, #b5cea8)",
2827
- fontFamily: "var(--vj-font, monospace)",
2828
- fontSize: "var(--vj-input-font-size, 13px)",
2829
- padding: "2px 6px",
2830
- flex: 1,
2831
- outline: "none"
2832
- }
2833
- }
2834
- ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2835
- "input",
2836
- {
2837
- value: displayValue(),
2838
- onChange: (e) => handleValueChange(e.target.value),
2839
- placeholder: "<value>",
2840
- style: {
2841
- background: "none",
2842
- border: "1px solid transparent",
2843
- borderRadius: 3,
2844
- color: node.type === "string" ? "var(--vj-string, #ce9178)" : "var(--vj-number, #b5cea8)",
2845
- fontFamily: "var(--vj-font, monospace)",
2846
- fontSize: "var(--vj-input-font-size, 13px)",
2847
- padding: "2px 6px",
2848
- flex: 1,
2849
- textAlign: "right"
2850
- }
2851
- }
2852
- ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2853
- "span",
2854
- {
2855
- style: {
2856
- color: "var(--vj-text-dim, #666666)",
2857
- fontFamily: "var(--vj-font, monospace)",
2858
- fontSize: 13,
2859
- flex: 1,
2860
- textAlign: "right"
2861
- },
2862
- children: displayValue()
2863
- }
2864
- ),
2865
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2866
- "div",
2867
- {
2868
- style: {
2869
- display: "flex",
2870
- gap: 2,
2871
- opacity: hoveredRow ? 1 : 0,
2872
- transition: "opacity 0.1s",
2873
- flexShrink: 0
2874
- },
2875
- children: [
2876
- isContainer && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2877
- "button",
2878
- {
2879
- onClick: handleAddChild,
2880
- style: {
2881
- background: "none",
2882
- border: "none",
2883
- color: "var(--vj-text-muted, #888888)",
2884
- cursor: "pointer",
2885
- padding: "2px 4px",
2886
- fontSize: 15,
2887
- fontFamily: "var(--vj-font, monospace)",
2888
- borderRadius: 3,
2889
- lineHeight: 1
2890
- },
2891
- title: "Add child",
2892
- children: "+"
2893
- }
2894
- ),
2895
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2896
- "button",
2897
- {
2898
- onClick: handleRemove,
2899
- style: {
2900
- background: "none",
2901
- border: "none",
2902
- color: "var(--vj-text-muted, #888888)",
2903
- cursor: "pointer",
2904
- padding: "2px 4px",
2905
- fontSize: 15,
2906
- fontFamily: "var(--vj-font, monospace)",
2907
- borderRadius: 3,
2908
- lineHeight: 1
2909
- },
2910
- title: "Remove",
2911
- children: "\xD7"
2912
- }
2913
- )
2914
- ]
2915
- }
2916
- )
2917
- ]
2918
- }
2919
- ),
2920
- description && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2921
- "div",
2922
- {
2923
- style: {
2924
- padding: "2px 12px 4px 44px",
2925
- fontSize: 11,
2926
- color: "var(--vj-text-dim, #666666)",
2927
- fontFamily: "var(--vj-font, monospace)",
2928
- borderBottom: "1px solid var(--vj-border-subtle, #2a2a2a)"
2929
- },
2930
- children: description
2931
- }
2932
- )
2933
- ]
2934
- }
2935
- );
2936
- }
2937
- function PropertyEditor({ className }) {
2938
- const { state, actions } = useStudio();
2939
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
2940
- const handleChangeType = (0, import_react11.useCallback)(
2941
- (newType) => {
2942
- if (!selectedNode) return;
2943
- const newTree = (0, import_core5.changeType)(state.tree, selectedNode.id, newType);
2944
- actions.setTree(newTree);
2945
- },
2946
- [state.tree, selectedNode, actions]
2947
- );
2948
- const handleDuplicate = (0, import_react11.useCallback)(() => {
2949
- if (!selectedNode) return;
2950
- const newTree = (0, import_core5.duplicateNode)(state.tree, selectedNode.id);
2951
- actions.setTree(newTree);
2952
- }, [state.tree, selectedNode, actions]);
2953
- const handleCopyPath = (0, import_react11.useCallback)(() => {
2954
- if (!selectedNode) return;
2955
- navigator.clipboard.writeText(selectedNode.path).catch(() => {
2956
- });
2957
- }, [selectedNode]);
2958
- const handleCopyValue = (0, import_react11.useCallback)(() => {
2959
- if (!selectedNode) return;
2960
- const value = (0, import_core5.toJson)(selectedNode);
2961
- const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
2962
- navigator.clipboard.writeText(text).catch(() => {
2963
- });
2964
- }, [selectedNode]);
2965
- if (!selectedNode) {
2966
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2967
- "div",
2968
- {
2969
- className,
2970
- style: {
2971
- display: "flex",
2972
- alignItems: "center",
2973
- justifyContent: "center",
2974
- backgroundColor: "var(--vj-bg, #1e1e1e)",
2975
- color: "var(--vj-text-dimmer, #555555)",
2976
- fontFamily: "var(--vj-font, monospace)",
2977
- fontSize: 13,
2978
- height: "100%"
2979
- },
2980
- children: "Select a node to edit"
2981
- }
2982
- );
2983
- }
2984
- const isContainer = selectedNode.type === "object" || selectedNode.type === "array";
2985
- const schema = state.schema ?? void 0;
2986
- const schemaTitle = schema?.title;
2987
- function getChildSchema(childKey) {
2988
- if (!schema || !selectedNode) return void 0;
2989
- const childPath = selectedNode.path === "/" ? `/${childKey}` : `${selectedNode.path}/${childKey}`;
2990
- const raw = (0, import_core5.getPropertySchema)(schema, childPath);
2991
- return raw ? (0, import_core5.resolveRef)(raw, schema) : void 0;
2992
- }
2993
- function handleAdd() {
2994
- if (!selectedNode) return;
2995
- const key = selectedNode.type === "array" ? String(selectedNode.children.length) : `key${selectedNode.children.length}`;
2996
- const newTree = (0, import_core5.addProperty)(state.tree, selectedNode.id, key, "");
2997
- actions.setTree(newTree);
2998
- }
2999
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3000
- "div",
3001
- {
3002
- className,
3003
- style: {
3004
- backgroundColor: "var(--vj-bg, #1e1e1e)",
3005
- color: "var(--vj-text, #cccccc)",
3006
- overflow: "auto",
3007
- height: "100%",
3008
- display: "flex",
3009
- flexDirection: "column"
3010
- },
3011
- children: [
3012
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3013
- "div",
3014
- {
3015
- style: {
3016
- display: "flex",
3017
- alignItems: "center",
3018
- justifyContent: "space-between",
3019
- padding: "6px 12px",
3020
- borderBottom: "1px solid var(--vj-border, #333333)",
3021
- fontSize: 12,
3022
- color: "var(--vj-text-muted, #999999)",
3023
- fontFamily: "var(--vj-font, monospace)",
3024
- flexShrink: 0,
3025
- backgroundColor: "var(--vj-bg-panel, #252526)"
3026
- },
3027
- children: [
3028
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3029
- "div",
3030
- {
3031
- style: {
3032
- display: "flex",
3033
- flexDirection: "column",
3034
- gap: 2,
3035
- flex: 1,
3036
- minWidth: 0
3037
- },
3038
- children: [
3039
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Breadcrumbs, {}),
3040
- schemaTitle && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3041
- "span",
3042
- {
3043
- style: { fontSize: 10, color: "var(--vj-text-dim, #666666)" },
3044
- children: schemaTitle
3045
- }
3046
- )
3047
- ]
3048
- }
3049
- ),
3050
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3051
- "div",
3052
- {
3053
- style: {
3054
- display: "flex",
3055
- alignItems: "center",
3056
- gap: 4,
3057
- flexShrink: 0
3058
- },
3059
- children: [
3060
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3061
- "select",
3062
- {
3063
- value: selectedNode.type,
3064
- onChange: (e) => handleChangeType(e.target.value),
3065
- style: {
3066
- background: "var(--vj-input-bg, #3c3c3c)",
3067
- border: "1px solid var(--vj-input-border, #555555)",
3068
- borderRadius: 3,
3069
- color: "var(--vj-text, #cccccc)",
3070
- fontSize: "var(--vj-input-font-size, 13px)",
3071
- fontFamily: "var(--vj-font, monospace)",
3072
- padding: "1px 4px",
3073
- cursor: "pointer"
3074
- },
3075
- title: "Change type",
3076
- children: ALL_TYPES.map((t) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: t, children: t }, t))
3077
- }
3078
- ),
3079
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3080
- "button",
3081
- {
3082
- onClick: handleCopyPath,
3083
- style: actionButtonStyle,
3084
- title: "Copy path",
3085
- children: "path"
3086
- }
3087
- ),
3088
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3089
- "button",
3090
- {
3091
- onClick: handleCopyValue,
3092
- style: actionButtonStyle,
3093
- title: "Copy value",
3094
- children: "value"
3095
- }
3096
- ),
3097
- selectedNode.parentId && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3098
- "button",
3099
- {
3100
- onClick: handleDuplicate,
3101
- style: actionButtonStyle,
3102
- title: "Duplicate",
3103
- children: "dup"
3104
- }
3105
- ),
3106
- isContainer && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3107
- "button",
3108
- {
3109
- onClick: handleAdd,
3110
- style: {
3111
- ...actionButtonStyle,
3112
- border: "1px solid var(--vj-input-border, #555555)"
3113
- },
3114
- children: "+ Add"
3115
- }
3116
- )
3117
- ]
3118
- }
3119
- )
3120
- ]
3121
- }
3122
- ),
3123
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: isContainer ? selectedNode.children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3124
- PropertyRow,
3125
- {
3126
- node: child,
3127
- schemaProperty: getChildSchema(child.key)
3128
- },
3129
- child.id
3130
- )) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PropertyRow, { node: selectedNode }) })
3131
- ]
3132
- }
3133
- );
3134
- }
3135
- var actionButtonStyle = {
3136
- background: "none",
3137
- border: "none",
3138
- borderRadius: 3,
3139
- color: "var(--vj-text-muted, #888888)",
3140
- cursor: "pointer",
3141
- padding: "1px 6px",
3142
- fontSize: 11,
3143
- fontFamily: "var(--vj-font, monospace)"
3144
- };
3145
-
3146
- // src/diff-view.tsx
3147
- var import_react12 = require("react");
3148
- var import_core6 = require("@visual-json/core");
3149
- var import_jsx_runtime10 = require("react/jsx-runtime");
3150
- var DIFF_COLORS = {
3151
- added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
3152
- removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
3153
- changed: { bg: "#3a3a1e", marker: "~", label: "#dcdcaa" }
3154
- };
3155
- function formatValue(value) {
3156
- if (value === void 0) return "";
3157
- if (value === null) return "null";
3158
- if (typeof value === "string") return JSON.stringify(value);
3159
- if (typeof value === "object") {
3160
- const json = JSON.stringify(value, null, 2);
3161
- if (json.length > 80) {
3162
- return JSON.stringify(value).slice(0, 77) + "...";
3163
- }
3164
- return json;
3165
- }
3166
- return String(value);
3167
- }
3168
3132
  function DiffRow({ entry }) {
3169
3133
  const colors = DIFF_COLORS[entry.type];
3170
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
3134
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3171
3135
  "div",
3172
3136
  {
3173
3137
  style: {
@@ -3181,7 +3145,7 @@ function DiffRow({ entry }) {
3181
3145
  gap: 8
3182
3146
  },
3183
3147
  children: [
3184
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3148
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3185
3149
  "span",
3186
3150
  {
3187
3151
  style: {
@@ -3194,7 +3158,7 @@ function DiffRow({ entry }) {
3194
3158
  children: colors.marker
3195
3159
  }
3196
3160
  ),
3197
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3161
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3198
3162
  "span",
3199
3163
  {
3200
3164
  style: {
@@ -3205,14 +3169,14 @@ function DiffRow({ entry }) {
3205
3169
  children: entry.path
3206
3170
  }
3207
3171
  ),
3208
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { flex: 1, display: "flex", gap: 8, overflow: "hidden" }, children: [
3209
- entry.type === "changed" && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3210
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) }),
3211
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "var(--vj-text-dim, #666666)" }, children: "\u2192" }),
3212
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) })
3172
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { flex: 1, display: "flex", gap: 8, overflow: "hidden" }, children: [
3173
+ entry.type === "changed" && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
3174
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) }),
3175
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "var(--vj-text-dim, #666666)" }, children: "\u2192" }),
3176
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) })
3213
3177
  ] }),
3214
- entry.type === "added" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) }),
3215
- entry.type === "removed" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) })
3178
+ entry.type === "added" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) }),
3179
+ entry.type === "removed" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) })
3216
3180
  ] })
3217
3181
  ]
3218
3182
  }
@@ -3223,14 +3187,14 @@ function DiffView({
3223
3187
  currentJson,
3224
3188
  className
3225
3189
  }) {
3226
- const entries = (0, import_react12.useMemo)(
3227
- () => (0, import_core6.computeDiff)(originalJson, currentJson),
3190
+ const entries = (0, import_react11.useMemo)(
3191
+ () => (0, import_core8.computeDiff)(originalJson, currentJson),
3228
3192
  [originalJson, currentJson]
3229
3193
  );
3230
3194
  const added = entries.filter((e) => e.type === "added").length;
3231
3195
  const removed = entries.filter((e) => e.type === "removed").length;
3232
3196
  const changed = entries.filter((e) => e.type === "changed").length;
3233
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
3197
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3234
3198
  "div",
3235
3199
  {
3236
3200
  className,
@@ -3243,7 +3207,7 @@ function DiffView({
3243
3207
  flexDirection: "column"
3244
3208
  },
3245
3209
  children: [
3246
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
3210
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
3247
3211
  "div",
3248
3212
  {
3249
3213
  style: {
@@ -3258,18 +3222,18 @@ function DiffView({
3258
3222
  flexShrink: 0
3259
3223
  },
3260
3224
  children: [
3261
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: "var(--vj-text-muted, #999999)" }, children: entries.length === 0 ? "No changes" : `${entries.length} changes` }),
3262
- added > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { color: "#4ec94e" }, children: [
3225
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { color: "var(--vj-text-muted, #999999)" }, children: entries.length === 0 ? "No changes" : `${entries.length} changes` }),
3226
+ added > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { color: "#4ec94e" }, children: [
3263
3227
  "+",
3264
3228
  added,
3265
3229
  " added"
3266
3230
  ] }),
3267
- removed > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { color: "#f48771" }, children: [
3231
+ removed > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { color: "#f48771" }, children: [
3268
3232
  "-",
3269
3233
  removed,
3270
3234
  " removed"
3271
3235
  ] }),
3272
- changed > 0 && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { color: "#dcdcaa" }, children: [
3236
+ changed > 0 && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { color: "#dcdcaa" }, children: [
3273
3237
  "~",
3274
3238
  changed,
3275
3239
  " modified"
@@ -3277,7 +3241,7 @@ function DiffView({
3277
3241
  ]
3278
3242
  }
3279
3243
  ),
3280
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
3244
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
3281
3245
  "div",
3282
3246
  {
3283
3247
  style: {
@@ -3291,7 +3255,7 @@ function DiffView({
3291
3255
  },
3292
3256
  children: "No differences detected"
3293
3257
  }
3294
- ) : entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(DiffRow, { entry }, i)) })
3258
+ ) : entries.map((entry, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DiffRow, { entry }, i)) })
3295
3259
  ]
3296
3260
  }
3297
3261
  );
@@ -3303,7 +3267,6 @@ function DiffView({
3303
3267
  DiffView,
3304
3268
  FormView,
3305
3269
  JsonEditor,
3306
- PropertyEditor,
3307
3270
  SearchBar,
3308
3271
  StudioContext,
3309
3272
  TreeView,