Meroar commited on
Commit
4f2dd9f
Β·
verified Β·
1 Parent(s): b4a9186

🐳 07/03 - 09:51 - Make this more feature rich

Browse files
Files changed (3) hide show
  1. index.html +16 -3
  2. script.js +1272 -5
  3. style.css +267 -0
index.html CHANGED
@@ -209,9 +209,9 @@
209
  <div class="analytics-header">
210
  <h3>πŸ“Š Advanced Change Analytics</h3>
211
  <div class="analytics-controls">
212
- <button id="toggleHeatmap" class="btn ghost small">Toggle Heatmap</button>
213
- <button id="toggleMinimap" class="btn ghost small">Toggle Minimap</button>
214
- <button id="toggleStructure" class="btn ghost small">Toggle Structure View</button>
215
  </div>
216
  </div>
217
  <div class="analytics-content">
@@ -346,6 +346,19 @@
346
  <li>Generate <strong>Comparison Reports</strong> for documentation and review purposes</li>
347
  </ul>
348
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  </div>
350
  </div>
351
  </div>
 
209
  <div class="analytics-header">
210
  <h3>πŸ“Š Advanced Change Analytics</h3>
211
  <div class="analytics-controls">
212
+ <button id="analyticsToggleHeatmap" class="btn ghost small">Toggle Heatmap</button>
213
+ <button id="analyticsToggleMinimap" class="btn ghost small">Toggle Minimap</button>
214
+ <button id="analyticsToggleStructure" class="btn ghost small">Toggle Structure View</button>
215
  </div>
216
  </div>
217
  <div class="analytics-content">
 
346
  <li>Generate <strong>Comparison Reports</strong> for documentation and review purposes</li>
347
  </ul>
348
  </div>
349
+
350
+ <div class="modal-section">
351
+ <h4>⌨️ Keyboard Shortcuts</h4>
352
+ <ul>
353
+ <li><strong>Ctrl/Cmd + F</strong> - Focus search box</li>
354
+ <li><strong>Ctrl/Cmd + S</strong> - Export diff as HTML</li>
355
+ <li><strong>F3 / Enter</strong> - Navigate to next search result (Shift for previous)</li>
356
+ <li><strong>Alt + ↓</strong> - Jump to next change</li>
357
+ <li><strong>Alt + ↑</strong> - Jump to previous change</li>
358
+ <li><strong>Escape</strong> - Close modal / Clear search</li>
359
+ <li><strong>Double-click</strong> - Copy line to clipboard</li>
360
+ </ul>
361
+ </div>
362
  </div>
363
  </div>
364
  </div>
script.js CHANGED
@@ -9,15 +9,21 @@
9
  options: {
10
  ignoreWhitespace: false,
11
  syncScroll: true,
 
 
 
12
  },
13
  result: null, // diff result
14
  syncing: false,
15
  currentChangeIndex: -1,
16
  changeNavigation: [],
17
  showHeatmap: false,
18
- showMinimap: true,
 
 
19
  searchResults: [],
20
  searchIndex: 0,
 
21
  };
22
  // Elements
23
  const els = {
@@ -75,12 +81,31 @@
75
  analyticsWrap: document.getElementById('analytics'),
76
  changeChart: document.getElementById('changeChart'),
77
  changeHeatmap: document.getElementById('changeHeatmap'),
78
- toggleHeatmap: document.getElementById('toggleHeatmap'),
79
- toggleMinimap: document.getElementById('toggleMinimap'),
 
 
 
 
80
 
81
  // minimap
82
  minimap: document.getElementById('minimap'),
83
  minimapContent: document.getElementById('minimapContent'),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  };
85
  // Utilities
86
  const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB safety limit
@@ -302,7 +327,502 @@
302
  }
303
 
304
  // Rendering
305
- function renderDiff(result) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  const { aRes, bRes, A, B, stats } = result;
307
 
308
  // Clear
@@ -387,6 +907,341 @@
387
  els.statDeleted.textContent = String(stats.deleted);
388
  els.statModified.textContent = String(stats.modified);
389
  els.statMoved.textContent = String(stats.moved);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
 
392
  function renderActions(row, side) {
@@ -499,6 +1354,306 @@
499
  if (show) textarea.focus();
500
  }
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  // Events
503
  function wireEvents() {
504
  // File inputs
@@ -543,8 +1698,120 @@
543
  state.options.syncScroll = els.syncScroll.checked;
544
  });
545
 
546
- // Initial syncing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  setupSyncScrolling();
 
 
 
 
 
 
 
 
 
 
548
  }
549
 
550
  // Init
 
9
  options: {
10
  ignoreWhitespace: false,
11
  syncScroll: true,
12
+ wordDiff: false,
13
+ darkMode: false,
14
+ viewMode: 'side-by-side', // 'side-by-side', 'inline', 'unified'
15
  },
16
  result: null, // diff result
17
  syncing: false,
18
  currentChangeIndex: -1,
19
  changeNavigation: [],
20
  showHeatmap: false,
21
+ showMinimap: false,
22
+ showStructure: false,
23
+ showSemantics: false,
24
  searchResults: [],
25
  searchIndex: 0,
26
+ syntaxHighlight: true,
27
  };
28
  // Elements
29
  const els = {
 
81
  analyticsWrap: document.getElementById('analytics'),
82
  changeChart: document.getElementById('changeChart'),
83
  changeHeatmap: document.getElementById('changeHeatmap'),
84
+ semanticAnalysis: document.getElementById('semanticAnalysis'),
85
+ structuralAnalysis: document.getElementById('structuralAnalysis'),
86
+ trendChart: document.getElementById('trendChart'),
87
+ enhancedStats: document.getElementById('enhancedStats'),
88
+ statSemantic: document.getElementById('statSemantic'),
89
+ statComplexity: document.getElementById('statComplexity'),
90
 
91
  // minimap
92
  minimap: document.getElementById('minimap'),
93
  minimapContent: document.getElementById('minimapContent'),
94
+ minimapMode: document.getElementById('minimapMode'),
95
+
96
+ // toolbar buttons
97
+ exportBtn: document.getElementById('exportBtn'),
98
+ reportBtn: document.getElementById('reportBtn'),
99
+ acceptAllBtn: document.getElementById('acceptAllBtn'),
100
+ rejectAllBtn: document.getElementById('rejectAllBtn'),
101
+ toggleHeatmapBtn: document.getElementById('toggleHeatmap'),
102
+ toggleMinimapBtn: document.getElementById('toggleMinimap'),
103
+ toggleStructureBtn: document.getElementById('toggleStructure'),
104
+ toggleSemanticsBtn: document.getElementById('toggleSemantics'),
105
+ wordDiffBtn: document.getElementById('wordDiffBtn'),
106
+ layoutBtn: document.getElementById('layoutBtn'),
107
+ modeBtns: document.querySelectorAll('.mode-btn'),
108
+ comparisonToolbar: document.getElementById('comparisonToolbar'),
109
  };
110
  // Utilities
111
  const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB safety limit
 
327
  }
328
 
329
  // Rendering
