documentation-hub 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SideBySideDiff - GitHub/VS Code style side-by-side document comparison
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Word-level diff highlighting
|
|
6
|
+
* - Synchronized scrolling
|
|
7
|
+
* - Line numbers on both sides
|
|
8
|
+
* - Change indicators in gutter (+, -, ~)
|
|
9
|
+
* - Jump to next/previous change
|
|
10
|
+
* - Collapse unchanged sections
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, useRef, useCallback, useMemo } from 'react';
|
|
14
|
+
import {
|
|
15
|
+
Columns,
|
|
16
|
+
ChevronUp,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
Link,
|
|
19
|
+
Unlink,
|
|
20
|
+
Minimize2,
|
|
21
|
+
Eye,
|
|
22
|
+
EyeOff,
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import { cn } from '@/utils/cn';
|
|
25
|
+
import { generateDocumentDiff, filterChangedParagraphs } from '@/utils/diffUtils';
|
|
26
|
+
import type { ParagraphDiff, DiffSegment } from '@/types/editor';
|
|
27
|
+
|
|
28
|
+
interface SideBySideDiffProps {
|
|
29
|
+
/** Original content (pre-processing paragraphs) */
|
|
30
|
+
originalContent: string[];
|
|
31
|
+
/** Modified content (post-processing paragraphs) */
|
|
32
|
+
modifiedContent: string[];
|
|
33
|
+
/** Enable synchronized scrolling (default: true) */
|
|
34
|
+
syncScroll?: boolean;
|
|
35
|
+
/** Show line numbers (default: true) */
|
|
36
|
+
showLineNumbers?: boolean;
|
|
37
|
+
/** Number of context lines around changes (default: 3) */
|
|
38
|
+
contextLines?: number;
|
|
39
|
+
/** Collapse unchanged sections (default: true) */
|
|
40
|
+
collapseUnchanged?: boolean;
|
|
41
|
+
/** Height of the component */
|
|
42
|
+
height?: string | number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render diff segments with appropriate styling
|
|
47
|
+
*/
|
|
48
|
+
function DiffSegments({
|
|
49
|
+
segments,
|
|
50
|
+
side,
|
|
51
|
+
}: {
|
|
52
|
+
segments: DiffSegment[];
|
|
53
|
+
side: 'left' | 'right';
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{segments.map((segment, idx) => {
|
|
58
|
+
let className = '';
|
|
59
|
+
|
|
60
|
+
if (segment.type === 'added') {
|
|
61
|
+
className =
|
|
62
|
+
'bg-green-200 dark:bg-green-900/50 text-green-800 dark:text-green-200';
|
|
63
|
+
} else if (segment.type === 'removed') {
|
|
64
|
+
className =
|
|
65
|
+
'bg-red-200 dark:bg-red-900/50 text-red-800 dark:text-red-200 line-through';
|
|
66
|
+
} else if (segment.type === 'modified') {
|
|
67
|
+
className =
|
|
68
|
+
'bg-yellow-200 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<span key={idx} className={className}>
|
|
73
|
+
{segment.text}
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Single diff line component
|
|
83
|
+
*/
|
|
84
|
+
function DiffLine({
|
|
85
|
+
paragraphDiff,
|
|
86
|
+
showLineNumbers,
|
|
87
|
+
side,
|
|
88
|
+
isHighlighted,
|
|
89
|
+
onClick,
|
|
90
|
+
}: {
|
|
91
|
+
paragraphDiff: ParagraphDiff;
|
|
92
|
+
showLineNumbers: boolean;
|
|
93
|
+
side: 'left' | 'right';
|
|
94
|
+
isHighlighted: boolean;
|
|
95
|
+
onClick?: () => void;
|
|
96
|
+
}) {
|
|
97
|
+
const text = side === 'left' ? paragraphDiff.original : paragraphDiff.modified;
|
|
98
|
+
const segments =
|
|
99
|
+
side === 'left' ? paragraphDiff.originalSegments : paragraphDiff.modifiedSegments;
|
|
100
|
+
|
|
101
|
+
// Determine line type
|
|
102
|
+
let lineType: 'added' | 'removed' | 'modified' | 'unchanged' = 'unchanged';
|
|
103
|
+
if (paragraphDiff.hasChanges) {
|
|
104
|
+
if (paragraphDiff.original === '' && paragraphDiff.modified !== '') {
|
|
105
|
+
lineType = side === 'right' ? 'added' : 'removed';
|
|
106
|
+
} else if (paragraphDiff.original !== '' && paragraphDiff.modified === '') {
|
|
107
|
+
lineType = side === 'left' ? 'removed' : 'added';
|
|
108
|
+
} else {
|
|
109
|
+
lineType = 'modified';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Background colors
|
|
114
|
+
const bgColors = {
|
|
115
|
+
added: 'bg-green-50 dark:bg-green-950/30',
|
|
116
|
+
removed: 'bg-red-50 dark:bg-red-950/30',
|
|
117
|
+
modified: 'bg-yellow-50 dark:bg-yellow-950/30',
|
|
118
|
+
unchanged: 'bg-transparent',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Gutter indicator
|
|
122
|
+
const gutterIndicators = {
|
|
123
|
+
added: <span className="text-green-600 dark:text-green-400 font-bold">+</span>,
|
|
124
|
+
removed: <span className="text-red-600 dark:text-red-400 font-bold">-</span>,
|
|
125
|
+
modified: <span className="text-yellow-600 dark:text-yellow-400 font-bold">~</span>,
|
|
126
|
+
unchanged: <span className="text-muted-foreground"> </span>,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Skip empty lines for the opposite side of added/removed
|
|
130
|
+
if (
|
|
131
|
+
(lineType === 'added' && side === 'left') ||
|
|
132
|
+
(lineType === 'removed' && side === 'right')
|
|
133
|
+
) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className={cn(
|
|
137
|
+
'flex items-stretch min-h-[28px] border-b border-border/30',
|
|
138
|
+
bgColors[lineType],
|
|
139
|
+
isHighlighted && 'ring-2 ring-primary ring-inset'
|
|
140
|
+
)}
|
|
141
|
+
onClick={onClick}
|
|
142
|
+
>
|
|
143
|
+
{showLineNumbers && (
|
|
144
|
+
<div className="w-12 flex-shrink-0 px-2 py-1 text-xs text-muted-foreground/50 text-right border-r border-border/30 select-none">
|
|
145
|
+
-
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
<div className="w-6 flex-shrink-0 flex items-center justify-center border-r border-border/30 select-none">
|
|
149
|
+
{gutterIndicators[lineType]}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="flex-1 px-3 py-1 text-sm font-mono text-muted-foreground/50 italic">
|
|
152
|
+
{/* Empty space */}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
className={cn(
|
|
161
|
+
'flex items-stretch min-h-[28px] border-b border-border/30 cursor-pointer hover:bg-muted/50 transition-colors',
|
|
162
|
+
bgColors[lineType],
|
|
163
|
+
isHighlighted && 'ring-2 ring-primary ring-inset'
|
|
164
|
+
)}
|
|
165
|
+
data-paragraph-index={paragraphDiff.index}
|
|
166
|
+
data-side={side}
|
|
167
|
+
onClick={onClick}
|
|
168
|
+
>
|
|
169
|
+
{/* Line number */}
|
|
170
|
+
{showLineNumbers && (
|
|
171
|
+
<div className="w-12 flex-shrink-0 px-2 py-1 text-xs text-muted-foreground text-right border-r border-border/30 select-none">
|
|
172
|
+
{paragraphDiff.index + 1}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Gutter indicator */}
|
|
177
|
+
<div className="w-6 flex-shrink-0 flex items-center justify-center border-r border-border/30 select-none">
|
|
178
|
+
{gutterIndicators[lineType]}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Content */}
|
|
182
|
+
<div className="flex-1 px-3 py-1 text-sm font-mono whitespace-pre-wrap break-words">
|
|
183
|
+
{segments.length > 0 ? (
|
|
184
|
+
<DiffSegments segments={segments} side={side} />
|
|
185
|
+
) : (
|
|
186
|
+
<span className={lineType !== 'unchanged' ? 'opacity-50' : ''}>
|
|
187
|
+
{text || '\u00A0'}
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Collapsed section indicator
|
|
197
|
+
*/
|
|
198
|
+
function CollapsedSection({
|
|
199
|
+
count,
|
|
200
|
+
onClick,
|
|
201
|
+
}: {
|
|
202
|
+
count: number;
|
|
203
|
+
onClick: () => void;
|
|
204
|
+
}) {
|
|
205
|
+
return (
|
|
206
|
+
<button
|
|
207
|
+
onClick={onClick}
|
|
208
|
+
className="w-full flex items-center justify-center gap-2 py-2 bg-muted/50 text-xs text-muted-foreground hover:bg-muted transition-colors border-y border-border/30"
|
|
209
|
+
>
|
|
210
|
+
<Minimize2 className="w-3 h-3" />
|
|
211
|
+
<span>{count} unchanged paragraphs</span>
|
|
212
|
+
<span className="text-primary">(click to expand)</span>
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Main SideBySideDiff component
|
|
219
|
+
*/
|
|
220
|
+
export function SideBySideDiff({
|
|
221
|
+
originalContent,
|
|
222
|
+
modifiedContent,
|
|
223
|
+
syncScroll = true,
|
|
224
|
+
showLineNumbers = true,
|
|
225
|
+
contextLines = 3,
|
|
226
|
+
collapseUnchanged = true,
|
|
227
|
+
height = '400px',
|
|
228
|
+
}: SideBySideDiffProps) {
|
|
229
|
+
const [isSyncScrollEnabled, setIsSyncScrollEnabled] = useState(syncScroll);
|
|
230
|
+
const [isCollapsed, setIsCollapsed] = useState(collapseUnchanged);
|
|
231
|
+
const [expandedSections, setExpandedSections] = useState<Set<number>>(new Set());
|
|
232
|
+
const [currentChangeIndex, setCurrentChangeIndex] = useState(0);
|
|
233
|
+
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
|
|
234
|
+
|
|
235
|
+
const leftScrollRef = useRef<HTMLDivElement>(null);
|
|
236
|
+
const rightScrollRef = useRef<HTMLDivElement>(null);
|
|
237
|
+
const isScrolling = useRef(false);
|
|
238
|
+
|
|
239
|
+
// Generate diff
|
|
240
|
+
const diff = useMemo(() => {
|
|
241
|
+
return generateDocumentDiff(originalContent, modifiedContent);
|
|
242
|
+
}, [originalContent, modifiedContent]);
|
|
243
|
+
|
|
244
|
+
// Get changed paragraph indices for navigation
|
|
245
|
+
const changedIndices = useMemo(() => {
|
|
246
|
+
return diff.paragraphDiffs
|
|
247
|
+
.map((p, i) => (p.hasChanges ? i : -1))
|
|
248
|
+
.filter((i) => i !== -1);
|
|
249
|
+
}, [diff.paragraphDiffs]);
|
|
250
|
+
|
|
251
|
+
// Filter paragraphs based on collapse setting
|
|
252
|
+
const visibleParagraphs = useMemo(() => {
|
|
253
|
+
if (!isCollapsed) {
|
|
254
|
+
return diff.paragraphDiffs;
|
|
255
|
+
}
|
|
256
|
+
return filterChangedParagraphs(diff.paragraphDiffs, contextLines);
|
|
257
|
+
}, [diff.paragraphDiffs, isCollapsed, contextLines]);
|
|
258
|
+
|
|
259
|
+
// Sync scroll handler
|
|
260
|
+
const handleScroll = useCallback(
|
|
261
|
+
(source: 'left' | 'right') => {
|
|
262
|
+
if (!isSyncScrollEnabled || isScrolling.current) return;
|
|
263
|
+
|
|
264
|
+
isScrolling.current = true;
|
|
265
|
+
const sourceRef = source === 'left' ? leftScrollRef : rightScrollRef;
|
|
266
|
+
const targetRef = source === 'left' ? rightScrollRef : leftScrollRef;
|
|
267
|
+
|
|
268
|
+
if (sourceRef.current && targetRef.current) {
|
|
269
|
+
targetRef.current.scrollTop = sourceRef.current.scrollTop;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
setTimeout(() => {
|
|
273
|
+
isScrolling.current = false;
|
|
274
|
+
}, 50);
|
|
275
|
+
},
|
|
276
|
+
[isSyncScrollEnabled]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Scroll to a specific paragraph index in both panels
|
|
280
|
+
const scrollToIndex = useCallback((paragraphIndex: number) => {
|
|
281
|
+
const leftPanel = leftScrollRef.current;
|
|
282
|
+
const rightPanel = rightScrollRef.current;
|
|
283
|
+
|
|
284
|
+
if (!leftPanel || !rightPanel) return;
|
|
285
|
+
|
|
286
|
+
// Try to find the actual DOM element using data attribute
|
|
287
|
+
const leftElement = leftPanel.querySelector(`[data-paragraph-index="${paragraphIndex}"][data-side="left"]`);
|
|
288
|
+
const rightElement = rightPanel.querySelector(`[data-paragraph-index="${paragraphIndex}"][data-side="right"]`);
|
|
289
|
+
|
|
290
|
+
if (leftElement && rightElement) {
|
|
291
|
+
// Use scrollIntoView for precise positioning
|
|
292
|
+
leftElement.scrollIntoView({
|
|
293
|
+
behavior: 'smooth',
|
|
294
|
+
block: 'center',
|
|
295
|
+
});
|
|
296
|
+
// The sync scroll handler will sync the right panel
|
|
297
|
+
} else {
|
|
298
|
+
// Fallback to calculated position
|
|
299
|
+
const lineHeight = 28;
|
|
300
|
+
const scrollPosition = paragraphIndex * lineHeight;
|
|
301
|
+
|
|
302
|
+
leftPanel.scrollTo({
|
|
303
|
+
top: scrollPosition,
|
|
304
|
+
behavior: 'smooth',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
rightPanel.scrollTo({
|
|
308
|
+
top: scrollPosition,
|
|
309
|
+
behavior: 'smooth',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
// Navigate to change
|
|
315
|
+
const navigateToChange = useCallback(
|
|
316
|
+
(direction: 'prev' | 'next') => {
|
|
317
|
+
const newIndex =
|
|
318
|
+
direction === 'next'
|
|
319
|
+
? Math.min(changedIndices.length - 1, currentChangeIndex + 1)
|
|
320
|
+
: Math.max(0, currentChangeIndex - 1);
|
|
321
|
+
|
|
322
|
+
setCurrentChangeIndex(newIndex);
|
|
323
|
+
const paragraphIndex = changedIndices[newIndex];
|
|
324
|
+
setHighlightedIndex(paragraphIndex);
|
|
325
|
+
|
|
326
|
+
// Scroll to the change
|
|
327
|
+
scrollToIndex(paragraphIndex);
|
|
328
|
+
},
|
|
329
|
+
[changedIndices, currentChangeIndex, scrollToIndex]
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Stats display
|
|
333
|
+
const statsText = useMemo(() => {
|
|
334
|
+
const { stats } = diff;
|
|
335
|
+
const parts: string[] = [];
|
|
336
|
+
if (stats.changedParagraphs > 0) parts.push(`${stats.changedParagraphs} modified`);
|
|
337
|
+
if (stats.addedParagraphs > 0) parts.push(`${stats.addedParagraphs} added`);
|
|
338
|
+
if (stats.removedParagraphs > 0) parts.push(`${stats.removedParagraphs} removed`);
|
|
339
|
+
return parts.length > 0 ? parts.join(', ') : 'No changes';
|
|
340
|
+
}, [diff]);
|
|
341
|
+
|
|
342
|
+
// Handle empty content
|
|
343
|
+
if (originalContent.length === 0 && modifiedContent.length === 0) {
|
|
344
|
+
return (
|
|
345
|
+
<div className="p-8 text-center text-muted-foreground">
|
|
346
|
+
<Columns className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|
347
|
+
<p className="text-sm">No content to compare</p>
|
|
348
|
+
<p className="text-xs mt-1">
|
|
349
|
+
Process a document to see before/after comparison
|
|
350
|
+
</p>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div className="flex flex-col">
|
|
357
|
+
{/* Toolbar */}
|
|
358
|
+
<div className="flex items-center justify-between px-4 py-2 bg-muted/30 border-b border-border">
|
|
359
|
+
{/* Stats */}
|
|
360
|
+
<div className="text-xs text-muted-foreground">
|
|
361
|
+
<span className="font-medium">{diff.stats.totalParagraphs}</span> paragraphs |{' '}
|
|
362
|
+
<span>{statsText}</span>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{/* Controls */}
|
|
366
|
+
<div className="flex items-center gap-2">
|
|
367
|
+
{/* Navigation */}
|
|
368
|
+
<div className="flex items-center gap-1 mr-2">
|
|
369
|
+
<button
|
|
370
|
+
onClick={() => navigateToChange('prev')}
|
|
371
|
+
disabled={currentChangeIndex === 0 || changedIndices.length === 0}
|
|
372
|
+
className="p-1 rounded hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
|
373
|
+
title="Previous change"
|
|
374
|
+
>
|
|
375
|
+
<ChevronUp className="w-4 h-4" />
|
|
376
|
+
</button>
|
|
377
|
+
<span className="text-xs text-muted-foreground min-w-[40px] text-center">
|
|
378
|
+
{changedIndices.length > 0
|
|
379
|
+
? `${currentChangeIndex + 1}/${changedIndices.length}`
|
|
380
|
+
: '0/0'}
|
|
381
|
+
</span>
|
|
382
|
+
<button
|
|
383
|
+
onClick={() => navigateToChange('next')}
|
|
384
|
+
disabled={
|
|
385
|
+
currentChangeIndex === changedIndices.length - 1 ||
|
|
386
|
+
changedIndices.length === 0
|
|
387
|
+
}
|
|
388
|
+
className="p-1 rounded hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
|
389
|
+
title="Next change"
|
|
390
|
+
>
|
|
391
|
+
<ChevronDown className="w-4 h-4" />
|
|
392
|
+
</button>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{/* Sync scroll toggle */}
|
|
396
|
+
<button
|
|
397
|
+
onClick={() => setIsSyncScrollEnabled(!isSyncScrollEnabled)}
|
|
398
|
+
className={cn(
|
|
399
|
+
'p-1.5 rounded transition-colors',
|
|
400
|
+
isSyncScrollEnabled
|
|
401
|
+
? 'bg-primary text-primary-foreground'
|
|
402
|
+
: 'hover:bg-muted'
|
|
403
|
+
)}
|
|
404
|
+
title={isSyncScrollEnabled ? 'Disable sync scroll' : 'Enable sync scroll'}
|
|
405
|
+
>
|
|
406
|
+
{isSyncScrollEnabled ? (
|
|
407
|
+
<Link className="w-4 h-4" />
|
|
408
|
+
) : (
|
|
409
|
+
<Unlink className="w-4 h-4" />
|
|
410
|
+
)}
|
|
411
|
+
</button>
|
|
412
|
+
|
|
413
|
+
{/* Collapse toggle */}
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
416
|
+
className={cn(
|
|
417
|
+
'p-1.5 rounded transition-colors',
|
|
418
|
+
isCollapsed ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
|
|
419
|
+
)}
|
|
420
|
+
title={isCollapsed ? 'Show all' : 'Collapse unchanged'}
|
|
421
|
+
>
|
|
422
|
+
{isCollapsed ? (
|
|
423
|
+
<Eye className="w-4 h-4" />
|
|
424
|
+
) : (
|
|
425
|
+
<EyeOff className="w-4 h-4" />
|
|
426
|
+
)}
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Diff panels */}
|
|
432
|
+
<div
|
|
433
|
+
className="flex"
|
|
434
|
+
style={{ height: typeof height === 'number' ? `${height}px` : height }}
|
|
435
|
+
>
|
|
436
|
+
{/* Left panel (Original) */}
|
|
437
|
+
<div className="flex-1 flex flex-col border-r border-border">
|
|
438
|
+
<div className="px-4 py-2 bg-red-50 dark:bg-red-950/30 border-b border-border text-sm font-medium text-red-700 dark:text-red-300">
|
|
439
|
+
Original (Before)
|
|
440
|
+
</div>
|
|
441
|
+
<div
|
|
442
|
+
ref={leftScrollRef}
|
|
443
|
+
className="flex-1 overflow-auto"
|
|
444
|
+
onScroll={() => handleScroll('left')}
|
|
445
|
+
>
|
|
446
|
+
{visibleParagraphs.map((paragraphDiff, idx) => (
|
|
447
|
+
<DiffLine
|
|
448
|
+
key={`left-${paragraphDiff.index}`}
|
|
449
|
+
paragraphDiff={paragraphDiff}
|
|
450
|
+
showLineNumbers={showLineNumbers}
|
|
451
|
+
side="left"
|
|
452
|
+
isHighlighted={highlightedIndex === paragraphDiff.index}
|
|
453
|
+
onClick={() => setHighlightedIndex(paragraphDiff.index)}
|
|
454
|
+
/>
|
|
455
|
+
))}
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
{/* Right panel (Modified) */}
|
|
460
|
+
<div className="flex-1 flex flex-col">
|
|
461
|
+
<div className="px-4 py-2 bg-green-50 dark:bg-green-950/30 border-b border-border text-sm font-medium text-green-700 dark:text-green-300">
|
|
462
|
+
Processed (After)
|
|
463
|
+
</div>
|
|
464
|
+
<div
|
|
465
|
+
ref={rightScrollRef}
|
|
466
|
+
className="flex-1 overflow-auto"
|
|
467
|
+
onScroll={() => handleScroll('right')}
|
|
468
|
+
>
|
|
469
|
+
{visibleParagraphs.map((paragraphDiff, idx) => (
|
|
470
|
+
<DiffLine
|
|
471
|
+
key={`right-${paragraphDiff.index}`}
|
|
472
|
+
paragraphDiff={paragraphDiff}
|
|
473
|
+
showLineNumbers={showLineNumbers}
|
|
474
|
+
side="right"
|
|
475
|
+
isHighlighted={highlightedIndex === paragraphDiff.index}
|
|
476
|
+
onClick={() => setHighlightedIndex(paragraphDiff.index)}
|
|
477
|
+
/>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export default SideBySideDiff;
|