@visual-json/react 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -22,6 +22,182 @@ import {
22
22
  getAncestorIds
23
23
  } from "@visual-json/core";
24
24
 
25
+ // ../../@internal/ui/dist/index.mjs
26
+ import {
27
+ reorderChildrenMulti,
28
+ removeNode,
29
+ insertNode,
30
+ isDescendant
31
+ } from "@visual-json/core";
32
+ import {
33
+ getPropertySchema,
34
+ resolveRef
35
+ } from "@visual-json/core";
36
+ function getVisibleNodes(root, isExpanded) {
37
+ const result = [];
38
+ function walk(node) {
39
+ result.push(node);
40
+ if (isExpanded(node.id) && (node.type === "object" || node.type === "array")) {
41
+ for (const child of node.children) {
42
+ walk(child);
43
+ }
44
+ }
45
+ }
46
+ walk(root);
47
+ return result;
48
+ }
49
+ var LABEL_FIELDS = ["name", "type", "title", "id", "label", "key"];
50
+ function getDisplayKey(node, state) {
51
+ if (node.parentId === null) return "/";
52
+ const parent = state.nodesById.get(node.parentId);
53
+ if (parent?.type !== "array" || node.type !== "object") return node.key;
54
+ for (const field of LABEL_FIELDS) {
55
+ const child = node.children.find((c) => c.key === field);
56
+ if (child?.value != null && child.value !== "") {
57
+ return String(child.value);
58
+ }
59
+ }
60
+ return node.key;
61
+ }
62
+ function collectAllIds(node) {
63
+ const ids = [node.id];
64
+ for (const child of node.children) {
65
+ ids.push(...collectAllIds(child));
66
+ }
67
+ return ids;
68
+ }
69
+ var DEFAULT_CSS_VARS = {
70
+ "--vj-bg": "#1e1e1e",
71
+ "--vj-bg-panel": "#252526",
72
+ "--vj-bg-hover": "#2a2d2e",
73
+ "--vj-bg-selected": "#2a5a1e",
74
+ "--vj-bg-selected-muted": "#2a2d2e",
75
+ "--vj-bg-match": "#3a3520",
76
+ "--vj-bg-match-active": "#51502b",
77
+ "--vj-border": "#333333",
78
+ "--vj-border-subtle": "#2a2a2a",
79
+ "--vj-text": "#cccccc",
80
+ "--vj-text-muted": "#888888",
81
+ "--vj-text-dim": "#666666",
82
+ "--vj-text-dimmer": "#555555",
83
+ "--vj-string": "#ce9178",
84
+ "--vj-number": "#b5cea8",
85
+ "--vj-boolean": "#569cd6",
86
+ "--vj-accent": "#007acc",
87
+ "--vj-accent-muted": "#094771",
88
+ "--vj-input-bg": "#3c3c3c",
89
+ "--vj-input-border": "#555555",
90
+ "--vj-error": "#f48771",
91
+ "--vj-font": "monospace",
92
+ "--vj-input-font-size": "13px"
93
+ };
94
+ var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
95
+ function sortByTreeOrder(root, ids) {
96
+ const result = [];
97
+ function walk(node) {
98
+ if (ids.has(node.id)) result.push(node.id);
99
+ for (const child of node.children) walk(child);
100
+ }
101
+ walk(root);
102
+ return result;
103
+ }
104
+ function computeDrop(tree, drag) {
105
+ const { draggedNodeIds, dropTargetNodeId, dropPosition } = drag;
106
+ if (draggedNodeIds.size === 0 || !dropTargetNodeId || !dropPosition)
107
+ return null;
108
+ const targetNode = tree.nodesById.get(dropTargetNodeId);
109
+ if (!targetNode || !targetNode.parentId) return null;
110
+ for (const id of draggedNodeIds) {
111
+ if (isDescendant(tree, dropTargetNodeId, id)) return null;
112
+ }
113
+ const targetParentId = targetNode.parentId;
114
+ const targetParent = tree.nodesById.get(targetParentId);
115
+ if (!targetParent) return null;
116
+ const parentChildren = targetParent.children;
117
+ const orderedDragIds = parentChildren.filter((c) => draggedNodeIds.has(c.id)).map((c) => c.id);
118
+ const allSameParent = orderedDragIds.length === draggedNodeIds.size && [...draggedNodeIds].every((id) => {
119
+ const n = tree.nodesById.get(id);
120
+ return n?.parentId === targetParentId;
121
+ });
122
+ if (allSameParent) {
123
+ return reorderChildrenMulti(
124
+ tree,
125
+ targetParentId,
126
+ orderedDragIds,
127
+ dropTargetNodeId,
128
+ dropPosition
129
+ );
130
+ }
131
+ const orderedIds = sortByTreeOrder(tree.root, draggedNodeIds);
132
+ const draggedNodes = orderedIds.map((id) => tree.nodesById.get(id)).filter((n) => !!n && n.parentId !== null).map((n) => structuredClone(n));
133
+ let newTree = tree;
134
+ for (const id of [...orderedIds].reverse()) {
135
+ if (newTree.nodesById.has(id)) {
136
+ newTree = removeNode(newTree, id);
137
+ }
138
+ }
139
+ const updatedTarget = newTree.nodesById.get(dropTargetNodeId);
140
+ if (!updatedTarget || !updatedTarget.parentId) return null;
141
+ const updatedParent = newTree.nodesById.get(updatedTarget.parentId);
142
+ if (!updatedParent) return null;
143
+ let insertIdx = updatedParent.children.findIndex(
144
+ (c) => c.id === dropTargetNodeId
145
+ );
146
+ if (dropPosition === "after") insertIdx++;
147
+ for (let i = 0; i < draggedNodes.length; i++) {
148
+ newTree = insertNode(
149
+ newTree,
150
+ updatedParent.id,
151
+ draggedNodes[i],
152
+ insertIdx + i
153
+ );
154
+ }
155
+ return newTree;
156
+ }
157
+ function setMultiDragImage(dataTransfer, count, rootEl) {
158
+ const ghost = document.createElement("div");
159
+ ghost.textContent = `${count} selected`;
160
+ const root = rootEl ?? document.querySelector("[data-form-container], [role='tree']");
161
+ const cs = root ? getComputedStyle(root) : null;
162
+ const bg = cs?.getPropertyValue("--vj-bg-selected").trim() || DEFAULT_CSS_VARS["--vj-bg-selected"];
163
+ const fg = cs?.getPropertyValue("--vj-text-selected").trim() || cs?.getPropertyValue("--vj-text").trim() || DEFAULT_CSS_VARS["--vj-text"];
164
+ const font = cs?.getPropertyValue("--vj-font").trim() || DEFAULT_CSS_VARS["--vj-font"];
165
+ ghost.style.cssText = [
166
+ "position:fixed",
167
+ "top:-1000px",
168
+ "left:-1000px",
169
+ "padding:4px 12px",
170
+ `background:${bg}`,
171
+ `color:${fg}`,
172
+ `font-family:${font}`,
173
+ "font-size:13px",
174
+ "border-radius:4px",
175
+ "white-space:nowrap",
176
+ "pointer-events:none"
177
+ ].join(";");
178
+ document.body.appendChild(ghost);
179
+ dataTransfer.setDragImage(ghost, 0, 14);
180
+ requestAnimationFrame(() => ghost.remove());
181
+ }
182
+ var DIFF_COLORS = {
183
+ added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
184
+ removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
185
+ changed: { bg: "#3a3a1e", marker: "~", label: "#dcdcaa" }
186
+ };
187
+ function formatValue(value) {
188
+ if (value === void 0) return "";
189
+ if (value === null) return "null";
190
+ if (typeof value === "string") return JSON.stringify(value);
191
+ if (typeof value === "object") {
192
+ const json = JSON.stringify(value, null, 2);
193
+ if (json.length > 80) {
194
+ return JSON.stringify(value).slice(0, 77) + "...";
195
+ }
196
+ return json;
197
+ }
198
+ return String(value);
199
+ }
200
+
25
201
  // src/context.ts
26
202
  import { createContext, useContext } from "react";
27
203
  var StudioContext = createContext(null);
@@ -33,15 +209,78 @@ function useStudio() {
33
209
  return ctx;
34
210
  }
35
211
 
