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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. 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;