RobinsAIWorld commited on
Commit
6c6c4eb
·
verified ·
1 Parent(s): 949d7d1

🐳 10/02 - 13:23 - Also, could you make the little highlights that appear on the active line, be colour-coded for each indent level, and alwayds show, with 'star trek: tng LCARS-type' lines joining sa

Browse files
Files changed (2) hide show
  1. index.html +383 -216
  2. style.css +101 -0
index.html CHANGED
@@ -25,73 +25,52 @@
25
  min-height: 500px;
26
  max-height: 70vh;
27
  overflow-y: auto;
28
- background-color: #1e293b;
29
- background-image: radial-gradient(#334155 1px, transparent 1px);
 
 
30
  background-size: 20px 20px;
31
  padding: 20px;
32
  border-radius: 8px;
 
33
  }
34
  .json-item {
35
  transition: all 0.2s ease;
36
- margin: 5px 0;
37
  border-radius: 6px;
38
- background-color: #334155;
39
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
40
  position: relative;
41
- display: block;
 
42
  }
43
  .json-item-content {
44
- padding: 8px 12px;
45
  margin-left: 0;
46
- display: inline-block;
 
47
  min-width: 200px;
48
- border-left: 3px solid transparent;
49
  }
50
  .json-item:hover .json-item-content {
51
- background-color: #475569;
52
- border-left: 3px solid #60a5fa;
53
  }
54
- .json-item.selected .json-item-content {
55
- background-color: #475569;
56
- border-left: 3px solid #60a5fa;
57
- box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3);
58
  }
59
  .json-item.editing .json-item-content {
60
- background-color: #64748b;
61
- border-left: 3px solid #fbbf24;
62
- box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.5);
63
- }
64
- .json-item:hover {
65
- background-color: #475569;
66
- border-left: 3px solid #60a5fa;
67
- }
68
- .json-item.selected {
69
- background-color: #475569;
70
- border-left: 3px solid #60a5fa;
71
- box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.3);
72
- }
73
- .json-item.editing {
74
- background-color: #64748b;
75
- border-left: 3px solid #fbbf24;
76
- box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.5);
77
- }
78
- .json-item.dragging {
79
- opacity: 0.5;
80
- background-color: #475569;
81
- }
82
- .json-item.drag-over {
83
- border-top: 2px dashed #60a5fa;
84
  }
85
  .json-key {
86
- font-weight: 600;
87
  color: #93c5fd;
88
- margin-right: 8px;
89
  }
90
  .json-value {
91
  color: #6ee7b7;
92
  }
93
  .json-bracket {
94
- color: #94a3b8;
 
95
  }
96
  .btn {
97
  transition: all 0.2s ease;
@@ -104,22 +83,7 @@
104
  left: 0;
105
  top: 0;
106
  bottom: 0;
107
- width: 1px;
108
- background-color: #475569;
109
- }
110
- .cursor-pointer {
111
- cursor: pointer;
112
- }
113
- .editable:focus {
114
- outline: 2px solid #60a5fa;
115
- border-radius: 4px;
116
- }
117
- .editable {
118
- min-width: 20px;
119
- display: inline-block;
120
- background-color: rgba(96, 165, 250, 0.1);
121
- padding: 2px 4px;
122
- border-radius: 4px;
123
  }
124
  .toolbar-btn {
125
  transition: all 0.2s;
@@ -139,7 +103,7 @@
139
  transform: translateX(0);
140
  }
141
  .error-highlight {
142
- background-color: #fee2e2;
143
  border-left: 3px solid #ef4444;
144
  }
