@visual-json/react 0.1.1 → 0.2.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,7 @@ var import_react10 = require("react");
40
39
 
41
40
  // src/visual-json.tsx
42
41
  var import_react2 = require("react");
43
- var import_core = require("@visual-json/core");
42
+ var import_core2 = require("@visual-json/core");
44
43
 
45
44
  // src/context.ts
46
45
  var import_react = require("react");
@@ -53,6 +52,91 @@ function useStudio() {
53
52
  return ctx;
54
53
  }
55
54
 
55
+ // src/get-visible-nodes.ts
56
+ function getVisibleNodes(root, isExpanded) {
57
+ const result = [];
58
+ function walk(node) {
59
+ result.push(node);
60
+ if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
61
+ for (const child of node.children) {
62
+ walk(child);
63
+ }
64
+ }
65
+ }
66
+ walk(root);
67
+ return result;
68
+ }
69
+
70
+ // src/selection-utils.ts
71
+ var import_core = require("@visual-json/core");
72
+ function computeSelectAllIds(tree, focusedNodeId, currentlySelected) {
73
+ if (!focusedNodeId) return null;
74
+ let node = tree.nodesById.get(focusedNodeId);
75
+ if (!node) return null;
76
+ while (node) {
77
+ const parent = node.parentId ? tree.nodesById.get(node.parentId) : void 0;
78
+ const siblings = parent ? parent.children : [tree.root];
79
+ const siblingIds = new Set(siblings.map((s) => s.id));
80
+ const allSelected = siblings.every((s) => currentlySelected.has(s.id));
81
+ if (!allSelected) {
82
+ return siblingIds;
83
+ }
84
+ if (!parent || !parent.parentId) return siblingIds;
85
+ node = parent;
86
+ }
87
+ return null;
88
+ }
89
+ function computeRangeIds(visibleNodes, anchorId, targetId) {
90
+ const anchorIdx = visibleNodes.findIndex((n) => n.id === anchorId);
91
+ const targetIdx = visibleNodes.findIndex((n) => n.id === targetId);
92
+ if (anchorIdx === -1 || targetIdx === -1) return null;
93
+ const start = Math.min(anchorIdx, targetIdx);
94
+ const end = Math.max(anchorIdx, targetIdx);
95
+ const ids = /* @__PURE__ */ new Set();
96
+ for (let i = start; i <= end; i++) {
97
+ ids.add(visibleNodes[i].id);
98
+ }
99
+ return ids;
100
+ }
101
+ function deleteSelectedNodes(tree, selectedIds, visibleNodes) {
102
+ const idsToDelete = [...selectedIds].filter((id) => {
103
+ const node = tree.nodesById.get(id);
104
+ if (!node || node.parentId === null) return false;
105
+ let cur = tree.nodesById.get(node.parentId);
106
+ while (cur) {
107
+ if (selectedIds.has(cur.id)) return false;
108
+ cur = cur.parentId ? tree.nodesById.get(cur.parentId) : void 0;
109
+ }
110
+ return true;
111
+ });
112
+ if (idsToDelete.length === 0) return { newTree: tree, nextFocusId: null };
113
+ const firstDeletedIdx = visibleNodes.findIndex((n) => selectedIds.has(n.id));
114
+ let newTree = tree;
115
+ for (const id of idsToDelete) {
116
+ if (newTree.nodesById.has(id)) {
117
+ newTree = (0, import_core.removeNode)(newTree, id);
118
+ }
119
+ }
120
+ let nextFocusId = null;
121
+ for (let i = firstDeletedIdx; i < visibleNodes.length; i++) {
122
+ const id = visibleNodes[i].id;
123
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
124
+ nextFocusId = id;
125
+ break;
126
+ }
127
+ }
128
+ if (!nextFocusId) {
129
+ for (let i = firstDeletedIdx - 1; i >= 0; i--) {
130
+ const id = visibleNodes[i].id;
131
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
132
+ nextFocusId = id;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ return { newTree, nextFocusId };
138
+ }
139
+
56
140
  // src/visual-json.tsx
57
141
  var import_jsx_runtime = require("react/jsx-runtime");
58
142
  function collectAllIds(node) {
@@ -68,12 +152,44 @@ function VisualJson({
68
152
  schema,
69
153
  children
70
154
  }) {
71
- const [tree, setTreeState] = (0, import_react2.useState)(() => (0, import_core.fromJson)(value));
72
- const [selectedNodeId, setSelectedNodeId] = (0, import_react2.useState)(null);
155
+ const [tree, setTreeState] = (0, import_react2.useState)(() => (0, import_core2.fromJson)(value));
156
+ const [focusedNodeId, setFocusedNodeId] = (0, import_react2.useState)(null);
157
+ const [selectedNodeIds, setSelectedNodeIdsState] = (0, import_react2.useState)(
158
+ () => /* @__PURE__ */ new Set()
159
+ );
160
+ const selectedNodeIdsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
161
+ const setSelectedNodeIds = (0, import_react2.useCallback)((ids) => {
162
+ selectedNodeIdsRef.current = ids;
163
+ setSelectedNodeIdsState(ids);
164
+ }, []);
165
+ const anchorNodeIdRef = (0, import_react2.useRef)(null);
166
+ const [anchorNodeId, setAnchorNodeIdState] = (0, import_react2.useState)(null);
167
+ const [drillDownNodeId, setDrillDownNodeId] = (0, import_react2.useState)(null);
73
168
  const [expandedNodeIds, setExpandedNodeIds] = (0, import_react2.useState)(
74
169
  () => /* @__PURE__ */ new Set([tree.root.id])
75
170
  );
76
- const historyRef = (0, import_react2.useRef)(new import_core.History());
171
+ const setAnchorNodeId = (0, import_react2.useCallback)((id) => {
172
+ anchorNodeIdRef.current = id;
173
+ setAnchorNodeIdState(id);
174
+ }, []);
175
+ const focusSelectAndDrillDown = (0, import_react2.useCallback)(
176
+ (nodeId) => {
177
+ setFocusedNodeId(nodeId);
178
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
179
+ setAnchorNodeId(nodeId);
180
+ setDrillDownNodeId(nodeId);
181
+ },
182
+ [setSelectedNodeIds, setAnchorNodeId]
183
+ );
184
+ const visibleNodes = (0, import_react2.useMemo)(
185
+ () => getVisibleNodes(tree.root, (id) => expandedNodeIds.has(id)),
186
+ [tree.root, expandedNodeIds]
187
+ );
188
+ const visibleNodesOverrideRef = (0, import_react2.useRef)(null);
189
+ const setVisibleNodesOverride = (0, import_react2.useCallback)((nodes) => {
190
+ visibleNodesOverrideRef.current = nodes;
191
+ }, []);
192
+ const historyRef = (0, import_react2.useRef)(new import_core2.History());
77
193
  const isInternalChange = (0, import_react2.useRef)(false);
78
194
  const hasMounted = (0, import_react2.useRef)(false);
79
195
  const [canUndo, setCanUndo] = (0, import_react2.useState)(false);
@@ -98,11 +214,11 @@ function VisualJson({
98
214
  isInternalChange.current = false;
99
215
  return;
100
216
  }
101
- const newTree = (0, import_core.fromJson)(value);
217
+ const newTree = (0, import_core2.fromJson)(value);
102
218
  setTreeState(newTree);
103
219
  setExpandedNodeIds(/* @__PURE__ */ new Set([newTree.root.id]));
104
- setSelectedNodeId(null);
105
- historyRef.current = new import_core.History();
220
+ focusSelectAndDrillDown(null);
221
+ historyRef.current = new import_core2.History();
106
222
  historyRef.current.push(newTree);
107
223
  setCanUndo(false);
108
224
  setCanRedo(false);
@@ -118,7 +234,7 @@ function VisualJson({
118
234
  setCanUndo(historyRef.current.canUndo);
119
235
  setCanRedo(historyRef.current.canRedo);
120
236
  isInternalChange.current = true;
121
- onChange?.((0, import_core.toJson)(newTree.root));
237
+ onChange?.((0, import_core2.toJson)(newTree.root));
122
238
  },
123
239
  [onChange]
124
240
  );
@@ -129,7 +245,7 @@ function VisualJson({
129
245
  setCanUndo(historyRef.current.canUndo);
130
246
  setCanRedo(historyRef.current.canRedo);
131
247
  isInternalChange.current = true;
132
- onChange?.((0, import_core.toJson)(prev.root));
248
+ onChange?.((0, import_core2.toJson)(prev.root));
133
249
  }
134
250
  }, [onChange]);
135
251
  const redo = (0, import_react2.useCallback)(() => {
@@ -139,7 +255,7 @@ function VisualJson({
139
255
  setCanUndo(historyRef.current.canUndo);
140
256
  setCanRedo(historyRef.current.canRedo);
141
257
  isInternalChange.current = true;
142
- onChange?.((0, import_core.toJson)(next.root));
258
+ onChange?.((0, import_core2.toJson)(next.root));
143
259
  }
144
260
  }, [onChange]);
145
261
  (0, import_react2.useEffect)(() => {
@@ -159,8 +275,66 @@ function VisualJson({
159
275
  document.addEventListener("keydown", handleKeyDown);
160
276
  return () => document.removeEventListener("keydown", handleKeyDown);
161
277
  }, [undo, redo]);
162
- const selectNode = (0, import_react2.useCallback)((nodeId) => {
163
- setSelectedNodeId(nodeId);
278
+ const selectNode = (0, import_react2.useCallback)(
279
+ (nodeId) => {
280
+ setFocusedNodeId(nodeId);
281
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
282
+ setAnchorNodeId(nodeId);
283
+ },
284
+ [setSelectedNodeIds, setAnchorNodeId]
285
+ );
286
+ const selectAndDrillDown = focusSelectAndDrillDown;
287
+ const toggleNodeSelection = (0, import_react2.useCallback)(
288
+ (nodeId) => {
289
+ const next = new Set(selectedNodeIdsRef.current);
290
+ if (next.has(nodeId)) {
291
+ next.delete(nodeId);
292
+ } else {
293
+ next.add(nodeId);
294
+ }
295
+ setSelectedNodeIds(next);
296
+ if (next.size === 0) {
297
+ setFocusedNodeId(null);
298
+ setAnchorNodeId(null);
299
+ } else {
300
+ setFocusedNodeId(nodeId);
301
+ setAnchorNodeId(nodeId);
302
+ }
303
+ },
304
+ [setSelectedNodeIds, setAnchorNodeId]
305
+ );
306
+ const selectNodeRange = (0, import_react2.useCallback)(
307
+ (toNodeId) => {
308
+ const nodes = visibleNodesOverrideRef.current ?? visibleNodes;
309
+ const anchor = anchorNodeIdRef.current;
310
+ if (!anchor) {
311
+ setFocusedNodeId(toNodeId);
312
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
313
+ setAnchorNodeId(toNodeId);
314
+ return;
315
+ }
316
+ const rangeIds = computeRangeIds(nodes, anchor, toNodeId);
317
+ if (!rangeIds) {
318
+ setFocusedNodeId(toNodeId);
319
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
320
+ setAnchorNodeId(toNodeId);
321
+ return;
322
+ }
323
+ setSelectedNodeIds(rangeIds);
324
+ setFocusedNodeId(toNodeId);
325
+ },
326
+ [visibleNodes, setSelectedNodeIds, setAnchorNodeId]
327
+ );
328
+ const setSelection = (0, import_react2.useCallback)(
329
+ (focusedId, newSelectedIds, newAnchorId) => {
330
+ setFocusedNodeId(focusedId);
331
+ setSelectedNodeIds(newSelectedIds);
332
+ setAnchorNodeId(newAnchorId);
333
+ },
334
+ [setSelectedNodeIds, setAnchorNodeId]
335
+ );
336
+ const drillDown = (0, import_react2.useCallback)((nodeId) => {
337
+ setDrillDownNodeId(nodeId);
164
338
  }, []);
165
339
  const toggleExpand = (0, import_react2.useCallback)((nodeId) => {
166
340
  setExpandedNodeIds((prev) => {
@@ -200,13 +374,14 @@ function VisualJson({
200
374
  setSearchMatchNodeIds(/* @__PURE__ */ new Set());
201
375
  return;
202
376
  }
203
- const matches = (0, import_core.searchNodes)(tree, query);
377
+ const matches = (0, import_core2.searchNodes)(tree, query);
204
378
  setSearchMatches(matches);
205
379
  setSearchMatchIndex(0);
206
380
  const matchIds = new Set(matches.map((m) => m.nodeId));
207
381
  setSearchMatchNodeIds(matchIds);
208
382
  if (matches.length > 0) {
209
- const ancestors = (0, import_core.getAncestorIds)(
383
+ const firstId = matches[0].nodeId;
384
+ const ancestors = (0, import_core2.getAncestorIds)(
210
385
  tree,
211
386
  matches.map((m) => m.nodeId)
212
387
  );
@@ -215,7 +390,7 @@ function VisualJson({
215
390
  for (const id of ancestors) next.add(id);
216
391
  return next;
217
392
  });
218
- setSelectedNodeId(matches[0].nodeId);
393
+ focusSelectAndDrillDown(firstId);
219
394
  }
220
395
  },
221
396
  [tree]
@@ -224,17 +399,17 @@ function VisualJson({
224
399
  if (searchMatches.length === 0) return;
225
400
  const nextIdx = (searchMatchIndex + 1) % searchMatches.length;
226
401
  setSearchMatchIndex(nextIdx);
227
- setSelectedNodeId(searchMatches[nextIdx].nodeId);
228
- }, [searchMatches, searchMatchIndex]);
402
+ focusSelectAndDrillDown(searchMatches[nextIdx].nodeId);
403
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
229
404
  const prevSearchMatch = (0, import_react2.useCallback)(() => {
230
405
  if (searchMatches.length === 0) return;
231
406
  const prevIdx = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length;
232
407
  setSearchMatchIndex(prevIdx);
233
- setSelectedNodeId(searchMatches[prevIdx].nodeId);
234
- }, [searchMatches, searchMatchIndex]);
408
+ focusSelectAndDrillDown(searchMatches[prevIdx].nodeId);
409
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
235
410
  (0, import_react2.useEffect)(() => {
236
411
  if (!searchQuery.trim()) return;
237
- const matches = (0, import_core.searchNodes)(tree, searchQuery);
412
+ const matches = (0, import_core2.searchNodes)(tree, searchQuery);
238
413
  setSearchMatches(matches);
239
414
  setSearchMatchIndex(
240
415
  (prev) => Math.min(prev, Math.max(matches.length - 1, 0))
@@ -244,7 +419,10 @@ function VisualJson({
244
419
  const state = (0, import_react2.useMemo)(
245
420
  () => ({
246
421
  tree,
247
- selectedNodeId,
422
+ focusedNodeId,
423
+ selectedNodeIds,
424
+ anchorNodeId,
425
+ drillDownNodeId,
248
426
  expandedNodeIds,
249
427
  schema: schema ?? null,
250
428
  searchQuery,
@@ -254,7 +432,10 @@ function VisualJson({
254
432
  }),
255
433
  [
256
434
  tree,
257
- selectedNodeId,
435
+ focusedNodeId,
436
+ selectedNodeIds,
437
+ anchorNodeId,
438
+ drillDownNodeId,
258
439
  expandedNodeIds,
259
440
  schema,
260
441
  searchQuery,
@@ -267,6 +448,12 @@ function VisualJson({
267
448
  () => ({
268
449
  setTree,
269
450
  selectNode,
451
+ selectAndDrillDown,
452
+ toggleNodeSelection,
453
+ selectNodeRange,
454
+ setSelection,
455
+ setVisibleNodesOverride,
456
+ drillDown,
270
457
  toggleExpand,
271
458
  expandNode,
272
459
  collapseNode,
@@ -283,6 +470,12 @@ function VisualJson({
283
470
  [
284
471
  setTree,
285
472
  selectNode,
473
+ selectAndDrillDown,
474
+ toggleNodeSelection,
475
+ selectNodeRange,
476
+ setSelection,
477
+ setVisibleNodesOverride,
478
+ drillDown,
286
479
  toggleExpand,
287
480
  expandNode,
288
481
  collapseNode,
@@ -303,7 +496,7 @@ function VisualJson({
303
496
 
304
497
  // src/tree-view.tsx
305
498
  var import_react5 = require("react");
306
- var import_core3 = require("@visual-json/core");
499
+ var import_core4 = require("@visual-json/core");
307
500
 
308
501
  // src/context-menu.tsx
309
502
  var import_react3 = require("react");
@@ -417,96 +610,194 @@ function getDisplayKey(node, state) {
417
610
  return node.key;
418
611
  }
419
612
 
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
613
  // src/use-drag-drop.ts
436
614
  var import_react4 = require("react");
437
- var import_core2 = require("@visual-json/core");
615
+ var import_core3 = require("@visual-json/core");
616
+
617
+ // src/theme.ts
618
+ var DEFAULT_CSS_VARS = {
619
+ "--vj-bg": "#1e1e1e",
620
+ "--vj-bg-panel": "#252526",
621
+ "--vj-bg-hover": "#2a2d2e",
622
+ "--vj-bg-selected": "#2a5a1e",
623
+ "--vj-bg-selected-muted": "#2a2d2e",
624
+ "--vj-bg-match": "#3a3520",
625
+ "--vj-bg-match-active": "#51502b",
626
+ "--vj-border": "#333333",
627
+ "--vj-border-subtle": "#2a2a2a",
628
+ "--vj-text": "#cccccc",
629
+ "--vj-text-muted": "#888888",
630
+ "--vj-text-dim": "#666666",
631
+ "--vj-text-dimmer": "#555555",
632
+ "--vj-string": "#ce9178",
633
+ "--vj-number": "#b5cea8",
634
+ "--vj-boolean": "#569cd6",
635
+ "--vj-accent": "#007acc",
636
+ "--vj-accent-muted": "#094771",
637
+ "--vj-input-bg": "#3c3c3c",
638
+ "--vj-input-border": "#555555",
639
+ "--vj-error": "#f48771",
640
+ "--vj-font": "monospace",
641
+ "--vj-input-font-size": "13px"
642
+ };
643
+
644
+ // src/use-drag-drop.ts
645
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
438
646
  var INITIAL_DRAG_STATE = {
439
- draggedNodeId: null,
647
+ draggedNodeIds: EMPTY_SET,
440
648
  dropTargetNodeId: null,
441
649
  dropPosition: null
442
650
  };
443
- function useDragDrop() {
651
+ function sortByTreeOrder(root, ids) {
652
+ const result = [];
653
+ function walk(node) {
654
+ if (ids.has(node.id)) result.push(node.id);
655
+ for (const child of node.children) walk(child);
656
+ }
657
+ walk(root);
658
+ return result;
659
+ }
660
+ function setMultiDragImage(e, count) {
661
+ const ghost = document.createElement("div");
662
+ ghost.textContent = `${count} selected`;
663
+ const root = document.querySelector("[data-form-container], [role='tree']");
664
+ const cs = root ? getComputedStyle(root) : null;
665
+ const bg = cs?.getPropertyValue("--vj-bg-selected").trim() || DEFAULT_CSS_VARS["--vj-bg-selected"];
666
+ const fg = cs?.getPropertyValue("--vj-text-selected").trim() || cs?.getPropertyValue("--vj-text").trim() || DEFAULT_CSS_VARS["--vj-text"];
667
+ const font = cs?.getPropertyValue("--vj-font").trim() || DEFAULT_CSS_VARS["--vj-font"];
668
+ ghost.style.cssText = [
669
+ "position:fixed",
670
+ "top:-1000px",
671
+ "left:-1000px",
672
+ "padding:4px 12px",
673
+ `background:${bg}`,
674
+ `color:${fg}`,
675
+ `font-family:${font}`,
676
+ "font-size:13px",
677
+ "border-radius:4px",
678
+ "white-space:nowrap",
679
+ "pointer-events:none"
680
+ ].join(";");
681
+ document.body.appendChild(ghost);
682
+ e.dataTransfer.setDragImage(ghost, 0, 14);
683
+ requestAnimationFrame(() => ghost.remove());
684
+ }
685
+ function useDragDrop(visibleNodes, selectedNodeIds) {
444
686
  const { state, actions } = useStudio();
445
687
  const [dragState, setDragState] = (0, import_react4.useState)(INITIAL_DRAG_STATE);
446
688
  const dragStateRef = (0, import_react4.useRef)(dragState);
447
689
  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)(
690
+ const visibleNodeIndexMap = (0, import_react4.useMemo)(() => {
691
+ const map = /* @__PURE__ */ new Map();
692
+ visibleNodes.forEach((n, i) => map.set(n.id, i));
693
+ return map;
694
+ }, [visibleNodes]);
695
+ const handleDragStart = (0, import_react4.useCallback)(
696
+ (nodeId) => {
697
+ let ids;
698
+ if (selectedNodeIds.size > 0 && selectedNodeIds.has(nodeId)) {
699
+ ids = selectedNodeIds;
700
+ } else {
701
+ ids = /* @__PURE__ */ new Set([nodeId]);
702
+ }
703
+ setDragState({
704
+ draggedNodeIds: ids,
705
+ dropTargetNodeId: null,
706
+ dropPosition: null
707
+ });
708
+ },
709
+ [selectedNodeIds]
710
+ );
711
+ const rawDragOver = (0, import_react4.useCallback)(
456
712
  (nodeId, position) => {
713
+ const draggedIds = dragStateRef.current.draggedNodeIds;
714
+ for (const draggedId of draggedIds) {
715
+ if (nodeId === draggedId || (0, import_core3.isDescendant)(state.tree, nodeId, draggedId)) {
716
+ return;
717
+ }
718
+ }
457
719
  setDragState((prev) => ({
458
720
  ...prev,
459
721
  dropTargetNodeId: nodeId,
460
722
  dropPosition: position
461
723
  }));
462
724
  },
463
- []
725
+ [state.tree]
726
+ );
727
+ const handleDragOver = (0, import_react4.useCallback)(
728
+ (nodeId, position) => {
729
+ if (position === "before") {
730
+ const idx = visibleNodeIndexMap.get(nodeId);
731
+ if (idx !== void 0 && idx > 0) {
732
+ rawDragOver(visibleNodes[idx - 1].id, "after");
733
+ return;
734
+ }
735
+ }
736
+ rawDragOver(nodeId, position);
737
+ },
738
+ [visibleNodes, visibleNodeIndexMap, rawDragOver]
464
739
  );
465
740
  const handleDragEnd = (0, import_react4.useCallback)(() => {
466
741
  setDragState(INITIAL_DRAG_STATE);
467
742
  }, []);
468
743
  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);
744
+ const { draggedNodeIds, dropTargetNodeId, dropPosition } = dragStateRef.current;
745
+ if (draggedNodeIds.size === 0 || !dropTargetNodeId || !dropPosition) return;
472
746
  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);
747
+ if (!targetNode || !targetNode.parentId) return;
748
+ for (const id of draggedNodeIds) {
749
+ if ((0, import_core3.isDescendant)(state.tree, dropTargetNodeId, id)) return;
750
+ }
751
+ const targetParentId = targetNode.parentId;
752
+ const targetParent = state.tree.nodesById.get(targetParentId);
753
+ if (!targetParent) return;
754
+ const parentChildren = targetParent.children;
755
+ const orderedDragIds = parentChildren.filter((c) => draggedNodeIds.has(c.id)).map((c) => c.id);
756
+ const allSameParent = orderedDragIds.length === draggedNodeIds.size && [...draggedNodeIds].every((id) => {
757
+ const n = state.tree.nodesById.get(id);
758
+ return n?.parentId === targetParentId;
759
+ });
760
+ if (allSameParent) {
761
+ const newTree = (0, import_core3.reorderChildrenMulti)(
762
+ state.tree,
763
+ targetParentId,
764
+ orderedDragIds,
765
+ dropTargetNodeId,
766
+ dropPosition
767
+ );
768
+ actions.setTree(newTree);
769
+ } else {
770
+ const orderedIds = sortByTreeOrder(state.tree.root, draggedNodeIds);
771
+ const draggedNodes = orderedIds.map((id) => state.tree.nodesById.get(id)).filter((n) => !!n && n.parentId !== null).map((n) => structuredClone(n));
772
+ let newTree = state.tree;
773
+ for (const id of [...orderedIds].reverse()) {
774
+ if (newTree.nodesById.has(id)) {
775
+ newTree = (0, import_core3.removeNode)(newTree, id);
493
776
  }
494
777
  }
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
778
+ const updatedTarget = newTree.nodesById.get(dropTargetNodeId);
779
+ if (!updatedTarget || !updatedTarget.parentId) {
780
+ setDragState(INITIAL_DRAG_STATE);
781
+ return;
782
+ }
783
+ const updatedParent = newTree.nodesById.get(updatedTarget.parentId);
784
+ if (!updatedParent) {
785
+ setDragState(INITIAL_DRAG_STATE);
786
+ return;
787
+ }
788
+ let insertIdx = updatedParent.children.findIndex(
789
+ (c) => c.id === dropTargetNodeId
790
+ );
791
+ if (dropPosition === "after") insertIdx++;
792
+ for (let i = 0; i < draggedNodes.length; i++) {
793
+ newTree = (0, import_core3.insertNode)(
794
+ newTree,
795
+ updatedParent.id,
796
+ draggedNodes[i],
797
+ insertIdx + i
507
798
  );
508
- actions.setTree(newTree);
509
799
  }
800
+ actions.setTree(newTree);
510
801
  }
511
802
  setDragState(INITIAL_DRAG_STATE);
512
803
  }, [state.tree, actions]);
@@ -528,6 +819,7 @@ function TreeNodeRow({
528
819
  showValues,
529
820
  showCounts,
530
821
  isFocused,
822
+ onSelectRange,
531
823
  onDragStart,
532
824
  onDragOver,
533
825
  onDragEnd,
@@ -535,19 +827,15 @@ function TreeNodeRow({
535
827
  onContextMenu
536
828
  }) {
537
829
  const { state, actions } = useStudio();
538
- const isSelected = state.selectedNodeId === node.id;
830
+ const isSelected = state.selectedNodeIds.has(node.id);
539
831
  const isExpanded = state.expandedNodeIds.has(node.id);
540
832
  const isContainer = node.type === "object" || node.type === "array";
541
833
  const [hovered, setHovered] = (0, import_react5.useState)(false);
542
834
  const isRoot = node.parentId === null;
543
835
  const isSearchMatch = state.searchMatchNodeIds.has(node.id);
544
836
  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
837
  const isDragTarget = dragState.dropTargetNodeId === node.id;
550
- const isDraggedNode = dragState.draggedNodeId === node.id;
838
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
551
839
  function displayValue() {
552
840
  if (isContainer) {
553
841
  return node.type === "array" ? `[${node.children.length}]` : `{${node.children.length}}`;
@@ -563,12 +851,12 @@ function TreeNodeRow({
563
851
  const position = e.clientY < midY ? "before" : "after";
564
852
  onDragOver(node.id, position);
565
853
  }
566
- let borderTop = "none";
567
- let borderBottom = "none";
854
+ let borderTopColor = "transparent";
855
+ let borderBottomColor = "transparent";
568
856
  if (isDragTarget && dragState.dropPosition === "before") {
569
- borderTop = "2px solid var(--vj-accent, #007acc)";
857
+ borderTopColor = "var(--vj-accent, #007acc)";
570
858
  } else if (isDragTarget && dragState.dropPosition === "after") {
571
- borderBottom = "2px solid var(--vj-accent, #007acc)";
859
+ borderBottomColor = "var(--vj-accent, #007acc)";
572
860
  }
573
861
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
574
862
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
@@ -577,13 +865,24 @@ function TreeNodeRow({
577
865
  role: "treeitem",
578
866
  "aria-selected": isSelected,
579
867
  "aria-expanded": isContainer ? isExpanded : void 0,
580
- onClick: () => actions.selectNode(node.id),
868
+ onClick: (e) => {
869
+ if (e.shiftKey) {
870
+ onSelectRange(node.id);
871
+ } else if (e.metaKey || e.ctrlKey) {
872
+ actions.toggleNodeSelection(node.id);
873
+ } else {
874
+ actions.selectAndDrillDown(node.id);
875
+ }
876
+ },
581
877
  onMouseEnter: () => setHovered(true),
582
878
  onMouseLeave: () => setHovered(false),
583
879
  onContextMenu: (e) => onContextMenu(e, node),
584
880
  draggable: !isRoot,
585
881
  onDragStart: (e) => {
586
882
  e.dataTransfer.effectAllowed = "move";
883
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
884
+ setMultiDragImage(e, state.selectedNodeIds.size);
885
+ }
587
886
  onDragStart(node.id);
588
887
  },
589
888
  onDragOver: handleDragOverEvent,
@@ -597,15 +896,16 @@ function TreeNodeRow({
597
896
  display: "flex",
598
897
  alignItems: "center",
599
898
  gap: 6,
600
- padding: "3px 8px",
899
+ padding: "1px 8px",
601
900
  paddingLeft: 8 + depth * 16,
602
901
  cursor: "pointer",
603
902
  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
903
  minHeight: 28,
605
904
  userSelect: "none",
606
905
  opacity: isDraggedNode ? 0.4 : 1,
607
- borderTop,
608
- borderBottom,
906
+ borderTop: `2px solid ${borderTopColor}`,
907
+ borderBottom: `2px solid ${borderBottomColor}`,
908
+ boxSizing: "border-box",
609
909
  color: isSelected && isFocused ? "var(--vj-text-selected, var(--vj-text, #cccccc))" : "var(--vj-text, #cccccc)"
610
910
  },
611
911
  children: [
@@ -688,6 +988,7 @@ function TreeNodeRow({
688
988
  showValues,
689
989
  showCounts,
690
990
  isFocused,
991
+ onSelectRange,
691
992
  onDragStart,
692
993
  onDragOver,
693
994
  onDragEnd,
@@ -705,25 +1006,34 @@ function TreeView({
705
1006
  }) {
706
1007
  const { state, actions } = useStudio();
707
1008
  const containerRef = (0, import_react5.useRef)(null);
1009
+ const visibleNodes = (0, import_react5.useMemo)(
1010
+ () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
1011
+ [state.tree.root, state.expandedNodeIds]
1012
+ );
708
1013
  const {
709
1014
  dragState,
710
1015
  handleDragStart,
711
1016
  handleDragOver,
712
1017
  handleDragEnd,
713
1018
  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]
1019
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
1020
+ const handleSelectRange = (0, import_react5.useCallback)(
1021
+ (nodeId) => {
1022
+ actions.setVisibleNodesOverride(visibleNodes);
1023
+ actions.selectNodeRange(nodeId);
1024
+ },
1025
+ [visibleNodes, actions]
719
1026
  );
1027
+ const [contextMenu, setContextMenu] = (0, import_react5.useState)(null);
720
1028
  const handleContextMenu = (0, import_react5.useCallback)(
721
1029
  (e, node) => {
722
1030
  e.preventDefault();
723
- actions.selectNode(node.id);
1031
+ if (!state.selectedNodeIds.has(node.id)) {
1032
+ actions.selectAndDrillDown(node.id);
1033
+ }
724
1034
  setContextMenu({ x: e.clientX, y: e.clientY, node });
725
1035
  },
726
- [actions]
1036
+ [actions, state.selectedNodeIds]
727
1037
  );
728
1038
  const buildContextMenuItems = (0, import_react5.useCallback)(
729
1039
  (node) => {
@@ -767,7 +1077,7 @@ function TreeView({
767
1077
  items.push({
768
1078
  label: "Copy value as JSON",
769
1079
  action: () => {
770
- const val = (0, import_core3.toJson)(node);
1080
+ const val = (0, import_core4.toJson)(node);
771
1081
  const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
772
1082
  navigator.clipboard.writeText(text).catch(() => {
773
1083
  });
@@ -778,7 +1088,7 @@ function TreeView({
778
1088
  items.push({
779
1089
  label: "Duplicate",
780
1090
  action: () => {
781
- const newTree = (0, import_core3.duplicateNode)(state.tree, node.id);
1091
+ const newTree = (0, import_core4.duplicateNode)(state.tree, node.id);
782
1092
  actions.setTree(newTree);
783
1093
  }
784
1094
  });
@@ -792,7 +1102,7 @@ function TreeView({
792
1102
  ].filter((t) => t !== node.type).map((t) => ({
793
1103
  label: `Change to ${t}`,
794
1104
  action: () => {
795
- const newTree = (0, import_core3.changeType)(state.tree, node.id, t);
1105
+ const newTree = (0, import_core4.changeType)(state.tree, node.id, t);
796
1106
  actions.setTree(newTree);
797
1107
  }
798
1108
  }));
@@ -802,7 +1112,7 @@ function TreeView({
802
1112
  items.push({
803
1113
  label: "Delete",
804
1114
  action: () => {
805
- const newTree = (0, import_core3.removeNode)(state.tree, node.id);
1115
+ const newTree = (0, import_core4.removeNode)(state.tree, node.id);
806
1116
  actions.setTree(newTree);
807
1117
  }
808
1118
  });
@@ -814,19 +1124,31 @@ function TreeView({
814
1124
  const handleKeyDown = (0, import_react5.useCallback)(
815
1125
  (e) => {
816
1126
  const currentIndex = visibleNodes.findIndex(
817
- (n) => n.id === state.selectedNodeId
1127
+ (n) => n.id === state.focusedNodeId
818
1128
  );
819
1129
  switch (e.key) {
820
1130
  case "ArrowDown": {
821
1131
  e.preventDefault();
822
1132
  const next = visibleNodes[currentIndex + 1];
823
- if (next) actions.selectNode(next.id);
1133
+ if (next) {
1134
+ if (e.shiftKey) {
1135
+ handleSelectRange(next.id);
1136
+ } else {
1137
+ actions.selectNode(next.id);
1138
+ }
1139
+ }
824
1140
  break;
825
1141
  }
826
1142
  case "ArrowUp": {
827
1143
  e.preventDefault();
828
1144
  const prev = visibleNodes[currentIndex - 1];
829
- if (prev) actions.selectNode(prev.id);
1145
+ if (prev) {
1146
+ if (e.shiftKey) {
1147
+ handleSelectRange(prev.id);
1148
+ } else {
1149
+ actions.selectNode(prev.id);
1150
+ }
1151
+ }
830
1152
  break;
831
1153
  }
832
1154
  case "ArrowRight": {
@@ -853,17 +1175,47 @@ function TreeView({
853
1175
  }
854
1176
  break;
855
1177
  }
1178
+ case "a": {
1179
+ if (e.metaKey || e.ctrlKey) {
1180
+ e.preventDefault();
1181
+ const ids = computeSelectAllIds(
1182
+ state.tree,
1183
+ state.focusedNodeId,
1184
+ state.selectedNodeIds
1185
+ );
1186
+ if (ids) {
1187
+ actions.setSelection(
1188
+ state.focusedNodeId,
1189
+ ids,
1190
+ state.focusedNodeId
1191
+ );
1192
+ }
1193
+ }
1194
+ break;
1195
+ }
1196
+ case "Escape": {
1197
+ e.preventDefault();
1198
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
1199
+ actions.selectNode(state.focusedNodeId);
1200
+ } else {
1201
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
1202
+ }
1203
+ break;
1204
+ }
856
1205
  case "Delete":
857
1206
  case "Backspace": {
858
1207
  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
- }
1208
+ const { newTree, nextFocusId } = deleteSelectedNodes(
1209
+ state.tree,
1210
+ state.selectedNodeIds,
1211
+ visibleNodes
1212
+ );
1213
+ if (newTree === state.tree) break;
1214
+ actions.setTree(newTree);
1215
+ if (nextFocusId) {
1216
+ actions.selectNode(nextFocusId);
1217
+ } else {
1218
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
867
1219
  }
868
1220
  break;
869
1221
  }
@@ -871,23 +1223,25 @@ function TreeView({
871
1223
  },
872
1224
  [
873
1225
  visibleNodes,
874
- state.selectedNodeId,
1226
+ state.focusedNodeId,
1227
+ state.selectedNodeIds,
875
1228
  state.expandedNodeIds,
876
1229
  state.tree,
877
- actions
1230
+ actions,
1231
+ handleSelectRange
878
1232
  ]
879
1233
  );
880
1234
  const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
881
1235
  (0, import_react5.useEffect)(() => {
882
- if (state.selectedNodeId && containerRef.current) {
1236
+ if (state.focusedNodeId && containerRef.current) {
883
1237
  const el = containerRef.current.querySelector(
884
- `[data-node-id="${state.selectedNodeId}"]`
1238
+ `[data-node-id="${state.focusedNodeId}"]`
885
1239
  );
886
1240
  if (el) {
887
1241
  el.scrollIntoView({ block: "nearest" });
888
1242
  }
889
1243
  }
890
- }, [state.selectedNodeId]);
1244
+ }, [state.focusedNodeId]);
891
1245
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
892
1246
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
893
1247
  "div",
@@ -917,6 +1271,7 @@ function TreeView({
917
1271
  showValues,
918
1272
  showCounts,
919
1273
  isFocused,
1274
+ onSelectRange: handleSelectRange,
920
1275
  onDragStart: handleDragStart,
921
1276
  onDragOver: handleDragOver,
922
1277
  onDragEnd: handleDragEnd,
@@ -940,7 +1295,7 @@ function TreeView({
940
1295
 
941
1296
  // src/form-view.tsx
942
1297
  var import_react8 = require("react");
943
- var import_core4 = require("@visual-json/core");
1298
+ var import_core5 = require("@visual-json/core");
944
1299
 
945
1300
  // src/breadcrumbs.tsx
946
1301
  var import_react6 = require("react");
@@ -949,8 +1304,8 @@ var MAX_SUGGESTIONS = 20;
949
1304
  var DROPDOWN_MAX_HEIGHT = 200;
950
1305
  function Breadcrumbs({ className }) {
951
1306
  const { state, actions } = useStudio();
952
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
953
- const currentPath = selectedNode?.path ?? "/";
1307
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
1308
+ const currentPath = drillDownNode?.path ?? "/";
954
1309
  const [inputValue, setInputValue] = (0, import_react6.useState)(currentPath);
955
1310
  const [open, setOpen] = (0, import_react6.useState)(false);
956
1311
  const [highlightIndex, setHighlightIndex] = (0, import_react6.useState)(0);
@@ -980,7 +1335,7 @@ function Breadcrumbs({ className }) {
980
1335
  (path) => {
981
1336
  for (const [id, node] of state.tree.nodesById) {
982
1337
  if (node.path === path) {
983
- actions.selectNode(id);
1338
+ actions.selectAndDrillDown(id);
984
1339
  break;
985
1340
  }
986
1341
  }
@@ -1068,7 +1423,7 @@ function Breadcrumbs({ className }) {
1068
1423
  style: {
1069
1424
  width: "100%",
1070
1425
  boxSizing: "border-box",
1071
- padding: "2px 0",
1426
+ padding: "3px 0",
1072
1427
  fontSize: "var(--vj-input-font-size, 13px)",
1073
1428
  fontFamily: "var(--vj-font, monospace)",
1074
1429
  color: "var(--vj-text-muted, #999999)",
@@ -1295,9 +1650,9 @@ function EnumInput({
1295
1650
  var import_jsx_runtime6 = require("react/jsx-runtime");
1296
1651
  function getResolvedSchema(schema, rootSchema, path) {
1297
1652
  if (!schema) return void 0;
1298
- const raw = (0, import_core4.getPropertySchema)(schema, path, rootSchema);
1653
+ const raw = (0, import_core5.getPropertySchema)(schema, path, rootSchema);
1299
1654
  if (!raw) return void 0;
1300
- return (0, import_core4.resolveRef)(raw, rootSchema ?? schema);
1655
+ return (0, import_core5.resolveRef)(raw, rootSchema ?? schema);
1301
1656
  }
1302
1657
  function getValueColor(node) {
1303
1658
  if (node.type === "boolean" || node.type === "null")
@@ -1318,7 +1673,6 @@ function FormField({
1318
1673
  depth,
1319
1674
  showDescriptions,
1320
1675
  showCounts,
1321
- formSelectedNodeId,
1322
1676
  editingNodeId,
1323
1677
  collapsedIds,
1324
1678
  maxKeyLength,
@@ -1336,26 +1690,26 @@ function FormField({
1336
1690
  const { state, actions } = useStudio();
1337
1691
  const isContainer = node.type === "object" || node.type === "array";
1338
1692
  const collapsed = collapsedIds.has(node.id);
1339
- const isSelected = formSelectedNodeId === node.id;
1693
+ const isSelected = state.selectedNodeIds.has(node.id);
1340
1694
  const isEditing = editingNodeId === node.id;
1341
1695
  const propSchema = getResolvedSchema(schema, rootSchema, node.path);
1342
1696
  const isRequired = checkRequired(node, schema, rootSchema);
1343
1697
  const [hovered, setHovered] = (0, import_react8.useState)(false);
1344
1698
  const isRoot = node.parentId === null;
1345
1699
  const isDragTarget = dragState.dropTargetNodeId === node.id;
1346
- const isDraggedNode = dragState.draggedNodeId === node.id;
1700
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
1347
1701
  function handleDragOverEvent(e) {
1348
1702
  e.preventDefault();
1349
1703
  const rect = e.currentTarget.getBoundingClientRect();
1350
1704
  const midY = rect.top + rect.height / 2;
1351
1705
  onDragOver(node.id, e.clientY < midY ? "before" : "after");
1352
1706
  }
1353
- let borderTop = "none";
1354
- let borderBottom = "none";
1707
+ let borderTopColor = "transparent";
1708
+ let borderBottomColor = "transparent";
1355
1709
  if (isDragTarget && dragState.dropPosition === "before") {
1356
- borderTop = "2px solid var(--vj-accent, #007acc)";
1710
+ borderTopColor = "var(--vj-accent, #007acc)";
1357
1711
  } else if (isDragTarget && dragState.dropPosition === "after") {
1358
- borderBottom = "2px solid var(--vj-accent, #007acc)";
1712
+ borderBottomColor = "var(--vj-accent, #007acc)";
1359
1713
  }
1360
1714
  const valueRef = (0, import_react8.useRef)(null);
1361
1715
  const keyRef = (0, import_react8.useRef)(null);
@@ -1385,29 +1739,28 @@ function FormField({
1385
1739
  } else {
1386
1740
  parsed = newValue;
1387
1741
  }
1388
- const newTree = (0, import_core4.setValue)(state.tree, node.id, parsed);
1742
+ const newTree = (0, import_core5.setValue)(state.tree, node.id, parsed);
1389
1743
  actions.setTree(newTree);
1390
1744
  },
1391
1745
  [state.tree, node.id, node.type, actions, propSchema]
1392
1746
  );
1393
1747
  const handleKeyChange = (0, import_react8.useCallback)(
1394
1748
  (newKey) => {
1395
- const newTree = (0, import_core4.setKey)(state.tree, node.id, newKey);
1749
+ const newTree = (0, import_core5.setKey)(state.tree, node.id, newKey);
1396
1750
  actions.setTree(newTree);
1397
1751
  },
1398
1752
  [state.tree, node.id, actions]
1399
1753
  );
1400
1754
  const handleRemove = (0, import_react8.useCallback)(() => {
1401
- const newTree = (0, import_core4.removeNode)(state.tree, node.id);
1755
+ const newTree = (0, import_core5.removeNode)(state.tree, node.id);
1402
1756
  actions.setTree(newTree);
1403
1757
  }, [state.tree, node.id, actions]);
1404
1758
  const handleAddChild = (0, import_react8.useCallback)(() => {
1405
1759
  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, "");
1760
+ const newTree = (0, import_core5.addProperty)(state.tree, node.id, key, "");
1407
1761
  actions.setTree(newTree);
1408
1762
  }, [state.tree, node.id, node.type, node.children.length, actions]);
1409
1763
  const description = propSchema?.description;
1410
- const defaultVal = propSchema?.default;
1411
1764
  const isDeprecated = propSchema?.deprecated;
1412
1765
  const fieldTitle = propSchema?.title;
1413
1766
  const parentIsObject = node.parentId && state.tree.nodesById.get(node.parentId)?.type === "object";
@@ -1422,6 +1775,9 @@ function FormField({
1422
1775
  draggable: !isRoot,
1423
1776
  onDragStart: (e) => {
1424
1777
  e.dataTransfer.effectAllowed = "move";
1778
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1779
+ setMultiDragImage(e, state.selectedNodeIds.size);
1780
+ }
1425
1781
  onDragStart(node.id);
1426
1782
  },
1427
1783
  onDragOver: handleDragOverEvent,
@@ -1434,20 +1790,21 @@ function FormField({
1434
1790
  display: "flex",
1435
1791
  alignItems: "center",
1436
1792
  gap: 6,
1437
- padding: "3px 8px",
1793
+ padding: "1px 8px",
1438
1794
  paddingLeft: 8 + depth * 16,
1439
1795
  cursor: "pointer",
1440
1796
  backgroundColor: rowBg,
1441
1797
  color: rowColor,
1442
1798
  height: 28,
1799
+ boxSizing: "border-box",
1443
1800
  userSelect: "none",
1444
1801
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1445
- borderTop,
1446
- borderBottom
1802
+ borderTop: `2px solid ${borderTopColor}`,
1803
+ borderBottom: `2px solid ${borderBottomColor}`
1447
1804
  },
1448
1805
  onClick: (e) => {
1449
1806
  e.stopPropagation();
1450
- onSelect(node.id);
1807
+ onSelect(node.id, e);
1451
1808
  },
1452
1809
  onDoubleClick: () => onToggleCollapse(node.id),
1453
1810
  onMouseEnter: () => setHovered(true),
@@ -1503,7 +1860,7 @@ function FormField({
1503
1860
  {
1504
1861
  style: {
1505
1862
  color: !isRoot && !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1506
- fontSize: 13,
1863
+ fontSize: "var(--vj-input-font-size, 13px)",
1507
1864
  fontFamily: "var(--vj-font, monospace)",
1508
1865
  fontWeight: 500,
1509
1866
  flexShrink: 0,
@@ -1604,7 +1961,6 @@ function FormField({
1604
1961
  depth: depth + 1,
1605
1962
  showDescriptions,
1606
1963
  showCounts,
1607
- formSelectedNodeId,
1608
1964
  editingNodeId,
1609
1965
  collapsedIds,
1610
1966
  maxKeyLength,
@@ -1632,6 +1988,9 @@ function FormField({
1632
1988
  draggable: !isRoot,
1633
1989
  onDragStart: (e) => {
1634
1990
  e.dataTransfer.effectAllowed = "move";
1991
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1992
+ setMultiDragImage(e, state.selectedNodeIds.size);
1993
+ }
1635
1994
  onDragStart(node.id);
1636
1995
  },
1637
1996
  onDragOver: handleDragOverEvent,
@@ -1644,20 +2003,21 @@ function FormField({
1644
2003
  display: "flex",
1645
2004
  alignItems: "center",
1646
2005
  gap: 6,
1647
- padding: "3px 8px",
2006
+ padding: "1px 8px",
1648
2007
  paddingLeft: 8 + depth * 16,
1649
2008
  cursor: "pointer",
1650
2009
  backgroundColor: rowBg,
1651
2010
  color: rowColor,
1652
2011
  height: 28,
2012
+ boxSizing: "border-box",
1653
2013
  userSelect: "none",
1654
2014
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1655
- borderTop,
1656
- borderBottom
2015
+ borderTop: `2px solid ${borderTopColor}`,
2016
+ borderBottom: `2px solid ${borderBottomColor}`
1657
2017
  },
1658
2018
  onClick: (e) => {
1659
2019
  e.stopPropagation();
1660
- onSelect(node.id);
2020
+ onSelect(node.id, e);
1661
2021
  },
1662
2022
  onDoubleClick: () => onStartEditing(node.id),
1663
2023
  onMouseEnter: () => setHovered(true),
@@ -1694,7 +2054,7 @@ function FormField({
1694
2054
  {
1695
2055
  style: {
1696
2056
  color: !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1697
- fontSize: 13,
2057
+ fontSize: "var(--vj-input-font-size, 13px)",
1698
2058
  fontFamily: "var(--vj-font, monospace)",
1699
2059
  flexShrink: 0,
1700
2060
  display: "inline-block",
@@ -1738,7 +2098,7 @@ function FormField({
1738
2098
  {
1739
2099
  style: {
1740
2100
  color: valueColor,
1741
- fontSize: 13,
2101
+ fontSize: "var(--vj-input-font-size, 13px)",
1742
2102
  fontFamily: "var(--vj-font, monospace)",
1743
2103
  overflow: "hidden",
1744
2104
  textOverflow: "ellipsis",
@@ -1816,7 +2176,7 @@ function renderEditInput(node, propSchema, displayValue, handleValueChange, inpu
1816
2176
  style: {
1817
2177
  color: "var(--vj-boolean, #569cd6)",
1818
2178
  fontFamily: "var(--vj-font, monospace)",
1819
- fontSize: 13,
2179
+ fontSize: "var(--vj-input-font-size, 13px)",
1820
2180
  fontStyle: "italic",
1821
2181
  flex: 1
1822
2182
  },
@@ -1849,11 +2209,8 @@ function FormView({
1849
2209
  }) {
1850
2210
  const { state, actions } = useStudio();
1851
2211
  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
- );
2212
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
2213
+ const displayNode = drillDownNode ?? state.tree.root;
1857
2214
  const [editingNodeId, setEditingNodeId] = (0, import_react8.useState)(null);
1858
2215
  const preEditTreeRef = (0, import_react8.useRef)(null);
1859
2216
  const [collapsedIds, setCollapsedIds] = (0, import_react8.useState)(
@@ -1861,15 +2218,7 @@ function FormView({
1861
2218
  );
1862
2219
  const containerRef = (0, import_react8.useRef)(null);
1863
2220
  const [isFocused, setIsFocused] = (0, import_react8.useState)(false);
1864
- const {
1865
- dragState,
1866
- handleDragStart,
1867
- handleDragOver,
1868
- handleDragEnd,
1869
- handleDrop
1870
- } = useDragDrop();
1871
2221
  (0, import_react8.useEffect)(() => {
1872
- setFormSelectedNodeId(null);
1873
2222
  setEditingNodeId(null);
1874
2223
  setCollapsedIds(/* @__PURE__ */ new Set());
1875
2224
  }, [displayNode.id]);
@@ -1877,6 +2226,17 @@ function FormView({
1877
2226
  () => getVisibleNodes(displayNode, (id) => !collapsedIds.has(id)),
1878
2227
  [displayNode, collapsedIds]
1879
2228
  );
2229
+ const {
2230
+ dragState,
2231
+ handleDragStart,
2232
+ handleDragOver,
2233
+ handleDragEnd,
2234
+ handleDrop
2235
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
2236
+ (0, import_react8.useEffect)(() => {
2237
+ actions.setVisibleNodesOverride(visibleNodes);
2238
+ return () => actions.setVisibleNodesOverride(null);
2239
+ }, [visibleNodes, actions]);
1880
2240
  const { maxKeyLength, maxDepth } = (0, import_react8.useMemo)(() => {
1881
2241
  let maxKey = 1;
1882
2242
  let maxD = 0;
@@ -1890,10 +2250,20 @@ function FormView({
1890
2250
  }
1891
2251
  return { maxKeyLength: maxKey, maxDepth: maxD };
1892
2252
  }, [visibleNodes, displayNode.path, state.tree]);
1893
- const handleSelect = (0, import_react8.useCallback)((nodeId) => {
1894
- setFormSelectedNodeId(nodeId);
1895
- setEditingNodeId(null);
1896
- }, []);
2253
+ const handleSelect = (0, import_react8.useCallback)(
2254
+ (nodeId, e) => {
2255
+ setEditingNodeId(null);
2256
+ if (e.shiftKey) {
2257
+ actions.setVisibleNodesOverride(visibleNodes);
2258
+ actions.selectNodeRange(nodeId);
2259
+ } else if (e.metaKey || e.ctrlKey) {
2260
+ actions.toggleNodeSelection(nodeId);
2261
+ } else {
2262
+ actions.selectNode(nodeId);
2263
+ }
2264
+ },
2265
+ [actions, visibleNodes]
2266
+ );
1897
2267
  const handleToggleCollapse = (0, import_react8.useCallback)((nodeId) => {
1898
2268
  setCollapsedIds((prev) => {
1899
2269
  const next = new Set(prev);
@@ -1941,15 +2311,23 @@ function FormView({
1941
2311
  }
1942
2312
  return;
1943
2313
  }
1944
- const currentIndex = visibleNodes.findIndex(
1945
- (n) => n.id === formSelectedNodeId
2314
+ let currentIndex = visibleNodes.findIndex(
2315
+ (n) => n.id === state.focusedNodeId
1946
2316
  );
2317
+ if (currentIndex === -1 && visibleNodes.length > 0) {
2318
+ currentIndex = 0;
2319
+ }
1947
2320
  switch (e.key) {
1948
2321
  case "ArrowDown": {
1949
2322
  e.preventDefault();
1950
2323
  const next = visibleNodes[currentIndex + 1];
1951
2324
  if (next) {
1952
- setFormSelectedNodeId(next.id);
2325
+ if (e.shiftKey) {
2326
+ actions.setVisibleNodesOverride(visibleNodes);
2327
+ actions.selectNodeRange(next.id);
2328
+ } else {
2329
+ actions.selectNode(next.id);
2330
+ }
1953
2331
  scrollToNode(next.id);
1954
2332
  }
1955
2333
  break;
@@ -1958,7 +2336,12 @@ function FormView({
1958
2336
  e.preventDefault();
1959
2337
  const prev = visibleNodes[currentIndex - 1];
1960
2338
  if (prev) {
1961
- setFormSelectedNodeId(prev.id);
2339
+ if (e.shiftKey) {
2340
+ actions.setVisibleNodesOverride(visibleNodes);
2341
+ actions.selectNodeRange(prev.id);
2342
+ } else {
2343
+ actions.selectNode(prev.id);
2344
+ }
1962
2345
  scrollToNode(prev.id);
1963
2346
  }
1964
2347
  break;
@@ -1974,7 +2357,7 @@ function FormView({
1974
2357
  return next;
1975
2358
  });
1976
2359
  } else if (node.children.length > 0) {
1977
- setFormSelectedNodeId(node.children[0].id);
2360
+ actions.selectNode(node.children[0].id);
1978
2361
  scrollToNode(node.children[0].id);
1979
2362
  }
1980
2363
  }
@@ -1996,7 +2379,7 @@ function FormView({
1996
2379
  (n) => n.id === current.parentId
1997
2380
  );
1998
2381
  if (parentInVisible) {
1999
- setFormSelectedNodeId(parentInVisible.id);
2382
+ actions.selectNode(parentInVisible.id);
2000
2383
  scrollToNode(parentInVisible.id);
2001
2384
  }
2002
2385
  }
@@ -2004,30 +2387,54 @@ function FormView({
2004
2387
  }
2005
2388
  case "Enter": {
2006
2389
  e.preventDefault();
2007
- if (formSelectedNodeId) {
2390
+ if (state.focusedNodeId) {
2008
2391
  preEditTreeRef.current = state.tree;
2009
- setEditingNodeId(formSelectedNodeId);
2392
+ actions.selectNode(state.focusedNodeId);
2393
+ setEditingNodeId(state.focusedNodeId);
2394
+ }
2395
+ break;
2396
+ }
2397
+ case "a": {
2398
+ if (e.metaKey || e.ctrlKey) {
2399
+ e.preventDefault();
2400
+ const ids = computeSelectAllIds(
2401
+ state.tree,
2402
+ state.focusedNodeId,
2403
+ state.selectedNodeIds
2404
+ );
2405
+ if (ids) {
2406
+ actions.setSelection(
2407
+ state.focusedNodeId,
2408
+ ids,
2409
+ state.focusedNodeId
2410
+ );
2411
+ }
2010
2412
  }
2011
2413
  break;
2012
2414
  }
2013
2415
  case "Escape": {
2014
2416
  e.preventDefault();
2015
- setEditingNodeId(null);
2417
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
2418
+ actions.selectNode(state.focusedNodeId);
2419
+ } else {
2420
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2421
+ }
2016
2422
  break;
2017
2423
  }
2018
2424
  case "Delete":
2019
2425
  case "Backspace": {
2020
2426
  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
- }
2427
+ const { newTree, nextFocusId } = deleteSelectedNodes(
2428
+ state.tree,
2429
+ state.selectedNodeIds,
2430
+ visibleNodes
2431
+ );
2432
+ if (newTree === state.tree) break;
2433
+ actions.setTree(newTree);
2434
+ if (nextFocusId) {
2435
+ actions.selectNode(nextFocusId);
2436
+ } else {
2437
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2031
2438
  }
2032
2439
  break;
2033
2440
  }
@@ -2035,7 +2442,8 @@ function FormView({
2035
2442
  },
2036
2443
  [
2037
2444
  visibleNodes,
2038
- formSelectedNodeId,
2445
+ state.focusedNodeId,
2446
+ state.selectedNodeIds,
2039
2447
  editingNodeId,
2040
2448
  collapsedIds,
2041
2449
  scrollToNode,
@@ -2060,9 +2468,11 @@ function FormView({
2060
2468
  "div",
2061
2469
  {
2062
2470
  style: {
2471
+ display: "flex",
2472
+ alignItems: "center",
2063
2473
  padding: "4px 8px",
2064
2474
  borderBottom: "1px solid var(--vj-border, #333333)",
2065
- backgroundColor: "var(--vj-bg-panel, #252526)",
2475
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2066
2476
  flexShrink: 0
2067
2477
  },
2068
2478
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Breadcrumbs, {})
@@ -2095,7 +2505,6 @@ function FormView({
2095
2505
  depth: 0,
2096
2506
  showDescriptions,
2097
2507
  showCounts,
2098
- formSelectedNodeId,
2099
2508
  editingNodeId,
2100
2509
  collapsedIds,
2101
2510
  maxKeyLength,
@@ -2165,7 +2574,7 @@ function SearchBar({ className }) {
2165
2574
  alignItems: "center",
2166
2575
  gap: 6,
2167
2576
  padding: "4px 8px",
2168
- backgroundColor: "var(--vj-bg-panel, #252526)",
2577
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2169
2578
  borderBottom: "1px solid var(--vj-border, #333333)"
2170
2579
  },
2171
2580
  children: [
@@ -2372,31 +2781,6 @@ function SearchBar({ className }) {
2372
2781
 
2373
2782
  // src/json-editor.tsx
2374
2783
  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
2784
  function JsonEditor({
2401
2785
  value,
2402
2786
  defaultValue,
@@ -2723,430 +3107,10 @@ function EditorLayout({
2723
3107
  ] });
2724
3108
  }
2725
3109
 
2726
- // src/property-editor.tsx
2727
- var import_react11 = require("react");
2728
- var import_core5 = require("@visual-json/core");
2729
- 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
3110
  // src/diff-view.tsx
3147
- var import_react12 = require("react");
3111
+ var import_react11 = require("react");
3148
3112
  var import_core6 = require("@visual-json/core");
3149
- var import_jsx_runtime10 = require("react/jsx-runtime");
3113
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3150
3114
  var DIFF_COLORS = {
3151
3115
  added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
3152
3116
  removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
@@ -3167,7 +3131,7 @@ function formatValue(value) {
3167
3131
  }
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)(
3190
+ const entries = (0, import_react11.useMemo)(
3227
3191
  () => (0, import_core6.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,