From 980172423256e56cb637c0e8bcfa1674550dd0d7 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Mon, 27 Oct 2025 12:20:21 +0000 Subject: [PATCH] pushing final tweaks around scolling and addition/deletion dropdown filtering --- .../public/locales/en-GB/translation.json | 1 + .../public/locales/en-US/translation.json | 1 + .../tools/compare/CompareDocumentPane.tsx | 2 +- .../compare/CompareNavigationDropdown.tsx | 17 ++++++++-- .../hooks/useCompareChangeNavigation.ts | 33 +++++++++++++++++++ .../compare/hooks/useCompareHighlights.ts | 25 ++++++++++---- .../tools/compare/hooks/useComparePanZoom.ts | 33 ++++++++++++++++--- frontend/src/tools/Compare.tsx | 13 ++++++-- 8 files changed, 107 insertions(+), 18 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index f6f8d828c..4fdc5b79b 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1992,6 +1992,7 @@ "2": "Document 2" }, "submit": "Compare", + "newLine": "new-line", "complex": { "message": "One or both of the provided documents are large files, accuracy of comparison may be reduced" }, diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index b57f6cf59..535b0fce4 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1321,6 +1321,7 @@ "removed": "Removed from base", "added": "Added in comparison" }, + "newLine": "new-line", "summary": { "baseHeading": "Base document", "comparisonHeading": "Comparison document", diff --git a/frontend/src/components/tools/compare/CompareDocumentPane.tsx b/frontend/src/components/tools/compare/CompareDocumentPane.tsx index 1dc9756ac..81253b247 100644 --- a/frontend/src/components/tools/compare/CompareDocumentPane.tsx +++ b/frontend/src/components/tools/compare/CompareDocumentPane.tsx @@ -229,7 +229,7 @@ const CompareDocumentPane = ({ style={{ width: `${Math.round(page.width * fit)}px` }} >
; @@ -14,6 +15,8 @@ const CompareNavigationDropdown = ({ className, onNavigate, }: NavigationDropdownProps) => { + const { t } = useTranslation(); + const newLineLabel = t('compare.newLine', 'new-line'); const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), }); @@ -50,13 +53,21 @@ const CompareNavigationDropdown = ({ const [query, setQuery] = useState(''); const normalizedChanges = useMemo(() => { + // Helper to strip localized new-line marker occurrences from labels + const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const stripNewLine = (s: string) => + s + .replace(new RegExp(`\\b${esc(newLineLabel)}\\b`, 'gi'), ' ') + .replace(/\s+/g, ' ') + .trim(); + const cleaned = changes - .map((c) => ({ value: c.value, label: sanitize(c.label), pageNumber: c.pageNumber })) - .filter((c) => isMeaningful(c.label)); + .map((c) => ({ value: c.value, label: stripNewLine(sanitize(c.label)), pageNumber: c.pageNumber })) + .filter((c) => isMeaningful(c.label) && c.label.length > 0 && c.label.toLowerCase() !== newLineLabel.toLowerCase()); const q = sanitize(query).toLowerCase(); if (!q) return cleaned; return cleaned.filter((c) => c.label.toLowerCase().includes(q)); - }, [changes, query]); + }, [changes, query, newLineLabel]); return ( (); nodes.forEach((element) => { const inner = element.closest('.compare-diff-page__inner') as HTMLElement | null; diff --git a/frontend/src/components/tools/compare/hooks/useCompareHighlights.ts b/frontend/src/components/tools/compare/hooks/useCompareHighlights.ts index f86f5d885..c0bf49f08 100644 --- a/frontend/src/components/tools/compare/hooks/useCompareHighlights.ts +++ b/frontend/src/components/tools/compare/hooks/useCompareHighlights.ts @@ -7,6 +7,7 @@ import type { import type { CompareChangeOption } from '../../../../types/compareWorkbench'; import type { PagePreview } from '../../../../hooks/useProgressivePagePreviews'; import type { WordHighlightEntry } from '../types'; +import { PARAGRAPH_SENTINEL } from '../../../../types/compare'; interface TokenGroupMap { base: Map; @@ -42,22 +43,32 @@ const buildWordChanges = ( for (let i = 0; i < tokens.length; i += 1) { const token = tokens[i]; if (token.type === targetType) { - const startIndex = metadataIndex; const parts: string[] = []; const runIndices: number[] = []; const pageNumber = metadata[metadataIndex]?.page ?? 1; while (i < tokens.length && tokens[i].type === targetType) { - parts.push(tokens[i].text); - runIndices.push(metadataIndex); + const t = tokens[i].text; + const isPara = t === PARAGRAPH_SENTINEL || t.startsWith('\uE000') || t.includes('PARA'); + // Skip paragraph sentinel tokens entirely from labels and grouping + if (!isPara) { + parts.push(t); + // Only add to grouping if there is a corresponding metadata index + if (metadata[metadataIndex]) { + runIndices.push(metadataIndex); + } + } metadataIndex += 1; i += 1; } i -= 1; - const endIndex = metadataIndex - 1; - const groupId = `${groupPrefix}-${startIndex}-${endIndex}`; - runIndices.forEach((idx) => tokenIndexToGroupId.set(idx, groupId)); const label = parts.join(' ').trim(); - items.push({ value: groupId, label: label || '(…)', pageNumber }); + if (label.length > 0 && runIndices.length > 0) { + const startIndexForId = runIndices[0]; + const endIndexForId = runIndices[runIndices.length - 1]; + const groupId = `${groupPrefix}-${startIndexForId}-${endIndexForId}`; + runIndices.forEach((idx) => tokenIndexToGroupId.set(idx, groupId)); + items.push({ value: groupId, label, pageNumber }); + } continue; } if (token.type !== (targetType === 'added' ? 'removed' : 'added')) { diff --git a/frontend/src/components/tools/compare/hooks/useComparePanZoom.ts b/frontend/src/components/tools/compare/hooks/useComparePanZoom.ts index 288d057e3..39bf7bf59 100644 --- a/frontend/src/components/tools/compare/hooks/useComparePanZoom.ts +++ b/frontend/src/components/tools/compare/hooks/useComparePanZoom.ts @@ -29,6 +29,13 @@ interface ScrollLinkDelta { horizontal: number; } +// Pixel-based anchors captured when linking scroll, to preserve the +// visual offset between panes and avoid an initial snap. +interface ScrollLinkAnchors { + deltaPixelsBaseToComp: number; + deltaPixelsCompToBase: number; +} + interface PanDragState { active: boolean; source: Pane | null; @@ -93,6 +100,10 @@ export const useComparePanZoom = ({ const comparisonScrollRef = useRef(null); const isSyncingRef = useRef(false); const scrollLinkDeltaRef = useRef({ vertical: 0, horizontal: 0 }); + const scrollLinkAnchorsRef = useRef({ + deltaPixelsBaseToComp: 0, + deltaPixelsCompToBase: 0, + }); const [isScrollLinked, setIsScrollLinked] = useState(true); const [isPanMode, setIsPanMode] = useState(false); const panDragRef = useRef({ @@ -328,12 +339,16 @@ export const useComparePanZoom = ({ lastActivePaneRef.current = source === baseScrollRef.current ? 'base' : 'comparison'; const sourceIsBase = source === baseScrollRef.current; - const deltaV = scrollLinkDeltaRef.current.vertical; const targetVerticalRange = Math.max(1, target.scrollHeight - target.clientHeight); const mappedTop = mapScrollTopBetweenPanes(source.scrollTop, sourceIsBase); - const offset = deltaV * targetVerticalRange; - const desiredTop = Math.max(0, Math.min(targetVerticalRange, mappedTop + (sourceIsBase ? offset : -offset))); + + // Use pixel anchors captured at link time to preserve offset + const deltaPx = sourceIsBase + ? scrollLinkAnchorsRef.current.deltaPixelsBaseToComp + : scrollLinkAnchorsRef.current.deltaPixelsCompToBase; + + const desiredTop = Math.max(0, Math.min(targetVerticalRange, mappedTop + deltaPx)); isSyncingRef.current = true; target.scrollTop = desiredTop; @@ -623,6 +638,7 @@ export const useComparePanZoom = ({ const compEl = comparisonScrollRef.current; if (!baseEl || !compEl) { scrollLinkDeltaRef.current = { vertical: 0, horizontal: 0 }; + scrollLinkAnchorsRef.current = { deltaPixelsBaseToComp: 0, deltaPixelsCompToBase: 0 }; return; } const baseVMax = Math.max(1, baseEl.scrollHeight - baseEl.clientHeight); @@ -639,10 +655,19 @@ export const useComparePanZoom = ({ vertical: compV - baseV, horizontal: compH - baseH, }; - }, []); + + // Capture pixel anchors in mapped space + const mappedBaseToComp = mapScrollTopBetweenPanes(baseEl.scrollTop, true); + const mappedCompToBase = mapScrollTopBetweenPanes(compEl.scrollTop, false); + scrollLinkAnchorsRef.current = { + deltaPixelsBaseToComp: compEl.scrollTop - mappedBaseToComp, + deltaPixelsCompToBase: baseEl.scrollTop - mappedCompToBase, + }; + }, [mapScrollTopBetweenPanes]); const clearScrollLinkDelta = useCallback(() => { scrollLinkDeltaRef.current = { vertical: 0, horizontal: 0 }; + scrollLinkAnchorsRef.current = { deltaPixelsBaseToComp: 0, deltaPixelsCompToBase: 0 }; }, []); const zoomLimits = useMemo(() => ({ min: ZOOM_MIN, max: ZOOM_MAX, step: ZOOM_STEP }), []); diff --git a/frontend/src/tools/Compare.tsx b/frontend/src/tools/Compare.tsx index bb0663c55..def35e21d 100644 --- a/frontend/src/tools/Compare.tsx +++ b/frontend/src/tools/Compare.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import CompareRoundedIcon from '@mui/icons-material/CompareRounded'; import { Box, Group, Stack, Text, Button } from '@mantine/core'; +import { Tooltip } from '../components/shared/Tooltip'; import { createToolFlow } from '../components/tools/shared/createToolFlow'; import { useBaseTool } from '../hooks/tools/shared/useBaseTool'; import { BaseToolProps, ToolComponent } from '../types/tool'; @@ -22,6 +23,7 @@ import type { StirlingFile } from '../types/fileContext'; import DocumentThumbnail from '../components/shared/filePreview/DocumentThumbnail'; import './compareTool.css'; import type { CompareWorkbenchData } from '../types/compareWorkbench'; +import FitText from '../components/shared/FitText'; const CUSTOM_VIEW_ID = 'compareWorkbenchView'; const CUSTOM_WORKBENCH_ID = 'custom:compareWorkbenchView' as const; @@ -234,9 +236,14 @@ const Compare = (props: BaseToolProps) => { - - {stub?.name} - + + + {meta && ( {meta}