💄 diff view for revisions
This commit is contained in:
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import { useEditorStore } from '@renderer/store/editorStore'
|
import { useEditorStore } from '@renderer/store/editorStore'
|
||||||
import { MarkdownEditor } from '@renderer/components/Editor/MarkdownEditor'
|
import { MarkdownEditor } from '@renderer/components/Editor/MarkdownEditor'
|
||||||
import { ChatPanel } from '@renderer/components/AIChat/ChatPanel'
|
import { ChatPanel } from '@renderer/components/AIChat/ChatPanel'
|
||||||
|
import { RevisionPanel } from '@renderer/components/Revisions/RevisionPanel'
|
||||||
import '@renderer/styles/global.css'
|
import '@renderer/styles/global.css'
|
||||||
import '@renderer/styles/app.css'
|
import '@renderer/styles/app.css'
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ store.setAnnotations([
|
|||||||
])
|
])
|
||||||
|
|
||||||
function DemoApp(): JSX.Element {
|
function DemoApp(): JSX.Element {
|
||||||
const [revActive, setRevActive] = useState(false)
|
const { revisionPanelOpen, toggleRevisionPanel } = useEditorStore()
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: 'var(--bg-base)' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: 'var(--bg-base)' }}>
|
||||||
{/* Titlebar – same markup as App.tsx */}
|
{/* Titlebar – same markup as App.tsx */}
|
||||||
@@ -83,8 +84,8 @@ function DemoApp(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<div className="app-titlebar-icon-group">
|
<div className="app-titlebar-icon-group">
|
||||||
<button
|
<button
|
||||||
className={`app-titlebar-theme-btn app-titlebar-revision-btn${revActive ? ' active' : ''}`}
|
className={`app-titlebar-theme-btn app-titlebar-revision-btn${revisionPanelOpen ? ' active' : ''}`}
|
||||||
onClick={() => setRevActive(v => !v)}
|
onClick={toggleRevisionPanel}
|
||||||
title="Revision history"
|
title="Revision history"
|
||||||
>⟳</button>
|
>⟳</button>
|
||||||
<button className="app-titlebar-theme-btn" title="Toggle theme">☀</button>
|
<button className="app-titlebar-theme-btn" title="Toggle theme">☀</button>
|
||||||
@@ -93,8 +94,9 @@ function DemoApp(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
{/* Editor content below */}
|
{/* Editor content below */}
|
||||||
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
||||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', position: 'relative' }}>
|
||||||
<MarkdownEditor />
|
<MarkdownEditor />
|
||||||
|
{revisionPanelOpen && <RevisionPanel />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '300px', flexShrink: 0 }}>
|
<div style={{ width: '300px', flexShrink: 0 }}>
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -17,8 +17,10 @@
|
|||||||
"@codemirror/view": "^6.35.0",
|
"@codemirror/view": "^6.35.0",
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@types/diff": "^7.0.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"diff": "^8.0.3",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
"marked": "^17.0.3",
|
"marked": "^17.0.3",
|
||||||
"pdf-parse": "^2.4.5",
|
"pdf-parse": "^2.4.5",
|
||||||
@@ -2175,6 +2177,12 @@
|
|||||||
"@types/ms": "*"
|
"@types/ms": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/diff": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/dompurify": {
|
"node_modules/@types/dompurify": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
@@ -3452,6 +3460,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "8.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz",
|
||||||
|
"integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dir-compare": {
|
"node_modules/dir-compare": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
|
||||||
|
|||||||
@@ -32,8 +32,10 @@
|
|||||||
"@codemirror/view": "^6.35.0",
|
"@codemirror/view": "^6.35.0",
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@types/diff": "^7.0.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
"diff": "^8.0.3",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
"marked": "^17.0.3",
|
"marked": "^17.0.3",
|
||||||
"pdf-parse": "^2.4.5",
|
"pdf-parse": "^2.4.5",
|
||||||
|
|||||||
@@ -188,3 +188,144 @@
|
|||||||
.revision-restore-btn:hover {
|
.revision-restore-btn:hover {
|
||||||
background: var(--accent-hover);
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── View mode toggle (Diff | Preview) ─────────────────────────────────── */
|
||||||
|
.revision-view-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--toolbar-bg);
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-view-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 8px 10px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-view-btn:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision-view-btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Shared scroll container ────────────────────────────────────────────── */
|
||||||
|
.revision-preview-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Diff renderer ──────────────────────────────────────────────────────── */
|
||||||
|
.rdiff-root {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.75;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
min-height: 1.75em;
|
||||||
|
padding-right: 16px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-gutter {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
user-select: none;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Added lines (in current, not in revision) */
|
||||||
|
.rdiff-line--added {
|
||||||
|
background: rgba(80, 160, 80, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line--added .rdiff-gutter {
|
||||||
|
color: #6abf6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line--added .rdiff-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Removed lines (in revision, not in current) */
|
||||||
|
.rdiff-line--removed {
|
||||||
|
background: rgba(180, 60, 60, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line--removed .rdiff-gutter {
|
||||||
|
color: #c07070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line--removed .rdiff-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: rgba(180, 60, 60, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unchanged context lines */
|
||||||
|
.rdiff-line--context .rdiff-gutter {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-line--context .rdiff-text {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Changed hunk group */
|
||||||
|
.rdiff-hunk {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin: 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expand collapsed context */
|
||||||
|
.rdiff-collapse-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdiff-collapse-btn:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { diffLines } from 'diff'
|
||||||
|
import type { Change } from 'diff'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { useEditorStore } from '../../store/editorStore'
|
import { useEditorStore } from '../../store/editorStore'
|
||||||
@@ -6,6 +8,126 @@ import { currentEditorView } from '../Editor/MarkdownEditor'
|
|||||||
import type { RevisionMeta } from '../../types/editor'
|
import type { RevisionMeta } from '../../types/editor'
|
||||||
import './RevisionPanel.css'
|
import './RevisionPanel.css'
|
||||||
|
|
||||||
|
// ─── Diff view ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CONTEXT_LINES = 3
|
||||||
|
|
||||||
|
type Segment =
|
||||||
|
| { kind: 'changed'; changes: Change[] }
|
||||||
|
| { kind: 'context'; lines: string[]; segIndex: number }
|
||||||
|
|
||||||
|
interface DiffViewProps {
|
||||||
|
oldText: string
|
||||||
|
newText: string
|
||||||
|
expandedSegments: Set<number>
|
||||||
|
onExpand: (segIndex: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiffView({ oldText, newText, expandedSegments, onExpand }: DiffViewProps): JSX.Element {
|
||||||
|
const changes = diffLines(oldText, newText)
|
||||||
|
|
||||||
|
// Build flat list of segments: changed hunks and unchanged context runs
|
||||||
|
const segments: Segment[] = []
|
||||||
|
let pendingLines: string[] = []
|
||||||
|
let segCounter = 0
|
||||||
|
|
||||||
|
const flushContext = (): void => {
|
||||||
|
if (pendingLines.length > 0) {
|
||||||
|
segments.push({ kind: 'context', lines: pendingLines, segIndex: segCounter++ })
|
||||||
|
pendingLines = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.added || change.removed) {
|
||||||
|
flushContext()
|
||||||
|
const last = segments[segments.length - 1]
|
||||||
|
if (last?.kind === 'changed') {
|
||||||
|
last.changes.push(change)
|
||||||
|
} else {
|
||||||
|
segments.push({ kind: 'changed', changes: [change] })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lines = change.value.split('\n')
|
||||||
|
if (lines[lines.length - 1] === '') lines.pop()
|
||||||
|
pendingLines.push(...lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushContext()
|
||||||
|
|
||||||
|
if (segments.length === 0 || !changes.some((c) => c.added || c.removed)) {
|
||||||
|
return <p className="revision-preview-empty">No changes — this revision matches the current content.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rdiff-root">
|
||||||
|
{segments.map((seg, i) => {
|
||||||
|
if (seg.kind === 'changed') {
|
||||||
|
return (
|
||||||
|
<div key={i} className="rdiff-hunk">
|
||||||
|
{seg.changes.flatMap((ch, j) => {
|
||||||
|
const prefix = ch.added ? '+' : '-'
|
||||||
|
const cls = ch.added ? 'rdiff-line--added' : 'rdiff-line--removed'
|
||||||
|
const lines = ch.value.split('\n')
|
||||||
|
if (lines[lines.length - 1] === '') lines.pop()
|
||||||
|
return lines.map((line, k) => (
|
||||||
|
<div key={`${j}-${k}`} className={`rdiff-line ${cls}`}>
|
||||||
|
<span className="rdiff-gutter">{prefix}</span>
|
||||||
|
<span className="rdiff-text">{line || '\u00A0'}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lines, segIndex } = seg
|
||||||
|
const total = lines.length
|
||||||
|
const isExpanded = expandedSegments.has(segIndex)
|
||||||
|
|
||||||
|
if (total <= CONTEXT_LINES * 2 || isExpanded) {
|
||||||
|
return (
|
||||||
|
<div key={i} className="rdiff-context">
|
||||||
|
{lines.map((line, k) => (
|
||||||
|
<div key={k} className="rdiff-line rdiff-line--context">
|
||||||
|
<span className="rdiff-gutter"> </span>
|
||||||
|
<span className="rdiff-text">{line || '\u00A0'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const topLines = lines.slice(0, CONTEXT_LINES)
|
||||||
|
const bottomLines = lines.slice(total - CONTEXT_LINES)
|
||||||
|
const hiddenCount = total - CONTEXT_LINES * 2
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={i} className="rdiff-context">
|
||||||
|
{topLines.map((line, k) => (
|
||||||
|
<div key={`t${k}`} className="rdiff-line rdiff-line--context">
|
||||||
|
<span className="rdiff-gutter"> </span>
|
||||||
|
<span className="rdiff-text">{line || '\u00A0'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button className="rdiff-collapse-btn" onClick={() => onExpand(segIndex)}>
|
||||||
|
··· {hiddenCount} unchanged line{hiddenCount !== 1 ? 's' : ''} ···
|
||||||
|
</button>
|
||||||
|
{bottomLines.map((line, k) => (
|
||||||
|
<div key={`b${k}`} className="rdiff-line rdiff-line--context">
|
||||||
|
<span className="rdiff-gutter"> </span>
|
||||||
|
<span className="rdiff-text">{line || '\u00A0'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatDate(ts: number): string {
|
function formatDate(ts: number): string {
|
||||||
const d = new Date(ts)
|
const d = new Date(ts)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -17,12 +139,19 @@ function formatDate(ts: number): string {
|
|||||||
return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' · ' + time
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' · ' + time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── RevisionPanel ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ViewMode = 'diff' | 'preview'
|
||||||
|
|
||||||
export function RevisionPanel(): JSX.Element {
|
export function RevisionPanel(): JSX.Element {
|
||||||
const { activeFilePath, toggleRevisionPanel, revisions, setRevisions } = useEditorStore()
|
const { activeFilePath, activeFileContent, toggleRevisionPanel, revisions, setRevisions } =
|
||||||
|
useEditorStore()
|
||||||
const [selected, setSelected] = useState<RevisionMeta | null>(null)
|
const [selected, setSelected] = useState<RevisionMeta | null>(null)
|
||||||
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
const [previewHtml, setPreviewHtml] = useState<string | null>(null)
|
||||||
const [previewRaw, setPreviewRaw] = useState<string | null>(null)
|
const [previewRaw, setPreviewRaw] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('diff')
|
||||||
|
const [expandedSegments, setExpandedSegments] = useState<Set<number>>(new Set())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeFilePath) return
|
if (!activeFilePath) return
|
||||||
@@ -37,6 +166,8 @@ export function RevisionPanel(): JSX.Element {
|
|||||||
setSelected(rev)
|
setSelected(rev)
|
||||||
setPreviewHtml(null)
|
setPreviewHtml(null)
|
||||||
setPreviewRaw(null)
|
setPreviewRaw(null)
|
||||||
|
setViewMode('diff')
|
||||||
|
setExpandedSegments(new Set())
|
||||||
if (!activeFilePath) return
|
if (!activeFilePath) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -82,6 +213,8 @@ export function RevisionPanel(): JSX.Element {
|
|||||||
<span className="revision-panel-title">Revision History</span>
|
<span className="revision-panel-title">Revision History</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="revision-panel-body">
|
<div className="revision-panel-body">
|
||||||
|
|
||||||
|
{/* ── Left: revision list ── */}
|
||||||
<div className="revision-list">
|
<div className="revision-list">
|
||||||
{revisions.length === 0 ? (
|
{revisions.length === 0 ? (
|
||||||
<p className="revision-empty">No revisions yet.<br />Save with ⌘S to create one.</p>
|
<p className="revision-empty">No revisions yet.<br />Save with ⌘S to create one.</p>
|
||||||
@@ -106,17 +239,47 @@ export function RevisionPanel(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Right: preview / diff ── */}
|
||||||
<div className="revision-preview">
|
<div className="revision-preview">
|
||||||
{selected === null ? (
|
{selected === null ? (
|
||||||
<p className="revision-preview-empty">Select a revision to preview</p>
|
<p className="revision-preview-empty">Select a revision to preview</p>
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
<p className="revision-preview-empty">Loading…</p>
|
<p className="revision-preview-empty">Loading…</p>
|
||||||
) : previewHtml !== null ? (
|
) : previewRaw !== null ? (
|
||||||
<>
|
<>
|
||||||
|
<div className="revision-view-toggle">
|
||||||
|
<button
|
||||||
|
className={`revision-view-btn${viewMode === 'diff' ? ' active' : ''}`}
|
||||||
|
onClick={() => setViewMode('diff')}
|
||||||
|
>
|
||||||
|
Diff
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`revision-view-btn${viewMode === 'preview' ? ' active' : ''}`}
|
||||||
|
onClick={() => setViewMode('preview')}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="revision-preview-scroll">
|
||||||
|
{viewMode === 'diff' ? (
|
||||||
|
<DiffView
|
||||||
|
oldText={previewRaw}
|
||||||
|
newText={activeFileContent}
|
||||||
|
expandedSegments={expandedSegments}
|
||||||
|
onExpand={(idx) =>
|
||||||
|
setExpandedSegments((prev) => new Set([...prev, idx]))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="revision-preview-content"
|
className="revision-preview-content"
|
||||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
dangerouslySetInnerHTML={{ __html: previewHtml ?? '' }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="revision-preview-footer">
|
<div className="revision-preview-footer">
|
||||||
<button className="revision-restore-btn" onClick={restore}>
|
<button className="revision-restore-btn" onClick={restore}>
|
||||||
Restore this version
|
Restore this version
|
||||||
@@ -125,6 +288,7 @@ export function RevisionPanel(): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user