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,430 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ ArrowRight,
5
+ CheckCircle,
6
+ XCircle,
7
+ AlertCircle,
8
+ Eye,
9
+ EyeOff,
10
+ Filter,
11
+ Search,
12
+ ExternalLink,
13
+ Hash,
14
+ } from 'lucide-react';
15
+ import { cn } from '@/utils/cn';
16
+ import { sanitizeUrl } from '@/utils/urlSanitizer';
17
+ import { Input } from '@/components/common/Input';
18
+ import { Button } from '@/components/common/Button';
19
+
20
+ interface HyperlinkChange {
21
+ id: string;
22
+ displayText: string;
23
+ originalUrl: string;
24
+ newUrl?: string;
25
+ type: 'append' | 'update' | 'remove' | 'validate';
26
+ status: 'pending' | 'approved' | 'rejected' | 'applied';
27
+ location: string; // e.g., "Main Document", "Header", "Footer"
28
+ context?: string; // Surrounding text
29
+ willAppendContentId?: boolean;
30
+ contentId?: string;
31
+ }
32
+
33
+ interface HyperlinkPreviewProps {
34
+ changes: HyperlinkChange[];
35
+ onApprove?: (changeId: string) => void;
36
+ onReject?: (changeId: string) => void;
37
+ onApproveAll?: () => void;
38
+ onRejectAll?: () => void;
39
+ onApply?: () => void;
40
+ isReadOnly?: boolean;
41
+ className?: string;
42
+ }
43
+
44
+ export function HyperlinkPreview({
45
+ changes,
46
+ onApprove,
47
+ onReject,
48
+ onApproveAll,
49
+ onRejectAll,
50
+ onApply,
51
+ isReadOnly = false,
52
+ className,
53
+ }: HyperlinkPreviewProps) {
54
+ const [searchTerm, setSearchTerm] = useState('');
55
+ const [filterType, setFilterType] = useState<'all' | 'append' | 'update' | 'remove'>('all');
56
+ const [showOnlyPending, setShowOnlyPending] = useState(false);
57
+ const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
58
+
59
+ // Memoize filtered changes to prevent recalculation on every render
60
+ const filteredChanges = useMemo(() => {
61
+ return changes.filter((change) => {
62
+ if (filterType !== 'all' && change.type !== filterType) return false;
63
+ if (showOnlyPending && change.status !== 'pending') return false;
64
+ if (searchTerm) {
65
+ const search = searchTerm.toLowerCase();
66
+ return (
67
+ change.displayText.toLowerCase().includes(search) ||
68
+ change.originalUrl.toLowerCase().includes(search) ||
69
+ change.newUrl?.toLowerCase().includes(search)
70
+ );
71
+ }
72
+ return true;
73
+ });
74
+ }, [changes, filterType, showOnlyPending, searchTerm]);
75
+
76
+ // Memoize status counts to prevent recalculation
77
+ const { pendingCount, approvedCount, rejectedCount } = useMemo(
78
+ () => ({
79
+ pendingCount: changes.filter((c) => c.status === 'pending').length,
80
+ approvedCount: changes.filter((c) => c.status === 'approved').length,
81
+ rejectedCount: changes.filter((c) => c.status === 'rejected').length,
82
+ }),
83
+ [changes]
84
+ );
85
+
86
+ const toggleExpanded = useCallback((id: string) => {
87
+ setExpandedItems((prev) => {
88
+ const newSet = new Set(prev);
89
+ if (newSet.has(id)) {
90
+ newSet.delete(id);
91
+ } else {
92
+ newSet.add(id);
93
+ }
94
+ return newSet;
95
+ });
96
+ }, []);
97
+
98
+ // Returns icon with accessible text label for change type
99
+ const getChangeIcon = (type: HyperlinkChange['type']) => {
100
+ switch (type) {
101
+ case 'append':
102
+ return (
103
+ <span className="inline-flex items-center gap-1" title="Append Content ID">
104
+ <Hash className="w-4 h-4 text-blue-500" aria-hidden="true" />
105
+ <span className="sr-only">Append</span>
106
+ </span>
107
+ );
108
+ case 'update':
109
+ return (
110
+ <span className="inline-flex items-center gap-1" title="Update URL">
111
+ <ArrowRight className="w-4 h-4 text-yellow-500" aria-hidden="true" />
112
+ <span className="sr-only">Update</span>
113
+ </span>
114
+ );
115
+ case 'remove':
116
+ return (
117
+ <span className="inline-flex items-center gap-1" title="Remove Hyperlink">
118
+ <XCircle className="w-4 h-4 text-red-500" aria-hidden="true" />
119
+ <span className="sr-only">Remove</span>
120
+ </span>
121
+ );
122
+ case 'validate':
123
+ return (
124
+ <span className="inline-flex items-center gap-1" title="Validate URL">
125
+ <CheckCircle className="w-4 h-4 text-green-500" aria-hidden="true" />
126
+ <span className="sr-only">Validate</span>
127
+ </span>
128
+ );
129
+ }
130
+ };
131
+
132
+ const getStatusBadge = (status: HyperlinkChange['status']) => {
133
+ switch (status) {
134
+ case 'pending':
135
+ return (
136
+ <span className="px-2 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full">
137
+ Pending
138
+ </span>
139
+ );
140
+ case 'approved':
141
+ return (
142
+ <span className="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-700 rounded-full">
143
+ Approved
144
+ </span>
145
+ );
146
+ case 'rejected':
147
+ return (
148
+ <span className="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700 rounded-full">
149
+ Rejected
150
+ </span>
151
+ );
152
+ case 'applied':
153
+ return (
154
+ <span className="px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-700 rounded-full">
155
+ Applied
156
+ </span>
157
+ );
158
+ }
159
+ };
160
+
161
+ const truncateUrl = (url: string, maxLength: number = 50): string => {
162
+ if (url.length <= maxLength) return url;
163
+ return url.substring(0, maxLength - 3) + '...';
164
+ };
165
+
166
+ return (
167
+ <div className={cn('space-y-4', className)}>
168
+ {/* Header */}
169
+ <div className="flex items-center justify-between">
170
+ <div>
171
+ <h3 className="font-semibold text-lg">Hyperlink Changes Preview</h3>
172
+ <p className="text-sm text-muted-foreground">
173
+ Review and approve changes before applying
174
+ </p>
175
+ </div>
176
+ {!isReadOnly && pendingCount > 0 && (
177
+ <div className="flex items-center gap-2">
178
+ <Button variant="outline" size="sm" onClick={onRejectAll}>
179
+ Reject All
180
+ </Button>
181
+ <Button variant="outline" size="sm" onClick={onApproveAll}>
182
+ Approve All
183
+ </Button>
184
+ </div>
185
+ )}
186
+ </div>
187
+
188
+ {/* Stats */}
189
+ <div className="grid grid-cols-4 gap-3">
190
+ <div className="text-center p-3 bg-muted/30 rounded-lg">
191
+ <p className="text-lg font-bold">{changes.length}</p>
192
+ <p className="text-xs text-muted-foreground">Total Changes</p>
193
+ </div>
194
+ <div className="text-center p-3 bg-yellow-500/10 rounded-lg">
195
+ <p className="text-lg font-bold text-yellow-600">{pendingCount}</p>
196
+ <p className="text-xs text-muted-foreground">Pending</p>
197
+ </div>
198
+ <div className="text-center p-3 bg-green-500/10 rounded-lg">
199
+ <p className="text-lg font-bold text-green-600">{approvedCount}</p>
200
+ <p className="text-xs text-muted-foreground">Approved</p>
201
+ </div>
202
+ <div className="text-center p-3 bg-red-500/10 rounded-lg">
203
+ <p className="text-lg font-bold text-red-600">{rejectedCount}</p>
204
+ <p className="text-xs text-muted-foreground">Rejected</p>
205
+ </div>
206
+ </div>
207
+
208
+ {/* Filters */}
209
+ <div className="flex flex-col sm:flex-row gap-3">
210
+ <div className="flex-1">
211
+ <Input
212
+ value={searchTerm}
213
+ onChange={(e) => setSearchTerm(e.target.value)}
214
+ placeholder="Search hyperlinks..."
215
+ leftIcon={<Search className="w-4 h-4" />}
216
+ onClear={() => setSearchTerm('')}
217
+ />
218
+ </div>
219
+ <div className="flex items-center gap-2">
220
+ <button
221
+ onClick={() => setFilterType('all')}
222
+ className={cn(
223
+ 'px-3 py-1.5 text-sm rounded-lg transition-colors',
224
+ filterType === 'all'
225
+ ? 'bg-primary text-primary-foreground'
226
+ : 'bg-muted hover:bg-muted/70'
227
+ )}
228
+ >
229
+ All
230
+ </button>
231
+ <button
232
+ onClick={() => setFilterType('append')}
233
+ className={cn(
234
+ 'px-3 py-1.5 text-sm rounded-lg transition-colors',
235
+ filterType === 'append'
236
+ ? 'bg-primary text-primary-foreground'
237
+ : 'bg-muted hover:bg-muted/70'
238
+ )}
239
+ >
240
+ Append
241
+ </button>
242
+ <button
243
+ onClick={() => setFilterType('update')}
244
+ className={cn(
245
+ 'px-3 py-1.5 text-sm rounded-lg transition-colors',
246
+ filterType === 'update'
247
+ ? 'bg-primary text-primary-foreground'
248
+ : 'bg-muted hover:bg-muted/70'
249
+ )}
250
+ >
251
+ Update
252
+ </button>
253
+ <button
254
+ onClick={() => setShowOnlyPending(!showOnlyPending)}
255
+ className={cn(
256
+ 'p-1.5 rounded-lg transition-colors',
257
+ showOnlyPending ? 'bg-primary text-primary-foreground' : 'bg-muted hover:bg-muted/70'
258
+ )}
259
+ >
260
+ <Filter className="w-4 h-4" />
261
+ </button>
262
+ </div>
263
+ </div>
264
+
265
+ {/* Changes List */}
266
+ <div className="space-y-2 max-h-[500px] overflow-y-auto">
267
+ <AnimatePresence>
268
+ {filteredChanges.map((change, index) => {
269
+ const isExpanded = expandedItems.has(change.id);
270
+
271
+ return (
272
+ <motion.div
273
+ key={change.id}
274
+ initial={{ opacity: 0, y: 10 }}
275
+ animate={{ opacity: 1, y: 0 }}
276
+ exit={{ opacity: 0, y: -10 }}
277
+ transition={{ delay: index * 0.02 }}
278
+ className={cn(
279
+ 'border rounded-lg transition-all',
280
+ change.status === 'approved' && 'bg-green-500/5 border-green-500/20',
281
+ change.status === 'rejected' && 'bg-red-500/5 border-red-500/20',
282
+ change.status === 'pending' && 'hover:bg-muted/50'
283
+ )}
284
+ >
285
+ <div className="p-3">
286
+ <div className="flex items-start gap-3">
287
+ {getChangeIcon(change.type)}
288
+
289
+ <div className="flex-1 min-w-0">
290
+ {/* Display Text and Status */}
291
+ <div className="flex items-center gap-2 mb-1">
292
+ <span className="font-medium truncate">{change.displayText}</span>
293
+ {getStatusBadge(change.status)}
294
+ <span className="text-xs text-muted-foreground">{change.location}</span>
295
+ </div>
296
+
297
+ {/* URL Changes */}
298
+ <div className="space-y-1">
299
+ <div className="flex items-center gap-2 text-sm">
300
+ <span className="text-muted-foreground">From:</span>
301
+ <code className="text-xs bg-muted px-1 py-0.5 rounded">
302
+ {truncateUrl(change.originalUrl)}
303
+ </code>
304
+ </div>
305
+
306
+ {change.newUrl && (
307
+ <div className="flex items-center gap-2 text-sm">
308
+ <span className="text-muted-foreground">To:</span>
309
+ <code className="text-xs bg-primary/10 text-primary px-1 py-0.5 rounded">
310
+ {truncateUrl(change.newUrl)}
311
+ </code>
312
+ </div>
313
+ )}
314
+
315
+ {change.willAppendContentId && change.contentId && (
316
+ <div className="flex items-center gap-2 text-sm">
317
+ <Hash className="w-3 h-3 text-blue-500" />
318
+ <span className="text-blue-600">
319
+ Will append: <code>{change.contentId}</code>
320
+ </span>
321
+ </div>
322
+ )}
323
+ </div>
324
+
325
+ {/* Expanded Context */}
326
+ <AnimatePresence>
327
+ {isExpanded && change.context && (
328
+ <motion.div
329
+ initial={{ height: 0, opacity: 0 }}
330
+ animate={{ height: 'auto', opacity: 1 }}
331
+ exit={{ height: 0, opacity: 0 }}
332
+ transition={{ duration: 0.2 }}
333
+ className="mt-2 pt-2 border-t"
334
+ >
335
+ <p className="text-sm text-muted-foreground">
336
+ <strong>Context:</strong> {change.context}
337
+ </p>
338
+ <div className="mt-2 flex items-center gap-2">
339
+ {/*
340
+ Security: URLs are sanitized via sanitizeUrl() which validates protocols
341
+ and blocks javascript:, data:, vbscript:, file: and other dangerous schemes.
342
+ Only http:, https:, mailto:, tel:, ftp: protocols are allowed.
343
+ See: src/utils/urlSanitizer.ts
344
+ */}
345
+ {/* deepcode ignore XSS: URL is sanitized via sanitizeUrl() function */}
346
+ <a
347
+ href={sanitizeUrl(change.originalUrl)}
348
+ target="_blank"
349
+ rel="noopener noreferrer"
350
+ className="text-xs text-primary hover:underline flex items-center gap-1"
351
+ >
352
+ View original <ExternalLink className="w-3 h-3" />
353
+ </a>
354
+ {change.newUrl && (
355
+ // deepcode ignore XSS: URL is sanitized via sanitizeUrl() function
356
+ <a
357
+ href={sanitizeUrl(change.newUrl)}
358
+ target="_blank"
359
+ rel="noopener noreferrer"
360
+ className="text-xs text-primary hover:underline flex items-center gap-1"
361
+ >
362
+ View new <ExternalLink className="w-3 h-3" />
363
+ </a>
364
+ )}
365
+ </div>
366
+ </motion.div>
367
+ )}
368
+ </AnimatePresence>
369
+ </div>
370
+
371
+ {/* Actions */}
372
+ <div className="flex items-center gap-1" role="group" aria-label="Change actions">
373
+ <button
374
+ onClick={() => toggleExpanded(change.id)}
375
+ className="p-1 hover:bg-muted rounded-md transition-colors"
376
+ aria-label={isExpanded ? 'Hide details' : 'Show details'}
377
+ aria-expanded={isExpanded}
378
+ >
379
+ {isExpanded ? (
380
+ <EyeOff className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
381
+ ) : (
382
+ <Eye className="w-4 h-4 text-muted-foreground" aria-hidden="true" />
383
+ )}
384
+ </button>
385
+
386
+ {!isReadOnly && change.status === 'pending' && (
387
+ <>
388
+ <button
389
+ onClick={() => onApprove?.(change.id)}
390
+ className="p-1 hover:bg-green-500/10 rounded-md transition-colors"
391
+ aria-label="Approve change"
392
+ >
393
+ <CheckCircle className="w-4 h-4 text-green-500" aria-hidden="true" />
394
+ </button>
395
+ <button
396
+ onClick={() => onReject?.(change.id)}
397
+ aria-label="Reject change"
398
+ className="p-1 hover:bg-red-500/10 rounded-md transition-colors"
399
+ >
400
+ <XCircle className="w-4 h-4 text-red-500" />
401
+ </button>
402
+ </>
403
+ )}
404
+ </div>
405
+ </div>
406
+ </div>
407
+ </motion.div>
408
+ );
409
+ })}
410
+ </AnimatePresence>
411
+
412
+ {filteredChanges.length === 0 && (
413
+ <div className="text-center py-8">
414
+ <AlertCircle className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
415
+ <p className="text-muted-foreground">No changes found</p>
416
+ </div>
417
+ )}
418
+ </div>
419
+
420
+ {/* Apply Button */}
421
+ {!isReadOnly && approvedCount > 0 && (
422
+ <div className="flex justify-end">
423
+ <Button onClick={onApply} className="min-w-[120px]">
424
+ Apply {approvedCount} Change{approvedCount !== 1 ? 's' : ''}
425
+ </Button>
426
+ </div>
427
+ )}
428
+ </div>
429
+ );
430
+ }