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,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">&nbsp;</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;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Comparison Components - Document comparison and diff views
3
+ *
4
+ * Exports:
5
+ * - SideBySideDiff: GitHub/VS Code style side-by-side comparison
6
+ */
7
+
8
+ export { SideBySideDiff } from './SideBySideDiff';