cleanups, delete unused files, revert unnecessary git changes, etc etc

This commit is contained in:
EthanHealy01 2025-10-29 01:43:33 +00:00
parent 48be000a1b
commit 28173fe9a4
31 changed files with 411 additions and 428 deletions

View File

@ -2173,6 +2173,9 @@
"comparisonHeading": "Comparison document",
"pageLabel": "Page"
},
"rendering": {
"rendering": "rendering"
},
"dropdown": {
"deletions": "Deletions ({{count}})",
"additions": "Additions ({{count}})",

View File

@ -1336,6 +1336,9 @@
"comparisonHeading": "Comparison document",
"pageLabel": "Page"
},
"rendering": {
"rendering": "rendering"
},
"status": {
"extracting": "Extracting text...",
"processing": "Analyzing differences...",

View File

@ -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

View File

@ -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 dont 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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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();

View File

@ -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})`;
};

View File

@ -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 {

View File

@ -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;
}

View File

@ -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>;

View File

@ -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,

View File

@ -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[]>(() => [
{

View File

@ -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;
}

View File

@ -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

View File

@ -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();

View File

@ -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(() => {

View File

@ -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,

View File

@ -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)

View File

@ -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);

View File

@ -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,

View 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;
};

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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 */

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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