145
  .menu-bar {
@@ -227,6 +191,20 @@
227
  border-radius: 4px;
228
  padding: 2px 4px;
229
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </style>
231
  </head>
232
  <body class="bg-slate-900 min-h-screen p-4 md:p-8">
@@ -334,7 +312,7 @@
334
  <div class="bg-slate-700 rounded-lg p-4 h-full">
335
  <div class="flex justify-between items-center mb-3">
336
  <h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
337
- <span class="text-xs text-slate-400 bg-slate-800 px-2 py-1 rounded">Double-click to edit</span>
338
  </div>
339
  <div id="jsonEditor" class="json-editor border rounded-lg p-4 font-mono min-h-[500px] max-h-[600px] overflow-auto">
340
  <!-- JSON content will be rendered here -->
@@ -347,7 +325,8 @@
347
  <div class="bg-gray-50 rounded-lg p-4 h-full">
348
  <div class="flex justify-between items-center mb-3">
349
  <h2 class="text-lg font-semibold text-gray-700">Code Editor</h2>
350
- <div class="flex gap-2">
 
351
  <button id="applyCodeBtn" class="btn bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-lg text-sm">
352
  <i class="fas fa-check mr-1"></i> Apply
353
  </button>
@@ -385,17 +364,17 @@
385
  </div>
386
  <div class="bg-green-50 p-4 rounded-lg border border-green-200">
387
  <div class="text-green-500 text-2xl mb-2">
388
- <i class="fas fa-sync-alt"></i>
389
  </div>
390
- <h3 class="font-bold text-lg mb-2">Real-time Sync</h3>
391
- <p class="text-gray-700">Changes in the visual editor are immediately reflected in the JSON output, and vice versa. Validate your JSON at any time.</p>
392
  </div>
393
  <div class="bg-purple-50 p-4 rounded-lg border border-purple-200">
394
  <div class="text-purple-500 text-2xl mb-2">
395
- <i class="fas fa-robot"></i>
396
  </div>
397
- <h3 class="font-bold text-lg mb-2">Auto-Repair</h3>
398
- <p class="text-gray-700">Paste malformed JSON and our editor will attempt to automatically repair common syntax errors and structural issues.</p>
399
  </div>
400
  </div>
401
  </div>
@@ -444,6 +423,13 @@
444
  const downloadBtn = document.getElementById('downloadBtn');
445
  const applyCodeBtn = document.getElementById('applyCodeBtn');
446
  const validationStatus = document.getElementById('validationStatus');
 
 
 
 
 
 
 
447
  let isApplyingChanges = false;
448
 
449
  // Current state
@@ -451,6 +437,17 @@
451
  let selectedElement = null;
452
  let history = [];
453
  let historyIndex = -1;
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  // Initialize with sample data
456
  loadJSON(sampleJSON);
@@ -518,36 +515,81 @@
518
  // Render the JSON editor
519
  function renderEditor() {
520
  jsonEditor.innerHTML = '';
 
 
521
  renderElement(jsonEditor, jsonData, 0, 'root');
522
  }
523
 
524
  // Render a single JSON element
525
  function renderElement(container, data, depth, key = null, parentKey = null) {
 
526
  const wrapper = document.createElement('div');
527
- wrapper.className = 'json-item relative';
528
  wrapper.dataset.key = key;
529
  wrapper.dataset.parent = parentKey;
530
  wrapper.dataset.depth = depth;
 
531
 
532
- // Add indent lines
533
- if (depth > 0) {
534
- const indentLine = document.createElement('div');
535
- indentLine.className = 'indent-line';
536
- indentLine.style.left = `${depth * 20}px`;
537
- wrapper.appendChild(indentLine);
538
- }
 
 
 
 
539
 
540
  // Create element content
541
  const content = document.createElement('div');
542
  content.className = 'json-item-content flex items-start py-1';
543
- content.style.marginLeft = `${depth * 20 + 8}px`;
544
 
545
- // Key
546
  if (key !== null && key !== 'root') {
547
- const keyElement = document.createElement('span');
548
- keyElement.className = 'json-key mr-2';
549
- keyElement.textContent = `"${key}": `;
550
- content.appendChild(keyElement);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  }
552
 
553
  // Value or children
@@ -555,7 +597,7 @@
555
  if (Array.isArray(data)) {
556
  // Array
557
  const bracket = document.createElement('span');
558
- bracket.className = 'json-bracket';
559
  bracket.textContent = '[';
560
  content.appendChild(bracket);
561
 
@@ -569,23 +611,22 @@
569
 
570
  // Closing bracket
571
  const closingWrapper = document.createElement('div');
572
- closingWrapper.className = 'json-item relative';
573
  closingWrapper.dataset.key = 'closing';
574
  closingWrapper.dataset.parent = key;
575
  closingWrapper.dataset.depth = depth;
 
576
 
577
- if (depth > 0) {
578
- const indentLine = document.createElement('div');
579
- indentLine.className = 'indent-line';
580
- indentLine.style.left = `${depth * 20}px`;
581
- closingWrapper.appendChild(indentLine);
582
- }
583
 
584
  const closingContent = document.createElement('div');
585
  closingContent.className = 'json-item-content flex items-start py-1';
586
- closingContent.style.marginLeft = `${depth * 20 + 8}px`;
587
  const closingBracket = document.createElement('span');
588
- closingBracket.className = 'json-bracket';
589
  closingBracket.textContent = ']';
590
  closingContent.appendChild(closingBracket);
591
  closingWrapper.appendChild(closingContent);
@@ -593,7 +634,7 @@
593
  } else {
594
  // Object
595
  const bracket = document.createElement('span');
596
- bracket.className = 'json-bracket';
597
  bracket.textContent = '{';
598
  content.appendChild(bracket);
599
 
@@ -607,177 +648,290 @@
607
 
608
  // Closing bracket
609
  const closingWrapper = document.createElement('div');
610
- closingWrapper.className = 'json-item relative';
611
  closingWrapper.dataset.key = 'closing';
612
  closingWrapper.dataset.parent = key;
613
  closingWrapper.dataset.depth = depth;
 
614
 
615
- if (depth > 0) {
616
- const indentLine = document.createElement('div');
617
- indentLine.className = 'indent-line';
618
- indentLine.style.left = `${depth * 20}px`;
619
- closingWrapper.appendChild(indentLine);
620
- }
621
 
622
  const closingContent = document.createElement('div');
623
  closingContent.className = 'json-item-content flex items-start py-1';
624
- closingContent.style.marginLeft = `${depth * 20 + 8}px`;
625
  const closingBracket = document.createElement('span');
626
- closingBracket.className = 'json-bracket';
627
  closingBracket.textContent = '}';
628
  closingContent.appendChild(closingBracket);
629
  closingWrapper.appendChild(closingContent);
630
  container.appendChild(closingWrapper);
631
  }
632
  } else {
633
- // Primitive value
634
- const valueElement = document.createElement('span');
635
- valueElement.className = 'json-value';
 
 
 
 
 
636
 
637
  if (typeof data === 'string') {
638
- valueElement.textContent = `"${data}"`;
639
  } else if (typeof data === 'boolean') {
640
- valueElement.textContent = data.toString();
 
 
641
  } else {
642
- valueElement.textContent = data;
643
  }
644
 
645
- content.appendChild(valueElement);
646
  wrapper.appendChild(content);
647
  container.appendChild(wrapper);
648
- }
649
-
650
- // Add event listeners for editing
651
- if (wrapper.dataset.key !== 'closing') {
652
- wrapper.addEventListener('dblclick', () => editElement(wrapper));
653
- wrapper.addEventListener('click', () => selectElement(wrapper));
654
- }
655
- }
656
-
657
- // Edit an element
658
- function editElement(element) {
659
- if (element.dataset.key === 'closing') return;
660
-
661
- const keyElement = element.querySelector('.json-key');
662
- const valueElement = element.querySelector('.json-value');
663
-
664
- if (keyElement) {
665
- const keyText = keyElement.textContent.replace(/"/g, '').replace(':', '').trim();
666
- const input = document.createElement('input');
667
- input.type = 'text';
668
- input.className = 'editable bg-yellow-100 px-1';
669
- input.value = keyText;
670
- keyElement.replaceWith(input);
671
- input.focus();
672
 
673
- input.addEventListener('blur', () => {
674
- const newKey = input.value;
675
- updateKey(element, newKey);
676
- input.replaceWith(keyElement);
677
- keyElement.textContent = `"${newKey}": `;
 
 
678
  });
679
 
680
- input.addEventListener('keydown', (e) => {
681
- if (e.key === 'Enter') {
682
- input.blur();
683
- }
684
  });
685
- }
686
-
687
- if (valueElement) {
688
- const valueText = valueElement.textContent.replace(/"/g, '');
689
- const input = document.createElement('input');
690
- input.type = 'text';
691
- input.className = 'editable bg-yellow-100 px-1';
692
- input.value = valueText;
693
- valueElement.replaceWith(input);
694
- input.focus();
695
 
696
- input.addEventListener('blur', () => {
697
- const newValue = parseValue(input.value);
698
- updateValue(element, newValue);
699
- input.replaceWith(valueElement);
700
- valueElement.textContent = formatValue(newValue);
701
  });
702
 
703
- input.addEventListener('keydown', (e) => {
704
- if (e.key === 'Enter') {
705
- input.blur();
706
- }
707
  });
 
 
708
  }
709
  }
710
 
711
- // Parse a value from string to appropriate type
712
- function parseValue(value) {
713
- if (value === 'true') return true;
714
- if (value === 'false') return false;
715
- if (value === 'null') return null;
716
- if (!isNaN(value) && value.trim() !== '') return Number(value);
717
- return value;
718
  }
719
 
720
- // Format a value for display
721
- function formatValue(value) {
722
- if (typeof value === 'string') return `"${value}"`;
723
- if (typeof value === 'boolean') return value.toString();
724
- if (value === null) return 'null';
725
- return value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  }
727
 
728
- // Update a key
729
- function updateKey(element, newKey) {
730
- const parentKey = element.dataset.parent;
731
- const oldKey = element.dataset.key;
732
 
733
- if (parentKey === 'root') {
734
- const newData = {};
735
- Object.keys(jsonData).forEach(key => {
736
- if (key === oldKey) {
737
- newData[newKey] = jsonData[key];
738
- } else if (key !== newKey) {
739
- newData[key] = jsonData[key];
 
 
 
 
 
 
 
740
  }
 
741
  });
 
 
 
 
 
 
742
  jsonData = newData;
743
  } else {
744
- // Find parent in nested structure
745
- const parent = findElementByKey(jsonData, parentKey);
746
- if (parent && typeof parent === 'object') {
747
- const newData = {};
748
- Object.keys(parent).forEach(key => {
749
- if (key === oldKey) {
750
- newData[newKey] = parent[key];
751
- } else if (key !== newKey) {
752
- newData[key] = parent[key];
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  }
 
754
  });
755
- Object.keys(parent).forEach(key => delete parent[key]);
756
- Object.assign(parent, newData);
757
  }
758
  }
759
 
760
- element.dataset.key = newKey;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
  updateOutput();
762
  saveToHistory();
 
763
  }
764
 
765
- // Update a value
766
- function updateValue(element, newValue) {
767
- const key = element.dataset.key;
768
- const parentKey = element.dataset.parent;
 
 
 
 
 
 
769
 
770
  if (parentKey === 'root') {
771
- jsonData[key] = newValue;
772
  } else {
773
  const parent = findElementByKey(jsonData, parentKey);
774
  if (parent && typeof parent === 'object') {
775
- parent[key] = newValue;
776
  }
777
  }
778
 
779
  updateOutput();
780
  saveToHistory();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  }
782
 
783
  // Find an element by key in nested structure
@@ -794,23 +948,14 @@
794
  return null;
795
  }
796
 
797
- // Select an element
798
- function selectElement(element) {
799
- if (selectedElement) {
800
- selectedElement.classList.remove('selected');
801
- }
802
-
803
- element.classList.add('selected');
804
- selectedElement = element;
805
- }
806
-
807
- // Update JSON output
808
  function updateOutput() {
809
  if (isApplyingChanges) return;
810
  try {
811
- jsonOutput.value = JSON.stringify(jsonData, null, 2);
 
812
  jsonOutput.classList.remove('error-highlight');
813
- jsonOutput.style.borderColor = '#d1d5db';
814
  validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
815
  } catch (e) {
816
  jsonOutput.value = 'Invalid JSON structure';
@@ -860,6 +1005,17 @@
860
  }
861
  });
