mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
cleanups, delete unused files, revert unnecessary git changes, etc etc
This commit is contained in:
parent
48be000a1b
commit
28173fe9a4
@ -2173,6 +2173,9 @@
|
||||
"comparisonHeading": "Comparison document",
|
||||
"pageLabel": "Page"
|
||||
},
|
||||
"rendering": {
|
||||
"rendering": "rendering"
|
||||
},
|
||||
"dropdown": {
|
||||
"deletions": "Deletions ({{count}})",
|
||||
"additions": "Additions ({{count}})",
|
||||
|
||||
@ -1336,6 +1336,9 @@
|
||||
"comparisonHeading": "Comparison document",
|
||||
"pageLabel": "Page"
|
||||
},
|
||||
"rendering": {
|
||||
"rendering": "rendering"
|
||||
},
|
||||
"status": {
|
||||
"extracting": "Extracting text...",
|
||||
"processing": "Analyzing differences...",
|
||||
|
||||
@ -1,45 +1,21 @@
|
||||
import { Alert, Group, Loader, Stack, Text } from '@mantine/core';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import type { PagePreview, WordHighlightEntry } from './types';
|
||||
import type { TokenBoundingBox } from '@app/types/compare';
|
||||
import { useMemo } from 'react';
|
||||
import type { PagePreview } from '@app/types/compare';
|
||||
import type { TokenBoundingBox, CompareDocumentPaneProps } from '@app/types/compare';
|
||||
import CompareNavigationDropdown from './CompareNavigationDropdown';
|
||||
import { toRgba } from './compareUtils';
|
||||
import LazyLoadContainer from '@app/components/shared/LazyLoadContainer';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
|
||||
interface CompareDocumentPaneProps {
|
||||
pane: 'base' | 'comparison';
|
||||
layout: 'side-by-side' | 'stacked';
|
||||
scrollRef: RefObject<HTMLDivElement | null>;
|
||||
peerScrollRef: RefObject<HTMLDivElement | null>;
|
||||
handleScrollSync: (source: HTMLDivElement | null, target: HTMLDivElement | null) => void;
|
||||
beginPan: (pane: 'base' | 'comparison', event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
continuePan: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
endPan: () => void;
|
||||
handleWheelZoom: (pane: 'base' | 'comparison', event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
handleWheelOverscroll: (pane: 'base' | 'comparison', event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
onTouchStart: (pane: 'base' | 'comparison', event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchMove: (event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchEnd: (event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
isPanMode: boolean;
|
||||
zoom: number;
|
||||
pan?: { x: number; y: number };
|
||||
title: string;
|
||||
dropdownPlaceholder?: string;
|
||||
changes: Array<{ value: string; label: string; pageNumber?: number }>;
|
||||
onNavigateChange: (id: string, pageNumber?: number) => void;
|
||||
isLoading: boolean;
|
||||
processingMessage: string;
|
||||
emptyMessage: string;
|
||||
pages: PagePreview[];
|
||||
pairedPages: PagePreview[];
|
||||
getRowHeightPx: (pageNumber: number) => number;
|
||||
wordHighlightMap: Map<number, WordHighlightEntry[]>;
|
||||
metaIndexToGroupId: Map<number, string>;
|
||||
documentLabel: string;
|
||||
pageLabel: string;
|
||||
altLabel: string;
|
||||
}
|
||||
const toRgba = (hexColor: string, alpha: number): string => {
|
||||
const hex = hexColor.replace('#', '');
|
||||
if (hex.length !== 6) {
|
||||
return hexColor;
|
||||
}
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
// Merge overlapping or touching rects into larger non-overlapping blocks.
|
||||
// This is more robust across rotations (vertical "lines" etc.) and prevents dark spots.
|
||||
@ -110,7 +86,7 @@ const CompareDocumentPane = ({
|
||||
pageLabel,
|
||||
altLabel,
|
||||
}: CompareDocumentPaneProps) => {
|
||||
const isMobileViewport = useMediaQuery('(max-width: 1024px)');
|
||||
const isMobileViewport = useIsMobile();
|
||||
const pairedPageMap = useMemo(() => {
|
||||
const map = new Map<number, PagePreview>();
|
||||
pairedPages.forEach((item) => {
|
||||
@ -139,6 +115,7 @@ const CompareDocumentPane = ({
|
||||
placeholder={dropdownPlaceholder ?? ''}
|
||||
className={pane === 'comparison' ? 'compare-changes-select--comparison' : undefined}
|
||||
onNavigate={onNavigateChange}
|
||||
renderedPageNumbers={useMemo(() => new Set(pages.map(p => p.pageNumber)), [pages])}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
@ -180,7 +157,9 @@ const CompareDocumentPane = ({
|
||||
const highlightOffset = OFFSET_PIXELS / page.height;
|
||||
const rotationNorm = ((page.rotation ?? 0) % 360 + 360) % 360;
|
||||
const isPortrait = rotationNorm === 0 || rotationNorm === 180;
|
||||
const isLandscape = rotationNorm === 90 || rotationNorm === 270;
|
||||
const isStackedPortrait = layout === 'stacked' && isPortrait;
|
||||
const isStackedLandscape = layout === 'stacked' && isLandscape;
|
||||
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 1200;
|
||||
const containerW = scrollRef.current?.clientWidth ?? viewportWidth;
|
||||
const stackedWidth = isMobileViewport
|
||||
@ -213,7 +192,11 @@ const CompareDocumentPane = ({
|
||||
</Text>
|
||||
<div
|
||||
className="compare-diff-page__canvas compare-diff-page__canvas--zoom"
|
||||
style={{ width: `${Math.round(page.width * fit)}px` }}
|
||||
style={isStackedPortrait
|
||||
? { width: `${stackedWidth}px`, height: `${stackedHeight}px`, marginLeft: 'auto', marginRight: 'auto' }
|
||||
: isStackedLandscape
|
||||
? { width: `${Math.round(page.width * fit)}px`, marginLeft: 'auto', marginRight: 'auto' }
|
||||
: { width: `${Math.round(page.width * fit)}px` }}
|
||||
>
|
||||
<div
|
||||
className="compare-diff-page__inner"
|
||||
@ -249,6 +232,8 @@ const CompareDocumentPane = ({
|
||||
className="compare-diff-page__canvas compare-diff-page__canvas--zoom"
|
||||
style={isStackedPortrait
|
||||
? { width: `${stackedWidth}px`, height: `${stackedHeight}px`, marginLeft: 'auto', marginRight: 'auto' }
|
||||
: isStackedLandscape
|
||||
? { width: `${Math.round(page.width * fit)}px`, marginLeft: 'auto', marginRight: 'auto' }
|
||||
: { width: `${Math.round(page.width * fit)}px` }}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import { Combobox, ScrollArea, useCombobox } from '@mantine/core';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface NavigationDropdownProps {
|
||||
changes: Array<{ value: string; label: string; pageNumber?: number }>;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
onNavigate: (value: string, pageNumber?: number) => void;
|
||||
}
|
||||
import type { NavigationDropdownProps } from '@app/types/compare';
|
||||
|
||||
const CompareNavigationDropdown = ({
|
||||
changes,
|
||||
placeholder,
|
||||
className,
|
||||
onNavigate,
|
||||
renderedPageNumbers,
|
||||
}: NavigationDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const newLineLabel = t('compare.newLine', 'new-line');
|
||||
@ -63,15 +58,26 @@ const CompareNavigationDropdown = ({
|
||||
|
||||
const isMeaningful = (s: string) => {
|
||||
const t = sanitize(s);
|
||||
// Keep only items that have at least one letter or digit (unicode-aware)
|
||||
try {
|
||||
if (!/[\p{L}\p{N}]/u.test(t)) return false;
|
||||
} catch {
|
||||
if (!/[A-Za-z0-9]/.test(t)) return false;
|
||||
}
|
||||
|
||||
// Build a unicode-aware regex if supported; otherwise fall back to a plain ASCII class.
|
||||
const rx =
|
||||
(() => {
|
||||
try {
|
||||
// Construct at runtime so old engines don’t fail parse-time
|
||||
console.debug('Using Unicode props');
|
||||
return new RegExp('[\\p{L}\\p{N}\\p{P}\\p{S}]', 'u');
|
||||
} catch {
|
||||
// Fallback (no Unicode props): letters, digits, and common punctuation/symbols
|
||||
console.debug('No Unicode props, falling back to ASCII class');
|
||||
return /[A-Za-z0-9.,!?;:(){}"'`~@#$%^&*+=|<>/[\]]/;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!rx.test(t)) return false;
|
||||
return t.length > 0;
|
||||
};
|
||||
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const [stickyPage, setStickyPage] = useState<number | null>(null);
|
||||
@ -168,6 +174,9 @@ const CompareNavigationDropdown = ({
|
||||
{stickyPage != null && (
|
||||
<div className="compare-dropdown-sticky" style={{ top: 0 }}>
|
||||
{t('compare.summary.pageLabel', 'Page')}{' '}{stickyPage}
|
||||
{renderedPageNumbers && !renderedPageNumbers.has(stickyPage) && (
|
||||
<span className="compare-dropdown-rendering-flag"> — {t('compare.rendering.rendering', 'rendering')}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Combobox.Options className="compare-dropdown-options">
|
||||
@ -184,6 +193,9 @@ const CompareNavigationDropdown = ({
|
||||
key={`group-${lastPage}`}
|
||||
>
|
||||
{t('compare.summary.pageLabel', 'Page')}{' '}{lastPage}
|
||||
{renderedPageNumbers && !renderedPageNumbers.has(lastPage) && (
|
||||
<span className="compare-dropdown-rendering-flag"> — {t('compare.rendering.rendering', 'rendering')}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||
import SwapHorizRoundedIcon from '@mui/icons-material/SwapHorizRounded';
|
||||
import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CompareReviewActionsProps {
|
||||
onSwitchOrder: () => void;
|
||||
onDownloadSummary: () => void;
|
||||
disableDownload?: boolean;
|
||||
disableSwitch?: boolean;
|
||||
}
|
||||
|
||||
const CompareReviewActions = ({
|
||||
onSwitchOrder,
|
||||
onDownloadSummary,
|
||||
disableDownload = false,
|
||||
disableSwitch = false,
|
||||
}: CompareReviewActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('compare.review.actionsHint', 'Review the comparison, switch document roles, or export the summary.')}
|
||||
</Text>
|
||||
<Group grow>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="var(--mantine-color-gray-6)"
|
||||
leftSection={<SwapHorizRoundedIcon fontSize="small" />}
|
||||
onClick={onSwitchOrder}
|
||||
disabled={disableSwitch}
|
||||
>
|
||||
{t('compare.review.switchOrder', 'Switch order')}
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
leftSection={<DownloadRoundedIcon fontSize="small" />}
|
||||
onClick={onDownloadSummary}
|
||||
disabled={disableDownload}
|
||||
>
|
||||
{t('compare.review.exportSummary', 'Export summary')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareReviewActions;
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Badge, Card, Group, Select, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAllFiles } from '../../../contexts/FileContext';
|
||||
import { formatFileSize } from '../../../utils/fileUtils';
|
||||
import type { FileId } from '../../../types/file';
|
||||
|
||||
interface CompareSelectionStepProps {
|
||||
role: 'base' | 'comparison';
|
||||
selectedFileId: FileId | null;
|
||||
onFileSelect: (fileId: FileId | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const CompareSelectionStep = ({
|
||||
role,
|
||||
selectedFileId,
|
||||
onFileSelect,
|
||||
disabled = false,
|
||||
}: CompareSelectionStepProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { fileStubs } = useAllFiles();
|
||||
|
||||
const labels = useMemo(() => {
|
||||
if (role === 'base') {
|
||||
return {
|
||||
title: t('compare.base.label', 'Base document'),
|
||||
placeholder: t('compare.base.placeholder', 'Select a base PDF'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('compare.comparison.label', 'Comparison document'),
|
||||
placeholder: t('compare.comparison.placeholder', 'Select a comparison PDF'),
|
||||
};
|
||||
}, [role, t]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return fileStubs
|
||||
.filter((stub) => stub.type?.includes('pdf') || stub.name.toLowerCase().endsWith('.pdf'))
|
||||
.map((stub) => ({
|
||||
value: stub.id as unknown as string,
|
||||
label: stub.name,
|
||||
}));
|
||||
}, [fileStubs]);
|
||||
|
||||
const selectedStub = useMemo(() => fileStubs.find((stub) => stub.id === selectedFileId), [fileStubs, selectedFileId]);
|
||||
|
||||
const selectValue = selectedFileId ? (selectedFileId as unknown as string) : null;
|
||||
|
||||
// Hide dropdown until there are files in the workbench
|
||||
if (options.length === 0) {
|
||||
return (
|
||||
<Card withBorder padding="sm" radius="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('compare.addFilesHint', 'Add PDFs in the Files step to enable selection.')}
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
data={options}
|
||||
searchable
|
||||
clearable
|
||||
value={selectValue}
|
||||
label={labels.title}
|
||||
placeholder={labels.placeholder}
|
||||
onChange={(value) => onFileSelect(value ? (value as FileId) : null)}
|
||||
nothingFoundMessage={t('compare.noFiles', 'No PDFs available yet')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{selectedStub && (
|
||||
<Card withBorder padding="sm" radius="md">
|
||||
<Stack gap={4}>
|
||||
<Text fw={600} size="sm">
|
||||
{selectedStub.name}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Badge color="blue" variant="light">
|
||||
{formatFileSize(selectedStub.size ?? 0)}
|
||||
</Badge>
|
||||
{selectedStub.processedFile?.totalPages && (
|
||||
<Badge color="gray" variant="light">
|
||||
{t('compare.pageCount', '{{count}} pages', { count: selectedStub.processedFile.totalPages })}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{selectedStub.lastModified && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('compare.lastModified', 'Last modified')}{' '}
|
||||
{new Date(selectedStub.lastModified).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompareSelectionStep;
|
||||
@ -3,30 +3,9 @@ import { Button, Stack, Text } from '@mantine/core';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { formatFileSize } from '@app/utils/fileUtils';
|
||||
import type { StirlingFileStub } from '@app/types/fileContext';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface UploadColumnProps {
|
||||
role: 'base' | 'comparison';
|
||||
file: File | null;
|
||||
stub: StirlingFileStub | null;
|
||||
title: string;
|
||||
description: string;
|
||||
accentClass: string;
|
||||
disabled: boolean;
|
||||
onDrop: (files: File[]) => void;
|
||||
onSelectExisting: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
interface CompareUploadSectionProps {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
disabled: boolean;
|
||||
base: UploadColumnProps;
|
||||
comparison: UploadColumnProps;
|
||||
}
|
||||
import type { UploadColumnProps, CompareUploadSectionProps } from '@app/types/compare';
|
||||
|
||||
const CompareUploadColumn = ({
|
||||
role,
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import {
|
||||
CompareResultData,
|
||||
CompareWorkbenchData,
|
||||
CompareChangeOption,
|
||||
} from '@app/types/compare';
|
||||
import type { CompareWorkbenchData } from '@app/types/compareWorkbench';
|
||||
import type { FileId } from '@app/types/file';
|
||||
import type { StirlingFileStub, StirlingFile } from '@app/types/fileContext';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
@ -17,7 +18,6 @@ import { useComparePagePreviews } from './hooks/useComparePagePreviews';
|
||||
import { useComparePanZoom } from './hooks/useComparePanZoom';
|
||||
import { useCompareHighlights } from './hooks/useCompareHighlights';
|
||||
import { useCompareChangeNavigation } from './hooks/useCompareChangeNavigation';
|
||||
import type { CompareChangeOption } from '@app/types/compareWorkbench';
|
||||
import './compareView.css';
|
||||
import { useCompareRightRailButtons } from './hooks/useCompareRightRailButtons';
|
||||
import { alert, updateToast, updateToastProgress, dismissToast } from '@app/components/toast';
|
||||
@ -74,7 +74,7 @@ const mapChangesForDropdown = (changes: CompareChangeOption[]) =>
|
||||
|
||||
const CompareWorkbenchView = ({ data }: CompareWorkbenchViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const prefersStacked = useMediaQuery('(max-width: 1024px)') ?? false;
|
||||
const prefersStacked = useIsMobile();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { actions: fileActions } = useFileActions();
|
||||
const { selectors } = useFileContext();
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
export const toRgba = (hexColor: string, alpha: number): string => {
|
||||
const hex = hexColor.replace('#', '');
|
||||
if (hex.length !== 6) {
|
||||
return hexColor;
|
||||
}
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
@ -135,15 +135,17 @@
|
||||
|
||||
/* Style the dropdown container */
|
||||
.compare-changes-select .mantine-Combobox-dropdown {
|
||||
border: 1px solid var(--mantine-color-gray-3) !important;
|
||||
border: 1px solid var(--border-subtle) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
background-color: var(--bg-surface) !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-Combobox-dropdown {
|
||||
border: 1px solid var(--mantine-color-gray-3) !important;
|
||||
border: 1px solid var(--border-subtle) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
background-color: var(--bg-surface) !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for ScrollArea */
|
||||
@ -152,17 +154,17 @@
|
||||
}
|
||||
|
||||
.compare-changes-select .mantine-ScrollArea-viewport::-webkit-scrollbar-track {
|
||||
background: var(--mantine-color-gray-1) !important;
|
||||
background: var(--bg-muted) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.compare-changes-select .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb {
|
||||
background: var(--mantine-color-gray-4) !important;
|
||||
background: var(--border-strong) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.compare-changes-select .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--mantine-color-gray-5) !important;
|
||||
background: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-ScrollArea-viewport::-webkit-scrollbar {
|
||||
@ -170,17 +172,17 @@
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-ScrollArea-viewport::-webkit-scrollbar-track {
|
||||
background: var(--mantine-color-gray-1) !important;
|
||||
background: var(--bg-muted) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb {
|
||||
background: var(--mantine-color-gray-4) !important;
|
||||
background: var(--border-strong) !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-ScrollArea-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--mantine-color-gray-5) !important;
|
||||
background: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
/* Style the dropdown options */
|
||||
@ -206,21 +208,21 @@
|
||||
.compare-changes-select .mantine-Combobox-search {
|
||||
font-size: 0.875rem !important;
|
||||
padding: 8px 12px !important;
|
||||
border-bottom: 1px solid var(--mantine-color-gray-3) !important;
|
||||
border-bottom: 1px solid var(--border-subtle) !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-Combobox-search {
|
||||
font-size: 0.875rem !important;
|
||||
padding: 8px 12px !important;
|
||||
border-bottom: 1px solid var(--mantine-color-gray-3) !important;
|
||||
border-bottom: 1px solid var(--border-subtle) !important;
|
||||
}
|
||||
|
||||
.compare-changes-select .mantine-Combobox-search::placeholder {
|
||||
color: var(--mantine-color-gray-5) !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.compare-changes-select--comparison .mantine-Combobox-search::placeholder {
|
||||
color: var(--mantine-color-gray-5) !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
/* Style the chevron - ensure proper coloring */
|
||||
@ -317,10 +319,10 @@
|
||||
|
||||
.compare-diff-page__canvas {
|
||||
position: relative;
|
||||
border: 1px solid var(--mantine-color-gray-4);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--mantine-color-white);
|
||||
background-color: var(--bg-surface);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -352,7 +354,7 @@
|
||||
.compare-diff-highlight {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: normal; /* reduce dark spots on overlap */
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
/* Compare dropdown option formatting (page + clamped text) */
|
||||
@ -398,6 +400,12 @@
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
/* Light grey rendering flag next to page labels in the dropdown */
|
||||
.compare-dropdown-rendering-flag {
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Inline paragraph highlights in summary */
|
||||
.compare-inline {
|
||||
border-radius: 0.2rem;
|
||||
@ -431,7 +439,7 @@
|
||||
|
||||
.compare-upload-divider {
|
||||
width: 1px;
|
||||
background: rgba(148, 163, 184, 0.5);
|
||||
background: var(--compare-upload-divider);
|
||||
}
|
||||
|
||||
.compare-upload-column {
|
||||
@ -441,9 +449,9 @@
|
||||
|
||||
.compare-upload-dropzone {
|
||||
flex: 1;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.6);
|
||||
border: 1px dashed var(--compare-upload-dropzone-border);
|
||||
border-radius: 1rem;
|
||||
background: rgba(241, 245, 249, 0.45);
|
||||
background: var(--compare-upload-dropzone-bg);
|
||||
padding: 0;
|
||||
transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
@ -479,8 +487,8 @@
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
border-radius: 999px;
|
||||
color: rgba(17, 24, 39, 0.75);
|
||||
background: rgba(148, 163, 184, 0.2);
|
||||
color: var(--compare-upload-icon-color);
|
||||
background: var(--compare-upload-icon-bg);
|
||||
}
|
||||
|
||||
.compare-upload-icon--base {
|
||||
|
||||
@ -46,6 +46,22 @@ export const useCompareChangeNavigation = (
|
||||
return;
|
||||
}
|
||||
if (nodes.length === 0) {
|
||||
// Fallback: ensure we at least scroll both panes to the page if available
|
||||
if (pageNumber) {
|
||||
// Main container already handled via scrollToPageIfNeeded; replicate for peer
|
||||
const peerRef = pane === 'base' ? comparisonScrollRef : baseScrollRef;
|
||||
const peer = peerRef.current;
|
||||
if (peer) {
|
||||
const peerPageEl = peer.querySelector(
|
||||
`.compare-diff-page[data-page-number="${pageNumber}"]`
|
||||
) as HTMLElement | null;
|
||||
if (peerPageEl) {
|
||||
const peerMaxTop = Math.max(0, peer.scrollHeight - peer.clientHeight);
|
||||
const top = Math.max(0, Math.min(peerMaxTop, peerPageEl.offsetTop - Math.round(peer.clientHeight * 0.2)));
|
||||
peer.scrollTo({ top, behavior: 'auto' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type {
|
||||
CompareFilteredTokenInfo,
|
||||
WordHighlightEntry,
|
||||
CompareResultData,
|
||||
} from '../../../../types/compare';
|
||||
import type { CompareChangeOption } from '../../../../types/compareWorkbench';
|
||||
import type { PagePreview } from '../../../../hooks/useProgressivePagePreviews';
|
||||
import type { WordHighlightEntry } from '../types';
|
||||
CompareChangeOption,
|
||||
PagePreview,
|
||||
} from '@app/types/compare';
|
||||
|
||||
interface MetaGroupMap {
|
||||
base: Map<number, string>;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@ -11,7 +10,17 @@ import type {
|
||||
TouchEvent as ReactTouchEvent,
|
||||
WheelEvent as ReactWheelEvent,
|
||||
} from 'react';
|
||||
import type { PagePreview } from '../../../../hooks/useProgressivePagePreviews';
|
||||
import type {
|
||||
PagePreview,
|
||||
ComparePane as Pane,
|
||||
PanState,
|
||||
ScrollLinkDelta,
|
||||
ScrollLinkAnchors,
|
||||
PanDragState,
|
||||
PinchState,
|
||||
UseComparePanZoomOptions,
|
||||
UseComparePanZoomReturn,
|
||||
} from '@app/types/compare';
|
||||
|
||||
const ZOOM_MIN = 0.5;
|
||||
const ZOOM_MAX = 100000;
|
||||
@ -22,80 +31,7 @@ const ZOOM_STEP = 0.1;
|
||||
const DEFAULT_ROW_STRUCTURAL_EXTRA = 32;
|
||||
const DEFAULT_ROW_GAP = 8;
|
||||
|
||||
type Pane = 'base' | 'comparison';
|
||||
|
||||
interface PanState {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface ScrollLinkDelta {
|
||||
vertical: number;
|
||||
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;
|
||||
startX: number;
|
||||
startY: number;
|
||||
startPanX: number;
|
||||
startPanY: number;
|
||||
targetStartPanX: number;
|
||||
targetStartPanY: number;
|
||||
}
|
||||
|
||||
interface PinchState {
|
||||
active: boolean;
|
||||
pane: Pane | null;
|
||||
startDistance: number;
|
||||
startZoom: number;
|
||||
}
|
||||
|
||||
export interface UseComparePanZoomOptions {
|
||||
prefersStacked: boolean;
|
||||
basePages: PagePreview[];
|
||||
comparisonPages: PagePreview[];
|
||||
}
|
||||
|
||||
export interface UseComparePanZoomReturn {
|
||||
layout: 'side-by-side' | 'stacked';
|
||||
setLayout: (layout: 'side-by-side' | 'stacked') => void;
|
||||
toggleLayout: () => void;
|
||||
baseScrollRef: RefObject<HTMLDivElement | null>;
|
||||
comparisonScrollRef: RefObject<HTMLDivElement | null>;
|
||||
isScrollLinked: boolean;
|
||||
setIsScrollLinked: (value: boolean) => void;
|
||||
captureScrollLinkDelta: () => void;
|
||||
clearScrollLinkDelta: () => void;
|
||||
isPanMode: boolean;
|
||||
setIsPanMode: (value: boolean) => void;
|
||||
baseZoom: number;
|
||||
setBaseZoom: (value: number) => void;
|
||||
comparisonZoom: number;
|
||||
setComparisonZoom: (value: number) => void;
|
||||
basePan: PanState;
|
||||
comparisonPan: PanState;
|
||||
centerPanForZoom: (pane: Pane, zoom: number) => void;
|
||||
clampPanForZoom: (pane: Pane, zoom: number) => void;
|
||||
handleScrollSync: (source: HTMLDivElement | null, target: HTMLDivElement | null) => void;
|
||||
beginPan: (pane: Pane, event: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
continuePan: (event: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
endPan: () => void;
|
||||
handleWheelZoom: (pane: Pane, event: ReactWheelEvent<HTMLDivElement>) => void;
|
||||
handleWheelOverscroll: (pane: Pane, event: ReactWheelEvent<HTMLDivElement>) => void;
|
||||
onTouchStart: (pane: Pane, event: ReactTouchEvent<HTMLDivElement>) => void;
|
||||
onTouchMove: (event: ReactTouchEvent<HTMLDivElement>) => void;
|
||||
onTouchEnd: () => void;
|
||||
zoomLimits: { min: number; max: number; step: number };
|
||||
}
|
||||
// (Interfaces moved to @app/types/compare)
|
||||
|
||||
export const useComparePanZoom = ({
|
||||
basePages,
|
||||
|
||||
@ -4,7 +4,7 @@ import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { alert } from '@app/components/toast';
|
||||
import type { ToastLocation } from '@app/components/toast/types';
|
||||
import type { RightRailButtonWithAction } from '@app/hooks/useRightRailButtons';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
|
||||
type Pane = 'base' | 'comparison';
|
||||
|
||||
@ -44,7 +44,7 @@ export const useCompareRightRailButtons = ({
|
||||
zoomLimits,
|
||||
}: UseCompareRightRailButtonsOptions): RightRailButtonWithAction[] => {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useMediaQuery('(max-width: 768px)') ?? false;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return useMemo<RightRailButtonWithAction[]>(() => [
|
||||
{
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import type { TokenBoundingBox } from '../../../types/compare';
|
||||
|
||||
export interface PagePreview {
|
||||
pageNumber: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface WordHighlightEntry {
|
||||
rect: TokenBoundingBox;
|
||||
metaIndex: number;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
@ -63,7 +63,7 @@ const FileEditorThumbnail = ({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showHoverMenu, setShowHoverMenu] = useState(false);
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
const isMobile = useIsMobile();
|
||||
const [showCloseModal, setShowCloseModal] = useState(false);
|
||||
|
||||
// Resolve the actual File object for pin/unpin operations
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Text, Checkbox } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import RotateLeftIcon from '@mui/icons-material/RotateLeft';
|
||||
@ -68,7 +68,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
const isMobile = useIsMobile();
|
||||
const dragElementRef = useRef<HTMLDivElement>(null);
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { Modal, Text, ActionIcon } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import Overview from '@app/components/shared/config/configSections/Overview';
|
||||
import { createConfigNavSections } from '@app/components/shared/config/configNavSections';
|
||||
@ -8,6 +7,7 @@ import { NavKey } from '@app/components/shared/config/types';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import '@app/components/shared/AppConfigModal.css';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
|
||||
interface AppConfigModalProps {
|
||||
opened: boolean;
|
||||
@ -16,7 +16,7 @@ interface AppConfigModalProps {
|
||||
|
||||
const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
const [active, setActive] = useState<NavKey>('overview');
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
const isMobile = useIsMobile();
|
||||
const { config } = useAppConfig();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -10,7 +10,7 @@ import { useSidebarContext } from "@app/contexts/SidebarContext";
|
||||
import rainbowStyles from '@app/styles/rainbow.module.css';
|
||||
import { ActionIcon, ScrollArea } from '@mantine/core';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import DoubleArrowIcon from '@mui/icons-material/DoubleArrow';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FullscreenToolSurface from '@app/components/tools/FullscreenToolSurface';
|
||||
@ -26,7 +26,7 @@ export default function ToolPanel() {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef, quickAccessRef, rightRailRef } = sidebarRefs;
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
leftPanelView,
|
||||
|
||||
@ -12,7 +12,6 @@ export interface FilesStepConfig {
|
||||
minFiles?: number;
|
||||
onCollapsedClick?: () => void;
|
||||
isVisible?: boolean;
|
||||
autoExpandNextOnFiles?: boolean;
|
||||
}
|
||||
|
||||
export interface MiddleStepConfig {
|
||||
@ -68,7 +67,6 @@ export interface ToolFlowConfig {
|
||||
*/
|
||||
export function createToolFlow(config: ToolFlowConfig) {
|
||||
const steps = createToolSteps();
|
||||
const hasFiles = (config.files.selectedFiles?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<Stack gap="sm" p="sm" >
|
||||
@ -85,11 +83,9 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
})}
|
||||
|
||||
{/* Middle Steps */}
|
||||
{config.steps.map((stepConfig, index) =>
|
||||
{config.steps.map((stepConfig) =>
|
||||
steps.create(stepConfig.title, {
|
||||
isVisible: stepConfig.isVisible,
|
||||
// If enabled, auto-expand the first middle step when files exist
|
||||
isCollapsed: index === 0 && config.files.autoExpandNextOnFiles ? !hasFiles : stepConfig.isCollapsed,
|
||||
onCollapsedClick: stepConfig.onCollapsedClick,
|
||||
tooltip: stepConfig.tooltip
|
||||
}, stepConfig.content)
|
||||
|
||||
@ -6,7 +6,7 @@ import { fileStorage } from '@app/services/fileStorage';
|
||||
|
||||
interface FilesModalContextType {
|
||||
isFilesModalOpen: boolean;
|
||||
openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void; maxNumberOfFiles?: number }) => void;
|
||||
openFilesModal: (options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => void;
|
||||
closeFilesModal: () => void;
|
||||
onFileUpload: (files: File[]) => void;
|
||||
onRecentFileSelect: (stirlingFileStubs: StirlingFileStub[]) => void;
|
||||
@ -24,7 +24,7 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
const [insertAfterPage, setInsertAfterPage] = useState<number | undefined>();
|
||||
const [customHandler, setCustomHandler] = useState<((files: File[], insertAfterPage?: number) => void) | undefined>();
|
||||
|
||||
const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void; maxNumberOfFiles?: number }) => {
|
||||
const openFilesModal = useCallback((options?: { insertAfterPage?: number; customHandler?: (files: File[], insertAfterPage?: number) => void }) => {
|
||||
setInsertAfterPage(options?.insertAfterPage);
|
||||
setCustomHandler(() => options?.customHandler);
|
||||
setIsFilesModalOpen(true);
|
||||
|
||||
@ -39,9 +39,13 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
const { selectors } = useFileContext();
|
||||
const workerRef = useRef<Worker | null>(null);
|
||||
const previousUrl = useRef<string | null>(null);
|
||||
const activeRunIdRef = useRef(0);
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
type OperationStatus = 'idle' | 'extracting' | 'processing' | 'complete' | 'cancelled' | 'error';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [status, setStatus] = useState('');
|
||||
const [statusState, setStatusState] = useState<OperationStatus>('idle');
|
||||
const [statusDetailMs, setStatusDetailMs] = useState<number | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
@ -74,7 +78,8 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
cleanupDownloadUrl();
|
||||
setDownloadUrl(null);
|
||||
setDownloadFilename('');
|
||||
setStatus('');
|
||||
setStatusState('idle');
|
||||
setStatusDetailMs(null);
|
||||
setErrorMessage(null);
|
||||
}, [cleanupDownloadUrl]);
|
||||
|
||||
@ -95,6 +100,11 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
const collectedTokens: CompareDiffToken[] = [];
|
||||
|
||||
const handleMessage = (event: MessageEvent<CompareWorkerResponse>) => {
|
||||
if (cancelledRef.current) {
|
||||
cleanup();
|
||||
reject(Object.assign(new Error('Operation cancelled'), { code: 'CANCELLED' as const }));
|
||||
return;
|
||||
}
|
||||
const message = event.data;
|
||||
if (!message) {
|
||||
return;
|
||||
@ -141,7 +151,11 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
cleanup();
|
||||
reject(event.error ?? new Error(event.message));
|
||||
if (cancelledRef.current) {
|
||||
reject(Object.assign(new Error('Operation cancelled'), { code: 'CANCELLED' as const }));
|
||||
} else {
|
||||
reject(event.error ?? new Error(event.message));
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
@ -175,6 +189,9 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
|
||||
const executeOperation = useCallback(
|
||||
async (params: CompareParameters, selectedFiles: StirlingFile[]) => {
|
||||
// start new run
|
||||
const runId = ++activeRunIdRef.current;
|
||||
cancelledRef.current = false;
|
||||
if (!params.baseFileId || !params.comparisonFileId) {
|
||||
setErrorMessage(t('compare.error.selectRequired', 'Select a base and comparison document.'));
|
||||
return;
|
||||
@ -191,7 +208,8 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setStatus(t('compare.status.extracting', 'Extracting text...'));
|
||||
setStatusState('extracting');
|
||||
setStatusDetailMs(null);
|
||||
setErrorMessage(null);
|
||||
setWarnings([]);
|
||||
setResult(null);
|
||||
@ -220,11 +238,13 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
extractContentFromPdf(comparisonFile),
|
||||
]);
|
||||
|
||||
if (cancelledRef.current || activeRunIdRef.current !== runId) return;
|
||||
|
||||
if (baseContent.tokens.length === 0 || comparisonContent.tokens.length === 0) {
|
||||
throw Object.assign(new Error(warningMessages.emptyTextMessage), { code: 'EMPTY_TEXT' });
|
||||
}
|
||||
|
||||
setStatus(t('compare.status.processing', 'Analyzing differences...'));
|
||||
setStatusState('processing');
|
||||
|
||||
// Filter out paragraph sentinels before diffing to avoid large false-positive runs
|
||||
const baseFiltered = filterTokensForDiff(baseContent.tokens, baseContent.metadata);
|
||||
@ -257,6 +277,8 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
warningMessages
|
||||
);
|
||||
|
||||
if (cancelledRef.current || activeRunIdRef.current !== runId) return;
|
||||
|
||||
const baseHasHighlight = new Array<boolean>(baseFiltered.tokens.length).fill(false);
|
||||
const comparisonHasHighlight = new Array<boolean>(comparisonFiltered.tokens.length).fill(false);
|
||||
|
||||
@ -363,7 +385,7 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
setDownloadUrl(blobUrl);
|
||||
setDownloadFilename(summaryFile.name);
|
||||
|
||||
setStatus(t('compare.status.complete', 'Comparison ready'));
|
||||
setStatusState('complete');
|
||||
} catch (error: unknown) {
|
||||
console.error('[compare] operation failed', error);
|
||||
const errorCode = getWorkerErrorCode(error);
|
||||
@ -381,7 +403,7 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
}
|
||||
} finally {
|
||||
const duration = performance.now() - operationStart;
|
||||
setStatus((prev) => (prev ? `${prev} (${Math.round(duration)} ms)` : prev));
|
||||
setStatusDetailMs(Math.round(duration));
|
||||
setIsLoading(false);
|
||||
if (longRunningToastIdRef.current) {
|
||||
dismissToast(longRunningToastIdRef.current);
|
||||
@ -393,11 +415,22 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
);
|
||||
|
||||
const cancelOperation = useCallback(() => {
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
setStatus(t('operationCancelled', 'Operation cancelled'));
|
||||
if (!isLoading) return;
|
||||
cancelledRef.current = true;
|
||||
setIsLoading(false);
|
||||
setStatusState('cancelled');
|
||||
if (workerRef.current) {
|
||||
try {
|
||||
workerRef.current.terminate();
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
workerRef.current = null;
|
||||
}
|
||||
}, [isLoading, t]);
|
||||
if (longRunningToastIdRef.current) {
|
||||
dismissToast(longRunningToastIdRef.current);
|
||||
longRunningToastIdRef.current = null;
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
const undoOperation = useCallback(async () => {
|
||||
resetResults();
|
||||
@ -417,6 +450,18 @@ export const useCompareOperation = (): CompareOperationHook => {
|
||||
};
|
||||
}, [cleanupDownloadUrl]);
|
||||
|
||||
const status = useMemo(() => {
|
||||
const label =
|
||||
statusState === 'idle' ? ''
|
||||
: statusState === 'extracting' ? t('compare.status.extracting', 'Extracting text...')
|
||||
: statusState === 'processing' ? t('compare.status.processing', 'Analyzing differences...')
|
||||
: statusState === 'complete' ? t('compare.status.complete', 'Comparison ready')
|
||||
: statusState === 'cancelled' ? t('operationCancelled', 'Operation cancelled')
|
||||
: '';
|
||||
if (label && statusDetailMs != null) return `${label} (${statusDetailMs} ms)`;
|
||||
return label;
|
||||
}, [statusState, statusDetailMs, t]);
|
||||
|
||||
return useMemo<CompareOperationHook>(
|
||||
() => ({
|
||||
files,
|
||||
|
||||
9
frontend/src/core/hooks/useIsMobile.ts
Normal file
9
frontend/src/core/hooks/useIsMobile.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
/**
|
||||
* Custom hook to detect mobile viewport
|
||||
* Uses a consistent breakpoint across the application
|
||||
*/
|
||||
export const useIsMobile = (): boolean => {
|
||||
return useMediaQuery('(max-width: 1024px)') ?? false;
|
||||
};
|
||||
@ -1,14 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { pdfWorkerManager } from '@app/services/pdfWorkerManager';
|
||||
|
||||
// Define PagePreview type locally since it's specific to this hook
|
||||
export interface PagePreview {
|
||||
pageNumber: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
url: string;
|
||||
}
|
||||
import { PagePreview } from '@app/types/compare';
|
||||
|
||||
const DISPLAY_SCALE = 1;
|
||||
const BATCH_SIZE = 10; // Render 10 pages at a time
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useIsMobile } from './useIsMobile';
|
||||
|
||||
export function useShouldShowWelcomeModal(): boolean {
|
||||
const { preferences } = usePreferences();
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return !preferences.hasCompletedOnboarding
|
||||
&& preferences.toolPanelModePromptSeen
|
||||
|
||||
@ -6,7 +6,7 @@ import { useSidebarContext } from "@app/contexts/SidebarContext";
|
||||
import { useDocumentMeta } from "@app/hooks/useDocumentMeta";
|
||||
import { BASE_PATH } from "@app/constants/app";
|
||||
import { useBaseUrl } from "@app/hooks/useBaseUrl";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||
|
||||
@ -46,7 +46,7 @@ export default function HomePage() {
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { config } = useAppConfig();
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
const isMobile = useIsMobile();
|
||||
const sliderRef = useRef<HTMLDivElement | null>(null);
|
||||
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
|
||||
const isProgrammaticScroll = useRef(false);
|
||||
|
||||
@ -285,6 +285,13 @@
|
||||
--pdf-light-report-container-bg: 249 250 251;
|
||||
--pdf-light-simulated-page-bg: 255 255 255;
|
||||
--pdf-light-simulated-page-text: 15 23 42;
|
||||
|
||||
/* Compare tool specific colors - only for colors that don't have existing theme pairs */
|
||||
--compare-upload-dropzone-bg: rgba(241, 245, 249, 0.45);
|
||||
--compare-upload-dropzone-border: rgba(148, 163, 184, 0.6);
|
||||
--compare-upload-icon-bg: rgba(148, 163, 184, 0.2);
|
||||
--compare-upload-icon-color: rgba(17, 24, 39, 0.75);
|
||||
--compare-upload-divider: rgba(148, 163, 184, 0.5);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] {
|
||||
@ -502,6 +509,13 @@
|
||||
--modal-nav-item-active-bg: rgba(10, 139, 255, 0.15);
|
||||
--modal-content-bg: #2A2F36;
|
||||
--modal-header-border: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* Compare tool specific colors (dark mode) - only for colors that don't have existing theme pairs */
|
||||
--compare-upload-dropzone-bg: rgba(31, 35, 41, 0.45);
|
||||
--compare-upload-dropzone-border: rgba(75, 85, 99, 0.6);
|
||||
--compare-upload-icon-bg: rgba(75, 85, 99, 0.2);
|
||||
--compare-upload-icon-color: rgba(243, 244, 246, 0.75);
|
||||
--compare-upload-divider: rgba(75, 85, 99, 0.5);
|
||||
}
|
||||
|
||||
/* Dropzone drop state styling */
|
||||
|
||||
@ -22,7 +22,7 @@ import type { FileId } from '@app/types/file';
|
||||
import type { StirlingFile } from '@app/types/fileContext';
|
||||
import DocumentThumbnail from '@app/components/shared/filePreview/DocumentThumbnail';
|
||||
import './compareTool.css';
|
||||
import type { CompareWorkbenchData } from '@app/types/compareWorkbench';
|
||||
import type { CompareWorkbenchData } from '@app/types/compare';
|
||||
import FitText from '@app/components/shared/FitText';
|
||||
|
||||
const CUSTOM_VIEW_ID = 'compareWorkbenchView';
|
||||
@ -146,10 +146,6 @@ const Compare = (props: BaseToolProps) => {
|
||||
params,
|
||||
]);
|
||||
|
||||
// const handleOpenWorkbench = useCallback(() => {
|
||||
// navigationActions.setWorkbench(CUSTOM_WORKBENCH_ID);
|
||||
// }, [navigationActions]);
|
||||
|
||||
const handleExecuteCompare = useCallback(async () => {
|
||||
const selected: StirlingFile[] = [];
|
||||
const baseSel = params.baseFileId ? selectors.getFile(params.baseFileId) : null;
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import type { StirlingFileStub } from '@app/types/fileContext';
|
||||
import type { FileId } from '@app/types/file';
|
||||
import type { StirlingFile } from '@app/types/fileContext';
|
||||
|
||||
export type CompareDiffTokenType = 'unchanged' | 'removed' | 'added';
|
||||
|
||||
export interface CompareDiffToken {
|
||||
@ -133,3 +137,187 @@ export type CompareWorkerResponse =
|
||||
message: string;
|
||||
code?: 'EMPTY_TEXT' | 'TOO_LARGE';
|
||||
};
|
||||
|
||||
export interface CompareDocumentPaneProps {
|
||||
pane: 'base' | 'comparison';
|
||||
layout: 'side-by-side' | 'stacked';
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
peerScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleScrollSync: (source: HTMLDivElement | null, target: HTMLDivElement | null) => void;
|
||||
beginPan: (pane: 'base' | 'comparison', event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
continuePan: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
endPan: () => void;
|
||||
handleWheelZoom: (pane: 'base' | 'comparison', event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
handleWheelOverscroll: (pane: 'base' | 'comparison', event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
onTouchStart: (pane: 'base' | 'comparison', event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchMove: (event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchEnd: (event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
isPanMode: boolean;
|
||||
zoom: number;
|
||||
pan?: { x: number; y: number };
|
||||
title: string;
|
||||
dropdownPlaceholder?: string;
|
||||
changes: Array<{ value: string; label: string; pageNumber?: number }>;
|
||||
onNavigateChange: (id: string, pageNumber?: number) => void;
|
||||
isLoading: boolean;
|
||||
processingMessage: string;
|
||||
emptyMessage: string;
|
||||
pages: PagePreview[];
|
||||
pairedPages: PagePreview[];
|
||||
getRowHeightPx: (pageNumber: number) => number;
|
||||
wordHighlightMap: Map<number, WordHighlightEntry[]>;
|
||||
metaIndexToGroupId: Map<number, string>;
|
||||
documentLabel: string;
|
||||
pageLabel: string;
|
||||
altLabel: string;
|
||||
}
|
||||
|
||||
// Import types that are referenced in CompareDocumentPaneProps
|
||||
export interface PagePreview {
|
||||
pageNumber: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface WordHighlightEntry {
|
||||
rect: TokenBoundingBox;
|
||||
metaIndex: number;
|
||||
}
|
||||
|
||||
export interface NavigationDropdownProps {
|
||||
changes: Array<{ value: string; label: string; pageNumber?: number }>;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
onNavigate: (value: string, pageNumber?: number) => void;
|
||||
// Optional: pages that currently have previews rendered (1-based page numbers)
|
||||
renderedPageNumbers?: Set<number>;
|
||||
}
|
||||
|
||||
// Pan/Zoom and Compare Workbench shared types (moved out of hooks for reuse)
|
||||
import type React from 'react';
|
||||
|
||||
export type ComparePane = 'base' | 'comparison';
|
||||
|
||||
export interface PanState {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ScrollLinkDelta {
|
||||
vertical: number;
|
||||
horizontal: number;
|
||||
}
|
||||
|
||||
export interface ScrollLinkAnchors {
|
||||
deltaPixelsBaseToComp: number;
|
||||
deltaPixelsCompToBase: number;
|
||||
}
|
||||
|
||||
export interface PanDragState {
|
||||
active: boolean;
|
||||
source: ComparePane | null;
|
||||
startX: number;
|
||||
startY: number;
|
||||
startPanX: number;
|
||||
startPanY: number;
|
||||
targetStartPanX: number;
|
||||
targetStartPanY: number;
|
||||
}
|
||||
|
||||
export interface PinchState {
|
||||
active: boolean;
|
||||
pane: ComparePane | null;
|
||||
startDistance: number;
|
||||
startZoom: number;
|
||||
}
|
||||
|
||||
export interface UseComparePanZoomOptions {
|
||||
prefersStacked: boolean;
|
||||
basePages: PagePreview[];
|
||||
comparisonPages: PagePreview[];
|
||||
}
|
||||
|
||||
export interface UseComparePanZoomReturn {
|
||||
layout: 'side-by-side' | 'stacked';
|
||||
setLayout: (layout: 'side-by-side' | 'stacked') => void;
|
||||
toggleLayout: () => void;
|
||||
baseScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
comparisonScrollRef: React.RefObject<HTMLDivElement | null>;
|
||||
isScrollLinked: boolean;
|
||||
setIsScrollLinked: (value: boolean) => void;
|
||||
captureScrollLinkDelta: () => void;
|
||||
clearScrollLinkDelta: () => void;
|
||||
isPanMode: boolean;
|
||||
setIsPanMode: (value: boolean) => void;
|
||||
baseZoom: number;
|
||||
setBaseZoom: (value: number) => void;
|
||||
comparisonZoom: number;
|
||||
setComparisonZoom: (value: number) => void;
|
||||
basePan: PanState;
|
||||
comparisonPan: PanState;
|
||||
centerPanForZoom: (pane: ComparePane, zoom: number) => void;
|
||||
clampPanForZoom: (pane: ComparePane, zoom: number) => void;
|
||||
handleScrollSync: (source: HTMLDivElement | null, target: HTMLDivElement | null) => void;
|
||||
beginPan: (pane: ComparePane, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
continuePan: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
endPan: () => void;
|
||||
handleWheelZoom: (pane: ComparePane, event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
handleWheelOverscroll: (pane: ComparePane, event: React.WheelEvent<HTMLDivElement>) => void;
|
||||
onTouchStart: (pane: ComparePane, event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchMove: (event: React.TouchEvent<HTMLDivElement>) => void;
|
||||
onTouchEnd: () => void;
|
||||
zoomLimits: { min: number; max: number; step: number };
|
||||
}
|
||||
|
||||
export interface PagePreview {
|
||||
pageNumber: number;
|
||||
width: number;
|
||||
height: number;
|
||||
rotation: number;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export interface WordHighlightEntry {
|
||||
rect: TokenBoundingBox;
|
||||
metaIndex: number;
|
||||
}
|
||||
|
||||
export interface UploadColumnProps {
|
||||
role: 'base' | 'comparison';
|
||||
file: File | null;
|
||||
stub: StirlingFileStub | null;
|
||||
title: string;
|
||||
description: string;
|
||||
accentClass: string;
|
||||
disabled: boolean;
|
||||
onDrop: (files: File[]) => void;
|
||||
onSelectExisting: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export interface CompareUploadSectionProps {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
disabled: boolean;
|
||||
base: UploadColumnProps;
|
||||
comparison: UploadColumnProps;
|
||||
}
|
||||
|
||||
export interface CompareWorkbenchData {
|
||||
result: CompareResultData | null;
|
||||
baseFileId: FileId | null;
|
||||
comparisonFileId: FileId | null;
|
||||
onSelectBase?: (fileId: FileId | null) => void;
|
||||
onSelectComparison?: (fileId: FileId | null) => void;
|
||||
isLoading?: boolean;
|
||||
baseLocalFile?: StirlingFile | null;
|
||||
comparisonLocalFile?: StirlingFile | null;
|
||||
}
|
||||
|
||||
export interface CompareChangeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
pageNumber: number;
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import type { CompareResultData } from '@app/types/compare';
|
||||
import type { FileId } from '@app/types/file';
|
||||
import type { StirlingFile } from '@app/types/fileContext';
|
||||
|
||||
export interface CompareWorkbenchData {
|
||||
result: CompareResultData | null;
|
||||
baseFileId: FileId | null;
|
||||
comparisonFileId: FileId | null;
|
||||
onSelectBase?: (fileId: FileId | null) => void;
|
||||
onSelectComparison?: (fileId: FileId | null) => void;
|
||||
isLoading?: boolean;
|
||||
baseLocalFile?: StirlingFile | null;
|
||||
comparisonLocalFile?: StirlingFile | null;
|
||||
}
|
||||
|
||||
export interface CompareChangeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
pageNumber: number;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { usePreferences } from '@app/contexts/PreferencesContext';
|
||||
import { useAuth } from '@app/auth/UseSession';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
|
||||
export function useShouldShowWelcomeModal(): boolean {
|
||||
const { preferences } = usePreferences();
|
||||
const { session, loading } = useAuth();
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Only show welcome modal if user is authenticated (session exists)
|
||||
// This prevents the modal from showing on login screens when security is enabled
|
||||
|
||||
Loading…
Reference in New Issue
Block a user