|
1 | 1 | import { useEffect, useState, useRef } from 'react' |
2 | | -import Editor, { loader } from '@monaco-editor/react' |
| 2 | +import Editor, { DiffEditor, loader } from '@monaco-editor/react' |
3 | 3 | import { useDocumentStore } from '@stores/documentStore' |
4 | 4 | import { useAppStore } from '@stores/appStore' |
5 | 5 | import { JSONLayerAnalyzer } from '@utils/jsonAnalyzer' |
@@ -58,6 +58,8 @@ export function MainLayout() { |
58 | 58 | const [showDocSelector, setShowDocSelector] = useState(false) |
59 | 59 | const [draggedIndex, setDraggedIndex] = useState<number | null>(null) |
60 | 60 | const [dragOverIndex, setDragOverIndex] = useState<number | null>(null) |
| 61 | + const [compareDocId, setCompareDocId] = useState<string | null>(null) |
| 62 | + const [showOnlyDifferences, setShowOnlyDifferences] = useState(true) |
61 | 63 | const inputRef = useRef<HTMLInputElement>(null) |
62 | 64 |
|
63 | 65 | const currentDoc = getCurrentDocument() |
@@ -625,6 +627,205 @@ export function MainLayout() { |
625 | 627 | </div> |
626 | 628 | ) |
627 | 629 |
|
| 630 | + case 'diff': |
| 631 | + const otherDocs = documents.filter(d => d.id !== currentDocId) |
| 632 | + const compareDoc = compareDocId ? documents.find(d => d.id === compareDocId) : null |
| 633 | + |
| 634 | + // Format JSON for better comparison |
| 635 | + const formatJson = (content: string) => { |
| 636 | + try { |
| 637 | + const parsed = JSON.parse(content) |
| 638 | + return JSON.stringify(parsed, null, 2) |
| 639 | + } catch { |
| 640 | + return content |
| 641 | + } |
| 642 | + } |
| 643 | + |
| 644 | + return ( |
| 645 | + <div className="content" id="diffMode" style={{ display: 'flex', flexDirection: 'column' }}> |
| 646 | + <div className="panel-header" style={{ |
| 647 | + flexShrink: 0, |
| 648 | + display: 'flex', |
| 649 | + alignItems: 'center', |
| 650 | + padding: '0 20px', |
| 651 | + height: '35px', |
| 652 | + borderBottom: '1px solid var(--border)' |
| 653 | + }}> |
| 654 | + <span style={{ display: 'flex', alignItems: 'center', gap: '15px' }}> |
| 655 | + <span style={{ color: 'var(--text-secondary)', fontSize: '11px', fontWeight: '600' }}> |
| 656 | + DIFF |
| 657 | + </span> |
| 658 | + <span className="panel-info" style={{ fontSize: '11px' }}> |
| 659 | + {currentDoc?.title} |
| 660 | + </span> |
| 661 | + <span style={{ color: 'var(--text-dim)', fontSize: '11px' }}>vs</span> |
| 662 | + <select |
| 663 | + value={compareDocId || ''} |
| 664 | + onChange={(e) => setCompareDocId(e.target.value || null)} |
| 665 | + style={{ |
| 666 | + background: 'var(--bg-panel)', |
| 667 | + border: '1px solid var(--border)', |
| 668 | + color: 'var(--text-secondary)', |
| 669 | + padding: '4px 8px', |
| 670 | + borderRadius: '4px', |
| 671 | + fontSize: '11px', |
| 672 | + fontFamily: 'JetBrains Mono, monospace', |
| 673 | + cursor: 'pointer', |
| 674 | + minWidth: '150px' |
| 675 | + }} |
| 676 | + > |
| 677 | + <option value="">Select document...</option> |
| 678 | + {otherDocs.map(doc => ( |
| 679 | + <option key={doc.id} value={doc.id}>{doc.title}</option> |
| 680 | + ))} |
| 681 | + </select> |
| 682 | + </span> |
| 683 | + |
| 684 | + {compareDoc && ( |
| 685 | + <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '12px' }}> |
| 686 | + <label style={{ |
| 687 | + display: 'flex', |
| 688 | + alignItems: 'center', |
| 689 | + gap: '6px', |
| 690 | + fontSize: '11px', |
| 691 | + color: 'var(--text-dim)', |
| 692 | + cursor: 'pointer' |
| 693 | + }}> |
| 694 | + <input |
| 695 | + type="checkbox" |
| 696 | + checked={showOnlyDifferences} |
| 697 | + onChange={(e) => setShowOnlyDifferences(e.target.checked)} |
| 698 | + style={{ |
| 699 | + cursor: 'pointer' |
| 700 | + }} |
| 701 | + /> |
| 702 | + Show only differences |
| 703 | + </label> |
| 704 | + </div> |
| 705 | + )} |
| 706 | + </div> |
| 707 | + |
| 708 | + <div style={{ flex: 1, overflow: 'hidden' }}> |
| 709 | + {compareDoc && currentDoc ? ( |
| 710 | + <DiffEditor |
| 711 | + height="100%" |
| 712 | + language="json" |
| 713 | + theme="superJSON" |
| 714 | + originalModelPath={`original-${currentDoc.id}.json`} |
| 715 | + modifiedModelPath={`modified-${compareDoc.id}.json`} |
| 716 | + original={formatJson(currentDoc.inputContent)} |
| 717 | + modified={formatJson(compareDoc.inputContent)} |
| 718 | + options={{ |
| 719 | + fontSize: 13, |
| 720 | + minimap: { enabled: false }, |
| 721 | + wordWrap: 'on', |
| 722 | + scrollBeyondLastLine: false, |
| 723 | + automaticLayout: true, |
| 724 | + readOnly: false, |
| 725 | + renderSideBySide: true, |
| 726 | + renderIndicators: true, |
| 727 | + originalEditable: true, // Left side editable |
| 728 | + modifiedEditable: false, // Right side read-only |
| 729 | + ignoreTrimWhitespace: false, |
| 730 | + renderOverviewRuler: true, |
| 731 | + enableSplitViewResizing: true, |
| 732 | + renderLineHighlight: 'all', |
| 733 | + fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Source Han Sans SC', monospace", |
| 734 | + diffWordWrap: 'on', |
| 735 | + diffAlgorithm: 'advanced', |
| 736 | + renderWhitespace: 'none', |
| 737 | + hideUnchangedRegions: { |
| 738 | + enabled: showOnlyDifferences, |
| 739 | + revealLineCount: 3, |
| 740 | + minimumLineCount: 3, |
| 741 | + contextLineCount: 3 |
| 742 | + } |
| 743 | + }} |
| 744 | + onMount={(diffEditor, monaco) => { |
| 745 | + const originalEditor = diffEditor.getOriginalEditor() |
| 746 | + |
| 747 | + // Ensure the editor has proper undo/redo stack |
| 748 | + originalEditor.pushUndoStop() |
| 749 | + |
| 750 | + // Focus the original editor for shortcuts to work |
| 751 | + originalEditor.focus() |
| 752 | + |
| 753 | + // Handle changes to the original (left) side without losing cursor position |
| 754 | + let isInternalUpdate = false |
| 755 | + originalEditor.onDidChangeModelContent(() => { |
| 756 | + if (isInternalUpdate) return |
| 757 | + |
| 758 | + const newContent = originalEditor.getValue() |
| 759 | + if (currentDoc) { |
| 760 | + // Update the store without triggering re-render |
| 761 | + updateInputContent(currentDoc.id, newContent) |
| 762 | + } |
| 763 | + }) |
| 764 | + }} |
| 765 | + /> |
| 766 | + ) : ( |
| 767 | + // Show current document on left, empty on right when no comparison selected |
| 768 | + <div style={{ display: 'flex', height: '100%' }}> |
| 769 | + <div style={{ flex: 1, borderRight: '1px solid var(--border)' }}> |
| 770 | + {currentDoc && ( |
| 771 | + <Editor |
| 772 | + height="100%" |
| 773 | + defaultLanguage="json" |
| 774 | + theme="superJSON" |
| 775 | + key={`standalone-${currentDoc.id}`} |
| 776 | + defaultValue={formatJson(currentDoc.inputContent)} |
| 777 | + onChange={(value) => { |
| 778 | + if (value !== undefined && currentDoc) { |
| 779 | + updateInputContent(currentDoc.id, value) |
| 780 | + } |
| 781 | + }} |
| 782 | + onMount={(editor, monaco) => { |
| 783 | + // Set initial value |
| 784 | + editor.setValue(formatJson(currentDoc.inputContent)) |
| 785 | + |
| 786 | + // Ensure editor has focus for shortcuts to work |
| 787 | + editor.focus() |
| 788 | + |
| 789 | + // Make sure undo/redo stack is maintained |
| 790 | + editor.pushUndoStop() |
| 791 | + }} |
| 792 | + options={{ |
| 793 | + fontSize: 13, |
| 794 | + minimap: { enabled: false }, |
| 795 | + wordWrap: 'on', |
| 796 | + lineNumbers: 'on', |
| 797 | + scrollBeyondLastLine: false, |
| 798 | + automaticLayout: true, |
| 799 | + readOnly: false, |
| 800 | + formatOnPaste: true, |
| 801 | + formatOnType: true, |
| 802 | + folding: true, |
| 803 | + tabSize: 2, |
| 804 | + fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Source Han Sans SC', monospace", |
| 805 | + // Ensure shortcuts work |
| 806 | + contextmenu: true, |
| 807 | + suggestOnTriggerCharacters: true, |
| 808 | + }} |
| 809 | + /> |
| 810 | + )} |
| 811 | + </div> |
| 812 | + <div style={{ |
| 813 | + flex: 1, |
| 814 | + display: 'flex', |
| 815 | + alignItems: 'center', |
| 816 | + justifyContent: 'center', |
| 817 | + color: 'var(--text-dim)', |
| 818 | + fontFamily: 'JetBrains Mono, monospace', |
| 819 | + fontSize: '12px' |
| 820 | + }}> |
| 821 | + Select a document to compare |
| 822 | + </div> |
| 823 | + </div> |
| 824 | + )} |
| 825 | + </div> |
| 826 | + </div> |
| 827 | + ) |
| 828 | + |
628 | 829 | case 'hero': |
629 | 830 | return ( |
630 | 831 | <div className="content" id="heroMode"> |
@@ -1012,6 +1213,10 @@ export function MainLayout() { |
1012 | 1213 | return null // Hero mode has inline buttons |
1013 | 1214 | } |
1014 | 1215 |
|
| 1216 | + if (viewMode === 'diff') { |
| 1217 | + return null // Diff mode doesn't need actions |
| 1218 | + } |
| 1219 | + |
1015 | 1220 | // Layer mode |
1016 | 1221 | return ( |
1017 | 1222 | <div className="actions"> |
@@ -1054,6 +1259,15 @@ export function MainLayout() { |
1054 | 1259 | </svg> |
1055 | 1260 | <span className="mode-label">HERO</span> |
1056 | 1261 | </button> |
| 1262 | + <button |
| 1263 | + className={`mode-btn ${viewMode === 'diff' ? 'active' : ''}`} |
| 1264 | + onClick={() => setViewMode('diff')} |
| 1265 | + > |
| 1266 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> |
| 1267 | + <path d="M16 2v20M8 2v20M3 12h18"/> |
| 1268 | + </svg> |
| 1269 | + <span className="mode-label">DIFF</span> |
| 1270 | + </button> |
1057 | 1271 | </div> |
1058 | 1272 |
|
1059 | 1273 | <div className="main-area"> |
@@ -1149,6 +1363,9 @@ export function MainLayout() { |
1149 | 1363 | {viewMode === 'layer' && `${currentDoc?.layers.length || 0} layers`} |
1150 | 1364 | {viewMode === 'processor' && 'Processor'} |
1151 | 1365 | {viewMode === 'hero' && 'Hero View'} |
| 1366 | + {viewMode === 'diff' && (compareDocId |
| 1367 | + ? `Comparing: ${documents.find(d => d.id === compareDocId)?.title} ↔ ${currentDoc?.title}` |
| 1368 | + : 'Select document to compare')} |
1152 | 1369 | </div> |
1153 | 1370 | </div> |
1154 | 1371 | </div> |
|
0 commit comments