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,707 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InlineChangesView - Word-like inline tracked changes display
|
|
3
|
+
*
|
|
4
|
+
* Renders document text with tracked changes highlighted:
|
|
5
|
+
* - Insertions: Green background
|
|
6
|
+
* - Deletions: Red background with strikethrough
|
|
7
|
+
* - Formatting changes: Blue dashed underline with tooltip
|
|
8
|
+
* - Hyperlink changes: Link icon badge with URL diff tooltip
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
FileText,
|
|
14
|
+
Link2,
|
|
15
|
+
Type,
|
|
16
|
+
Table,
|
|
17
|
+
Trash2,
|
|
18
|
+
Plus,
|
|
19
|
+
Edit3,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
MoveRight,
|
|
23
|
+
ArrowRightFromLine,
|
|
24
|
+
ArrowRightToLine,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { cn } from '@/utils/cn';
|
|
27
|
+
import type { ChangeEntry } from '@/types/session';
|
|
28
|
+
|
|
29
|
+
interface InlineChangesViewProps {
|
|
30
|
+
/** All changes from tracked changes */
|
|
31
|
+
changes: ChangeEntry[];
|
|
32
|
+
/** Document paragraphs (if available) */
|
|
33
|
+
paragraphs?: Array<{
|
|
34
|
+
text: string;
|
|
35
|
+
runs?: Array<{
|
|
36
|
+
text: string;
|
|
37
|
+
bold?: boolean;
|
|
38
|
+
italic?: boolean;
|
|
39
|
+
underline?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
}>;
|
|
42
|
+
/** Callback when a change is clicked */
|
|
43
|
+
onChangeClick?: (change: ChangeEntry, index: number) => void;
|
|
44
|
+
/** Current highlighted change index */
|
|
45
|
+
highlightedChangeIndex?: number;
|
|
46
|
+
/** Whether to use virtualization for large documents */
|
|
47
|
+
virtualized?: boolean;
|
|
48
|
+
/** Control all paragraphs expanded/collapsed from parent */
|
|
49
|
+
allExpanded?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get icon for change type
|
|
54
|
+
*/
|
|
55
|
+
function getChangeIcon(revisionType: string) {
|
|
56
|
+
switch (revisionType) {
|
|
57
|
+
case 'insert':
|
|
58
|
+
return Plus;
|
|
59
|
+
case 'delete':
|
|
60
|
+
return Trash2;
|
|
61
|
+
case 'moveFrom':
|
|
62
|
+
return ArrowRightFromLine;
|
|
63
|
+
case 'moveTo':
|
|
64
|
+
return ArrowRightToLine;
|
|
65
|
+
case 'runPropertiesChange':
|
|
66
|
+
case 'paragraphPropertiesChange':
|
|
67
|
+
return Edit3;
|
|
68
|
+
case 'hyperlinkChange':
|
|
69
|
+
return Link2;
|
|
70
|
+
case 'tablePropertiesChange':
|
|
71
|
+
case 'tableCellPropertiesChange':
|
|
72
|
+
return Table;
|
|
73
|
+
default:
|
|
74
|
+
return Type;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get styling classes for change type
|
|
80
|
+
*/
|
|
81
|
+
function getChangeStyles(revisionType: string, isHighlighted: boolean, isMoveLinked: boolean = false) {
|
|
82
|
+
const baseClasses = 'inline-flex items-center rounded px-1 py-0.5 transition-all';
|
|
83
|
+
const highlightClasses = isHighlighted ? 'ring-2 ring-primary ring-offset-1' : '';
|
|
84
|
+
const moveLinkClasses = isMoveLinked ? 'ring-2 ring-amber-400 ring-offset-1 animate-pulse' : '';
|
|
85
|
+
|
|
86
|
+
switch (revisionType) {
|
|
87
|
+
case 'insert':
|
|
88
|
+
return cn(
|
|
89
|
+
baseClasses,
|
|
90
|
+
highlightClasses,
|
|
91
|
+
'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
92
|
+
);
|
|
93
|
+
case 'delete':
|
|
94
|
+
return cn(
|
|
95
|
+
baseClasses,
|
|
96
|
+
highlightClasses,
|
|
97
|
+
'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 line-through'
|
|
98
|
+
);
|
|
99
|
+
case 'moveFrom':
|
|
100
|
+
return cn(
|
|
101
|
+
baseClasses,
|
|
102
|
+
highlightClasses,
|
|
103
|
+
moveLinkClasses,
|
|
104
|
+
'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border border-dashed border-amber-400'
|
|
105
|
+
);
|
|
106
|
+
case 'moveTo':
|
|
107
|
+
return cn(
|
|
108
|
+
baseClasses,
|
|
109
|
+
highlightClasses,
|
|
110
|
+
moveLinkClasses,
|
|
111
|
+
'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 border border-solid border-amber-400'
|
|
112
|
+
);
|
|
113
|
+
case 'runPropertiesChange':
|
|
114
|
+
case 'paragraphPropertiesChange':
|
|
115
|
+
return cn(
|
|
116
|
+
baseClasses,
|
|
117
|
+
highlightClasses,
|
|
118
|
+
'border-b-2 border-dashed border-blue-500 text-blue-700 dark:text-blue-300'
|
|
119
|
+
);
|
|
120
|
+
case 'hyperlinkChange':
|
|
121
|
+
return cn(
|
|
122
|
+
baseClasses,
|
|
123
|
+
highlightClasses,
|
|
124
|
+
'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-300'
|
|
125
|
+
);
|
|
126
|
+
case 'tablePropertiesChange':
|
|
127
|
+
case 'tableCellPropertiesChange':
|
|
128
|
+
return cn(
|
|
129
|
+
baseClasses,
|
|
130
|
+
highlightClasses,
|
|
131
|
+
'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300'
|
|
132
|
+
);
|
|
133
|
+
default:
|
|
134
|
+
return cn(
|
|
135
|
+
baseClasses,
|
|
136
|
+
highlightClasses,
|
|
137
|
+
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Change badge component for inline display
|
|
144
|
+
*/
|
|
145
|
+
function ChangeBadge({
|
|
146
|
+
change,
|
|
147
|
+
index,
|
|
148
|
+
isHighlighted,
|
|
149
|
+
isMoveLinked,
|
|
150
|
+
onClick,
|
|
151
|
+
onMoveHover,
|
|
152
|
+
}: {
|
|
153
|
+
change: ChangeEntry;
|
|
154
|
+
index: number;
|
|
155
|
+
isHighlighted: boolean;
|
|
156
|
+
isMoveLinked?: boolean;
|
|
157
|
+
onClick?: () => void;
|
|
158
|
+
onMoveHover?: (moveId: string | null) => void;
|
|
159
|
+
}) {
|
|
160
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
161
|
+
const Icon = getChangeIcon(change.revisionType);
|
|
162
|
+
const isMove = change.revisionType === 'moveFrom' || change.revisionType === 'moveTo';
|
|
163
|
+
const moveId = (change as ChangeEntry & { moveId?: string }).moveId;
|
|
164
|
+
|
|
165
|
+
const getDisplayText = () => {
|
|
166
|
+
if (change.content?.affectedText) {
|
|
167
|
+
return change.content.affectedText;
|
|
168
|
+
}
|
|
169
|
+
if (change.content?.after) {
|
|
170
|
+
return change.content.after;
|
|
171
|
+
}
|
|
172
|
+
if (change.content?.before) {
|
|
173
|
+
return change.content.before;
|
|
174
|
+
}
|
|
175
|
+
return change.description || 'Change';
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleMouseEnter = () => {
|
|
179
|
+
setShowTooltip(true);
|
|
180
|
+
if (isMove && moveId && onMoveHover) {
|
|
181
|
+
onMoveHover(moveId);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleMouseLeave = () => {
|
|
186
|
+
setShowTooltip(false);
|
|
187
|
+
if (isMove && onMoveHover) {
|
|
188
|
+
onMoveHover(null);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<span className="relative inline-block">
|
|
194
|
+
<span
|
|
195
|
+
className={cn(getChangeStyles(change.revisionType, isHighlighted, isMoveLinked), 'cursor-pointer')}
|
|
196
|
+
onClick={onClick}
|
|
197
|
+
onMouseEnter={handleMouseEnter}
|
|
198
|
+
onMouseLeave={handleMouseLeave}
|
|
199
|
+
>
|
|
200
|
+
<Icon className="w-3 h-3 mr-1 inline" />
|
|
201
|
+
<span className="text-sm">{getDisplayText()}</span>
|
|
202
|
+
{/* Move indicator */}
|
|
203
|
+
{isMove && (
|
|
204
|
+
<MoveRight className="w-3 h-3 ml-1 inline opacity-60" />
|
|
205
|
+
)}
|
|
206
|
+
</span>
|
|
207
|
+
|
|
208
|
+
{/* Tooltip */}
|
|
209
|
+
{showTooltip && (
|
|
210
|
+
<div className="absolute z-50 bottom-full left-0 mb-1 p-2 bg-popover border border-border rounded-lg shadow-lg text-xs max-w-xs">
|
|
211
|
+
<div className="font-medium mb-1">{change.description}</div>
|
|
212
|
+
{change.author && (
|
|
213
|
+
<div className="text-muted-foreground">By: {change.author}</div>
|
|
214
|
+
)}
|
|
215
|
+
{change.date && (
|
|
216
|
+
<div className="text-muted-foreground">
|
|
217
|
+
{new Date(change.date).toLocaleString()}
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
{/* Content ID for hyperlink changes */}
|
|
221
|
+
{change.content?.hyperlinkChange?.contentId && (
|
|
222
|
+
<div className="mt-1 pt-1 border-t border-border">
|
|
223
|
+
<span className="text-muted-foreground">Content ID:</span>{' '}
|
|
224
|
+
<code className="bg-primary/10 text-primary px-1 py-0.5 rounded text-[10px]">
|
|
225
|
+
{change.content.hyperlinkChange.contentId}
|
|
226
|
+
</code>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
{/* URL change for hyperlinks */}
|
|
230
|
+
{change.content?.hyperlinkChange?.urlBefore && change.content?.hyperlinkChange?.urlAfter && (
|
|
231
|
+
<div className="mt-1 pt-1 border-t border-border">
|
|
232
|
+
<div className="text-muted-foreground mb-0.5">URL:</div>
|
|
233
|
+
<div className="line-through text-red-500 break-all">
|
|
234
|
+
{change.content.hyperlinkChange.urlBefore.slice(0, 60)}
|
|
235
|
+
{change.content.hyperlinkChange.urlBefore.length > 60 && '...'}
|
|
236
|
+
</div>
|
|
237
|
+
<div className="text-green-500 break-all">
|
|
238
|
+
{change.content.hyperlinkChange.urlAfter.slice(0, 60)}
|
|
239
|
+
{change.content.hyperlinkChange.urlAfter.length > 60 && '...'}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
{/* Text to Display change for hyperlinks */}
|
|
244
|
+
{change.content?.hyperlinkChange?.textBefore && change.content?.hyperlinkChange?.textAfter && (
|
|
245
|
+
<div className="mt-1 pt-1 border-t border-border">
|
|
246
|
+
<div className="text-muted-foreground mb-0.5">Text to Display:</div>
|
|
247
|
+
<div className="line-through text-red-500">
|
|
248
|
+
{change.content.hyperlinkChange.textBefore.slice(0, 50)}
|
|
249
|
+
{change.content.hyperlinkChange.textBefore.length > 50 && '...'}
|
|
250
|
+
</div>
|
|
251
|
+
<div className="text-green-500">
|
|
252
|
+
{change.content.hyperlinkChange.textAfter.slice(0, 50)}
|
|
253
|
+
{change.content.hyperlinkChange.textAfter.length > 50 && '...'}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
{change.propertyChange && (
|
|
258
|
+
<div className="mt-1 pt-1 border-t border-border">
|
|
259
|
+
<span className="text-muted-foreground">{change.propertyChange.property}:</span>{' '}
|
|
260
|
+
<span className="line-through text-red-500">
|
|
261
|
+
{change.propertyChange.oldValue || 'none'}
|
|
262
|
+
</span>{' '}
|
|
263
|
+
<span className="text-green-500">
|
|
264
|
+
{change.propertyChange.newValue || 'none'}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
{/* Generic before/after (for non-hyperlink changes) */}
|
|
269
|
+
{!change.content?.hyperlinkChange && change.content?.before && change.content?.after && (
|
|
270
|
+
<div className="mt-1 pt-1 border-t border-border">
|
|
271
|
+
<div className="line-through text-red-500">
|
|
272
|
+
{change.content.before.slice(0, 50)}
|
|
273
|
+
{change.content.before.length > 50 && '...'}
|
|
274
|
+
</div>
|
|
275
|
+
<div className="text-green-500">
|
|
276
|
+
{change.content.after.slice(0, 50)}
|
|
277
|
+
{change.content.after.length > 50 && '...'}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</span>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Group changes by paragraph
|
|
289
|
+
*/
|
|
290
|
+
function groupChangesByParagraph(changes: ChangeEntry[]) {
|
|
291
|
+
const groups = new Map<number, ChangeEntry[]>();
|
|
292
|
+
|
|
293
|
+
for (const change of changes) {
|
|
294
|
+
const paragraphIndex = change.location?.paragraphIndex ?? -1;
|
|
295
|
+
if (!groups.has(paragraphIndex)) {
|
|
296
|
+
groups.set(paragraphIndex, []);
|
|
297
|
+
}
|
|
298
|
+
groups.get(paragraphIndex)!.push(change);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Sort by paragraph index
|
|
302
|
+
return Array.from(groups.entries()).sort((a, b) => a[0] - b[0]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Paragraph with inline changes
|
|
307
|
+
*/
|
|
308
|
+
function ParagraphWithChanges({
|
|
309
|
+
paragraphIndex,
|
|
310
|
+
paragraphText,
|
|
311
|
+
changes,
|
|
312
|
+
highlightedChangeIndex,
|
|
313
|
+
onChangeClick,
|
|
314
|
+
allExpanded,
|
|
315
|
+
hoveredMoveId,
|
|
316
|
+
onMoveHover,
|
|
317
|
+
}: {
|
|
318
|
+
paragraphIndex: number;
|
|
319
|
+
paragraphText?: string;
|
|
320
|
+
changes: ChangeEntry[];
|
|
321
|
+
highlightedChangeIndex?: number;
|
|
322
|
+
onChangeClick?: (change: ChangeEntry, index: number) => void;
|
|
323
|
+
allExpanded?: boolean;
|
|
324
|
+
hoveredMoveId?: string | null;
|
|
325
|
+
onMoveHover?: (moveId: string | null) => void;
|
|
326
|
+
}) {
|
|
327
|
+
const [isLocalExpanded, setIsLocalExpanded] = useState(true);
|
|
328
|
+
|
|
329
|
+
// Sync with parent's allExpanded when it changes
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (allExpanded !== undefined) {
|
|
332
|
+
setIsLocalExpanded(allExpanded);
|
|
333
|
+
}
|
|
334
|
+
}, [allExpanded]);
|
|
335
|
+
|
|
336
|
+
const isExpanded = allExpanded !== undefined ? allExpanded : isLocalExpanded;
|
|
337
|
+
|
|
338
|
+
// Sort changes by run index
|
|
339
|
+
const sortedChanges = useMemo(() => {
|
|
340
|
+
return [...changes].sort((a, b) => {
|
|
341
|
+
const aRun = a.location?.runIndex ?? 0;
|
|
342
|
+
const bRun = b.location?.runIndex ?? 0;
|
|
343
|
+
return aRun - bRun;
|
|
344
|
+
});
|
|
345
|
+
}, [changes]);
|
|
346
|
+
|
|
347
|
+
// Check if any change in this paragraph is linked via move
|
|
348
|
+
const hasMoveChanges = changes.some(
|
|
349
|
+
(c) => c.revisionType === 'moveFrom' || c.revisionType === 'moveTo'
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<div className="border-b border-border/50 last:border-b-0">
|
|
354
|
+
{/* Paragraph header */}
|
|
355
|
+
<button
|
|
356
|
+
onClick={() => setIsLocalExpanded(!isLocalExpanded)}
|
|
357
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:bg-muted/50 transition-colors"
|
|
358
|
+
>
|
|
359
|
+
{isExpanded ? (
|
|
360
|
+
<ChevronDown className="w-3 h-3" />
|
|
361
|
+
) : (
|
|
362
|
+
<ChevronRight className="w-3 h-3" />
|
|
363
|
+
)}
|
|
364
|
+
<span>Paragraph {paragraphIndex + 1}</span>
|
|
365
|
+
{hasMoveChanges && (
|
|
366
|
+
<span className="px-1.5 py-0.5 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-full text-[10px]">
|
|
367
|
+
has moves
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
<span className="ml-auto px-1.5 py-0.5 bg-muted rounded-full">
|
|
371
|
+
{changes.length} {changes.length === 1 ? 'change' : 'changes'}
|
|
372
|
+
</span>
|
|
373
|
+
</button>
|
|
374
|
+
|
|
375
|
+
{/* Paragraph content */}
|
|
376
|
+
{isExpanded && (
|
|
377
|
+
<div className="px-4 py-3 bg-muted/10">
|
|
378
|
+
{paragraphText && (
|
|
379
|
+
<p className="text-sm text-muted-foreground mb-2 font-mono">
|
|
380
|
+
{paragraphText.slice(0, 200)}
|
|
381
|
+
{paragraphText.length > 200 && '...'}
|
|
382
|
+
</p>
|
|
383
|
+
)}
|
|
384
|
+
<div className="flex flex-wrap gap-2">
|
|
385
|
+
{sortedChanges.map((change, idx) => {
|
|
386
|
+
const changeWithMoveId = change as ChangeEntry & { moveId?: string };
|
|
387
|
+
const isMoveLinked = hoveredMoveId !== null && changeWithMoveId.moveId === hoveredMoveId;
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<ChangeBadge
|
|
391
|
+
key={`${change.id || idx}-${idx}`}
|
|
392
|
+
change={change}
|
|
393
|
+
index={idx}
|
|
394
|
+
isHighlighted={
|
|
395
|
+
highlightedChangeIndex !== undefined &&
|
|
396
|
+
changes.indexOf(change) === highlightedChangeIndex
|
|
397
|
+
}
|
|
398
|
+
isMoveLinked={isMoveLinked}
|
|
399
|
+
onClick={() => onChangeClick?.(change, idx)}
|
|
400
|
+
onMoveHover={onMoveHover}
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
})}
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Changes without paragraph location
|
|
413
|
+
*/
|
|
414
|
+
function OtherChanges({
|
|
415
|
+
changes,
|
|
416
|
+
highlightedChangeIndex,
|
|
417
|
+
onChangeClick,
|
|
418
|
+
allExpanded,
|
|
419
|
+
hoveredMoveId,
|
|
420
|
+
onMoveHover,
|
|
421
|
+
}: {
|
|
422
|
+
changes: ChangeEntry[];
|
|
423
|
+
highlightedChangeIndex?: number;
|
|
424
|
+
onChangeClick?: (change: ChangeEntry, index: number) => void;
|
|
425
|
+
allExpanded?: boolean;
|
|
426
|
+
hoveredMoveId?: string | null;
|
|
427
|
+
onMoveHover?: (moveId: string | null) => void;
|
|
428
|
+
}) {
|
|
429
|
+
const [isLocalExpanded, setIsLocalExpanded] = useState(true);
|
|
430
|
+
|
|
431
|
+
// Sync with parent's allExpanded when it changes
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
if (allExpanded !== undefined) {
|
|
434
|
+
setIsLocalExpanded(allExpanded);
|
|
435
|
+
}
|
|
436
|
+
}, [allExpanded]);
|
|
437
|
+
|
|
438
|
+
const isExpanded = allExpanded !== undefined ? allExpanded : isLocalExpanded;
|
|
439
|
+
|
|
440
|
+
if (changes.length === 0) return null;
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<div className="border-b border-border/50">
|
|
444
|
+
<button
|
|
445
|
+
onClick={() => setIsLocalExpanded(!isLocalExpanded)}
|
|
446
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground hover:bg-muted/50 transition-colors"
|
|
447
|
+
>
|
|
448
|
+
{isExpanded ? (
|
|
449
|
+
<ChevronDown className="w-3 h-3" />
|
|
450
|
+
) : (
|
|
451
|
+
<ChevronRight className="w-3 h-3" />
|
|
452
|
+
)}
|
|
453
|
+
<span>Document-level Changes</span>
|
|
454
|
+
<span className="ml-auto px-1.5 py-0.5 bg-muted rounded-full">
|
|
455
|
+
{changes.length} {changes.length === 1 ? 'change' : 'changes'}
|
|
456
|
+
</span>
|
|
457
|
+
</button>
|
|
458
|
+
|
|
459
|
+
{isExpanded && (
|
|
460
|
+
<div className="px-4 py-3 bg-muted/10">
|
|
461
|
+
<div className="flex flex-wrap gap-2">
|
|
462
|
+
{changes.map((change, idx) => {
|
|
463
|
+
const changeWithMoveId = change as ChangeEntry & { moveId?: string };
|
|
464
|
+
const isMoveLinked = hoveredMoveId !== null && changeWithMoveId.moveId === hoveredMoveId;
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<ChangeBadge
|
|
468
|
+
key={`other-${change.id || idx}-${idx}`}
|
|
469
|
+
change={change}
|
|
470
|
+
index={idx}
|
|
471
|
+
isHighlighted={highlightedChangeIndex === idx}
|
|
472
|
+
isMoveLinked={isMoveLinked}
|
|
473
|
+
onClick={() => onChangeClick?.(change, idx)}
|
|
474
|
+
onMoveHover={onMoveHover}
|
|
475
|
+
/>
|
|
476
|
+
);
|
|
477
|
+
})}
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Virtualized paragraph wrapper - only renders content when visible
|
|
487
|
+
*/
|
|
488
|
+
function VirtualizedParagraph({
|
|
489
|
+
paragraphIndex,
|
|
490
|
+
paragraphText,
|
|
491
|
+
changes,
|
|
492
|
+
highlightedChangeIndex,
|
|
493
|
+
onChangeClick,
|
|
494
|
+
allExpanded,
|
|
495
|
+
hoveredMoveId,
|
|
496
|
+
onMoveHover,
|
|
497
|
+
}: {
|
|
498
|
+
paragraphIndex: number;
|
|
499
|
+
paragraphText?: string;
|
|
500
|
+
changes: ChangeEntry[];
|
|
501
|
+
highlightedChangeIndex?: number;
|
|
502
|
+
onChangeClick?: (change: ChangeEntry, index: number) => void;
|
|
503
|
+
allExpanded?: boolean;
|
|
504
|
+
hoveredMoveId?: string | null;
|
|
505
|
+
onMoveHover?: (moveId: string | null) => void;
|
|
506
|
+
}) {
|
|
507
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
508
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
509
|
+
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
const observer = new IntersectionObserver(
|
|
512
|
+
([entry]) => {
|
|
513
|
+
// Once visible, keep it rendered to avoid layout shifts
|
|
514
|
+
if (entry.isIntersecting) {
|
|
515
|
+
setIsVisible(true);
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
rootMargin: '100px', // Pre-load items 100px before they come into view
|
|
520
|
+
threshold: 0,
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
if (ref.current) {
|
|
525
|
+
observer.observe(ref.current);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return () => observer.disconnect();
|
|
529
|
+
}, []);
|
|
530
|
+
|
|
531
|
+
// Placeholder height when not visible
|
|
532
|
+
if (!isVisible) {
|
|
533
|
+
return (
|
|
534
|
+
<div
|
|
535
|
+
ref={ref}
|
|
536
|
+
className="border-b border-border/50 h-10 flex items-center px-3 text-xs text-muted-foreground"
|
|
537
|
+
>
|
|
538
|
+
<ChevronRight className="w-3 h-3 mr-2" />
|
|
539
|
+
Paragraph {paragraphIndex + 1} ({changes.length} changes)
|
|
540
|
+
</div>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<div ref={ref}>
|
|
546
|
+
<ParagraphWithChanges
|
|
547
|
+
paragraphIndex={paragraphIndex}
|
|
548
|
+
paragraphText={paragraphText}
|
|
549
|
+
changes={changes}
|
|
550
|
+
highlightedChangeIndex={highlightedChangeIndex}
|
|
551
|
+
onChangeClick={onChangeClick}
|
|
552
|
+
allExpanded={allExpanded}
|
|
553
|
+
hoveredMoveId={hoveredMoveId}
|
|
554
|
+
onMoveHover={onMoveHover}
|
|
555
|
+
/>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Threshold for enabling virtualization
|
|
561
|
+
const VIRTUALIZATION_THRESHOLD = 100;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Main InlineChangesView component
|
|
565
|
+
*/
|
|
566
|
+
export function InlineChangesView({
|
|
567
|
+
changes,
|
|
568
|
+
paragraphs,
|
|
569
|
+
onChangeClick,
|
|
570
|
+
highlightedChangeIndex,
|
|
571
|
+
virtualized = false,
|
|
572
|
+
allExpanded,
|
|
573
|
+
}: InlineChangesViewProps) {
|
|
574
|
+
// State for move operation linking
|
|
575
|
+
const [hoveredMoveId, setHoveredMoveId] = useState<string | null>(null);
|
|
576
|
+
|
|
577
|
+
// Group changes by paragraph
|
|
578
|
+
const groupedChanges = useMemo(() => groupChangesByParagraph(changes), [changes]);
|
|
579
|
+
|
|
580
|
+
// Separate paragraph changes from document-level changes
|
|
581
|
+
const paragraphChanges = groupedChanges.filter(([index]) => index >= 0);
|
|
582
|
+
const otherChanges = groupedChanges.find(([index]) => index === -1)?.[1] || [];
|
|
583
|
+
|
|
584
|
+
// Auto-enable virtualization for large change lists
|
|
585
|
+
const shouldVirtualize = virtualized || changes.length >= VIRTUALIZATION_THRESHOLD;
|
|
586
|
+
|
|
587
|
+
// Statistics
|
|
588
|
+
const stats = useMemo(() => {
|
|
589
|
+
const insertions = changes.filter((c) => c.revisionType === 'insert').length;
|
|
590
|
+
const deletions = changes.filter((c) => c.revisionType === 'delete').length;
|
|
591
|
+
const formatting = changes.filter(
|
|
592
|
+
(c) =>
|
|
593
|
+
c.revisionType === 'runPropertiesChange' ||
|
|
594
|
+
c.revisionType === 'paragraphPropertiesChange'
|
|
595
|
+
).length;
|
|
596
|
+
const hyperlinks = changes.filter((c) => c.revisionType === 'hyperlinkChange').length;
|
|
597
|
+
const moves = changes.filter(
|
|
598
|
+
(c) => c.revisionType === 'moveFrom' || c.revisionType === 'moveTo'
|
|
599
|
+
).length;
|
|
600
|
+
|
|
601
|
+
return { insertions, deletions, formatting, hyperlinks, moves };
|
|
602
|
+
}, [changes]);
|
|
603
|
+
|
|
604
|
+
// Handler for move hover linking
|
|
605
|
+
const handleMoveHover = useCallback((moveId: string | null) => {
|
|
606
|
+
setHoveredMoveId(moveId);
|
|
607
|
+
}, []);
|
|
608
|
+
|
|
609
|
+
if (changes.length === 0) {
|
|
610
|
+
return (
|
|
611
|
+
<div className="p-8 text-center text-muted-foreground">
|
|
612
|
+
<FileText className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|
613
|
+
<p className="text-sm">No tracked changes to display</p>
|
|
614
|
+
<p className="text-xs mt-1">Process a document to see tracked changes here</p>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div className="flex flex-col">
|
|
621
|
+
{/* Statistics bar */}
|
|
622
|
+
<div className="flex items-center gap-4 px-4 py-2 bg-muted/30 border-b border-border text-xs">
|
|
623
|
+
<span className="text-muted-foreground">Changes:</span>
|
|
624
|
+
{stats.insertions > 0 && (
|
|
625
|
+
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
|
626
|
+
<Plus className="w-3 h-3" />
|
|
627
|
+
{stats.insertions} inserted
|
|
628
|
+
</span>
|
|
629
|
+
)}
|
|
630
|
+
{stats.deletions > 0 && (
|
|
631
|
+
<span className="flex items-center gap-1 text-red-600 dark:text-red-400">
|
|
632
|
+
<Trash2 className="w-3 h-3" />
|
|
633
|
+
{stats.deletions} deleted
|
|
634
|
+
</span>
|
|
635
|
+
)}
|
|
636
|
+
{stats.formatting > 0 && (
|
|
637
|
+
<span className="flex items-center gap-1 text-blue-600 dark:text-blue-400">
|
|
638
|
+
<Type className="w-3 h-3" />
|
|
639
|
+
{stats.formatting} formatted
|
|
640
|
+
</span>
|
|
641
|
+
)}
|
|
642
|
+
{stats.hyperlinks > 0 && (
|
|
643
|
+
<span className="flex items-center gap-1 text-cyan-600 dark:text-cyan-400">
|
|
644
|
+
<Link2 className="w-3 h-3" />
|
|
645
|
+
{stats.hyperlinks} links
|
|
646
|
+
</span>
|
|
647
|
+
)}
|
|
648
|
+
{stats.moves > 0 && (
|
|
649
|
+
<span className="flex items-center gap-1 text-amber-600 dark:text-amber-400">
|
|
650
|
+
<MoveRight className="w-3 h-3" />
|
|
651
|
+
{stats.moves} moved
|
|
652
|
+
</span>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
{/* Virtualization indicator */}
|
|
657
|
+
{shouldVirtualize && (
|
|
658
|
+
<div className="px-4 py-1 bg-amber-50 dark:bg-amber-900/20 border-b border-border text-xs text-amber-700 dark:text-amber-300">
|
|
659
|
+
Large change list ({changes.length} changes) - using optimized rendering
|
|
660
|
+
</div>
|
|
661
|
+
)}
|
|
662
|
+
|
|
663
|
+
{/* Changes by paragraph */}
|
|
664
|
+
<div className="divide-y divide-border/50">
|
|
665
|
+
{paragraphChanges.map(([paragraphIndex, paragraphChangesList]) =>
|
|
666
|
+
shouldVirtualize ? (
|
|
667
|
+
<VirtualizedParagraph
|
|
668
|
+
key={`para-${paragraphIndex}`}
|
|
669
|
+
paragraphIndex={paragraphIndex}
|
|
670
|
+
paragraphText={paragraphs?.[paragraphIndex]?.text}
|
|
671
|
+
changes={paragraphChangesList}
|
|
672
|
+
highlightedChangeIndex={highlightedChangeIndex}
|
|
673
|
+
onChangeClick={onChangeClick}
|
|
674
|
+
allExpanded={allExpanded}
|
|
675
|
+
hoveredMoveId={hoveredMoveId}
|
|
676
|
+
onMoveHover={handleMoveHover}
|
|
677
|
+
/>
|
|
678
|
+
) : (
|
|
679
|
+
<ParagraphWithChanges
|
|
680
|
+
key={`para-${paragraphIndex}`}
|
|
681
|
+
paragraphIndex={paragraphIndex}
|
|
682
|
+
paragraphText={paragraphs?.[paragraphIndex]?.text}
|
|
683
|
+
changes={paragraphChangesList}
|
|
684
|
+
highlightedChangeIndex={highlightedChangeIndex}
|
|
685
|
+
onChangeClick={onChangeClick}
|
|
686
|
+
allExpanded={allExpanded}
|
|
687
|
+
hoveredMoveId={hoveredMoveId}
|
|
688
|
+
onMoveHover={handleMoveHover}
|
|
689
|
+
/>
|
|
690
|
+
)
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{/* Document-level changes */}
|
|
694
|
+
<OtherChanges
|
|
695
|
+
changes={otherChanges}
|
|
696
|
+
highlightedChangeIndex={highlightedChangeIndex}
|
|
697
|
+
onChangeClick={onChangeClick}
|
|
698
|
+
allExpanded={allExpanded}
|
|
699
|
+
hoveredMoveId={hoveredMoveId}
|
|
700
|
+
onMoveHover={handleMoveHover}
|
|
701
|
+
/>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export default InlineChangesView;
|