862
 
 
 
 
 
 
 
 
 
 
 
 
863
  // Show notification
864
  function showNotification(message) {
865
  const notificationContent = notification.querySelector('p');
@@ -878,6 +1034,17 @@
878
  history = history.slice(0, historyIndex + 1);
879
  }
880
 
 
 
 
 
 
 
 
 
 
 
 
881
  history.push(JSON.parse(JSON.stringify(jsonData)));
882
  historyIndex = history.length - 1;
883
  }
@@ -1062,7 +1229,7 @@
1062
 
1063
  // Instructions button
1064
  document.getElementById('instructionsBtn').addEventListener('click', () => {
1065
- showNotification('Visual: Double-click to edit | Code: Type and click Apply or Ctrl+Enter');
1066
  });
1067
 
1068
  // Sample JSON button
 
25
  min-height: 500px;
26
  max-height: 70vh;
27
  overflow-y: auto;
28
+ background-color: #0f172a;
29
+ background-image:
30
+ linear-gradient(rgba(30, 41, 59, 0.5) 1px, transparent 1px),
31
+ linear-gradient(90deg, rgba(30, 41, 59, 0.5) 1px, transparent 1px);
32
  background-size: 20px 20px;
33
  padding: 20px;
34
  border-radius: 8px;
35
+ position: relative;
36
  }
