@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.mjs CHANGED
@@ -33,6 +33,91 @@ function useStudio() {
33
33
  return ctx;
34
34
  }
35
35
 
36
+ // src/get-visible-nodes.ts
37
+ function getVisibleNodes(root, isExpanded) {
38
+ const result = [];
39
+ function walk(node) {
40
+ result.push(node);
41
+ if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
42
+ for (const child of node.children) {
43
+ walk(child);
44
+ }
45
+ }
46
+ }
47
+ walk(root);
48
+ return result;
49
+ }
50
+
51
+ // src/selection-utils.ts
52
+ import { removeNode } from "@visual-json/core";
53
+ function computeSelectAllIds(tree, focusedNodeId, currentlySelected) {
54
+ if (!focusedNodeId) return null;
55
+ let node = tree.nodesById.get(focusedNodeId);
56
+ if (!node) return null;
57
+ while (node) {
58
+ const parent = node.parentId ? tree.nodesById.get(node.parentId) : void 0;
59
+ const siblings = parent ? parent.children : [tree.root];
60
+ const siblingIds = new Set(siblings.map((s) => s.id));
61
+ const allSelected = siblings.every((s) => currentlySelected.has(s.id));
62
+ if (!allSelected) {
63
+ return siblingIds;
64
+ }
65
+ if (!parent || !parent.parentId) return siblingIds;
66
+ node = parent;
67
+ }
68
+ return null;
69
+ }
70
+ function computeRangeIds(visibleNodes, anchorId, targetId) {
71
+ const anchorIdx = visibleNodes.findIndex((n) => n.id === anchorId);
72
+ const targetIdx = visibleNodes.findIndex((n) => n.id === targetId);
73
+ if (anchorIdx === -1 || targetIdx === -1) return null;
74
+ const start = Math.min(anchorIdx, targetIdx);
75
+ const end = Math.max(anchorIdx, targetIdx);
76
+ const ids = /* @__PURE__ */ new Set();
77
+ for (let i = start; i <= end; i++) {
78
+ ids.add(visibleNodes[i].id);
79
+ }
80
+ return ids;
81
+ }
82
+ function deleteSelectedNodes(tree, selectedIds, visibleNodes) {
83
+ const idsToDelete = [...selectedIds].filter((id) => {
84
+ const node = tree.nodesById.get(id);
85
+ if (!node || node.parentId === null) return false;
86
+ let cur = tree.nodesById.get(node.parentId);
87
+ while (cur) {
88
+ if (selectedIds.has(cur.id)) return false;
89
+ cur = cur.parentId ? tree.nodesById.get(cur.parentId) : void 0;
90
+ }
91
+ return true;
92
+ });
93
+ if (idsToDelete.length === 0) return { newTree: tree, nextFocusId: null };
94
+ const firstDeletedIdx = visibleNodes.findIndex((n) => selectedIds.has(n.id));
95
+ let newTree = tree;
96
+ for (const id of idsToDelete) {
97
+ if (newTree.nodesById.has(id)) {
98
+ newTree = removeNode(newTree, id);
99
+ }
100
+ }
101
+ let nextFocusId = null;
102
+ for (let i = firstDeletedIdx; i < visibleNodes.length; i++) {
103
+ const id = visibleNodes[i].id;
104
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
105
+ nextFocusId = id;
106
+ break;
107
+ }
108
+ }
109
+ if (!nextFocusId) {
110
+ for (let i = firstDeletedIdx - 1; i >= 0; i--) {
111
+ const id = visibleNodes[i].id;
112
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
113
+ nextFocusId = id;
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ return { newTree, nextFocusId };
119
+ }
120
+
36
121
  // src/visual-json.tsx
37
122
  import { jsx } from "react/jsx-runtime";
38
123
  function collectAllIds(node) {
@@ -49,10 +134,42 @@ function VisualJson({
49
134
  children
50
135
  }) {
51
136
  const [tree, setTreeState] = useState(() => fromJson(value));
52
- const [selectedNodeId, setSelectedNodeId] = useState(null);
137
+ const [focusedNodeId, setFocusedNodeId] = useState(null);
138
+ const [selectedNodeIds, setSelectedNodeIdsState] = useState(
139
+ () => /* @__PURE__ */ new Set()
140
+ );
141
+ const selectedNodeIdsRef = useRef(/* @__PURE__ */ new Set());
142
+ const setSelectedNodeIds = useCallback((ids) => {
143
+ selectedNodeIdsRef.current = ids;
144
+ setSelectedNodeIdsState(ids);
145
+ }, []);
146
+ const anchorNodeIdRef = useRef(null);
147
+ const [anchorNodeId, setAnchorNodeIdState] = useState(null);
148
+ const [drillDownNodeId, setDrillDownNodeId] = useState(null);
53
149
  const [expandedNodeIds, setExpandedNodeIds] = useState(
54
150
  () => /* @__PURE__ */ new Set([tree.root.id])
55
151
  );
152
+ const setAnchorNodeId = useCallback((id) => {
153
+ anchorNodeIdRef.current = id;
154
+ setAnchorNodeIdState(id);
155
+ }, []);
156
+ const focusSelectAndDrillDown = useCallback(
157
+ (nodeId) => {
158
+ setFocusedNodeId(nodeId);
159
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
160
+ setAnchorNodeId(nodeId);
161
+ setDrillDownNodeId(nodeId);
162
+ },
163
+ [setSelectedNodeIds, setAnchorNodeId]
164
+ );
165
+ const visibleNodes = useMemo(
166
+ () => getVisibleNodes(tree.root, (id) => expandedNodeIds.has(id)),
167
+ [tree.root, expandedNodeIds]
168
+ );
169
+ const visibleNodesOverrideRef = useRef(null);
170
+ const setVisibleNodesOverride = useCallback((nodes) => {
171
+ visibleNodesOverrideRef.current = nodes;
172
+ }, []);
56
173
  const historyRef = useRef(new History());
57
174
  const isInternalChange = useRef(false);
58
175
  const hasMounted = useRef(false);
@@ -81,7 +198,7 @@ function VisualJson({
81
198
  const newTree = fromJson(value);
82
199
  setTreeState(newTree);
83
200
  setExpandedNodeIds(/* @__PURE__ */ new Set([newTree.root.id]));
84
- setSelectedNodeId(null);
201
+ focusSelectAndDrillDown(null);
85
202
  historyRef.current = new History();
86
203
  historyRef.current.push(newTree);
87
204
  setCanUndo(false);
@@ -139,8 +256,66 @@ function VisualJson({
139
256
  document.addEventListener("keydown", handleKeyDown);
140
257
  return () => document.removeEventListener("keydown", handleKeyDown);
141
258
  }, [undo, redo]);
142
- const selectNode = useCallback((nodeId) => {
143
- setSelectedNodeId(nodeId);
259
+ const selectNode = useCallback(
260
+ (nodeId) => {
261
+ setFocusedNodeId(nodeId);
262
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
263
+ setAnchorNodeId(nodeId);
264
+ },
265
+ [setSelectedNodeIds, setAnchorNodeId]
266
+ );
267
+ const selectAndDrillDown = focusSelectAndDrillDown;
268
+ const toggleNodeSelection = useCallback(
269
+ (nodeId) => {
270
+ const next = new Set(selectedNodeIdsRef.current);
271
+ if (next.has(nodeId)) {
272
+ next.delete(nodeId);
273
+ } else {
274
+ next.add(nodeId);
275
+ }
276
+ setSelectedNodeIds(next);
277
+ if (next.size === 0) {
278
+ setFocusedNodeId(null);
279
+ setAnchorNodeId(null);
280
+ } else {
281
+ setFocusedNodeId(nodeId);
282
+ setAnchorNodeId(nodeId);
283
+ }
284
+ },
285
+ [setSelectedNodeIds, setAnchorNodeId]
286
+ );
287
+ const selectNodeRange = useCallback(
288
+ (toNodeId) => {
289
+ const nodes = visibleNodesOverrideRef.current ?? visibleNodes;
290
+ const anchor = anchorNodeIdRef.current;
291
+ if (!anchor) {
292
+ setFocusedNodeId(toNodeId);
293
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
294
+ setAnchorNodeId(toNodeId);
295
+ return;
296
+ }
297
+ const rangeIds = computeRangeIds(nodes, anchor, toNodeId);
298
+ if (!rangeIds) {
299
+ setFocusedNodeId(toNodeId);
300
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
301
+ setAnchorNodeId(toNodeId);
302
+ return;
303
+ }
304
+ setSelectedNodeIds(rangeIds);
305
+ setFocusedNodeId(toNodeId);
306
+ },
307
+ [visibleNodes, setSelectedNodeIds, setAnchorNodeId]
308
+ );
309
+ const setSelection = useCallback(
310
+ (focusedId, newSelectedIds, newAnchorId) => {
311
+ setFocusedNodeId(focusedId);
312
+ setSelectedNodeIds(newSelectedIds);
313
+ setAnchorNodeId(newAnchorId);
314
+ },
315
+ [setSelectedNodeIds, setAnchorNodeId]
316
+ );
317
+ const drillDown = useCallback((nodeId) => {
318
+ setDrillDownNodeId(nodeId);
144
319
  }, []);
145
320
  const toggleExpand = useCallback((nodeId) => {
146
321
  setExpandedNodeIds((prev) => {
@@ -186,6 +361,7 @@ function VisualJson({
186
361
  const matchIds = new Set(matches.map((m) => m.nodeId));
187
362
  setSearchMatchNodeIds(matchIds);
188
363
  if (matches.length > 0) {
364
+ const firstId = matches[0].nodeId;
189
365
  const ancestors = getAncestorIds(
190
366
  tree,
191
367
  matches.map((m) => m.nodeId)
@@ -195,7 +371,7 @@ function VisualJson({
195
371
  for (const id of ancestors) next.add(id);
196
372
  return next;
197
373
  });
198
- setSelectedNodeId(matches[0].nodeId);
374
+ focusSelectAndDrillDown(firstId);
199
375
  }
200
376
  },
201
377
  [tree]
@@ -204,14 +380,14 @@ function VisualJson({
204
380
  if (searchMatches.length === 0) return;
205
381
  const nextIdx = (searchMatchIndex + 1) % searchMatches.length;
206
382
  setSearchMatchIndex(nextIdx);
207
- setSelectedNodeId(searchMatches[nextIdx].nodeId);
208
- }, [searchMatches, searchMatchIndex]);
383
+ focusSelectAndDrillDown(searchMatches[nextIdx].nodeId);
384
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
209
385
  const prevSearchMatch = useCallback(() => {
210
386
  if (searchMatches.length === 0) return;
211
387
  const prevIdx = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length;
212
388
  setSearchMatchIndex(prevIdx);
213
- setSelectedNodeId(searchMatches[prevIdx].nodeId);
214
- }, [searchMatches, searchMatchIndex]);
389
+ focusSelectAndDrillDown(searchMatches[prevIdx].nodeId);
390
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
215
391
  useEffect(() => {
216
392
  if (!searchQuery.trim()) return;
217
393
  const matches = searchNodes(tree, searchQuery);
@@ -224,7 +400,10 @@ function VisualJson({
224
400
  const state = useMemo(
225
401
  () => ({
226
402
  tree,
227
- selectedNodeId,
403
+ focusedNodeId,
404
+ selectedNodeIds,
405
+ anchorNodeId,
406
+ drillDownNodeId,
228
407
  expandedNodeIds,
229
408
  schema: schema ?? null,
230
409
  searchQuery,
@@ -234,7 +413,10 @@ function VisualJson({
234
413
  }),
235
414
  [
236
415
  tree,
237
- selectedNodeId,
416
+ focusedNodeId,
417
+ selectedNodeIds,
418
+ anchorNodeId,
419
+ drillDownNodeId,
238
420
  expandedNodeIds,
239
421
  schema,
240
422
  searchQuery,
@@ -247,6 +429,12 @@ function VisualJson({
247
429
  () => ({
248
430
  setTree,
249
431
  selectNode,
432
+ selectAndDrillDown,
433
+ toggleNodeSelection,
434
+ selectNodeRange,
435
+ setSelection,
436
+ setVisibleNodesOverride,
437
+ drillDown,
250
438
  toggleExpand,
251
439
  expandNode,
252
440
  collapseNode,
@@ -263,6 +451,12 @@ function VisualJson({
263
451
  [
264
452
  setTree,
265
453
  selectNode,
454
+ selectAndDrillDown,
455
+ toggleNodeSelection,
456
+ selectNodeRange,
457
+ setSelection,
458
+ setVisibleNodesOverride,
459
+ drillDown,
266
460
  toggleExpand,
267
461
  expandNode,
268
462
  collapseNode,
@@ -282,11 +476,9 @@ function VisualJson({
282
476
  }
283
477
 
284
478
  // src/tree-view.tsx
285
- import { useState as useState4, useMemo as useMemo2, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect3 } from "react";
479
+ import { useState as useState4, useMemo as useMemo3, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect3 } from "react";
286
480
  import {
287
- removeNode,
288
- getPropertySchema,
289
- validateNode,
481
+ removeNode as removeNode3,
290
482
  duplicateNode,
291
483
  changeType,
292
484
  toJson as toJson2
@@ -404,96 +596,199 @@ function getDisplayKey(node, state) {
404
596
  return node.key;
405
597
  }
406
598
 
407
- // src/get-visible-nodes.ts
408
- function getVisibleNodes(root, isExpanded) {
409
- const result = [];
410
- function walk(node) {
411
- result.push(node);
412
- if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
413
- for (const child of node.children) {
414
- walk(child);
415
- }
416
- }
417
- }
418
- walk(root);
419
- return result;
420
- }
599
+ // src/use-drag-drop.ts
600
+ import { useState as useState3, useCallback as useCallback2, useRef as useRef3, useMemo as useMemo2 } from "react";
601
+ import {
602
+ removeNode as removeNode2,
603
+ insertNode,
604
+ reorderChildrenMulti,
605
+ isDescendant
606
+ } from "@visual-json/core";
607
+
608
+ // src/theme.ts
609
+ var DEFAULT_CSS_VARS = {
610
+ "--vj-bg": "#1e1e1e",
611
+ "--vj-bg-panel": "#252526",
612
+ "--vj-bg-hover": "#2a2d2e",
613
+ "--vj-bg-selected": "#2a5a1e",
614
+ "--vj-bg-selected-muted": "#2a2d2e",
615
+ "--vj-bg-match": "#3a3520",
616
+ "--vj-bg-match-active": "#51502b",
617
+ "--vj-border": "#333333",
618
+ "--vj-border-subtle": "#2a2a2a",
619
+ "--vj-text": "#cccccc",
620
+ "--vj-text-muted": "#888888",
621
+ "--vj-text-dim": "#666666",
622
+ "--vj-text-dimmer": "#555555",
623
+ "--vj-string": "#ce9178",
624
+ "--vj-number": "#b5cea8",
625
+ "--vj-boolean": "#569cd6",
626
+ "--vj-accent": "#007acc",
627
+ "--vj-accent-muted": "#094771",
628
+ "--vj-input-bg": "#3c3c3c",
629
+ "--vj-input-border": "#555555",
630
+ "--vj-error": "#f48771",
631
+ "--vj-font": "monospace",
632
+ "--vj-input-font-size": "13px"
633
+ };
421
634
 
422
635
  // src/use-drag-drop.ts
423
- import { useState as useState3, useCallback as useCallback2, useRef as useRef3 } from "react";
424
- import { reorderChildren, moveNode } from "@visual-json/core";
636
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
425
637
  var INITIAL_DRAG_STATE = {
426
- draggedNodeId: null,
638
+ draggedNodeIds: EMPTY_SET,
427
639
  dropTargetNodeId: null,
428
640
  dropPosition: null
429
641
  };
430
- function useDragDrop() {
642
+ function sortByTreeOrder(root, ids) {
643
+ const result = [];
644
+ function walk(node) {
645
+ if (ids.has(node.id)) result.push(node.id);
646
+ for (const child of node.children) walk(child);
647
+ }
648
+ walk(root);
649
+ return result;
650
+ }
651
+ function setMultiDragImage(e, count) {
652
+ const ghost = document.createElement("div");
653
+ ghost.textContent = `${count} selected`;
654
+ const root = document.querySelector("[data-form-container], [role='tree']");
655
+ const cs = root ? getComputedStyle(root) : null;
656
+ const bg = cs?.getPropertyValue("--vj-bg-selected").trim() || DEFAULT_CSS_VARS["--vj-bg-selected"];
657
+ const fg = cs?.getPropertyValue("--vj-text-selected").trim() || cs?.getPropertyValue("--vj-text").trim() || DEFAULT_CSS_VARS["--vj-text"];
658
+ const font = cs?.getPropertyValue("--vj-font").trim() || DEFAULT_CSS_VARS["--vj-font"];
659
+ ghost.style.cssText = [
660
+ "position:fixed",
661
+ "top:-1000px",
662
+ "left:-1000px",
663
+ "padding:4px 12px",
664
+ `background:${bg}`,
665
+ `color:${fg}`,
666
+ `font-family:${font}`,
667
+ "font-size:13px",
668
+ "border-radius:4px",
669
+ "white-space:nowrap",
670
+ "pointer-events:none"
671
+ ].join(";");
672
+ document.body.appendChild(ghost);
673
+ e.dataTransfer.setDragImage(ghost, 0, 14);
674
+ requestAnimationFrame(() => ghost.remove());
675
+ }
676
+ function useDragDrop(visibleNodes, selectedNodeIds) {
431
677
  const { state, actions } = useStudio();
432
678
  const [dragState, setDragState] = useState3(INITIAL_DRAG_STATE);
433
679
  const dragStateRef = useRef3(dragState);
434
680
  dragStateRef.current = dragState;
435
- const handleDragStart = useCallback2((nodeId) => {
436
- setDragState({
437
- draggedNodeId: nodeId,
438
- dropTargetNodeId: null,
439
- dropPosition: null
440
- });
441
- }, []);
442
- const handleDragOver = useCallback2(
681
+ const visibleNodeIndexMap = useMemo2(() => {
682
+ const map = /* @__PURE__ */ new Map();
683
+ visibleNodes.forEach((n, i) => map.set(n.id, i));
684
+ return map;
685
+ }, [visibleNodes]);
686
+ const handleDragStart = useCallback2(
687
+ (nodeId) => {
688
+ let ids;
689
+ if (selectedNodeIds.size > 0 && selectedNodeIds.has(nodeId)) {
690
+ ids = selectedNodeIds;
691
+ } else {
692
+ ids = /* @__PURE__ */ new Set([nodeId]);
693
+ }
694
+ setDragState({
695
+ draggedNodeIds: ids,
696
+ dropTargetNodeId: null,
697
+ dropPosition: null
698
+ });
699
+ },
700
+ [selectedNodeIds]
701
+ );
702
+ const rawDragOver = useCallback2(
443
703
  (nodeId, position) => {
704
+ const draggedIds = dragStateRef.current.draggedNodeIds;
705
+ for (const draggedId of draggedIds) {
706
+ if (nodeId === draggedId || isDescendant(state.tree, nodeId, draggedId)) {
707
+ return;
708
+ }
709
+ }
444
710
  setDragState((prev) => ({
445
711
  ...prev,
446
712
  dropTargetNodeId: nodeId,
447
713
  dropPosition: position
448
714
  }));
449
715
  },
450
- []
716
+ [state.tree]
717
+ );
718
+ const handleDragOver = useCallback2(
719
+ (nodeId, position) => {
720
+ if (position === "before") {
721
+ const idx = visibleNodeIndexMap.get(nodeId);
722
+ if (idx !== void 0 && idx > 0) {
723
+ rawDragOver(visibleNodes[idx - 1].id, "after");
724
+ return;
725
+ }
726
+ }
727
+ rawDragOver(nodeId, position);
728
+ },
729
+ [visibleNodes, visibleNodeIndexMap, rawDragOver]
451
730
  );
452
731
  const handleDragEnd = useCallback2(() => {
453
732
  setDragState(INITIAL_DRAG_STATE);
454
733
  }, []);
455
734
  const handleDrop = useCallback2(() => {
456
- const { draggedNodeId, dropTargetNodeId, dropPosition } = dragStateRef.current;
457
- if (!draggedNodeId || !dropTargetNodeId || !dropPosition) return;
458
- const draggedNode = state.tree.nodesById.get(draggedNodeId);
735
+ const { draggedNodeIds, dropTargetNodeId, dropPosition } = dragStateRef.current;
736
+ if (draggedNodeIds.size === 0 || !dropTargetNodeId || !dropPosition) return;
459
737
  const targetNode = state.tree.nodesById.get(dropTargetNodeId);
460
- if (!draggedNode || !targetNode) return;
461
- if (draggedNode.parentId && draggedNode.parentId === targetNode.parentId) {
462
- const parent = state.tree.nodesById.get(draggedNode.parentId);
463
- if (parent) {
464
- const fromIndex = parent.children.findIndex(
465
- (c) => c.id === draggedNodeId
466
- );
467
- let toIndex = parent.children.findIndex(
468
- (c) => c.id === dropTargetNodeId
469
- );
470
- if (dropPosition === "after") toIndex++;
471
- if (fromIndex < toIndex) toIndex--;
472
- if (fromIndex !== toIndex && fromIndex >= 0 && toIndex >= 0) {
473
- const newTree = reorderChildren(
474
- state.tree,
475
- parent.id,
476
- fromIndex,
477
- toIndex
478
- );
479
- actions.setTree(newTree);
738
+ if (!targetNode || !targetNode.parentId) return;
739
+ for (const id of draggedNodeIds) {
740
+ if (isDescendant(state.tree, dropTargetNodeId, id)) return;
741
+ }
742
+ const targetParentId = targetNode.parentId;
743
+ const targetParent = state.tree.nodesById.get(targetParentId);
744
+ if (!targetParent) return;
745
+ const parentChildren = targetParent.children;
746
+ const orderedDragIds = parentChildren.filter((c) => draggedNodeIds.has(c.id)).map((c) => c.id);
747
+ const allSameParent = orderedDragIds.length === draggedNodeIds.size && [...draggedNodeIds].every((id) => {
748
+ const n = state.tree.nodesById.get(id);
749
+ return n?.parentId === targetParentId;
750
+ });
751
+ if (allSameParent) {
752
+ const newTree = reorderChildrenMulti(
753
+ state.tree,
754
+ targetParentId,
755
+ orderedDragIds,
756
+ dropTargetNodeId,
757
+ dropPosition
758
+ );
759
+ actions.setTree(newTree);
760
+ } else {
761
+ const orderedIds = sortByTreeOrder(state.tree.root, draggedNodeIds);
762
+ const draggedNodes = orderedIds.map((id) => state.tree.nodesById.get(id)).filter((n) => !!n && n.parentId !== null).map((n) => structuredClone(n));
763
+ let newTree = state.tree;
764
+ for (const id of [...orderedIds].reverse()) {
765
+ if (newTree.nodesById.has(id)) {
766
+ newTree = removeNode2(newTree, id);
480
767
  }
481
768
  }
482
- } else if (targetNode.parentId) {
483
- const newParent = state.tree.nodesById.get(targetNode.parentId);
484
- if (newParent) {
485
- let toIndex = newParent.children.findIndex(
486
- (c) => c.id === dropTargetNodeId
487
- );
488
- if (dropPosition === "after") toIndex++;
489
- const newTree = moveNode(
490
- state.tree,
491
- draggedNodeId,
492
- newParent.id,
493
- toIndex
769
+ const updatedTarget = newTree.nodesById.get(dropTargetNodeId);
770
+ if (!updatedTarget || !updatedTarget.parentId) {
771
+ setDragState(INITIAL_DRAG_STATE);
772
+ return;
773
+ }
774
+ const updatedParent = newTree.nodesById.get(updatedTarget.parentId);
775
+ if (!updatedParent) {
776
+ setDragState(INITIAL_DRAG_STATE);
777
+ return;
778
+ }
779
+ let insertIdx = updatedParent.children.findIndex(
780
+ (c) => c.id === dropTargetNodeId
781
+ );
782
+ if (dropPosition === "after") insertIdx++;
783
+ for (let i = 0; i < draggedNodes.length; i++) {
784
+ newTree = insertNode(
785
+ newTree,
786
+ updatedParent.id,
787
+ draggedNodes[i],
788
+ insertIdx + i
494
789
  );
495
- actions.setTree(newTree);
496
790
  }
791
+ actions.setTree(newTree);
497
792
  }
498
793
  setDragState(INITIAL_DRAG_STATE);
499
794
  }, [state.tree, actions]);
@@ -515,6 +810,7 @@ function TreeNodeRow({
515
810
  showValues,
516
811
  showCounts,
517
812
  isFocused,
813
+ onSelectRange,
518
814
  onDragStart,
519
815
  onDragOver,
520
816
  onDragEnd,
@@ -522,19 +818,15 @@ function TreeNodeRow({
522
818
  onContextMenu
523
819
  }) {
524
820
  const { state, actions } = useStudio();
525
- const isSelected = state.selectedNodeId === node.id;
821
+ const isSelected = state.selectedNodeIds.has(node.id);
526
822
  const isExpanded = state.expandedNodeIds.has(node.id);
527
823
  const isContainer = node.type === "object" || node.type === "array";
528
824
  const [hovered, setHovered] = useState4(false);
529
825
  const isRoot = node.parentId === null;
530
826
  const isSearchMatch = state.searchMatchNodeIds.has(node.id);
531
827
  const isActiveMatch = state.searchMatches.length > 0 && state.searchMatches[state.searchMatchIndex]?.nodeId === node.id;
532
- const schema = state.schema;
533
- const nodeSchema = schema ? getPropertySchema(schema, node.path) : void 0;
534
- const validation = nodeSchema ? validateNode(node, nodeSchema) : null;
535
- const hasError = validation ? !validation.valid : false;
536
828
  const isDragTarget = dragState.dropTargetNodeId === node.id;
537
- const isDraggedNode = dragState.draggedNodeId === node.id;
829
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
538
830
  function displayValue() {
539
831
  if (isContainer) {
540
832
  return node.type === "array" ? `[${node.children.length}]` : `{${node.children.length}}`;
@@ -550,12 +842,12 @@ function TreeNodeRow({
550
842
  const position = e.clientY < midY ? "before" : "after";
551
843
  onDragOver(node.id, position);
552
844
  }
553
- let borderTop = "none";
554
- let borderBottom = "none";
845
+ let borderTopColor = "transparent";
846
+ let borderBottomColor = "transparent";
555
847
  if (isDragTarget && dragState.dropPosition === "before") {
556
- borderTop = "2px solid var(--vj-accent, #007acc)";
848
+ borderTopColor = "var(--vj-accent, #007acc)";
557
849
  } else if (isDragTarget && dragState.dropPosition === "after") {
558
- borderBottom = "2px solid var(--vj-accent, #007acc)";
850
+ borderBottomColor = "var(--vj-accent, #007acc)";
559
851
  }
560
852
  return /* @__PURE__ */ jsxs(Fragment, { children: [
561
853
  /* @__PURE__ */ jsxs(
@@ -564,13 +856,24 @@ function TreeNodeRow({
564
856
  role: "treeitem",
565
857
  "aria-selected": isSelected,
566
858
  "aria-expanded": isContainer ? isExpanded : void 0,
567
- onClick: () => actions.selectNode(node.id),
859
+ onClick: (e) => {
860
+ if (e.shiftKey) {
861
+ onSelectRange(node.id);
862
+ } else if (e.metaKey || e.ctrlKey) {
863
+ actions.toggleNodeSelection(node.id);
864
+ } else {
865
+ actions.selectAndDrillDown(node.id);
866
+ }
867
+ },
568
868
  onMouseEnter: () => setHovered(true),
569
869
  onMouseLeave: () => setHovered(false),
570
870
  onContextMenu: (e) => onContextMenu(e, node),
571
871
  draggable: !isRoot,
572
872
  onDragStart: (e) => {
573
873
  e.dataTransfer.effectAllowed = "move";
874
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
875
+ setMultiDragImage(e, state.selectedNodeIds.size);
876
+ }
574
877
  onDragStart(node.id);
575
878
  },
576
879
  onDragOver: handleDragOverEvent,
@@ -584,15 +887,16 @@ function TreeNodeRow({
584
887
  display: "flex",
585
888
  alignItems: "center",
586
889
  gap: 6,
587
- padding: "3px 8px",
890
+ padding: "1px 8px",
588
891
  paddingLeft: 8 + depth * 16,
589
892
  cursor: "pointer",
590
893
  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",
591
894
  minHeight: 28,
592
895
  userSelect: "none",
593
896
  opacity: isDraggedNode ? 0.4 : 1,
594
- borderTop,
595
- borderBottom,
897
+ borderTop: `2px solid ${borderTopColor}`,
898
+ borderBottom: `2px solid ${borderBottomColor}`,
899
+ boxSizing: "border-box",
596
900
  color: isSelected && isFocused ? "var(--vj-text-selected, var(--vj-text, #cccccc))" : "var(--vj-text, #cccccc)"
597
901
  },
598
902
  children: [
@@ -675,6 +979,7 @@ function TreeNodeRow({
675
979
  showValues,
676
980
  showCounts,
677
981
  isFocused,
982
+ onSelectRange,
678
983
  onDragStart,
679
984
  onDragOver,
680
985
  onDragEnd,
@@ -692,25 +997,34 @@ function TreeView({
692
997
  }) {
693
998
  const { state, actions } = useStudio();
694
999
  const containerRef = useRef4(null);
1000
+ const visibleNodes = useMemo3(
1001
+ () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
1002
+ [state.tree.root, state.expandedNodeIds]
1003
+ );
695
1004
  const {
696
1005
  dragState,
697
1006
  handleDragStart,
698
1007
  handleDragOver,
699
1008
  handleDragEnd,
700
1009
  handleDrop
701
- } = useDragDrop();
702
- const [contextMenu, setContextMenu] = useState4(null);
703
- const visibleNodes = useMemo2(
704
- () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
705
- [state.tree.root, state.expandedNodeIds]
1010
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
1011
+ const handleSelectRange = useCallback3(
1012
+ (nodeId) => {
1013
+ actions.setVisibleNodesOverride(visibleNodes);
1014
+ actions.selectNodeRange(nodeId);
1015
+ },
1016
+ [visibleNodes, actions]
706
1017
  );
1018
+ const [contextMenu, setContextMenu] = useState4(null);
707
1019
  const handleContextMenu = useCallback3(
708
1020
  (e, node) => {
709
1021
  e.preventDefault();
710
- actions.selectNode(node.id);
1022
+ if (!state.selectedNodeIds.has(node.id)) {
1023
+ actions.selectAndDrillDown(node.id);
1024
+ }
711
1025
  setContextMenu({ x: e.clientX, y: e.clientY, node });
712
1026
  },
713
- [actions]
1027
+ [actions, state.selectedNodeIds]
714
1028
  );
715
1029
  const buildContextMenuItems = useCallback3(
716
1030
  (node) => {
@@ -789,7 +1103,7 @@ function TreeView({
789
1103
  items.push({
790
1104
  label: "Delete",
791
1105
  action: () => {
792
- const newTree = removeNode(state.tree, node.id);
1106
+ const newTree = removeNode3(state.tree, node.id);
793
1107
  actions.setTree(newTree);
794
1108
  }
795
1109
  });
@@ -801,19 +1115,31 @@ function TreeView({
801
1115
  const handleKeyDown = useCallback3(
802
1116
  (e) => {
803
1117
  const currentIndex = visibleNodes.findIndex(
804
- (n) => n.id === state.selectedNodeId
1118
+ (n) => n.id === state.focusedNodeId
805
1119
  );
806
1120
  switch (e.key) {
807
1121
  case "ArrowDown": {
808
1122
  e.preventDefault();
809
1123
  const next = visibleNodes[currentIndex + 1];
810
- if (next) actions.selectNode(next.id);
1124
+ if (next) {
1125
+ if (e.shiftKey) {
1126
+ handleSelectRange(next.id);
1127
+ } else {
1128
+ actions.selectNode(next.id);
1129
+ }
1130
+ }
811
1131
  break;
812
1132
  }
813
1133
  case "ArrowUp": {
814
1134
  e.preventDefault();
815
1135
  const prev = visibleNodes[currentIndex - 1];
816
- if (prev) actions.selectNode(prev.id);
1136
+ if (prev) {
1137
+ if (e.shiftKey) {
1138
+ handleSelectRange(prev.id);
1139
+ } else {
1140
+ actions.selectNode(prev.id);
1141
+ }
1142
+ }
817
1143
  break;
818
1144
  }
819
1145
  case "ArrowRight": {
@@ -840,17 +1166,47 @@ function TreeView({
840
1166
  }
841
1167
  break;
842
1168
  }
1169
+ case "a": {
1170
+ if (e.metaKey || e.ctrlKey) {
1171
+ e.preventDefault();
1172
+ const ids = computeSelectAllIds(
1173
+ state.tree,
1174
+ state.focusedNodeId,
1175
+ state.selectedNodeIds
1176
+ );
1177
+ if (ids) {
1178
+ actions.setSelection(
1179
+ state.focusedNodeId,
1180
+ ids,
1181
+ state.focusedNodeId
1182
+ );
1183
+ }
1184
+ }
1185
+ break;
1186
+ }
1187
+ case "Escape": {
1188
+ e.preventDefault();
1189
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
1190
+ actions.selectNode(state.focusedNodeId);
1191
+ } else {
1192
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
1193
+ }
1194
+ break;
1195
+ }
843
1196
  case "Delete":
844
1197
  case "Backspace": {
845
1198
  e.preventDefault();
846
- const toDelete = currentIndex >= 0 ? visibleNodes[currentIndex] : null;
847
- if (toDelete && toDelete.parentId) {
848
- const nextSelect = visibleNodes[currentIndex + 1] ?? visibleNodes[currentIndex - 1];
849
- const newTree = removeNode(state.tree, toDelete.id);
850
- actions.setTree(newTree);
851
- if (nextSelect && nextSelect.id !== toDelete.id) {
852
- actions.selectNode(nextSelect.id);
853
- }
1199
+ const { newTree, nextFocusId } = deleteSelectedNodes(
1200
+ state.tree,
1201
+ state.selectedNodeIds,
1202
+ visibleNodes
1203
+ );
1204
+ if (newTree === state.tree) break;
1205
+ actions.setTree(newTree);
1206
+ if (nextFocusId) {
1207
+ actions.selectNode(nextFocusId);
1208
+ } else {
1209
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
854
1210
  }
855
1211
  break;
856
1212
  }
@@ -858,23 +1214,25 @@ function TreeView({
858
1214
  },
859
1215
  [
860
1216
  visibleNodes,
861
- state.selectedNodeId,
1217
+ state.focusedNodeId,
1218
+ state.selectedNodeIds,
862
1219
  state.expandedNodeIds,
863
1220
  state.tree,
864
- actions
1221
+ actions,
1222
+ handleSelectRange
865
1223
  ]
866
1224
  );
867
1225
  const [isFocused, setIsFocused] = useState4(false);
868
1226
  useEffect3(() => {
869
- if (state.selectedNodeId && containerRef.current) {
1227
+ if (state.focusedNodeId && containerRef.current) {
870
1228
  const el = containerRef.current.querySelector(
871
- `[data-node-id="${state.selectedNodeId}"]`
1229
+ `[data-node-id="${state.focusedNodeId}"]`
872
1230
  );
873
1231
  if (el) {
874
1232
  el.scrollIntoView({ block: "nearest" });
875
1233
  }
876
1234
  }
877
- }, [state.selectedNodeId]);
1235
+ }, [state.focusedNodeId]);
878
1236
  return /* @__PURE__ */ jsxs(Fragment, { children: [
879
1237
  /* @__PURE__ */ jsx3(
880
1238
  "div",
@@ -904,6 +1262,7 @@ function TreeView({
904
1262
  showValues,
905
1263
  showCounts,
906
1264
  isFocused,
1265
+ onSelectRange: handleSelectRange,
907
1266
  onDragStart: handleDragStart,
908
1267
  onDragOver: handleDragOver,
909
1268
  onDragEnd: handleDragEnd,
@@ -926,25 +1285,25 @@ function TreeView({
926
1285
  }
927
1286
 
928
1287
  // src/form-view.tsx
929
- import { useState as useState7, useCallback as useCallback6, useRef as useRef7, useEffect as useEffect6, useMemo as useMemo5 } from "react";
1288
+ import { useState as useState7, useCallback as useCallback6, useRef as useRef7, useEffect as useEffect6, useMemo as useMemo6 } from "react";
930
1289
  import {
931
1290
  setValue,
932
1291
  setKey,
933
1292
  addProperty,
934
- removeNode as removeNode2,
935
- getPropertySchema as getPropertySchema2,
1293
+ removeNode as removeNode4,
1294
+ getPropertySchema,
936
1295
  resolveRef
937
1296
  } from "@visual-json/core";
938
1297
 
939
1298
  // src/breadcrumbs.tsx
940
- import { useState as useState5, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3 } from "react";
1299
+ import { useState as useState5, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo4 } from "react";
941
1300
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
942
1301
  var MAX_SUGGESTIONS = 20;
943
1302
  var DROPDOWN_MAX_HEIGHT = 200;
944
1303
  function Breadcrumbs({ className }) {
945
1304
  const { state, actions } = useStudio();
946
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
947
- const currentPath = selectedNode?.path ?? "/";
1305
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
1306
+ const currentPath = drillDownNode?.path ?? "/";
948
1307
  const [inputValue, setInputValue] = useState5(currentPath);
949
1308
  const [open, setOpen] = useState5(false);
950
1309
  const [highlightIndex, setHighlightIndex] = useState5(0);
@@ -954,7 +1313,7 @@ function Breadcrumbs({ className }) {
954
1313
  useEffect4(() => {
955
1314
  setInputValue(currentPath);
956
1315
  }, [currentPath]);
957
- const suggestions = useMemo3(() => {
1316
+ const suggestions = useMemo4(() => {
958
1317
  if (!open) return [];
959
1318
  const query = inputValue.toLowerCase();
960
1319
  const matches = [];
@@ -974,7 +1333,7 @@ function Breadcrumbs({ className }) {
974
1333
  (path) => {
975
1334
  for (const [id, node] of state.tree.nodesById) {
976
1335
  if (node.path === path) {
977
- actions.selectNode(id);
1336
+ actions.selectAndDrillDown(id);
978
1337
  break;
979
1338
  }
980
1339
  }
@@ -1062,7 +1421,7 @@ function Breadcrumbs({ className }) {
1062
1421
  style: {
1063
1422
  width: "100%",
1064
1423
  boxSizing: "border-box",
1065
- padding: "2px 0",
1424
+ padding: "3px 0",
1066
1425
  fontSize: "var(--vj-input-font-size, 13px)",
1067
1426
  fontFamily: "var(--vj-font, monospace)",
1068
1427
  color: "var(--vj-text-muted, #999999)",
@@ -1124,7 +1483,7 @@ import {
1124
1483
  useRef as useRef6,
1125
1484
  useEffect as useEffect5,
1126
1485
  useCallback as useCallback5,
1127
- useMemo as useMemo4
1486
+ useMemo as useMemo5
1128
1487
  } from "react";
1129
1488
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1130
1489
  var DROPDOWN_MAX_HEIGHT2 = 200;
@@ -1143,7 +1502,7 @@ function EnumInput({
1143
1502
  useEffect5(() => {
1144
1503
  setInputValue(value);
1145
1504
  }, [value]);
1146
- const suggestions = useMemo4(
1505
+ const suggestions = useMemo5(
1147
1506
  () => enumValues.map((v) => String(v)),
1148
1507
  [enumValues]
1149
1508
  );
@@ -1295,7 +1654,7 @@ function EnumInput({
1295
1654
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1296
1655
  function getResolvedSchema(schema, rootSchema, path) {
1297
1656
  if (!schema) return void 0;
1298
- const raw = getPropertySchema2(schema, path, rootSchema);
1657
+ const raw = getPropertySchema(schema, path, rootSchema);
1299
1658
  if (!raw) return void 0;
1300
1659
  return resolveRef(raw, rootSchema ?? schema);
1301
1660
  }
@@ -1318,7 +1677,6 @@ function FormField({
1318
1677
  depth,
1319
1678
  showDescriptions,
1320
1679
  showCounts,
1321
- formSelectedNodeId,
1322
1680
  editingNodeId,
1323
1681
  collapsedIds,
1324
1682
  maxKeyLength,
@@ -1336,26 +1694,26 @@ function FormField({
1336
1694
  const { state, actions } = useStudio();
1337
1695
  const isContainer = node.type === "object" || node.type === "array";
1338
1696
  const collapsed = collapsedIds.has(node.id);
1339
- const isSelected = formSelectedNodeId === node.id;
1697
+ const isSelected = state.selectedNodeIds.has(node.id);
1340
1698
  const isEditing = editingNodeId === node.id;
1341
1699
  const propSchema = getResolvedSchema(schema, rootSchema, node.path);
1342
1700
  const isRequired = checkRequired(node, schema, rootSchema);
1343
1701
  const [hovered, setHovered] = useState7(false);
1344
1702
  const isRoot = node.parentId === null;
1345
1703
  const isDragTarget = dragState.dropTargetNodeId === node.id;
1346
- const isDraggedNode = dragState.draggedNodeId === node.id;
1704
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
1347
1705
  function handleDragOverEvent(e) {
1348
1706
  e.preventDefault();
1349
1707
  const rect = e.currentTarget.getBoundingClientRect();
1350
1708
  const midY = rect.top + rect.height / 2;
1351
1709
  onDragOver(node.id, e.clientY < midY ? "before" : "after");
1352
1710
  }
1353
- let borderTop = "none";
1354
- let borderBottom = "none";
1711
+ let borderTopColor = "transparent";
1712
+ let borderBottomColor = "transparent";
1355
1713
  if (isDragTarget && dragState.dropPosition === "before") {
1356
- borderTop = "2px solid var(--vj-accent, #007acc)";
1714
+ borderTopColor = "var(--vj-accent, #007acc)";
1357
1715
  } else if (isDragTarget && dragState.dropPosition === "after") {
1358
- borderBottom = "2px solid var(--vj-accent, #007acc)";
1716
+ borderBottomColor = "var(--vj-accent, #007acc)";
1359
1717
  }
1360
1718
  const valueRef = useRef7(null);
1361
1719
  const keyRef = useRef7(null);
@@ -1398,7 +1756,7 @@ function FormField({
1398
1756
  [state.tree, node.id, actions]
1399
1757
  );
1400
1758
  const handleRemove = useCallback6(() => {
1401
- const newTree = removeNode2(state.tree, node.id);
1759
+ const newTree = removeNode4(state.tree, node.id);
1402
1760
  actions.setTree(newTree);
1403
1761
  }, [state.tree, node.id, actions]);
1404
1762
  const handleAddChild = useCallback6(() => {
@@ -1407,7 +1765,6 @@ function FormField({
1407
1765
  actions.setTree(newTree);
1408
1766
  }, [state.tree, node.id, node.type, node.children.length, actions]);
1409
1767
  const description = propSchema?.description;
1410
- const defaultVal = propSchema?.default;
1411
1768
  const isDeprecated = propSchema?.deprecated;
1412
1769
  const fieldTitle = propSchema?.title;
1413
1770
  const parentIsObject = node.parentId && state.tree.nodesById.get(node.parentId)?.type === "object";
@@ -1422,6 +1779,9 @@ function FormField({
1422
1779
  draggable: !isRoot,
1423
1780
  onDragStart: (e) => {
1424
1781
  e.dataTransfer.effectAllowed = "move";
1782
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1783
+ setMultiDragImage(e, state.selectedNodeIds.size);
1784
+ }
1425
1785
  onDragStart(node.id);
1426
1786
  },
1427
1787
  onDragOver: handleDragOverEvent,
@@ -1434,20 +1794,21 @@ function FormField({
1434
1794
  display: "flex",
1435
1795
  alignItems: "center",
1436
1796
  gap: 6,
1437
- padding: "3px 8px",
1797
+ padding: "1px 8px",
1438
1798
  paddingLeft: 8 + depth * 16,
1439
1799
  cursor: "pointer",
1440
1800
  backgroundColor: rowBg,
1441
1801
  color: rowColor,
1442
1802
  height: 28,
1803
+ boxSizing: "border-box",
1443
1804
  userSelect: "none",
1444
1805
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1445
- borderTop,
1446
- borderBottom
1806
+ borderTop: `2px solid ${borderTopColor}`,
1807
+ borderBottom: `2px solid ${borderBottomColor}`
1447
1808
  },
1448
1809
  onClick: (e) => {
1449
1810
  e.stopPropagation();
1450
- onSelect(node.id);
1811
+ onSelect(node.id, e);
1451
1812
  },
1452
1813
  onDoubleClick: () => onToggleCollapse(node.id),
1453
1814
  onMouseEnter: () => setHovered(true),
@@ -1503,7 +1864,7 @@ function FormField({
1503
1864
  {
1504
1865
  style: {
1505
1866
  color: !isRoot && !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1506
- fontSize: 13,
1867
+ fontSize: "var(--vj-input-font-size, 13px)",
1507
1868
  fontFamily: "var(--vj-font, monospace)",
1508
1869
  fontWeight: 500,
1509
1870
  flexShrink: 0,
@@ -1604,7 +1965,6 @@ function FormField({
1604
1965
  depth: depth + 1,
1605
1966
  showDescriptions,
1606
1967
  showCounts,
1607
- formSelectedNodeId,
1608
1968
  editingNodeId,
1609
1969
  collapsedIds,
1610
1970
  maxKeyLength,
@@ -1632,6 +1992,9 @@ function FormField({
1632
1992
  draggable: !isRoot,
1633
1993
  onDragStart: (e) => {
1634
1994
  e.dataTransfer.effectAllowed = "move";
1995
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1996
+ setMultiDragImage(e, state.selectedNodeIds.size);
1997
+ }
1635
1998
  onDragStart(node.id);
1636
1999
  },
1637
2000
  onDragOver: handleDragOverEvent,
@@ -1644,20 +2007,21 @@ function FormField({
1644
2007
  display: "flex",
1645
2008
  alignItems: "center",
1646
2009
  gap: 6,
1647
- padding: "3px 8px",
2010
+ padding: "1px 8px",
1648
2011
  paddingLeft: 8 + depth * 16,
1649
2012
  cursor: "pointer",
1650
2013
  backgroundColor: rowBg,
1651
2014
  color: rowColor,
1652
2015
  height: 28,
2016
+ boxSizing: "border-box",
1653
2017
  userSelect: "none",
1654
2018
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1655
- borderTop,
1656
- borderBottom
2019
+ borderTop: `2px solid ${borderTopColor}`,
2020
+ borderBottom: `2px solid ${borderBottomColor}`
1657
2021
  },
1658
2022
  onClick: (e) => {
1659
2023
  e.stopPropagation();
1660
- onSelect(node.id);
2024
+ onSelect(node.id, e);
1661
2025
  },
1662
2026
  onDoubleClick: () => onStartEditing(node.id),
1663
2027
  onMouseEnter: () => setHovered(true),
@@ -1694,7 +2058,7 @@ function FormField({
1694
2058
  {
1695
2059
  style: {
1696
2060
  color: !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1697
- fontSize: 13,
2061
+ fontSize: "var(--vj-input-font-size, 13px)",
1698
2062
  fontFamily: "var(--vj-font, monospace)",
1699
2063
  flexShrink: 0,
1700
2064
  display: "inline-block",
@@ -1738,7 +2102,7 @@ function FormField({
1738
2102
  {
1739
2103
  style: {
1740
2104
  color: valueColor,
1741
- fontSize: 13,
2105
+ fontSize: "var(--vj-input-font-size, 13px)",
1742
2106
  fontFamily: "var(--vj-font, monospace)",
1743
2107
  overflow: "hidden",
1744
2108
  textOverflow: "ellipsis",
@@ -1816,7 +2180,7 @@ function renderEditInput(node, propSchema, displayValue, handleValueChange, inpu
1816
2180
  style: {
1817
2181
  color: "var(--vj-boolean, #569cd6)",
1818
2182
  fontFamily: "var(--vj-font, monospace)",
1819
- fontSize: 13,
2183
+ fontSize: "var(--vj-input-font-size, 13px)",
1820
2184
  fontStyle: "italic",
1821
2185
  flex: 1
1822
2186
  },
@@ -1849,11 +2213,8 @@ function FormView({
1849
2213
  }) {
1850
2214
  const { state, actions } = useStudio();
1851
2215
  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] = useState7(
1855
- null
1856
- );
2216
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
2217
+ const displayNode = drillDownNode ?? state.tree.root;
1857
2218
  const [editingNodeId, setEditingNodeId] = useState7(null);
1858
2219
  const preEditTreeRef = useRef7(null);
1859
2220
  const [collapsedIds, setCollapsedIds] = useState7(
@@ -1861,23 +2222,26 @@ function FormView({
1861
2222
  );
1862
2223
  const containerRef = useRef7(null);
1863
2224
  const [isFocused, setIsFocused] = useState7(false);
1864
- const {
1865
- dragState,
1866
- handleDragStart,
1867
- handleDragOver,
1868
- handleDragEnd,
1869
- handleDrop
1870
- } = useDragDrop();
1871
2225
  useEffect6(() => {
1872
- setFormSelectedNodeId(null);
1873
2226
  setEditingNodeId(null);
1874
2227
  setCollapsedIds(/* @__PURE__ */ new Set());
1875
2228
  }, [displayNode.id]);
1876
- const visibleNodes = useMemo5(
2229
+ const visibleNodes = useMemo6(
1877
2230
  () => getVisibleNodes(displayNode, (id) => !collapsedIds.has(id)),
1878
2231
  [displayNode, collapsedIds]
1879
2232
  );
1880
- const { maxKeyLength, maxDepth } = useMemo5(() => {
2233
+ const {
2234
+ dragState,
2235
+ handleDragStart,
2236
+ handleDragOver,
2237
+ handleDragEnd,
2238
+ handleDrop
2239
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
2240
+ useEffect6(() => {
2241
+ actions.setVisibleNodesOverride(visibleNodes);
2242
+ return () => actions.setVisibleNodesOverride(null);
2243
+ }, [visibleNodes, actions]);
2244
+ const { maxKeyLength, maxDepth } = useMemo6(() => {
1881
2245
  let maxKey = 1;
1882
2246
  let maxD = 0;
1883
2247
  const baseSegments = displayNode.path === "/" ? 0 : displayNode.path.split("/").filter(Boolean).length;
@@ -1890,10 +2254,20 @@ function FormView({
1890
2254
  }
1891
2255
  return { maxKeyLength: maxKey, maxDepth: maxD };
1892
2256
  }, [visibleNodes, displayNode.path, state.tree]);
1893
- const handleSelect = useCallback6((nodeId) => {
1894
- setFormSelectedNodeId(nodeId);
1895
- setEditingNodeId(null);
1896
- }, []);
2257
+ const handleSelect = useCallback6(
2258
+ (nodeId, e) => {
2259
+ setEditingNodeId(null);
2260
+ if (e.shiftKey) {
2261
+ actions.setVisibleNodesOverride(visibleNodes);
2262
+ actions.selectNodeRange(nodeId);
2263
+ } else if (e.metaKey || e.ctrlKey) {
2264
+ actions.toggleNodeSelection(nodeId);
2265
+ } else {
2266
+ actions.selectNode(nodeId);
2267
+ }
2268
+ },
2269
+ [actions, visibleNodes]
2270
+ );
1897
2271
  const handleToggleCollapse = useCallback6((nodeId) => {
1898
2272
  setCollapsedIds((prev) => {
1899
2273
  const next = new Set(prev);
@@ -1941,15 +2315,23 @@ function FormView({
1941
2315
  }
1942
2316
  return;
1943
2317
  }
1944
- const currentIndex = visibleNodes.findIndex(
1945
- (n) => n.id === formSelectedNodeId
2318
+ let currentIndex = visibleNodes.findIndex(
2319
+ (n) => n.id === state.focusedNodeId
1946
2320
  );
2321
+ if (currentIndex === -1 && visibleNodes.length > 0) {
2322
+ currentIndex = 0;
2323
+ }
1947
2324
  switch (e.key) {
1948
2325
  case "ArrowDown": {
1949
2326
  e.preventDefault();
1950
2327
  const next = visibleNodes[currentIndex + 1];
1951
2328
  if (next) {
1952
- setFormSelectedNodeId(next.id);
2329
+ if (e.shiftKey) {
2330
+ actions.setVisibleNodesOverride(visibleNodes);
2331
+ actions.selectNodeRange(next.id);
2332
+ } else {
2333
+ actions.selectNode(next.id);
2334
+ }
1953
2335
  scrollToNode(next.id);
1954
2336
  }
1955
2337
  break;
@@ -1958,7 +2340,12 @@ function FormView({
1958
2340
  e.preventDefault();
1959
2341
  const prev = visibleNodes[currentIndex - 1];
1960
2342
  if (prev) {
1961
- setFormSelectedNodeId(prev.id);
2343
+ if (e.shiftKey) {
2344
+ actions.setVisibleNodesOverride(visibleNodes);
2345
+ actions.selectNodeRange(prev.id);
2346
+ } else {
2347
+ actions.selectNode(prev.id);
2348
+ }
1962
2349
  scrollToNode(prev.id);
1963
2350
  }
1964
2351
  break;
@@ -1974,7 +2361,7 @@ function FormView({
1974
2361
  return next;
1975
2362
  });
1976
2363
  } else if (node.children.length > 0) {
1977
- setFormSelectedNodeId(node.children[0].id);
2364
+ actions.selectNode(node.children[0].id);
1978
2365
  scrollToNode(node.children[0].id);
1979
2366
  }
1980
2367
  }
@@ -1996,7 +2383,7 @@ function FormView({
1996
2383
  (n) => n.id === current.parentId
1997
2384
  );
1998
2385
  if (parentInVisible) {
1999
- setFormSelectedNodeId(parentInVisible.id);
2386
+ actions.selectNode(parentInVisible.id);
2000
2387
  scrollToNode(parentInVisible.id);
2001
2388
  }
2002
2389
  }
@@ -2004,30 +2391,54 @@ function FormView({
2004
2391
  }
2005
2392
  case "Enter": {
2006
2393
  e.preventDefault();
2007
- if (formSelectedNodeId) {
2394
+ if (state.focusedNodeId) {
2008
2395
  preEditTreeRef.current = state.tree;
2009
- setEditingNodeId(formSelectedNodeId);
2396
+ actions.selectNode(state.focusedNodeId);
2397
+ setEditingNodeId(state.focusedNodeId);
2398
+ }
2399
+ break;
2400
+ }
2401
+ case "a": {
2402
+ if (e.metaKey || e.ctrlKey) {
2403
+ e.preventDefault();
2404
+ const ids = computeSelectAllIds(
2405
+ state.tree,
2406
+ state.focusedNodeId,
2407
+ state.selectedNodeIds
2408
+ );
2409
+ if (ids) {
2410
+ actions.setSelection(
2411
+ state.focusedNodeId,
2412
+ ids,
2413
+ state.focusedNodeId
2414
+ );
2415
+ }
2010
2416
  }
2011
2417
  break;
2012
2418
  }
2013
2419
  case "Escape": {
2014
2420
  e.preventDefault();
2015
- setEditingNodeId(null);
2421
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
2422
+ actions.selectNode(state.focusedNodeId);
2423
+ } else {
2424
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2425
+ }
2016
2426
  break;
2017
2427
  }
2018
2428
  case "Delete":
2019
2429
  case "Backspace": {
2020
2430
  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 = removeNode2(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
- }
2431
+ const { newTree, nextFocusId } = deleteSelectedNodes(
2432
+ state.tree,
2433
+ state.selectedNodeIds,
2434
+ visibleNodes
2435
+ );
2436
+ if (newTree === state.tree) break;
2437
+ actions.setTree(newTree);
2438
+ if (nextFocusId) {
2439
+ actions.selectNode(nextFocusId);
2440
+ } else {
2441
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2031
2442
  }
2032
2443
  break;
2033
2444
  }
@@ -2035,7 +2446,8 @@ function FormView({
2035
2446
  },
2036
2447
  [
2037
2448
  visibleNodes,
2038
- formSelectedNodeId,
2449
+ state.focusedNodeId,
2450
+ state.selectedNodeIds,
2039
2451
  editingNodeId,
2040
2452
  collapsedIds,
2041
2453
  scrollToNode,
@@ -2060,9 +2472,11 @@ function FormView({
2060
2472
  "div",
2061
2473
  {
2062
2474
  style: {
2475
+ display: "flex",
2476
+ alignItems: "center",
2063
2477
  padding: "4px 8px",
2064
2478
  borderBottom: "1px solid var(--vj-border, #333333)",
2065
- backgroundColor: "var(--vj-bg-panel, #252526)",
2479
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2066
2480
  flexShrink: 0
2067
2481
  },
2068
2482
  children: /* @__PURE__ */ jsx6(Breadcrumbs, {})
@@ -2095,7 +2509,6 @@ function FormView({
2095
2509
  depth: 0,
2096
2510
  showDescriptions,
2097
2511
  showCounts,
2098
- formSelectedNodeId,
2099
2512
  editingNodeId,
2100
2513
  collapsedIds,
2101
2514
  maxKeyLength,
@@ -2165,7 +2578,7 @@ function SearchBar({ className }) {
2165
2578
  alignItems: "center",
2166
2579
  gap: 6,
2167
2580
  padding: "4px 8px",
2168
- backgroundColor: "var(--vj-bg-panel, #252526)",
2581
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2169
2582
  borderBottom: "1px solid var(--vj-border, #333333)"
2170
2583
  },
2171
2584
  children: [
@@ -2372,31 +2785,6 @@ function SearchBar({ className }) {
2372
2785
 
2373
2786
  // src/json-editor.tsx
2374
2787
  import { jsx as jsx8, jsxs as jsxs6 } from "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
2788
  function JsonEditor({
2401
2789
  value,
2402
2790
  defaultValue,
@@ -2723,440 +3111,10 @@ function EditorLayout({
2723
3111
  ] });
2724
3112
  }
2725
3113
 
2726
- // src/property-editor.tsx
2727
- import { useState as useState9, useCallback as useCallback9, useRef as useRef10 } from "react";
2728
- import {
2729
- setValue as setValue2,
2730
- setKey as setKey2,
2731
- addProperty as addProperty2,
2732
- removeNode as removeNode3,
2733
- changeType as changeType2,
2734
- duplicateNode as duplicateNode2,
2735
- toJson as toJson3,
2736
- getPropertySchema as getPropertySchema3,
2737
- resolveRef as resolveRef2
2738
- } from "@visual-json/core";
2739
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2740
- var ALL_TYPES = [
2741
- "string",
2742
- "number",
2743
- "boolean",
2744
- "null",
2745
- "object",
2746
- "array"
2747
- ];
2748
- function PropertyRow({ node, schemaProperty }) {
2749
- const { state, actions } = useStudio();
2750
- const isContainer = node.type === "object" || node.type === "array";
2751
- const [hoveredRow, setHoveredRow] = useState9(false);
2752
- const enumRef = useRef10(null);
2753
- function handleValueChange(newValue) {
2754
- let parsed;
2755
- if (newValue === "null") parsed = null;
2756
- else if (newValue === "true") parsed = true;
2757
- else if (newValue === "false") parsed = false;
2758
- else if ((node.type === "number" || schemaProperty?.type === "number" || schemaProperty?.type === "integer") && !isNaN(Number(newValue)) && newValue.trim() !== "")
2759
- parsed = Number(newValue);
2760
- else parsed = newValue;
2761
- const newTree = setValue2(state.tree, node.id, parsed);
2762
- actions.setTree(newTree);
2763
- }
2764
- function handleKeyChange(newKey) {
2765
- const newTree = setKey2(state.tree, node.id, newKey);
2766
- actions.setTree(newTree);
2767
- }
2768
- function handleRemove() {
2769
- const newTree = removeNode3(state.tree, node.id);
2770
- actions.setTree(newTree);
2771
- }
2772
- function handleAddChild() {
2773
- const key = node.type === "array" ? String(node.children.length) : `key${node.children.length}`;
2774
- const newTree = addProperty2(state.tree, node.id, key, "");
2775
- actions.setTree(newTree);
2776
- }
2777
- function displayValue() {
2778
- if (isContainer) {
2779
- return node.type === "array" ? `[${node.children.length} items]` : `{${node.children.length} keys}`;
2780
- }
2781
- if (node.value === null) return "";
2782
- if (node.value === void 0) return "";
2783
- if (typeof node.value === "string" && node.value === "") return "";
2784
- return String(node.value);
2785
- }
2786
- const hasEnumValues = schemaProperty?.enum && schemaProperty.enum.length > 0;
2787
- const description = schemaProperty?.description;
2788
- return /* @__PURE__ */ jsxs7(
2789
- "div",
2790
- {
2791
- onMouseEnter: () => setHoveredRow(true),
2792
- onMouseLeave: () => setHoveredRow(false),
2793
- children: [
2794
- /* @__PURE__ */ jsxs7(
2795
- "div",
2796
- {
2797
- style: {
2798
- display: "flex",
2799
- alignItems: "center",
2800
- gap: 8,
2801
- padding: "4px 12px",
2802
- borderBottom: "1px solid var(--vj-border-subtle, #2a2a2a)",
2803
- minHeight: 32,
2804
- backgroundColor: hoveredRow ? "var(--vj-bg-hover, #2a2d2e)" : "transparent"
2805
- },
2806
- children: [
2807
- /* @__PURE__ */ jsx9(
2808
- "input",
2809
- {
2810
- value: node.key,
2811
- onChange: (e) => handleKeyChange(e.target.value),
2812
- style: {
2813
- background: "none",
2814
- border: "1px solid transparent",
2815
- borderRadius: 3,
2816
- color: "var(--vj-text, #cccccc)",
2817
- fontFamily: "var(--vj-font, monospace)",
2818
- fontSize: "var(--vj-input-font-size, 13px)",
2819
- padding: "2px 6px",
2820
- width: 140,
2821
- flexShrink: 0
2822
- }
2823
- }
2824
- ),
2825
- !isContainer ? hasEnumValues ? /* @__PURE__ */ jsx9(
2826
- EnumInput,
2827
- {
2828
- enumValues: schemaProperty.enum,
2829
- value: displayValue(),
2830
- onValueChange: handleValueChange,
2831
- inputRef: enumRef,
2832
- inputStyle: {
2833
- background: "none",
2834
- border: "1px solid transparent",
2835
- borderRadius: 3,
2836
- color: node.type === "string" ? "var(--vj-string, #ce9178)" : "var(--vj-number, #b5cea8)",
2837
- fontFamily: "var(--vj-font, monospace)",
2838
- fontSize: "var(--vj-input-font-size, 13px)",
2839
- padding: "2px 6px",
2840
- flex: 1,
2841
- outline: "none"
2842
- }
2843
- }
2844
- ) : /* @__PURE__ */ jsx9(
2845
- "input",
2846
- {
2847
- value: displayValue(),
2848
- onChange: (e) => handleValueChange(e.target.value),
2849
- placeholder: "<value>",
2850
- style: {
2851
- background: "none",
2852
- border: "1px solid transparent",
2853
- borderRadius: 3,
2854
- color: node.type === "string" ? "var(--vj-string, #ce9178)" : "var(--vj-number, #b5cea8)",
2855
- fontFamily: "var(--vj-font, monospace)",
2856
- fontSize: "var(--vj-input-font-size, 13px)",
2857
- padding: "2px 6px",
2858
- flex: 1,
2859
- textAlign: "right"
2860
- }
2861
- }
2862
- ) : /* @__PURE__ */ jsx9(
2863
- "span",
2864
- {
2865
- style: {
2866
- color: "var(--vj-text-dim, #666666)",
2867
- fontFamily: "var(--vj-font, monospace)",
2868
- fontSize: 13,
2869
- flex: 1,
2870
- textAlign: "right"
2871
- },
2872
- children: displayValue()
2873
- }
2874
- ),
2875
- /* @__PURE__ */ jsxs7(
2876
- "div",
2877
- {
2878
- style: {
2879
- display: "flex",
2880
- gap: 2,
2881
- opacity: hoveredRow ? 1 : 0,
2882
- transition: "opacity 0.1s",
2883
- flexShrink: 0
2884
- },
2885
- children: [
2886
- isContainer && /* @__PURE__ */ jsx9(
2887
- "button",
2888
- {
2889
- onClick: handleAddChild,
2890
- style: {
2891
- background: "none",
2892
- border: "none",
2893
- color: "var(--vj-text-muted, #888888)",
2894
- cursor: "pointer",
2895
- padding: "2px 4px",
2896
- fontSize: 15,
2897
- fontFamily: "var(--vj-font, monospace)",
2898
- borderRadius: 3,
2899
- lineHeight: 1
2900
- },
2901
- title: "Add child",
2902
- children: "+"
2903
- }
2904
- ),
2905
- /* @__PURE__ */ jsx9(
2906
- "button",
2907
- {
2908
- onClick: handleRemove,
2909
- style: {
2910
- background: "none",
2911
- border: "none",
2912
- color: "var(--vj-text-muted, #888888)",
2913
- cursor: "pointer",
2914
- padding: "2px 4px",
2915
- fontSize: 15,
2916
- fontFamily: "var(--vj-font, monospace)",
2917
- borderRadius: 3,
2918
- lineHeight: 1
2919
- },
2920
- title: "Remove",
2921
- children: "\xD7"
2922
- }
2923
- )
2924
- ]
2925
- }
2926
- )
2927
- ]
2928
- }
2929
- ),
2930
- description && /* @__PURE__ */ jsx9(
2931
- "div",
2932
- {
2933
- style: {
2934
- padding: "2px 12px 4px 44px",
2935
- fontSize: 11,
2936
- color: "var(--vj-text-dim, #666666)",
2937
- fontFamily: "var(--vj-font, monospace)",
2938
- borderBottom: "1px solid var(--vj-border-subtle, #2a2a2a)"
2939
- },
2940
- children: description
2941
- }
2942
- )
2943
- ]
2944
- }
2945
- );
2946
- }
2947
- function PropertyEditor({ className }) {
2948
- const { state, actions } = useStudio();
2949
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
2950
- const handleChangeType = useCallback9(
2951
- (newType) => {
2952
- if (!selectedNode) return;
2953
- const newTree = changeType2(state.tree, selectedNode.id, newType);
2954
- actions.setTree(newTree);
2955
- },
2956
- [state.tree, selectedNode, actions]
2957
- );
2958
- const handleDuplicate = useCallback9(() => {
2959
- if (!selectedNode) return;
2960
- const newTree = duplicateNode2(state.tree, selectedNode.id);
2961
- actions.setTree(newTree);
2962
- }, [state.tree, selectedNode, actions]);
2963
- const handleCopyPath = useCallback9(() => {
2964
- if (!selectedNode) return;
2965
- navigator.clipboard.writeText(selectedNode.path).catch(() => {
2966
- });
2967
- }, [selectedNode]);
2968
- const handleCopyValue = useCallback9(() => {
2969
- if (!selectedNode) return;
2970
- const value = toJson3(selectedNode);
2971
- const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
2972
- navigator.clipboard.writeText(text).catch(() => {
2973
- });
2974
- }, [selectedNode]);
2975
- if (!selectedNode) {
2976
- return /* @__PURE__ */ jsx9(
2977
- "div",
2978
- {
2979
- className,
2980
- style: {
2981
- display: "flex",
2982
- alignItems: "center",
2983
- justifyContent: "center",
2984
- backgroundColor: "var(--vj-bg, #1e1e1e)",
2985
- color: "var(--vj-text-dimmer, #555555)",
2986
- fontFamily: "var(--vj-font, monospace)",
2987
- fontSize: 13,
2988
- height: "100%"
2989
- },
2990
- children: "Select a node to edit"
2991
- }
2992
- );
2993
- }
2994
- const isContainer = selectedNode.type === "object" || selectedNode.type === "array";
2995
- const schema = state.schema ?? void 0;
2996
- const schemaTitle = schema?.title;
2997
- function getChildSchema(childKey) {
2998
- if (!schema || !selectedNode) return void 0;
2999
- const childPath = selectedNode.path === "/" ? `/${childKey}` : `${selectedNode.path}/${childKey}`;
3000
- const raw = getPropertySchema3(schema, childPath);
3001
- return raw ? resolveRef2(raw, schema) : void 0;
3002
- }
3003
- function handleAdd() {
3004
- if (!selectedNode) return;
3005
- const key = selectedNode.type === "array" ? String(selectedNode.children.length) : `key${selectedNode.children.length}`;
3006
- const newTree = addProperty2(state.tree, selectedNode.id, key, "");
3007
- actions.setTree(newTree);
3008
- }
3009
- return /* @__PURE__ */ jsxs7(
3010
- "div",
3011
- {
3012
- className,
3013
- style: {
3014
- backgroundColor: "var(--vj-bg, #1e1e1e)",
3015
- color: "var(--vj-text, #cccccc)",
3016
- overflow: "auto",
3017
- height: "100%",
3018
- display: "flex",
3019
- flexDirection: "column"
3020
- },
3021
- children: [
3022
- /* @__PURE__ */ jsxs7(
3023
- "div",
3024
- {
3025
- style: {
3026
- display: "flex",
3027
- alignItems: "center",
3028
- justifyContent: "space-between",
3029
- padding: "6px 12px",
3030
- borderBottom: "1px solid var(--vj-border, #333333)",
3031
- fontSize: 12,
3032
- color: "var(--vj-text-muted, #999999)",
3033
- fontFamily: "var(--vj-font, monospace)",
3034
- flexShrink: 0,
3035
- backgroundColor: "var(--vj-bg-panel, #252526)"
3036
- },
3037
- children: [
3038
- /* @__PURE__ */ jsxs7(
3039
- "div",
3040
- {
3041
- style: {
3042
- display: "flex",
3043
- flexDirection: "column",
3044
- gap: 2,
3045
- flex: 1,
3046
- minWidth: 0
3047
- },
3048
- children: [
3049
- /* @__PURE__ */ jsx9(Breadcrumbs, {}),
3050
- schemaTitle && /* @__PURE__ */ jsx9(
3051
- "span",
3052
- {
3053
- style: { fontSize: 10, color: "var(--vj-text-dim, #666666)" },
3054
- children: schemaTitle
3055
- }
3056
- )
3057
- ]
3058
- }
3059
- ),
3060
- /* @__PURE__ */ jsxs7(
3061
- "div",
3062
- {
3063
- style: {
3064
- display: "flex",
3065
- alignItems: "center",
3066
- gap: 4,
3067
- flexShrink: 0
3068
- },
3069
- children: [
3070
- /* @__PURE__ */ jsx9(
3071
- "select",
3072
- {
3073
- value: selectedNode.type,
3074
- onChange: (e) => handleChangeType(e.target.value),
3075
- style: {
3076
- background: "var(--vj-input-bg, #3c3c3c)",
3077
- border: "1px solid var(--vj-input-border, #555555)",
3078
- borderRadius: 3,
3079
- color: "var(--vj-text, #cccccc)",
3080
- fontSize: "var(--vj-input-font-size, 13px)",
3081
- fontFamily: "var(--vj-font, monospace)",
3082
- padding: "1px 4px",
3083
- cursor: "pointer"
3084
- },
3085
- title: "Change type",
3086
- children: ALL_TYPES.map((t) => /* @__PURE__ */ jsx9("option", { value: t, children: t }, t))
3087
- }
3088
- ),
3089
- /* @__PURE__ */ jsx9(
3090
- "button",
3091
- {
3092
- onClick: handleCopyPath,
3093
- style: actionButtonStyle,
3094
- title: "Copy path",
3095
- children: "path"
3096
- }
3097
- ),
3098
- /* @__PURE__ */ jsx9(
3099
- "button",
3100
- {
3101
- onClick: handleCopyValue,
3102
- style: actionButtonStyle,
3103
- title: "Copy value",
3104
- children: "value"
3105
- }
3106
- ),
3107
- selectedNode.parentId && /* @__PURE__ */ jsx9(
3108
- "button",
3109
- {
3110
- onClick: handleDuplicate,
3111
- style: actionButtonStyle,
3112
- title: "Duplicate",
3113
- children: "dup"
3114
- }
3115
- ),
3116
- isContainer && /* @__PURE__ */ jsx9(
3117
- "button",
3118
- {
3119
- onClick: handleAdd,
3120
- style: {
3121
- ...actionButtonStyle,
3122
- border: "1px solid var(--vj-input-border, #555555)"
3123
- },
3124
- children: "+ Add"
3125
- }
3126
- )
3127
- ]
3128
- }
3129
- )
3130
- ]
3131
- }
3132
- ),
3133
- /* @__PURE__ */ jsx9("div", { style: { flex: 1, overflow: "auto" }, children: isContainer ? selectedNode.children.map((child) => /* @__PURE__ */ jsx9(
3134
- PropertyRow,
3135
- {
3136
- node: child,
3137
- schemaProperty: getChildSchema(child.key)
3138
- },
3139
- child.id
3140
- )) : /* @__PURE__ */ jsx9(PropertyRow, { node: selectedNode }) })
3141
- ]
3142
- }
3143
- );
3144
- }
3145
- var actionButtonStyle = {
3146
- background: "none",
3147
- border: "none",
3148
- borderRadius: 3,
3149
- color: "var(--vj-text-muted, #888888)",
3150
- cursor: "pointer",
3151
- padding: "1px 6px",
3152
- fontSize: 11,
3153
- fontFamily: "var(--vj-font, monospace)"
3154
- };
3155
-
3156
3114
  // src/diff-view.tsx
3157
- import { useMemo as useMemo6 } from "react";
3115
+ import { useMemo as useMemo7 } from "react";
3158
3116
  import { computeDiff } from "@visual-json/core";
3159
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
3117
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3160
3118
  var DIFF_COLORS = {
3161
3119
  added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
3162
3120
  removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
@@ -3177,7 +3135,7 @@ function formatValue(value) {
3177
3135
  }
3178
3136
  function DiffRow({ entry }) {
3179
3137
  const colors = DIFF_COLORS[entry.type];
3180
- return /* @__PURE__ */ jsxs8(
3138
+ return /* @__PURE__ */ jsxs7(
3181
3139
  "div",
3182
3140
  {
3183
3141
  style: {
@@ -3191,7 +3149,7 @@ function DiffRow({ entry }) {
3191
3149
  gap: 8
3192
3150
  },
3193
3151
  children: [
3194
- /* @__PURE__ */ jsx10(
3152
+ /* @__PURE__ */ jsx9(
3195
3153
  "span",
3196
3154
  {
3197
3155
  style: {
@@ -3204,7 +3162,7 @@ function DiffRow({ entry }) {
3204
3162
  children: colors.marker
3205
3163
  }
3206
3164
  ),
3207
- /* @__PURE__ */ jsx10(
3165
+ /* @__PURE__ */ jsx9(
3208
3166
  "span",
3209
3167
  {
3210
3168
  style: {
@@ -3215,14 +3173,14 @@ function DiffRow({ entry }) {
3215
3173
  children: entry.path
3216
3174
  }
3217
3175
  ),
3218
- /* @__PURE__ */ jsxs8("span", { style: { flex: 1, display: "flex", gap: 8, overflow: "hidden" }, children: [
3219
- entry.type === "changed" && /* @__PURE__ */ jsxs8(Fragment3, { children: [
3220
- /* @__PURE__ */ jsx10("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) }),
3221
- /* @__PURE__ */ jsx10("span", { style: { color: "var(--vj-text-dim, #666666)" }, children: "\u2192" }),
3222
- /* @__PURE__ */ jsx10("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) })
3176
+ /* @__PURE__ */ jsxs7("span", { style: { flex: 1, display: "flex", gap: 8, overflow: "hidden" }, children: [
3177
+ entry.type === "changed" && /* @__PURE__ */ jsxs7(Fragment3, { children: [
3178
+ /* @__PURE__ */ jsx9("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) }),
3179
+ /* @__PURE__ */ jsx9("span", { style: { color: "var(--vj-text-dim, #666666)" }, children: "\u2192" }),
3180
+ /* @__PURE__ */ jsx9("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) })
3223
3181
  ] }),
3224
- entry.type === "added" && /* @__PURE__ */ jsx10("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) }),
3225
- entry.type === "removed" && /* @__PURE__ */ jsx10("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) })
3182
+ entry.type === "added" && /* @__PURE__ */ jsx9("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) }),
3183
+ entry.type === "removed" && /* @__PURE__ */ jsx9("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) })
3226
3184
  ] })
3227
3185
  ]
3228
3186
  }
@@ -3233,14 +3191,14 @@ function DiffView({
3233
3191
  currentJson,
3234
3192
  className
3235
3193
  }) {
3236
- const entries = useMemo6(
3194
+ const entries = useMemo7(
3237
3195
  () => computeDiff(originalJson, currentJson),
3238
3196
  [originalJson, currentJson]
3239
3197
  );
3240
3198
  const added = entries.filter((e) => e.type === "added").length;
3241
3199
  const removed = entries.filter((e) => e.type === "removed").length;
3242
3200
  const changed = entries.filter((e) => e.type === "changed").length;
3243
- return /* @__PURE__ */ jsxs8(
3201
+ return /* @__PURE__ */ jsxs7(
3244
3202
  "div",
3245
3203
  {
3246
3204
  className,
@@ -3253,7 +3211,7 @@ function DiffView({
3253
3211
  flexDirection: "column"
3254
3212
  },
3255
3213
  children: [
3256
- /* @__PURE__ */ jsxs8(
3214
+ /* @__PURE__ */ jsxs7(
3257
3215
  "div",
3258
3216
  {
3259
3217
  style: {
@@ -3268,18 +3226,18 @@ function DiffView({
3268
3226
  flexShrink: 0
3269
3227
  },
3270
3228
  children: [
3271
- /* @__PURE__ */ jsx10("span", { style: { color: "var(--vj-text-muted, #999999)" }, children: entries.length === 0 ? "No changes" : `${entries.length} changes` }),
3272
- added > 0 && /* @__PURE__ */ jsxs8("span", { style: { color: "#4ec94e" }, children: [
3229
+ /* @__PURE__ */ jsx9("span", { style: { color: "var(--vj-text-muted, #999999)" }, children: entries.length === 0 ? "No changes" : `${entries.length} changes` }),
3230
+ added > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#4ec94e" }, children: [
3273
3231
  "+",
3274
3232
  added,
3275
3233
  " added"
3276
3234
  ] }),
3277
- removed > 0 && /* @__PURE__ */ jsxs8("span", { style: { color: "#f48771" }, children: [
3235
+ removed > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#f48771" }, children: [
3278
3236
  "-",
3279
3237
  removed,
3280
3238
  " removed"
3281
3239
  ] }),
3282
- changed > 0 && /* @__PURE__ */ jsxs8("span", { style: { color: "#dcdcaa" }, children: [
3240
+ changed > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#dcdcaa" }, children: [
3283
3241
  "~",
3284
3242
  changed,
3285
3243
  " modified"
@@ -3287,7 +3245,7 @@ function DiffView({
3287
3245
  ]
3288
3246
  }
3289
3247
  ),
3290
- /* @__PURE__ */ jsx10("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ jsx10(
3248
+ /* @__PURE__ */ jsx9("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ jsx9(
3291
3249
  "div",
3292
3250
  {
3293
3251
  style: {
@@ -3301,7 +3259,7 @@ function DiffView({
3301
3259
  },
3302
3260
  children: "No differences detected"
3303
3261
  }
3304
- ) : entries.map((entry, i) => /* @__PURE__ */ jsx10(DiffRow, { entry }, i)) })
3262
+ ) : entries.map((entry, i) => /* @__PURE__ */ jsx9(DiffRow, { entry }, i)) })
3305
3263
  ]
3306
3264
  }
3307
3265
  );
@@ -3312,7 +3270,6 @@ export {
3312
3270
  DiffView,
3313
3271
  FormView,
3314
3272
  JsonEditor,
3315
- PropertyEditor,
3316
3273
  SearchBar,
3317
3274
  StudioContext,
3318
3275
  TreeView,