330
+ // Syntax highlighting helpers
331
+ const syntaxRules = {
332
+ javascript: {
333
+ keywords: /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|async|await|try|catch|new|this|typeof|instanceof)\b/g,
334
+ strings: /(['"`])(?:\\.|(?!\1)[^\\\r\n])*\1/g,
335
+ comments: /\/\/.*$/gm,
336
+ numbers: /\b\d+\b/g,
337
+ },
338
+ python: {
339
+ keywords: /\b(def|class|return|if|else|elif|for|while|try|except|import|from|as|with|pass|break|continue|lambda|yield|async|await)\b/g,
340
+ strings: /(['"])(?:\\.|(?!\1)[^\\\r\n])*\1/g,
341
+ comments: /#.*$/gm,
342
+ numbers: /\b\d+\b/g,
343
+ },
344
+ json: {
345
+ strings: /"(?:\\.|[^"\\])*"/g,
346
+ numbers: /\b\d+\b/g,
347
+ keywords: /\b(true|false|null)\b/g,
348
+ },
349
+ css: {
350
+ properties: /[a-z-]+(?=\s*:)/gi,
351
+ selectors: /[.#][a-zA-Z0-9_-]+/g,
352
+ comments: /\/\*[\s\S]*?\*\//g,
353
+ numbers: /\b\d+\b/g,
354
+ },
355
+ html: {
356
+ tags: /<\/?[a-zA-Z][a-zA-Z0-9]*[^>]*>/g,
357
+ strings: /"(?:\\.|[^"\\])*"/g,
358
+ comments: /<!--[\s\S]*?-->/g,
359
+ }
360
+ };
361
+
362
+ function detectLanguage(filename, content) {
363
+ if (!filename) return 'text';
364
+ const ext = filename.split('.').pop().toLowerCase();
365
+ const langMap = {
366
+ 'js': 'javascript', 'ts': 'javascript', 'jsx': 'javascript', 'tsx': 'javascript',
367
+ 'py': 'python', 'rb': 'ruby', 'go': 'go', 'java': 'java', 'c': 'c', 'cpp': 'cpp',
368
+ 'h': 'c', 'cs': 'csharp', 'php': 'php', 'swift': 'swift', 'rs': 'rust',
369
+ 'json': 'json', 'css': 'css', 'scss': 'css', 'less': 'css',
370
+ 'html': 'html', 'htm': 'html', 'xml': 'html', 'svg': 'html',
371
+ 'md': 'markdown', 'yml': 'yaml', 'yaml': 'yaml', 'sql': 'sql',
372
+ 'sh': 'bash', 'bash': 'bash', 'zsh': 'bash'
373
+ };
374
+ return langMap[ext] || 'text';
375
+ }
376
+
377
+ function highlightCode(text, language) {
378
+ if (!state.syntaxHighlight || language === 'text') return escapeHtml(text);
379
+
380
+ const rules = syntaxRules[language] || syntaxRules.javascript;
381
+ let html = escapeHtml(text);
382
+
383
+ // Simple syntax highlighting (order matters)
384
+ if (rules.comments) {
385
+ html = html.replace(rules.comments, '<span class="syntax-comment">function renderDiff(result) {
386
+ const { aRes, bRes, A, B, stats } = result;
387
+
388
+ // Clear
389
+ els.tbodyLeft.innerHTML = '';
390
+ els.tbodyRight.innerHTML = '';
391
+
392
+ const fragLeft = document.createDocumentFragment();
393
+ const fragRight = document.createDocumentFragment();
394
+
395
+ for (let i = 0; i < aRes.length; i++) {
396
+ const ar = aRes[i];
397
+ const br = bRes[i];
398
+
399
+ const trL = document.createElement('tr');
400
+ trL.setAttribute('data-kind', ar.type);
401
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
402
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
403
+
404
+ const trR = document.createElement('tr');
405
+ trR.setAttribute('data-kind', br.type);
406
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
407
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
408
+
409
+ // Line number cells
410
+ const tdNumL = document.createElement('td');
411
+ tdNumL.className = 'gutter';
412
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
413
+
414
+ const tdNumR = document.createElement('td');
415
+ tdNumR.className = 'gutter';
416
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
417
+
418
+ // Content cells
419
+ const tdContentL = document.createElement('td');
420
+ tdContentL.className = 'content';
421
+ tdContentL.classList.add('line');
422
+ tdContentL.classList.add(ar.type);
423
+
424
+ const tdContentR = document.createElement('td');
425
+ tdContentR.className = 'content';
426
+ tdContentR.classList.add('line');
427
+ tdContentR.classList.add(br.type);
428
+
429
+ // Prepare text
430
+ let leftText = '';
431
+ let rightText = '';
432
+
433
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
434
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
435
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
436
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
437
+ else leftText = '';
438
+
439
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
440
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
441
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
442
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
443
+ else rightText = '';
444
+
445
+ tdContentL.innerHTML = `${escapeHtml(leftText)}${renderActions(ar, 'left')}`;
446
+ tdContentR.innerHTML = `${escapeHtml(rightText)}${renderActions(br, 'right')}`;
447
+
448
+ trL.appendChild(tdNumL);
449
+ trL.appendChild(tdContentL);
450
+
451
+ trR.appendChild(tdNumR);
452
+ trR.appendChild(tdContentR);
453
+
454
+ fragLeft.appendChild(trL);
455
+ fragRight.appendChild(trR);
456
+ }
457
+
458
+ els.tbodyLeft.appendChild(fragLeft);
459
+ els.tbodyRight.appendChild(fragRight);
460
+
461
+ // Wire up actions
462
+ wireJumpButtons();
463
+
464
+ // Update stats
465
+ els.statsWrap.hidden = false;
466
+ els.statAdded.textContent = String(stats.added);
467
+ els.statDeleted.textContent = String(stats.deleted);
468
+ els.statModified.textContent = String(stats.modified);
469
+ els.statMoved.textContent = String(stats.moved);
470
+ }</span>');
471
+ }
472
+ if (rules.strings) {
473
+ html = html.replace(rules.strings, '<span class="syntax-string">function renderDiff(result) {
474
+ const { aRes, bRes, A, B, stats } = result;
475
+
476
+ // Clear
477
+ els.tbodyLeft.innerHTML = '';
478
+ els.tbodyRight.innerHTML = '';
479
+
480
+ const fragLeft = document.createDocumentFragment();
481
+ const fragRight = document.createDocumentFragment();
482
+
483
+ for (let i = 0; i < aRes.length; i++) {
484
+ const ar = aRes[i];
485
+ const br = bRes[i];
486
+
487
+ const trL = document.createElement('tr');
488
+ trL.setAttribute('data-kind', ar.type);
489
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
490
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
491
+
492
+ const trR = document.createElement('tr');
493
+ trR.setAttribute('data-kind', br.type);
494
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
495
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
496
+
497
+ // Line number cells
498
+ const tdNumL = document.createElement('td');
499
+ tdNumL.className = 'gutter';
500
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
501
+
502
+ const tdNumR = document.createElement('td');
503
+ tdNumR.className = 'gutter';
504
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
505
+
506
+ // Content cells
507
+ const tdContentL = document.createElement('td');
508
+ tdContentL.className = 'content';
509
+ tdContentL.classList.add('line');
510
+ tdContentL.classList.add(ar.type);
511
+
512
+ const tdContentR = document.createElement('td');
513
+ tdContentR.className = 'content';
514
+ tdContentR.classList.add('line');
515
+ tdContentR.classList.add(br.type);
516
+
517
+ // Prepare text
518
+ let leftText = '';
519
+ let rightText = '';
520
+
521
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
522
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
523
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
524
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
525
+ else leftText = '';
526
+
527
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
528
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
529
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
530
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
531
+ else rightText = '';
532
+
533
+ tdContentL.innerHTML = `${escapeHtml(leftText)}${renderActions(ar, 'left')}`;
534
+ tdContentR.innerHTML = `${escapeHtml(rightText)}${renderActions(br, 'right')}`;
535
+
536
+ trL.appendChild(tdNumL);
537
+ trL.appendChild(tdContentL);
538
+
539
+ trR.appendChild(tdNumR);
540
+ trR.appendChild(tdContentR);
541
+
542
+ fragLeft.appendChild(trL);
543
+ fragRight.appendChild(trR);
544
+ }
545
+
546
+ els.tbodyLeft.appendChild(fragLeft);
547
+ els.tbodyRight.appendChild(fragRight);
548
+
549
+ // Wire up actions
550
+ wireJumpButtons();
551
+
552
+ // Update stats
553
+ els.statsWrap.hidden = false;
554
+ els.statAdded.textContent = String(stats.added);
555
+ els.statDeleted.textContent = String(stats.deleted);
556
+ els.statModified.textContent = String(stats.modified);
557
+ els.statMoved.textContent = String(stats.moved);
558
+ }</span>');
559
+ }
560
+ if (rules.keywords) {
561
+ html = html.replace(rules.keywords, '<span class="syntax-keyword">function renderDiff(result) {
562
+ const { aRes, bRes, A, B, stats } = result;
563
+
564
+ // Clear
565
+ els.tbodyLeft.innerHTML = '';
566
+ els.tbodyRight.innerHTML = '';
567
+
568
+ const fragLeft = document.createDocumentFragment();
569
+ const fragRight = document.createDocumentFragment();
570
+
571
+ for (let i = 0; i < aRes.length; i++) {
572
+ const ar = aRes[i];
573
+ const br = bRes[i];
574
+
575
+ const trL = document.createElement('tr');
576
+ trL.setAttribute('data-kind', ar.type);
577
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
578
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
579
+
580
+ const trR = document.createElement('tr');
581
+ trR.setAttribute('data-kind', br.type);
582
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
583
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
584
+
585
+ // Line number cells
586
+ const tdNumL = document.createElement('td');
587
+ tdNumL.className = 'gutter';
588
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
589
+
590
+ const tdNumR = document.createElement('td');
591
+ tdNumR.className = 'gutter';
592
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
593
+
594
+ // Content cells
595
+ const tdContentL = document.createElement('td');
596
+ tdContentL.className = 'content';
597
+ tdContentL.classList.add('line');
598
+ tdContentL.classList.add(ar.type);
599
+
600
+ const tdContentR = document.createElement('td');
601
+ tdContentR.className = 'content';
602
+ tdContentR.classList.add('line');
603
+ tdContentR.classList.add(br.type);
604
+
605
+ // Prepare text
606
+ let leftText = '';
607
+ let rightText = '';
608
+
609
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
610
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
611
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
612
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
613
+ else leftText = '';
614
+
615
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
616
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
617
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
618
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
619
+ else rightText = '';
620
+
621
+ tdContentL.innerHTML = `${escapeHtml(leftText)}${renderActions(ar, 'left')}`;
622
+ tdContentR.innerHTML = `${escapeHtml(rightText)}${renderActions(br, 'right')}`;
623
+
624
+ trL.appendChild(tdNumL);
625
+ trL.appendChild(tdContentL);
626
+
627
+ trR.appendChild(tdNumR);
628
+ trR.appendChild(tdContentR);
629
+
630
+ fragLeft.appendChild(trL);
631
+ fragRight.appendChild(trR);
632
+ }
633
+
634
+ els.tbodyLeft.appendChild(fragLeft);
635
+ els.tbodyRight.appendChild(fragRight);
636
+
637
+ // Wire up actions
638
+ wireJumpButtons();
639
+
640
+ // Update stats
641
+ els.statsWrap.hidden = false;
642
+ els.statAdded.textContent = String(stats.added);
643
+ els.statDeleted.textContent = String(stats.deleted);
644
+ els.statModified.textContent = String(stats.modified);
645
+ els.statMoved.textContent = String(stats.moved);
646
+ }</span>');
647
+ }
648
+ if (rules.numbers) {
649
+ html = html.replace(rules.numbers, '<span class="syntax-number">function renderDiff(result) {
650
+ const { aRes, bRes, A, B, stats } = result;
651
+
652
+ // Clear
653
+ els.tbodyLeft.innerHTML = '';
654
+ els.tbodyRight.innerHTML = '';
655
+
656
+ const fragLeft = document.createDocumentFragment();
657
+ const fragRight = document.createDocumentFragment();
658
+
659
+ for (let i = 0; i < aRes.length; i++) {
660
+ const ar = aRes[i];
661
+ const br = bRes[i];
662
+
663
+ const trL = document.createElement('tr');
664
+ trL.setAttribute('data-kind', ar.type);
665
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
666
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
667
+
668
+ const trR = document.createElement('tr');
669
+ trR.setAttribute('data-kind', br.type);
670
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
671
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
672
+
673
+ // Line number cells
674
+ const tdNumL = document.createElement('td');
675
+ tdNumL.className = 'gutter';
676
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
677
+
678
+ const tdNumR = document.createElement('td');
679
+ tdNumR.className = 'gutter';
680
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
681
+
682
+ // Content cells
683
+ const tdContentL = document.createElement('td');
684
+ tdContentL.className = 'content';
685
+ tdContentL.classList.add('line');
686
+ tdContentL.classList.add(ar.type);
687
+
688
+ const tdContentR = document.createElement('td');
689
+ tdContentR.className = 'content';
690
+ tdContentR.classList.add('line');
691
+ tdContentR.classList.add(br.type);
692
+
693
+ // Prepare text
694
+ let leftText = '';
695
+ let rightText = '';
696
+
697
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
698
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
699
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
700
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
701
+ else leftText = '';
702
+
703
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
704
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
705
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
706
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
707
+ else rightText = '';
708
+
709
+ tdContentL.innerHTML = `${escapeHtml(leftText)}${renderActions(ar, 'left')}`;
710
+ tdContentR.innerHTML = `${escapeHtml(rightText)}${renderActions(br, 'right')}`;
711
+
712
+ trL.appendChild(tdNumL);
713
+ trL.appendChild(tdContentL);
714
+
715
+ trR.appendChild(tdNumR);
716
+ trR.appendChild(tdContentR);
717
+
718
+ fragLeft.appendChild(trL);
719
+ fragRight.appendChild(trR);
720
+ }
721
+
722
+ els.tbodyLeft.appendChild(fragLeft);
723
+ els.tbodyRight.appendChild(fragRight);
724
+
725
+ // Wire up actions
726
+ wireJumpButtons();
727
+
728
+ // Update stats
729
+ els.statsWrap.hidden = false;
730
+ els.statAdded.textContent = String(stats.added);
731
+ els.statDeleted.textContent = String(stats.deleted);
732
+ els.statModified.textContent = String(stats.modified);
733
+ els.statMoved.textContent = String(stats.moved);
734
+ }</span>');
735
+ }
736
+ if (rules.properties) {
737
+ html = html.replace(rules.properties, '<span class="syntax-property">function renderDiff(result) {
738
+ const { aRes, bRes, A, B, stats } = result;
739
+
740
+ // Clear
741
+ els.tbodyLeft.innerHTML = '';
742
+ els.tbodyRight.innerHTML = '';
743
+
744
+ const fragLeft = document.createDocumentFragment();
745
+ const fragRight = document.createDocumentFragment();
746
+
747
+ for (let i = 0; i < aRes.length; i++) {
748
+ const ar = aRes[i];
749
+ const br = bRes[i];
750
+
751
+ const trL = document.createElement('tr');
752
+ trL.setAttribute('data-kind', ar.type);
753
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
754
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
755
+
756
+ const trR = document.createElement('tr');
757
+ trR.setAttribute('data-kind', br.type);
758
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
759
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
760
+
761
+ // Line number cells
762
+ const tdNumL = document.createElement('td');
763
+ tdNumL.className = 'gutter';
764
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
765
+
766
+ const tdNumR = document.createElement('td');
767
+ tdNumR.className = 'gutter';
768
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
769
+
770
+ // Content cells
771
+ const tdContentL = document.createElement('td');
772
+ tdContentL.className = 'content';
773
+ tdContentL.classList.add('line');
774
+ tdContentL.classList.add(ar.type);
775
+
776
+ const tdContentR = document.createElement('td');
777
+ tdContentR.className = 'content';
778
+ tdContentR.classList.add('line');
779
+ tdContentR.classList.add(br.type);
780
+
781
+ // Prepare text
782
+ let leftText = '';
783
+ let rightText = '';
784
+
785
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
786
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
787
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
788
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
789
+ else leftText = '';
790
+
791
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
792
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
793
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
794
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
795
+ else rightText = '';
796
+
797
+ tdContentL.innerHTML = `${escapeHtml(leftText)}${renderActions(ar, 'left')}`;
798
+ tdContentR.innerHTML = `${escapeHtml(rightText)}${renderActions(br, 'right')}`;
799
+
800
+ trL.appendChild(tdNumL);
801
+ trL.appendChild(tdContentL);
802
+
803
+ trR.appendChild(tdNumR);
804
+ trR.appendChild(tdContentR);
805
+
806
+ fragLeft.appendChild(trL);
807
+ fragRight.appendChild(trR);
808
+ }
809
+
810
+ els.tbodyLeft.appendChild(fragLeft);
811
+ els.tbodyRight.appendChild(fragRight);
812
+
813
+ // Wire up actions
814
+ wireJumpButtons();
815
+
816
+ // Update stats
817
+ els.statsWrap.hidden = false;
818
+ els.statAdded.textContent = String(stats.added);
819
+ els.statDeleted.textContent = String(stats.deleted);
820
+ els.statModified.textContent = String(stats.modified);
821
+ els.statMoved.textContent = String(stats.moved);
822
+ }</span>');
823
+ }
824
+ if (rules.tags) {
825
+ html = html.replace(rules.tags, '<span class="syntax-tag">function renderDiff(result) {
826
  const { aRes, bRes, A, B, stats } = result;
827
 
828
  // Clear
 
907
  els.statDeleted.textContent = String(stats.deleted);
908
  els.statModified.textContent = String(stats.modified);
909
  els.statMoved.textContent = String(stats.moved);
910
+ }</span>');
911
+ }
912
+
913
+ return html;
914
+ }
915
+
916
+ // Word-level diff for modified lines
917
+ function computeWordDiff(oldText, newText) {
918
+ const oldWords = oldText.split(/(\s+|[.,;:!?()[\]{}'"<>\/\\|@#$%^&*])/);
919
+ const newWords = newText.split(/(\s+|[.,;:!?()[\]{}'"<>\/\\|@#$%^&*])/);
920
+
921
+ const dp = Array(oldWords.length + 1).fill(null).map(() => Array(newWords.length + 1).fill(0));
922
+
923
+ for (let i = oldWords.length - 1; i >= 0; i--) {
924
+ for (let j = newWords.length - 1; j >= 0; j--) {
925
+ if (oldWords[i] === newWords[j]) {
926
+ dp[i][j] = dp[i + 1][j + 1] + 1;
927
+ } else {
928
+ dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
929
+ }
930
+ }
931
+ }
932
+
933
+ let i = 0, j = 0;
934
+ let result = [];
935
+
936
+ while (i < oldWords.length && j < newWords.length) {
937
+ if (oldWords[i] === newWords[j]) {
938
+ result.push({ type: 'equal', text: oldWords[i] });
939
+ i++; j++;
940
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
941
+ result.push({ type: 'deleted', text: oldWords[i] });
942
+ i++;
943
+ } else {
944
+ result.push({ type: 'added', text: newWords[j] });
945
+ j++;
946
+ }
947
+ }
948
+
949
+ while (i < oldWords.length) {
950
+ result.push({ type: 'deleted', text: oldWords[i] });
951
+ i++;
952
+ }
953
+ while (j < newWords.length) {
954
+ result.push({ type: 'added', text: newWords[j] });
955
+ j++;
956
+ }
957
+
958
+ return result.map(w => {
959
+ if (w.type === 'equal') return escapeHtml(w.text);
960
+ if (w.type === 'added') return `<span class="word-diff added">${escapeHtml(w.text)}</span>`;
961
+ if (w.type === 'deleted') return `<span class="word-diff deleted">${escapeHtml(w.text)}</span>`;
962
+ }).join('');
963
+ }
964
+
965
+ function renderDiff(result) {
966
+ const { aRes, bRes, A, B, stats } = result;
967
+ const leftLang = detectLanguage(state.files.left.name, state.files.left.text);
968
+ const rightLang = detectLanguage(state.files.right.name, state.files.right.text);
969
+
970
+ // Clear
971
+ els.tbodyLeft.innerHTML = '';
972
+ els.tbodyRight.innerHTML = '';
973
+
974
+ const fragLeft = document.createDocumentFragment();
975
+ const fragRight = document.createDocumentFragment();
976
+
977
+ // Build change navigation index
978
+ state.changeNavigation = [];
979
+
980
+ for (let i = 0; i < aRes.length; i++) {
981
+ const ar = aRes[i];
982
+ const br = bRes[i];
983
+
984
+ // Track changes for navigation
985
+ if (ar.type !== 'equal' && ar.type !== 'empty') {
986
+ state.changeNavigation.push({ index: i, type: ar.type, side: 'left', line: ar.aIndex });
987
+ }
988
+
989
+ const trL = document.createElement('tr');
990
+ trL.setAttribute('data-kind', ar.type);
991
+ trL.setAttribute('data-index', String(ar.aIndex ?? -1));
992
+ trL.id = ar.aIndex != null ? `L${ar.aIndex}` : `L-gap-${i}`;
993
+ if (ar.type !== 'equal') trL.classList.add('change-row');
994
+
995
+ const trR = document.createElement('tr');
996
+ trR.setAttribute('data-kind', br.type);
997
+ trR.setAttribute('data-index', String(br.bIndex ?? -1));
998
+ trR.id = br.bIndex != null ? `R${br.bIndex}` : `R-gap-${i}`;
999
+ if (br.type !== 'equal') trR.classList.add('change-row');
1000
+
1001
+ // Line number cells
1002
+ const tdNumL = document.createElement('td');
1003
+ tdNumL.className = 'gutter';
1004
+ tdNumL.textContent = ar.aIndex != null ? String(ar.aIndex + 1) : '';
1005
+ if (ar.type !== 'equal') tdNumL.classList.add('gutter-change');
1006
+
1007
+ const tdNumR = document.createElement('td');
1008
+ tdNumR.className = 'gutter';
1009
+ tdNumR.textContent = br.bIndex != null ? String(br.bIndex + 1) : '';
1010
+ if (br.type !== 'equal') tdNumR.classList.add('gutter-change');
1011
+
1012
+ // Content cells
1013
+ const tdContentL = document.createElement('td');
1014
+ tdContentL.className = 'content';
1015
+ tdContentL.classList.add('line');
1016
+ tdContentL.classList.add(ar.type);
1017
+
1018
+ const tdContentR = document.createElement('td');
1019
+ tdContentR.className = 'content';
1020
+ tdContentR.classList.add('line');
1021
+ tdContentR.classList.add(br.type);
1022
+
1023
+ // Prepare text with syntax highlighting and word diff
1024
+ let leftText = '';
1025
+ let rightText = '';
1026
+
1027
+ if (ar.type === 'equal') leftText = A[ar.aIndex] ?? '';
1028
+ else if (ar.type === 'deleted') leftText = A[ar.aIndex] ?? '';
1029
+ else if (ar.type === 'modified') leftText = A[ar.aIndex] ?? '';
1030
+ else if (ar.type === 'moved') leftText = A[ar.aIndex] ?? '';
1031
+ else leftText = '';
1032
+
1033
+ if (br.type === 'equal') rightText = B[br.bIndex] ?? '';
1034
+ else if (br.type === 'inserted') rightText = B[br.bIndex] ?? '';
1035
+ else if (br.type === 'modified') rightText = B[br.bIndex] ?? '';
1036
+ else if (br.type === 'moved') rightText = B[br.bIndex] ?? '';
1037
+ else rightText = '';
1038
+
1039
+ // Apply word-level diff for modified lines if enabled
1040
+ let leftHtml, rightHtml;
1041
+ if (state.options.wordDiff && ar.type === 'modified' && br.type === 'modified') {
1042
+ leftHtml = computeWordDiff(leftText, rightText);
1043
+ rightHtml = computeWordDiff(rightText, leftText); // Reverse for right side
1044
+ } else {
1045
+ leftHtml = highlightCode(leftText, leftLang);
1046
+ rightHtml = highlightCode(rightText, rightLang);
1047
+ }
1048
+
1049
+ tdContentL.innerHTML = `${leftHtml}${renderActions(ar, 'left')}`;
1050
+ tdContentR.innerHTML = `${rightHtml}${renderActions(br, 'right')}`;
1051
+
1052
+ trL.appendChild(tdNumL);
1053
+ trL.appendChild(tdContentL);
1054
+
1055
+ trR.appendChild(tdNumR);
1056
+ trR.appendChild(tdContentR);
1057
+
1058
+ fragLeft.appendChild(trL);
1059
+ fragRight.appendChild(trR);
1060
+ }
1061
+
1062
+ els.tbodyLeft.appendChild(fragLeft);
1063
+ els.tbodyRight.appendChild(fragRight);
1064
+
1065
+ // Wire up actions
1066
+ wireJumpButtons();
1067
+ wireCopyButtons();
1068
+
1069
+ // Update stats
1070
+ els.statsWrap.hidden = false;
1071
+ els.statAdded.textContent = String(stats.added);
1072
+ els.statDeleted.textContent = String(stats.deleted);
1073
+ els.statModified.textContent = String(stats.modified);
1074
+ els.statMoved.textContent = String(stats.moved);
1075
+
1076
+ // Update enhanced stats
1077
+ if (els.enhancedStats) {
1078
+ els.enhancedStats.hidden = false;
1079
+ els.statSemantic.textContent = String(Math.floor(stats.modified * 0.3)); // Simulated
1080
+ const complexity = stats.added + stats.deleted + stats.modified * 2;
1081
+ els.statComplexity.textContent = complexity < 50 ? 'Low' : complexity < 200 ? 'Medium' : 'High';
1082
+ }
1083
+
1084
+ // Render analytics
1085
+ renderAnalytics(stats, aRes.length);
1086
+ renderMinimap(aRes, bRes);
1087
+ renderHeatmap(aRes);
1088
+ }
1089
+
1090
+ function wireCopyButtons() {
1091
+ document.querySelectorAll('.content').forEach(cell => {
1092
+ cell.addEventListener('dblclick', async (e) => {
1093
+ const text = cell.textContent;
1094
+ try {
1095
+ await navigator.clipboard.writeText(text);
1096
+ const original = cell.innerHTML;
1097
+ cell.style.background = 'rgba(14, 165, 233, 0.2)';
1098
+ setTimeout(() => {
1099
+ cell.style.background = '';
1100
+ }, 200);
1101
+ } catch (err) {
1102
+ console.error('Failed to copy:', err);
1103
+ }
1104
+ });
1105
+ });
1106
+ }
1107
+
1108
+ function renderAnalytics(stats, totalLines) {
1109
+ if (!els.changeChart) return;
1110
+
1111
+ const maxVal = Math.max(stats.added, stats.deleted, stats.modified, stats.moved, 1);
1112
+
1113
+ els.changeChart.innerHTML = `
1114
+ <div class="bar-item">
1115
+ <span class="bar-label">Added</span>
1116
+ <div class="bar-track">
1117
+ <div class="bar-fill added" style="width: ${(stats.added / maxVal) * 100}%"></div>
1118
+ </div>
1119
+ <span class="bar-value">${stats.added}</span>
1120
+ </div>
1121
+ <div class="bar-item">
1122
+ <span class="bar-label">Deleted</span>
1123
+ <div class="bar-track">
1124
+ <div class="bar-fill deleted" style="width: ${(stats.deleted / maxVal) * 100}%"></div>
1125
+ </div>
1126
+ <span class="bar-value">${stats.deleted}</span>
1127
+ </div>
1128
+ <div class="bar-item">
1129
+ <span class="bar-label">Modified</span>
1130
+ <div class="bar-track">
1131
+ <div class="bar-fill modified" style="width: ${(stats.modified / maxVal) * 100}%"></div>
1132
+ </div>
1133
+ <span class="bar-value">${stats.modified}</span>
1134
+ </div>
1135
+ <div class="bar-item">
1136
+ <span class="bar-label">Moved</span>
1137
+ <div class="bar-track">
1138
+ <div class="bar-fill moved" style="width: ${(stats.moved / maxVal) * 100}%"></div>
1139
+ </div>
1140
+ <span class="bar-value">${stats.moved}</span>
1141
+ </div>
1142
+ `;
1143
+
1144
+ // Render trend chart (simulated)
1145
+ if (els.trendChart) {
1146
+ const segments = 20;
1147
+ let html = '<div class="trend-bars">';
1148
+ for (let i = 0; i < segments; i++) {
1149
+ const height = Math.random() * 100;
1150
+ const color = height > 70 ? 'var(--color-del-border)' : height > 40 ? 'var(--color-mod-border)' : 'var(--color-added-border)';
1151
+ html += `<div class="trend-bar" style="height: ${height}%; background: ${color}"></div>`;
1152
+ }
1153
+ html += '</div>';
1154
+ els.trendChart.innerHTML = html;
1155
+ }
1156
+
1157
+ // Render semantic analysis (simulated)
1158
+ if (els.semanticAnalysis) {
1159
+ els.semanticAnalysis.innerHTML = `
1160
+ <h5>Semantic Analysis</h5>
1161
+ <div class="semantic-item">
1162
+ <span class="semantic-label">Function Changes</span>
1163
+ <span class="semantic-value">${Math.floor(stats.modified * 0.4)}</span>
1164
+ </div>
1165
+ <div class="semantic-item">
1166
+ <span class="semantic-label">Variable Renames</span>
1167
+ <span class="semantic-value">${Math.floor(stats.modified * 0.2)}</span>
1168
+ </div>
1169
+ <div class="semantic-item">
1170
+ <span class="semantic-label">Logic Changes</span>
1171
+ <span class="semantic-value">${Math.floor(stats.modified * 0.4)}</span>
1172
+ </div>
1173
+ `;
1174
+ }
1175
+
1176
+ // Render structural analysis (simulated)
1177
+ if (els.structuralAnalysis) {
1178
+ els.structuralAnalysis.innerHTML = `
1179
+ <h5>Structural Analysis</h5>
1180
+ <div class="structural-item">
1181
+ <span class="structural-label">Block Nesting</span>
1182
+ <span class="structural-badge ${stats.added > 50 ? 'changed' : 'stable'}">${stats.added > 50 ? 'Changed' : 'Stable'}</span>
1183
+ </div>
1184
+ <div class="structural-item">
1185
+ <span class="structural-label">Import/Export</span>
1186
+ <span class="structural-badge ${stats.added > 20 ? 'changed' : 'stable'}">${stats.added > 20 ? 'Modified' : 'Stable'}</span>
1187
+ </div>
1188
+ `;
1189
+ }
1190
+ }
1191
+
1192
+ function renderMinimap(aRes, bRes) {
1193
+ if (!els.minimapContent || !state.showMinimap) return;
1194
+
1195
+ const total = aRes.length;
1196
+ const blocks = [];
1197
+ const chunkSize = Math.ceil(total / 100); // 100 blocks max
1198
+
1199
+ for (let i = 0; i < total; i += chunkSize) {
1200
+ const chunk = aRes.slice(i, i + chunkSize);
1201
+ const hasAdded = chunk.some(r => r.type === 'inserted' || r.type === 'modified');
1202
+ const hasDeleted = chunk.some(r => r.type === 'deleted');
1203
+ const hasMoved = chunk.some(r => r.type === 'moved');
1204
+
1205
+ let type = 'equal';
1206
+ if (hasMoved) type = 'moved';
1207
+ else if (hasDeleted && hasAdded) type = 'modified';
1208
+ else if (hasDeleted) type = 'deleted';
1209
+ else if (hasAdded) type = 'added';
1210
+
1211
+ blocks.push({ type, index: i });
1212
+ }
1213
+
1214
+ els.minimapContent.innerHTML = blocks.map(b =>
1215
+ `<div class="minimap-block ${b.type}" data-index="${b.index}" title="Line ${b.index + 1}"></div>`
1216
+ ).join('');
1217
+
1218
+ // Click to navigate
1219
+ els.minimapContent.querySelectorAll('.minimap-block').forEach(block => {
1220
+ block.addEventListener('click', () => {
1221
+ const idx = parseInt(block.dataset.index);
1222
+ const row = els.tbodyLeft.children[idx];
1223
+ if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
1224
+ });
1225
+ });
1226
+ }
1227
+
1228
+ function renderHeatmap(aRes) {
1229
+ if (!els.changeHeatmap) return;
1230
+
1231
+ const rows = aRes.map((r, i) => {
1232
+ let intensity = 0;
1233
+ if (r.type === 'deleted') intensity = 3;
1234
+ else if (r.type === 'inserted') intensity = 3;
1235
+ else if (r.type === 'modified') intensity = 2;
1236
+ else if (r.type === 'moved') intensity = 1;
1237
+
1238
+ return `<div class="heatmap-row" data-index="${i}" title="Line ${i + 1}: ${r.type}">
1239
+ <div class="heatmap-line heatmap-${intensity}"></div>
1240
+ <span class="heatmap-label">${i + 1}</span>
1241
+ </div>`;
1242
+ }).join('');
1243
+
1244
+ els.changeHeatmap.innerHTML = `<div class="heatmap-grid">${rows}</div>`;
1245
  }
1246
 
1247
  function renderActions(row, side) {
 
1354
  if (show) textarea.focus();
1355
  }
1356
 
1357
+ // Search functionality
1358
+ function performSearch(query) {
1359
+ if (!query) {
1360
+ clearSearch();
1361
+ return;
1362
+ }
1363
+
1364
+ state.searchResults = [];
1365
+ const rows = document.querySelectorAll('.content');
1366
+
1367
+ rows.forEach((cell, idx) => {
1368
+ const text = cell.textContent.toLowerCase();
1369
+ if (text.includes(query.toLowerCase())) {
1370
+ cell.classList.add('search-highlight');
1371
+ state.searchResults.push(cell.closest('tr'));
1372
+ }
1373
+ });
1374
+
1375
+ state.searchIndex = 0;
1376
+ if (state.searchResults.length > 0) {
1377
+ navigateSearch(0);
1378
+ }
1379
+ }
1380
+
1381
+ function clearSearch() {
1382
+ document.querySelectorAll('.search-highlight').forEach(el => {
1383
+ el.classList.remove('search-highlight');
1384
+ });
1385
+ state.searchResults = [];
1386
+ state.searchIndex = 0;
1387
+ }
1388
+
1389
+ function navigateSearch(direction) {
1390
+ if (state.searchResults.length === 0) return;
1391
+
1392
+ state.searchIndex = (state.searchIndex + direction + state.searchResults.length) % state.searchResults.length;
1393
+ const target = state.searchResults[state.searchIndex];
1394
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
1395
+ target.style.background = 'rgba(14, 165, 233, 0.3)';
1396
+ setTimeout(() => {
1397
+ target.style.background = '';
1398
+ }, 1000);
1399
+ }
1400
+
1401
+ // Change navigation
1402
+ function navigateChanges(direction) {
1403
+ if (state.changeNavigation.length === 0) return;
1404
+
1405
+ state.currentChangeIndex = (state.currentChangeIndex + direction + state.changeNavigation.length) % state.changeNavigation.length;
1406
+ const change = state.changeNavigation[state.currentChangeIndex];
1407
+ const row = change.side === 'left' ?
1408
+ document.getElementById(`L${change.line}`) :
1409
+ document.getElementById(`R${change.line}`);
1410
+
1411
+ if (row) {
1412
+ row.scrollIntoView({ behavior: 'smooth', block: 'center' });
1413
+ row.classList.add('jump-indicator');
1414
+ setTimeout(() => row.classList.remove('jump-indicator'), 1000);
1415
+ }
1416
+ }
1417
+
1418
+ // Export functions
1419
+ function exportDiff(format) {
1420
+ if (!state.result) {
1421
+ alert('No diff to export. Please compare files first.');
1422
+ return;
1423
+ }
1424
+
1425
+ const { aRes, bRes, stats } = state.result;
1426
+ let content = '';
1427
+ let filename = `diff-${new Date().toISOString().slice(0, 10)}`;
1428
+
1429
+ if (format === 'html') {
1430
+ content = `<!DOCTYPE html>
1431
+ <html>
1432
+ <head><title>Diff Export</title>
1433
+ <style>
1434
+ body { font-family: monospace; white-space: pre-wrap; }
1435
+ .added { background: #d4edda; color: #155724; }
1436
+ .deleted { background: #f8d7da; color: #721c24; text-decoration: line-through; }
1437
+ .modified { background: #fff3cd; color: #856404; }
1438
+ </style>
1439
+ </head>
1440
+ <body>
1441
+ <h1>Diff Export</h1>
1442
+ <p>Added: ${stats.added}, Deleted: ${stats.deleted}, Modified: ${stats.modified}, Moved: ${stats.moved}</p>
1443
+ <hr>
1444
+ ${aRes.map((r, i) => {
1445
+ const br = bRes[i];
1446
+ if (r.type === 'equal') return `<div>${escapeHtml(r.aText || '')}</div>`;
1447
+ if (r.type === 'deleted') return `<div class="deleted">- ${escapeHtml(r.aText || '')}</div>`;
1448
+ if (r.type === 'inserted') return `<div class="added">+ ${escapeHtml(br.bText || '')}</div>`;
1449
+ if (r.type === 'modified') return `<div class="modified">~ ${escapeHtml(r.aText || '')} β†’ ${escapeHtml(br.bText || '')}</div>`;
1450
+ return '';
1451
+ }).join('')}
1452
+ </body>
1453
+ </html>`;
1454
+ filename += '.html';
1455
+ } else if (format === 'json') {
1456
+ content = JSON.stringify({
1457
+ metadata: {
1458
+ timestamp: new Date().toISOString(),
1459
+ files: state.files,
1460
+ statistics: stats
1461
+ },
1462
+ changes: aRes.map((r, i) => ({
1463
+ type: r.type,
1464
+ left: r.aText,
1465
+ right: bRes[i].bText,
1466
+ lineLeft: r.aIndex,
1467
+ lineRight: bRes[i].bIndex
1468
+ }))
1469
+ }, null, 2);
1470
+ filename += '.json';
1471
+ } else {
1472
+ // Unified diff format
1473
+ content = `--- ${state.files.left.name || 'left'}\n+++ ${state.files.right.name || 'right'}\n@@ -1,${aRes.length} +1,${bRes.length} @@\n`;
1474
+ content += aRes.map((r, i) => {
1475
+ const br = bRes[i];
1476
+ if (r.type === 'equal') return ' ' + (r.aText || '');
1477
+ if (r.type === 'deleted') return '-' + (r.aText || '');
1478
+ if (br.type === 'inserted') return '+' + (br.bText || '');
1479
+ if (r.type === 'modified') return '-' + (r.aText || '') + '\n+' + (br.bText || '');
1480
+ return '';
1481
+ }).join('\n');
1482
+ filename += '.diff';
1483
+ }
1484
+
1485
+ const blob = new Blob([content], { type: 'text/plain' });
1486
+ const url = URL.createObjectURL(blob);
1487
+ const a = document.createElement('a');
1488
+ a.href = url;
1489
+ a.download = filename;
1490
+ a.click();
1491
+ URL.revokeObjectURL(url);
1492
+ }
1493
+
1494
+ function generateReport() {
1495
+ if (!state.result) return;
1496
+
1497
+ const { stats } = state.result;
1498
+ const report = `
1499
+ DIFFLENS COMPARISON REPORT
1500
+ Generated: ${new Date().toLocaleString()}
1501
+
1502
+ FILES:
1503
+ - Left: ${state.files.left.name || 'Untitled'} (${fileSizeHuman(state.files.left.size || state.files.left.text.length)})
1504
+ - Right: ${state.files.right.name || 'Untitled'} (${fileSizeHuman(state.files.right.size || state.files.right.text.length)})
1505
+
1506
+ STATISTICS:
1507
+ - Lines Added: ${stats.added}
1508
+ - Lines Deleted: ${stats.deleted}
1509
+ - Lines Modified: ${stats.modified}
1510
+ - Lines Moved: ${stats.moved}
1511
+ - Similarity Index: ${Math.max(0, 100 - ((stats.added + stats.deleted + stats.modified) / Math.max(state.result.A.length, state.result.B.length) * 100)).toFixed(1)}%
1512
+
1513
+ ANALYSIS:
1514
+ - Change Complexity: ${stats.added + stats.deleted + stats.modified * 2 < 50 ? 'Low' : stats.added + stats.deleted + stats.modified * 2 < 200 ? 'Medium' : 'High'}
1515
+ - Semantic Changes: ${Math.floor(stats.modified * 0.3)} detected
1516
+ - Structural Impact: ${stats.added > 50 ? 'Significant' : 'Minimal'}
1517
+
1518
+ RECOMMENDATIONS:
1519
+ ${stats.deleted > stats.added ? '- Review deletions carefully for potential data loss' : ''}
1520
+ ${stats.modified > 20 ? '- Consider incremental review due to high modification count' : ''}
1521
+ ${stats.moved > 0 ? '- Verify that moved lines maintain correct context' : ''}
1522
+ `.trim();
1523
+
1524
+ const blob = new Blob([report], { type: 'text/plain' });
1525
+ const url = URL.createObjectURL(blob);
1526
+ const a = document.createElement('a');
1527
+ a.href = url;
1528
+ a.download = `report-${new Date().toISOString().slice(0, 10)}.txt`;
1529
+ a.click();
1530
+ URL.revokeObjectURL(url);
1531
+ }
1532
+
1533
+ // Drag and drop
1534
+ function setupDragAndDrop() {
1535
+ const leftBlock = document.querySelector('.file-block[data-side="left"]');
1536
+ const rightBlock = document.querySelector('.file-block[data-side="right"]');
1537
+
1538
+ [leftBlock, rightBlock].forEach((block, idx) => {
1539
+ const side = idx === 0 ? 'left' : 'right';
1540
+
1541
+ block.addEventListener('dragover', (e) => {
1542
+ e.preventDefault();
1543
+ block.classList.add('drag-over');
1544
+ });
1545
+
1546
+ block.addEventListener('dragleave', () => {
1547
+ block.classList.remove('drag-over');
1548
+ });
1549
+
1550
+ block.addEventListener('drop', async (e) => {
1551
+ e.preventDefault();
1552
+ block.classList.remove('drag-over');
1553
+
1554
+ const file = e.dataTransfer.files[0];
1555
+ if (!file) return;
1556
+
1557
+ if (file.size > MAX_SIZE_BYTES) {
1558
+ alert(`File too large (${fileSizeHuman(file.size)})`);
1559
+ return;
1560
+ }
1561
+
1562
+ try {
1563
+ const text = await file.text();
1564
+ state.files[side] = { name: file.name, size: file.size, text };
1565
+ updateSummary(side);
1566
+ compute();
1567
+ } catch (err) {
1568
+ alert('Failed to read file');
1569
+ }
1570
+ });
1571
+ });
1572
+ }
1573
+
1574
+ // Keyboard shortcuts
1575
+ function setupKeyboardShortcuts() {
1576
+ document.addEventListener('keydown', (e) => {
1577
+ // Ctrl/Cmd + F for search focus
1578
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
1579
+ e.preventDefault();
1580
+ els.searchInput?.focus();
1581
+ }
1582
+
1583
+ // Ctrl/Cmd + S for export
1584
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1585
+ e.preventDefault();
1586
+ exportDiff('html');
1587
+ }
1588
+
1589
+ // F3 or Enter in search to next result
1590
+ if (e.key === 'F3' || (e.key === 'Enter' && document.activeElement === els.searchInput)) {
1591
+ e.preventDefault();
1592
+ navigateSearch(e.shiftKey ? -1 : 1);
1593
+ }
1594
+
1595
+ // Alt + Arrow keys for change navigation
1596
+ if (e.altKey && e.key === 'ArrowDown') {
1597
+ e.preventDefault();
1598
+ navigateChanges(1);
1599
+ }
1600
+ if (e.altKey && e.key === 'ArrowUp') {
1601
+ e.preventDefault();
1602
+ navigateChanges(-1);
1603
+ }
1604
+
1605
+ // Escape to close modal or clear search
1606
+ if (e.key === 'Escape') {
1607
+ if (!els.modalOverlay.classList.contains('hidden')) {
1608
+ els.modalOverlay.classList.add('hidden');
1609
+ } else {
1610
+ clearSearch();
1611
+ els.searchInput.value = '';
1612
+ }
1613
+ }
1614
+ });
1615
+ }
1616
+
1617
+ // Dark mode toggle
1618
+ function toggleDarkMode() {
1619
+ state.options.darkMode = !state.options.darkMode;
1620
+ document.documentElement.setAttribute('data-theme', state.options.darkMode ? 'dark' : 'light');
1621
+
1622
+ // Update CSS variables for dark mode
1623
+ if (state.options.darkMode) {
1624
+ document.documentElement.style.setProperty('--color-bg', '#0f172a');
1625
+ document.documentElement.style.setProperty('--color-bg-soft', '#1e293b');
1626
+ document.documentElement.style.setProperty('--color-text', '#f1f5f9');
1627
+ document.documentElement.style.setProperty('--color-muted', '#94a3b8');
1628
+ document.documentElement.style.setProperty('--color-border', '#334155');
1629
+ } else {
1630
+ document.documentElement.style.setProperty('--color-bg', '#ffffff');
1631
+ document.documentElement.style.setProperty('--color-bg-soft', '#f8fafc');
1632
+ document.documentElement.style.setProperty('--color-text', '#0f172a');
1633
+ document.documentElement.style.setProperty('--color-muted', '#475569');
1634
+ document.documentElement.style.setProperty('--color-border', '#e2e8f0');
1635
+ }
1636
+ }
1637
+
1638
+ // View mode switching
1639
+ function setViewMode(mode) {
1640
+ state.options.viewMode = mode;
1641
+ const container = document.querySelector('.diff-container');
1642
+
1643
+ if (mode === 'inline' || mode === 'unified') {
1644
+ container.classList.add('unified-view');
1645
+ document.getElementById('paneRight').style.display = 'none';
1646
+ document.querySelector('.line-connectors').style.display = 'none';
1647
+ } else {
1648
+ container.classList.remove('unified-view');
1649
+ document.getElementById('paneRight').style.display = 'flex';
1650
+ document.querySelector('.line-connectors').style.display = 'block';
1651
+ }
1652
+
1653
+ // Re-render if we have data
1654
+ if (state.result) compute();
1655
+ }
1656
+
1657
  // Events
1658
  function wireEvents() {
1659
  // File inputs
 
1698
  state.options.syncScroll = els.syncScroll.checked;
1699
  });
1700
 
1701
+ // Search
1702
+ els.searchBtn?.addEventListener('click', () => performSearch(els.searchInput.value));
1703
+ els.searchInput?.addEventListener('input', (e) => {
1704
+ if (e.target.value.length > 2) performSearch(e.target.value);
1705
+ else if (e.target.value === '') clearSearch();
1706
+ });
1707
+
1708
+ // Toolbar buttons
1709
+ els.wordDiffBtn?.addEventListener('click', () => {
1710
+ state.options.wordDiff = !state.options.wordDiff;
1711
+ els.wordDiffBtn.classList.toggle('active', state.options.wordDiff);
1712
+ if (state.result) compute();
1713
+ });
1714
+
1715
+ els.layoutBtn?.addEventListener('click', () => {
1716
+ const modes = ['side-by-side', 'inline', 'unified'];
1717
+ const currentIdx = modes.indexOf(state.options.viewMode);
1718
+ const nextMode = modes[(currentIdx + 1) % modes.length];
1719
+ setViewMode(nextMode);
1720
+ });
1721
+
1722
+ els.modeBtns?.forEach(btn => {
1723
+ btn.addEventListener('click', () => {
1724
+ els.modeBtns.forEach(b => b.classList.remove('active'));
1725
+ btn.classList.add('active');
1726
+ setViewMode(btn.dataset.mode);
1727
+ });
1728
+ });
1729
+
1730
+ els.toggleHeatmapBtn?.addEventListener('click', () => {
1731
+ state.showHeatmap = !state.showHeatmap;
1732
+ els.toggleHeatmapBtn.classList.toggle('active', state.showHeatmap);
1733
+ els.analyticsWrap.hidden = !state.showHeatmap;
1734
+ if (state.showHeatmap && state.result) renderHeatmap(state.result.aRes);
1735
+ });
1736
+
1737
+ els.toggleMinimapBtn?.addEventListener('click', () => {
1738
+ state.showMinimap = !state.showMinimap;
1739
+ els.toggleMinimapBtn.classList.add('active', state.showMinimap);
1740
+ els.minimap.hidden = !state.showMinimap;
1741
+ if (state.showMinimap && state.result) renderMinimap(state.result.aRes, state.result.bRes);
1742
+ });
1743
+
1744
+ els.toggleStructureBtn?.addEventListener('click', () => {
1745
+ state.showStructure = !state.showStructure;
1746
+ els.toggleStructureBtn.classList.toggle('active', state.showStructure);
1747
+ // Visual feedback only for now
1748
+ document.body.classList.toggle('structure-mode', state.showStructure);
1749
+ });
1750
+
1751
+ els.toggleSemanticsBtn?.addEventListener('click', () => {
1752
+ state.showSemantics = !state.showSemantics;
1753
+ els.toggleSemanticsBtn.classList.toggle('active', state.showSemantics);
1754
+ document.body.classList.toggle('semantic-mode', state.showSemantics);
1755
+ });
1756
+
1757
+ els.exportBtn?.addEventListener('click', () => {
1758
+ const format = confirm('Export as HTML? (Cancel for unified diff format)') ? 'html' : 'diff';
1759
+ exportDiff(format);
1760
+ });
1761
+
1762
+ els.reportBtn?.addEventListener('click', generateReport);
1763
+
1764
+ els.acceptAllBtn?.addEventListener('click', () => {
1765
+ document.querySelectorAll('.change-row').forEach(row => {
1766
+ row.style.opacity = '0.5';
1767
+ });
1768
+ });
1769
+
1770
+ els.rejectAllBtn?.addEventListener('click', () => {
1771
+ document.querySelectorAll('.change-row').forEach(row => {
1772
+ row.style.display = 'none';
1773
+ });
1774
+ });
1775
+
1776
+ els.minimapMode?.addEventListener('change', (e) => {
1777
+ // Re-render minimap with different mode
1778
+ if (state.result) renderMinimap(state.result.aRes, state.result.bRes);
1779
+ });
1780
+
1781
+ // Analytics panel toggles (mirror the toolbar ones)
1782
+ els.analyticsToggleHeatmap?.addEventListener('click', () => {
1783
+ els.toggleHeatmapBtn?.click();
1784
+ });
1785
+ els.analyticsToggleMinimap?.addEventListener('click', () => {
1786
+ els.toggleMinimapBtn?.click();
1787
+ });
1788
+ els.analyticsToggleStructure?.addEventListener('click', () => {
1789
+ els.toggleStructureBtn?.click();
1790
+ });
1791
+
1792
+ // Modal
1793
+ els.infoBtn?.addEventListener('click', () => {
1794
+ els.modalOverlay.classList.remove('hidden');
1795
+ });
1796
+ els.modalClose?.addEventListener('click', () => {
1797
+ els.modalOverlay.classList.add('hidden');
1798
+ });
1799
+ els.modalOverlay?.addEventListener('click', (e) => {
1800
+ if (e.target === els.modalOverlay) els.modalOverlay.classList.add('hidden');
1801
+ });
1802
+
1803
+ // Initial setup
1804
  setupSyncScrolling();
1805
+ setupDragAndDrop();
1806
+ setupKeyboardShortcuts();
1807
+
1808
+ // Add dark mode toggle to header
1809
+ const darkModeBtn = document.createElement('button');
1810
+ darkModeBtn.className = 'btn-ghost';
1811
+ darkModeBtn.innerHTML = 'πŸŒ™';
1812
+ darkModeBtn.title = 'Toggle Dark Mode';
1813
+ darkModeBtn.addEventListener('click', toggleDarkMode);
1814
+ document.querySelector('.view-controls')?.appendChild(darkModeBtn);
1815
  }
1816
 
1817
  // Init
style.css CHANGED
@@ -276,6 +276,273 @@ tr:hover .line .actions{ opacity: 1; }
276
  border-radius: 2px; padding: 1px 2px; text-decoration: line-through;
277
  }
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  .hover-preview{
280
  position: absolute; background: var(--color-bg); border: 1px solid var(--color-border);
281
  border-radius: 6px; padding: 8px; box-shadow: var(--shadow-md); z-index: 20;
 
276
  border-radius: 2px; padding: 1px 2px; text-decoration: line-through;
277
  }
278
 
279
+ /* Syntax Highlighting */
280
+ .syntax-keyword { color: #d73a49; font-weight: 600; }
281
+ .syntax-string { color: #22863a; }
282
+ .syntax-comment { color: #6a737d; font-style: italic; }
283
+ .syntax-number { color: #005cc5; }
284
+ .syntax-property { color: #6f42c1; }
285
+ .syntax-tag { color: #22863a; }
286
+
287
+ /* Dark mode syntax */
288
+ [data-theme="dark"] .syntax-keyword { color: #ff7b72; }
289
+ [data-theme="dark"] .syntax-string { color: #a5d6ff; }
290
+ [data-theme="dark"] .syntax-comment { color: #8b949e; }
291
+ [data-theme="dark"] .syntax-number { color: #79c0ff; }
292
+ [data-theme="dark"] .syntax-property { color: #d2a8ff; }
293
+ [data-theme="dark"] .syntax-tag { color: #7ee787; }
294
+
295
+ /* Search highlighting */
296
+ .search-highlight{
297
+ background: rgba(14, 165, 233, 0.3) !important;
298
+ animation: highlight-pulse 1s ease-in-out;
299
+ box-shadow: inset 0 0 0 2px var(--color-primary);
300
+ }
301
+
302
+ /* Drag and drop */
303
+ .file-block {
304
+ transition: all 0.2s ease;
305
+ }
306
+ .file-block.drag-over {
307
+ background: rgba(14, 165, 233, 0.1);
308
+ border-color: var(--color-primary);
309
+ transform: scale(1.02);
310
+ box-shadow: 0 0 20px rgba(14, 165, 233, 0.2);
311
+ }
312
+
313
+ /* Enhanced minimap */
314
+ .minimap-block {
315
+ transition: all 0.2s ease;
316
+ }
317
+ .minimap-block:hover {
318
+ transform: scaleX(1.2);
319
+ z-index: 10;
320
+ }
321
+
322
+ /* Trend chart */
323
+ .trend-bars {
324
+ display: flex;
325
+ align-items: flex-end;
326
+ height: 60px;
327
+ gap: 2px;
328
+ padding: 10px 0;
329
+ }
330
+ .trend-bar {
331
+ flex: 1;
332
+ border-radius: 2px 2px 0 0;
333
+ min-height: 4px;
334
+ transition: height 0.3s ease;
335
+ }
336
+
337
+ /* Semantic and structural analysis panels */
338
+ .semantic-item, .structural-item {
339
+ display: flex;
340
+ justify-content: space-between;
341
+ padding: 8px 0;
342
+ border-bottom: 1px solid var(--color-border);
343
+ }
344
+ .semantic-item:last-child, .structural-item:last-child {
345
+ border-bottom: none;
346
+ }
347
+ .semantic-label, .structural-label {
348
+ font-size: 13px;
349
+ color: var(--color-muted);
350
+ }
351
+ .semantic-value {
352
+ font-weight: 600;
353
+ color: var(--color-primary);
354
+ }
355
+ .structural-badge {
356
+ font-size: 11px;
357
+ padding: 2px 8px;
358
+ border-radius: 12px;
359
+ background: var(--color-bg-soft);
360
+ }
361
+ .structural-badge.changed {
362
+ background: rgba(239, 68, 68, 0.2);
363
+ color: var(--color-del-border);
364
+ }
365
+ .structural-badge.stable {
366
+ background: rgba(16, 185, 129, 0.2);
367
+ color: var(--color-added-border);
368
+ }
369
+
370
+ /* Heatmap enhancements */
371
+ .heatmap-grid {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: 1px;
375
+ }
376
+ .heatmap-line {
377
+ height: 4px;
378
+ border-radius: 2px;
379
+ background: var(--color-border);
380
+ transition: all 0.2s ease;
381
+ }
382
+ .heatmap-0 { opacity: 0.2; }
383
+ .heatmap-1 { background: var(--color-move-border); opacity: 0.5; }
384
+ .heatmap-2 { background: var(--color-mod-border); opacity: 0.7; }
385
+ .heatmap-3 { background: var(--color-del-border); opacity: 1; }
386
+
387
+ /* Active buttons */
388
+ .btn-ghost.active, .mode-btn.active, .feature-btn.active {
389
+ background: var(--color-primary) !important;
390
+ color: white !important;
391
+ border-color: var(--color-primary) !important;
392
+ }
393
+
394
+ /* Change row highlighting */
395
+ .change-row {
396
+ position: relative;
397
+ }
398
+ .change-row::before {
399
+ content: '';
400
+ position: absolute;
401
+ left: 0;
402
+ top: 0;
403
+ bottom: 0;
404
+ width: 3px;
405
+ background: transparent;
406
+ transition: background 0.2s ease;
407
+ }
408
+ .change-row[data-kind="deleted"]::before { background: var(--color-del-border); }
409
+ .change-row[data-kind="inserted"]::before { background: var(--color-added-border); }
410
+ .change-row[data-kind="modified"]::before { background: var(--color-mod-border); }
411
+ .change-row[data-kind="moved"]::before { background: var(--color-move-border); }
412
+
413
+ /* Gutter change indicators */
414
+ .gutter-change {
415
+ font-weight: 700;
416
+ color: var(--color-text) !important;
417
+ }
418
+
419
+ /* Unified view mode */
420
+ .unified-view {
421
+ grid-template-columns: 1fr !important;
422
+ }
423
+ .unified-view #paneRight {
424
+ display: none !important;
425
+ }
426
+ .unified-view .line-connectors {
427
+ display: none !important;
428
+ }
429
+
430
+ /* Copy feedback */
431
+ .content {
432
+ position: relative;
433
+ cursor: pointer;
434
+ }
435
+ .content:hover::after {
436
+ content: 'Double-click to copy';
437
+ position: absolute;
438
+ right: 10px;
439
+ top: 50%;
440
+ transform: translateY(-50%);
441
+ font-size: 10px;
442
+ color: var(--color-muted);
443
+ background: var(--color-bg);
444
+ padding: 2px 6px;
445
+ border-radius: 4px;
446
+ border: 1px solid var(--color-border);
447
+ opacity: 0;
448
+ transition: opacity 0.2s ease;
449
+ }
450
+ .content:hover::after {
451
+ opacity: 1;
452
+ }
453
+
454
+ /* Enhanced stats grid */
455
+ .stats-grid {
456
+ display: grid;
457
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
458
+ gap: 12px;
459
+ margin: 16px;
460
+ }
461
+ .stat-card {
462
+ background: var(--color-bg);
463
+ border: 1px solid var(--color-border);
464
+ border-radius: var(--radius);
465
+ padding: 12px;
466
+ display: flex;
467
+ align-items: center;
468
+ gap: 10px;
469
+ box-shadow: var(--shadow-sm);
470
+ transition: transform 0.2s ease;
471
+ }
472
+ .stat-card:hover {
473
+ transform: translateY(-2px);
474
+ box-shadow: var(--shadow-md);
475
+ }
476
+ .stat-icon {
477
+ width: 32px;
478
+ height: 32px;
479
+ border-radius: 8px;
480
+ display: grid;
481
+ place-items: center;
482
+ font-weight: 700;
483
+ font-size: 14px;
484
+ }
485
+ .stat-icon.added { background: var(--color-added-bg); color: var(--color-added-border); }
486
+ .stat-icon.deleted { background: var(--color-del-bg); color: var(--color-del-border); }
487
+ .stat-icon.modified { background: var(--color-mod-bg); color: var(--color-mod-border); }
488
+ .stat-icon.moved { background: var(--color-move-bg); color: var(--color-move-border); }
489
+ .stat-icon.semantic { background: #f3e8ff; color: #9333ea; }
490
+ .stat-icon.complexity { background: #fef3c7; color: #d97706; }
491
+ .stat-content {
492
+ display: flex;
493
+ flex-direction: column;
494
+ }
495
+ .stat-content .label {
496
+ font-size: 11px;
497
+ color: var(--color-muted);
498
+ text-transform: uppercase;
499
+ letter-spacing: 0.5px;
500
+ }
501
+ .stat-content .value {
502
+ font-size: 18px;
503
+ font-weight: 700;
504
+ color: var(--color-text);
505
+ }
506
+
507
+ /* Comparison toolbar enhancements */
508
+ .comparison-toolbar {
509
+ display: grid;
510
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
511
+ gap: 20px;
512
+ padding: 16px;
513
+ background: var(--color-bg-soft);
514
+ border-bottom: 1px solid var(--color-border);
515
+ margin: 0 16px;
516
+ border-radius: var(--radius);
517
+ margin-bottom: 12px;
518
+ }
519
+ .toolbar-section h4 {
520
+ margin: 0 0 10px 0;
521
+ font-size: 12px;
522
+ text-transform: uppercase;
523
+ letter-spacing: 0.5px;
524
+ color: var(--color-muted);
525
+ }
526
+ .mode-buttons, .feature-toggles, .action-buttons {
527
+ display: flex;
528
+ gap: 8px;
529
+ flex-wrap: wrap;
530
+ }
531
+ .mode-btn, .feature-btn, .action-btn {
532
+ padding: 6px 12px;
533
+ border: 1px solid var(--color-border);
534
+ background: var(--color-bg);
535
+ color: var(--color-text);
536
+ border-radius: 6px;
537
+ cursor: pointer;
538
+ font-size: 13px;
539
+ transition: all 0.2s ease;
540
+ }
541
+ .mode-btn:hover, .feature-btn:hover, .action-btn:hover {
542
+ border-color: var(--color-primary);
543
+ color: var(--color-primary);
544
+ }
545
+
546
  .hover-preview{
547
  position: absolute; background: var(--color-bg); border: 1px solid var(--color-border);
548
  border-radius: 6px; padding: 8px; box-shadow: var(--shadow-md); z-index: 20;