37
  .json-item {
38
  transition: all 0.2s ease;
39
+ margin: 4px 0;
40
  border-radius: 6px;
41
+ background-color: rgba(30, 41, 59, 0.6);
 
42
  position: relative;
43
+ display: flex;
44
+ align-items: center;
45
  }
46
  .json-item-content {
47
+ padding: 6px 12px;
48
  margin-left: 0;
49
+ display: inline-flex;
50
+ align-items: center;
51
  min-width: 200px;
52
+ gap: 8px;
53
  }
54
  .json-item:hover .json-item-content {
55
+ background-color: rgba(51, 65, 85, 0.5);
 
56
  }
57
+ .json-item.active .json-item-content {
58
+ background-color: rgba(51, 65, 85, 0.7);
 
 
59
  }
60
  .json-item.editing .json-item-content {
61
+ background-color: rgba(100, 116, 139, 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
  .json-key {
64
+ font-weight: 500;
65
  color: #93c5fd;
66
+ margin-right: 4px;
67
  }
68
  .json-value {
69
  color: #6ee7b7;
70
  }
71
  .json-bracket {
72
+ font-weight: bold;
73
+ font-family: monospace;
74
  }
75
  .btn {
76
  transition: all 0.2s ease;
 
83
  left: 0;
84
  top: 0;
85
  bottom: 0;
86
+ width: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
  .toolbar-btn {
89
  transition: all 0.2s;
 
103
  transform: translateX(0);
104
  }
105
  .error-highlight {
106
+ background-color: rgba(239, 68, 68, 0.1);
107
  border-left: 3px solid #ef4444;
108
  }
109
  .menu-bar {
 
191
  border-radius: 4px;
192
  padding: 2px 4px;
193
  }
194
+
195
+ /* Code Editor bracket coloring */
196
+ #jsonOutput {
197
+ line-height: 1.6;
198
+ }
199
+
200
+ .bracket-layer-0 { color: #ef4444; }
201
+ .bracket-layer-1 { color: #f97316; }
202
+ .bracket-layer-2 { color: #eab308; }
203
+ .bracket-layer-3 { color: #22c55e; }
204
+ .bracket-layer-4 { color: #06b6d4; }
205
+ .bracket-layer-5 { color: #3b82f6; }
206
+ .bracket-layer-6 { color: #8b5cf6; }
207
+ .bracket-layer-7 { color: #ec4899; }
208
  </style>
209
  </head>
210
  <body class="bg-slate-900 min-h-screen p-4 md:p-8">
 
312
  <div class="bg-slate-700 rounded-lg p-4 h-full">
313
  <div class="flex justify-between items-center mb-3">
314
  <h2 class="text-lg font-semibold text-slate-200">Visual Editor</h2>
315
+ <span class="text-xs text-slate-400 bg-slate-800 px-2 py-1 rounded">Single-click to edit • Tab to navigate • Shift+Enter for new field</span>
316
  </div>
317
  <div id="jsonEditor" class="json-editor border rounded-lg p-4 font-mono min-h-[500px] max-h-[600px] overflow-auto">
318
  <!-- JSON content will be rendered here -->
 
325
  <div class="bg-gray-50 rounded-lg p-4 h-full">
326
  <div class="flex justify-between items-center mb-3">
327
  <h2 class="text-lg font-semibold text-gray-700">Code Editor</h2>
328
+ <div class="flex gap-2 items-center">
329
+ <span id="autoSaveStatus" class="text-xs text-green-600 hidden"><i class="fas fa-check-circle mr-1"></i>Auto-saved</span>
330
  <button id="applyCodeBtn" class="btn bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-lg text-sm">
331
  <i class="fas fa-check mr-1"></i> Apply
332
  </button>
 
364
  </div>
365
  <div class="bg-green-50 p-4 rounded-lg border border-green-200">
366
  <div class="text-green-500 text-2xl mb-2">
367
+ <i class="fas fa-keyboard"></i>
368
  </div>
369
+ <h3 class="font-bold text-lg mb-2">Keyboard Navigation</h3>
370
+ <p class="text-gray-700">Use Tab/Shift+Tab to navigate fields, Enter to move forward, Shift+Enter to insert new fields. Everything auto-saves!</p>
371
  </div>
372
  <div class="bg-purple-50 p-4 rounded-lg border border-purple-200">
373
  <div class="text-purple-500 text-2xl mb-2">
374
+ <i class="fas fa-undo"></i>
375
  </div>
376
+ <h3 class="font-bold text-lg mb-2">50-Step Undo History</h3>
377
+ <p class="text-gray-700">Made a mistake? No problem. With 50 levels of undo history, you can always go back. Auto-save protects your work.</p>
378
  </div>
379
  </div>
380
  </div>
 
423
  const downloadBtn = document.getElementById('downloadBtn');
424
  const applyCodeBtn = document.getElementById('applyCodeBtn');
425
  const validationStatus = document.getElementById('validationStatus');
426
+
427
+ // Create saved indicator
428
+ const savedIndicator = document.createElement('div');
429
+ savedIndicator.className = 'saved-indicator';
430
+ savedIndicator.innerHTML = '<i class="fas fa-check-circle mr-2"></i>Saved';
431
+ document.body.appendChild(savedIndicator);
432
+
433
  let isApplyingChanges = false;
434
 
435
  // Current state
 
437
  let selectedElement = null;
438
  let history = [];
439
  let historyIndex = -1;
440
+ const MAX_HISTORY = 50; // Increased undo history
441
+
442
+ // Track all editable fields for TAB navigation
443
+ let editableFields = [];
444
+ let currentFieldIndex = -1;
445
+
446
+ // Layer colors for visual distinction
447
+ const layerColors = [
448
+ '#ef4444', '#f97316', '#eab308', '#22c55e',
449
+ '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'
450
+ ];
451
 
452
  // Initialize with sample data
453
  loadJSON(sampleJSON);
 
515
  // Render the JSON editor
516
  function renderEditor() {
517
  jsonEditor.innerHTML = '';
518
+ editableFields = []; // Clear tracked fields
519
+ currentFieldIndex = -1;
520
  renderElement(jsonEditor, jsonData, 0, 'root');
521
  }
522
 
523
  // Render a single JSON element
524
  function renderElement(container, data, depth, key = null, parentKey = null) {
525
+ const layerClass = `lcars-layer-${Math.min(depth, 7)}`;
526
  const wrapper = document.createElement('div');
527
+ wrapper.className = `json-item relative ${layerClass}`;
528
  wrapper.dataset.key = key;
529
  wrapper.dataset.parent = parentKey;
530
  wrapper.dataset.depth = depth;
531
+ wrapper.dataset.layer = Math.min(depth, 7);
532
 
533
+ // Add LCARS-style layer indicator
534
+ const layerIndicator = document.createElement('div');
535
+ layerIndicator.className = 'layer-indicator';
536
+ layerIndicator.style.left = `${depth * 24}px`;
537
+ wrapper.appendChild(layerIndicator);
538
+
539
+ // Add LCARS connector line
540
+ const connector = document.createElement('div');
541
+ connector.className = 'lcars-connector';
542
+ connector.style.left = `${depth * 24 + 4}px`;
543
+ wrapper.appendChild(connector);
544
 
545
  // Create element content
546
  const content = document.createElement('div');
547
  content.className = 'json-item-content flex items-start py-1';
548
+ content.style.marginLeft = `${depth * 24 + 12}px`;
549
 
550
+ // Key - make it editable with single click
551
  if (key !== null && key !== 'root') {
552
+ const keyInput = document.createElement('input');
553
+ keyInput.type = 'text';
554
+ keyInput.className = 'editable-field key-input';
555
+ keyInput.value = key;
556
+ keyInput.dataset.fieldType = 'key';
557
+ keyInput.dataset.parentKey = parentKey;
558
+ keyInput.dataset.depth = depth;
559
+ content.appendChild(keyInput);
560
+
561
+ const colon = document.createElement('span');
562
+ colon.className = 'json-bracket';
563
+ colon.textContent = ':';
564
+ content.appendChild(colon);
565
+
566
+ // Track editable field
567
+ editableFields.push({
568
+ element: keyInput,
569
+ type: 'key',
570
+ key: key,
571
+ parentKey: parentKey,
572
+ depth: depth
573
+ });
574
+
575
+ // Single-click to edit
576
+ keyInput.addEventListener('click', (e) => {
577
+ e.stopPropagation();
578
+ activateField(keyInput);
579
+ });
580
+
581
+ keyInput.addEventListener('focus', () => {
582
+ wrapper.classList.add('active');
583
+ wrapper.classList.add('editing');
584
+ });
585
+
586
+ keyInput.addEventListener('blur', () => {
587
+ wrapper.classList.remove('active');
588
+ wrapper.classList.remove('editing');
589
+ updateKey(keyInput);
590
+ });
591
+
592
+ keyInput.addEventListener('keydown', handleKeyNavigation);
593
  }
594
 
595
  // Value or children
 
597
  if (Array.isArray(data)) {
598
  // Array
599
  const bracket = document.createElement('span');
600
+ bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
601
  bracket.textContent = '[';
602
  content.appendChild(bracket);
603
 
 
611
 
612
  // Closing bracket
613
  const closingWrapper = document.createElement('div');
614
+ closingWrapper.className = `json-item relative ${layerClass}`;
615
  closingWrapper.dataset.key = 'closing';
616
  closingWrapper.dataset.parent = key;
617
  closingWrapper.dataset.depth = depth;
618
+ closingWrapper.dataset.layer = Math.min(depth, 7);
619
 
620
+ const closingLayerIndicator = document.createElement('div');
621
+ closingLayerIndicator.className = 'layer-indicator';
622
+ closingLayerIndicator.style.left = `${depth * 24}px`;
623
+ closingWrapper.appendChild(closingLayerIndicator);
 
 
624
 
625
  const closingContent = document.createElement('div');
626
  closingContent.className = 'json-item-content flex items-start py-1';
627
+ closingContent.style.marginLeft = `${depth * 24 + 12}px`;
628
  const closingBracket = document.createElement('span');
629
+ closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
630
  closingBracket.textContent = ']';
631
  closingContent.appendChild(closingBracket);
632
  closingWrapper.appendChild(closingContent);
 
634
  } else {
635
  // Object
636
  const bracket = document.createElement('span');
637
+ bracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
638
  bracket.textContent = '{';
639
  content.appendChild(bracket);
640
 
 
648
 
649
  // Closing bracket
650
  const closingWrapper = document.createElement('div');
651
+ closingWrapper.className = `json-item relative ${layerClass}`;
652
  closingWrapper.dataset.key = 'closing';
653
  closingWrapper.dataset.parent = key;
654
  closingWrapper.dataset.depth = depth;
655
+ closingWrapper.dataset.layer = Math.min(depth, 7);
656
 
657
+ const closingLayerIndicator = document.createElement('div');
658
+ closingLayerIndicator.className = 'layer-indicator';
659
+ closingLayerIndicator.style.left = `${depth * 24}px`;
660
+ closingWrapper.appendChild(closingLayerIndicator);
 
 
661
 
662
  const closingContent = document.createElement('div');
663
  closingContent.className = 'json-item-content flex items-start py-1';
664
+ closingContent.style.marginLeft = `${depth * 24 + 12}px`;
665
  const closingBracket = document.createElement('span');
666
+ closingBracket.className = `json-bracket bracket-layer-${Math.min(depth, 7)}`;
667
  closingBracket.textContent = '}';
668
  closingContent.appendChild(closingBracket);
669
  closingWrapper.appendChild(closingContent);
670
  container.appendChild(closingWrapper);
671
  }
672
  } else {
673
+ // Primitive value - make it editable
674
+ const valueInput = document.createElement('input');
675
+ valueInput.type = 'text';
676
+ valueInput.className = 'editable-field value-input';
677
+ valueInput.dataset.fieldType = 'value';
678
+ valueInput.dataset.key = key;
679
+ valueInput.dataset.parentKey = parentKey;
680
+ valueInput.dataset.depth = depth;
681
 
682
  if (typeof data === 'string') {
683
+ valueInput.value = data;
684
  } else if (typeof data === 'boolean') {
685
+ valueInput.value = data.toString();
686
+ } else if (data === null) {
687
+ valueInput.value = 'null';
688
  } else {
689
+ valueInput.value = data.toString();
690
  }
691
 
692
+ content.appendChild(valueInput);
693
  wrapper.appendChild(content);
694
  container.appendChild(wrapper);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
+ // Track editable field
697
+ editableFields.push({
698
+ element: valueInput,
699
+ type: 'value',
700
+ key: key,
701
+ parentKey: parentKey,
702
+ depth: depth
703
  });
704
 
705
+ // Single-click to edit
706
+ valueInput.addEventListener('click', (e) => {
707
+ e.stopPropagation();
708
+ activateField(valueInput);
709
  });
 
 
 
 
 
 
 
 
 
 
710
 
711
+ valueInput.addEventListener('focus', () => {
712
+ wrapper.classList.add('active');
713
+ wrapper.classList.add('editing');
 
 
714
  });
715
 
716
+ valueInput.addEventListener('blur', () => {
717
+ wrapper.classList.remove('active');
718
+ wrapper.classList.remove('editing');
719
+ updateValue(valueInput);
720
  });
721
+
722
+ valueInput.addEventListener('keydown', handleKeyNavigation);
723
  }
724
  }
725
 
726
+ // Activate a field and set it as current
727
+ function activateField(input) {
728
+ currentFieldIndex = editableFields.findIndex(f => f.element === input);
729
+ if (currentFieldIndex !== -1) {
730
+ editableFields.forEach(f => f.element.parentElement.classList.remove('active'));
731
+ input.parentElement.classList.add('active');
732
+ }
733
  }
734
 
735
+ // Handle keyboard navigation
736
+ function handleKeyNavigation(e) {
737
+ const input = e.target;
738
+ const fieldData = editableFields.find(f => f.element === input);
739
+
740
+ if (e.key === 'Tab') {
741
+ e.preventDefault();
742
+ if (e.shiftKey) {
743
+ navigateToField(-1);
744
+ } else {
745
+ navigateToField(1);
746
+ }
747
+ } else if (e.key === 'Enter' && e.shiftKey) {
748
+ e.preventDefault();
749
+ // Insert new field after current one
750
+ insertNewField(fieldData);
751
+ } else if (e.key === 'Enter') {
752
+ e.preventDefault();
753
+ // Navigate to next field
754
+ navigateToField(1);
755
+ }
756
+ }
757
+
758
+ // Navigate to next/previous field
759
+ function navigateToField(direction) {
760
+ const newIndex = currentFieldIndex + direction;
761
+ if (newIndex >= 0 && newIndex < editableFields.length) {
762
+ const nextField = editableFields[newIndex];
763
+ nextField.element.focus();
764
+ nextField.element.select();
765
+ currentFieldIndex = newIndex;
766
+ }
767
  }
768
 
769
+ // Insert a new field after the current one
770
+ function insertNewField(currentFieldData) {
771
+ let newData;
772
+ let insertAfterKey;
773
 
774
+ if (currentFieldData.parentKey === 'root') {
775
+ newData = {};
776
+ insertAfterKey = currentFieldData.key;
777
+
778
+ // Get all keys and find insertion point
779
+ const keys = Object.keys(jsonData);
780
+ const insertIndex = keys.indexOf(insertAfterKey) + 1;
781
+
782
+ // Create new object with inserted field
783
+ let index = 0;
784
+ keys.forEach(key => {
785
+ newData[key] = jsonData[key];
786
+ if (key === insertAfterKey) {
787
+ newData['newField'] = '';
788
  }
789
+ index++;
790
  });
791
+
792
+ // If insertAfterKey was the last key, append at end
793
+ if (insertIndex >= keys.length) {
794
+ newData['newField'] = '';
795
+ }
796
+
797
  jsonData = newData;
798
  } else {
799
+ // Handle nested objects
800
+ const parent = findElementByKey(jsonData, currentFieldData.parentKey);
801
+ if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
802
+ const keys = Object.keys(parent);
803
+ insertAfterKey = currentFieldData.key;
804
+
805
+ let index = 0;
806
+ keys.forEach(key => {
807
+ if (key === insertAfterKey && index < keys.length) {
808
+ // Insert after this key
809
+ const tempData = {};
810
+ let inserted = false;
811
+ keys.forEach(k => {
812
+ tempData[k] = parent[k];
813
+ if (!inserted && k === insertAfterKey) {
814
+ tempData['newField'] = '';
815
+ inserted = true;
816
+ }
817
+ });
818
+ // Clear parent
819
+ Object.keys(parent).forEach(k => delete parent[k]);
820
+ Object.assign(parent, tempData);
821
  }
822
+ index++;
823
  });
 
 
824
  }
825
  }
826
 
827
+ // Re-render and focus on new field
828
+ renderEditor();
829
+ updateOutput();
830
+ saveToHistory();
831
+
832
+ // Find and focus the new field
833
+ setTimeout(() => {
834
+ const newField = editableFields.find(f => f.key === 'newField');
835
+ if (newField) {
836
+ newField.element.focus();
837
+ newField.element.select();
838
+ }
839
+ }, 100);
840
+
841
+ showSavedIndicator();
842
+ }
843
+
844
+ // Update a key from input
845
+ function updateKey(input) {
846
+ const newKey = input.value.trim();
847
+ const oldKey = input.dataset.originalKey || input.value;
848
+ const parentKey = input.dataset.parentKey;
849
+
850
+ // Only save if value actually changed
851
+ if (newKey === oldKey) return;
852
+
853
+ input.dataset.originalKey = newKey;
854
+
855
+ if (parentKey === 'root') {
856
+ // Root level
857
+ const value = jsonData[oldKey];
858
+ delete jsonData[oldKey];
859
+ jsonData[newKey] = value;
860
+ } else {
861
+ // Nested level
862
+ const parent = findElementByKey(jsonData, parentKey);
863
+ if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
864
+ const value = parent[oldKey];
865
+ delete parent[oldKey];
866
+ parent[newKey] = value;
867
+ }
868
+ }
869
+
870
+ // Update tracking
871
+ const fieldData = editableFields.find(f => f.element === input);
872
+ if (fieldData) {
873
+ fieldData.key = newKey;
874
+ }
875
+
876
  updateOutput();
877
  saveToHistory();
878
+ showSavedIndicator();
879
  }
880
 
881
+ // Update a value from input
882
+ function updateValue(input) {
883
+ const rawValue = input.value.trim();
884
+ const parsedValue = parseValue(rawValue);
885
+ const key = input.dataset.key;
886
+ const parentKey = input.dataset.parentKey;
887
+
888
+ // Only save if value actually changed
889
+ const currentValue = getNestedValue(jsonData, parentKey, key);
890
+ if (JSON.stringify(currentValue) === JSON.stringify(parsedValue)) return;
891
 
892
  if (parentKey === 'root') {
893
+ jsonData[key] = parsedValue;
894
  } else {
895
  const parent = findElementByKey(jsonData, parentKey);
896
  if (parent && typeof parent === 'object') {
897
+ parent[key] = parsedValue;
898
  }
899
  }
900
 
901
  updateOutput();
902
  saveToHistory();
903
+ showSavedIndicator();
904
+ }
905
+
906
+ // Get nested value
907
+ function getNestedValue(obj, parentKey, key) {
908
+ if (parentKey === 'root') {
909
+ return obj[key];
910
+ }
911
+ const parent = findElementByKey(obj, parentKey);
912
+ if (parent && typeof parent === 'object') {
913
+ return parent[key];
914
+ }
915
+ return undefined;
916
+ }
917
+
918
+ // Parse a value from string to appropriate type
919
+ function parseValue(value) {
920
+ if (value === 'true') return true;
921
+ if (value === 'false') return false;
922
+ if (value === 'null') return null;
923
+ if (!isNaN(value) && value.trim() !== '' && !isNaN(Number(value))) {
924
+ return Number(value);
925
+ }
926
+ return value;
927
+ }
928
+
929
+ // Show saved indicator
930
+ function showSavedIndicator() {
931
+ savedIndicator.classList.add('show');
932
+ setTimeout(() => {
933
+ savedIndicator.classList.remove('show');
934
+ }, 1500);
935
  }
936
 
937
  // Find an element by key in nested structure
 
948
  return null;
949
  }
950
 
951
+ // Update JSON output with colored brackets
 
 
 
 
 
 
 
 
 
 
952
  function updateOutput() {
953
  if (isApplyingChanges) return;
954
  try {
955
+ const jsonString = JSON.stringify(jsonData, null, 2);
956
+ jsonOutput.value = jsonString;
957
  jsonOutput.classList.remove('error-highlight');
958
+ jsonOutput.style.borderColor = '#10b981';
959
  validationStatus.innerHTML = '<i class="fas fa-check-circle text-green-500"></i><span class="text-green-600">Valid JSON</span>';
960
  } catch (e) {
961
  jsonOutput.value = 'Invalid JSON structure';
 
1005
  }
1006
  });
1007
 
1008
+ // Auto-apply on blur (when clicking away)
1009
+ jsonOutput.addEventListener('blur', () => {
1010
+ const autoSaveStatus = document.getElementById('autoSaveStatus');
1011
+ if (applyCodeChanges()) {
1012
+ autoSaveStatus.classList.remove('hidden');
1013
+ setTimeout(() => {
1014
+ autoSaveStatus.classList.add('hidden');
1015
+ }, 2000);
1016
+ }
1017
+ }, true);
1018
+
1019
  // Show notification
1020
  function showNotification(message) {
1021
  const notificationContent = notification.querySelector('p');
 
1034
  history = history.slice(0, historyIndex + 1);
1035
  }
1036
 
1037
+ // Limit history size
1038
+ if (history.length >= MAX_HISTORY) {
1039
+ history.shift();
1040
+ historyIndex--;
1041
+ }
1042
+
1043
+ // Don't save if it's the same as current
1044
+ if (history.length > 0 && JSON.stringify(history[historyIndex]) === JSON.stringify(jsonData)) {
1045
+ return;
1046
+ }
1047
+
1048
  history.push(JSON.parse(JSON.stringify(jsonData)));
1049
  historyIndex = history.length - 1;
1050
  }
 
1229
 
1230
  // Instructions button
1231
  document.getElementById('instructionsBtn').addEventListener('click', () => {
1232
+ showNotification('Visual: Single-click to edit Tab/Shift+Tab to navigate Shift+Enter for new field • Auto-save enabled');
1233
  });
1234
 
1235
  // Sample JSON button
style.css CHANGED
@@ -26,3 +26,104 @@ p {
26
  .card p:last-child {
27
  margin-bottom: 0;
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  .card p:last-child {
27
  margin-bottom: 0;
28
  }
29
+
30
+ /* LCARS-style layer colors */
31
+ .lcars-layer-0 { --layer-color: #ef4444; }
32
+ .lcars-layer-1 { --layer-color: #f97316; }
33
+ .lcars-layer-2 { --layer-color: #eab308; }
34
+ .lcars-layer-3 { --layer-color: #22c55e; }
35
+ .lcars-layer-4 { --layer-color: #06b6d4; }
36
+ .lcars-layer-5 { --layer-color: #3b82f6; }
37
+ .lcars-layer-6 { --layer-color: #8b5cf6; }
38
+ .lcars-layer-7 { --layer-color: #ec4899; }
39
+
40
+ /* Layer indicator line */
41
+ .layer-indicator {
42
+ position: absolute;
43
+ left: 0;
44
+ top: 0;
45
+ bottom: 0;
46
+ width: 4px;
47
+ background: var(--layer-color);
48
+ border-radius: 0 4px 4px 0;
49
+ box-shadow: 2px 0 8px var(--layer-color);
50
+ }
51
+
52
+ /* LCARS horizontal connector */
53
+ .lcars-connector {
54
+ position: absolute;
55
+ left: 4px;
56
+ top: 50%;
57
+ height: 2px;
58
+ width: calc(100% - 4px);
59
+ background: var(--layer-color);
60
+ opacity: 0.4;
61
+ }
62
+
63
+ /* Active row - bold text instead of background */
64
+ .json-item.active {
65
+ background-color: transparent;
66
+ }
67
+
68
+ .json-item.active .json-key,
69
+ .json-item.active .json-value {
70
+ font-weight: bold;
71
+ text-shadow: 0 0 10px var(--layer-color);
72
+ }
73
+
74
+ /* Saved indicator */
75
+ .saved-indicator {
76
+ position: fixed;
77
+ bottom: 20px;
78
+ right: 20px;
79
+ background: linear-gradient(135deg, #10b981, #059669);
80
+ color: white;
81
+ padding: 12px 24px;
82
+ border-radius: 8px;
83
+ font-weight: bold;
84
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
85
+ transform: translateY(100px);
86
+ transition: transform 0.3s ease;
87
+ z-index: 1000;
88
+ }
89
+
90
+ .saved-indicator.show {
91
+ transform: translateY(0);
92
+ }
93
+
94
+ /* Editable fields styling */
95
+ .editable-field {
96
+ border: none;
97
+ background: transparent;
98
+ padding: 2px 4px;
99
+ border-radius: 4px;
100
+ font-family: inherit;
101
+ font-size: inherit;
102
+ width: auto;
103
+ min-width: 30px;
104
+ transition: all 0.2s ease;
105
+ }
106
+
107
+ .editable-field:focus {
108
+ outline: none;
109
+ background: rgba(255,255,255,0.15);
110
+ box-shadow: inset 0 0 0 2px rgba(255,255,255,0.3);
111
+ }
112
+
113
+ .editable-field.key-input {
114
+ color: #93c5fd;
115
+ }
116
+
117
+ .editable-field.value-input {
118
+ color: #6ee7b7;
119
+ }
120
+
121
+ /* New field indicator */
122
+ .new-field-highlight {
123
+ animation: pulse-green 1s ease-in-out infinite;
124
+ }
125
+
126
+ @keyframes pulse-green {
127
+ 0%, 100% { background-color: rgba(34, 197, 94, 0.2); }
128
+ 50% { background-color: rgba(34, 197, 94, 0.4); }
129
+ }