Skip to content

Commit 1d5c8f8

Browse files
hrhrngclaude
andcommitted
✨ Add diff mode with side-by-side comparison
- Left panel: Current document (editable with working undo/redo) - Right panel: Selectable document for comparison (read-only) - Monaco DiffEditor for professional diff visualization - Fixed cursor position issues during editing - Fixed keyboard shortcuts (Cmd+Z for undo, etc.) - "Show only differences" checkbox to toggle view - Left panel always visible before comparison selection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 393f425 commit 1d5c8f8

4 files changed

Lines changed: 459 additions & 2 deletions

File tree

image.png

566 KB
Loading

src/components/Layout/MainLayout.tsx

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState, useRef } from 'react'
2-
import Editor, { loader } from '@monaco-editor/react'
2+
import Editor, { DiffEditor, loader } from '@monaco-editor/react'
33
import { useDocumentStore } from '@stores/documentStore'
44
import { useAppStore } from '@stores/appStore'
55
import { JSONLayerAnalyzer } from '@utils/jsonAnalyzer'
@@ -58,6 +58,8 @@ export function MainLayout() {
5858
const [showDocSelector, setShowDocSelector] = useState(false)
5959
const [draggedIndex, setDraggedIndex] = useState<number | null>(null)
6060
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
61+
const [compareDocId, setCompareDocId] = useState<string | null>(null)
62+
const [showOnlyDifferences, setShowOnlyDifferences] = useState(true)
6163
const inputRef = useRef<HTMLInputElement>(null)
6264

6365
const currentDoc = getCurrentDocument()
@@ -625,6 +627,205 @@ export function MainLayout() {
625627
</div>
626628
)
627629

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+
628829
case 'hero':
629830
return (
630831
<div className="content" id="heroMode">
@@ -1012,6 +1213,10 @@ export function MainLayout() {
10121213
return null // Hero mode has inline buttons
10131214
}
10141215

1216+
if (viewMode === 'diff') {
1217+
return null // Diff mode doesn't need actions
1218+
}
1219+
10151220
// Layer mode
10161221
return (
10171222
<div className="actions">
@@ -1054,6 +1259,15 @@ export function MainLayout() {
10541259
</svg>
10551260
<span className="mode-label">HERO</span>
10561261
</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>
10571271
</div>
10581272

10591273
<div className="main-area">
@@ -1149,6 +1363,9 @@ export function MainLayout() {
11491363
{viewMode === 'layer' && `${currentDoc?.layers.length || 0} layers`}
11501364
{viewMode === 'processor' && 'Processor'}
11511365
{viewMode === 'hero' && 'Hero View'}
1366+
{viewMode === 'diff' && (compareDocId
1367+
? `Comparing: ${documents.find(d => d.id === compareDocId)?.title}${currentDoc?.title}`
1368+
: 'Select document to compare')}
11521369
</div>
11531370
</div>
11541371
</div>

src/stores/appStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { create } from 'zustand'
22

3-
type ViewMode = 'layer' | 'processor' | 'hero'
3+
type ViewMode = 'layer' | 'processor' | 'hero' | 'diff'
44

55
interface AppStore {
66
viewMode: ViewMode

0 commit comments

Comments
 (0)