36
- // src/visual-json.tsx
37
- import { jsx } from "react/jsx-runtime";
38
- function collectAllIds(node) {
39
- const ids = [node.id];
40
- for (const child of node.children) {
41
- ids.push(...collectAllIds(child));
212
+ // src/selection-utils.ts
213
+ import { removeNode as removeNode2 } from "@visual-json/core";
214
+ function computeSelectAllIds(tree, focusedNodeId, currentlySelected) {
215
+ if (!focusedNodeId) return null;
216
+ let node = tree.nodesById.get(focusedNodeId);
217
+ if (!node) return null;
218
+ while (node) {
219
+ const parent = node.parentId ? tree.nodesById.get(node.parentId) : void 0;
220
+ const siblings = parent ? parent.children : [tree.root];
221
+ const siblingIds = new Set(siblings.map((s) => s.id));
222
+ const allSelected = siblings.every((s) => currentlySelected.has(s.id));
223
+ if (!allSelected) {
224
+ return siblingIds;
225
+ }
226
+ if (!parent || !parent.parentId) return siblingIds;
227
+ node = parent;
228
+ }
229
+ return null;
230
+ }
231
+ function computeRangeIds(visibleNodes, anchorId, targetId) {
232
+ const anchorIdx = visibleNodes.findIndex((n) => n.id === anchorId);
233
+ const targetIdx = visibleNodes.findIndex((n) => n.id === targetId);
234
+ if (anchorIdx === -1 || targetIdx === -1) return null;
235
+ const start = Math.min(anchorIdx, targetIdx);
236
+ const end = Math.max(anchorIdx, targetIdx);
237
+ const ids = /* @__PURE__ */ new Set();
238
+ for (let i = start; i <= end; i++) {
239
+ ids.add(visibleNodes[i].id);
42
240
  }
43
241
  return ids;
44
242
  }
243
+ function deleteSelectedNodes(tree, selectedIds, visibleNodes) {
244
+ const idsToDelete = [...selectedIds].filter((id) => {
245
+ const node = tree.nodesById.get(id);
246
+ if (!node || node.parentId === null) return false;
247
+ let cur = tree.nodesById.get(node.parentId);
248
+ while (cur) {
249
+ if (selectedIds.has(cur.id)) return false;
250
+ cur = cur.parentId ? tree.nodesById.get(cur.parentId) : void 0;
251
+ }
252
+ return true;
253
+ });
254
+ if (idsToDelete.length === 0) return { newTree: tree, nextFocusId: null };
255
+ const firstDeletedIdx = visibleNodes.findIndex((n) => selectedIds.has(n.id));
256
+ let newTree = tree;
257
+ for (const id of idsToDelete) {
258
+ if (newTree.nodesById.has(id)) {
259
+ newTree = removeNode2(newTree, id);
260
+ }
261
+ }
262
+ let nextFocusId = null;
263
+ for (let i = firstDeletedIdx; i < visibleNodes.length; i++) {
264
+ const id = visibleNodes[i].id;
265
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
266
+ nextFocusId = id;
267
+ break;
268
+ }
269
+ }
270
+ if (!nextFocusId) {
271
+ for (let i = firstDeletedIdx - 1; i >= 0; i--) {
272
+ const id = visibleNodes[i].id;
273
+ if (!selectedIds.has(id) && newTree.nodesById.has(id)) {
274
+ nextFocusId = id;
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ return { newTree, nextFocusId };
280
+ }
281
+
282
+ // src/visual-json.tsx
283
+ import { jsx } from "react/jsx-runtime";
45
284
  function VisualJson({
46
285
  value,
47
286
  onChange,
@@ -49,10 +288,42 @@ function VisualJson({
49
288
  children
50
289
  }) {
51
290
  const [tree, setTreeState] = useState(() => fromJson(value));
52
- const [selectedNodeId, setSelectedNodeId] = useState(null);
291
+ const [focusedNodeId, setFocusedNodeId] = useState(null);
292
+ const [selectedNodeIds, setSelectedNodeIdsState] = useState(
293
+ () => /* @__PURE__ */ new Set()
294
+ );
295
+ const selectedNodeIdsRef = useRef(/* @__PURE__ */ new Set());
296
+ const setSelectedNodeIds = useCallback((ids) => {
297
+ selectedNodeIdsRef.current = ids;
298
+ setSelectedNodeIdsState(ids);
299
+ }, []);
300
+ const anchorNodeIdRef = useRef(null);
301
+ const [anchorNodeId, setAnchorNodeIdState] = useState(null);
302
+ const [drillDownNodeId, setDrillDownNodeId] = useState(null);
53
303
  const [expandedNodeIds, setExpandedNodeIds] = useState(
54
304
  () => /* @__PURE__ */ new Set([tree.root.id])
55
305
  );
306
+ const setAnchorNodeId = useCallback((id) => {
307
+ anchorNodeIdRef.current = id;
308
+ setAnchorNodeIdState(id);
309
+ }, []);
310
+ const focusSelectAndDrillDown = useCallback(
311
+ (nodeId) => {
312
+ setFocusedNodeId(nodeId);
313
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
314
+ setAnchorNodeId(nodeId);
315
+ setDrillDownNodeId(nodeId);
316
+ },
317
+ [setSelectedNodeIds, setAnchorNodeId]
318
+ );
319
+ const visibleNodes = useMemo(
320
+ () => getVisibleNodes(tree.root, (id) => expandedNodeIds.has(id)),
321
+ [tree.root, expandedNodeIds]
322
+ );
323
+ const visibleNodesOverrideRef = useRef(null);
324
+ const setVisibleNodesOverride = useCallback((nodes) => {
325
+ visibleNodesOverrideRef.current = nodes;
326
+ }, []);
56
327
  const historyRef = useRef(new History());
57
328
  const isInternalChange = useRef(false);
58
329
  const hasMounted = useRef(false);
@@ -81,7 +352,7 @@ function VisualJson({
81
352
  const newTree = fromJson(value);
82
353
  setTreeState(newTree);
83
354
  setExpandedNodeIds(/* @__PURE__ */ new Set([newTree.root.id]));
84
- setSelectedNodeId(null);
355
+ focusSelectAndDrillDown(null);
85
356
  historyRef.current = new History();
86
357
  historyRef.current.push(newTree);
87
358
  setCanUndo(false);
@@ -139,8 +410,66 @@ function VisualJson({
139
410
  document.addEventListener("keydown", handleKeyDown);
140
411
  return () => document.removeEventListener("keydown", handleKeyDown);
141
412
  }, [undo, redo]);
142
- const selectNode = useCallback((nodeId) => {
143
- setSelectedNodeId(nodeId);
413
+ const selectNode = useCallback(
414
+ (nodeId) => {
415
+ setFocusedNodeId(nodeId);
416
+ setSelectedNodeIds(nodeId ? /* @__PURE__ */ new Set([nodeId]) : /* @__PURE__ */ new Set());
417
+ setAnchorNodeId(nodeId);
418
+ },
419
+ [setSelectedNodeIds, setAnchorNodeId]
420
+ );
421
+ const selectAndDrillDown = focusSelectAndDrillDown;
422
+ const toggleNodeSelection = useCallback(
423
+ (nodeId) => {
424
+ const next = new Set(selectedNodeIdsRef.current);
425
+ if (next.has(nodeId)) {
426
+ next.delete(nodeId);
427
+ } else {
428
+ next.add(nodeId);
429
+ }
430
+ setSelectedNodeIds(next);
431
+ if (next.size === 0) {
432
+ setFocusedNodeId(null);
433
+ setAnchorNodeId(null);
434
+ } else {
435
+ setFocusedNodeId(nodeId);
436
+ setAnchorNodeId(nodeId);
437
+ }
438
+ },
439
+ [setSelectedNodeIds, setAnchorNodeId]
440
+ );
441
+ const selectNodeRange = useCallback(
442
+ (toNodeId) => {
443
+ const nodes = visibleNodesOverrideRef.current ?? visibleNodes;
444
+ const anchor = anchorNodeIdRef.current;
445
+ if (!anchor) {
446
+ setFocusedNodeId(toNodeId);
447
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
448
+ setAnchorNodeId(toNodeId);
449
+ return;
450
+ }
451
+ const rangeIds = computeRangeIds(nodes, anchor, toNodeId);
452
+ if (!rangeIds) {
453
+ setFocusedNodeId(toNodeId);
454
+ setSelectedNodeIds(/* @__PURE__ */ new Set([toNodeId]));
455
+ setAnchorNodeId(toNodeId);
456
+ return;
457
+ }
458
+ setSelectedNodeIds(rangeIds);
459
+ setFocusedNodeId(toNodeId);
460
+ },
461
+ [visibleNodes, setSelectedNodeIds, setAnchorNodeId]
462
+ );
463
+ const setSelection = useCallback(
464
+ (focusedId, newSelectedIds, newAnchorId) => {
465
+ setFocusedNodeId(focusedId);
466
+ setSelectedNodeIds(newSelectedIds);
467
+ setAnchorNodeId(newAnchorId);
468
+ },
469
+ [setSelectedNodeIds, setAnchorNodeId]
470
+ );
471
+ const drillDown = useCallback((nodeId) => {
472
+ setDrillDownNodeId(nodeId);
144
473
  }, []);
145
474
  const toggleExpand = useCallback((nodeId) => {
146
475
  setExpandedNodeIds((prev) => {
@@ -186,6 +515,7 @@ function VisualJson({
186
515
  const matchIds = new Set(matches.map((m) => m.nodeId));
187
516
  setSearchMatchNodeIds(matchIds);
188
517
  if (matches.length > 0) {
518
+ const firstId = matches[0].nodeId;
189
519
  const ancestors = getAncestorIds(
190
520
  tree,
191
521
  matches.map((m) => m.nodeId)
@@ -195,7 +525,7 @@ function VisualJson({
195
525
  for (const id of ancestors) next.add(id);
196
526
  return next;
197
527
  });
198
- setSelectedNodeId(matches[0].nodeId);
528
+ focusSelectAndDrillDown(firstId);
199
529
  }
200
530
  },
201
531
  [tree]
@@ -204,14 +534,14 @@ function VisualJson({
204
534
  if (searchMatches.length === 0) return;
205
535
  const nextIdx = (searchMatchIndex + 1) % searchMatches.length;
206
536
  setSearchMatchIndex(nextIdx);
207
- setSelectedNodeId(searchMatches[nextIdx].nodeId);
208
- }, [searchMatches, searchMatchIndex]);
537
+ focusSelectAndDrillDown(searchMatches[nextIdx].nodeId);
538
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
209
539
  const prevSearchMatch = useCallback(() => {
210
540
  if (searchMatches.length === 0) return;
211
541
  const prevIdx = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length;
212
542
  setSearchMatchIndex(prevIdx);
213
- setSelectedNodeId(searchMatches[prevIdx].nodeId);
214
- }, [searchMatches, searchMatchIndex]);
543
+ focusSelectAndDrillDown(searchMatches[prevIdx].nodeId);
544
+ }, [searchMatches, searchMatchIndex, focusSelectAndDrillDown]);
215
545
  useEffect(() => {
216
546
  if (!searchQuery.trim()) return;
217
547
  const matches = searchNodes(tree, searchQuery);
@@ -224,7 +554,10 @@ function VisualJson({
224
554
  const state = useMemo(
225
555
  () => ({
226
556
  tree,
227
- selectedNodeId,
557
+ focusedNodeId,
558
+ selectedNodeIds,
559
+ anchorNodeId,
560
+ drillDownNodeId,
228
561
  expandedNodeIds,
229
562
  schema: schema ?? null,
230
563
  searchQuery,
@@ -234,7 +567,10 @@ function VisualJson({
234
567
  }),
235
568
  [
236
569
  tree,
237
- selectedNodeId,
570
+ focusedNodeId,
571
+ selectedNodeIds,
572
+ anchorNodeId,
573
+ drillDownNodeId,
238
574
  expandedNodeIds,
239
575
  schema,
240
576
  searchQuery,
@@ -247,6 +583,12 @@ function VisualJson({
247
583
  () => ({
248
584
  setTree,
249
585
  selectNode,
586
+ selectAndDrillDown,
587
+ toggleNodeSelection,
588
+ selectNodeRange,
589
+ setSelection,
590
+ setVisibleNodesOverride,
591
+ drillDown,
250
592
  toggleExpand,
251
593
  expandNode,
252
594
  collapseNode,
@@ -263,6 +605,12 @@ function VisualJson({
263
605
  [
264
606
  setTree,
265
607
  selectNode,
608
+ selectAndDrillDown,
609
+ toggleNodeSelection,
610
+ selectNodeRange,
611
+ setSelection,
612
+ setVisibleNodesOverride,
613
+ drillDown,
266
614
  toggleExpand,
267
615
  expandNode,
268
616
  collapseNode,
@@ -282,11 +630,9 @@ function VisualJson({
282
630
  }
283
631
 
284
632
  // src/tree-view.tsx
285
- import { useState as useState4, useMemo as useMemo2, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect3 } from "react";
633
+ import { useState as useState4, useMemo as useMemo3, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect3 } from "react";
286
634
  import {
287
- removeNode,
288
- getPropertySchema,
289
- validateNode,
635
+ removeNode as removeNode3,
290
636
  duplicateNode,
291
637
  changeType,
292
638
  toJson as toJson2
@@ -389,111 +735,81 @@ function ContextMenu({ x, y, items, onClose }) {
389
735
  );
390
736
  }
391
737
 
392
- // src/display-key.ts
393
- var LABEL_FIELDS = ["name", "type", "title", "id", "label", "key"];
394
- function getDisplayKey(node, state) {
395
- if (node.parentId === null) return "/";
396
- const parent = state.nodesById.get(node.parentId);
397
- if (parent?.type !== "array" || node.type !== "object") return node.key;
398
- for (const field of LABEL_FIELDS) {
399
- const child = node.children.find((c) => c.key === field);
400
- if (child?.value != null && child.value !== "") {
401
- return String(child.value);
402
- }
403
- }
404
- return node.key;
405
- }
406
-
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
- }
421
-
422
738
  // 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";
739
+ import { useState as useState3, useCallback as useCallback2, useRef as useRef3, useMemo as useMemo2 } from "react";
740
+ import { isDescendant as isDescendant2 } from "@visual-json/core";
741
+ var EMPTY_SET2 = Object.freeze(/* @__PURE__ */ new Set());
425
742
  var INITIAL_DRAG_STATE = {
426
- draggedNodeId: null,
743
+ draggedNodeIds: EMPTY_SET2,
427
744
  dropTargetNodeId: null,
428
745
  dropPosition: null
429
746
  };
430
- function useDragDrop() {
747
+ function setMultiDragImage2(e, count) {
748
+ setMultiDragImage(e.dataTransfer, count);
749
+ }
750
+ function useDragDrop(visibleNodes, selectedNodeIds) {
431
751
  const { state, actions } = useStudio();
432
752
  const [dragState, setDragState] = useState3(INITIAL_DRAG_STATE);
433
753
  const dragStateRef = useRef3(dragState);
434
754
  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(
755
+ const visibleNodeIndexMap = useMemo2(() => {
756
+ const map = /* @__PURE__ */ new Map();
757
+ visibleNodes.forEach((n, i) => map.set(n.id, i));
758
+ return map;
759
+ }, [visibleNodes]);
760
+ const handleDragStart = useCallback2(
761
+ (nodeId) => {
762
+ let ids;
763
+ if (selectedNodeIds.size > 0 && selectedNodeIds.has(nodeId)) {
764
+ ids = selectedNodeIds;
765
+ } else {
766
+ ids = /* @__PURE__ */ new Set([nodeId]);
767
+ }
768
+ setDragState({
769
+ draggedNodeIds: ids,
770
+ dropTargetNodeId: null,
771
+ dropPosition: null
772
+ });
773
+ },
774
+ [selectedNodeIds]
775
+ );
776
+ const rawDragOver = useCallback2(
443
777
  (nodeId, position) => {
778
+ const draggedIds = dragStateRef.current.draggedNodeIds;
779
+ for (const draggedId of draggedIds) {
780
+ if (nodeId === draggedId || isDescendant2(state.tree, nodeId, draggedId)) {
781
+ return;
782
+ }
783
+ }
444
784
  setDragState((prev) => ({
445
785
  ...prev,
446
786
  dropTargetNodeId: nodeId,
447
787
  dropPosition: position
448
788
  }));
449
789
  },
450
- []
790
+ [state.tree]
791
+ );
792
+ const handleDragOver = useCallback2(
793
+ (nodeId, position) => {
794
+ if (position === "before") {
795
+ const idx = visibleNodeIndexMap.get(nodeId);
796
+ if (idx !== void 0 && idx > 0) {
797
+ rawDragOver(visibleNodes[idx - 1].id, "after");
798
+ return;
799
+ }
800
+ }
801
+ rawDragOver(nodeId, position);
802
+ },
803
+ [visibleNodes, visibleNodeIndexMap, rawDragOver]
451
804
  );
452
805
  const handleDragEnd = useCallback2(() => {
453
806
  setDragState(INITIAL_DRAG_STATE);
454
807
  }, []);
455
808
  const handleDrop = useCallback2(() => {
456
- const { draggedNodeId, dropTargetNodeId, dropPosition } = dragStateRef.current;
457
- if (!draggedNodeId || !dropTargetNodeId || !dropPosition) return;
458
- const draggedNode = state.tree.nodesById.get(draggedNodeId);
459
- 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);
480
- }
481
- }
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
494
- );
495
- actions.setTree(newTree);
496
- }
809
+ const currentDragState = dragStateRef.current;
810
+ const newTree = computeDrop(state.tree, currentDragState);
811
+ if (newTree) {
812
+ actions.setTree(newTree);
497
813
  }
498
814
  setDragState(INITIAL_DRAG_STATE);
499
815
  }, [state.tree, actions]);
@@ -515,6 +831,7 @@ function TreeNodeRow({
515
831
  showValues,
516
832
  showCounts,
517
833
  isFocused,
834
+ onSelectRange,
518
835
  onDragStart,
519
836
  onDragOver,
520
837
  onDragEnd,
@@ -522,19 +839,15 @@ function TreeNodeRow({
522
839
  onContextMenu
523
840
  }) {
524
841
  const { state, actions } = useStudio();
525
- const isSelected = state.selectedNodeId === node.id;
842
+ const isSelected = state.selectedNodeIds.has(node.id);
526
843
  const isExpanded = state.expandedNodeIds.has(node.id);
527
844
  const isContainer = node.type === "object" || node.type === "array";
528
845
  const [hovered, setHovered] = useState4(false);
529
846
  const isRoot = node.parentId === null;
530
847
  const isSearchMatch = state.searchMatchNodeIds.has(node.id);
531
848
  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
849
  const isDragTarget = dragState.dropTargetNodeId === node.id;
537
- const isDraggedNode = dragState.draggedNodeId === node.id;
850
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
538
851
  function displayValue() {
539
852
  if (isContainer) {
540
853
  return node.type === "array" ? `[${node.children.length}]` : `{${node.children.length}}`;
@@ -550,12 +863,12 @@ function TreeNodeRow({
550
863
  const position = e.clientY < midY ? "before" : "after";
551
864
  onDragOver(node.id, position);
552
865
  }
553
- let borderTop = "none";
554
- let borderBottom = "none";
866
+ let borderTopColor = "transparent";
867
+ let borderBottomColor = "transparent";
555
868
  if (isDragTarget && dragState.dropPosition === "before") {
556
- borderTop = "2px solid var(--vj-accent, #007acc)";
869
+ borderTopColor = "var(--vj-accent, #007acc)";
557
870
  } else if (isDragTarget && dragState.dropPosition === "after") {
558
- borderBottom = "2px solid var(--vj-accent, #007acc)";
871
+ borderBottomColor = "var(--vj-accent, #007acc)";
559
872
  }
560
873
  return /* @__PURE__ */ jsxs(Fragment, { children: [
561
874
  /* @__PURE__ */ jsxs(
@@ -564,13 +877,24 @@ function TreeNodeRow({
564
877
  role: "treeitem",
565
878
  "aria-selected": isSelected,
566
879
  "aria-expanded": isContainer ? isExpanded : void 0,
567
- onClick: () => actions.selectNode(node.id),
880
+ onClick: (e) => {
881
+ if (e.shiftKey) {
882
+ onSelectRange(node.id);
883
+ } else if (e.metaKey || e.ctrlKey) {
884
+ actions.toggleNodeSelection(node.id);
885
+ } else {
886
+ actions.selectAndDrillDown(node.id);
887
+ }
888
+ },
568
889
  onMouseEnter: () => setHovered(true),
569
890
  onMouseLeave: () => setHovered(false),
570
891
  onContextMenu: (e) => onContextMenu(e, node),
571
892
  draggable: !isRoot,
572
893
  onDragStart: (e) => {
573
894
  e.dataTransfer.effectAllowed = "move";
895
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
896
+ setMultiDragImage2(e, state.selectedNodeIds.size);
897
+ }
574
898
  onDragStart(node.id);
575
899
  },
576
900
  onDragOver: handleDragOverEvent,
@@ -584,15 +908,16 @@ function TreeNodeRow({
584
908
  display: "flex",
585
909
  alignItems: "center",
586
910
  gap: 6,
587
- padding: "3px 8px",
911
+ padding: "1px 8px",
588
912
  paddingLeft: 8 + depth * 16,
589
913
  cursor: "pointer",
590
914
  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
915
  minHeight: 28,
592
916
  userSelect: "none",
593
917
  opacity: isDraggedNode ? 0.4 : 1,
594
- borderTop,
595
- borderBottom,
918
+ borderTop: `2px solid ${borderTopColor}`,
919
+ borderBottom: `2px solid ${borderBottomColor}`,
920
+ boxSizing: "border-box",
596
921
  color: isSelected && isFocused ? "var(--vj-text-selected, var(--vj-text, #cccccc))" : "var(--vj-text, #cccccc)"
597
922
  },
598
923
  children: [
@@ -675,6 +1000,7 @@ function TreeNodeRow({
675
1000
  showValues,
676
1001
  showCounts,
677
1002
  isFocused,
1003
+ onSelectRange,
678
1004
  onDragStart,
679
1005
  onDragOver,
680
1006
  onDragEnd,
@@ -692,25 +1018,34 @@ function TreeView({
692
1018
  }) {
693
1019
  const { state, actions } = useStudio();
694
1020
  const containerRef = useRef4(null);
1021
+ const visibleNodes = useMemo3(
1022
+ () => getVisibleNodes(state.tree.root, (id) => state.expandedNodeIds.has(id)),
1023
+ [state.tree.root, state.expandedNodeIds]
1024
+ );
695
1025
  const {
696
1026
  dragState,
697
1027
  handleDragStart,
698
1028
  handleDragOver,
699
1029
  handleDragEnd,
700
1030
  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]
1031
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
1032
+ const handleSelectRange = useCallback3(
1033
+ (nodeId) => {
1034
+ actions.setVisibleNodesOverride(visibleNodes);
1035
+ actions.selectNodeRange(nodeId);
1036
+ },
1037
+ [visibleNodes, actions]
706
1038
  );
1039
+ const [contextMenu, setContextMenu] = useState4(null);
707
1040
  const handleContextMenu = useCallback3(
708
1041
  (e, node) => {
709
1042
  e.preventDefault();
710
- actions.selectNode(node.id);
1043
+ if (!state.selectedNodeIds.has(node.id)) {
1044
+ actions.selectAndDrillDown(node.id);
1045
+ }
711
1046
  setContextMenu({ x: e.clientX, y: e.clientY, node });
712
1047
  },
713
- [actions]
1048
+ [actions, state.selectedNodeIds]
714
1049
  );
715
1050
  const buildContextMenuItems = useCallback3(
716
1051
  (node) => {
@@ -789,7 +1124,7 @@ function TreeView({
789
1124
  items.push({
790
1125
  label: "Delete",
791
1126
  action: () => {
792
- const newTree = removeNode(state.tree, node.id);
1127
+ const newTree = removeNode3(state.tree, node.id);
793
1128
  actions.setTree(newTree);
794
1129
  }
795
1130
  });
@@ -801,19 +1136,31 @@ function TreeView({
801
1136
  const handleKeyDown = useCallback3(
802
1137
  (e) => {
803
1138
  const currentIndex = visibleNodes.findIndex(
804
- (n) => n.id === state.selectedNodeId
1139
+ (n) => n.id === state.focusedNodeId
805
1140
  );
806
1141
  switch (e.key) {
807
1142
  case "ArrowDown": {
808
1143
  e.preventDefault();
809
1144
  const next = visibleNodes[currentIndex + 1];
810
- if (next) actions.selectNode(next.id);
1145
+ if (next) {
1146
+ if (e.shiftKey) {
1147
+ handleSelectRange(next.id);
1148
+ } else {
1149
+ actions.selectNode(next.id);
1150
+ }
1151
+ }
811
1152
  break;
812
1153
  }
813
1154
  case "ArrowUp": {
814
1155
  e.preventDefault();
815
1156
  const prev = visibleNodes[currentIndex - 1];
816
- if (prev) actions.selectNode(prev.id);
1157
+ if (prev) {
1158
+ if (e.shiftKey) {
1159
+ handleSelectRange(prev.id);
1160
+ } else {
1161
+ actions.selectNode(prev.id);
1162
+ }
1163
+ }
817
1164
  break;
818
1165
  }
819
1166
  case "ArrowRight": {
@@ -840,17 +1187,47 @@ function TreeView({
840
1187
  }
841
1188
  break;
842
1189
  }
1190
+ case "a": {
1191
+ if (e.metaKey || e.ctrlKey) {
1192
+ e.preventDefault();
1193
+ const ids = computeSelectAllIds(
1194
+ state.tree,
1195
+ state.focusedNodeId,
1196
+ state.selectedNodeIds
1197
+ );
1198
+ if (ids) {
1199
+ actions.setSelection(
1200
+ state.focusedNodeId,
1201
+ ids,
1202
+ state.focusedNodeId
1203
+ );
1204
+ }
1205
+ }
1206
+ break;
1207
+ }
1208
+ case "Escape": {
1209
+ e.preventDefault();
1210
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
1211
+ actions.selectNode(state.focusedNodeId);
1212
+ } else {
1213
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
1214
+ }
1215
+ break;
1216
+ }
843
1217
  case "Delete":
844
1218
  case "Backspace": {
845
1219
  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
- }
1220
+ const { newTree, nextFocusId } = deleteSelectedNodes(
1221
+ state.tree,
1222
+ state.selectedNodeIds,
1223
+ visibleNodes
1224
+ );
1225
+ if (newTree === state.tree) break;
1226
+ actions.setTree(newTree);
1227
+ if (nextFocusId) {
1228
+ actions.selectNode(nextFocusId);
1229
+ } else {
1230
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
854
1231
  }
855
1232
  break;
856
1233
  }
@@ -858,23 +1235,25 @@ function TreeView({
858
1235
  },
859
1236
  [
860
1237
  visibleNodes,
861
- state.selectedNodeId,
1238
+ state.focusedNodeId,
1239
+ state.selectedNodeIds,
862
1240
  state.expandedNodeIds,
863
1241
  state.tree,
864
- actions
1242
+ actions,
1243
+ handleSelectRange
865
1244
  ]
866
1245
  );
867
1246
  const [isFocused, setIsFocused] = useState4(false);
868
1247
  useEffect3(() => {
869
- if (state.selectedNodeId && containerRef.current) {
1248
+ if (state.focusedNodeId && containerRef.current) {
870
1249
  const el = containerRef.current.querySelector(
871
- `[data-node-id="${state.selectedNodeId}"]`
1250
+ `[data-node-id="${state.focusedNodeId}"]`
872
1251
  );
873
1252
  if (el) {
874
1253
  el.scrollIntoView({ block: "nearest" });
875
1254
  }
876
1255
  }
877
- }, [state.selectedNodeId]);
1256
+ }, [state.focusedNodeId]);
878
1257
  return /* @__PURE__ */ jsxs(Fragment, { children: [
879
1258
  /* @__PURE__ */ jsx3(
880
1259
  "div",
@@ -904,6 +1283,7 @@ function TreeView({
904
1283
  showValues,
905
1284
  showCounts,
906
1285
  isFocused,
1286
+ onSelectRange: handleSelectRange,
907
1287
  onDragStart: handleDragStart,
908
1288
  onDragOver: handleDragOver,
909
1289
  onDragEnd: handleDragEnd,
@@ -926,25 +1306,25 @@ function TreeView({
926
1306
  }
927
1307
 
928
1308
  // src/form-view.tsx
929
- import { useState as useState7, useCallback as useCallback6, useRef as useRef7, useEffect as useEffect6, useMemo as useMemo5 } from "react";
1309
+ import { useState as useState7, useCallback as useCallback6, useRef as useRef7, useEffect as useEffect6, useMemo as useMemo6 } from "react";
930
1310
  import {
931
1311
  setValue,
932
1312
  setKey,
933
1313
  addProperty,
934
- removeNode as removeNode2,
1314
+ removeNode as removeNode4,
935
1315
  getPropertySchema as getPropertySchema2,
936
- resolveRef
1316
+ resolveRef as resolveRef2
937
1317
  } from "@visual-json/core";
938
1318
 
939
1319
  // src/breadcrumbs.tsx
940
- import { useState as useState5, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo3 } from "react";
1320
+ import { useState as useState5, useRef as useRef5, useEffect as useEffect4, useCallback as useCallback4, useMemo as useMemo4 } from "react";
941
1321
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
942
1322
  var MAX_SUGGESTIONS = 20;
943
1323
  var DROPDOWN_MAX_HEIGHT = 200;
944
1324
  function Breadcrumbs({ className }) {
945
1325
  const { state, actions } = useStudio();
946
- const selectedNode = state.selectedNodeId ? state.tree.nodesById.get(state.selectedNodeId) : null;
947
- const currentPath = selectedNode?.path ?? "/";
1326
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
1327
+ const currentPath = drillDownNode?.path ?? "/";
948
1328
  const [inputValue, setInputValue] = useState5(currentPath);
949
1329
  const [open, setOpen] = useState5(false);
950
1330
  const [highlightIndex, setHighlightIndex] = useState5(0);
@@ -954,7 +1334,7 @@ function Breadcrumbs({ className }) {
954
1334
  useEffect4(() => {
955
1335
  setInputValue(currentPath);
956
1336
  }, [currentPath]);
957
- const suggestions = useMemo3(() => {
1337
+ const suggestions = useMemo4(() => {
958
1338
  if (!open) return [];
959
1339
  const query = inputValue.toLowerCase();
960
1340
  const matches = [];
@@ -974,7 +1354,7 @@ function Breadcrumbs({ className }) {
974
1354
  (path) => {
975
1355
  for (const [id, node] of state.tree.nodesById) {
976
1356
  if (node.path === path) {
977
- actions.selectNode(id);
1357
+ actions.selectAndDrillDown(id);
978
1358
  break;
979
1359
  }
980
1360
  }
@@ -1062,7 +1442,7 @@ function Breadcrumbs({ className }) {
1062
1442
  style: {
1063
1443
  width: "100%",
1064
1444
  boxSizing: "border-box",
1065
- padding: "2px 0",
1445
+ padding: "3px 0",
1066
1446
  fontSize: "var(--vj-input-font-size, 13px)",
1067
1447
  fontFamily: "var(--vj-font, monospace)",
1068
1448
  color: "var(--vj-text-muted, #999999)",
@@ -1124,7 +1504,7 @@ import {
1124
1504
  useRef as useRef6,
1125
1505
  useEffect as useEffect5,
1126
1506
  useCallback as useCallback5,
1127
- useMemo as useMemo4
1507
+ useMemo as useMemo5
1128
1508
  } from "react";
1129
1509
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1130
1510
  var DROPDOWN_MAX_HEIGHT2 = 200;
@@ -1143,7 +1523,7 @@ function EnumInput({
1143
1523
  useEffect5(() => {
1144
1524
  setInputValue(value);
1145
1525
  }, [value]);
1146
- const suggestions = useMemo4(
1526
+ const suggestions = useMemo5(
1147
1527
  () => enumValues.map((v) => String(v)),
1148
1528
  [enumValues]
1149
1529
  );
@@ -1297,7 +1677,7 @@ function getResolvedSchema(schema, rootSchema, path) {
1297
1677
  if (!schema) return void 0;
1298
1678
  const raw = getPropertySchema2(schema, path, rootSchema);
1299
1679
  if (!raw) return void 0;
1300
- return resolveRef(raw, rootSchema ?? schema);
1680
+ return resolveRef2(raw, rootSchema ?? schema);
1301
1681
  }
1302
1682
  function getValueColor(node) {
1303
1683
  if (node.type === "boolean" || node.type === "null")
@@ -1318,7 +1698,6 @@ function FormField({
1318
1698
  depth,
1319
1699
  showDescriptions,
1320
1700
  showCounts,
1321
- formSelectedNodeId,
1322
1701
  editingNodeId,
1323
1702
  collapsedIds,
1324
1703
  maxKeyLength,
@@ -1336,26 +1715,26 @@ function FormField({
1336
1715
  const { state, actions } = useStudio();
1337
1716
  const isContainer = node.type === "object" || node.type === "array";
1338
1717
  const collapsed = collapsedIds.has(node.id);
1339
- const isSelected = formSelectedNodeId === node.id;
1718
+ const isSelected = state.selectedNodeIds.has(node.id);
1340
1719
  const isEditing = editingNodeId === node.id;
1341
1720
  const propSchema = getResolvedSchema(schema, rootSchema, node.path);
1342
1721
  const isRequired = checkRequired(node, schema, rootSchema);
1343
1722
  const [hovered, setHovered] = useState7(false);
1344
1723
  const isRoot = node.parentId === null;
1345
1724
  const isDragTarget = dragState.dropTargetNodeId === node.id;
1346
- const isDraggedNode = dragState.draggedNodeId === node.id;
1725
+ const isDraggedNode = dragState.draggedNodeIds.has(node.id);
1347
1726
  function handleDragOverEvent(e) {
1348
1727
  e.preventDefault();
1349
1728
  const rect = e.currentTarget.getBoundingClientRect();
1350
1729
  const midY = rect.top + rect.height / 2;
1351
1730
  onDragOver(node.id, e.clientY < midY ? "before" : "after");
1352
1731
  }
1353
- let borderTop = "none";
1354
- let borderBottom = "none";
1732
+ let borderTopColor = "transparent";
1733
+ let borderBottomColor = "transparent";
1355
1734
  if (isDragTarget && dragState.dropPosition === "before") {
1356
- borderTop = "2px solid var(--vj-accent, #007acc)";
1735
+ borderTopColor = "var(--vj-accent, #007acc)";
1357
1736
  } else if (isDragTarget && dragState.dropPosition === "after") {
1358
- borderBottom = "2px solid var(--vj-accent, #007acc)";
1737
+ borderBottomColor = "var(--vj-accent, #007acc)";
1359
1738
  }
1360
1739
  const valueRef = useRef7(null);
1361
1740
  const keyRef = useRef7(null);
@@ -1398,7 +1777,7 @@ function FormField({
1398
1777
  [state.tree, node.id, actions]
1399
1778
  );
1400
1779
  const handleRemove = useCallback6(() => {
1401
- const newTree = removeNode2(state.tree, node.id);
1780
+ const newTree = removeNode4(state.tree, node.id);
1402
1781
  actions.setTree(newTree);
1403
1782
  }, [state.tree, node.id, actions]);
1404
1783
  const handleAddChild = useCallback6(() => {
@@ -1407,7 +1786,6 @@ function FormField({
1407
1786
  actions.setTree(newTree);
1408
1787
  }, [state.tree, node.id, node.type, node.children.length, actions]);
1409
1788
  const description = propSchema?.description;
1410
- const defaultVal = propSchema?.default;
1411
1789
  const isDeprecated = propSchema?.deprecated;
1412
1790
  const fieldTitle = propSchema?.title;
1413
1791
  const parentIsObject = node.parentId && state.tree.nodesById.get(node.parentId)?.type === "object";
@@ -1422,6 +1800,9 @@ function FormField({
1422
1800
  draggable: !isRoot,
1423
1801
  onDragStart: (e) => {
1424
1802
  e.dataTransfer.effectAllowed = "move";
1803
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
1804
+ setMultiDragImage2(e, state.selectedNodeIds.size);
1805
+ }
1425
1806
  onDragStart(node.id);
1426
1807
  },
1427
1808
  onDragOver: handleDragOverEvent,
@@ -1434,20 +1815,21 @@ function FormField({
1434
1815
  display: "flex",
1435
1816
  alignItems: "center",
1436
1817
  gap: 6,
1437
- padding: "3px 8px",
1818
+ padding: "1px 8px",
1438
1819
  paddingLeft: 8 + depth * 16,
1439
1820
  cursor: "pointer",
1440
1821
  backgroundColor: rowBg,
1441
1822
  color: rowColor,
1442
1823
  height: 28,
1824
+ boxSizing: "border-box",
1443
1825
  userSelect: "none",
1444
1826
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1445
- borderTop,
1446
- borderBottom
1827
+ borderTop: `2px solid ${borderTopColor}`,
1828
+ borderBottom: `2px solid ${borderBottomColor}`
1447
1829
  },
1448
1830
  onClick: (e) => {
1449
1831
  e.stopPropagation();
1450
- onSelect(node.id);
1832
+ onSelect(node.id, e);
1451
1833
  },
1452
1834
  onDoubleClick: () => onToggleCollapse(node.id),
1453
1835
  onMouseEnter: () => setHovered(true),
@@ -1503,7 +1885,7 @@ function FormField({
1503
1885
  {
1504
1886
  style: {
1505
1887
  color: !isRoot && !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1506
- fontSize: 13,
1888
+ fontSize: "var(--vj-input-font-size, 13px)",
1507
1889
  fontFamily: "var(--vj-font, monospace)",
1508
1890
  fontWeight: 500,
1509
1891
  flexShrink: 0,
@@ -1604,7 +1986,6 @@ function FormField({
1604
1986
  depth: depth + 1,
1605
1987
  showDescriptions,
1606
1988
  showCounts,
1607
- formSelectedNodeId,
1608
1989
  editingNodeId,
1609
1990
  collapsedIds,
1610
1991
  maxKeyLength,
@@ -1632,6 +2013,9 @@ function FormField({
1632
2013
  draggable: !isRoot,
1633
2014
  onDragStart: (e) => {
1634
2015
  e.dataTransfer.effectAllowed = "move";
2016
+ if (state.selectedNodeIds.size > 1 && state.selectedNodeIds.has(node.id)) {
2017
+ setMultiDragImage2(e, state.selectedNodeIds.size);
2018
+ }
1635
2019
  onDragStart(node.id);
1636
2020
  },
1637
2021
  onDragOver: handleDragOverEvent,
@@ -1644,20 +2028,21 @@ function FormField({
1644
2028
  display: "flex",
1645
2029
  alignItems: "center",
1646
2030
  gap: 6,
1647
- padding: "3px 8px",
2031
+ padding: "1px 8px",
1648
2032
  paddingLeft: 8 + depth * 16,
1649
2033
  cursor: "pointer",
1650
2034
  backgroundColor: rowBg,
1651
2035
  color: rowColor,
1652
2036
  height: 28,
2037
+ boxSizing: "border-box",
1653
2038
  userSelect: "none",
1654
2039
  opacity: isDeprecated ? 0.5 : isDraggedNode ? 0.4 : 1,
1655
- borderTop,
1656
- borderBottom
2040
+ borderTop: `2px solid ${borderTopColor}`,
2041
+ borderBottom: `2px solid ${borderBottomColor}`
1657
2042
  },
1658
2043
  onClick: (e) => {
1659
2044
  e.stopPropagation();
1660
- onSelect(node.id);
2045
+ onSelect(node.id, e);
1661
2046
  },
1662
2047
  onDoubleClick: () => onStartEditing(node.id),
1663
2048
  onMouseEnter: () => setHovered(true),
@@ -1694,7 +2079,7 @@ function FormField({
1694
2079
  {
1695
2080
  style: {
1696
2081
  color: !parentIsObject && !isSelected ? "var(--vj-text-muted, #888888)" : "inherit",
1697
- fontSize: 13,
2082
+ fontSize: "var(--vj-input-font-size, 13px)",
1698
2083
  fontFamily: "var(--vj-font, monospace)",
1699
2084
  flexShrink: 0,
1700
2085
  display: "inline-block",
@@ -1738,7 +2123,7 @@ function FormField({
1738
2123
  {
1739
2124
  style: {
1740
2125
  color: valueColor,
1741
- fontSize: 13,
2126
+ fontSize: "var(--vj-input-font-size, 13px)",
1742
2127
  fontFamily: "var(--vj-font, monospace)",
1743
2128
  overflow: "hidden",
1744
2129
  textOverflow: "ellipsis",
@@ -1816,7 +2201,7 @@ function renderEditInput(node, propSchema, displayValue, handleValueChange, inpu
1816
2201
  style: {
1817
2202
  color: "var(--vj-boolean, #569cd6)",
1818
2203
  fontFamily: "var(--vj-font, monospace)",
1819
- fontSize: 13,
2204
+ fontSize: "var(--vj-input-font-size, 13px)",
1820
2205
  fontStyle: "italic",
1821
2206
  flex: 1
1822
2207
  },
@@ -1849,11 +2234,8 @@ function FormView({
1849
2234
  }) {
1850
2235
  const { state, actions } = useStudio();
1851
2236
  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
- );
2237
+ const drillDownNode = state.drillDownNodeId ? state.tree.nodesById.get(state.drillDownNodeId) : null;
2238
+ const displayNode = drillDownNode ?? state.tree.root;
1857
2239
  const [editingNodeId, setEditingNodeId] = useState7(null);
1858
2240
  const preEditTreeRef = useRef7(null);
1859
2241
  const [collapsedIds, setCollapsedIds] = useState7(
@@ -1861,23 +2243,26 @@ function FormView({
1861
2243
  );
1862
2244
  const containerRef = useRef7(null);
1863
2245
  const [isFocused, setIsFocused] = useState7(false);
1864
- const {
1865
- dragState,
1866
- handleDragStart,
1867
- handleDragOver,
1868
- handleDragEnd,
1869
- handleDrop
1870
- } = useDragDrop();
1871
2246
  useEffect6(() => {
1872
- setFormSelectedNodeId(null);
1873
2247
  setEditingNodeId(null);
1874
2248
  setCollapsedIds(/* @__PURE__ */ new Set());
1875
2249
  }, [displayNode.id]);
1876
- const visibleNodes = useMemo5(
2250
+ const visibleNodes = useMemo6(
1877
2251
  () => getVisibleNodes(displayNode, (id) => !collapsedIds.has(id)),
1878
2252
  [displayNode, collapsedIds]
1879
2253
  );
1880
- const { maxKeyLength, maxDepth } = useMemo5(() => {
2254
+ const {
2255
+ dragState,
2256
+ handleDragStart,
2257
+ handleDragOver,
2258
+ handleDragEnd,
2259
+ handleDrop
2260
+ } = useDragDrop(visibleNodes, state.selectedNodeIds);
2261
+ useEffect6(() => {
2262
+ actions.setVisibleNodesOverride(visibleNodes);
2263
+ return () => actions.setVisibleNodesOverride(null);
2264
+ }, [visibleNodes, actions]);
2265
+ const { maxKeyLength, maxDepth } = useMemo6(() => {
1881
2266
  let maxKey = 1;
1882
2267
  let maxD = 0;
1883
2268
  const baseSegments = displayNode.path === "/" ? 0 : displayNode.path.split("/").filter(Boolean).length;
@@ -1890,10 +2275,20 @@ function FormView({
1890
2275
  }
1891
2276
  return { maxKeyLength: maxKey, maxDepth: maxD };
1892
2277
  }, [visibleNodes, displayNode.path, state.tree]);
1893
- const handleSelect = useCallback6((nodeId) => {
1894
- setFormSelectedNodeId(nodeId);
1895
- setEditingNodeId(null);
1896
- }, []);
2278
+ const handleSelect = useCallback6(
2279
+ (nodeId, e) => {
2280
+ setEditingNodeId(null);
2281
+ if (e.shiftKey) {
2282
+ actions.setVisibleNodesOverride(visibleNodes);
2283
+ actions.selectNodeRange(nodeId);
2284
+ } else if (e.metaKey || e.ctrlKey) {
2285
+ actions.toggleNodeSelection(nodeId);
2286
+ } else {
2287
+ actions.selectNode(nodeId);
2288
+ }
2289
+ },
2290
+ [actions, visibleNodes]
2291
+ );
1897
2292
  const handleToggleCollapse = useCallback6((nodeId) => {
1898
2293
  setCollapsedIds((prev) => {
1899
2294
  const next = new Set(prev);
@@ -1941,15 +2336,23 @@ function FormView({
1941
2336
  }
1942
2337
  return;
1943
2338
  }
1944
- const currentIndex = visibleNodes.findIndex(
1945
- (n) => n.id === formSelectedNodeId
2339
+ let currentIndex = visibleNodes.findIndex(
2340
+ (n) => n.id === state.focusedNodeId
1946
2341
  );
2342
+ if (currentIndex === -1 && visibleNodes.length > 0) {
2343
+ currentIndex = 0;
2344
+ }
1947
2345
  switch (e.key) {
1948
2346
  case "ArrowDown": {
1949
2347
  e.preventDefault();
1950
2348
  const next = visibleNodes[currentIndex + 1];
1951
2349
  if (next) {
1952
- setFormSelectedNodeId(next.id);
2350
+ if (e.shiftKey) {
2351
+ actions.setVisibleNodesOverride(visibleNodes);
2352
+ actions.selectNodeRange(next.id);
2353
+ } else {
2354
+ actions.selectNode(next.id);
2355
+ }
1953
2356
  scrollToNode(next.id);
1954
2357
  }
1955
2358
  break;
@@ -1958,7 +2361,12 @@ function FormView({
1958
2361
  e.preventDefault();
1959
2362
  const prev = visibleNodes[currentIndex - 1];
1960
2363
  if (prev) {
1961
- setFormSelectedNodeId(prev.id);
2364
+ if (e.shiftKey) {
2365
+ actions.setVisibleNodesOverride(visibleNodes);
2366
+ actions.selectNodeRange(prev.id);
2367
+ } else {
2368
+ actions.selectNode(prev.id);
2369
+ }
1962
2370
  scrollToNode(prev.id);
1963
2371
  }
1964
2372
  break;
@@ -1974,7 +2382,7 @@ function FormView({
1974
2382
  return next;
1975
2383
  });
1976
2384
  } else if (node.children.length > 0) {
1977
- setFormSelectedNodeId(node.children[0].id);
2385
+ actions.selectNode(node.children[0].id);
1978
2386
  scrollToNode(node.children[0].id);
1979
2387
  }
1980
2388
  }
@@ -1996,7 +2404,7 @@ function FormView({
1996
2404
  (n) => n.id === current.parentId
1997
2405
  );
1998
2406
  if (parentInVisible) {
1999
- setFormSelectedNodeId(parentInVisible.id);
2407
+ actions.selectNode(parentInVisible.id);
2000
2408
  scrollToNode(parentInVisible.id);
2001
2409
  }
2002
2410
  }
@@ -2004,30 +2412,54 @@ function FormView({
2004
2412
  }
2005
2413
  case "Enter": {
2006
2414
  e.preventDefault();
2007
- if (formSelectedNodeId) {
2415
+ if (state.focusedNodeId) {
2008
2416
  preEditTreeRef.current = state.tree;
2009
- setEditingNodeId(formSelectedNodeId);
2417
+ actions.selectNode(state.focusedNodeId);
2418
+ setEditingNodeId(state.focusedNodeId);
2419
+ }
2420
+ break;
2421
+ }
2422
+ case "a": {
2423
+ if (e.metaKey || e.ctrlKey) {
2424
+ e.preventDefault();
2425
+ const ids = computeSelectAllIds(
2426
+ state.tree,
2427
+ state.focusedNodeId,
2428
+ state.selectedNodeIds
2429
+ );
2430
+ if (ids) {
2431
+ actions.setSelection(
2432
+ state.focusedNodeId,
2433
+ ids,
2434
+ state.focusedNodeId
2435
+ );
2436
+ }
2010
2437
  }
2011
2438
  break;
2012
2439
  }
2013
2440
  case "Escape": {
2014
2441
  e.preventDefault();
2015
- setEditingNodeId(null);
2442
+ if (state.selectedNodeIds.size > 1 && state.focusedNodeId) {
2443
+ actions.selectNode(state.focusedNodeId);
2444
+ } else {
2445
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2446
+ }
2016
2447
  break;
2017
2448
  }
2018
2449
  case "Delete":
2019
2450
  case "Backspace": {
2020
2451
  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
- }
2452
+ const { newTree, nextFocusId } = deleteSelectedNodes(
2453
+ state.tree,
2454
+ state.selectedNodeIds,
2455
+ visibleNodes
2456
+ );
2457
+ if (newTree === state.tree) break;
2458
+ actions.setTree(newTree);
2459
+ if (nextFocusId) {
2460
+ actions.selectNode(nextFocusId);
2461
+ } else {
2462
+ actions.setSelection(null, /* @__PURE__ */ new Set(), null);
2031
2463
  }
2032
2464
  break;
2033
2465
  }
@@ -2035,7 +2467,8 @@ function FormView({
2035
2467
  },
2036
2468
  [
2037
2469
  visibleNodes,
2038
- formSelectedNodeId,
2470
+ state.focusedNodeId,
2471
+ state.selectedNodeIds,
2039
2472
  editingNodeId,
2040
2473
  collapsedIds,
2041
2474
  scrollToNode,
@@ -2060,9 +2493,11 @@ function FormView({
2060
2493
  "div",
2061
2494
  {
2062
2495
  style: {
2496
+ display: "flex",
2497
+ alignItems: "center",
2063
2498
  padding: "4px 8px",
2064
2499
  borderBottom: "1px solid var(--vj-border, #333333)",
2065
- backgroundColor: "var(--vj-bg-panel, #252526)",
2500
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2066
2501
  flexShrink: 0
2067
2502
  },
2068
2503
  children: /* @__PURE__ */ jsx6(Breadcrumbs, {})
@@ -2095,7 +2530,6 @@ function FormView({
2095
2530
  depth: 0,
2096
2531
  showDescriptions,
2097
2532
  showCounts,
2098
- formSelectedNodeId,
2099
2533
  editingNodeId,
2100
2534
  collapsedIds,
2101
2535
  maxKeyLength,
@@ -2165,7 +2599,7 @@ function SearchBar({ className }) {
2165
2599
  alignItems: "center",
2166
2600
  gap: 6,
2167
2601
  padding: "4px 8px",
2168
- backgroundColor: "var(--vj-bg-panel, #252526)",
2602
+ backgroundColor: "var(--vj-bg, #1e1e1e)",
2169
2603
  borderBottom: "1px solid var(--vj-border, #333333)"
2170
2604
  },
2171
2605
  children: [
@@ -2372,31 +2806,6 @@ function SearchBar({ className }) {
2372
2806
 
2373
2807
  // src/json-editor.tsx
2374
2808
  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
2809
  function JsonEditor({
2401
2810
  value,
2402
2811
  defaultValue,
@@ -2723,461 +3132,13 @@ function EditorLayout({
2723
3132
  ] });
2724
3133
  }
2725
3134
 
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
3135
  // src/diff-view.tsx
3157
- import { useMemo as useMemo6 } from "react";
3136
+ import { useMemo as useMemo7 } from "react";
3158
3137
  import { computeDiff } from "@visual-json/core";
3159
- import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
3160
- var DIFF_COLORS = {
3161
- added: { bg: "#1e3a1e", marker: "+", label: "#4ec94e" },
3162
- removed: { bg: "#3a1e1e", marker: "-", label: "#f48771" },
3163
- changed: { bg: "#3a3a1e", marker: "~", label: "#dcdcaa" }
3164
- };
3165
- function formatValue(value) {
3166
- if (value === void 0) return "";
3167
- if (value === null) return "null";
3168
- if (typeof value === "string") return JSON.stringify(value);
3169
- if (typeof value === "object") {
3170
- const json = JSON.stringify(value, null, 2);
3171
- if (json.length > 80) {
3172
- return JSON.stringify(value).slice(0, 77) + "...";
3173
- }
3174
- return json;
3175
- }
3176
- return String(value);
3177
- }
3138
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
3178
3139
  function DiffRow({ entry }) {
3179
3140
  const colors = DIFF_COLORS[entry.type];
3180
- return /* @__PURE__ */ jsxs8(
3141
+ return /* @__PURE__ */ jsxs7(
3181
3142
  "div",
3182
3143
  {
3183
3144
  style: {
@@ -3191,7 +3152,7 @@ function DiffRow({ entry }) {
3191
3152
  gap: 8
3192
3153
  },
3193
3154
  children: [
3194
- /* @__PURE__ */ jsx10(
3155
+ /* @__PURE__ */ jsx9(
3195
3156
  "span",
3196
3157
  {
3197
3158
  style: {
@@ -3204,7 +3165,7 @@ function DiffRow({ entry }) {
3204
3165
  children: colors.marker
3205
3166
  }
3206
3167
  ),
3207
- /* @__PURE__ */ jsx10(
3168
+ /* @__PURE__ */ jsx9(
3208
3169
  "span",
3209
3170
  {
3210
3171
  style: {
@@ -3215,14 +3176,14 @@ function DiffRow({ entry }) {
3215
3176
  children: entry.path
3216
3177
  }
3217
3178
  ),
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) })
3179
+ /* @__PURE__ */ jsxs7("span", { style: { flex: 1, display: "flex", gap: 8, overflow: "hidden" }, children: [
3180
+ entry.type === "changed" && /* @__PURE__ */ jsxs7(Fragment3, { children: [
3181
+ /* @__PURE__ */ jsx9("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) }),
3182
+ /* @__PURE__ */ jsx9("span", { style: { color: "var(--vj-text-dim, #666666)" }, children: "\u2192" }),
3183
+ /* @__PURE__ */ jsx9("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) })
3223
3184
  ] }),
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) })
3185
+ entry.type === "added" && /* @__PURE__ */ jsx9("span", { style: { color: "#4ec94e" }, children: formatValue(entry.newValue) }),
3186
+ entry.type === "removed" && /* @__PURE__ */ jsx9("span", { style: { color: "#f48771", textDecoration: "line-through" }, children: formatValue(entry.oldValue) })
3226
3187
  ] })
3227
3188
  ]
3228
3189
  }
@@ -3233,14 +3194,14 @@ function DiffView({
3233
3194
  currentJson,
3234
3195
  className
3235
3196
  }) {
3236
- const entries = useMemo6(
3197
+ const entries = useMemo7(
3237
3198
  () => computeDiff(originalJson, currentJson),
3238
3199
  [originalJson, currentJson]
3239
3200
  );
3240
3201
  const added = entries.filter((e) => e.type === "added").length;
3241
3202
  const removed = entries.filter((e) => e.type === "removed").length;
3242
3203
  const changed = entries.filter((e) => e.type === "changed").length;
3243
- return /* @__PURE__ */ jsxs8(
3204
+ return /* @__PURE__ */ jsxs7(
3244
3205
  "div",
3245
3206
  {
3246
3207
  className,
@@ -3253,7 +3214,7 @@ function DiffView({
3253
3214
  flexDirection: "column"
3254
3215
  },
3255
3216
  children: [
3256
- /* @__PURE__ */ jsxs8(
3217
+ /* @__PURE__ */ jsxs7(
3257
3218
  "div",
3258
3219
  {
3259
3220
  style: {
@@ -3268,18 +3229,18 @@ function DiffView({
3268
3229
  flexShrink: 0
3269
3230
  },
3270
3231
  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: [
3232
+ /* @__PURE__ */ jsx9("span", { style: { color: "var(--vj-text-muted, #999999)" }, children: entries.length === 0 ? "No changes" : `${entries.length} changes` }),
3233
+ added > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#4ec94e" }, children: [
3273
3234
  "+",
3274
3235
  added,
3275
3236
  " added"
3276
3237
  ] }),
3277
- removed > 0 && /* @__PURE__ */ jsxs8("span", { style: { color: "#f48771" }, children: [
3238
+ removed > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#f48771" }, children: [
3278
3239
  "-",
3279
3240
  removed,
3280
3241
  " removed"
3281
3242
  ] }),
3282
- changed > 0 && /* @__PURE__ */ jsxs8("span", { style: { color: "#dcdcaa" }, children: [
3243
+ changed > 0 && /* @__PURE__ */ jsxs7("span", { style: { color: "#dcdcaa" }, children: [
3283
3244
  "~",
3284
3245
  changed,
3285
3246
  " modified"
@@ -3287,7 +3248,7 @@ function DiffView({
3287
3248
  ]
3288
3249
  }
3289
3250
  ),
3290
- /* @__PURE__ */ jsx10("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ jsx10(
3251
+ /* @__PURE__ */ jsx9("div", { style: { flex: 1, overflow: "auto" }, children: entries.length === 0 ? /* @__PURE__ */ jsx9(
3291
3252
  "div",
3292
3253
  {
3293
3254
  style: {
@@ -3301,7 +3262,7 @@ function DiffView({
3301
3262
  },
3302
3263
  children: "No differences detected"
3303
3264
  }
3304
- ) : entries.map((entry, i) => /* @__PURE__ */ jsx10(DiffRow, { entry }, i)) })
3265
+ ) : entries.map((entry, i) => /* @__PURE__ */ jsx9(DiffRow, { entry }, i)) })
3305
3266
  ]
3306
3267
  }
3307
3268
  );
@@ -3312,7 +3273,6 @@ export {
3312
3273
  DiffView,
3313
3274
  FormView,
3314
3275
  JsonEditor,
3315
- PropertyEditor,
3316
3276
  SearchBar,
3317
3277
  StudioContext,
3318
3278
  TreeView,